├── .env.dist ├── .gitignore ├── README.md ├── bin ├── file-diff ├── file-encrypt ├── view-csv └── view-json ├── composer.json ├── composer.lock ├── images └── csv-view.jpeg ├── phpcs.xml ├── phpstan.neon ├── phpunit.xml ├── src ├── CsvFileHandler.php ├── DependencyInjection │ └── ServiceContainer.php ├── Exception │ ├── FileEncryptorException.php │ ├── FileHandlerException.php │ ├── HashException.php │ └── StreamException.php ├── FileEncryptor.php ├── FileHandler.php ├── FileHashChecker.php ├── JsonFileHandler.php ├── StreamHandler.php ├── TempFileHandler.php ├── Utilities │ └── RowColumnHelper.php ├── Validator │ └── FileValidatorTrait.php └── config │ └── services.yaml └── tests ├── Base └── BaseTest.php ├── Integration ├── FileDiffCommandTest.php ├── FileEncryptCommandTest.php ├── StreamHandlerTest.php ├── ViewCsvCommandTest.php └── ViewJsonCommandTest.php ├── bootstrap.php └── unit ├── CsvFileHandlerTest.php ├── FileEncryptorTest.php ├── FileHandlerTest.php ├── FileHashCheckerTest.php ├── FileValidatorTest.php ├── JsonFileHandlerTest.php └── TempFileHandlerTest.php /.env.dist: -------------------------------------------------------------------------------- 1 | SECRET_KEY="rahul_chavan" 2 | FILE_NAME='/home/rahul/Documents/hashlist' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .phpunit.cache 3 | vendor 4 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP File Helper 2 | 3 | ![License](https://img.shields.io/badge/License-MIT-green.svg) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/c6450a9c0f99488e93b34911f1adfb2e)](https://app.codacy.com/gh/rcsofttech85/php-file-helper/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 5 | [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/c6450a9c0f99488e93b34911f1adfb2e)](https://app.codacy.com/gh/rcsofttech85/php-file-helper/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) 6 | 7 | A simple PHP file helper for various file operations. 8 | 9 | --- 10 | 11 | ## Table of Contents 12 | 13 | * [About](#about) 14 | * [Installation](#installation) 15 | * [Usage](#usage) 16 | * [Search by Keyword](#search-by-keyword) 17 | * [Search by keyword and return array](#search-and-return-array) 18 | * [Write Multiple Files](#write-multiple-files-simultaneously) 19 | * [Converting File to Array](#converting-file-to-an-array) 20 | * [Find and Replace in CSV](#find-and-replace-in-csv-file) 21 | * [Converting File to JSON](#converting-file-to-json-format) 22 | * [Encrypt and Decrypt Files](#encrypt-and-decrypt-files) 23 | * [Stream and Save Content from URL](#stream-and-save-content-from-url-to-file) 24 | * [File Compression and Decompression](#file-compression-and-decompression) 25 | * [File Difference](#file-difference) 26 | * [File Integrity check](#file-integrity-check) 27 | * [View CSV in Terminal (table format)](#view-csv-in-terminal) 28 | * [View JSON in Terminal (table format)](#view-json-in-terminal) 29 | 30 | ## About 31 | 32 | This PHP File Helper is designed to simplify various file-related operations. It offers a range of features to handle 33 | tasks such as searching for keywords in files, converting files to different formats, encrypting and decrypting files, 34 | and more. Whether you're working with CSV, JSON, or plain text files, this library can streamline your file management 35 | processes. 36 | 37 | ## Installation 38 | 39 | You can install this PHP File Helper library via Composer: 40 | 41 | ```bash 42 | composer require rcsofttech85/file-handler 43 | 44 | ``` 45 | 46 | ## Usage 47 | 48 | ## search by keyword 49 | 50 | ``` 51 | $temp = new TempFileHandler(); 52 | $csv = new CsvFileHandler($temp); 53 | 54 | $findByKeyword = $csv->searchInCsvFile("movies.csv","Twilight","Film"); 55 | 56 | 57 | ``` 58 | 59 | ## Search and return array 60 | 61 | ``` 62 | $temp = new TempFileHandler(); 63 | $csv = new CsvFileHandler($temp); 64 | 65 | $findByKeyword = $csv->searchInCsvFile("movies.csv","Twilight","Film",FileHandler::ARRAY_FORMAT); 66 | 67 | // output 68 | 69 | [ 70 | [Film] => Twilight 71 | [Genre] => Romance 72 | [Lead Studio] => Summit 73 | [Audience score %] => 82 74 | [Profitability] => 10.18002703 75 | [Rotten Tomatoes %] => 49 76 | [Worldwide Gross] => $376.66 77 | [Year] => 2008 78 | 79 | 80 | ]; 81 | ``` 82 | 83 | ## Write multiple files simultaneously 84 | 85 | ``` 86 | $fileHandler = new FileHandler(); 87 | 88 | $fileHandler->open('file.txt'); 89 | 90 | $fileHandler->open('php://stdout'); 91 | 92 | $fileHandler->write(data: "hello world"); 93 | 94 | $fileHandler->close(); 95 | 96 | ``` 97 | 98 | ## Converting file to an array 99 | 100 | ``` 101 | 102 | $temp = new TempFileHandler(); 103 | $csv = new CsvFileHandler($temp); 104 | 105 | $findByKeyword = $csv->toArray("movies.csv"); 106 | // output 107 | $data[0] = [ 108 | 'Film' => 'Zack and Miri Make a Porno', 109 | 'Genre' => 'Romance', 110 | 'Lead Studio' => 'The Weinstein Company', 111 | 'Audience score %' => '70', 112 | 'Profitability' => '1.747541667', 113 | 'Rotten Tomatoes %' => '64', 114 | 'Worldwide Gross' => '$41.94 ', 115 | 'Year' => '2008' 116 | 117 | ]; 118 | 119 | ``` 120 | 121 | ## Find and replace in csv file 122 | 123 | ``` 124 | 125 | $temp = new TempFileHandler(); 126 | $csv = new CsvFileHandler($temp); 127 | 128 | $findByKeyword = $csv->findAndReplaceInCsv("movies.csv","Twilight","Inception"); 129 | 130 | ``` 131 | 132 | **Find and replace a specific keyword in a particular column of a CSV file** 133 | 134 | ``` 135 | 136 | $temp = new TempFileHandler(); 137 | $csv = new CsvFileHandler($temp); 138 | 139 | $findByKeyword = $csv->findAndReplaceInCsv("movies.csv","Inception","Twilight",column: "Film"); 140 | 141 | ``` 142 | 143 | ## Converting file to json format 144 | 145 | ``` 146 | 147 | $temp = new TempFileHandler(); 148 | $csv = new CsvFileHandler($temp); 149 | 150 | $findByKeyword = $csv->toJson("movies.csv"); 151 | 152 | //output 153 | [{"Film":"Zack and Miri Make a Porno","Genre":"Romance","Lead Studio":"The Weinstein Company","Audience score %":"70","Profitability":"1.747541667","Rotten Tomatoes %":"64","Worldwide Gross":"$41.94 ","Year":"2008"},{"Film":"Youth in Revolt","Genre":"Comedy","Lead Studio":"The Weinstein Company","Audience score %":"52","Profitability":"1.09","Rotten Tomatoes %":"68","Worldwide Gross":"$19.62 ","Year":"2010"},{"Film":"Twilight","Genre":"Romance","Lead Studio":"Independent","Audience score %":"68","Profitability":"6.383363636","Rotten Tomatoes %":"26","Worldwide Gross":"$702.17 ","Year":"2011"}] 154 | 155 | ``` 156 | 157 | ## Encrypt and decrypt files 158 | 159 | ``` 160 | 161 | $secret = getenv('SECRET_KEY'); 162 | 163 | $fileEncryptor = new FileEncryptor('movie.csv', $secret); 164 | $fileEncryptor->encryptFile(); 165 | $fileEncryptor->decryptFile(); 166 | 167 | ``` 168 | 169 | ## Stream and save content from url to file 170 | 171 | ``` 172 | 173 | $url = "https://gist.github.com/rcsofttech85/629b37d483c4796db7bdcb3704067631#file-gistfile1-txt"; 174 | $stream = new Stream($url, "outputFile.html"); 175 | $stream->startStreaming(); 176 | 177 | ``` 178 | 179 | ## File compression and decompression 180 | 181 | ``` 182 | 183 | $testFile = 'movie.csv'; 184 | $compressedZipFilename = 'compressed.zip'; 185 | 186 | $this->fileHandler->compress($testFile, $compressedZipFilename); 187 | 188 | 189 | 190 | $compressedZipFilename = 'compressed.zip'; 191 | $extractPath = 'extracted_contents'; 192 | 193 | $this->fileHandler->decompress($compressedZipFilename, $extractPath); 194 | 195 | ``` 196 | 197 | ## File Difference 198 | 199 | ``` 200 | vendor/bin/file-diff oldFile newFile 201 | 202 | ``` 203 | 204 | ## File Integrity Check 205 | 206 | ``` 207 | $fileHasher = new FileHashChecker(); 208 | 209 | $fileHasher->hashFile(); 210 | 211 | $fileHasher->verifyHash($hashListFile); 212 | 213 | ``` 214 | 215 | ## View csv in terminal 216 | 217 | ``` 218 | vendor/bin/view-csv movies.csv --hide-column Film --limit 5 219 | 220 | ``` 221 | 222 | ## View json in terminal 223 | 224 | ``` 225 | vendor/bin/view-json movies.json --hide-column Film --limit 5 226 | 227 | ``` 228 | 229 | ## Applying filters to a csv file 230 | 231 | ![Csv File](images/csv-view.jpeg) 232 | 233 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /bin/file-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | addArgument('oldFile', InputArgument::REQUIRED, 'old file name') 17 | ->addArgument('newFile', InputArgument::REQUIRED, 'new file name') 18 | ->setCode(function (InputInterface $input, OutputInterface $output): int { 19 | $io = new SymfonyStyle($input, $output); 20 | $oldFile = $input->getArgument('oldFile'); 21 | $newFile = $input->getArgument("newFile"); 22 | 23 | if (!file_exists($oldFile) || !file_exists($newFile)) { 24 | $io->error("file does not exists"); 25 | return Command::FAILURE; 26 | } 27 | 28 | fileDiff($oldFile, $newFile); 29 | return Command::SUCCESS; 30 | })->run(); 31 | 32 | 33 | function fileDiff($oldFilePath, $newFilePath) 34 | { 35 | $oldLines = file($oldFilePath, FILE_IGNORE_NEW_LINES); 36 | $newLines = file($newFilePath, FILE_IGNORE_NEW_LINES); 37 | 38 | 39 | $oldLineCount = count($oldLines); 40 | $newLineCount = count($newLines); 41 | 42 | 43 | $maxLineCount = max($oldLineCount, $newLineCount); 44 | 45 | $changes = []; 46 | for ($i = 0; $i < $maxLineCount; $i++) { 47 | $oldLine = $i < $oldLineCount ? $oldLines[$i] : null; 48 | $newLine = $i < $newLineCount ? $newLines[$i] : null; 49 | 50 | if ($oldLine === $newLine) { 51 | continue; 52 | } 53 | 54 | $colorGreen = "\e[32m"; 55 | $colorRed = "\e[31m"; 56 | $colorReset = "\e[0m"; 57 | 58 | 59 | $oldLineNumber = $i + 1; 60 | $newLineNumber = $i + 1; 61 | 62 | 63 | $changes[] = ($oldLine === null ? "$colorGreen+ $newFilePath (Line $newLineNumber): " : "$colorRed- $oldFilePath (Line $oldLineNumber): ") . ($oldLine ?? $newLine) . "$colorReset"; 64 | 65 | if ($oldLine !== null && $newLine !== null) { 66 | $changes[] = "$colorGreen+ $newFilePath (Line $newLineNumber): " . $newLine . "$colorReset"; 67 | } 68 | } 69 | 70 | 71 | $console = fopen("php://stdout", "w"); 72 | foreach ($changes as $change) { 73 | fwrite($console, $change . PHP_EOL); 74 | } 75 | 76 | fclose($console); 77 | } 78 | -------------------------------------------------------------------------------- /bin/file-encrypt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | addArgument('file', InputArgument::REQUIRED, 'file name to encrypt to decrypt') 25 | ->addOption( 26 | 'mode', 27 | null, 28 | InputOption::VALUE_REQUIRED, 29 | 'encrypt or decrypt mode', 30 | 'encryption' 31 | ) 32 | ->setCode(function (InputInterface $input, OutputInterface $output): int { 33 | $io = new SymfonyStyle($input, $output); 34 | 35 | $file = $input->getArgument('file'); 36 | $mode = $input->getOption('mode'); 37 | 38 | if (!in_array($mode, [ENCRYPT, DECRYPT])) { 39 | $io->error('invalid mode provided'); 40 | return Command::FAILURE; 41 | } 42 | $filValidator = (new class { 43 | use FileValidatorTrait; 44 | }); 45 | 46 | try { 47 | $filValidator->validateFileName($file); 48 | } catch (FileHandlerException) { 49 | $io->error( 50 | "{$file} does not exists" 51 | ); 52 | return Command::FAILURE; 53 | } 54 | 55 | $serviceContainer = new ServiceContainer(); 56 | 57 | /** @var FileEncryptor $encryptor */ 58 | $encryptor = $serviceContainer->getContainerBuilder()->get('file_encryptor'); 59 | try { 60 | $output->writeln("=================================="); 61 | $io->newLine(); 62 | $progressBar = new ProgressBar($output, 1); 63 | $progressBar->start(); 64 | $progressBar->setBarCharacter('█'); 65 | $progressBar->setEmptyBarCharacter("█"); 66 | $progressBar->setProgressCharacter("➤"); 67 | 68 | $progressBar->start(); 69 | 70 | $isEncrypted = false; 71 | 72 | if ($mode === DECRYPT) { 73 | $isEncrypted = $encryptor->decryptFile($file); 74 | } 75 | if ($mode === ENCRYPT) { 76 | $isEncrypted = $encryptor->encryptFile($file); 77 | } 78 | $progressBar->finish(); 79 | $io->newLine(); 80 | $output->writeln("=================================="); 81 | $io->newLine(); 82 | } catch (FileEncryptorException $e) { 83 | $io->newLine(); 84 | $io->error($e->getMessage()); 85 | return Command::FAILURE; 86 | } 87 | 88 | 89 | if (!$isEncrypted) { 90 | $io->error("could not {$mode} file"); 91 | return Command::FAILURE; 92 | } 93 | 94 | $io->success("The file {$mode} was successful"); 95 | return Command::SUCCESS; 96 | })->run(); 97 | -------------------------------------------------------------------------------- /bin/view-csv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | addArgument('csvFile', InputArgument::REQUIRED, 'csv file name') 20 | ->addOption('hide-column', null, InputOption::VALUE_REQUIRED, 'Columns to hide (comma-separated)') 21 | ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'limit number of records') 22 | ->setCode(function (InputInterface $input, OutputInterface $output): int { 23 | $io = new SymfonyStyle($input, $output); 24 | $csvFile = $input->getArgument('csvFile'); 25 | $hiddenColumns = $input->getOption('hide-column'); 26 | $limit = $input->getOption('limit'); 27 | 28 | try { 29 | $filValidator = (new class { 30 | use FileValidatorTrait; 31 | }); 32 | if ($filValidator->isFileRestricted($csvFile, 'STORED_HASH_FILE')) { 33 | throw new FileHandlerException(); 34 | } 35 | $csvFile = $filValidator->validateFileName($csvFile); 36 | } catch (FileHandlerException) { 37 | $io->error( 38 | "{$csvFile} does not exists" 39 | ); 40 | return Command::FAILURE; 41 | } 42 | 43 | if (isset($limit) && !is_numeric($limit)) { 44 | $io->error( 45 | "{$limit} is not numeric" 46 | ); 47 | return Command::FAILURE; 48 | } 49 | 50 | $hiddenColumns = $hiddenColumns ? explode(',', $hiddenColumns) : false; 51 | $limit = $limit ? (int)$limit : false; 52 | 53 | $serviceContainer = new ServiceContainer(); 54 | /** @var CsvFileHandler $csvFileHandler */ 55 | $csvFileHandler = $serviceContainer->getContainerBuilder()->get('csv_file_handler'); 56 | 57 | try { 58 | $data = $csvFileHandler->toArray($csvFile, $hiddenColumns, $limit); 59 | } catch (FileHandlerException) { 60 | $io->error('invalid csv file'); 61 | return Command::FAILURE; 62 | } 63 | 64 | $headers = array_keys(reset($data)); 65 | $io->title($csvFile); 66 | $table = $io->createTable(); 67 | $table->setHeaders($headers); 68 | $table->setRows($data); 69 | $table->render(); 70 | $io->newLine(); 71 | 72 | return Command::SUCCESS; 73 | })->run(); 74 | -------------------------------------------------------------------------------- /bin/view-json: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | addArgument('jsonFile', InputArgument::REQUIRED, 'json file name') 20 | ->addOption('hide-column', null, InputOption::VALUE_REQUIRED, 'Columns to hide (comma-separated)') 21 | ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'limit number of records') 22 | ->setCode(function (InputInterface $input, OutputInterface $output): int { 23 | $io = new SymfonyStyle($input, $output); 24 | $jsonFile = $input->getArgument('jsonFile'); 25 | $limit = $input->getOption('limit'); 26 | $hiddenColumns = $input->getOption('hide-column'); 27 | $hiddenColumns = $hiddenColumns ? explode(',', $hiddenColumns) : false; 28 | 29 | try { 30 | $jsonFile = (new class { 31 | use FileValidatorTrait; 32 | })->validateFileName($jsonFile); 33 | } catch (FileHandlerException) { 34 | $io->error("{$jsonFile} does not exists"); 35 | return Command::FAILURE; 36 | } 37 | 38 | $serviceContainer = new ServiceContainer(); 39 | /** @var JsonFileHandler $jsonFileHandler */ 40 | $jsonFileHandler = $serviceContainer->getContainerBuilder()->get('json_file_handler'); 41 | 42 | if (isset($limit) && !is_numeric($limit)) { 43 | $io->error("{$limit} is not numeric"); 44 | return Command::FAILURE; 45 | } 46 | 47 | $limit = $limit ? (int)$limit : false; 48 | $headers = []; 49 | try { 50 | $data = $jsonFileHandler->toArray( 51 | filename: $jsonFile, 52 | headers: $headers, 53 | hideColumns: $hiddenColumns, 54 | limit: $limit 55 | ); 56 | } catch (FileHandlerException) { 57 | $io->error('invalid json file'); 58 | $io->writeln( 59 | ' 60 | Expected Format 61 | ======================================= 62 | 63 | [ 64 | 65 | { 66 | "title": "The Catcher in the Rye", 67 | "author": "J.D. Salinger", 68 | "published_year": 1951 69 | }, 70 | { 71 | "title": "To Kill a Mockingbird", 72 | "author": "Harper Lee", 73 | "published_year": 1960 74 | }, 75 | { 76 | "title": "1984", 77 | "author": "George Orwell", 78 | "published_year": 1949 79 | } 80 | 81 | ] 82 | 83 | 84 | 85 | 86 | ======================================= 87 | 88 | ' 89 | ); 90 | return Command::FAILURE; 91 | } 92 | 93 | 94 | $io->title($jsonFile); 95 | $table = $io->createTable(); 96 | $table->setHeaders($headers); 97 | $table->setRows($data); 98 | $table->render(); 99 | $io->newLine(); 100 | 101 | return Command::SUCCESS; 102 | })->run(); 103 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rcsofttech85/file-handler", 3 | "description": "A simple library for abstracting various file operations and providing additional helper functions for file manipulation", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Rcsofttech85\\FileHandler\\": "src/" 9 | } 10 | }, 11 | "authors": [ 12 | { 13 | "name": "rahul", 14 | "email": "rcsofttech85@gmail.com" 15 | } 16 | ], 17 | "minimum-stability": "stable", 18 | "require": { 19 | "php": ">8.2", 20 | "ext-sodium": "*", 21 | "ext-ctype": "*", 22 | "ext-zip": "*", 23 | "ext-fileinfo": "*", 24 | "symfony/console": "^6.3", 25 | "symfony/dependency-injection": "^6.3", 26 | "symfony/config": "^6.3", 27 | "symfony/yaml": "^6.3", 28 | "symfony/process": "^6.3", 29 | "symfony/dotenv": "^6.3" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^10", 33 | "squizlabs/php_codesniffer": "^3.7", 34 | "symfony/var-dumper": "^6.3", 35 | "phpstan/phpstan": "^1.10" 36 | }, 37 | "bin": [ 38 | "bin/file-diff", 39 | "bin/view-csv", 40 | "bin/view-json", 41 | "bin/file-encrypt" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /images/csv-view.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcsofttech85/php-file-helper/a4591d75c4f5bf6cb117c671a0564770277fa067/images/csv-view.jpeg -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coding standard for file handler library 4 | src 5 | tests 6 | 7 | */migrations/* 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 7 3 | checkGenericClassInNonGenericObjectType: false 4 | paths: 5 | - src 6 | - tests -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | tests 14 | tests/Base/BaseTest.php 15 | 16 | 17 | 18 | 19 | 20 | src 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/CsvFileHandler.php: -------------------------------------------------------------------------------- 1 | 26 | * @throws FileHandlerException 27 | */ 28 | public function searchInCsvFile( 29 | string $filename, 30 | string $keyword, 31 | string $column, 32 | string|null $format = null 33 | ): bool|array { 34 | foreach ($this->getRows($filename) as $row) { 35 | if ($keyword === $row[$column]) { 36 | return ($format === FileHandler::ARRAY_FORMAT) ? $row : true; 37 | } 38 | } 39 | return false; 40 | } 41 | 42 | /** 43 | * @param string $filename 44 | * @return string|false 45 | * @throws FileHandlerException 46 | */ 47 | 48 | public function toJson(string $filename): string|false 49 | { 50 | $data = $this->toArray($filename); 51 | 52 | return json_encode($data); 53 | } 54 | 55 | /** 56 | * @param string $filename 57 | * @param array $hideColumns 58 | * @param int|false $limit 59 | * @return array> 60 | * @throws FileHandlerException 61 | */ 62 | public function toArray(string $filename, array|false $hideColumns = false, int|false $limit = false): array 63 | { 64 | return iterator_to_array($this->getRows($filename, $hideColumns, $limit)); 65 | } 66 | 67 | /** 68 | * @param string $filename 69 | * @param string $keyword 70 | * @param string $replace 71 | * @param string|null $column 72 | * @return bool 73 | * @throws FileHandlerException 74 | */ 75 | public function findAndReplaceInCsv( 76 | string $filename, 77 | string $keyword, 78 | string $replace, 79 | string|null $column = null, 80 | string|null $dirName = null 81 | ): bool { 82 | $headers = $this->extractHeader($filename); 83 | 84 | if (!$headers) { 85 | throw new FileHandlerException('failed to extract header'); 86 | } 87 | 88 | $tempFilePath = $this->tempFileHandler->createTempFileWithHeaders($headers, $dirName); 89 | if (!$tempFilePath) { 90 | throw new FileHandlerException('could not create temp file'); 91 | } 92 | 93 | try { 94 | $count = 0; 95 | foreach ($this->getRows($filename) as $row) { 96 | $count += (!$column) 97 | ? $this->replaceKeywordInRow($row, $keyword, $replace) 98 | : $this->replaceKeywordInColumn($row, $column, $keyword, $replace); 99 | 100 | $this->tempFileHandler->writeRowToTempFile($tempFilePath, $row); 101 | } 102 | 103 | 104 | if ($count < 1) { 105 | return false; 106 | } 107 | 108 | $this->tempFileHandler->renameTempFile($tempFilePath, $filename); 109 | } finally { 110 | $this->tempFileHandler->cleanupTempFile($tempFilePath); 111 | } 112 | 113 | return true; 114 | } 115 | 116 | /** 117 | * @param mixed $file 118 | * @return array|false 119 | */ 120 | 121 | private function extractHeader(mixed $file): array|false 122 | { 123 | $headers = []; 124 | if (is_resource($file)) { 125 | $headers = fgetcsv($file); 126 | } 127 | if (is_string($file)) { 128 | $file = fopen($file, 'r'); 129 | if (!$file) { 130 | return false; 131 | } 132 | $headers = fgetcsv($file); 133 | fclose($file); 134 | } 135 | 136 | if (!$headers) { 137 | return false; 138 | } 139 | 140 | if (!$this->isValidCsvFileFormat($headers)) { 141 | return false; 142 | } 143 | 144 | 145 | return $headers; 146 | } 147 | 148 | /** 149 | * @param array $row 150 | * @return bool 151 | */ 152 | private function isValidCsvFileFormat(array $row): bool 153 | { 154 | if (count($row) <= 1) { 155 | return false; 156 | } 157 | return true; 158 | } 159 | 160 | 161 | /** 162 | * @param string $filename 163 | * @param array|false $hideColumns 164 | * @param int|false $limit 165 | * @return Generator 166 | * @throws FileHandlerException 167 | */ 168 | private function getRows(string $filename, array|false $hideColumns = false, int|false $limit = false): Generator 169 | { 170 | $filename = $this->validateFileName($filename); 171 | $csvFile = $this->openFileAndReturnResource($filename, 'r'); 172 | $headers = $this->extractHeader($csvFile); 173 | if (!is_array($headers)) { 174 | fclose($csvFile); 175 | throw new FileHandlerException('could not extract header'); 176 | } 177 | 178 | $indices = is_array($hideColumns) ? $this->setColumnsToHide($headers, $hideColumns) : []; 179 | 180 | $isEmptyFile = true; 181 | $count = 0; 182 | 183 | try { 184 | while (($row = fgetcsv($csvFile)) !== false) { 185 | $isEmptyFile = false; 186 | if (!$this->isValidCsvFileFormat($row)) { 187 | throw new FileHandlerException('invalid csv file format'); 188 | } 189 | 190 | if (!empty($indices)) { 191 | $this->removeElementByIndex($row, $indices); 192 | } 193 | 194 | $item = array_combine($headers, $row); 195 | yield $item; 196 | $count++; 197 | 198 | if (is_int($limit) && $limit <= $count) { 199 | break; 200 | } 201 | } 202 | } finally { 203 | fclose($csvFile); 204 | } 205 | 206 | if ($isEmptyFile) { 207 | throw new FileHandlerException('invalid csv file format'); 208 | } 209 | } 210 | 211 | 212 | /** 213 | * @param array $row 214 | * @param string $keyword 215 | * @param string $replace 216 | * @return int 217 | */ 218 | private function replaceKeywordInRow(array &$row, string $keyword, string $replace): int 219 | { 220 | $count = 0; 221 | $replacement = array_search($keyword, $row); 222 | 223 | if ($replacement !== false) { 224 | $row[$replacement] = $replace; 225 | $count++; 226 | } 227 | 228 | return $count; 229 | } 230 | 231 | /** 232 | * @param array $row 233 | * @param string $column 234 | * @param string $keyword 235 | * @param string $replace 236 | * @return int 237 | * @throws FileHandlerException 238 | */ 239 | private function replaceKeywordInColumn(array &$row, string $column, string $keyword, string $replace): int 240 | { 241 | if (!array_key_exists($column, $row)) { 242 | throw new FileHandlerException("invalid column name"); 243 | } 244 | $count = 0; 245 | 246 | if ($keyword === $row[$column]) { 247 | $row[$column] = $replace; 248 | $count++; 249 | } 250 | 251 | return $count; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/DependencyInjection/ServiceContainer.php: -------------------------------------------------------------------------------- 1 | load('.env'); 16 | 17 | 18 | $containerBuilder = new ContainerBuilder(); 19 | $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__ . '/../../src/config')); 20 | $loader->load('services.yaml'); 21 | 22 | foreach ($_ENV as $key => $value) { 23 | $containerBuilder->setParameter($key, $value); 24 | } 25 | 26 | 27 | return $containerBuilder; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exception/FileEncryptorException.php: -------------------------------------------------------------------------------- 1 | validateFileName($filename); 26 | $plainText = file_get_contents($filename); 27 | 28 | if (!$plainText) { 29 | throw new FileEncryptorException('File has no content'); 30 | } 31 | if (ctype_xdigit($plainText)) { 32 | throw new FileEncryptorException('file is already encrypted'); 33 | } 34 | 35 | 36 | $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 37 | 38 | 39 | $secret = $this->getParam(self::ENCRYPT_PASSWORD); 40 | 41 | $key = hash('sha256', $secret, true); 42 | 43 | 44 | $ciphertext = sodium_crypto_secretbox($plainText, $nonce, $key); 45 | 46 | $output = bin2hex($nonce . $ciphertext); 47 | 48 | $file = $this->openFileAndReturnResource($filename); 49 | 50 | try { 51 | fwrite($file, $output); 52 | } finally { 53 | fclose($file); 54 | } 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * @return bool 61 | * @throws FileEncryptorException 62 | * @throws FileHandlerException 63 | * @throws SodiumException 64 | */ 65 | public function decryptFile(string $filename): bool 66 | { 67 | $this->validateFileName($filename); 68 | $encryptedData = file_get_contents($filename); 69 | 70 | if (!$encryptedData) { 71 | throw new FileEncryptorException('File has no content'); 72 | } 73 | 74 | if (!ctype_xdigit($encryptedData)) { 75 | throw new FileEncryptorException('file is not encrypted'); 76 | } 77 | 78 | 79 | $bytes = $this->convertHexToBin($encryptedData); 80 | 81 | $nonce = substr($bytes, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 82 | $ciphertext = substr($bytes, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 83 | 84 | 85 | $secret = $this->getParam(self::ENCRYPT_PASSWORD); 86 | 87 | $key = hash('sha256', $secret, true); 88 | 89 | $plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key); 90 | $file = $this->openFileAndReturnResource($filename); 91 | 92 | if (!$plaintext) { 93 | fwrite($file, $encryptedData); 94 | throw new FileEncryptorException('could not decrypt file'); 95 | } 96 | 97 | 98 | try { 99 | fwrite($file, $plaintext); 100 | } finally { 101 | fclose($file); 102 | } 103 | 104 | return true; 105 | } 106 | 107 | /** 108 | * @param string $encryptedData 109 | * @return string 110 | * @throws FileEncryptorException 111 | */ 112 | public function convertHexToBin(string $encryptedData): string 113 | { 114 | $bytes = hex2bin($encryptedData); 115 | if (!$bytes) { 116 | throw new FileEncryptorException('could not convert hex to bin'); 117 | } 118 | return $bytes; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/FileHandler.php: -------------------------------------------------------------------------------- 1 | |null 18 | */ 19 | private null|array $files = []; 20 | 21 | 22 | /** 23 | * @throws FileHandlerException 24 | */ 25 | public function open( 26 | string $filename, 27 | string $mode = "w", 28 | bool $include_path = false, 29 | mixed $context = null 30 | ): self { 31 | $filename = $this->sanitize($filename); 32 | $file = fopen($filename, $mode, $include_path, $context); 33 | 34 | if (!$file) { 35 | throw new FileHandlerException('File not found'); 36 | } 37 | 38 | $this->files[] = $file; 39 | 40 | return $this; 41 | } 42 | 43 | 44 | /** 45 | * @param string $data 46 | * @param int<0, max>|null $length 47 | * @return void 48 | * @throws FileHandlerException 49 | */ 50 | public function write(string $data, ?int $length = null): void 51 | { 52 | if (!$this->files) { 53 | throw new FileHandlerException('no files available to write'); 54 | } 55 | foreach ($this->files as $file) { 56 | $byteWritten = fwrite($file, $data, $length); 57 | if (!$byteWritten) { 58 | throw new FileHandlerException('Error writing to file'); 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * @throws FileHandlerException 65 | */ 66 | public function compress(string $filename, string $zipFilename, int $flag = ZipArchive::CREATE): void 67 | { 68 | $filename = $this->validateFileName($filename); 69 | 70 | $zip = new ZipArchive(); 71 | 72 | 73 | if (true !== $zip->open($zipFilename, $flag)) { 74 | throw new FileHandlerException('Failed to create the ZIP archive.'); 75 | } 76 | 77 | $zip->addFile($filename); 78 | $zip->close(); 79 | } 80 | 81 | /** 82 | * @throws FileHandlerException 83 | */ 84 | public function getMimeType(string $filename): string|false 85 | { 86 | $filename = $this->validateFileName($filename); 87 | 88 | 89 | $fileInfo = new finfo(FILEINFO_MIME_TYPE); 90 | $mimeType = $fileInfo->file($filename); 91 | if ($mimeType === 'application/octet-stream') { 92 | throw new FileHandlerException('unknown mime type'); 93 | } 94 | 95 | return $mimeType; 96 | } 97 | 98 | /** 99 | * @throws FileHandlerException 100 | */ 101 | public function decompress(string $zipFilename, string $extractPath = "./", int $flag = ZipArchive::CREATE): void 102 | { 103 | $zipFilename = $this->validateFileName($zipFilename); 104 | 105 | $zip = new ZipArchive(); 106 | 107 | if (true !== $zip->open($zipFilename, $flag)) { 108 | throw new FileHandlerException('Invalid or uninitialized Zip object'); 109 | } 110 | 111 | 112 | if (!$zip->extractTo($extractPath)) { 113 | throw new FileHandlerException('Failed to extract the ZIP archive.'); 114 | } 115 | 116 | $zip->close(); 117 | } 118 | 119 | 120 | public function close(): void 121 | { 122 | foreach ($this->files as $file) { 123 | fclose($file); 124 | } 125 | $this->resetFiles(); 126 | } 127 | 128 | public function resetFiles(): void 129 | { 130 | $this->files = null; 131 | } 132 | 133 | /** 134 | * @throws FileHandlerException 135 | */ 136 | public function delete(string $filename): void 137 | { 138 | $filename = $this->validateFileName($filename); 139 | 140 | unlink($filename); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/FileHashChecker.php: -------------------------------------------------------------------------------- 1 | getParam(self::STORED_HASH_FILE); 39 | $file = $this->csvFileHandler->searchInCsvFile( 40 | filename: $storedHashesFile, 41 | keyword: $filename, 42 | column: self::SEARCH_COLUMN_NAME, 43 | format: FileHandler::ARRAY_FORMAT 44 | ); 45 | 46 | 47 | if (!$file || !is_array($file)) { 48 | throw new HashException('this file is not hashed'); 49 | } 50 | 51 | $expectedHash = $file['Hash']; 52 | $hash = $this->hashFile($filename, $algo); 53 | 54 | 55 | if ($hash !== $expectedHash) { 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | 62 | /** 63 | * @param string $filename 64 | * @param string $algo 65 | * @param string $env 66 | * @return string 67 | * @throws FileHandlerException 68 | * @throws HashException 69 | */ 70 | 71 | public function hashFile( 72 | string $filename, 73 | string $algo = self::ALGO_256, 74 | string $env = self::STORED_HASH_FILE 75 | ): string { 76 | $this->validateFileName($filename); 77 | if (!in_array($algo, [self::ALGO_512, self::ALGO_256])) { 78 | throw new HashException('algorithm not supported'); 79 | } 80 | 81 | if (!$hash = hash_file($algo, $filename)) { 82 | throw new HashException('could not hash file'); 83 | } 84 | 85 | $storedHashesFile = $this->getParam($env); 86 | 87 | 88 | $file = fopen($storedHashesFile, 'a+'); 89 | if (!$file) { 90 | throw new FileHandlerException('file not found'); 91 | } 92 | $this->checkHeaderExists($file); 93 | 94 | 95 | try { 96 | $filenameExists = $this->csvFileHandler->searchInCsvFile( 97 | filename: $storedHashesFile, 98 | keyword: $filename, 99 | column: 'File' 100 | ); 101 | 102 | if (!$filenameExists) { 103 | fputcsv($file, [$filename, $hash]); 104 | } 105 | } catch (FileHandlerException $e) { 106 | fputcsv($file, [$filename, $hash]); 107 | } finally { 108 | fclose($file); 109 | } 110 | 111 | 112 | return $hash; 113 | } 114 | 115 | /** 116 | * @param mixed $storedHashFile 117 | * @return void 118 | */ 119 | public function checkHeaderExists(mixed $storedHashFile): void 120 | { 121 | $header = fgetcsv($storedHashFile); 122 | 123 | if (!$header || $header[0] !== self::SEARCH_COLUMN_NAME || $header[1] !== self::SEARCH_COLUMN_VALUE) { 124 | fseek($storedHashFile, 0); 125 | fputcsv($storedHashFile, [self::SEARCH_COLUMN_NAME, self::SEARCH_COLUMN_VALUE]); 126 | fflush($storedHashFile); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/JsonFileHandler.php: -------------------------------------------------------------------------------- 1 | $headers 18 | * @param array|false $hideColumns 19 | * @param int|false $limit 20 | * @return array> 21 | * @throws FileHandlerException 22 | */ 23 | public function toArray( 24 | string $filename, 25 | array &$headers = [], 26 | array|false $hideColumns = false, 27 | int|false $limit = false 28 | ): array { 29 | return iterator_to_array($this->getRows($filename, $headers, $hideColumns, $limit)); 30 | } 31 | 32 | /** 33 | * @param string $filename 34 | * @return array> 35 | * @throws FileHandlerException 36 | */ 37 | 38 | private function validateFile(string $filename): array 39 | { 40 | $filename = $this->validateFileName($filename); 41 | return $this->getValidJsonData($filename); 42 | } 43 | 44 | 45 | /** 46 | * @param string $filename 47 | * @return array> 48 | * @throws FileHandlerException 49 | */ 50 | public function getValidJsonData(string $filename): array 51 | { 52 | $jsonContents = file_get_contents($filename); 53 | if (!$jsonContents) { 54 | throw new FileHandlerException("{$filename} is not valid"); 55 | } 56 | 57 | $data = json_decode($jsonContents, true); 58 | if (json_last_error() !== JSON_ERROR_NONE || !is_array($data[0])) { 59 | throw new FileHandlerException('could not decode json'); 60 | } 61 | 62 | 63 | $firstArrayKeys = array_keys($data[0]); 64 | 65 | foreach ($data as $item) { 66 | $currentArrayKeys = array_keys($item); 67 | 68 | if ($firstArrayKeys !== $currentArrayKeys) { 69 | throw new FileHandlerException('Inconsistent JSON data'); 70 | } 71 | } 72 | 73 | return $data; 74 | } 75 | 76 | /** 77 | * @param string $filename 78 | * @param array $headers 79 | * @param array|false $hideColumns 80 | * @param int|false $limit 81 | * @return Generator 82 | * @throws FileHandlerException 83 | */ 84 | public function getRows( 85 | string $filename, 86 | array &$headers, 87 | array|false $hideColumns = false, 88 | int|false $limit = false 89 | ): Generator { 90 | $contents = $this->validateFile($filename); 91 | 92 | $headers = array_keys($contents[0]); 93 | $indices = is_array($hideColumns) ? $this->setColumnsToHide($headers, $hideColumns) : []; 94 | 95 | $count = 0; 96 | $shouldLimit = is_int($limit); 97 | 98 | foreach ($contents as $content) { 99 | if (!empty($indices)) { 100 | $content = array_values($content); 101 | $this->removeElementByIndex($content, $indices); 102 | } 103 | 104 | yield $content; 105 | $count++; 106 | 107 | if ($shouldLimit && $count >= $limit) { 108 | break; 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/StreamHandler.php: -------------------------------------------------------------------------------- 1 | $fibers 13 | */ 14 | private array $fibers = []; 15 | 16 | 17 | /** 18 | * @param array $streamUrls 19 | * @param int<0,100> $chunk 20 | * @throws StreamException 21 | */ 22 | public function __construct(public readonly array $streamUrls, public readonly int $chunk = 100) 23 | { 24 | if (!$this->streamUrls) { 25 | throw new StreamException('No stream URLs provided.'); 26 | } 27 | } 28 | 29 | 30 | private function stream(string $streamUrl, string $outputFilename): Fiber 31 | { 32 | return new Fiber(function () use ($streamUrl, $outputFilename) { 33 | $stream = fopen($streamUrl, 'r'); 34 | $outputFile = fopen($outputFilename, 'w'); 35 | if (!$stream || !$outputFile) { 36 | throw new StreamException("Failed to open stream: $streamUrl"); 37 | } 38 | 39 | stream_set_blocking($stream, false); 40 | 41 | 42 | try { 43 | while (!feof($stream)) { 44 | $length = $this->chunk; 45 | $contents = fread($stream, $length); 46 | if ($contents) { 47 | fwrite($outputFile, $contents); 48 | } 49 | 50 | Fiber::suspend(); 51 | } 52 | } finally { 53 | fclose($stream); 54 | fclose($outputFile); 55 | } 56 | }); 57 | } 58 | 59 | public function initiateConcurrentStreams(): self 60 | { 61 | foreach ($this->streamUrls as $outputFile => $streamUrl) { 62 | $fiber = $this->stream($streamUrl, $outputFile); 63 | 64 | $this->fibers[] = $fiber; 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @return $this 72 | * @throws StreamException 73 | * @throws Throwable 74 | */ 75 | public function start(): self 76 | { 77 | if (!$this->fibers) { 78 | throw new StreamException("No fibers available to start"); 79 | } 80 | 81 | foreach ($this->fibers as $fiber) { 82 | $fiber->start(); 83 | } 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * @throws Throwable 90 | */ 91 | public function resume(bool $resumeOnce = false): void 92 | { 93 | if (!$this->fibers) { 94 | throw new StreamException("No fibers are currently running"); 95 | } 96 | 97 | foreach ($this->fibers as $fiber) { 98 | while (!$fiber->isTerminated()) { 99 | $fiber->resume(); 100 | if ($resumeOnce) { 101 | break; 102 | } 103 | } 104 | } 105 | 106 | $this->fibers = []; 107 | } 108 | 109 | public function resetFibers(): void 110 | { 111 | $this->fibers = []; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/TempFileHandler.php: -------------------------------------------------------------------------------- 1 | $row 35 | * @return void 36 | */ 37 | public function writeRowToTempFile(string $tempFilePath, array $row): void 38 | { 39 | $tempFileHandle = fopen($tempFilePath, 'a'); 40 | if ($tempFileHandle) { 41 | fputs($tempFileHandle, implode(',', $row) . PHP_EOL); 42 | fclose($tempFileHandle); 43 | } 44 | } 45 | 46 | /** 47 | * @param array $headers 48 | * @param string|null $dirName 49 | * @param string|null $prefix 50 | * @return string|false 51 | * @throws FileHandlerException 52 | */ 53 | public function createTempFileWithHeaders( 54 | array $headers, 55 | string|null $dirName = null, 56 | string|null $prefix = null 57 | ): string|false { 58 | if (null === $dirName) { 59 | $dirName = sys_get_temp_dir(); 60 | } 61 | if (null === $prefix) { 62 | $prefix = 'tempfile_'; 63 | } 64 | 65 | $tempFilePath = $this->getTempName($dirName, $prefix); 66 | 67 | if (!$tempFilePath) { 68 | return false; 69 | } 70 | $tempFileHandle = $this->openFileAndReturnResource($tempFilePath); 71 | 72 | fputs($tempFileHandle, implode(',', $headers) . PHP_EOL); 73 | fclose($tempFileHandle); 74 | 75 | 76 | return $tempFilePath; 77 | } 78 | 79 | public function getTempName(string $directory, string $prefix): string|false 80 | { 81 | if (!is_dir($directory)) { 82 | return false; 83 | } 84 | return tempnam($directory, $prefix); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Utilities/RowColumnHelper.php: -------------------------------------------------------------------------------- 1 | $headers 9 | * @param array $hideColumns 10 | * @return array,int> 11 | */ 12 | private function setColumnsToHide(array &$headers, array $hideColumns): array 13 | { 14 | $indices = []; 15 | if (!empty($hideColumns)) { 16 | foreach ($hideColumns as $hideColumn) { 17 | $index = array_search($hideColumn, $headers); 18 | if ($index !== false) { 19 | $indices[] = (int)$index; 20 | unset($headers[$index]); 21 | } 22 | } 23 | $headers = array_values($headers); 24 | } 25 | return $indices; 26 | } 27 | 28 | /** 29 | * @param array $row 30 | * @param array, int> $indices 31 | * @return void 32 | */ 33 | private function removeElementByIndex(array &$row, array $indices): void 34 | { 35 | foreach ($indices as $index) { 36 | if (isset($row[$index])) { 37 | unset($row[$index]); 38 | } 39 | } 40 | 41 | $row = array_values($row); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Validator/FileValidatorTrait.php: -------------------------------------------------------------------------------- 1 | isFileSafe($filename, self::STORED_HASH_FILE)) { 23 | $filename = $this->sanitize($filename); 24 | } 25 | 26 | 27 | if ($path) { 28 | $absolutePath = realpath($path); 29 | $absolutePath ?: 30 | throw new FileHandlerException("path {$path} is not valid')"); 31 | $filename = $absolutePath . DIRECTORY_SEPARATOR . $filename; 32 | } 33 | if (!file_exists($filename)) { 34 | throw new FileHandlerException("file does not exists!"); 35 | } 36 | 37 | return $filename; 38 | } 39 | 40 | /** 41 | * @throws FileHandlerException 42 | */ 43 | public function sanitize(string $filename): string 44 | { 45 | $pattern = '/^[a-zA-Z0-9_.-]+$/'; 46 | if (!preg_match($pattern, $filename)) { 47 | throw new FileHandlerException('file not found'); 48 | } 49 | 50 | return $filename; 51 | } 52 | 53 | /** 54 | * @param string $filename 55 | * @param string $envVariable 56 | * @return bool 57 | * @throws FileHandlerException 58 | */ 59 | private function isFileSafe(string $filename, string $envVariable): bool 60 | { 61 | $safeFile = $this->getParam($envVariable); 62 | 63 | 64 | if ($safeFile !== $filename) { 65 | return false; 66 | } 67 | 68 | return true; 69 | } 70 | 71 | /** 72 | * @param string $filename 73 | * @param string $envVariable 74 | * @return bool 75 | * @throws FileHandlerException 76 | */ 77 | public function isFileRestricted(string $filename, string $envVariable): bool 78 | { 79 | return $this->isFileSafe($filename, $envVariable); 80 | } 81 | 82 | /** 83 | * @return ContainerBuilder 84 | */ 85 | private function getContainerBuilder(): ContainerBuilder 86 | { 87 | return (new ServiceContainer())->getContainerBuilder(); 88 | } 89 | 90 | /** 91 | * @param string $parameter 92 | * @param ContainerBuilder|null $container 93 | * @return string 94 | * @throws FileHandlerException 95 | */ 96 | public function getParam(string $parameter, ContainerBuilder $container = null): string 97 | { 98 | if (null === $container) { 99 | $container = $this->getContainerBuilder(); 100 | } 101 | $param = $container->getParameter($parameter); 102 | if (!is_string($param)) { 103 | throw new FileHandlerException("{$parameter} is not string type"); 104 | } 105 | 106 | return $param; 107 | } 108 | 109 | /** 110 | * @param string $filename 111 | * @param string $mode 112 | * @return mixed 113 | * @throws FileHandlerException 114 | */ 115 | public function openFileAndReturnResource(string $filename, string $mode = 'w'): mixed 116 | { 117 | $file = fopen($filename, $mode); 118 | if (!$file) { 119 | throw new FileHandlerException('file is not valid'); 120 | } 121 | return $file; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | file_handler: 3 | class: 'Rcsofttech85\FileHandler\FileHandler' 4 | 5 | file_encryptor: 6 | class: 'Rcsofttech85\FileHandler\FileEncryptor' 7 | 8 | 9 | 10 | temp_file_handler: 11 | class: 'Rcsofttech85\FileHandler\TempFileHandler' 12 | 13 | csv_file_handler: 14 | class: 'Rcsofttech85\FileHandler\CsvFileHandler' 15 | arguments: [ '@temp_file_handler' ] 16 | 17 | file_hash: 18 | class: 'Rcsofttech85\FileHandler\FileHashChecker' 19 | arguments: [ '@csv_file_handler' ] 20 | 21 | json_file_handler: 22 | class: 'Rcsofttech85\FileHandler\JsonFileHandler' 23 | 24 | -------------------------------------------------------------------------------- /tests/Base/BaseTest.php: -------------------------------------------------------------------------------- 1 | |null 13 | */ 14 | protected static array|null $files = []; 15 | 16 | protected static ContainerBuilder|null $containerBuilder; 17 | 18 | public static function setUpBeforeClass(): void 19 | { 20 | parent::setUpBeforeClass(); 21 | $serviceContainer = new ServiceContainer(); 22 | self::$containerBuilder = $serviceContainer->getContainerBuilder(); 23 | $content = "Film,Genre,Lead Studio,Audience score %,Profitability,Rotten Tomatoes %,Worldwide Gross,Year\n" 24 | . "Zack and Miri Make a Porno,Romance,The Weinstein Company,70,1.747541667,64,$41.94 ,2008\n" 25 | . "Youth in Revolt,Comedy,The Weinstein Company,52,1.09,68,$19.62 ,2010\n" 26 | . "Twilight,Romance,Independent,68,6.383363636,26,$702.17 ,2011"; 27 | self::$files[] = 'movie.csv'; 28 | file_put_contents('movie.csv', $content); 29 | } 30 | 31 | public static function tearDownAfterClass(): void 32 | { 33 | parent::tearDownAfterClass(); 34 | foreach (static::$files as $file) { 35 | if (file_exists($file)) { 36 | unlink(filename: $file); 37 | } 38 | } 39 | static::$files = null; 40 | } 41 | 42 | protected function isFileValid(string $filename): mixed 43 | { 44 | if (!file_exists($filename) || !$data = file_get_contents($filename)) { 45 | $this->fail('file does not exists or has no content'); 46 | } 47 | return $data; 48 | } 49 | 50 | protected function setObjectHandler(string $classname, string $serviceId): mixed 51 | { 52 | $objectHandler = self::$containerBuilder->get($serviceId); 53 | 54 | if (!is_a($objectHandler, $classname)) { 55 | $this->fail("provided service is not an instance of " . $classname); 56 | } 57 | return $objectHandler; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Integration/FileDiffCommandTest.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | public static function commandArgumentProvider(): iterable 25 | { 26 | file_put_contents("old", "this is old file" . PHP_EOL, FILE_APPEND); 27 | file_put_contents("new", "this is new file" . PHP_EOL, FILE_APPEND); 28 | 29 | file_put_contents("old", "this line has some difference" . PHP_EOL, FILE_APPEND); 30 | file_put_contents("new", "this line has same old code" . PHP_EOL, FILE_APPEND); 31 | 32 | 33 | yield ['old', 'new', 'old (Line 1):']; 34 | yield ['old', 'new', 'old (Line 2):']; 35 | } 36 | 37 | /** 38 | * @return iterable> 39 | */ 40 | 41 | public static function fileWithSameDataProvider(): iterable 42 | { 43 | file_put_contents("old", "this has matching content" . PHP_EOL, FILE_APPEND); 44 | file_put_contents("new", "this has matching content" . PHP_EOL, FILE_APPEND); 45 | 46 | 47 | yield ['old', 'new', 'old (Line 3):']; 48 | } 49 | 50 | #[Test] 51 | #[DataProvider('commandArgumentProvider')] 52 | public function fileDiffShowsCorrectChanges(string $oldFile, string $newFile, string $expected): void 53 | { 54 | $command = "php bin/file-diff $oldFile $newFile"; 55 | exec($command, $output, $exitCode); 56 | 57 | $actualOutput = implode("\n", $output); 58 | 59 | $this->assertStringContainsString($expected, $actualOutput); 60 | 61 | 62 | $this->assertSame(0, $exitCode); 63 | } 64 | 65 | #[Test] 66 | public function throwsExceptionIfArgumentIsNotValidFile(): void 67 | { 68 | $command = "php bin/file-diff unknown unknown"; 69 | exec($command, $output, $exitCode); 70 | 71 | $actualOutput = implode("\n", $output); 72 | $expectedOutput = "file does not exists"; 73 | 74 | 75 | $this->assertSame(1, $exitCode); 76 | $this->assertStringContainsString($expectedOutput, $actualOutput); 77 | } 78 | 79 | #[Test] 80 | #[DataProvider('fileWithSameDataProvider')] 81 | public function sameContentShouldNotBeDisplayedInTheResult(string $oldFile, string $newFile, string $expected): void 82 | { 83 | $command = "php bin/file-diff $oldFile $newFile"; 84 | exec($command, $output, $exitCode); 85 | 86 | $actualOutput = implode("\n", $output); 87 | 88 | $expected = "Old (Line 3)"; 89 | 90 | $this->assertStringNotContainsString($expected, $actualOutput); 91 | $this->assertSame(0, $exitCode); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Integration/FileEncryptCommandTest.php: -------------------------------------------------------------------------------- 1 | run(); 25 | 26 | 27 | $actualOutput = $process->getOutput(); 28 | $exitCode = $process->getExitCode(); 29 | 30 | $this->assertStringContainsString("The file encryption was successful", $actualOutput); 31 | $this->assertSame(0, $exitCode); 32 | } 33 | 34 | #[Test] 35 | public function fileIsDecryptedProperly(): void 36 | { 37 | $command = "bin/file-encrypt dummy.txt --mode=decryption"; 38 | $process = Process::fromShellCommandline($command); 39 | $process->run(); 40 | 41 | 42 | $actualOutput = $process->getOutput(); 43 | $exitCode = $process->getExitCode(); 44 | 45 | $this->assertStringContainsString("The file decryption was successful", $actualOutput); 46 | $this->assertSame(0, $exitCode); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Integration/StreamHandlerTest.php: -------------------------------------------------------------------------------- 1 | streamHandler = null; 33 | } 34 | 35 | /** 36 | * @return void 37 | * @throws StreamException 38 | * @throws Throwable 39 | */ 40 | #[Test] 41 | public function resumeAtOnceWorkingProperly(): void 42 | { 43 | $data = "hello world"; 44 | file_put_contents("profile", $data); 45 | $this->streamHandler = new StreamHandler([ 46 | "output.html" => "profile" 47 | 48 | ]); 49 | 50 | $this->streamHandler->initiateConcurrentStreams()->start()->resume(resumeOnce: true); 51 | $content = file_get_contents('output.html'); 52 | if (!$content) { 53 | $this->fail('file has no content'); 54 | } 55 | 56 | $this->assertStringContainsString($data, $content); 57 | } 58 | 59 | /** 60 | * @param array $urls 61 | * @return void 62 | * @throws StreamException 63 | * @throws Throwable 64 | */ 65 | #[Test] 66 | #[DataProvider('streamDataProvider')] 67 | public function streamAndWriteToFile(array $urls): void 68 | { 69 | $this->streamHandler = new StreamHandler($urls); 70 | $this->streamHandler->initiateConcurrentStreams()->start()->resume(); 71 | 72 | $files = array_keys($urls); 73 | foreach ($files as $file) { 74 | $data = $this->isFileValid($file); 75 | $this->assertGreaterThan(0, filesize($file)); 76 | $this->assertStringContainsString('', $data); 77 | $this->assertStringContainsString('', $data); 78 | } 79 | } 80 | 81 | /** 82 | * @param string $outputFile 83 | * @param string $url 84 | * @return void 85 | * @throws StreamException 86 | * @throws Throwable 87 | */ 88 | 89 | #[Test] 90 | #[DataProvider('wrongStreamDataProvider')] 91 | public function throwExceptionIfUrlIsInvalid(string $outputFile, string $url): void 92 | { 93 | $this->streamHandler = new StreamHandler([$outputFile => $url]); 94 | 95 | 96 | $this->expectException(StreamException::class); 97 | $this->streamHandler->initiateConcurrentStreams()->start()->resume(); 98 | } 99 | 100 | /** 101 | * @return void 102 | * @throws StreamException 103 | * @throws Throwable 104 | */ 105 | 106 | #[Test] 107 | public function throwExceptionIfNoFibersAreAvailable(): void 108 | { 109 | $this->streamHandler = new StreamHandler([ 110 | "output.html" => 111 | "https://gist.github.com/rcsofttech85/629b37d483c4796db7bdcb3704067631#file-gistfile1-txt", 112 | 113 | ]); 114 | 115 | $this->expectException(StreamException::class); 116 | $this->expectExceptionMessage('No fibers available to start'); 117 | $this->streamHandler->resetFibers(); 118 | $this->streamHandler->start(); 119 | } 120 | 121 | 122 | /** 123 | * @return void 124 | * @throws StreamException 125 | * @throws Throwable 126 | */ 127 | #[Test] 128 | public function throwExceptionIfNoFibersAreAvailableToResume(): void 129 | { 130 | $this->streamHandler = new StreamHandler([ 131 | "output.html" => 132 | "https://gist.github.com/rcsofttech85/629b37d483c4796db7bdcb3704067631#file-gistfile1-txt", 133 | 134 | ]); 135 | 136 | $this->expectException(StreamException::class); 137 | $this->expectExceptionMessage('No fibers are currently running'); 138 | $this->streamHandler->resetFibers(); 139 | $this->streamHandler->resume(); 140 | } 141 | 142 | /** 143 | * @return void 144 | * @throws StreamException 145 | */ 146 | #[Test] 147 | public function throwExceptionIfEmptyDataProvided(): void 148 | { 149 | $this->expectException(StreamException::class); 150 | $this->expectExceptionMessage('No stream URLs provided.'); 151 | $this->streamHandler = new StreamHandler([]); 152 | } 153 | 154 | /** 155 | * @return iterable> 156 | */ 157 | public static function wrongStreamDataProvider(): iterable 158 | { 159 | yield ["output.html", "https://gist.github"]; 160 | } 161 | 162 | 163 | /** 164 | * @return iterable>> 165 | */ 166 | public static function streamDataProvider(): iterable 167 | { 168 | yield [ 169 | [ 170 | "output.html" => 171 | "https://gist.github.com/rcsofttech85/629b37d483c4796db7bdcb3704067631#file-gistfile1-txt", 172 | 173 | "output1.html" => 174 | "https://gist.github.com/rcsofttech85/f71f2454b1fc40a077cda14ef3097385#file-gistfile1-txt", 175 | 176 | 177 | "output2.html" => 178 | "https://gist.github.com/rcsofttech85/79ab19f1502e72c95cfa97d5205fa47d#file-gistfile1-txt" 179 | ] 180 | ]; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/Integration/ViewCsvCommandTest.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | public static function fileProvider(): iterable 25 | { 26 | $file = "profile.csv"; 27 | $csvData = <<> 47 | */ 48 | public static function invalidFileProvider(): iterable 49 | { 50 | $file = "invalidProfile.csv"; 51 | $csvData = <<assertSame(0, $exitCode); 69 | $this->assertStringContainsString($file, $actualOutput); 70 | unlink($file); 71 | } 72 | 73 | #[Test] 74 | public function ifLimitIsSetToNonNumericThenCommandShouldFail(): void 75 | { 76 | $limit = 'hello'; 77 | $command = "bin/view-csv movie.csv --limit {$limit}"; 78 | exec($command, $output, $exitCode); 79 | $actualOutput = implode("\n", $output); 80 | 81 | $this->assertSame(1, $exitCode); 82 | $this->assertStringContainsString("{$limit} is not numeric", $actualOutput); 83 | } 84 | 85 | #[Test] 86 | #[DataProvider('InvalidFileProvider')] 87 | public function commandShouldReturnErrorIfFileIsInvalid(string $file): void 88 | { 89 | $command = "bin/view-csv {$file}"; 90 | exec($command, $output, $exitCode); 91 | $actualOutput = implode("\n", $output); 92 | 93 | $this->assertStringContainsString('invalid csv file', $actualOutput); 94 | $this->assertSame(1, $exitCode); 95 | unlink($file); 96 | } 97 | 98 | #[Test] 99 | public function viewBlockedForRestrictedFile(): void 100 | { 101 | $restrictedFile = self::$containerBuilder->getParameter('STORED_HASH_FILE'); 102 | if (!is_string($restrictedFile)) { 103 | $this->fail('restricted files is expected to be string'); 104 | } 105 | $command = "bin/view-csv {$restrictedFile}"; 106 | exec($command, $output, $exitCode); 107 | $actualOutput = implode("\n", $output); 108 | 109 | $this->assertStringContainsString("{$restrictedFile} does not exists", $actualOutput); 110 | $this->assertSame(1, $exitCode); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Integration/ViewJsonCommandTest.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | public static function fileProvider(): iterable 15 | { 16 | $file = "book.json"; 17 | $csvData = '[ 18 | { 19 | "title": "The Catcher in the Rye", 20 | "author": "J.D. Salinger", 21 | "published_year": 1951 22 | }, 23 | { 24 | "title": "To Kill a Mockingbird", 25 | "author": "Harper Lee", 26 | "published_year": 1960 27 | }, 28 | { 29 | "title": "1984", 30 | "author": "George Orwell", 31 | "published_year": 1949 32 | } 33 | ]'; 34 | file_put_contents($file, $csvData); 35 | 36 | yield [$file]; 37 | } 38 | 39 | #[Test] 40 | #[DataProvider('fileProvider')] 41 | public function viewJsonFileDisplayInformationCorrectly(string $file): void 42 | { 43 | $command = "php bin/view-json {$file}"; 44 | exec($command, $output, $exitCode); 45 | $actualOutput = implode("\n", $output); 46 | 47 | $this->assertSame(0, $exitCode); 48 | $this->assertStringContainsString($file, $actualOutput); 49 | $this->assertStringContainsString('The Catcher in the Rye', $actualOutput); 50 | unlink($file); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | csvFileHandler = $this->setObjectHandler(CsvFileHandler::class, 'csv_file_handler'); 21 | } 22 | 23 | protected function tearDown(): void 24 | { 25 | parent::tearDown(); 26 | 27 | $this->csvFileHandler = null; 28 | unlink('temp'); 29 | } 30 | 31 | #[Test] 32 | #[DataProvider("wrongColumnNameProvider")] 33 | public function throwExceptionIfWrongColumnNameProvided(string $columnName): void 34 | { 35 | $this->expectException(FileHandlerException::class); 36 | $this->expectExceptionMessage("invalid column name"); 37 | $this->csvFileHandler->findAndReplaceInCsv("movie.csv", "Twilight", "hello", $columnName); 38 | } 39 | 40 | /** 41 | * @return void 42 | */ 43 | #[Test] 44 | public function throwExceptionIfHeadersCouldNotBeExtracted(): void 45 | { 46 | file_put_contents("temp", ""); 47 | $this->expectException(FileHandlerException::class); 48 | $this->expectExceptionMessage('failed to extract header'); 49 | $this->csvFileHandler->findAndReplaceInCsv("temp", "Twilight", "hello"); 50 | } 51 | 52 | #[Test] 53 | public function throwExceptionIfFileFormatIsNotValid(): void 54 | { 55 | file_put_contents("temp", "File,Hash\nHello"); 56 | $this->expectException(FileHandlerException::class); 57 | $this->expectExceptionMessage('invalid csv file format'); 58 | $this->csvFileHandler->findAndReplaceInCsv("temp", "Twilight", "hello"); 59 | } 60 | 61 | #[Test] 62 | public function throwExceptionIfFilenameIsInvalid(): void 63 | { 64 | $this->expectException(FileHandlerException::class); 65 | $this->expectExceptionMessage('failed to extract header'); 66 | $this->csvFileHandler->findAndReplaceInCsv("temp", "Twilight", "hello"); 67 | } 68 | 69 | /** 70 | * @return void 71 | * @throws FileHandlerException 72 | */ 73 | #[Test] 74 | public function shouldReturnFalseIfKeywordIsNotMatched(): void 75 | { 76 | $this->csvFileHandler->findAndReplaceInCsv("movie.csv", "Twil", "hello"); 77 | $this->assertFalse(false); 78 | } 79 | 80 | #[Test] 81 | public function findAndReplaceShouldThrowErrorIfDirNameIsInValid(): void 82 | { 83 | $this->expectException(FileHandlerException::class); 84 | $this->expectExceptionMessage("could not create temp file"); 85 | $this->csvFileHandler->findAndReplaceInCsv( 86 | filename: 'movie.csv', 87 | keyword: 'hello', 88 | replace: 'how', 89 | dirName: '/ab' 90 | ); 91 | } 92 | 93 | 94 | /** 95 | * @return void 96 | * @throws FileHandlerException 97 | */ 98 | #[Test] 99 | public function findAndReplaceInCsvMethodShouldReplaceTextWithoutColumnOption(): void 100 | { 101 | $hasReplaced = $this->csvFileHandler->findAndReplaceInCsv("movie.csv", "Twilight", "Inception"); 102 | 103 | $data = $this->isFileValid('movie.csv'); 104 | 105 | $this->assertTrue($hasReplaced); 106 | $this->assertStringContainsString('Inception', $data); 107 | } 108 | 109 | /** 110 | * @return void 111 | * @throws FileHandlerException 112 | */ 113 | #[Test] 114 | public function findAndReplaceInCsvMethodShouldReplaceTextUsingColumnOption(): void 115 | { 116 | $hasReplaced = $this->csvFileHandler->findAndReplaceInCsv("movie.csv", "Inception", "Twilight", "Film"); 117 | 118 | $data = $this->isFileValid('movie.csv'); 119 | $this->assertTrue($hasReplaced); 120 | $this->assertStringContainsString('Twilight', $data); 121 | } 122 | 123 | /** 124 | * @param string $keyword 125 | * @return void 126 | * @throws FileHandlerException 127 | */ 128 | 129 | #[Test] 130 | #[DataProvider('provideMovieNames')] 131 | public function searchByKeyword(string $keyword): void 132 | { 133 | $isMovieAvailable = $this->csvFileHandler->searchInCsvFile( 134 | filename: "movie.csv", 135 | keyword: $keyword, 136 | column: 'Film' 137 | ); 138 | $this->assertTrue($isMovieAvailable); 139 | } 140 | 141 | /** 142 | * @param string $keyword 143 | * @return void 144 | * @throws FileHandlerException 145 | */ 146 | #[Test] 147 | #[DataProvider('provideStudioNames')] 148 | public function searchByStudioName(string $keyword): void 149 | { 150 | $isStudioFound = $this->csvFileHandler->searchInCsvFile( 151 | filename: "movie.csv", 152 | keyword: $keyword, 153 | column: 'Lead Studio' 154 | ); 155 | $this->assertTrue($isStudioFound); 156 | } 157 | 158 | /** 159 | * @return void 160 | * @throws FileHandlerException 161 | */ 162 | #[Test] 163 | public function toArrayMethodReturnsValidArray(): void 164 | { 165 | $data = $this->csvFileHandler->toArray("movie.csv"); 166 | $expected = [ 167 | 'Film' => 'Zack and Miri Make a Porno', 168 | 'Genre' => 'Romance', 169 | 'Lead Studio' => 'The Weinstein Company', 170 | 'Audience score %' => '70', 171 | 'Profitability' => '1.747541667', 172 | 'Rotten Tomatoes %' => '64', 173 | 'Worldwide Gross' => '$41.94 ', 174 | 'Year' => '2008' 175 | 176 | ]; 177 | 178 | $this->assertEquals($expected, $data[0]); 179 | } 180 | 181 | /** 182 | * @param array $columnsToHide 183 | * @param array $expected 184 | * @return void 185 | * @throws FileHandlerException 186 | */ 187 | #[Test] 188 | #[DataProvider('columnsToHideDataProvider')] 189 | public function toArrayMethodWithHideColumnsOptionReturnsValidArray(array $columnsToHide, array $expected): void 190 | { 191 | $data = $this->csvFileHandler->toArray("movie.csv", $columnsToHide); 192 | $this->assertEquals($expected, $data[0]); 193 | } 194 | 195 | /** 196 | * @param int $limit 197 | * @return void 198 | * @throws FileHandlerException 199 | */ 200 | #[Test] 201 | #[DataProvider('limitDataProvider')] 202 | public function toArrayMethodShouldRestrictNumberOfRecordsWhenLimitIsSet(int $limit): void 203 | { 204 | $data = $this->csvFileHandler->toArray("movie.csv", ["Year"], $limit); 205 | 206 | $count = count($data); 207 | 208 | $this->assertSame($count, $limit); 209 | } 210 | 211 | #[Test] 212 | public function searchByKeywordAndReturnArray(): void 213 | { 214 | $expected = [ 215 | 'Film' => 'Zack and Miri Make a Porno', 216 | 'Genre' => 'Romance', 217 | 'Lead Studio' => 'The Weinstein Company', 218 | 'Audience score %' => '70', 219 | 'Profitability' => '1.747541667', 220 | 'Rotten Tomatoes %' => '64', 221 | 'Worldwide Gross' => '$41.94 ', 222 | 'Year' => '2008' 223 | 224 | ]; 225 | 226 | $data = $this->csvFileHandler->searchInCsvFile( 227 | filename: "movie.csv", 228 | keyword: 'Zack and Miri Make a Porno', 229 | column: 'Film', 230 | format: FileHandler::ARRAY_FORMAT 231 | ); 232 | 233 | $this->assertEquals($expected, $data); 234 | } 235 | 236 | 237 | #[Test] 238 | public function toJsonMethodReturnsValidJsonFormat(): void 239 | { 240 | $jsonData = $this->csvFileHandler->toJson("movie.csv"); 241 | if (!$jsonData) { 242 | $this->fail('Could not convert to JSON format'); 243 | } 244 | 245 | $expectedData = '[ 246 | { 247 | "Film": "Zack and Miri Make a Porno", 248 | "Genre": "Romance", 249 | "Lead Studio": "The Weinstein Company", 250 | "Audience score %": "70", 251 | "Profitability": "1.747541667", 252 | "Rotten Tomatoes %": "64", 253 | "Worldwide Gross": "$41.94 ", 254 | "Year": "2008" 255 | }, 256 | { 257 | "Film": "Youth in Revolt", 258 | "Genre": "Comedy", 259 | "Lead Studio": "The Weinstein Company", 260 | "Audience score %": "52", 261 | "Profitability": "1.09", 262 | "Rotten Tomatoes %": "68", 263 | "Worldwide Gross": "$19.62 ", 264 | "Year": "2010" 265 | }, 266 | { 267 | "Film": "Twilight", 268 | "Genre": "Romance", 269 | "Lead Studio": "Independent", 270 | "Audience score %": "68", 271 | "Profitability": "6.383363636", 272 | "Rotten Tomatoes %": "26", 273 | "Worldwide Gross": "$702.17 ", 274 | "Year": "2011" 275 | } 276 | ]'; 277 | 278 | $this->assertJson($jsonData); 279 | $this->assertJsonStringEqualsJsonString($expectedData, $jsonData); 280 | } 281 | 282 | 283 | #[Test] 284 | #[DataProvider('fileProvider')] 285 | public function throwErrorIfFileFormatIsInvalid(string $file): void 286 | { 287 | $message = ($file === 'file1.txt') ? 'invalid csv file format' : 'could not extract header'; 288 | $this->expectException(FileHandlerException::class); 289 | $this->expectExceptionMessage($message); 290 | $this->expectExceptionMessage($message); 291 | 292 | try { 293 | $this->csvFileHandler->searchInCsvFile( 294 | filename: $file, 295 | keyword: 'Twilight', 296 | column: 'Summit' 297 | ); 298 | } finally { 299 | unlink($file); 300 | } 301 | } 302 | 303 | 304 | /** 305 | * @return iterable> 306 | */ 307 | public static function provideStudioNames(): iterable 308 | { 309 | yield ["The Weinstein Company"]; 310 | yield ["Independent"]; 311 | } 312 | 313 | /** 314 | * @return iterable> 315 | */ 316 | public static function provideMovieNames(): iterable 317 | { 318 | yield ["Zack and Miri Make a Porno"]; 319 | yield ["Youth in Revolt"]; 320 | yield ["Twilight"]; 321 | } 322 | 323 | /** 324 | * @return iterable> 325 | */ 326 | 327 | public static function fileProvider(): iterable 328 | { 329 | $file1 = 'file1.txt'; 330 | $file2 = 'file2.txt'; 331 | $file3 = 'file3.txt'; 332 | 333 | file_put_contents($file1, "film,year"); 334 | file_put_contents($file2, "film\nyear"); 335 | file_put_contents($file3, "Film"); 336 | 337 | 338 | yield [$file1]; 339 | yield [$file2]; 340 | yield [$file3]; 341 | } 342 | 343 | /** 344 | * @return iterable> 345 | */ 346 | public static function wrongColumnNameProvider(): iterable 347 | { 348 | yield ["wrong"]; 349 | yield ["honey bee"]; 350 | } 351 | 352 | /** 353 | * @return iterable>> 354 | */ 355 | public static function columnsToHideDataProvider(): iterable 356 | { 357 | $hideSingleColumn = ["Film"]; 358 | $expected1 = [ 359 | 'Genre' => 'Romance', 360 | 'Lead Studio' => 'The Weinstein Company', 361 | 'Audience score %' => '70', 362 | 'Profitability' => '1.747541667', 363 | 'Rotten Tomatoes %' => '64', 364 | 'Worldwide Gross' => '$41.94 ', 365 | 'Year' => '2008' 366 | 367 | ]; 368 | 369 | $hideMultipleColumns = ["Film", "Profitability", "Year"]; 370 | $expected2 = [ 371 | 'Genre' => 'Romance', 372 | 'Lead Studio' => 'The Weinstein Company', 373 | 'Audience score %' => '70', 374 | 'Rotten Tomatoes %' => '64', 375 | 'Worldwide Gross' => '$41.94 ', 376 | 377 | 378 | ]; 379 | 380 | 381 | yield [$hideSingleColumn, $expected1]; 382 | yield [$hideMultipleColumns, $expected2]; 383 | } 384 | 385 | /** 386 | * max limit for the test file is 3 387 | * @return iterable> 388 | */ 389 | public static function limitDataProvider(): iterable 390 | { 391 | yield [1]; 392 | yield [2]; 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /tests/unit/FileEncryptorTest.php: -------------------------------------------------------------------------------- 1 | fileEncryptor = $this->setObjectHandler(FileEncryptor::class, 'file_encryptor'); 21 | } 22 | 23 | protected function tearDown(): void 24 | { 25 | parent::tearDown(); 26 | $this->fileEncryptor = null; 27 | unlink('test'); 28 | } 29 | 30 | /** 31 | * @return void 32 | * @throws FileEncryptorException 33 | * @throws FileHandlerException 34 | * @throws SodiumException 35 | */ 36 | #[Test] 37 | public function throwExceptionOnDecryptingNonEncryptedFile(): void 38 | { 39 | $this->expectException(FileEncryptorException::class); 40 | $this->expectExceptionMessage('file is not encrypted'); 41 | $this->fileEncryptor->decryptFile('movie.csv'); 42 | } 43 | 44 | 45 | /** 46 | * @return void 47 | * @throws FileEncryptorException 48 | */ 49 | #[Test] 50 | public function canEncryptFile(): void 51 | { 52 | $isFileEncrypted = $this->fileEncryptor->encryptFile('movie.csv'); 53 | 54 | $this->assertTrue($isFileEncrypted); 55 | } 56 | 57 | 58 | /** 59 | * @return void 60 | * @throws FileEncryptorException 61 | */ 62 | #[Test] 63 | public function throwExceptionIfAlreadyEncrypted(): void 64 | { 65 | $this->expectException(FileEncryptorException::class); 66 | $this->expectExceptionMessage('file is already encrypted'); 67 | $this->fileEncryptor->encryptFile('movie.csv'); 68 | } 69 | 70 | 71 | /** 72 | * @return void 73 | * @throws FileEncryptorException 74 | */ 75 | #[Test] 76 | public function throwExceptionIfFileHasNoContentWhileEncrypt(): void 77 | { 78 | file_put_contents("test", ""); 79 | $this->expectException(FileEncryptorException::class); 80 | $this->expectExceptionMessage('File has no content'); 81 | $this->fileEncryptor->encryptFile('test'); 82 | } 83 | 84 | 85 | #[Test] 86 | public function throwExceptionIfCouldNotConvertHexToBin(): void 87 | { 88 | $this->expectException(FileEncryptorException::class); 89 | $this->expectExceptionMessage('could not convert hex to bin'); 90 | $this->fileEncryptor->convertHexToBin('hello'); 91 | } 92 | 93 | /** 94 | * @return void 95 | * @throws FileEncryptorException 96 | * @throws SodiumException 97 | * @throws FileHandlerException 98 | */ 99 | #[Test] 100 | public function throwExceptionIfFileHasNoContent(): void 101 | { 102 | file_put_contents("test", ""); 103 | $this->expectException(FileEncryptorException::class); 104 | $this->expectExceptionMessage('File has no content'); 105 | $this->fileEncryptor->decryptFile('test'); 106 | } 107 | 108 | 109 | /** 110 | * @return void 111 | * @throws FileEncryptorException 112 | * @throws FileHandlerException 113 | * @throws SodiumException 114 | */ 115 | #[Test] 116 | public function throwExceptionIfDecryptionFails(): void 117 | { 118 | $filePath = '.env'; 119 | $originalContent = file_get_contents($filePath); 120 | if (!$originalContent) { 121 | $this->fail('file not found'); 122 | } 123 | $password = $_ENV[FileEncryptor::ENCRYPT_PASSWORD]; 124 | $updatedContent = str_replace($password, 'pass', $originalContent); 125 | 126 | file_put_contents($filePath, $updatedContent); 127 | try { 128 | $this->expectException(FileEncryptorException::class); 129 | $this->expectExceptionMessage('could not decrypt file'); 130 | $this->fileEncryptor->decryptFile('movie.csv'); 131 | } finally { 132 | file_put_contents($filePath, $originalContent); 133 | } 134 | } 135 | 136 | /** 137 | * @return void 138 | * @throws FileEncryptorException 139 | * @throws FileHandlerException 140 | * @throws SodiumException 141 | */ 142 | #[Test] 143 | public function canDecryptFile(): void 144 | { 145 | $isFileDecrypted = $this->fileEncryptor->decryptFile('movie.csv'); 146 | 147 | $this->assertTrue($isFileDecrypted); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/unit/FileHandlerTest.php: -------------------------------------------------------------------------------- 1 | fileHandler = $this->setObjectHandler(FileHandler::class, 'file_handler'); 20 | } 21 | 22 | public static function setUpBeforeClass(): void 23 | { 24 | parent::setUpBeforeClass(); 25 | static::$files = ["movie.csv", "file", 'file1', 'unknown_mime_type']; 26 | } 27 | 28 | public static function tearDownAfterClass(): void 29 | { 30 | parent::tearDownAfterClass(); 31 | } 32 | 33 | protected function tearDown(): void 34 | { 35 | parent::tearDown(); 36 | $this->fileHandler->resetFiles(); 37 | $this->fileHandler = null; 38 | } 39 | 40 | /** 41 | * @return void 42 | * @throws FileHandlerException 43 | */ 44 | #[Test] 45 | public function fileSuccessfullyWritten(): void 46 | { 47 | $this->fileHandler->open(filename: 'file'); 48 | 49 | $this->fileHandler->write(data: "hello world"); 50 | 51 | $this->assertEquals(expected: "hello world", actual: file_get_contents(filename: 'file')); 52 | } 53 | 54 | /** 55 | * @return void 56 | * @throws FileHandlerException 57 | */ 58 | #[Test] 59 | public function shouldThrowExceptionIfFileIsNotFound(): void 60 | { 61 | $this->expectException(FileHandlerException::class); 62 | $this->expectExceptionMessage('File not found'); 63 | $this->fileHandler->open(filename: 'unknown', mode: "r"); 64 | } 65 | 66 | 67 | /** 68 | * @return void 69 | * @throws FileHandlerException 70 | */ 71 | #[Test] 72 | public function shouldThrowExceptionIfFileIsNotWritable(): void 73 | { 74 | $this->fileHandler->open(filename: 'file', mode: 'r'); 75 | 76 | $this->expectException(FileHandlerException::class); 77 | $this->expectExceptionMessage('Error writing to file'); 78 | $this->fileHandler->write(data: "hello world"); 79 | $this->fileHandler->close(); 80 | } 81 | 82 | /** 83 | * @return void 84 | * @throws FileHandlerException 85 | */ 86 | #[Test] 87 | public function successfulCompression(): void 88 | { 89 | $testFile = 'movie.csv'; 90 | $compressedZip = 'compressed.zip'; 91 | 92 | $this->fileHandler->compress($testFile, $compressedZip); 93 | 94 | $mimeType = $this->fileHandler->getMimeType($compressedZip); 95 | 96 | $this->assertFileExists($compressedZip); 97 | $this->assertEquals('application/zip', $mimeType); 98 | } 99 | 100 | /** 101 | * @return void 102 | * @throws FileHandlerException 103 | */ 104 | #[Test] 105 | public function shouldThrowExceptionIfZipArchiveIsUnableToCreateWhileDecompress(): void 106 | { 107 | $testFile = 'movie.csv'; 108 | $compressedZip = 'compressed.zip'; 109 | 110 | $this->fileHandler->compress($testFile, $compressedZip); 111 | 112 | $this->expectException(FileHandlerException::class); 113 | $this->expectExceptionMessage('Invalid or uninitialized Zip object'); 114 | $this->fileHandler->decompress(zipFilename: 'compressed.zip', flag: 10); 115 | } 116 | 117 | /** 118 | * @return void 119 | * @throws FileHandlerException 120 | */ 121 | #[Test] 122 | public function shouldThrowExceptionIfZipArchiveHasInvalidExtractPathWhileDecompress(): void 123 | { 124 | $testFile = 'movie.csv'; 125 | $compressedZip = 'compressed.zip'; 126 | 127 | $this->fileHandler->compress($testFile, $compressedZip); 128 | 129 | $this->expectException(FileHandlerException::class); 130 | $this->expectExceptionMessage('Failed to extract the ZIP archive.'); 131 | $this->fileHandler->decompress(zipFilename: 'compressed.zip', extractPath: '/abcd'); 132 | } 133 | 134 | #[Test] 135 | public function shouldThrowExceptionIfZipArchiveIsUnableToCreate(): void 136 | { 137 | $testFile = 'movie.csv'; 138 | $compressedZip = 'compressed.zip'; 139 | $this->expectException(FileHandlerException::class); 140 | $this->expectExceptionMessage('Failed to create the ZIP archive.'); 141 | $this->fileHandler->compress($testFile, $compressedZip, ZipArchive::ER_EXISTS); 142 | } 143 | 144 | /** 145 | * @return void 146 | * @throws FileHandlerException 147 | */ 148 | 149 | #[Test] 150 | public function getMimeTypeThrowsErrorIfMimeTypeIsUnrecognised(): void 151 | { 152 | file_put_contents("unknown_mime_type", "%%"); 153 | 154 | $this->expectException(FileHandlerException::class); 155 | $this->expectExceptionMessage('unknown mime type'); 156 | $this->fileHandler->getMimeType('unknown_mime_type'); 157 | } 158 | 159 | 160 | /** 161 | * @return void 162 | * @throws FileHandlerException 163 | */ 164 | #[Test] 165 | public function getMimeTypeFunctionReturnsCorrectInfo(): void 166 | { 167 | $csvFile = $this->fileHandler->getMimeType("movie.csv"); 168 | $zipFile = $this->fileHandler->getMimeType("compressed.zip"); 169 | 170 | $this->assertEquals("text/csv", $csvFile); 171 | $this->assertEquals('application/zip', $zipFile); 172 | } 173 | 174 | /** 175 | * @return void 176 | * @throws FileHandlerException 177 | */ 178 | #[Test] 179 | public function successfulDecompression(): void 180 | { 181 | $compressedZip = 'compressed.zip'; 182 | $extractPath = 'extracted_contents'; 183 | 184 | $this->fileHandler->decompress($compressedZip, $extractPath); 185 | 186 | $expectedContent = <<assertEquals($expectedContent, file_get_contents("./extracted_contents/movie.csv")); 194 | 195 | 196 | unlink($compressedZip); 197 | unlink("./extracted_contents/movie.csv"); 198 | rmdir($extractPath); 199 | } 200 | 201 | /** 202 | * @return void 203 | * @throws FileHandlerException 204 | */ 205 | #[Test] 206 | public function fileIsClosedProperly(): void 207 | { 208 | $this->fileHandler->open(filename: 'file'); 209 | $this->fileHandler->write(data: "hello world"); 210 | $this->fileHandler->close(); 211 | 212 | $this->expectException(FileHandlerException::class); 213 | $this->expectExceptionMessage('no files available to write'); 214 | $this->fileHandler->write(data: "hello"); 215 | } 216 | 217 | /** 218 | * @return void 219 | * @throws FileHandlerException 220 | */ 221 | #[Test] 222 | public function multipleFileCanBeWrittenSimultaneously(): void 223 | { 224 | $this->fileHandler->open(filename: 'file'); 225 | 226 | $this->fileHandler->open(filename: 'file1'); 227 | 228 | $this->fileHandler->write(data: "hello world"); 229 | 230 | $this->assertEquals("hello world", file_get_contents(filename: 'file')); 231 | 232 | $this->assertEquals("hello world", file_get_contents(filename: 'file1')); 233 | $this->fileHandler->close(); 234 | } 235 | 236 | /** 237 | * @return void 238 | * @throws FileHandlerException 239 | */ 240 | #[Test] 241 | public function checkFilesAreDeletedProperly(): void 242 | { 243 | file_put_contents("deleteFile", ""); 244 | $this->fileHandler->delete("deleteFile"); 245 | 246 | if (!file_exists("deleteFile")) { 247 | $this->assertTrue(true); 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /tests/unit/FileHashCheckerTest.php: -------------------------------------------------------------------------------- 1 | fileHash = $this->setObjectHandler(FileHashChecker::class, 'file_hash'); 21 | } 22 | 23 | protected function tearDown(): void 24 | { 25 | parent::tearDown(); 26 | $this->fileHash = null; 27 | } 28 | 29 | public static function setUpBeforeClass(): void 30 | { 31 | parent::setUpBeforeClass(); 32 | 33 | static::$files[] = 'sample'; 34 | static::$files[] = 'headers'; 35 | static::$files[] = 'invalid'; 36 | } 37 | 38 | 39 | /** 40 | * @throws HashException 41 | * @throws FileHandlerException 42 | */ 43 | #[Test] 44 | public function shouldGenerateValidHashForDifferentAlgo(): void 45 | { 46 | $expectedHash = "5923032f7e18edf69e1a3221be3205ce658ec0e4fb274016212a09a804240683"; 47 | 48 | $actualHash = $this->fileHash->hashFile(filename: 'movie.csv'); //default ALGO_256 49 | 50 | $this->assertEquals($expectedHash, $actualHash); 51 | 52 | $expectedHash = "1050bcc2d7d840d634f067a22abb4cd693b1f2590849982e29a6f9bb28963f733" . 53 | "92b63ea24ae17edfaa500ee62b9e5482b9648af0b2b7d941992af3b0f9cbd3b"; 54 | 55 | $actualHash = $this->fileHash->hashFile(filename: 'movie.csv', algo: FileHashChecker::ALGO_512); 56 | 57 | $this->assertEquals($expectedHash, $actualHash); 58 | } 59 | 60 | /** 61 | * @throws HashException 62 | * @throws FileHandlerException 63 | */ 64 | #[Test] 65 | public function checkFileIntegrityReturnsTrueIfHashMatch(): void 66 | { 67 | $isVerified = $this->fileHash->verifyHash(filename: 'movie.csv'); 68 | 69 | $this->assertTrue($isVerified); 70 | } 71 | 72 | /** 73 | * @throws HashException 74 | * @throws FileHandlerException 75 | */ 76 | #[Test] 77 | public function shouldReturnFalseIfFileIsModified(): void 78 | { 79 | $backup = file_get_contents("movie.csv"); 80 | file_put_contents("movie.csv", "modified", FILE_APPEND); 81 | 82 | $isVerified = $this->fileHash->verifyHash('movie.csv'); 83 | 84 | $this->assertfalse($isVerified); 85 | 86 | file_put_contents("movie.csv", $backup); 87 | } 88 | 89 | /** 90 | * @return void 91 | * @throws FileHandlerException 92 | * @throws HashException 93 | */ 94 | 95 | #[Test] 96 | public function verifyHashMethodThrowsExceptionIfHashRecordNotFound(): void 97 | { 98 | $this->expectException(HashException::class); 99 | $this->expectExceptionMessage('this file is not hashed'); 100 | $isVerified = $this->fileHash->verifyHash(filename: 'movie'); 101 | 102 | $this->assertTrue($isVerified); 103 | } 104 | 105 | /** 106 | * @return void 107 | * @throws FileHandlerException 108 | * @throws HashException 109 | */ 110 | #[Test] 111 | public function verifyHashMethodThrowsExceptionIfInvalidFileProvided(): void 112 | { 113 | file_put_contents("invalid", ""); 114 | chmod("invalid", 0000); 115 | $this->expectException(HashException::class); 116 | $this->expectExceptionMessage('could not hash file'); 117 | $this->fileHash->hashFile(filename: 'invalid'); 118 | } 119 | 120 | /** 121 | * @return void 122 | * @throws FileHandlerException 123 | * @throws HashException 124 | */ 125 | #[Test] 126 | public function hashFileMethodThrowExceptionIfEnvVarIsNotFound(): void 127 | { 128 | $this->expectException(FileHandlerException::class); 129 | $this->expectExceptionMessage('file not found'); 130 | $this->fileHash->hashFile(filename: 'movie.csv', env: 'INVALID_ENV_VAR'); 131 | } 132 | 133 | 134 | /** 135 | * @return void 136 | * @throws FileHandlerException 137 | * @throws HashException 138 | */ 139 | #[Test] 140 | public function hashFileMethodThrowExceptionIfInvalidCsvProvided(): void 141 | { 142 | $storedHashFile = self::$containerBuilder->getParameter('STORED_HASH_FILE'); 143 | 144 | if (!is_string($storedHashFile)) { 145 | $this->fail('param must be a string type'); 146 | } 147 | $backUpFile = file_get_contents($storedHashFile); 148 | 149 | 150 | file_put_contents($storedHashFile, "File,Hash"); 151 | $this->fileHash->hashFile(filename: 'movie.csv'); 152 | 153 | $content = file_get_contents($storedHashFile); 154 | if (!$content) { 155 | $this->fail('file has no content'); 156 | } 157 | $this->assertStringContainsString("movie.csv", $content); 158 | 159 | file_put_contents($storedHashFile, $backUpFile); 160 | } 161 | 162 | /** 163 | * @return void 164 | */ 165 | 166 | #[Test] 167 | public function checkHeaderExistIfNotShouldCreateOne(): void 168 | { 169 | file_put_contents("headers", ""); 170 | $file = fopen("headers", 'w'); 171 | $this->fileHash->checkHeaderExists($file); 172 | $content = file_get_contents("headers"); 173 | if (!$content) { 174 | $this->fail('file has no content'); 175 | } 176 | $this->assertStringContainsString('File,Hash', $content); 177 | } 178 | 179 | /** 180 | * @throws HashException 181 | * @throws FileHandlerException 182 | */ 183 | #[Test] 184 | public function shouldReturnFalseIfDifferentAlgoIsUsedForVerifyHash(): void 185 | { 186 | $isVerified = $this->fileHash->verifyHash('movie.csv', FileHashChecker::ALGO_512); 187 | 188 | $this->assertFalse($isVerified); 189 | } 190 | 191 | /** 192 | * @throws HashException 193 | * @throws FileHandlerException 194 | */ 195 | #[Test] 196 | public function shouldAddRecordIfNewFileIsHashed(): void 197 | { 198 | file_put_contents('sample', "hello"); 199 | 200 | $this->fileHash->hashFile('sample', FileHashChecker::ALGO_512); 201 | 202 | $isVerified = $this->fileHash->verifyHash('sample', FileHashChecker::ALGO_512); 203 | 204 | $this->assertTrue($isVerified); 205 | } 206 | 207 | /** 208 | * @return void 209 | * @throws FileHandlerException 210 | * @throws HashException 211 | */ 212 | #[Test] 213 | public function shouldThrowExceptionIfInvalidAlgoProvided(): void 214 | { 215 | $this->expectException(HashException::class); 216 | $this->expectExceptionMessage('algorithm not supported'); 217 | $this->fileHash->hashFile('sample', 'invalid'); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /tests/unit/FileValidatorTest.php: -------------------------------------------------------------------------------- 1 | expectException(FileHandlerException::class); 21 | $this->expectExceptionMessage("path {$path} is not valid"); 22 | $this->validateFileName($filename, $path); 23 | } 24 | 25 | 26 | #[Test] 27 | public function shouldThrowExceptionIfFileNameContainsIllegalCharacter(): void 28 | { 29 | $this->expectException(FileHandlerException::class); 30 | $this->expectExceptionMessage("file not found"); 31 | $this->validateFileName('@#$'); 32 | } 33 | 34 | /** 35 | * @return void 36 | */ 37 | #[Test] 38 | public function checkFileIsRestricted(): void 39 | { 40 | $filename = self::$containerBuilder->getParameter('STORED_HASH_FILE'); 41 | if (!is_string($filename)) { 42 | $this->fail('expected string type'); 43 | } 44 | $isFileRestricted = $this->isFileRestricted($filename, self::STORED_HASH_FILE); 45 | $this->assertTrue($isFileRestricted); 46 | } 47 | 48 | #[Test] 49 | public function getParamMethodValidateForStringType(): void 50 | { 51 | $container = self::$containerBuilder; 52 | $container->setParameter('arr', []); 53 | $this->expectException(FileHandlerException::class); 54 | $this->expectExceptionMessage("arr is not string type"); 55 | $this->getParam('arr', $container); 56 | } 57 | 58 | #[Test] 59 | public function throwExceptionIfFileFailedToOpen(): void 60 | { 61 | $this->expectException(FileHandlerException::class); 62 | $this->expectExceptionMessage("file is not valid"); 63 | $this->openFileAndReturnResource('movie.csv', 'f'); 64 | } 65 | 66 | 67 | /** 68 | * @throws FileHandlerException 69 | */ 70 | #[Test] 71 | public function shouldNotThrowExceptionIfFileExists(): void 72 | { 73 | $filename = "sample"; 74 | $path = __DIR__; 75 | 76 | file_put_contents($path . '/' . $filename, ''); 77 | $this->expectNotToPerformAssertions(); 78 | $this->validateFileName($filename, $path); 79 | unlink($path . '/' . $filename); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/unit/JsonFileHandlerTest.php: -------------------------------------------------------------------------------- 1 | >>> 46 | */ 47 | public static function bookListProvider(): iterable 48 | { 49 | yield 50 | [ 51 | 52 | [ 53 | [ 54 | 'title' => 'The Catcher in the Rye', 55 | 'author' => 'J.D. Salinger', 56 | 'published_year' => '1951' 57 | ], 58 | [ 59 | 'title' => 'To Kill a Mockingbird', 60 | 'author' => 'Harper Lee', 61 | 'published_year' => '1960' 62 | ], 63 | [ 64 | 'title' => '1984', 65 | 'author' => 'George Orwell', 66 | 'published_year' => '1949' 67 | ], 68 | 69 | 70 | ] 71 | 72 | ]; 73 | } 74 | 75 | /** 76 | * @param array> $book 77 | * @return void 78 | * @throws FileHandlerException 79 | */ 80 | #[Test] 81 | #[DataProvider('bookListProvider')] 82 | public function jsonFormatToArray(array $book): void 83 | { 84 | $data = $this->jsonFileHandler->toArray('book.json'); 85 | 86 | $this->assertSame(3, count($data)); 87 | $this->assertEquals($data, $book); 88 | } 89 | 90 | #[Test] 91 | public function throwExceptionIfFileIsNotFound(): void 92 | { 93 | $this->expectException(FileHandlerException::class); 94 | $this->expectExceptionMessage('abc is not valid'); 95 | $this->jsonFileHandler->getValidJsonData('abc'); 96 | } 97 | 98 | #[Test] 99 | public function throwExceptionIfFileIsDoesNotContainValidJson(): void 100 | { 101 | file_put_contents("sample", "hello"); 102 | $this->expectException(FileHandlerException::class); 103 | $this->expectExceptionMessage('could not decode json'); 104 | $this->jsonFileHandler->getValidJsonData('sample'); 105 | unlink('sample'); 106 | } 107 | 108 | #[Test] 109 | public function throwExceptionIfJsonIsNotAList(): void 110 | { 111 | $content = '[ 112 | { 113 | "title": "The Catcher in the Rye", 114 | "author": "J.D. Salinger", 115 | "published_year": 1951 116 | }, 117 | { 118 | "title": "To Kill a Mockingbird", 119 | "author": "Harper Lee", 120 | "published_year": 1960 121 | }, 122 | { 123 | "titles": "1984", 124 | "authors": "George Orwell", 125 | "published_year": 1949 126 | } 127 | ]'; 128 | file_put_contents("sample", $content); 129 | $this->expectException(FileHandlerException::class); 130 | $this->expectExceptionMessage('Inconsistent JSON data'); 131 | $this->jsonFileHandler->getValidJsonData('sample'); 132 | } 133 | 134 | /** 135 | * @return void 136 | * @throws FileHandlerException 137 | */ 138 | #[Test] 139 | public function setLimitWorkingProperly(): void 140 | { 141 | $headers = []; 142 | $data = $this->jsonFileHandler->getRows(filename: 'book.json', headers: $headers, limit: 2); 143 | 144 | $data = iterator_to_array($data); 145 | 146 | $this->assertSame(2, count($data)); 147 | } 148 | 149 | /** 150 | * @return void 151 | * @throws FileHandlerException 152 | */ 153 | #[Test] 154 | public function setHideColumnWorkingProperly(): void 155 | { 156 | $headers = []; 157 | $data = $this->jsonFileHandler->getRows( 158 | filename: 'book.json', 159 | headers: $headers, 160 | hideColumns: ['title'], 161 | limit: 1 162 | ); 163 | $data = iterator_to_array($data); 164 | 165 | if (in_array('title', $headers)) { 166 | $this->fail('hide column is not working properly'); 167 | } 168 | 169 | $count = count($data[0]); 170 | 171 | $this->assertTrue(true); 172 | $this->assertSame(2, $count); 173 | } 174 | 175 | 176 | protected function setUp(): void 177 | { 178 | parent::setUp(); 179 | $this->jsonFileHandler = new JsonFileHandler(); 180 | } 181 | 182 | protected function tearDown(): void 183 | { 184 | parent::tearDown(); 185 | 186 | $this->jsonFileHandler = null; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/unit/TempFileHandlerTest.php: -------------------------------------------------------------------------------- 1 | tempFileHandler->writeRowToTempFile($tempFilePath, []); 26 | 27 | $this->tempFileHandler->renameTempFile($tempFilePath, $newFileName); 28 | 29 | $this->assertFileExists($newFileName); 30 | $this->assertFileDoesNotExist($tempFilePath); 31 | 32 | unlink($newFileName); 33 | } 34 | 35 | #[Test] 36 | public function writeRowToTempFile(): void 37 | { 38 | $tempFilePath = 'tempfile.txt'; 39 | $row = ['data', 'to', 'write']; 40 | 41 | 42 | $this->tempFileHandler->writeRowToTempFile($tempFilePath, $row); 43 | 44 | $fileContents = file_get_contents($tempFilePath); 45 | $expectedContents = implode(',', $row) . PHP_EOL; 46 | 47 | $this->assertEquals($expectedContents, $fileContents); 48 | 49 | unlink($tempFilePath); 50 | } 51 | 52 | #[Test] 53 | public function cleanupTempFile(): void 54 | { 55 | $tempFilePath = 'tempfile.txt'; 56 | 57 | 58 | $this->tempFileHandler->writeRowToTempFile($tempFilePath, []); 59 | 60 | $this->tempFileHandler->cleanupTempFile($tempFilePath); 61 | 62 | $this->assertFileDoesNotExist($tempFilePath); 63 | } 64 | 65 | #[Test] 66 | public function getTempNameReturnFalseOnWrongValue(): void 67 | { 68 | $isTempFileValid = $this->tempFileHandler->createTempFileWithHeaders([], 'abcd', 'abcd'); 69 | 70 | $this->assertFalse($isTempFileValid); 71 | } 72 | 73 | 74 | #[Test] 75 | public function createTempFileWithHeaders(): void 76 | { 77 | $headers = ['header1', 'header2', 'header3']; 78 | 79 | $tempFilePath = $this->tempFileHandler->createTempFileWithHeaders($headers); 80 | if (!$tempFilePath) { 81 | $this->fail('could not generate temp file with header'); 82 | } 83 | 84 | $fileContents = file_get_contents($tempFilePath); 85 | $expectedContents = implode(',', $headers) . PHP_EOL; 86 | 87 | $this->assertEquals($expectedContents, $fileContents); 88 | 89 | unlink($tempFilePath); 90 | } 91 | 92 | /** 93 | * @return void 94 | * @throws FileHandlerException 95 | */ 96 | #[Test] 97 | public function throwsExceptionIfRenameFails(): void 98 | { 99 | $this->expectException(FileHandlerException::class); 100 | $this->expectExceptionMessage('Failed to rename temp file'); 101 | 102 | $this->tempFileHandler->renameTempFile('', ''); 103 | } 104 | 105 | 106 | protected function setUp(): void 107 | { 108 | parent::setUp(); 109 | 110 | $this->tempFileHandler = new TempFileHandler(); 111 | } 112 | 113 | protected function tearDown(): void 114 | { 115 | parent::tearDown(); 116 | $this->tempFileHandler = null; 117 | } 118 | } 119 | --------------------------------------------------------------------------------