├── docker └── php │ ├── conf.d │ ├── error_reporting.ini │ └── xdebug.ini │ ├── docker-entrypoint.sh │ └── Dockerfile.php8 ├── tests ├── bootstrap.php ├── data │ ├── valid_test.csv │ ├── ascii_test.csv │ ├── between_test.csv │ ├── url_test.csv │ ├── valid_test_expected.json │ ├── valid_test_expected.xml │ └── valid_test_param_expected.xml └── src │ ├── UppercaseRule.php │ ├── CsvValidatorParserTest.php │ └── CsvValidatorTest.php ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── branch.yml ├── sample └── sample.csv ├── docker-compose.yml ├── src ├── Contracts │ ├── ParameterizedRuleInterface.php │ ├── ConverterHandlerInterface.php │ └── ValidationRuleInterface.php ├── Rules │ ├── AsciiOnly.php │ ├── Between.php │ ├── ClosureValidationRule.php │ └── Url.php ├── Converter │ ├── JsonConverter.php │ └── XmlConverter.php ├── Validator │ ├── ValidationRuleParser.php │ └── Validator.php └── Helpers │ └── FormatsMessages.php ├── .php-cs-fixer.dist.php ├── index.php ├── phpunit.xml ├── Makefile ├── composer.json ├── LICENSE └── README.md /docker/php/conf.d/error_reporting.ini: -------------------------------------------------------------------------------- 1 | error_reporting=E_ALL 2 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Well Health Hotels 5 |
Inga N. P.O. Box 567
6 | 3 7 | Kasper Zen 8 | http://well.org 9 |
10 |
11 | -------------------------------------------------------------------------------- /tests/data/valid_test_param_expected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Well Health Hotels 5 |
Inga N. P.O. Box 567
6 | 3 7 | Kasper Zen 8 | http://well.org 9 |
10 |
11 | -------------------------------------------------------------------------------- /sample/sample.csv: -------------------------------------------------------------------------------- 1 | name,stars,uri 2 | Beni Gold Hotel and Apartments,5,https://hotels.ng/hotel/86784-benigold-hotel-lagos 3 | Hotel Ibis Lagos Ikeja,4,https://hotels.ng/hotel/52497-hotel-ibis-lagos-ikeja-lagos 4 | Silver Grandeur Hotel,3,https://hotels.ng/hotel/88244-silver-grandeur-hotel-lagos 5 | "Limeridge Hotel, Lekki",2,https://hotels.ng/hotel/65735-limeridge-hotel-lagos -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | php: 3 | build: 4 | context: ./docker/php/ 5 | dockerfile: Dockerfile.php8 6 | volumes: 7 | - .:/var/www/html 8 | - ./docker/php/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini 9 | - ./docker/php/conf.d/error_reporting.ini:/usr/local/etc/php/conf.d/error_reporting.ini 10 | command: develop 11 | -------------------------------------------------------------------------------- /src/Contracts/ParameterizedRuleInterface.php: -------------------------------------------------------------------------------- 1 | exclude([ 5 | 'vendor', 6 | ]) 7 | ->in(__DIR__) 8 | ; 9 | 10 | $config = new PhpCsFixer\Config(); 11 | 12 | return $config->setRiskyAllowed(true) 13 | ->setRules([ 14 | '@Symfony' => true, 15 | 'concat_space' => ['spacing' => 'one'], 16 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 17 | ]) 18 | ->setFinder($finder) 19 | ->setUsingCache(false); 20 | 21 | -------------------------------------------------------------------------------- /tests/src/UppercaseRule.php: -------------------------------------------------------------------------------- 1 | ['between:7,10'], 15 | 'name' => ['ascii_only'], 16 | 'uri' => ['url', function ($value, $fail) { 17 | if (0 !== strpos($value, 'https://')) { 18 | return $fail('The URL passed must be https i.e it must start with https://'); 19 | } 20 | }], 21 | ] 22 | ); 23 | 24 | if (!$validator->fails()) { 25 | $validator->write(new JsonConverter()); 26 | $validator->write(new XmlConverter()); 27 | } else { 28 | $validator->write(new JsonConverter()); 29 | $validator->write(new XmlConverter('hotel')); 30 | print_r($validator->errors()); 31 | } 32 | -------------------------------------------------------------------------------- /src/Converter/JsonConverter.php: -------------------------------------------------------------------------------- 1 | data = json_encode( 26 | $data, 27 | JSON_PRETTY_PRINT | 28 | JSON_NUMERIC_CHECK | 29 | JSON_UNESCAPED_SLASHES | 30 | JSON_UNESCAPED_UNICODE 31 | ); 32 | 33 | return $this; 34 | } 35 | 36 | public function write(string $filename): bool 37 | { 38 | return (bool) file_put_contents($filename, $this->data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | src 26 | 27 | vendor 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Rules/Between.php: -------------------------------------------------------------------------------- 1 | = +$min && $convertedValue <= +$max; 28 | } 29 | 30 | if (is_string($value)) { 31 | $valueLength = strlen($value); 32 | 33 | return $valueLength >= +$min && $valueLength <= +$max; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | /** 40 | * Get the validation error message. 41 | */ 42 | public function message(): string 43 | { 44 | return 'The :attribute value :value is not between :min - :max on line :line.'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Rules/ClosureValidationRule.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 32 | } 33 | 34 | /** 35 | * Determine if the validation rule passes. 36 | */ 37 | public function passes($value, array $parameters, array $row): bool 38 | { 39 | $this->callback->__invoke($value, function ($message) { 40 | $this->failed = true; 41 | 42 | $this->message = $message; 43 | }); 44 | 45 | return !$this->failed; 46 | } 47 | 48 | /** 49 | * Get the validation error message. 50 | */ 51 | public function message(): string 52 | { 53 | return $this->message; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/branch.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: '8.1' 24 | tools: composer:v2 25 | coverage: xdebug 26 | 27 | - name: Validate composer.json and composer.lock 28 | run: composer validate --strict 29 | 30 | - name: Cache Composer packages 31 | id: composer-cache 32 | uses: actions/cache@v3 33 | with: 34 | path: vendor 35 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-php- 38 | 39 | - name: Install dependencies 40 | run: composer install --prefer-dist --no-progress 41 | 42 | - name: Run test suite 43 | run: composer run-script php-cs-fixer-check && composer run-script test 44 | 45 | - name: Upload coverage to Codecov 46 | uses: codecov/codecov-action@v4 47 | with: 48 | files: ./build/logs/clover.xml 49 | fail_ci_if_error: true 50 | env: 51 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UID ?= $(shell id -u) 2 | GID ?= $(shell id -g) 3 | 4 | help: 5 | @echo "\e[32m Usage make [target] " 6 | @echo 7 | @echo "\e[1m targets:" 8 | @egrep '^(.+)\:\ ##\ (.+)' ${MAKEFILE_LIST} | column -t -c 2 -s ':#' 9 | 10 | clean: ## Clean everything 11 | clean: clean-docker clean-logs clean-cache clean-dependencies 12 | .PHONY: clean 13 | 14 | clean-docker: ## Remove images, volumes, containers 15 | # Not implemented 16 | .PHONY: clean-docker 17 | 18 | clean-logs: ## Clean all log files 19 | # Not implemented 20 | .PHONY: clean-logs 21 | 22 | clean-cache: ## Clean local caches 23 | # Not implemented 24 | .PHONY: clean-cache 25 | 26 | clean-dependencies: ## Clean dev dependencies 27 | # Not implemented 28 | .PHONY: clean-dependencies 29 | 30 | build: ## Build PHP7 container 31 | # Hint: force a rebuild by passing --no-cache 32 | @UID=$(UID) GID=$(GID) docker-compose build --no-cache php 33 | .PHONY: install-web 34 | 35 | stop: ## Stop running containers 36 | @UID=$(UID) GID=$(GID) docker-compose stop 37 | .PHONY: stop 38 | 39 | shell: ## Start an interactive shell session for PHP7 container 40 | # Hint: adjust UID and GID to 0 if you want to use the shell as root 41 | @UID=$(UID) GID=$(GID) docker-compose run --rm -w /var/www/html -e SHELL_VERBOSITY=1 php bash 42 | .PHONY: shell 43 | 44 | watch-logs: ## Open a tail on all the logs 45 | @UID=$(UID) GID=$(GID) docker-compose logs -f -t 46 | .PHONY: watch-logs 47 | 48 | .DEFAULT_GOAL := help 49 | -------------------------------------------------------------------------------- /tests/src/CsvValidatorParserTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 15 | [$customRule, []], 16 | ValidationRuleParser::parse($customRule) 17 | ); 18 | } 19 | 20 | public function testWhenOtherRulesArePassed() 21 | { 22 | $this->assertSame( 23 | ['AsciiOnly', []], 24 | ValidationRuleParser::parse('ascii_only') 25 | ); 26 | } 27 | 28 | public function testWhenRulesAcceptParameters() 29 | { 30 | $this->assertSame( 31 | ['Between', ['1', '3']], 32 | ValidationRuleParser::parse('between:1,3') 33 | ); 34 | } 35 | 36 | public function testWhenTupleRuleWithStringParametersIsPassed() 37 | { 38 | $this->assertSame( 39 | ['Between', ['1', '3']], 40 | ValidationRuleParser::parse(['between', '1, 3']) 41 | ); 42 | } 43 | 44 | public function testWhenTupleRuleWithArrayParametersIsPassed() 45 | { 46 | $this->assertSame( 47 | ['Between', ['1', '3']], 48 | ValidationRuleParser::parse(['between', ['1', '3']]) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oshomo/csv-utils", 3 | "description": "A CSV utility to read, validate and write data to multiple format including JSON, XML etc.", 4 | "keywords": ["library", "csv-validator", "php", "php-csv-validator", "csv-converter", "csv-reading", "csv-json-converter", "csv-xml-converter"], 5 | "license": "Apache-2.0", 6 | "type": "library", 7 | "authors": [ 8 | { 9 | "name": "Oshomo Oforomeh", 10 | "email": "oshomo.oforomeh2010@gmail.com" 11 | } 12 | ], 13 | "minimum-stability": "dev", 14 | "require": { 15 | "php": ">=8.1", 16 | "ext-mbstring": "*", 17 | "ext-json": "*", 18 | "ext-simplexml": "*", 19 | "ext-dom": "*" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^9.0", 23 | "friendsofphp/php-cs-fixer": "^3.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Oshomo\\CsvUtils\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Oshomo\\CsvUtils\\Tests\\": "tests/" 33 | } 34 | }, 35 | "scripts": { 36 | "phpunit": "phpunit", 37 | "php-cs-fixer-check": "php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --dry-run --stop-on-violation --using-cache=no", 38 | "php-cs-fixer": "php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no", 39 | "fix-lint": [ 40 | "@php-cs-fixer" 41 | ], 42 | "test-setup": [ 43 | "php -v", 44 | "@php-cs-fixer-check" 45 | ], 46 | "test": [ 47 | "@phpunit" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Converter/XmlConverter.php: -------------------------------------------------------------------------------- 1 | recordElement = $recordElement; 32 | } 33 | 34 | $this->data = new \SimpleXMLElement(''); 35 | } 36 | 37 | public function getExtension(): string 38 | { 39 | return self::FILE_EXTENSION; 40 | } 41 | 42 | protected function toXml(array $data, \SimpleXMLElement $xmlData): void 43 | { 44 | foreach ($data as $key => $value) { 45 | if (is_numeric($key)) { 46 | $key = $this->recordElement; 47 | } 48 | if (is_array($value)) { 49 | $subNode = $xmlData->addChild($key); 50 | $this->toXml($value, $subNode); 51 | } else { 52 | $xmlData->addChild("$key", htmlspecialchars("$value")); 53 | } 54 | } 55 | } 56 | 57 | public function convert(array $data): ConverterHandlerInterface 58 | { 59 | $this->toXml($data, $this->data); 60 | 61 | return $this; 62 | } 63 | 64 | public function write(string $filename): bool 65 | { 66 | $dom = new \DOMDocument('1.0'); 67 | 68 | $dom->preserveWhiteSpace = false; 69 | 70 | $dom->formatOutput = true; 71 | 72 | $domXml = dom_import_simplexml($this->data); 73 | 74 | $domXml = $dom->importNode($domXml, true); 75 | 76 | $dom->appendChild($domXml); 77 | 78 | return (bool) $dom->save($filename); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Validator/ValidationRuleParser.php: -------------------------------------------------------------------------------- 1 | http://symfony.com 38 | */ 39 | $pattern = sprintf(static::PATTERN, implode('|', ['http', 'https'])); 40 | 41 | if (null === $value || '' === $value) { 42 | return false; 43 | } 44 | 45 | $value = (string) $value; 46 | 47 | return preg_match($pattern, $value) > 0; 48 | } 49 | 50 | /** 51 | * Get the validation error message. 52 | */ 53 | public function message(): string 54 | { 55 | return 'The :attribute value :value is not a valid url on line :line.'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Helpers/FormatsMessages.php: -------------------------------------------------------------------------------- 1 | getInlineMessage($attribute, $actualRule); 18 | 19 | if (!is_null($inlineMessage)) { 20 | return $inlineMessage; 21 | } 22 | 23 | return $rule->message(); 24 | } 25 | 26 | /** 27 | * Get the proper inline error message passed to the validator. 28 | */ 29 | protected function getInlineMessage(string $attribute, string $rule): ?string 30 | { 31 | return $this->getFromLocalArray($attribute, $this->ruleToLower($rule)); 32 | } 33 | 34 | /** 35 | * Get the inline message for a rule if it exists. 36 | */ 37 | protected function getFromLocalArray(string $attribute, string $lowerRule): ?string 38 | { 39 | $source = $this->customMessages; 40 | 41 | $keys = ["{$attribute}.{$lowerRule}", $lowerRule]; 42 | 43 | foreach ($keys as $key) { 44 | foreach (array_keys($source) as $sourceKey) { 45 | if ($sourceKey === $key) { 46 | return $source[$sourceKey]; 47 | } 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | protected function ruleToLower(string $rule): ?string 55 | { 56 | $lowerRule = preg_replace('/[A-Z]/', '_$0', $rule); 57 | 58 | $lowerRule = strtolower($lowerRule); 59 | 60 | return ltrim($lowerRule, '_'); 61 | } 62 | 63 | /** 64 | * Replace all error message place-holders with actual values. 65 | */ 66 | protected function makeReplacements( 67 | string $message, 68 | string $attribute, 69 | $value, 70 | ValidationRuleInterface $rule, 71 | array $parameters, 72 | int $lineNumber, 73 | ): string { 74 | $message = $this->replaceAttributePlaceholder($message, $attribute); 75 | 76 | if ($rule instanceof ParameterizedRuleInterface) { 77 | $message = $this->replaceParameterPlaceholder( 78 | $message, 79 | $rule->allowedParameters(), 80 | $parameters 81 | ); 82 | } 83 | 84 | $message = $this->replaceValuePlaceholder($message, $value); 85 | 86 | return $this->replaceErrorLinePlaceholder($message, $lineNumber); 87 | } 88 | 89 | /** 90 | * Replace the rule parameters placeholder in the given message. 91 | */ 92 | protected function replaceParameterPlaceholder( 93 | string $message, 94 | array $allowedParameters, 95 | array $parameters, 96 | ): string { 97 | return str_replace($allowedParameters, $parameters, $message); 98 | } 99 | 100 | /** 101 | * Replace the :attribute placeholder in the given message. 102 | */ 103 | protected function replaceAttributePlaceholder(string $message, string $attribute): string 104 | { 105 | return str_replace([':attribute'], [$attribute], $message); 106 | } 107 | 108 | /** 109 | * Replace the :value placeholder in the given message. 110 | */ 111 | protected function replaceValuePlaceholder(string $message, $value): string 112 | { 113 | return str_replace([':value'], [$value], $message); 114 | } 115 | 116 | /** 117 | * Replace the :line placeholder in the given message. 118 | */ 119 | protected function replaceErrorLinePlaceholder(string $message, int $lineNumber): array|string 120 | { 121 | return str_replace([':line'], [$lineNumber], $message); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/src/CsvValidatorTest.php: -------------------------------------------------------------------------------- 1 | testAssets = realpath(dirname(__FILE__) . '/../data'); 23 | } 24 | 25 | public function testInvalidCsvFilePath() 26 | { 27 | $file = $this->testAssets . '/tests.csv'; 28 | 29 | $validator = new Validator($file, [ 30 | 'stars' => ['between:0,5'], 31 | ]); 32 | 33 | $this->assertSame( 34 | $validator::INVALID_FILE_PATH_ERROR, 35 | $validator->validate()['message'] 36 | ); 37 | } 38 | 39 | public function testAsciiOnlyValidationRule() 40 | { 41 | $file = $this->testAssets . '/ascii_test.csv'; 42 | 43 | $validator = new Validator($file, [ 44 | 'name' => ['ascii_only'], 45 | ]); 46 | 47 | $this->assertTrue($validator->fails()); 48 | 49 | $this->assertSame( 50 | $validator::ERROR_MESSAGE, 51 | $validator->errors()['message'] 52 | ); 53 | 54 | $this->assertArrayHasKey( 55 | 'errors', 56 | $validator->errors()['data'][0] 57 | ); 58 | 59 | $this->assertContains( 60 | 'The name value Well Health Hotels¡ contains a non-ascii character on line 2.', 61 | $validator->errors()['data'][0]['errors'] 62 | ); 63 | } 64 | 65 | public function testBetweenValidationRule() 66 | { 67 | $file = $this->testAssets . '/between_test.csv'; 68 | 69 | $validator = new Validator($file, [ 70 | 'name' => ['between' => '50,90'], 71 | 'stars' => ['between:4,10'], 72 | ]); 73 | 74 | $this->assertTrue($validator->fails()); 75 | 76 | $result = $validator->errors(); 77 | $data = $result['data'][0]; 78 | 79 | $this->assertSame( 80 | $validator::ERROR_MESSAGE, 81 | $result['message'] 82 | ); 83 | 84 | $this->assertArrayHasKey( 85 | 'errors', 86 | $data 87 | ); 88 | 89 | $this->assertContains( 90 | 'The name value Well Health Hotels is not between 50 - 90 on line 2.', 91 | $data['errors'] 92 | ); 93 | 94 | $this->assertContains( 95 | 'The stars value 3 is not between 4 - 10 on line 2.', 96 | $data['errors'] 97 | ); 98 | } 99 | 100 | public function testUrlValidationRule() 101 | { 102 | $file = $this->testAssets . '/url_test.csv'; 103 | 104 | $validator = new Validator($file, [ 105 | 'uri' => ['url'], 106 | ]); 107 | 108 | $this->assertTrue($validator->fails()); 109 | 110 | $this->assertSame( 111 | $validator::ERROR_MESSAGE, 112 | $validator->errors()['message'] 113 | ); 114 | 115 | $validationErrors = $validator->errors(); 116 | 117 | for ($csvRow = 0; $csvRow < 3; ++$csvRow) { 118 | $this->assertArrayHasKey( 119 | 'errors', 120 | $validationErrors['data'][$csvRow] 121 | ); 122 | } 123 | 124 | $this->assertContains( 125 | 'The uri value http//:well.org is not a valid url on line 2.', 126 | $validationErrors['data'][0]['errors'] 127 | ); 128 | 129 | $this->assertContains( 130 | 'The uri value is not a valid url on line 3.', 131 | $validationErrors['data'][1]['errors'] 132 | ); 133 | 134 | $this->assertContains( 135 | 'The uri value is not a valid url on line 4.', 136 | $validationErrors['data'][2]['errors'] 137 | ); 138 | } 139 | 140 | public function testValidatorCsvOnEmptyRule() 141 | { 142 | $file = $this->testAssets . '/valid_test.csv'; 143 | 144 | $expectedArray = [ 145 | 'message' => 'CSV is valid.', 146 | 'data' => [ 147 | [ 148 | 'name' => 'Well Health Hotels', 149 | 'address' => 'Inga N. P.O. Box 567', 150 | 'stars' => '3', 151 | 'contact' => 'Kasper Zen', 152 | 'uri' => 'http://well.org', 153 | ], 154 | ], 155 | ]; 156 | 157 | $validator = new Validator($file, [ 158 | 'stars' => [''], 159 | ]); 160 | 161 | $this->assertSame($expectedArray, $validator->validate()); 162 | } 163 | 164 | public function testValidatorWithCustomRuleObject() 165 | { 166 | $file = $this->testAssets . '/ascii_test.csv'; 167 | 168 | $validator = new Validator($file, [ 169 | 'name' => [new UppercaseRule()], 170 | ]); 171 | 172 | $this->assertTrue($validator->fails()); 173 | 174 | $this->assertSame( 175 | $validator::ERROR_MESSAGE, 176 | $validator->errors()['message'] 177 | ); 178 | 179 | $this->assertArrayHasKey( 180 | 'errors', 181 | $validator->errors()['data'][0] 182 | ); 183 | 184 | $this->assertContains( 185 | 'The name value Well Health Hotels¡ must be uppercase on line 2.', 186 | $validator->errors()['data'][0]['errors'] 187 | ); 188 | } 189 | 190 | public function testValidatorWithCustomRuleClosure() 191 | { 192 | $file = $this->testAssets . '/url_test.csv'; 193 | 194 | $validator = new Validator($file, [ 195 | 'uri' => [function ($value, $fail) { 196 | if (!str_starts_with($value, 'https://')) { 197 | return $fail('The URL passed must be https i.e it must start with https://'); 198 | } 199 | 200 | return true; 201 | }], 202 | ]); 203 | 204 | $this->assertTrue($validator->fails()); 205 | 206 | $this->assertArrayHasKey( 207 | 'errors', 208 | $validator->errors()['data'][0] 209 | ); 210 | 211 | $this->assertContains( 212 | 'The URL passed must be https i.e it must start with https://', 213 | $validator->errors()['data'][0]['errors'] 214 | ); 215 | } 216 | 217 | public function testValidatorWithCustomErrorMessage() 218 | { 219 | $file = $this->testAssets . '/ascii_test.csv'; 220 | $customErrorMessage = 'The value supplied for the name attribute must only contain ascii characters'; 221 | 222 | $validator = new Validator( 223 | $file, 224 | ['name' => ['ascii_only']], 225 | ',', 226 | ['ascii_only' => $customErrorMessage] 227 | ); 228 | 229 | $this->assertTrue($validator->fails()); 230 | 231 | $this->assertSame( 232 | $validator::ERROR_MESSAGE, 233 | $validator->errors()['message'] 234 | ); 235 | 236 | $this->assertArrayHasKey( 237 | 'errors', 238 | $validator->errors()['data'][0] 239 | ); 240 | 241 | $this->assertContains( 242 | $customErrorMessage, 243 | $validator->errors()['data'][0]['errors'] 244 | ); 245 | } 246 | 247 | public function testValidatorWithCustomErrorMessageWithPlaceholder() 248 | { 249 | $file = $this->testAssets . '/between_test.csv'; 250 | 251 | $validator = new Validator( 252 | $file, 253 | ['stars' => ['between' => [4, 10]]], 254 | ',', 255 | ['between' => 'The value supplied for :attribute must be between :min and :max'] 256 | ); 257 | 258 | $this->assertTrue($validator->fails()); 259 | 260 | $this->assertSame( 261 | $validator::ERROR_MESSAGE, 262 | $validator->errors()['message'] 263 | ); 264 | 265 | $this->assertArrayHasKey( 266 | 'errors', 267 | $validator->errors()['data'][0] 268 | ); 269 | 270 | $this->assertContains( 271 | 'The value supplied for stars must be between 4 and 10', 272 | $validator->errors()['data'][0]['errors'] 273 | ); 274 | } 275 | 276 | public function testValidatorJsonWriter() 277 | { 278 | $file = $this->testAssets . '/valid_test.csv'; 279 | 280 | $validator = new Validator($file, [ 281 | 'name' => ['ascii_only'], 282 | 'stars' => ['between:3,10'], 283 | 'uri' => ['url'], 284 | ]); 285 | 286 | $this->assertFalse($validator->fails()); 287 | 288 | $this->assertSame( 289 | $validator::NO_ERROR_MESSAGE, 290 | $validator->errors()['message'] 291 | ); 292 | 293 | $this->assertTrue($validator->write(new JsonConverter())); 294 | 295 | $this->assertFileEquals( 296 | $this->testAssets . '/valid_test_expected.json', 297 | $this->testAssets . '/valid_test.json' 298 | ); 299 | } 300 | 301 | public function testValidatorXmlWriter() 302 | { 303 | $file = $this->testAssets . '/valid_test.csv'; 304 | 305 | $validator = new Validator($file, [ 306 | 'name' => ['ascii_only'], 307 | 'stars' => ['between:3,10'], 308 | 'uri' => ['url'], 309 | ]); 310 | 311 | $this->assertFalse($validator->fails()); 312 | 313 | $this->assertSame( 314 | $validator::NO_ERROR_MESSAGE, 315 | $validator->errors()['message'] 316 | ); 317 | 318 | $this->assertTrue($validator->write(new XmlConverter())); 319 | 320 | $this->assertFileEquals( 321 | $this->testAssets . '/valid_test_expected.xml', 322 | $this->testAssets . '/valid_test.xml' 323 | ); 324 | } 325 | 326 | public function testValidatorXmlWriterWithRecordElementParameter() 327 | { 328 | $file = $this->testAssets . '/valid_test.csv'; 329 | 330 | $validator = new Validator($file, [ 331 | 'name' => ['ascii_only'], 332 | 'stars' => ['between:3,10'], 333 | 'uri' => ['url'], 334 | ]); 335 | 336 | $this->assertFalse($validator->fails()); 337 | 338 | $this->assertSame( 339 | $validator::NO_ERROR_MESSAGE, 340 | $validator->errors()['message'] 341 | ); 342 | 343 | $this->assertTrue($validator->write(new XmlConverter('sample'))); 344 | 345 | $this->assertFileEquals( 346 | $this->testAssets . '/valid_test_param_expected.xml', 347 | $this->testAssets . '/valid_test.xml' 348 | ); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/Validator/Validator.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 94 | $this->delimiter = $delimiter; 95 | $this->rules = $rules; 96 | $this->customMessages = $messages; 97 | 98 | $this->setFileDirectory(); 99 | $this->setFileName(); 100 | } 101 | 102 | /** 103 | * Run the validator's rules against the supplied data. 104 | */ 105 | public function validate(): array 106 | { 107 | if ($this->fails()) { 108 | return $this->errors(); 109 | } 110 | 111 | return [ 112 | 'message' => self::SUCCESS_MESSAGE, 113 | 'data' => $this->data, 114 | ]; 115 | } 116 | 117 | /** 118 | * Return validation errors. 119 | */ 120 | public function errors(): array 121 | { 122 | if (empty($this->message) && empty($this->invalidRows)) { 123 | $message = self::NO_ERROR_MESSAGE; 124 | } elseif (empty($this->message)) { 125 | $message = self::ERROR_MESSAGE; 126 | } else { 127 | $message = $this->message; 128 | } 129 | 130 | return [ 131 | 'message' => $message, 132 | 'data' => $this->invalidRows, 133 | ]; 134 | } 135 | 136 | /** 137 | * Determine if the data fails the validation rules. 138 | */ 139 | public function fails(): bool 140 | { 141 | return !$this->passes(); 142 | } 143 | 144 | /** 145 | * Determine if the data passes the validation rules. 146 | */ 147 | protected function passes(): bool 148 | { 149 | if ($this->doesFileExistAndReadable($this->filePath)) { 150 | if (false !== ($handle = fopen($this->filePath, 'r'))) { 151 | while (false !== ($row = fgetcsv($handle, 0, $this->delimiter))) { 152 | ++$this->currentRowLineNumber; 153 | if (empty($this->headers)) { 154 | $this->setHeaders($row); 155 | continue; 156 | } 157 | 158 | $rowWithAttribute = []; 159 | 160 | foreach ($row as $key => $value) { 161 | $attribute = $this->headers[$key]; 162 | $rowWithAttribute[$attribute] = $value; 163 | } 164 | 165 | $this->validateRow($rowWithAttribute); 166 | } 167 | } 168 | } else { 169 | $this->message = self::INVALID_FILE_PATH_ERROR; 170 | } 171 | 172 | return empty($this->invalidRows) && empty($this->message); 173 | } 174 | 175 | /** 176 | * Write the output data into any supplied format. 177 | */ 178 | public function write(ConverterHandlerInterface $format): bool 179 | { 180 | return $format 181 | ->convert($this->data) 182 | ->write($this->getWriteFileName($format->getExtension())); 183 | } 184 | 185 | /** 186 | * Set CSV filename. 187 | */ 188 | protected function setFileName(): void 189 | { 190 | $this->fileName = basename($this->filePath, self::FILE_EXTENSION); 191 | } 192 | 193 | /** 194 | * Set CSV file directory. 195 | */ 196 | protected function setFileDirectory(): void 197 | { 198 | $this->directory = dirname($this->filePath) . DIRECTORY_SEPARATOR; 199 | } 200 | 201 | /** 202 | * Get the full path and name of the file to be written. 203 | */ 204 | protected function getWriteFileName(string $extension): string 205 | { 206 | return $this->directory . $this->fileName . '.' . $extension; 207 | } 208 | 209 | /** 210 | * Validate a given row with the supplied rules. 211 | */ 212 | protected function validateRow(array $row): void 213 | { 214 | $this->currentRowMessages = []; 215 | $this->currentRow = $row; 216 | 217 | foreach ($this->rules as $attribute => $attributeRules) { 218 | foreach ($attributeRules as $key => $value) { 219 | if (is_int($key)) { 220 | $rulePayload = $value; 221 | } else { 222 | $rulePayload = [$key, $value]; 223 | } 224 | 225 | $this->validateAttribute($attribute, $rulePayload, $row); 226 | } 227 | } 228 | 229 | if (!empty($this->currentRowMessages)) { 230 | $row['errors'] = $this->currentRowMessages; 231 | $this->invalidRows[] = $row; 232 | } 233 | 234 | $this->data[] = $row; 235 | } 236 | 237 | /** 238 | * Validate a given attribute against a rule. 239 | */ 240 | protected function validateAttribute(string $attribute, object|array|string $rule): void 241 | { 242 | list($rule, $parameters) = ValidationRuleParser::parse($rule); 243 | 244 | if ('' === $rule) { 245 | return; 246 | } 247 | 248 | $value = $this->getAttributeValueFromCurrentRow($attribute); 249 | 250 | if ($rule instanceof ValidationRule) { 251 | $this->validateUsingCustomRule($attribute, $value, $parameters, $rule); 252 | 253 | return; 254 | } 255 | 256 | if ($this->isValidateAble($rule, $parameters)) { 257 | $ruleClass = $this->getRuleClass($rule); 258 | if (!$ruleClass->passes($value, $parameters, $this->currentRow)) { 259 | $this->addFailure( 260 | $this->getMessage($attribute, $ruleClass, $rule), 261 | $attribute, 262 | $value, 263 | $ruleClass, 264 | $parameters 265 | ); 266 | } 267 | } 268 | } 269 | 270 | protected function doesFileExistAndReadable(string $filePath): bool 271 | { 272 | return file_exists($filePath) && is_readable($filePath); 273 | } 274 | 275 | protected function setHeaders(array $headers): void 276 | { 277 | $this->headers = $headers; 278 | } 279 | 280 | /** 281 | * Determine if the attribute is validate-able. 282 | */ 283 | protected function isValidateAble(object|string $rule, array $parameters): bool 284 | { 285 | return $this->ruleExists($rule) && $this->passesParameterCheck($rule, $parameters); 286 | } 287 | 288 | /** 289 | * Get the class of a rule. 290 | */ 291 | protected function getRuleClassName(string $rule): string 292 | { 293 | return 'Oshomo\\CsvUtils\\Rules\\' . $rule; 294 | } 295 | 296 | /** 297 | * Get the class of a rule. 298 | */ 299 | protected function getRuleClass(string $rule): ValidationRuleInterface 300 | { 301 | $ruleClassName = $this->getRuleClassName($rule); 302 | 303 | return new $ruleClassName(); 304 | } 305 | 306 | /** 307 | * Determine if a given rule exists. 308 | */ 309 | protected function ruleExists(object|string $rule): bool 310 | { 311 | return $rule instanceof ValidationRule || class_exists($this->getRuleClassName($rule)); 312 | } 313 | 314 | /** 315 | * Determine if a given rule expect parameters and that the parameters where sent. 316 | */ 317 | protected function passesParameterCheck(object|string $rule, array $parameters): bool 318 | { 319 | if (!$rule instanceof ValidationRule) { 320 | $rule = $this->getRuleClass($rule); 321 | } 322 | 323 | if ($rule instanceof ParameterizedRuleInterface) { 324 | $ruleParameterCount = count($rule->allowedParameters()); 325 | $parameterCount = count($parameters); 326 | 327 | return $parameterCount === $ruleParameterCount; 328 | } 329 | 330 | return true; 331 | } 332 | 333 | /** 334 | * Validate an attribute using a custom rule object. 335 | */ 336 | protected function validateUsingCustomRule( 337 | string $attribute, 338 | $value, 339 | array $parameters, 340 | ValidationRuleInterface $rule, 341 | ): void { 342 | if (!$rule->passes($value, $parameters, $this->currentRow)) { 343 | $this->addFailure($rule->message(), $attribute, $value, $rule, $parameters); 344 | } 345 | } 346 | 347 | /** 348 | * Add a failed rule and error message to the collection. 349 | */ 350 | protected function addFailure( 351 | string $message, 352 | string $attribute, 353 | $value, 354 | ValidationRuleInterface $rule, 355 | array $parameters = [], 356 | ): void { 357 | $this->currentRowMessages[] = $this->makeReplacements( 358 | $message, 359 | $attribute, 360 | $value, 361 | $rule, 362 | $parameters, 363 | $this->currentRowLineNumber 364 | ); 365 | } 366 | 367 | /** 368 | * Get the value of a given attribute. 369 | */ 370 | protected function getAttributeValueFromCurrentRow(string $attribute) 371 | { 372 | return $this->currentRow[$attribute]; 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2018] [Oshomo Oforomeh] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/hoshomoh/CSVUtils.svg?branch=master)](https://travis-ci.org/hoshomoh/CSVUtils) 2 | [![codecov](https://codecov.io/gh/hoshomoh/CSVUtils/branch/master/graph/badge.svg)](https://codecov.io/gh/hoshomoh/CSVUtils) 3 | 4 | # CSVUtils 5 | 6 | *Make sure you use a tagged version when requiring this package.* 7 | 8 | ## Table of Content 9 | 10 | - [Current Stable Versions](#current-stable-versions) 11 | - [How to Run](#how-to-run) 12 | - [Implementation](#implementation) 13 | - [Documentation](#documentation) 14 | - [Initializing a Validator](#initializing-a-validator) 15 | - [Validating the CSV](#validating-the-csv) 16 | - [Available rules](#available-rules) 17 | - [Writing CSV Output Data](#writing-csv-output-data) 18 | - [Passing Custom Rules to Validator Using Rule Object](#passing-custom-rules-to-validator-using-rule-object) 19 | - [Passing Custom Rules to Validator Using Closure](#passing-custom-rules-to-validator-using-closure) 20 | - [Writing CSV Output Data to Other Formats](#writing-csv-output-data-to-other-formats) 21 | - [Running Tests](#running-tests) 22 | - [Contributing to this Repo](#contributing-to-this-repo) 23 | 24 | ### Current Stable Versions 25 | - PHP 5 [v2.0.1](https://packagist.org/packages/oshomo/csv-utils#v2.0.1) 26 | - PHP 7 [v5.0.0](https://packagist.org/packages/oshomo/csv-utils#v5.0.0) 27 | 28 | ### How to Run 29 | 30 | I have added a sample `index.php` file for a quick test of how to use the package. To run the sample; from the package root, run `composer install` then using php built in server run `php -S localhost:8000`, this would start the server at `localhost:8000`. Visit the URL from your browser and you should see the generated files in the `sample` folder at the root of the package. 31 | 32 | ### Implementation 33 | 34 | The `Validator` expects a valid file path, the CSV delimiter, an array of validation rule(s) and an optional message(s) array to over-write the default messages of the validator. 35 | 36 | ### Documentation 37 | 38 | ##### Initializing a Validator 39 | 40 | Set a valid csv file path, pass the CSV delimiter and pass in your validation rules. 41 | 42 | ```php 43 | use Oshomo\CsvUtils\Validator\Validator; 44 | 45 | $validator = new Validator( 46 | "some/valid/file_path", 47 | [ 48 | "name" => ["ascii_only"], 49 | "uri" => ["url"], 50 | "stars" => ["between:0,5"] 51 | ] 52 | ); 53 | ``` 54 | 55 | ##### Validating the CSV 56 | 57 | Now we are ready to validate the CSV. The validator provides a `validate ` method that can be called like so: `$validator->validate();`. The `validate` method returns an array of the invalid rows if validation fails. If the validation passes the `validate` method returns the CSV data as an array 58 | 59 | A better implementation: 60 | 61 | ```php 62 | use Oshomo\CsvUtils\Validator\Validator; 63 | 64 | $validator = new Validator( 65 | "some/valid/file_path", 66 | [ 67 | 'title' => ["ascii_only", "url"] 68 | ] 69 | ); 70 | 71 | if ($validator->fails()) { 72 | // Do something when validation fails 73 | $errors = $validator->errors(); 74 | } 75 | ``` 76 | 77 | ##### Error messages 78 | 79 | To get the rows with validation errors and their errors. The validator expose `errors` method that can be used like so `$validator->errors()`. 80 | 81 | You can also customize the error messages for different validation rules and different attributes by passing a message array to the validator like so: 82 | 83 | ```php 84 | use Oshomo\CsvUtils\Validator\Validator; 85 | 86 | $validator = new Validator( 87 | "some/valid/file_path", 88 | ['title' => ["ascii_only", "url"]], 89 | [ 90 | 'ascii_only' => 'The :value supplied for :attribute attribute is invalid on line :line of the CSV.', 91 | // This specifies a custom message for a given attribute. 92 | 'hotel_link:url' => 'The :attribute must be a valid link. This error occured on line :line of the CSV.', 93 | ] 94 | ); 95 | ``` 96 | 97 | In this above example: 98 | 99 | The `:attribute` place-holder will be replaced by the actual name of the field under validation. 100 | The `:value` place-holder will be replaced with value being validated. 101 | The `:line` place-holder will also be replaced with the row/line number in the CSV in which the error happened. 102 | 103 | You may also utilize other place-holders in validation messages. For example the `between` rule exposes two other placeholder `min` and `max`. Find more about this in the available rules section 104 | 105 | ##### Available rules 106 | 107 | `between:min,max`: 108 | ``` 109 | Validates that a cell value is between a :min and :max. The rule exposes the :min and :max placeholder for inline messages 110 | ``` 111 | `ascii_only`: 112 | ``` 113 | Validates that a cell value does not contain a non-ascii character 114 | ``` 115 | `url`: 116 | ``` 117 | Validates that a cell value is a valid URL. By valid URL we mean 118 | 119 | (#protocol) 120 | (#basic auth) 121 | (#a domain name or #an IP address or #an IPv6 address) 122 | (#a port(optional)) then 123 | (#a /, nothing, a / with something, a query or a fragment) 124 | 125 | ``` 126 | 127 | ##### Writing CSV Output Data 128 | 129 | The output of the CSV file can be written into any format. The currently suported format is `xml` and `json`. The validator exposes a `write` method to write the output data into the same folder as the CSV. Find example implementation below: 130 | 131 | ```php 132 | use Oshomo\CsvUtils\Validator\Validator; 133 | use Oshomo\CsvUtils\Converter\JsonConverter; 134 | use Oshomo\CsvUtils\Converter\XmlConverter; 135 | 136 | $validator = new Validator( 137 | 'some/valid/file_path', 138 | [ 139 | "stars" => ["between:0,5"], 140 | "name" => ["ascii_only"], 141 | "uri" => ["url"], 142 | ] 143 | ); 144 | 145 | if(!$validator->fails()) { 146 | $validator->write(new JsonConverter()); 147 | $validator->write(new XmlConverter("hotel")); 148 | } else { 149 | print_r($validator->errors()); 150 | } 151 | ``` 152 | 153 | The `JsonConverter` simply writes the output data as JSON. The `XmlConverter` converter writes the data as XML. `XmlConverter` takes an optional parameter for setting the XML records element. If non is supplied it defaults to `item` e.g `$validator->write(new XmlConverter("hotel"));` would write the below: 154 | 155 | ``` 156 | 157 | 158 | 159 | Beni Gold Hotel and Apartments 160 | 5 161 | https://hotels.ng/hotel/86784-benigold-hotel-lagos 162 | 163 | 164 | Hotel Ibis Lagos Ikeja 165 | 4 166 | https://hotels.ng/hotel/52497-hotel-ibis-lagos-ikeja-lagos 167 | 168 | 169 | ``` 170 | 171 | **NOTE**: *Either validation passes or fails, you can always write the CSV output data to the available formats. In cases where validation fails there would be an extra error property in the written data.* 172 | 173 | ##### Passing Custom Rules to Validator Using Rule Object 174 | 175 | Passing a custom rule to the validator is easy. Create a CustomRule class the implements `Oshomo\CsvUtils\Contracts\ValidationRuleInterface` interface. And pass that class to the rule array, easy. E.g: 176 | 177 | ```php 178 | use Oshomo\CsvUtils\Validator\Validator; 179 | 180 | $validator = new Validator( 181 | 'some/valid/file_path', 182 | ["name" => ["ascii_only", new UppercaseRule]] 183 | ); 184 | ``` 185 | 186 | The class definition for `UppercaseRule`. Follow the same approach if you want to create your own rule. 187 | 188 | ```php 189 | use Oshomo\CsvUtils\Contracts\ValidationRuleInterface; 190 | 191 | class UppercaseRule implements ValidationRuleInterface 192 | { 193 | /** 194 | * Determines if the validation rule passes. This is where we do the 195 | * actual validation. If the validation passes return true else false 196 | */ 197 | public function passes($value, array $parameters, array $row): bool 198 | { 199 | return strtoupper($value) === $value; 200 | } 201 | 202 | /** 203 | * Get the validation error message. Specify the message that should 204 | * be returned if the validation fails. You can make use of the 205 | * :attribute and :value placeholders in the message string 206 | * 207 | * @return string 208 | */ 209 | public function message(): string 210 | { 211 | return "The :attribute value :value must be uppercase on line :line."; 212 | } 213 | } 214 | 215 | ``` 216 | 217 | If the CustomRule accepts parameters like the `between` rule, then your CustomRule class must implement both `Oshomo\CsvUtils\Contracts\ValidationRuleInterface` and `Oshomo\CsvUtils\Contracts\ParameterizedRuleInterface`. See `Oshomo\CsvUtils\Rules\Between` as an example. 218 | 219 | ##### Passing Custom Rules to Validator Using Closure 220 | 221 | If you only need the functionality of a custom rule once throughout your application, you may use a Closure instead of a rule object. The Closure receives the attribute's value, and a `$fail` callback that should be called if validation fails: 222 | 223 | ```php 224 | use Oshomo\CsvUtils\Validator\Validator; 225 | 226 | $validator = new Validator( 227 | "some/valid/file_path", 228 | [ 229 | "uri" => ["url", function($value, $fail) { 230 | if (strpos($value, "https://") !== 0) { 231 | return $fail('The URL passed must be https i.e it must start with https://'); 232 | } 233 | }] 234 | ]); 235 | ``` 236 | 237 | ##### Writing CSV Output Data to Other Formats 238 | 239 | Writing the CSV output data to other format is also very easy. Create a CustomConverter class the implements `Oshomo\CsvUtils\Contracts\ConverterHandlerInterface` interface. And pass that class to the `write` method of the validator, easy. Below is an sample implementation of a JSON converter 240 | 241 | ```php 242 | use Oshomo\CsvUtils\Contracts\ConverterHandlerInterface; 243 | 244 | class JsonConverter implements ConverterHandlerInterface 245 | { 246 | const FILE_EXTENSION = "json"; 247 | 248 | /** 249 | * The converted data 250 | * 251 | * @var string 252 | */ 253 | protected $data; 254 | 255 | /** 256 | * @return string 257 | */ 258 | public function getExtension(): string 259 | { 260 | return JsonConverter::FILE_EXTENSION; 261 | } 262 | 263 | /** 264 | * @param array $data 265 | * @return $this|mixed 266 | */ 267 | public function convert(array $data): ConverterHandlerInterface 268 | { 269 | $this->data = json_encode($data, 270 | JSON_PRETTY_PRINT | 271 | JSON_NUMERIC_CHECK | 272 | JSON_UNESCAPED_SLASHES | 273 | JSON_UNESCAPED_UNICODE 274 | ); 275 | 276 | return $this; 277 | } 278 | 279 | /** 280 | * @param string $filename 281 | * @return bool 282 | */ 283 | public function write(string $filename): bool 284 | { 285 | return (file_put_contents($filename, $this->data)) ? true : false; 286 | } 287 | } 288 | 289 | ////////////////////////////////////////////////////// 290 | // To use the converter above. 291 | ////////////////////////////////////////////////////// 292 | 293 | $validator->write(new JsonConverter()); 294 | 295 | ``` 296 | 297 | ### Running Tests 298 | 299 | Run `composer test` from the root of the Package. 300 | 301 | ### Contributing to this Repo 302 | 303 | Feel free to submit a pull request for a feature or bug fix. However, do note that before your pull request can be merged it must have test written or updated as the case maybe. 304 | The project run's automatic checks to make sure that the Symfony code standards are met using [php-cs-fixer](https://symfony.com/doc/current/contributing/code/standards.html). 305 | 306 | So, before pushing or making any pull request run the below command: 307 | 308 | * `composer test`: For running test 309 | * `composer fix-lint`: For running php-cs-fixer to fix linting errors 310 | --------------------------------------------------------------------------------