├── .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 | [![Latest Stable Version](https://poser.pugx.org/linio/input/v/stable.svg)](https://packagist.org/packages/linio/input) [![License](https://poser.pugx.org/linio/input/license.svg)](https://packagist.org/packages/linio/input) [![Build Status](https://secure.travis-ci.org/LinioIT/input.png)](http://travis-ci.org/LinioIT/input) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/LinioIT/input/badges/quality-score.png?b=master)](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 | --------------------------------------------------------------------------------