diff --git a/src/FileHandler.php b/src/FileHandler.php index abd5c07..773604f 100644 --- a/src/FileHandler.php +++ b/src/FileHandler.php @@ -13,6 +13,7 @@ class FileHandler private array $files = []; + /** * @throws FileHandlerException */ @@ -160,31 +161,44 @@ public function delete(string $filename): void /** * @throws FileHandlerException */ - private function getRows(): Generator + private function getRows(string|null $filename = null): Generator { - if (count($this->files) > 1) { - throw new FileHandlerException("multiple files not allowed"); - } - - $file = $this->files[0]; - $headers = fgetcsv($file); - - $this->isValidCsvFileFormat($headers); + $file = $this->ensureSingleFileProcessing($filename); + $headers = $this->extractHeader($file); $isEmptyFile = true; - while (($row = fgetcsv($file)) !== false) { - $isEmptyFile = false; - $this->isValidCsvFileFormat($row); - $item = array_combine($headers, $row); - yield $item; + try { + while (($row = fgetcsv($file)) !== false) { + $isEmptyFile = false; + $this->isValidCsvFileFormat($row); + $item = array_combine($headers, $row); + + yield $item; + } + } finally { + fclose($file); } - fclose($file); + if ($isEmptyFile) { throw new FileHandlerException('invalid file format'); } } + private function ensureSingleFileProcessing(string|null $filename): mixed + { + if (count($this->files) < 1) { + if (!$filename || !file_exists($filename)) { + throw new FileHandlerException("no files to process"); + } + $this->open($filename); + } + if (count($this->files) > 1) { + throw new FileHandlerException("multiple files not allowed"); + } + return $this->files[0]; + } + /** * @throws FileHandlerException */ @@ -198,6 +212,100 @@ private function search(string $keyword, string $column, string|null $format): b return false; } + public function findAndReplaceInCsv( + string $filename, + string $keyword, + string $replace, + string|null $column = null + ): bool { + $headers = $this->extractHeader($filename); + + + if (!$headers) { + throw new FileHandlerException('failed to extract header'); + } + + $tempFilePath = $this->createTempFileWithHeaders($headers); + + try { + $count = 0; + foreach ($this->getRows($filename) as $row) { + if (!$column) { + $count += $this->replaceKeywordInRow($row, $keyword, $replace); + } else { + $count += $this->replaceKeywordInColumn($row, $column, $keyword, $replace); + } + + $this->writeRowToTempFile($tempFilePath, $row); + } + + if ($count < 1) { + return false; + } + + $this->renameTempFile($tempFilePath, $filename); + } finally { + $this->cleanupTempFile($tempFilePath); + } + + return true; + } + + private function replaceKeywordInRow(array &$row, string $keyword, string $replace): int + { + $count = 0; + $replacement = array_search($keyword, $row); + + if ($replacement !== false) { + $row[$replacement] = $replace; + $count++; + } + + return $count; + } + + private function replaceKeywordInColumn(array &$row, string $column, string $keyword, string $replace): int + { + $count = 0; + + if ($keyword === $row[$column]) { + $row[$column] = $replace; + $count++; + } + + return $count; + } + + private function writeRowToTempFile(string $tempFilePath, array $row): void + { + $tempFileHandle = fopen($tempFilePath, 'a'); + fputs($tempFileHandle, implode(',', $row) . PHP_EOL); + fclose($tempFileHandle); + } + + private function renameTempFile(string $tempFilePath, string $filename): void + { + if (!rename($tempFilePath, $filename)) { + throw new FileHandlerException('Failed to rename temp file'); + } + } + + private function cleanupTempFile(string $tempFilePath): void + { + unlink($tempFilePath); + } + + private function createTempFileWithHeaders(array $headers): string + { + $tempFilePath = tempnam(sys_get_temp_dir(), 'tempfile_'); + $tempFileHandle = fopen($tempFilePath, 'w'); + fputs($tempFileHandle, implode(',', $headers) . PHP_EOL); + fclose($tempFileHandle); + + return $tempFilePath; + } + + /** * @throws FileHandlerException */ @@ -207,4 +315,28 @@ private function isValidCsvFileFormat(array|false $row): void throw new FileHandlerException('invalid file format'); } } + + private function extractHeader(mixed $file): array|false + { + if (is_resource($file)) { + $headers = fgetcsv($file); + } + if (is_string($file)) { + if (!file_exists($file)) { + return false; + } + try { + $file = fopen($file, 'r'); + $headers = fgetcsv($file); + } finally { + fclose($file); + } + } + + if ($this->isValidCsvFileFormat($headers) !== false) { + return $headers; + } + + return false; + } } diff --git a/tests/unit/FileHandlerTest.php b/tests/unit/FileHandlerTest.php index 371e064..403b7a8 100644 --- a/tests/unit/FileHandlerTest.php +++ b/tests/unit/FileHandlerTest.php @@ -80,6 +80,7 @@ public function shouldThrowExceptionIfFileIsNotWritable() $this->expectException(FileHandlerException::class); $this->expectExceptionMessage('Error writing to file'); $this->fileHandler->write(data: "hello world"); + $this->fileHandler->close(); } @@ -107,6 +108,7 @@ public function multipleFileCanBeWrittenSimultaneously() $this->assertEquals("hello world", file_get_contents(filename: 'file')); $this->assertEquals("hello world", file_get_contents(filename: 'file1')); + $this->fileHandler->close(); } @@ -252,6 +254,37 @@ public function throwErrorIfFileFormatIsInvalid(string $file) } } + #[Test] + public function findAndReplaceInCsvMethodShouldReplaceTextUsingColumnOption() + { + $fileHandler = new FileHandler(); + + $hasReplaced = $fileHandler->findAndReplaceInCsv("movie.csv", "Twilight", "Inception", "Film"); + + $this->assertTrue($hasReplaced); + + + $data = $this->fileHandler->open("movie.csv")->searchInCsvFile("Inception", "Film", FileHandler::ARRAY_FORMAT); + + $this->assertEquals($data["Film"], "Inception"); + } + + #[Test] + public function findAndReplaceInCsvMethodShouldReplaceTextWithoutColumnOption() + { + $fileHandler = new FileHandler(); + + + $hasReplaced = $fileHandler->findAndReplaceInCsv("movie.csv", "Inception", "Twilight"); + + $this->assertTrue($hasReplaced); + + + $data = $this->fileHandler->open("movie.csv")->searchInCsvFile("Twilight", "Film", FileHandler::ARRAY_FORMAT); + + $this->assertEquals($data["Film"], "Twilight"); + } + // Data Providers public static function provideStudioNames(): iterable