├── 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 | [](https://travis-ci.org/hoshomoh/CSVUtils)
2 | [](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 |
--------------------------------------------------------------------------------