├── .github
├── CODEOWNERS
├── mergeable.yml
├── stale.yml
└── workflows
│ └── php.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── LICENSE
├── README.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.ci
├── phpunit.xml.dist
├── src
├── Constraint
│ ├── Constraint.php
│ ├── ConstraintInterface.php
│ ├── DateRange.php
│ ├── DateTime.php
│ ├── Email.php
│ ├── Enum.php
│ ├── GuidValue.php
│ ├── NativeEnum.php
│ ├── NotNull.php
│ ├── Pattern.php
│ ├── Range.php
│ ├── StringSize.php
│ ├── Type.php
│ ├── Url.php
│ └── UuidValue.php
├── Exception
│ ├── InvalidConstraintException.php
│ ├── RequiredFieldException.php
│ └── TransformationException.php
├── InputHandler.php
├── Instantiator
│ ├── ConstructInstantiator.php
│ ├── InstantiatorInterface.php
│ ├── PropertyInstantiator.php
│ ├── ReflectionInstantiator.php
│ └── SetInstantiator.php
├── Node
│ ├── BaseNode.php
│ ├── BoolNode.php
│ ├── CollectionNode.php
│ ├── DateTimeNode.php
│ ├── FloatNode.php
│ ├── IntNode.php
│ ├── NumericNode.php
│ ├── ObjectNode.php
│ ├── ScalarCollectionNode.php
│ └── StringNode.php
├── SchemaBuilder.php
├── Transformer
│ ├── DateTimeTransformer.php
│ ├── TransformerInterface.php
│ └── UuidTransformer.php
└── TypeHandler.php
└── tests
├── Constraint
├── DateRangeTest.php
├── DateTimeTest.php
├── EmailTest.php
├── EnumTest.php
├── GuidValueTest.php
├── NativeEnumTest.php
├── NotNullTest.php
├── PatternTest.php
├── RangeTest.php
├── StringSizeTest.php
├── TypeTest.php
├── UrlTest.php
└── UuidValueTest.php
├── InputHandlerTest.php
├── Instantiator
├── ConstructInstantiatorTest.php
├── PropertyInstantiatorTest.php
├── ReflectionInstantiatorTest.php
└── SetInstantiatorTest.php
├── Node
├── BaseNodeTest.php
├── CollectionNodeTest.php
├── IntNodeTest.php
├── ObjectNodeTest.php
└── ScalarCollectionNodeTest.php
├── SchemaBuilderTest.php
├── TestCase.php
├── Transformer
├── DateTimeTransformerTest.php
└── UuidTransformerTest.php
└── TypeHandlerTest.php
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @LinioIT/backend-engineers
2 |
--------------------------------------------------------------------------------
/.github/mergeable.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | mergeable:
3 | - when: pull_request.*
4 | validate:
5 | - do: label
6 | must_exclude:
7 | regex: do-not-merge/blocked
8 | - do: title
9 | must_exclude:
10 | regex: /\[?wip\]?:?/i
11 | - do: label
12 | must_exclude:
13 | regex: do-not-merge/work-in-progress
14 | pass:
15 | - do: checks
16 | status: success
17 | payload:
18 | title: The PR is ready to be merged.
19 | summary: The pull request is ready to be merged.
20 | fail:
21 | - do: checks
22 | status: failure
23 | payload:
24 | title: The PR is not ready to be merged.
25 | summary: The pull request is not ready to be merged.
26 |
27 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-stale - https://github.com/probot/stale
2 |
3 | # Number of days of inactivity before an Issue or Pull Request becomes stale
4 | daysUntilStale: 3
5 |
6 | # Number of days of inactivity before a stale Issue or Pull Request is closed.
7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
8 | daysUntilClose: false
9 |
10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
11 | exemptLabels:
12 | - pinned
13 | - security
14 |
15 | # Set to true to ignore issues in a project (defaults to false)
16 | exemptProjects: false
17 |
18 | # Set to true to ignore issues in a milestone (defaults to false)
19 | exemptMilestones: false
20 |
21 | # Label to use when marking as stale
22 | staleLabel: stale
23 |
24 | # Comment to post when marking as stale. Set to `false` to disable
25 | markComment: >
26 | This issue has been automatically marked as stale because it has not had
27 | recent activity.
28 |
29 | # Comment to post when removing the stale label.
30 | unmarkComment: false
31 |
32 | # Comment to post when closing a stale Issue or Pull Request.
33 | # closeComment: >
34 | # Your comment here.
35 |
36 | # Limit the number of actions per hour, from 1-30. Default is 30
37 | limitPerRun: 30
38 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: PHP Composer
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - name: Setup PHP
18 | uses: shivammathur/setup-php@v2
19 | with:
20 | php-version: '8.1'
21 |
22 | - name: Validate composer.json and composer.lock
23 | run: composer validate --strict
24 |
25 | - name: Cache Composer packages
26 | id: composer-cache
27 | uses: actions/cache@v2
28 | with:
29 | path: vendor
30 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
31 | restore-keys: |
32 | ${{ runner.os }}-php-
33 |
34 | - name: Install dependencies
35 | run: composer install --prefer-dist --no-progress
36 |
37 | - name: Run linter
38 | run: vendor/bin/php-cs-fixer fix --dry-run -v
39 |
40 | - name: Run test suite
41 | run: vendor/bin/phpunit --verbose
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | vendor/
3 | bin/
4 | composer.phar
5 | composer.lock
6 | phpunit.xml
7 | cache.properties
8 | .php-cs-fixer.cache
9 | .phpunit.result.cache
10 | .idea/
11 | /data/cache/*
12 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in(__DIR__ . '/src')
5 | ->in(__DIR__ . '/tests');
6 |
7 | return (new PhpCsFixer\Config())
8 | ->setRules([
9 | '@Symfony' => true,
10 | '@PHP71Migration:risky' => true,
11 | '@PHPUnit60Migration:risky' => true,
12 | 'array_indentation' => true,
13 | 'array_syntax' => ['syntax' => 'short'],
14 | 'blank_line_after_opening_tag' => true,
15 | 'concat_space' => ['spacing' => 'one'],
16 | 'declare_strict_types' => true,
17 | 'increment_style' => ['style' => 'post'],
18 | 'is_null' => true,
19 | 'list_syntax' => ['syntax' => 'short'],
20 | 'method_argument_space' => true,
21 | 'method_chaining_indentation' => true,
22 | 'modernize_types_casting' => true,
23 | 'multiline_whitespace_before_semicolons' => false,
24 | 'no_superfluous_elseif' => true,
25 | 'no_superfluous_phpdoc_tags' => true,
26 | 'no_useless_else' => true,
27 | 'no_useless_return' => true,
28 | 'ordered_imports' => true,
29 | 'phpdoc_align' => false,
30 | 'phpdoc_order' => true,
31 | 'php_unit_construct' => true,
32 | 'php_unit_dedicate_assert' => true,
33 | 'return_assignment' => true,
34 | 'single_line_comment_style' => true,
35 | 'ternary_to_null_coalescing' => true,
36 | 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
37 | 'void_return' => true,
38 | ])
39 | ->setFinder($finder)
40 | ->setUsingCache(true)
41 | ->setRiskyAllowed(true);
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Bazaya México, S de RL de C.V.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of Linio Input nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Linio Input
2 | ===========
3 | [](https://packagist.org/packages/linio/input) [](https://packagist.org/packages/linio/input) [](http://travis-ci.org/LinioIT/input) [](https://scrutinizer-ci.com/g/LinioIT/input/?branch=master)
4 |
5 | Linio Input is yet another component of the Linio Framework. It aims to
6 | abstract HTTP request input handling, allowing a seamless integration with
7 | your domain model. The component is responsible for:
8 |
9 | * Parsing request body contents
10 | * Validating input data
11 | * Hydrating input data into objects
12 |
13 | Install
14 | -------
15 |
16 | The recommended way to install Linio Input is [through composer](http://getcomposer.org).
17 |
18 | ```JSON
19 | {
20 | "require": {
21 | "linio/input": "dev-master"
22 | }
23 | }
24 | ```
25 |
26 | Tests
27 | -----
28 |
29 | To run the test suite, you need install the dependencies via composer, then
30 | run PHPUnit.
31 |
32 | $ composer install
33 | $ phpunit
34 |
35 | Usage
36 | -----
37 |
38 | The library is very easy to use: first, you have to create your input
39 | handler class. The input handlers are responsible for specifying
40 | which data you're expecting to receive from requests. Let's create one:
41 |
42 | ```php
43 | add('referrer', 'string');
54 | $this->add('registration_date', 'datetime');
55 |
56 | $user = $this->add('user', 'Linio\Model\User');
57 | $user->add('name', 'string');
58 | $user->add('email', 'string');
59 | $user->add('age', 'integer');
60 | }
61 | }
62 | ```
63 |
64 | Now, in your controller, you just need to bind data to the handler:
65 |
66 | ```php
67 | bind($request->request->all());
80 |
81 | if (!$input->isValid()) {
82 | return new Response($input->getErrorsAsString());
83 | }
84 |
85 | $data = $input->getData();
86 | $data['referrer']; // string
87 | $data['registration_date']; // \DateTime
88 | $data['user']; // Linio\Model\User
89 |
90 | return new Response(['message' => 'Valid!']);
91 | }
92 | }
93 | ```
94 |
95 | Type Handler
96 | ------------
97 |
98 | When you are defining the fields for your input handler, there are a few types
99 | available: string, int, bool, datetime, etc. Those are predefined types
100 | provided by the library, but you can also create your own. This magic is
101 | handled by `Linio\Component\Input\TypeHandler`. The `TypeHandler` allows you to
102 | add new types, which are extensions of the `BaseNode` class.
103 |
104 | ```php
105 | addConstraint(new Linio\Component\Input\Constraint\GuidValue());
112 | }
113 | }
114 |
115 | $typeHandler = new Linio\Component\Input\TypeHandler();
116 | $typeHandler->addType('guid', GuidNode::class);
117 |
118 | $input = new RegistrationHandler();
119 | $input->setTypeHandler($typeHandler);
120 |
121 | ```
122 |
123 | In this example, we have created a new `guid` type, which has a built-in constraint
124 | to validate contents. You can use custom types to do all sorts of things: add
125 | predefined constraint chains, transformers, instantiators and also customize how
126 | values are generated.
127 |
128 |
129 | Constraints
130 | -----------
131 |
132 | Linio Input allows you to apply constraints to your fields. This can be done
133 | by providing a third argument for the `add()` method in your input handlers:
134 |
135 |
136 | ```php
137 | add('referrer', 'string', ['required' => true]);
146 | $this->add('registration_date', 'datetime');
147 |
148 | $user = $this->add('user', 'Linio\Model\User');
149 | $user->add('name', 'string');
150 | $user->add('email', 'string', ['constraints' => [new Pattern('/^\S+@\S+\.\S+$/')]]);
151 | $user->add('age', 'integer');
152 | }
153 | }
154 | ```
155 |
156 | The library includes several constraints by default:
157 |
158 | * Enum
159 | * GuidValue
160 | * NotNull
161 | * Pattern
162 | * StringSize
163 |
164 | Transformers
165 | ------------
166 |
167 | Linio Input allows you to create data transformers, responsible for converting
168 | simple input data, like timestamps and unique IDs, into something meaningful,
169 | like a datetime object or the full entity (by performing a query).
170 |
171 | ```php
172 | repository->find($value);
190 | } catch (\Exception $e) {
191 | return null;
192 | }
193 |
194 | return $entity;
195 | }
196 |
197 | public function setRepository(ObjectRepository $repository)
198 | {
199 | $this->repository = $repository;
200 | }
201 | }
202 |
203 | ```
204 |
205 | Data transformers can be added on a per-field basis during the definition
206 | of your input handler:
207 |
208 | ```php
209 | add('store_id', 'string', ['transformer' => $this->idTransformer]);
223 | }
224 |
225 | public function setIdTransformer(IdTransformer $idTransformer)
226 | {
227 | $this->idTransformer = $idTransformer;
228 | }
229 | }
230 | ```
231 |
232 | Instantiators
233 | -------------
234 |
235 | Linio Input allows you to use different object instantiators on a per-field
236 | basis. This can be done by providing a third argument for the `add()` method
237 | in your input handlers:
238 |
239 |
240 | ```php
241 | add('foobar', 'My\Foo\Class', ['instantiator' => new ConstructInstantiator()]);
251 | $this->add('barfoo', 'My\Bar\Class', ['instantiator' => new ReflectionInstantiator()]);
252 | }
253 | }
254 | ```
255 |
256 | The library includes several instantiators by default:
257 |
258 | * ConstructInstantiator
259 | * PropertyInstantiator
260 | * SetInstantiator
261 | * ReflectionInstantiator
262 |
263 | By default, the `SetInstantiator` is used by Object and Collection nodes.
264 |
265 | InputHandlers
266 | -------------
267 |
268 | Linio Input supports portable, reusable InputHandlers via nesting. This is accomplished
269 | by including the `handler` to the options parameter when adding fields.
270 |
271 | Suppose your application deals with mailing addresses:
272 |
273 | ```php
274 | add('shipping_address', Address::class);
281 | $address->add('street', 'string');
282 | $address->add('city', 'string');
283 | $address->add('state', 'string');
284 | $address->add('zip_code', 'integer');
285 | }
286 | }
287 | ```
288 |
289 | Rather than duplicating this everywhere you need to handle an address, you can extract the
290 | address into its own InputHandler and re-use it throughout your application.
291 |
292 | ```php
293 | add('street', 'string');
300 | $address->add('city', 'string');
301 | $address->add('state', 'string');
302 | $address->add('zip_code', 'integer');
303 | }
304 | }
305 |
306 | class OrderHandler extends InputHander
307 | {
308 | public function define()
309 | {
310 | $this->add('shipping_address', Address::Class, ['handler' => new AddressHandler()]);
311 | }
312 | }
313 |
314 | class RegistrationHandler extends InputHander
315 | {
316 | public function define()
317 | {
318 | $this->add('home_address', Address::Class, ['handler' => new AddressHandler()]);
319 | }
320 | }
321 | ```
322 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linio/input",
3 | "description": "Abstracts HTTP request input handling, providing an easy interface for data hydration and validation",
4 | "keywords": ["linio", "input", "form"],
5 | "type": "library",
6 | "license": "BSD-3-Clause",
7 | "require": {
8 | "php": "^8.1",
9 | "ramsey/uuid": "^4.5",
10 | "doctrine/inflector": "^2.0"
11 | },
12 | "require-dev": {
13 | "friendsofphp/php-cs-fixer": "^3.11",
14 | "michaelmoussa/php-coverage-checker": "^1.1",
15 | "phpunit/phpunit": "^9.5",
16 | "phpspec/prophecy": "^1.15",
17 | "phpspec/prophecy-phpunit": "^2.0",
18 | "phpstan/phpstan": "^0.12"
19 | },
20 | "scripts": {
21 | "lint": [
22 | "php-cs-fixer fix --ansi --verbose --show-progress=dots"
23 | ],
24 | "lint:check": [
25 | "@lint --dry-run"
26 | ],
27 | "test:base": [
28 | "php -d pcov.enabled=1 vendor/bin/phpunit --color=always"
29 | ],
30 | "test": [
31 | "@test:base --log-junit build/junit.xml --coverage-xml build/coverage-xml --coverage-clover build/coverage-clover.xml"
32 | ],
33 | "test:with-html-coverage": [
34 | "@test:base --coverage-html build/coverage-html"
35 | ],
36 | "test:coverage-checker": [
37 | "php-coverage-checker build/coverage-clover.xml 92;"
38 | ],
39 | "test:check": [
40 | "if [ -f build/coverage-clover.xml ]; then rm build/coverage-clover.xml; echo '>>> REMOVED OLD CLOVER.XML BUILD FILE!'; fi; # comment trick to allow composer params :D",
41 | "@test",
42 | "@test:coverage-checker"
43 | ],
44 | "check": [
45 | "@lint:check",
46 | "@test:check",
47 | "@static-analysis"
48 | ],
49 | "static-analysis": [
50 | "phpstan analyse --ansi --memory-limit=-1"
51 | ]
52 | },
53 | "autoload": {
54 | "psr-4": {
55 | "Linio\\Component\\Input\\": "src"
56 | }
57 | },
58 | "autoload-dev": {
59 | "psr-4": {
60 | "Linio\\Component\\Input\\": "tests/"
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 0 # start increasing the level
3 | paths:
4 | - src
5 | - tests
6 | tmpDir: data/cache/phpstan
7 | reportUnmatchedIgnoredErrors: false
8 |
--------------------------------------------------------------------------------
/phpunit.xml.ci:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ./src
25 |
26 | src/Input/Constraint/ConstraintInterface.php
27 | src/Input/Transformer/TransformerInterface.php
28 | src/Input/InputTrait.php
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./src
14 |
15 |
16 | src/Input/Constraint/ConstraintInterface.php
17 | src/Input/Transformer/TransformerInterface.php
18 | src/Input/InputTrait.php
19 |
20 |
21 |
22 |
23 | ./tests
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/Constraint/Constraint.php:
--------------------------------------------------------------------------------
1 | errorMessage);
17 | }
18 |
19 | public function setErrorMessage(string $errorMessage): void
20 | {
21 | $this->errorMessage = $errorMessage;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Constraint/ConstraintInterface.php:
--------------------------------------------------------------------------------
1 | min = $min;
22 | $this->max = $max;
23 |
24 | $this->setErrorMessage($errorMessage ?? sprintf('Date is not between "%s" and "%s"', $this->min, $this->max));
25 | }
26 |
27 | public function validate($content): bool
28 | {
29 | if (!is_scalar($content)) {
30 | return false;
31 | }
32 |
33 | $date = new \DateTime($content);
34 |
35 | return $date >= new \DateTime($this->min) && $date <= new \DateTime($this->max);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Constraint/DateTime.php:
--------------------------------------------------------------------------------
1 | setErrorMessage($errorMessage ?? 'Invalid date/time format');
12 | }
13 |
14 | public function validate($content): bool
15 | {
16 | if (!is_string($content)) {
17 | return false;
18 | }
19 |
20 | $date = date_parse($content);
21 |
22 | return $date['error_count'] ? false : true;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Constraint/Email.php:
--------------------------------------------------------------------------------
1 | setErrorMessage($errorMessage ?? 'Invalid email format');
12 | }
13 |
14 | public function validate($content): bool
15 | {
16 | return (bool) filter_var($content, FILTER_VALIDATE_EMAIL);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Constraint/Enum.php:
--------------------------------------------------------------------------------
1 | enumValues = $enumValues;
22 | $this->strictType = $strictType;
23 |
24 | $this->setErrorMessage(
25 | $errorMessage ?? 'Invalid option for enum. Allowed options are: ' . implode(', ', $this->enumValues)
26 | );
27 | }
28 |
29 | public function validate($content): bool
30 | {
31 | if (!is_scalar($content)) {
32 | return false;
33 | }
34 |
35 | return in_array($content, $this->enumValues, $this->strictType);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Constraint/GuidValue.php:
--------------------------------------------------------------------------------
1 | setErrorMessage($errorMessage ?? static::ERROR_MESSAGE);
14 | }
15 |
16 | public function validate($content): bool
17 | {
18 | if (!is_string($content) || strlen($content) != 36) {
19 | return false;
20 | }
21 |
22 | return (bool) preg_match('/^[0-9a-fA-F]{8}\-([0-9a-fA-F]{4}\-){3}[0-9a-fA-F]{12}$/', $content);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Constraint/NativeEnum.php:
--------------------------------------------------------------------------------
1 | enumClass = $enumClass;
14 |
15 | $this->setErrorMessage(
16 | $errorMessage ?? 'Invalid option for a native PHP enum. Allowed options are: ' . json_encode($this->enumClass::cases())
17 | );
18 | }
19 |
20 | public function validate($content): bool
21 | {
22 | if (!is_scalar($content)) {
23 | return false;
24 | }
25 |
26 | return !($this->enumClass::tryFrom($content) === null);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Constraint/NotNull.php:
--------------------------------------------------------------------------------
1 | setErrorMessage($errorMessage ?? 'Unexpected empty content');
12 | }
13 |
14 | public function validate($content): bool
15 | {
16 | if ($content && is_string($content)) {
17 | $content = trim($content);
18 | }
19 |
20 | return $content !== null && $content !== '';
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Constraint/Pattern.php:
--------------------------------------------------------------------------------
1 | pattern = $pattern;
17 |
18 | $this->setErrorMessage($errorMessage ?? 'Required pattern does not match');
19 | }
20 |
21 | public function validate($content): bool
22 | {
23 | if (!is_scalar($content)) {
24 | return false;
25 | }
26 |
27 | if (!$content) {
28 | return false;
29 | }
30 |
31 | return (bool) preg_match($this->pattern, $content);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Constraint/Range.php:
--------------------------------------------------------------------------------
1 | min = $min;
22 | $this->max = $max;
23 |
24 | $this->setErrorMessage($errorMessage ?? sprintf('Value is not between %d and %d', $this->min, $this->max));
25 | }
26 |
27 | public function validate($content): bool
28 | {
29 | if (!is_scalar($content)) {
30 | return false;
31 | }
32 |
33 | if ($content === null) {
34 | return false;
35 | }
36 |
37 | return $content >= $this->min && $content <= $this->max;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Constraint/StringSize.php:
--------------------------------------------------------------------------------
1 | minSize = $minSize;
22 | $this->maxSize = $maxSize;
23 |
24 | $this->setErrorMessage(
25 | $errorMessage ?? sprintf('Content out of min/max limit sizes [%s, %s]', $this->minSize, $this->maxSize)
26 | );
27 | }
28 |
29 | public function validate($content): bool
30 | {
31 | if (!is_scalar($content)) {
32 | return false;
33 | }
34 |
35 | if ($content === null) {
36 | return false;
37 | }
38 |
39 | $size = strlen($content);
40 |
41 | return $size >= $this->minSize && $size <= $this->maxSize;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Constraint/Type.php:
--------------------------------------------------------------------------------
1 | type = $type;
17 |
18 | $this->setErrorMessage($errorMessage ?? 'Value does not match type: ' . $this->type);
19 | }
20 |
21 | public function validate($content): bool
22 | {
23 | return call_user_func('is_' . $this->type, $content);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Constraint/Url.php:
--------------------------------------------------------------------------------
1 | setErrorMessage($errorMessage ?? 'Invalid URL format');
12 | }
13 |
14 | public function validate($content): bool
15 | {
16 | return (bool) filter_var($content, FILTER_VALIDATE_URL);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Constraint/UuidValue.php:
--------------------------------------------------------------------------------
1 | field = $field;
17 | }
18 |
19 | public function getField(): string
20 | {
21 | return $this->field;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/TransformationException.php:
--------------------------------------------------------------------------------
1 | root = new BaseNode();
35 | $this->typeHandler = $typeHandler ?? new TypeHandler();
36 | $this->root->setTypeHandler($this->typeHandler);
37 | }
38 |
39 | public function add(string $key, string $type, array $options = [], InputHandler $handler = null): BaseNode
40 | {
41 | return $this->root->add($key, $type, $options, $handler);
42 | }
43 |
44 | public function remove(string $key): void
45 | {
46 | $this->root->remove($key);
47 | }
48 |
49 | public function getRoot(): BaseNode
50 | {
51 | return $this->root;
52 | }
53 |
54 | public function setRootType(string $type): void
55 | {
56 | $this->root = $this->typeHandler->getType($type);
57 | }
58 |
59 | public function bind(array $input): void
60 | {
61 | $this->define();
62 |
63 | try {
64 | $this->output = $this->root->getValue('root', $this->root->walk($input));
65 | } catch (RequiredFieldException $exception) {
66 | $this->errors[] = 'Missing required field: ' . $exception->getField();
67 | } catch (\RuntimeException $exception) {
68 | $this->errors[] = $exception->getMessage();
69 | }
70 | }
71 |
72 | public function getData($index = null)
73 | {
74 | if (!$this->isValid()) {
75 | throw new \RuntimeException($this->getErrorsAsString());
76 | }
77 |
78 | if ($index) {
79 | return $this->output[$index];
80 | }
81 |
82 | return $this->output;
83 | }
84 |
85 | public function hasData($index)
86 | {
87 | return isset($this->output[$index]);
88 | }
89 |
90 | public function isValid(): bool
91 | {
92 | return empty($this->errors);
93 | }
94 |
95 | public function getErrors(): array
96 | {
97 | return $this->errors;
98 | }
99 |
100 | public function getErrorsAsString(): string
101 | {
102 | return implode(', ', $this->errors);
103 | }
104 |
105 | abstract public function define();
106 | }
107 |
--------------------------------------------------------------------------------
/src/Instantiator/ConstructInstantiator.php:
--------------------------------------------------------------------------------
1 | build();
18 | $object = new $class();
19 |
20 | foreach ($data as $key => $value) {
21 | $property = $inflector->camelize($key);
22 | $object->$property = $value;
23 | }
24 |
25 | return $object;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Instantiator/ReflectionInstantiator.php:
--------------------------------------------------------------------------------
1 | build();
14 | $object = new $class();
15 | $reflection = new \ReflectionClass($object);
16 |
17 | foreach ($data as $key => $value) {
18 | $property = $reflection->getProperty($inflector->camelize($key));
19 | if (!$property->isPublic()) {
20 | $property->setAccessible(true);
21 | }
22 |
23 | $property->setValue($object, $value);
24 | }
25 |
26 | return $object;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Instantiator/SetInstantiator.php:
--------------------------------------------------------------------------------
1 | build();
14 | $object = new $class();
15 |
16 | foreach ($data as $key => $value) {
17 | $method = 'set' . $inflector->classify($key);
18 | $object->$method($value);
19 | }
20 |
21 | return $object;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Node/BaseNode.php:
--------------------------------------------------------------------------------
1 | constraints = $constraints;
67 |
68 | return $this;
69 | }
70 |
71 | public function addConstraint(ConstraintInterface $constraint): self
72 | {
73 | $this->constraints[] = $constraint;
74 |
75 | return $this;
76 | }
77 |
78 | public function addConstraints(array $constraints): self
79 | {
80 | $this->constraints = array_merge($this->constraints, $constraints);
81 |
82 | return $this;
83 | }
84 |
85 | public function setTransformer(TransformerInterface $transformer): self
86 | {
87 | $this->transformer = $transformer;
88 |
89 | return $this;
90 | }
91 |
92 | public function setInstantiator(InstantiatorInterface $instantiator): self
93 | {
94 | $this->instantiator = $instantiator;
95 |
96 | return $this;
97 | }
98 |
99 | public function setTypeHandler(TypeHandler $typeHandler): self
100 | {
101 | $this->typeHandler = $typeHandler;
102 |
103 | return $this;
104 | }
105 |
106 | public function setType(string $type): self
107 | {
108 | $this->type = $type;
109 |
110 | return $this;
111 | }
112 |
113 | public function setTypeAlias(string $typeAlias): self
114 | {
115 | $this->typeAlias = $typeAlias;
116 |
117 | return $this;
118 | }
119 |
120 | public function getTypeAlias(): string
121 | {
122 | return $this->typeAlias;
123 | }
124 |
125 | public function setRequired(bool $required): self
126 | {
127 | $this->required = $required;
128 |
129 | return $this;
130 | }
131 |
132 | public function setDefault($default): self
133 | {
134 | $this->default = $default;
135 |
136 | return $this;
137 | }
138 |
139 | public function setAllowNull(bool $allowNull): self
140 | {
141 | $this->allowNull = $allowNull;
142 |
143 | return $this;
144 | }
145 |
146 | public function getDefault()
147 | {
148 | return $this->default;
149 | }
150 |
151 | public function hasDefault(): bool
152 | {
153 | return (bool) $this->default;
154 | }
155 |
156 | public function add(string $key, string $type, array $options = [], InputHandler $handler = null): BaseNode
157 | {
158 | $child = $this->typeHandler->getType($type);
159 |
160 | if (isset($handler)) {
161 | $child = $child->setHandler($handler, $type);
162 | }
163 |
164 | if (isset($options['handler']) && !isset($handler)) {
165 | $child = $child->setHandler($options['handler'], $type);
166 | }
167 |
168 | if (isset($options['required'])) {
169 | $child->setRequired($options['required']);
170 | }
171 |
172 | if (isset($options['default'])) {
173 | $child->setDefault($options['default']);
174 | }
175 |
176 | if (isset($options['instantiator'])) {
177 | $child->setInstantiator($options['instantiator']);
178 | }
179 |
180 | if (isset($options['transformer'])) {
181 | $child->setTransformer($options['transformer']);
182 | }
183 |
184 | if (isset($options['constraints'])) {
185 | $child->addConstraints($options['constraints']);
186 | }
187 |
188 | if (isset($options['allow_null'])) {
189 | $child->setAllowNull($options['allow_null']);
190 | }
191 |
192 | $this->children[$key] = $child;
193 |
194 | return $child;
195 | }
196 |
197 | public function remove(string $key): void
198 | {
199 | unset($this->children[$key]);
200 | }
201 |
202 | /**
203 | * @return BaseNode[]
204 | */
205 | public function getChildren(): array
206 | {
207 | return $this->children;
208 | }
209 |
210 | public function hasChildren(): bool
211 | {
212 | return !empty($this->children);
213 | }
214 |
215 | public function isRequired(): bool
216 | {
217 | if ($this->hasDefault()) {
218 | return false;
219 | }
220 |
221 | return $this->required;
222 | }
223 |
224 | public function allowNull(): bool
225 | {
226 | return $this->allowNull;
227 | }
228 |
229 | public function getValue(string $field, $value)
230 | {
231 | if ($this->allowNull() && $value === null) {
232 | return $value;
233 | }
234 |
235 | $this->checkConstraints($field, $value);
236 |
237 | if ($this->transformer) {
238 | return $this->transformer->transform($value);
239 | }
240 |
241 | return $value;
242 | }
243 |
244 | public function walk($input)
245 | {
246 | if (!is_array($input)) {
247 | return $input;
248 | }
249 |
250 | if (!$this->hasChildren()) {
251 | return $input;
252 | }
253 |
254 | $result = [];
255 |
256 | foreach ($this->getChildren() as $field => $config) {
257 | if (!array_key_exists($field, $input)) {
258 | if ($config->isRequired()) {
259 | throw new RequiredFieldException($field);
260 | }
261 |
262 | if (!$config->hasDefault()) {
263 | continue;
264 | }
265 |
266 | $input[$field] = $config->getDefault();
267 | }
268 |
269 | $result[$field] = $config->getValue($field, $config->walk($input[$field]));
270 | }
271 |
272 | return $result;
273 | }
274 |
275 | protected function checkConstraints(string $field, $value): void
276 | {
277 | foreach ($this->constraints as $constraint) {
278 | if (!$constraint->validate($value) && ($this->isRequired() || $this->checkIfFieldValueIsSpecified($value))) {
279 | throw new InvalidConstraintException($constraint->getErrorMessage($field));
280 | }
281 | }
282 | }
283 |
284 | private function checkIfFieldValueIsSpecified($value): bool
285 | {
286 | return $this->type === 'string' || $this->type === 'array' ? !empty($value) : $value !== null;
287 | }
288 |
289 | private function setHandler(InputHandler $handler, string $type): self
290 | {
291 | $handler->setRootType($type);
292 | $handler->define();
293 |
294 | return $handler->getRoot();
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/src/Node/BoolNode.php:
--------------------------------------------------------------------------------
1 | addConstraint(new Type('bool'));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Node/CollectionNode.php:
--------------------------------------------------------------------------------
1 | checkConstraints($field, $value);
14 |
15 | $items = [];
16 |
17 | foreach ($value as $collectionValue) {
18 | $items[] = $this->instantiator->instantiate($this->type, $collectionValue);
19 | }
20 |
21 | return $items;
22 | }
23 |
24 | public function walk($input)
25 | {
26 | $result = [];
27 |
28 | if (!$this->hasChildren()) {
29 | return $input;
30 | }
31 |
32 | foreach ($input as $inputItem) {
33 | $itemResult = [];
34 |
35 | foreach ($this->getChildren() as $field => $config) {
36 | if (!array_key_exists($field, $inputItem)) {
37 | if ($config->isRequired()) {
38 | throw new RequiredFieldException($field);
39 | }
40 |
41 | if (!$config->hasDefault()) {
42 | continue;
43 | }
44 |
45 | $inputItem[$field] = $config->getDefault();
46 | }
47 |
48 | $itemResult[$field] = $config->getValue($field, $config->walk($inputItem[$field]));
49 | }
50 |
51 | $result[] = $itemResult;
52 | }
53 |
54 | return $result;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Node/DateTimeNode.php:
--------------------------------------------------------------------------------
1 | addConstraint(new DateTime());
15 | $this->transformer = new DateTimeTransformer();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Node/FloatNode.php:
--------------------------------------------------------------------------------
1 | addConstraint(new Type('float'));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Node/IntNode.php:
--------------------------------------------------------------------------------
1 | addConstraint(new Type('int'));
14 | }
15 |
16 | public function hasDefault(): bool
17 | {
18 | return is_int($this->default);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Node/NumericNode.php:
--------------------------------------------------------------------------------
1 | addConstraint(new Type('numeric'));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Node/ObjectNode.php:
--------------------------------------------------------------------------------
1 | checkConstraints($field, $value);
12 |
13 | return $this->instantiator->instantiate($this->type, $value);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Node/ScalarCollectionNode.php:
--------------------------------------------------------------------------------
1 | addConstraint(new Type('array'));
15 | }
16 |
17 | public function getValue(string $field, $value)
18 | {
19 | $this->checkConstraints($field, $value);
20 |
21 | foreach ($value as $scalarValue) {
22 | if (!call_user_func('is_' . $this->type, $scalarValue)) {
23 | throw new InvalidConstraintException(sprintf('Value "%s" is not of type %s', $scalarValue, $this->type));
24 | }
25 | }
26 |
27 | return $value;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Node/StringNode.php:
--------------------------------------------------------------------------------
1 | addConstraint(new Type('string'));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/SchemaBuilder.php:
--------------------------------------------------------------------------------
1 | define();
14 |
15 | return $this->walk($inputHandler->getRoot());
16 | }
17 |
18 | protected function walk(BaseNode $node): array
19 | {
20 | if (!$node->hasChildren()) {
21 | return [];
22 | }
23 |
24 | foreach ($node->getChildren() as $field => $childNode) {
25 | $schema[$field]['type'] = $childNode->getTypeAlias();
26 | $schema[$field]['required'] = $childNode->isRequired();
27 | $schema[$field]['default'] = $childNode->getDefault();
28 | $schema[$field]['nullable'] = $childNode->allowNull();
29 | $schema[$field]['children'] = $this->walk($childNode);
30 | }
31 |
32 | return $schema;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Transformer/DateTimeTransformer.php:
--------------------------------------------------------------------------------
1 | getMessage());
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/TypeHandler.php:
--------------------------------------------------------------------------------
1 | types = [
35 | 'bool' => BoolNode::class,
36 | 'int' => IntNode::class,
37 | 'float' => FloatNode::class,
38 | 'double' => FloatNode::class,
39 | 'numeric' => NumericNode::class,
40 | 'string' => StringNode::class,
41 | 'array' => BaseNode::class,
42 | 'object' => ObjectNode::class,
43 | 'datetime' => DateTimeNode::class,
44 | ];
45 |
46 | $this->defaultInstantiator = new SetInstantiator();
47 | }
48 |
49 | public function addType(string $name, string $class): void
50 | {
51 | $this->types[$name] = $class;
52 | }
53 |
54 | public function getType(string $name): BaseNode
55 | {
56 | if (isset($this->types[$name])) {
57 | $type = new $this->types[$name]();
58 | $type->setTypeAlias($name);
59 | $type->setTypeHandler($this);
60 |
61 | return $type;
62 | }
63 |
64 | if ($this->isScalarCollectionType($name)) {
65 | $type = new ScalarCollectionNode();
66 | $type->setType($this->getCollectionType($name));
67 | $type->setTypeAlias($name);
68 | $type->setTypeHandler($this);
69 |
70 | return $type;
71 | }
72 |
73 | if ($this->isClassType($name)) {
74 | $type = new ObjectNode();
75 | $type->setType($name);
76 | $type->setTypeAlias('object');
77 | $type->setTypeHandler($this);
78 | $type->setInstantiator($this->defaultInstantiator);
79 |
80 | return $type;
81 | }
82 |
83 | if ($this->isCollectionType($name)) {
84 | $type = new CollectionNode();
85 | $type->setType($this->getCollectionType($name));
86 | $type->setTypeAlias('object[]');
87 | $type->setTypeHandler($this);
88 | $type->setInstantiator($this->defaultInstantiator);
89 |
90 | return $type;
91 | }
92 |
93 | throw new \InvalidArgumentException('Unknown type name: ' . $name);
94 | }
95 |
96 | protected function isClassType(string $type): bool
97 | {
98 | return (class_exists($type) || interface_exists($type)) && $type != 'datetime';
99 | }
100 |
101 | protected function isCollectionType(string $type): bool
102 | {
103 | $collectionType = $this->getCollectionType($type);
104 |
105 | if (!class_exists($collectionType)) {
106 | return false;
107 | }
108 |
109 | return true;
110 | }
111 |
112 | protected function isScalarCollectionType(string $type): bool
113 | {
114 | $collectionType = $this->getCollectionType($type);
115 |
116 | if (!function_exists('is_' . $collectionType)) {
117 | return false;
118 | }
119 |
120 | return true;
121 | }
122 |
123 | protected function getCollectionType(string $type): string
124 | {
125 | $pos = strrpos($type, '[]');
126 |
127 | if ($pos === false) {
128 | return $type;
129 | }
130 |
131 | return substr($type, 0, $pos);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/tests/Constraint/DateRangeTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('yesterday'));
15 | $this->assertFalse($constraint->validate('+8 days'));
16 |
17 | $this->assertFalse($constraint->validate(['now']));
18 | $obj = new \stdClass();
19 | $obj->var1 = 'now';
20 | $this->assertFalse($constraint->validate($obj));
21 | }
22 |
23 | public function testIsCheckingValidData(): void
24 | {
25 | $constraint = new DateRange('today', '+3 days');
26 | $this->assertTrue($constraint->validate('today'));
27 | $this->assertTrue($constraint->validate('tomorrow'));
28 | }
29 |
30 | public function testIsGettingErrorMessage(): void
31 | {
32 | $constraint = new DateRange('today', '+3 days');
33 | $this->assertFalse($constraint->validate('yesterday'));
34 | $this->assertEquals('[field] Date is not between "today" and "+3 days"', $constraint->getErrorMessage('field'));
35 | }
36 |
37 | public function testErrorMessageIsCustomizable(): void
38 | {
39 | $constraint = new DateRange('today', '+3days', 'CUSTOM!');
40 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Constraint/DateTimeTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('foobar@baz.com'));
15 | $this->assertFalse($constraint->validate('2018-01-99'));
16 | $this->assertFalse($constraint->validate(123));
17 | }
18 |
19 | public function testIsCheckingValidData(): void
20 | {
21 | $constraint = new DateTime();
22 | $this->assertTrue($constraint->validate('2018-01-01'));
23 | $this->assertTrue($constraint->validate('2010-12-31T00:00:00+00:00'));
24 | $this->assertTrue($constraint->validate('2006-12-12 10:00:00.5'));
25 | }
26 |
27 | public function testIsGettingErrorMessage(): void
28 | {
29 | $constraint = new DateTime();
30 | $this->assertFalse($constraint->validate('foo/bar'));
31 | $this->assertEquals('[field] Invalid date/time format', $constraint->getErrorMessage('field'));
32 | }
33 |
34 | public function testErrorMessageIsCustomizable(): void
35 | {
36 | $constraint = new DateTime('CUSTOM!');
37 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Constraint/EmailTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('foobar@bazcom'));
15 | $this->assertFalse($constraint->validate('foobar.com'));
16 | $this->assertFalse($constraint->validate('fooz@bar'));
17 | }
18 |
19 | public function testIsCheckingValidData(): void
20 | {
21 | $constraint = new Email();
22 | $this->assertTrue($constraint->validate('foo@bar.com'));
23 | $this->assertTrue($constraint->validate('foo@bar.cl'));
24 | $this->assertTrue($constraint->validate('foo@bar.pe'));
25 | }
26 |
27 | public function testIsGettingErrorMessage(): void
28 | {
29 | $constraint = new Email();
30 | $this->assertFalse($constraint->validate('foobar.com'));
31 | $this->assertEquals('[field] Invalid email format', $constraint->getErrorMessage('field'));
32 | }
33 |
34 | public function testErrorMessageIsCustomizable(): void
35 | {
36 | $constraint = new Email('CUSTOM!');
37 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Constraint/EnumTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('test'), 'The "test" value is not part of the Enum');
15 | $this->assertFalse($constraint->validate('blah'), 'The "blah" value is not part of the Enum');
16 |
17 | $this->assertFalse($constraint->validate(['foo']));
18 | $obj = new \stdClass();
19 | $obj->var1 = 'foo';
20 | $this->assertFalse($constraint->validate($obj));
21 | }
22 |
23 | public function testIsCheckingInvalidDataWithStrictType(): void
24 | {
25 | $constraint = new Enum(['01', '02', '03', '4', 5], null, true);
26 | $this->assertFalse($constraint->validate(2), 'The "2" value is not part of the Enum');
27 | $this->assertFalse($constraint->validate(4), 'The "01" value is not part of the Enum');
28 | $this->assertFalse($constraint->validate('5'), 'The "5" value is not part of the Enum');
29 | }
30 |
31 | public function testIsCheckingValidData(): void
32 | {
33 | $constraint = new Enum(['foo', 'bar', '08', 7]);
34 | $this->assertTrue($constraint->validate('foo'), 'The "foo" value should be part of the Enum');
35 | $this->assertTrue($constraint->validate('bar'), 'The "bar" value should be part of the Enum');
36 | $this->assertTrue($constraint->validate(8), 'The "8" value should be part of the Enum');
37 | $this->assertTrue($constraint->validate('07'), 'The "07" value should be part of the Enum');
38 | }
39 |
40 | public function testIsCheckingValidDataWithStrictType(): void
41 | {
42 | $constraint = new Enum(['01', '02', '03', '4'], null, true);
43 | $this->assertTrue($constraint->validate('02'), 'The "02" value should be part of the Enum');
44 | $this->assertTrue($constraint->validate('4'), 'The "4" value should be part of the Enum');
45 | }
46 |
47 | public function testIsGettingErrorMessage(): void
48 | {
49 | $constraint = new Enum(['foo', 'bar']);
50 | $this->assertFalse($constraint->validate('test'), 'The "test" value is not part of the Enum');
51 | $this->assertEquals('[field] Invalid option for enum. Allowed options are: foo, bar', $constraint->getErrorMessage('field'));
52 | }
53 |
54 | public function testErrorMessageIsCustomizable(): void
55 | {
56 | $constraint = new Enum(['foo', 'bar'], 'CUSTOM!');
57 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Constraint/GuidValueTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('0dga84b2-639d-4b06-bc87-7ab5ae3f5d4f'));
15 | $this->assertFalse($constraint->validate('0dca4b2-639d-4b06-bc87-7ab5ae3f5d4f'));
16 | $this->assertFalse($constraint->validate('0dca84b2-19d-4b06-bc87-7ab5ae3f5d4f'));
17 | $this->assertFalse($constraint->validate('0dca84b2-639d-406-bc87-7ab5ae3f5d4f'));
18 | $this->assertFalse($constraint->validate('0dca84b2-639d-4b06-c87-7ab5ae3f5d4f'));
19 | $this->assertFalse($constraint->validate('0dca84b2-639d-4b06-bc87-ab5ae3f5d4f'));
20 | $this->assertFalse($constraint->validate(null));
21 | $this->assertFalse($constraint->validate([]));
22 | $this->assertFalse($constraint->validate(new \stdClass()));
23 | }
24 |
25 | public function testIsCheckingValidData(): void
26 | {
27 | $constraint = new GuidValue();
28 | $this->assertTrue($constraint->validate('0dca84b2-639d-4b06-bc87-7ab5ae3f5d4f'));
29 | $this->assertTrue($constraint->validate('0DCA84B2-639D-4B06-BC87-7AB5AE3F5D4F'));
30 | }
31 |
32 | public function testIsGettingErrorMessage(): void
33 | {
34 | $constraint = new GuidValue();
35 | $this->assertFalse($constraint->validate('0dga84b2-639d-4b06-bc87-7ab5ae3f5d4f'));
36 | $this->assertEquals('[field] Invalid GUID format', $constraint->getErrorMessage('field'));
37 | }
38 |
39 | public function testErrorMessageIsCustomizable(): void
40 | {
41 | $constraint = new GuidValue('CUSTOM!');
42 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Constraint/NativeEnumTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('test'));
15 | $this->assertFalse($constraint->validate('blah'));
16 |
17 | $this->assertFalse($constraint->validate(['foo']));
18 | $obj = new \stdClass();
19 | $obj->var1 = 'foo';
20 | $this->assertFalse($constraint->validate($obj));
21 | }
22 |
23 | public function testIsCheckingValidData(): void
24 | {
25 | $constraint = new NativeEnum(FakeEnum::class);
26 | $this->assertTrue($constraint->validate('FOO'));
27 | $this->assertTrue($constraint->validate('BAR'));
28 | }
29 |
30 | public function testIsGettingErrorMessage(): void
31 | {
32 | $constraint = new NativeEnum(FakeEnum::class);
33 | $this->assertFalse($constraint->validate('test'));
34 | $this->assertEquals('[["FOO","BAR"]] Invalid option for a native PHP enum. Allowed options are: ["FOO","BAR"]', $constraint->getErrorMessage('["FOO","BAR"]'));
35 | }
36 | }
37 |
38 | enum FakeEnum: string
39 | {
40 | case Foo = 'FOO';
41 | case Bar = 'BAR';
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Constraint/NotNullTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate(null));
15 | $this->assertFalse($constraint->validate(''));
16 | $this->assertFalse($constraint->validate(''));
17 | $this->assertFalse($constraint->validate(' '));
18 | }
19 |
20 | public function testIsCheckingValidData(): void
21 | {
22 | $constraint = new NotNull();
23 | $this->assertTrue($constraint->validate(' test '));
24 | $this->assertTrue($constraint->validate(0));
25 |
26 | $this->assertTrue($constraint->validate(['']));
27 | $obj = new \stdClass();
28 | $obj->var1 = '';
29 | $this->assertTrue($constraint->validate($obj));
30 | }
31 |
32 | public function testIsGettingErrorMessage(): void
33 | {
34 | $constraint = new NotNull();
35 | $this->assertFalse($constraint->validate(null));
36 | $this->assertEquals('[field] Unexpected empty content', $constraint->getErrorMessage('field'));
37 | }
38 |
39 | public function testErrorMessageIsCustomizable(): void
40 | {
41 | $constraint = new NotNull('CUSTOM!');
42 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Constraint/PatternTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate(null));
15 | $this->assertFalse($constraint->validate(' 2014-04-22 '));
16 | $this->assertFalse($constraint->validate('2014-04-2'));
17 |
18 | $this->assertFalse($constraint->validate(['2014-04-22']));
19 | $obj = new \stdClass();
20 | $obj->var1 = '2014-04-22';
21 | $this->assertFalse($constraint->validate($obj));
22 | }
23 |
24 | public function testIsCheckingValidData(): void
25 | {
26 | $constraint = new Pattern('/^\d{4}\-\d{2}-\d{2}$/');
27 | $this->assertTrue($constraint->validate('2014-04-22'));
28 | }
29 |
30 | public function testIsGettingErrorMessage(): void
31 | {
32 | $constraint = new Pattern('/^\d{4}\-\d{2}-\d{2}$/');
33 | $this->assertFalse($constraint->validate(null));
34 | $this->assertEquals('[field] Required pattern does not match', $constraint->getErrorMessage('field'));
35 | }
36 |
37 | public function testErrorMessageIsCustomizable(): void
38 | {
39 | $constraint = new Pattern('/^\d{4}\-\d{2}-\d{2}$/', 'CUSTOM!');
40 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Constraint/RangeTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate(120));
15 | $this->assertFalse($constraint->validate(101));
16 | $this->assertFalse($constraint->validate(null));
17 |
18 | $this->assertFalse($constraint->validate([75]));
19 | $obj = new \stdClass();
20 | $obj->var1 = 75;
21 | $this->assertFalse($constraint->validate($obj));
22 | }
23 |
24 | public function testIsCheckingValidData(): void
25 | {
26 | $constraint = new Range(50, 100);
27 | $this->assertTrue($constraint->validate(50));
28 | $this->assertTrue($constraint->validate(85));
29 | }
30 |
31 | public function testIsGettingErrorMessage(): void
32 | {
33 | $constraint = new Range(50, 100);
34 | $this->assertFalse($constraint->validate(14));
35 | $this->assertEquals('[field] Value is not between 50 and 100', $constraint->getErrorMessage('field'));
36 | }
37 |
38 | public function testErrorMessageIsCustomizable(): void
39 | {
40 | $constraint = new Range(50, 100, 'CUSTOM!');
41 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Constraint/StringSizeTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint1->validate('ab'));
15 |
16 | $constraint2 = new StringSize(3, 5);
17 | $this->assertFalse($constraint2->validate('ab'));
18 | $this->assertFalse($constraint2->validate('abcdef'));
19 |
20 | $this->assertFalse($constraint1->validate(null));
21 | $this->assertFalse($constraint2->validate(null));
22 |
23 | $this->assertFalse($constraint1->validate(['abcd']));
24 | $this->assertFalse($constraint2->validate(['abcd']));
25 |
26 | $obj = new \stdClass();
27 | $obj->var1 = 'abcd';
28 | $this->assertFalse($constraint1->validate($obj));
29 | $this->assertFalse($constraint2->validate($obj));
30 | }
31 |
32 | public function testIsCheckingValidData(): void
33 | {
34 | $constraint1 = new StringSize(3);
35 | $this->assertTrue($constraint1->validate('abc'));
36 | $this->assertTrue($constraint1->validate('abcdefghijklmnopqrstuvxywz'));
37 |
38 | $constraint2 = new StringSize(3, 5);
39 | $this->assertTrue($constraint2->validate('abc'));
40 | $this->assertTrue($constraint2->validate('abcd'));
41 | $this->assertTrue($constraint2->validate('abce'));
42 | }
43 |
44 | public function testIsGettingErrorMessage(): void
45 | {
46 | $constraint = new StringSize(3);
47 | $this->assertFalse($constraint->validate('ab'));
48 | $this->assertEquals(sprintf('[field] Content out of min/max limit sizes [3, %s]', PHP_INT_MAX), $constraint->getErrorMessage('field'));
49 |
50 | $constraint = new StringSize(3, 5);
51 | $this->assertFalse($constraint->validate('ab'));
52 | $this->assertEquals('[field] Content out of min/max limit sizes [3, 5]', $constraint->getErrorMessage('field'));
53 | }
54 |
55 | public function testErrorMessageIsCustomizable(): void
56 | {
57 | $constraint = new StringSize(1, 2, 'CUSTOM!');
58 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/Constraint/TypeTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('test'));
15 | $this->assertFalse($constraint->validate('2'));
16 | }
17 |
18 | public function testIsCheckingValidData(): void
19 | {
20 | $constraint = new Type('int');
21 | $this->assertTrue($constraint->validate(2));
22 | $this->assertTrue($constraint->validate(123));
23 | }
24 |
25 | public function testIsGettingErrorMessage(): void
26 | {
27 | $constraint = new Type('int');
28 | $this->assertFalse($constraint->validate('test'));
29 | $this->assertEquals('[field] Value does not match type: int', $constraint->getErrorMessage('field'));
30 | }
31 |
32 | public function testErrorMessageIsCustomizable(): void
33 | {
34 | $constraint = new Type('int', 'CUSTOM!');
35 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Constraint/UrlTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('http//foobar.com'));
15 | $this->assertFalse($constraint->validate('foobarcom'));
16 | $this->assertFalse($constraint->validate('foobar.com'));
17 | $this->assertFalse($constraint->validate('www.foobar.com'));
18 | $this->assertFalse($constraint->validate('www.foábar.com'));
19 | }
20 |
21 | public function testIsCheckingValidData(): void
22 | {
23 | $constraint = new Url();
24 | $this->assertTrue($constraint->validate('http://foobar.com'));
25 | $this->assertTrue($constraint->validate('https://foobar.com'));
26 | $this->assertTrue($constraint->validate('ssh://foobar.com'));
27 | }
28 |
29 | public function testIsGettingErrorMessage(): void
30 | {
31 | $constraint = new Url();
32 | $this->assertFalse($constraint->validate('foobarcom'));
33 | $this->assertEquals('[field] Invalid URL format', $constraint->getErrorMessage('field'));
34 | }
35 |
36 | public function testErrorMessageIsCustomizable(): void
37 | {
38 | $constraint = new Url('CUSTOM!');
39 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Constraint/UuidValueTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($constraint->validate('0dga84b2-639d-4b06-bc87-7ab5ae3f5d4f'));
15 | $this->assertFalse($constraint->validate('0dca4b2-639d-4b06-bc87-7ab5ae3f5d4f'));
16 | $this->assertFalse($constraint->validate('0dca84b2-19d-4b06-bc87-7ab5ae3f5d4f'));
17 | $this->assertFalse($constraint->validate('0dca84b2-639d-406-bc87-7ab5ae3f5d4f'));
18 | $this->assertFalse($constraint->validate('0dca84b2-639d-4b06-c87-7ab5ae3f5d4f'));
19 | $this->assertFalse($constraint->validate('0dca84b2-639d-4b06-bc87-ab5ae3f5d4f'));
20 | $this->assertFalse($constraint->validate(null));
21 | $this->assertFalse($constraint->validate([]));
22 | $this->assertFalse($constraint->validate(new \stdClass()));
23 | }
24 |
25 | public function testIsCheckingValidData(): void
26 | {
27 | $constraint = new UuidValue();
28 | $this->assertTrue($constraint->validate('0dca84b2-639d-4b06-bc87-7ab5ae3f5d4f'));
29 | $this->assertTrue($constraint->validate('0DCA84B2-639D-4B06-BC87-7AB5AE3F5D4F'));
30 | }
31 |
32 | public function testIsGettingErrorMessage(): void
33 | {
34 | $constraint = new UuidValue();
35 | $this->assertFalse($constraint->validate('0dga84b2-639d-4b06-bc87-7ab5ae3f5d4f'));
36 | $this->assertEquals('[field] Invalid UUID format', $constraint->getErrorMessage('field'));
37 | }
38 |
39 | public function testErrorMessageIsCustomizable(): void
40 | {
41 | $constraint = new UuidValue('CUSTOM!');
42 | $this->assertSame('[field] CUSTOM!', $constraint->getErrorMessage('field'));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/InputHandlerTest.php:
--------------------------------------------------------------------------------
1 | name;
26 | }
27 |
28 | public function setName($name): void
29 | {
30 | $this->name = $name;
31 | }
32 |
33 | public function getAge()
34 | {
35 | return $this->age;
36 | }
37 |
38 | public function setAge($age): void
39 | {
40 | $this->age = $age;
41 | }
42 |
43 | public function getRelated(): TestUser
44 | {
45 | return $this->related;
46 | }
47 |
48 | public function setRelated(TestUser $related): void
49 | {
50 | $this->related = $related;
51 | }
52 |
53 | public function setIsActive(bool $isActive): void
54 | {
55 | $this->isActive = $isActive;
56 | }
57 |
58 | public function setBirthday(\DateTime $birthday): void
59 | {
60 | $this->birthday = $birthday;
61 | }
62 | }
63 |
64 | class TestInputHandler extends InputHandler
65 | {
66 | public function define(): void
67 | {
68 | $this->add('title', 'string');
69 | $this->add('size', 'int');
70 | $this->add('dimensions', 'int[]');
71 | $this->add('date', 'datetime');
72 | $this->add('metadata', 'array');
73 |
74 | $simple = $this->add('simple', 'array');
75 | $simple->add('title', 'string', ['default' => 'Barfoo']);
76 | $simple->add('size', 'int', ['required' => false, 'default' => 15]);
77 | $simple->add('date', 'datetime');
78 |
79 | $author = $this->add('author', 'Linio\Component\Input\TestUser');
80 | $author->add('name', 'string');
81 | $author->add('age', 'int');
82 | $author->add('is_active', 'bool', ['required' => false]);
83 | $related = $author->add('related', 'Linio\Component\Input\TestUser');
84 | $related->add('name', 'string');
85 | $related->add('age', 'int');
86 |
87 | $fans = $this->add('fans', 'Linio\Component\Input\TestUser[]');
88 | $fans->add('name', 'string');
89 | $fans->add('age', 'int');
90 | $fans->add('birthday', 'datetime');
91 | }
92 | }
93 |
94 | class TestRecursiveInputHandler extends InputHandler
95 | {
96 | public function define(): void
97 | {
98 | $this->add('title', 'string');
99 | $this->add('size', 'int');
100 | $this->add('child', \stdClass::class, ['handler' => new TestInputHandler(), 'instantiator' => new PropertyInstantiator()]);
101 | }
102 | }
103 |
104 | class TestRecursiveInputHandlerExplicit extends InputHandler
105 | {
106 | public function define(): void
107 | {
108 | $this->add('title', 'string');
109 | $this->add('size', 'int');
110 | $this->add('child', \stdClass::class, ['instantiator' => new PropertyInstantiator()], new TestInputHandler());
111 | }
112 | }
113 |
114 | class TestNullableInputHandler extends InputHandler
115 | {
116 | public function define(): void
117 | {
118 | $this->add('name', 'string');
119 | $this->add('address', 'string', ['allow_null' => true]);
120 | }
121 | }
122 |
123 | class TestNullableRecursiveInputHandler extends InputHandler
124 | {
125 | public function define(): void
126 | {
127 | $this->add('type', 'string');
128 | $this->add('data', \stdClass::class, [
129 | 'handler' => new TestNullableInputHandler(),
130 | 'instantiator' => new PropertyInstantiator(),
131 | 'allow_null' => true,
132 | ]);
133 | }
134 | }
135 |
136 | class TestInputHandlerCascade extends InputHandler
137 | {
138 | public function define(): void
139 | {
140 | $this->add('name', 'string')
141 | ->setRequired(true)
142 | ->addConstraint(new StringSize(1, 80));
143 |
144 | $this->add('age', 'int')
145 | ->setRequired(true)
146 | ->addConstraint(new Range(1, 99));
147 |
148 | $this->add('gender', 'string')
149 | ->setRequired(true)
150 | ->addConstraint(new Enum(['male', 'female', 'other']));
151 |
152 | $this->add('birthday', 'datetime')
153 | ->setRequired(false);
154 |
155 | $this->add('email', 'string')
156 | ->setRequired(false)
157 | ->addConstraint(new Email());
158 | }
159 | }
160 |
161 | class InputHandlerTest extends TestCase
162 | {
163 | public function testIsHandlingBasicInput(): void
164 | {
165 | $input = [
166 | 'title' => 'Foobar',
167 | 'size' => 35,
168 | 'dimensions' => [11, 22, 33],
169 | 'date' => '2015-01-01 22:50',
170 | 'metadata' => [
171 | 'foo' => 'bar',
172 | ],
173 | 'simple' => [
174 | 'date' => '2015-01-01 22:50',
175 | ],
176 | 'author' => [
177 | 'name' => 'Barfoo',
178 | 'age' => 28,
179 | 'related' => [
180 | 'name' => 'Barfoo',
181 | 'age' => 28,
182 | ],
183 | ],
184 | 'fans' => [
185 | [
186 | 'name' => 'A',
187 | 'age' => 18,
188 | 'birthday' => '2000-01-01',
189 | ],
190 | [
191 | 'name' => 'B',
192 | 'age' => 28,
193 | 'birthday' => '2000-01-02',
194 | ],
195 | [
196 | 'name' => 'C',
197 | 'age' => 38,
198 | 'birthday' => '2000-01-03',
199 | ],
200 | ],
201 | ];
202 |
203 | $inputHandler = new TestInputHandler();
204 | $inputHandler->bind($input);
205 | $this->assertTrue($inputHandler->isValid());
206 |
207 | // Basic fields
208 | $this->assertTrue($inputHandler->hasData('title'));
209 | $this->assertFalse($inputHandler->hasData('...'));
210 | $this->assertEquals('Foobar', $inputHandler->getData('title'));
211 | $this->assertEquals(35, $inputHandler->getData('size'));
212 |
213 | // Scalar collection
214 | $this->assertEquals([11, 22, 33], $inputHandler->getData('dimensions'));
215 |
216 | // Transformer
217 | $this->assertEquals(new \DateTime('2015-01-01 22:50'), $inputHandler->getData('date'));
218 |
219 | // Mixed array
220 | $this->assertEquals(['foo' => 'bar'], $inputHandler->getData('metadata'));
221 |
222 | // Typed array
223 | $this->assertEquals(['title' => 'Barfoo', 'size' => 15, 'date' => new \DateTime('2015-01-01 22:50')], $inputHandler->getData('simple'));
224 |
225 | // Object and nested object
226 | $related = new TestUser();
227 | $related->setName('Barfoo');
228 | $related->setAge(28);
229 | $author = new TestUser();
230 | $author->setName('Barfoo');
231 | $author->setAge(28);
232 | $author->setRelated($related);
233 | $this->assertEquals($author, $inputHandler->getData('author'));
234 |
235 | // Object collection
236 | $fanA = new TestUser();
237 | $fanA->setName('A');
238 | $fanA->setAge(18);
239 | $fanA->setBirthday(new \DateTime('2000-01-01'));
240 | $fanB = new TestUser();
241 | $fanB->setName('B');
242 | $fanB->setAge(28);
243 | $fanB->setBirthday(new \DateTime('2000-01-02'));
244 | $fanC = new TestUser();
245 | $fanC->setName('C');
246 | $fanC->setAge(38);
247 | $fanC->setBirthday(new \DateTime('2000-01-03'));
248 | $this->assertEquals([$fanA, $fanB, $fanC], $inputHandler->getData('fans'));
249 | }
250 |
251 | public function testIsHandlingErrors(): void
252 | {
253 | $input = [
254 | 'size' => '35',
255 | 'dimensions' => ['11', 22, 33],
256 | 'date' => '2015-01-01 22:50',
257 | 'metadata' => [
258 | 'foo' => 'bar',
259 | ],
260 | 'simple' => [
261 | 'date' => '2015-01-01 22:50',
262 | ],
263 | 'author' => [
264 | 'name' => 'Barfoo',
265 | 'age' => 28,
266 | 'related' => [
267 | 'name' => 'Barfoo',
268 | 'age' => 28,
269 | ],
270 | ],
271 | 'fans' => [
272 | [
273 | 'name' => 'A',
274 | 'age' => 18,
275 | 'birthday' => '2000-01-01',
276 | ],
277 | [
278 | 'name' => 'B',
279 | 'age' => 28,
280 | 'birthday' => '2000-01-01',
281 | ],
282 | [
283 | 'name' => 'C',
284 | 'age' => 38,
285 | 'birthday' => '2000-01-01',
286 | ],
287 | ],
288 | ];
289 |
290 | $inputHandler = new TestInputHandler();
291 | $inputHandler->bind($input);
292 | $this->assertFalse($inputHandler->isValid());
293 | $this->assertEquals([
294 | 'Missing required field: title',
295 | ], $inputHandler->getErrors());
296 | $this->assertEquals('Missing required field: title', $inputHandler->getErrorsAsString());
297 | }
298 |
299 | public function testIsHandlingTypeJuggling(): void
300 | {
301 | $input = [
302 | 'title' => '',
303 | 'size' => 0,
304 | 'dimensions' => [0, 0, 0],
305 | 'date' => '2015-01-01 22:50',
306 | 'metadata' => [
307 | 'foo' => 'bar',
308 | ],
309 | 'simple' => [
310 | 'date' => '2015-01-01 22:50',
311 | ],
312 | 'author' => [
313 | 'name' => 'Barfoo',
314 | 'age' => 28,
315 | 'is_active' => false,
316 | 'related' => [
317 | 'name' => 'Barfoo',
318 | 'age' => 28,
319 | ],
320 | ],
321 | 'fans' => [
322 | [
323 | 'name' => 'A',
324 | 'age' => 18,
325 | 'birthday' => '2000-01-01',
326 | ],
327 | [
328 | 'name' => 'B',
329 | 'age' => 28,
330 | 'birthday' => '2000-01-01',
331 | ],
332 | [
333 | 'name' => 'C',
334 | 'age' => 38,
335 | 'birthday' => '2000-01-01',
336 | ],
337 | ],
338 | ];
339 |
340 | $inputHandler = new TestInputHandler();
341 | $inputHandler->bind($input);
342 | $this->assertTrue($inputHandler->isValid());
343 |
344 | $this->assertEquals('', $inputHandler->getData('title'));
345 | $this->assertEquals(0, $inputHandler->getData('size'));
346 | $this->assertEquals([0, 0, 0], $inputHandler->getData('dimensions'));
347 | $this->assertFalse($inputHandler->getData('author')->isActive);
348 | }
349 |
350 | public function testIsHandlingInputValidationWithInstantiator(): void
351 | {
352 | $input = [
353 | 'title' => 'Foobar',
354 | 'size' => 35,
355 | 'dimensions' => [11, 22, 33],
356 | 'date' => '2015-01-01 22:50',
357 | 'metadata' => [
358 | 'foo' => 'bar',
359 | ],
360 | 'simple' => [
361 | 'date' => '2015-01-01 22:50',
362 | ],
363 | 'user' => [
364 | 'name' => false,
365 | 'age' => '28',
366 | ],
367 | 'author' => [
368 | 'name' => 'Barfoo',
369 | 'age' => 28,
370 | 'related' => [
371 | 'name' => 'Barfoo',
372 | 'age' => 28,
373 | ],
374 | ],
375 | 'fans' => [
376 | [
377 | 'name' => 'A',
378 | 'age' => 18,
379 | 'birthday' => '2000-01-01',
380 | ],
381 | [
382 | 'name' => 'B',
383 | 'age' => 28,
384 | 'birthday' => '2000-01-01',
385 | ],
386 | [
387 | 'name' => 'C',
388 | 'age' => 38,
389 | 'birthday' => '2000-01-01',
390 | ],
391 | ],
392 | ];
393 |
394 | $instantiator = $this->prophesize(InstantiatorInterface::class);
395 | $instantiator->instantiate('Linio\Component\Input\TestUser', [])->shouldNotBeCalled();
396 |
397 | $inputHandler = new TestInputHandler();
398 | $user = $inputHandler->add('user', 'Linio\Component\Input\TestUser', ['instantiator' => $instantiator->reveal()]);
399 | $user->add('name', 'string');
400 | $user->add('age', 'int');
401 | $inputHandler->bind($input);
402 | $this->assertFalse($inputHandler->isValid());
403 | $this->assertEquals([
404 | '[name] Value does not match type: string',
405 | ], $inputHandler->getErrors());
406 | }
407 |
408 | public function testIsHandlingInputWithRecursiveHandler(): void
409 | {
410 | $input = [
411 | 'title' => 'Barfoo',
412 | 'size' => 20,
413 | 'child' => [
414 | 'title' => 'Foobar',
415 | 'size' => 35,
416 | 'dimensions' => [11, 22, 33],
417 | 'date' => '2015-01-01 22:50',
418 | 'metadata' => [
419 | 'foo' => 'bar',
420 | ],
421 | 'simple' => [
422 | 'date' => '2015-01-01 22:50',
423 | ],
424 | 'user' => [
425 | 'name' => false,
426 | 'age' => '28',
427 | ],
428 | 'author' => [
429 | 'name' => 'Barfoo',
430 | 'age' => 28,
431 | 'related' => [
432 | 'name' => 'Barfoo',
433 | 'age' => 28,
434 | ],
435 | ],
436 | 'fans' => [
437 | [
438 | 'name' => 'A',
439 | 'age' => 18,
440 | 'birthday' => '2000-01-01',
441 | ],
442 | [
443 | 'name' => 'B',
444 | 'age' => 28,
445 | 'birthday' => '2000-01-02',
446 | ],
447 | [
448 | 'name' => 'C',
449 | 'age' => 38,
450 | 'birthday' => '2000-01-03',
451 | ],
452 | ],
453 | ],
454 | ];
455 |
456 | $inputHandler = new TestRecursiveInputHandler();
457 | $inputHandler->bind($input);
458 | $this->assertTrue($inputHandler->isValid());
459 |
460 | // Basic fields
461 | $this->assertEquals('Barfoo', $inputHandler->getData('title'));
462 | $this->assertEquals(20, $inputHandler->getData('size'));
463 | /** @var \stdClass $child */
464 | $child = $inputHandler->getData('child');
465 |
466 | // Scalar collection
467 | $this->assertEquals([11, 22, 33], $child->dimensions);
468 |
469 | // Transformer
470 | $this->assertEquals(new \DateTime('2015-01-01 22:50'), $child->date);
471 |
472 | // Mixed array
473 | $this->assertEquals(['foo' => 'bar'], $child->metadata);
474 |
475 | // Typed array
476 | $this->assertEquals(['title' => 'Barfoo', 'size' => 15, 'date' => new \DateTime('2015-01-01 22:50')], $child->simple);
477 |
478 | // Object and nested object
479 | $related = new TestUser();
480 | $related->setName('Barfoo');
481 | $related->setAge(28);
482 | $author = new TestUser();
483 | $author->setName('Barfoo');
484 | $author->setAge(28);
485 | $author->setRelated($related);
486 | $this->assertEquals($author, $child->author);
487 |
488 | // Object collection
489 | $fanA = new TestUser();
490 | $fanA->setName('A');
491 | $fanA->setAge(18);
492 | $fanA->setBirthday(new \DateTime('2000-01-01'));
493 | $fanB = new TestUser();
494 | $fanB->setName('B');
495 | $fanB->setAge(28);
496 | $fanB->setBirthday(new \DateTime('2000-01-02'));
497 | $fanC = new TestUser();
498 | $fanC->setName('C');
499 | $fanC->setAge(38);
500 | $fanC->setBirthday(new \DateTime('2000-01-03'));
501 | $this->assertEquals([$fanA, $fanB, $fanC], $child->fans);
502 | }
503 |
504 | public function testIsHandlingInputWithRecursiveHandlerExplicit(): void
505 | {
506 | $input = [
507 | 'title' => 'Barfoo',
508 | 'size' => 20,
509 | 'child' => [
510 | 'title' => 'Foobar',
511 | 'size' => 35,
512 | 'dimensions' => [11, 22, 33],
513 | 'date' => '2015-01-01 22:50',
514 | 'metadata' => [
515 | 'foo' => 'bar',
516 | ],
517 | 'simple' => [
518 | 'date' => '2015-01-01 22:50',
519 | ],
520 | 'user' => [
521 | 'name' => false,
522 | 'age' => '28',
523 | ],
524 | 'author' => [
525 | 'name' => 'Barfoo',
526 | 'age' => 28,
527 | 'related' => [
528 | 'name' => 'Barfoo',
529 | 'age' => 28,
530 | ],
531 | ],
532 | 'fans' => [
533 | [
534 | 'name' => 'A',
535 | 'age' => 18,
536 | 'birthday' => '2000-01-01',
537 | ],
538 | [
539 | 'name' => 'B',
540 | 'age' => 28,
541 | 'birthday' => '2000-01-02',
542 | ],
543 | [
544 | 'name' => 'C',
545 | 'age' => 38,
546 | 'birthday' => '2000-01-03',
547 | ],
548 | ],
549 | ],
550 | ];
551 |
552 | $inputHandler = new TestRecursiveInputHandlerExplicit();
553 | $inputHandler->bind($input);
554 | $this->assertTrue($inputHandler->isValid());
555 |
556 | // Basic fields
557 | $this->assertEquals('Barfoo', $inputHandler->getData('title'));
558 | $this->assertEquals(20, $inputHandler->getData('size'));
559 | /** @var \stdClass $child */
560 | $child = $inputHandler->getData('child');
561 |
562 | // Scalar collection
563 | $this->assertEquals([11, 22, 33], $child->dimensions);
564 |
565 | // Transformer
566 | $this->assertEquals(new \DateTime('2015-01-01 22:50'), $child->date);
567 |
568 | // Mixed array
569 | $this->assertEquals(['foo' => 'bar'], $child->metadata);
570 |
571 | // Typed array
572 | $this->assertEquals(['title' => 'Barfoo', 'size' => 15, 'date' => new \DateTime('2015-01-01 22:50')], $child->simple);
573 |
574 | // Object and nested object
575 | $related = new TestUser();
576 | $related->setName('Barfoo');
577 | $related->setAge(28);
578 | $author = new TestUser();
579 | $author->setName('Barfoo');
580 | $author->setAge(28);
581 | $author->setRelated($related);
582 | $this->assertEquals($author, $child->author);
583 |
584 | // Object collection
585 | $fanA = new TestUser();
586 | $fanA->setName('A');
587 | $fanA->setAge(18);
588 | $fanA->setBirthday(new \DateTime('2000-01-01'));
589 | $fanB = new TestUser();
590 | $fanB->setName('B');
591 | $fanB->setAge(28);
592 | $fanB->setBirthday(new \DateTime('2000-01-02'));
593 | $fanC = new TestUser();
594 | $fanC->setName('C');
595 | $fanC->setAge(38);
596 | $fanC->setBirthday(new \DateTime('2000-01-03'));
597 | $this->assertEquals([$fanA, $fanB, $fanC], $child->fans);
598 | }
599 |
600 | public function testOverride(): void
601 | {
602 | $input = [
603 | 'price' => 'igor',
604 | ];
605 |
606 | $inputHandler = new TestConstraintOverrideType();
607 | $inputHandler->bind($input);
608 | $this->assertFalse($inputHandler->isValid());
609 | }
610 |
611 | public function invalidDateProvider(): \Generator
612 | {
613 | yield [''];
614 |
615 | yield ['Invalid%20date'];
616 |
617 | yield [123];
618 |
619 | yield [false];
620 |
621 | yield [true];
622 |
623 | yield [[]];
624 |
625 | yield [null];
626 | }
627 |
628 | /**
629 | * @dataProvider invalidDateProvider
630 | */
631 | public function testDatetimeInvalidDatetimeInput($datetime): void
632 | {
633 | $input = [
634 | 'date' => $datetime,
635 | ];
636 |
637 | $inputHandler = new TestDatetimeNotValidatingDate();
638 | $inputHandler->bind($input);
639 | $this->assertFalse($inputHandler->isValid());
640 | }
641 |
642 | public function testIsHandlingInputWithNullValues(): void
643 | {
644 | $input = [
645 | 'type' => 'buyers',
646 | 'data' => [
647 | 'name' => 'John Doe',
648 | 'address' => null,
649 | ],
650 | ];
651 |
652 | $inputHandler = new TestNullableRecursiveInputHandler();
653 | $inputHandler->bind($input);
654 |
655 | $this->assertTrue($inputHandler->isValid());
656 |
657 | $data = $inputHandler->getData('data');
658 |
659 | $this->assertNull($data->address);
660 |
661 | $input = [
662 | 'type' => 'buyers',
663 | 'data' => null,
664 | ];
665 |
666 | $inputHandler = new TestNullableRecursiveInputHandler();
667 | $inputHandler->bind($input);
668 |
669 | $this->assertTrue($inputHandler->isValid());
670 |
671 | $data = $inputHandler->getData('data');
672 |
673 | $this->assertNull($data);
674 | }
675 |
676 | public function testInputHandlerOnCascade(): void
677 | {
678 | $input = [
679 | 'name' => 'A',
680 | 'age' => 18,
681 | 'gender' => 'male',
682 | 'birthday' => '2000-01-01',
683 | ];
684 |
685 | $inputHandler = new TestInputHandlerCascade();
686 | $inputHandler->bind($input);
687 |
688 | $this->assertTrue($inputHandler->isValid());
689 | }
690 | }
691 |
692 | class TestConstraintOverrideType extends InputHandler
693 | {
694 | public function define(): void
695 | {
696 | $this->add('price', 'int', [
697 | 'required' => true,
698 | 'constraints' => [new Range(0)],
699 | ]);
700 | }
701 | }
702 |
703 | class TestDatetimeNotValidatingDate extends InputHandler
704 | {
705 | public function define(): void
706 | {
707 | $this->add('date', 'datetime', [
708 | 'required' => true,
709 | ]);
710 | }
711 | }
712 |
--------------------------------------------------------------------------------
/tests/Instantiator/ConstructInstantiatorTest.php:
--------------------------------------------------------------------------------
1 | instantiate('ErrorException', ['foobar']);
16 | $this->assertInstanceOf('ErrorException', $instance);
17 | $this->assertEquals(new \ErrorException('foobar'), $instance);
18 | }
19 |
20 | public function testIsHandlingArraysWithStringKeys(): void
21 | {
22 | $instantiator = new ConstructInstantiator();
23 | $instance = $instantiator->instantiate(Enum::class, ['foo' => [1, 2], 'bar' => 'message']);
24 | $this->assertEquals(new Enum([1, 2], 'message'), $instance);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/Instantiator/PropertyInstantiatorTest.php:
--------------------------------------------------------------------------------
1 | instantiate(TestUser::class, ['is_active' => true]);
16 | $this->assertInstanceOf(TestUser::class, $instance);
17 | $this->assertTrue($instance->isActive);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/Instantiator/ReflectionInstantiatorTest.php:
--------------------------------------------------------------------------------
1 | instantiate(TestUser::class, ['name' => 'foobar', 'age' => 20, 'is_active' => true]);
16 |
17 | $this->assertInstanceOf(TestUser::class, $instance);
18 | $this->assertEquals('foobar', $instance->getName());
19 | $this->assertEquals(20, $instance->getAge());
20 | $this->assertTrue($instance->isActive);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Instantiator/SetInstantiatorTest.php:
--------------------------------------------------------------------------------
1 | instantiate(TestUser::class, ['name' => 'foobar', 'age' => 20, 'is_active' => true]);
16 | $this->assertInstanceOf(TestUser::class, $instance);
17 | $this->assertEquals('foobar', $instance->getName());
18 | $this->assertEquals(20, $instance->getAge());
19 | $this->assertTrue($instance->isActive);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Node/BaseNodeTest.php:
--------------------------------------------------------------------------------
1 | prophesize(TypeHandler::class);
20 | $typeHandler->getType('string')->willReturn(new BaseNode());
21 |
22 | $base = new BaseNode();
23 | $base->setTypeHandler($typeHandler->reveal());
24 | $child = $base->add('foobar', 'string');
25 |
26 | $this->assertInstanceOf(BaseNode::class, $child);
27 | $this->assertCount(1, $base->getChildren());
28 | }
29 |
30 | public function testIsAddingRequiredChildNode(): void
31 | {
32 | $typeHandler = $this->prophesize(TypeHandler::class);
33 | $typeHandler->getType('string')->willReturn(new BaseNode());
34 |
35 | $base = new BaseNode();
36 | $base->setTypeHandler($typeHandler->reveal());
37 | $child = $base->add('foobar', 'string', ['required' => false]);
38 |
39 | $this->assertInstanceOf(BaseNode::class, $child);
40 | $this->assertFalse($child->isRequired());
41 | $this->assertCount(1, $base->getChildren());
42 | }
43 |
44 | public function testIsNotOverridingNodeConstraints(): void
45 | {
46 | $typeHandler = $this->prophesize(TypeHandler::class);
47 | $typeHandler->getType('string')->willReturn(new class() extends StringNode {
48 | public function getConstraints(): array
49 | {
50 | return $this->constraints;
51 | }
52 | });
53 |
54 | $base = new BaseNode();
55 | $base->setTypeHandler($typeHandler->reveal());
56 | $child = $base->add('foobar', 'string', ['constraints' => [new NotNull()]]);
57 |
58 | $this->assertCount(2, $child->getConstraints());
59 | }
60 |
61 | public function testIsRemovingChildNode(): void
62 | {
63 | $typeHandler = $this->prophesize(TypeHandler::class);
64 | $typeHandler->getType('string')->willReturn(new BaseNode());
65 |
66 | $base = new BaseNode();
67 | $base->setTypeHandler($typeHandler->reveal());
68 | $child = $base->add('foobar', 'string', ['required' => false]);
69 | $this->assertCount(1, $base->getChildren());
70 |
71 | $base->remove('foobar');
72 | $this->assertCount(0, $base->getChildren());
73 | }
74 |
75 | public function testIsDetectingChildsNode(): void
76 | {
77 | $typeHandler = $this->prophesize(TypeHandler::class);
78 | $typeHandler->getType('string')->willReturn(new BaseNode());
79 |
80 | $base = new BaseNode();
81 | $base->setTypeHandler($typeHandler->reveal());
82 | $child = $base->add('foobar', 'string', ['required' => false]);
83 |
84 | $this->assertTrue($base->hasChildren());
85 | }
86 |
87 | public function testIsGettingValue(): void
88 | {
89 | $typeHandler = $this->prophesize(TypeHandler::class);
90 | $typeHandler->getType('string')->willReturn(new BaseNode());
91 |
92 | $base = new BaseNode();
93 | $base->setTypeHandler($typeHandler->reveal());
94 | $child = $base->add('foobar', 'string');
95 | $this->assertEquals('foobar', $child->getValue('foobar', 'foobar'));
96 | }
97 |
98 | public function testIsCheckingConstraintsOnValue(): void
99 | {
100 | $typeHandler = $this->prophesize(TypeHandler::class);
101 | $typeHandler->getType('string')->willReturn(new BaseNode());
102 |
103 | $base = new BaseNode();
104 | $base->setTypeHandler($typeHandler->reveal());
105 | $child = $base->add('foobar', 'string', ['constraints' => [new StringSize(2, 5)]]);
106 |
107 | $this->expectException(InvalidConstraintException::class);
108 | $child->getValue('foobar', 'foobar');
109 | }
110 |
111 | public function testAllowingNullValuesIfConstraintsWouldOtherwiseReject(): void
112 | {
113 | $typeHandler = $this->prophesize(TypeHandler::class);
114 | $typeHandler->getType('string')->willReturn(new BaseNode());
115 |
116 | $base = new BaseNode();
117 | $base->setTypeHandler($typeHandler->reveal());
118 | $child = $base->add('foobar', 'string', ['allow_null' => true, 'constraints' => [new NotNull()]]);
119 |
120 | $this->assertNull($child->getValue('foobar', null));
121 | }
122 |
123 | public function testIsGettingTransformedValue(): void
124 | {
125 | $typeHandler = $this->prophesize(TypeHandler::class);
126 | $typeHandler->getType('string')->willReturn(new BaseNode());
127 |
128 | $base = new BaseNode();
129 | $base->setTypeHandler($typeHandler->reveal());
130 | $child = $base->add('foobar', 'string', ['transformer' => new DateTimeTransformer()]);
131 | $this->assertEquals(new \DateTime('2014-01-01 00:00:00'), $child->getValue('foobar', '2014-01-01 00:00:00'));
132 | }
133 |
134 | public function testNotRequiredWithConstraints(): void
135 | {
136 | $typeHandler = $this->prophesize(TypeHandler::class);
137 | $typeHandler->getType('string')->willReturn(new BaseNode());
138 |
139 | $base = new BaseNode();
140 | $base->setTypeHandler($typeHandler->reveal());
141 | $child = $base->add('foobar', 'string')
142 | ->setRequired(false)
143 | ->addConstraint(new StringSize(1, 255));
144 |
145 | $this->assertNull($child->getValue('foobar', null));
146 | }
147 |
148 | public function testNotRequiredWithContraintsAndIntegerField(): void
149 | {
150 | $typeHandler = $this->prophesize(TypeHandler::class);
151 | $typeHandler->getType('integer')->willReturn(new BaseNode());
152 |
153 | $base = new BaseNode();
154 | $base->setTypeHandler($typeHandler->reveal());
155 | $base->setTypeAlias('integer');
156 | $child = $base->add('foobar', 'integer')
157 | ->setTypeAlias('integer')
158 | ->setRequired(false)
159 | ->addConstraint(new Range(1, 255))
160 | ->setType('integer');
161 |
162 | $this->assertEmpty($child->getValue('foobar', null));
163 |
164 | $this->expectException(InvalidConstraintException::class);
165 | $child->getValue('foobar', 0);
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/tests/Node/CollectionNodeTest.php:
--------------------------------------------------------------------------------
1 | 1389312000];
18 | $expectedObj = new \DateTime('@1389312000');
19 |
20 | $instantiator = $this->prophesize(InstantiatorInterface::class);
21 | $instantiator->instantiate('DateTime', $expectedInput)->willReturn($expectedObj);
22 |
23 | $collectionNode = new CollectionNode();
24 | $collectionNode->setInstantiator($instantiator->reveal());
25 |
26 | $typeHandler = $this->prophesize(TypeHandler::class);
27 | $typeHandler->getType('DateTime')->willReturn($collectionNode);
28 |
29 | $base = new CollectionNode();
30 | $base->setTypeHandler($typeHandler->reveal());
31 | $child = $base->add('foobar', 'DateTime');
32 | $child->setType('DateTime');
33 | $this->assertEquals([$expectedObj], $child->getValue('foobar', [$expectedInput]));
34 | }
35 |
36 | public function testIsCheckingConstraintsOnValue(): void
37 | {
38 | $typeHandler = $this->prophesize(TypeHandler::class);
39 | $typeHandler->getType('DateTime')->willReturn(new CollectionNode());
40 |
41 | $constraint = $this->prophesize(ConstraintInterface::class);
42 | $constraint->validate([['timestamp' => 1389312000]])->willReturn(false);
43 | $constraint->getErrorMessage('foobar')->shouldBeCalled();
44 |
45 | $base = new CollectionNode();
46 | $base->setTypeHandler($typeHandler->reveal());
47 | $child = $base->add('foobar', 'DateTime', ['constraints' => [$constraint->reveal()]]);
48 | $child->setType('DateTime');
49 |
50 | $this->expectException(InvalidConstraintException::class);
51 | $child->getValue('foobar', [['timestamp' => 1389312000]]);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Node/IntNodeTest.php:
--------------------------------------------------------------------------------
1 | setDefault(0);
15 |
16 | $this->assertTrue($node->hasDefault());
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/Node/ObjectNodeTest.php:
--------------------------------------------------------------------------------
1 | 1389312000];
18 | $expectedObj = new \DateTime('@1389312000');
19 |
20 | $instantiator = $this->prophesize(InstantiatorInterface::class);
21 | $instantiator->instantiate('DateTime', $expectedInput)->willReturn($expectedObj);
22 |
23 | $objectNode = new ObjectNode();
24 | $objectNode->setInstantiator($instantiator->reveal());
25 |
26 | $typeHandler = $this->prophesize(TypeHandler::class);
27 | $typeHandler->getType('DateTime')->willReturn($objectNode);
28 |
29 | $base = new ObjectNode();
30 | $base->setTypeHandler($typeHandler->reveal());
31 | $child = $base->add('foobar', 'DateTime');
32 | $child->setType('DateTime');
33 | $this->assertEquals($expectedObj, $child->getValue('foobar', $expectedInput));
34 | }
35 |
36 | public function testIsCheckingConstraintsOnValue(): void
37 | {
38 | $typeHandler = $this->prophesize(TypeHandler::class);
39 | $typeHandler->getType('DateTime')->willReturn(new ObjectNode());
40 |
41 | $constraint = $this->prophesize(ConstraintInterface::class);
42 | $constraint->validate(['timestamp' => 1389312000])->willReturn(false);
43 | $constraint->getErrorMessage('foobar')->shouldBeCalled();
44 |
45 | $base = new ObjectNode();
46 | $base->setTypeHandler($typeHandler->reveal());
47 | $child = $base->add('foobar', 'DateTime', ['constraints' => [$constraint->reveal()]]);
48 | $child->setType('DateTime');
49 |
50 | $this->expectException(InvalidConstraintException::class);
51 | $child->getValue('foobar', ['timestamp' => 1389312000]);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Node/ScalarCollectionNodeTest.php:
--------------------------------------------------------------------------------
1 | prophesize(TypeHandler::class);
17 | $typeHandler->getType('int')->willReturn(new ScalarCollectionNode());
18 |
19 | $base = new ScalarCollectionNode();
20 | $base->setTypeHandler($typeHandler->reveal());
21 | $child = $base->add('foobar', 'int');
22 | $child->setType('int');
23 | $this->assertEquals([15, 25, 36], $child->getValue('foobar', [15, 25, 36]));
24 | }
25 |
26 | public function testIsDetectingBadTypes(): void
27 | {
28 | $typeHandler = $this->prophesize(TypeHandler::class);
29 | $typeHandler->getType('int')->willReturn(new ScalarCollectionNode());
30 |
31 | $base = new ScalarCollectionNode();
32 | $base->setTypeHandler($typeHandler->reveal());
33 | $child = $base->add('foobar', 'int');
34 | $child->setType('int');
35 |
36 | $this->expectException(InvalidConstraintException::class);
37 | $this->expectExceptionMessage('Value "25" is not of type int');
38 | $child->getValue('foobar', [15, '25']);
39 | }
40 |
41 | public function testIsCheckingConstraintsOnValue(): void
42 | {
43 | $typeHandler = $this->prophesize(TypeHandler::class);
44 | $typeHandler->getType('int')->willReturn(new ScalarCollectionNode());
45 |
46 | $constraint = $this->prophesize(ConstraintInterface::class);
47 | $constraint->validate([15, 25, 36])->willReturn(false);
48 | $constraint->getErrorMessage('foobar')->shouldBeCalled();
49 |
50 | $base = new ScalarCollectionNode();
51 | $base->setTypeHandler($typeHandler->reveal());
52 | $child = $base->add('foobar', 'int', ['constraints' => [$constraint->reveal()]]);
53 | $child->setType('int');
54 |
55 | $this->expectException(InvalidConstraintException::class);
56 | $child->getValue('foobar', [15, 25, 36]);
57 | }
58 |
59 | public function testIsCheckingIfIsIterable(): void
60 | {
61 | $typeHandler = $this->prophesize(TypeHandler::class);
62 | $typeHandler->getType('int[]')->willReturn(new ScalarCollectionNode());
63 |
64 | $base = new ScalarCollectionNode();
65 | $base->setTypeHandler($typeHandler->reveal());
66 | $child = $base->add('foobar', 'int[]');
67 | $child->setType('int');
68 |
69 | $this->expectException(InvalidConstraintException::class);
70 | $child->getValue('foobar', 'foobar');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/SchemaBuilderTest.php:
--------------------------------------------------------------------------------
1 | add('title', 'string');
12 | $this->add('size', 'int');
13 | $this->add('dimensions', 'int[]');
14 | $this->add('date', 'datetime');
15 | $this->add('metadata', 'array');
16 |
17 | $simple = $this->add('simple', 'array');
18 | $simple->add('title', 'string', ['default' => 'Barfoo']);
19 | $simple->add('size', 'int', ['required' => false, 'default' => 15]);
20 | $simple->add('date', 'datetime');
21 |
22 | $author = $this->add('author', 'Linio\Component\Input\TestUser');
23 | $author->add('name', 'string');
24 | $author->add('age', 'int');
25 | $author->add('is_active', 'bool', ['required' => false]);
26 |
27 | $authors = $this->add('authors', 'Linio\Component\Input\TestUser[]');
28 | $authors->add('name', 'string');
29 | }
30 | }
31 |
32 | class SchemaBuilderTest extends TestCase
33 | {
34 | public function testIsBuildingSchema(): void
35 | {
36 | $expectedSchema = [
37 | 'title' => [
38 | 'type' => 'string',
39 | 'required' => true,
40 | 'default' => null,
41 | 'nullable' => false,
42 | 'children' => [],
43 | ],
44 | 'size' => [
45 | 'type' => 'int',
46 | 'required' => true,
47 | 'default' => null,
48 | 'nullable' => false,
49 | 'children' => [],
50 | ],
51 | 'dimensions' => [
52 | 'type' => 'int[]',
53 | 'required' => true,
54 | 'default' => null,
55 | 'nullable' => false,
56 | 'children' => [],
57 | ],
58 | 'date' => [
59 | 'type' => 'datetime',
60 | 'required' => true,
61 | 'default' => null,
62 | 'nullable' => false,
63 | 'children' => [],
64 | ],
65 | 'metadata' => [
66 | 'type' => 'array',
67 | 'required' => true,
68 | 'default' => null,
69 | 'nullable' => false,
70 | 'children' => [],
71 | ],
72 | 'simple' => [
73 | 'type' => 'array',
74 | 'required' => true,
75 | 'default' => null,
76 | 'nullable' => false,
77 | 'children' => [
78 | 'title' => [
79 | 'type' => 'string',
80 | 'required' => false,
81 | 'default' => 'Barfoo',
82 | 'nullable' => false,
83 | 'children' => [],
84 | ],
85 | 'size' => [
86 | 'type' => 'int',
87 | 'required' => false,
88 | 'default' => 15,
89 | 'nullable' => false,
90 | 'children' => [],
91 | ],
92 | 'date' => [
93 | 'type' => 'datetime',
94 | 'required' => true,
95 | 'default' => null,
96 | 'nullable' => false,
97 | 'children' => [],
98 | ],
99 | ],
100 | ],
101 | 'author' => [
102 | 'type' => 'object',
103 | 'required' => true,
104 | 'default' => null,
105 | 'nullable' => false,
106 | 'children' => [
107 | 'name' => [
108 | 'type' => 'string',
109 | 'required' => true,
110 | 'default' => null,
111 | 'nullable' => false,
112 | 'children' => [],
113 | ],
114 | 'age' => [
115 | 'type' => 'int',
116 | 'required' => true,
117 | 'default' => null,
118 | 'nullable' => false,
119 | 'children' => [],
120 | ],
121 | 'is_active' => [
122 | 'type' => 'bool',
123 | 'required' => false,
124 | 'default' => null,
125 | 'nullable' => false,
126 | 'children' => [],
127 | ],
128 | ],
129 | ],
130 | 'authors' => [
131 | 'type' => 'object[]',
132 | 'required' => true,
133 | 'default' => null,
134 | 'nullable' => false,
135 | 'children' => [
136 | 'name' => [
137 | 'type' => 'string',
138 | 'required' => true,
139 | 'default' => null,
140 | 'nullable' => false,
141 | 'children' => [],
142 | ],
143 | ],
144 | ],
145 | ];
146 |
147 | $schemaBuilder = new SchemaBuilder();
148 | $schema = $schemaBuilder->build(new SchemaTestInputHandler());
149 |
150 | $this->assertEquals($expectedSchema, $schema);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | transform('2014-01-01 00:00:01');
15 | $this->assertInstanceOf('\DateTime', $transformed);
16 | $this->assertEquals(new \DateTime('2014-01-01 00:00:01'), $transformed);
17 | }
18 |
19 | public function testIsReturningNullWithInvalidDate(): void
20 | {
21 | $transformer = new DateTimeTransformer();
22 | $transformed = $transformer->transform('2014-01x01');
23 | $this->assertNull($transformed);
24 | }
25 |
26 | public function testIsAllowingNullableValue(): void
27 | {
28 | $transformer = new DateTimeTransformer();
29 | $transformed = $transformer->transform(null);
30 |
31 | $this->assertNull($transformed);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Transformer/UuidTransformerTest.php:
--------------------------------------------------------------------------------
1 | transform('d1d6228d-604c-4a8a-9396-42e6c3b17754');
18 | $this->assertInstanceOf(UuidInterface::class, $transformed);
19 | $this->assertEquals(Uuid::fromString('d1d6228d-604c-4a8a-9396-42e6c3b17754'), $transformed);
20 | }
21 |
22 | public function testItDoesThrowExceptionBecauseOfInvalidString(): void
23 | {
24 | $transformer = new UuidTransformer();
25 |
26 | $this->expectException(TransformationException::class);
27 | $transformer->transform('d1d6228d-604c');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/TypeHandlerTest.php:
--------------------------------------------------------------------------------
1 | addType('foobar', BaseNode::class);
19 | $type = $typeHandler->getType('foobar');
20 | $this->assertInstanceOf(BaseNode::class, $type);
21 | }
22 |
23 | public function testIsCreatingScalarCollections(): void
24 | {
25 | $typeHandler = new TypeHandler();
26 | $type = $typeHandler->getType('int[]');
27 | $this->assertInstanceOf(ScalarCollectionNode::class, $type);
28 | }
29 |
30 | public function testIsCreatingCollections(): void
31 | {
32 | $typeHandler = new TypeHandler();
33 | $type = $typeHandler->getType('DateTime[]');
34 | $this->assertInstanceOf(CollectionNode::class, $type);
35 | }
36 |
37 | public function objectProvider()
38 | {
39 | return [
40 | ['DateTime'],
41 | [\DateTimeInterface::class],
42 | ];
43 | }
44 |
45 | /**
46 | * @dataProvider objectProvider
47 | */
48 | public function testIsCreatingObjects($className): void
49 | {
50 | $typeHandler = new TypeHandler();
51 | $type = $typeHandler->getType($className);
52 | $this->assertInstanceOf(ObjectNode::class, $type);
53 | }
54 |
55 | public function testIsDetectingConflictWithCaseInsensitive(): void
56 | {
57 | $typeHandler = new TypeHandler();
58 | $type = $typeHandler->getType('datetime');
59 | $this->assertInstanceOf(DateTimeNode::class, $type);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------