├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── coding-standards.yml │ ├── mutation-tests.yml │ ├── security-analysis.yml │ ├── static-analysis.yml │ └── unit-tests.yml ├── .gitignore ├── .phpcs.xml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── composer.json ├── infection.json.dist └── src └── AzJezz └── Input ├── Exception ├── BadInputException.php ├── ExceptionInterface.php └── TypeException.php ├── Hydrator.php ├── HydratorInterface.php └── InputInterface.php /.gitattributes: -------------------------------------------------------------------------------- 1 | tests export-ignore 2 | 3 | .travis.yml export-ignore 4 | .scrutinizer.yml export-ignore 5 | .php_cs.dist export-ignore 6 | .coveralls.yml export-ignore 7 | appveyor.yml export-ignore 8 | phpunit.xml.dist export-ignore 9 | psalm.xml export-ignore 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | assignees: azjezz 6 | 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Code to reproduce the behavior: 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Environment (please complete the following information):** 19 | - OS: [e.g. iOS, Ubuntu] 20 | - PHP version [e.g. 7.4, 8.0] 21 | - Version [e.g. 22] 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | assignees: azjezz 6 | 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: "coding standards" 2 | 3 | on: ["pull_request", "push"] 4 | 5 | jobs: 6 | coding-standards: 7 | name: "coding standards" 8 | runs-on: "ubuntu-latest" 9 | steps: 10 | - name: "checkout" 11 | uses: "actions/checkout@v2" 12 | 13 | - name: "installing PHP" 14 | uses: "shivammathur/setup-php@v2" 15 | with: 16 | php-version: "7.4" 17 | ini-values: memory_limit=-1 18 | tools: composer:v2, cs2pr 19 | 20 | - name: "installing dependencies" 21 | run: "composer update --no-interaction --no-progress" 22 | 23 | - name: "checking coding standards ( codesniffer )" 24 | run: "php vendor/bin/phpcs" 25 | 26 | - name: "checking coding standards ( php-cs-fixer )" 27 | run: "php vendor/bin/php-cs-fixer fix --dry-run --ansi" 28 | -------------------------------------------------------------------------------- /.github/workflows/mutation-tests.yml: -------------------------------------------------------------------------------- 1 | name: "mutation tests" 2 | 3 | on: ["pull_request", "push"] 4 | 5 | jobs: 6 | unit-tests: 7 | name: "mutation tests" 8 | 9 | runs-on: "ubuntu-latest" 10 | steps: 11 | - name: "checkout" 12 | uses: "actions/checkout@v2" 13 | 14 | - name: "installing PHP" 15 | uses: "shivammathur/setup-php@v2" 16 | with: 17 | php-version: "7.4" 18 | ini-values: memory_limit=-1 19 | tools: composer:v2, cs2pr 20 | 21 | - name: "installing dependencies" 22 | run: "composer update --no-interaction --no-progress" 23 | 24 | - name: "running mutation tests ( infection )" 25 | run: "./vendor/bin/infection" 26 | env: 27 | INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} 28 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} 29 | -------------------------------------------------------------------------------- /.github/workflows/security-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "security analysis" 2 | 3 | on: ["pull_request", "push"] 4 | 5 | jobs: 6 | security-analysis: 7 | name: "security analysis" 8 | runs-on: "ubuntu-latest" 9 | steps: 10 | - name: "checkout" 11 | uses: "actions/checkout@v2" 12 | 13 | - name: "installing PHP" 14 | uses: "shivammathur/setup-php@v2" 15 | with: 16 | php-version: "7.4" 17 | ini-values: memory_limit=-1 18 | tools: composer:v2, cs2pr 19 | 20 | - name: "installing dependencies" 21 | run: "composer update --no-interaction --no-progress" 22 | 23 | - name: "running security analysis ( psalm )" 24 | run: "php vendor/bin/psalm --output-format=github --taint-analysis" 25 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "static analysis" 2 | 3 | on: ["pull_request", "push"] 4 | 5 | jobs: 6 | static-analysis: 7 | name: "static analysis" 8 | runs-on: "ubuntu-latest" 9 | steps: 10 | - name: "checkout" 11 | uses: "actions/checkout@v2" 12 | 13 | - name: "installing PHP" 14 | uses: "shivammathur/setup-php@v2" 15 | with: 16 | php-version: "8.0" 17 | ini-values: memory_limit=-1 18 | tools: composer:v2, cs2pr 19 | 20 | - name: "installing dependencies" 21 | run: "composer update --no-interaction --no-progress --ignore-platform-req php" 22 | 23 | - name: "running static analysis ( psalm )" 24 | run: "php vendor/bin/psalm --output-format=github --shepherd --stats" 25 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: "unit tests" 2 | 3 | on: ["pull_request", "push"] 4 | 5 | jobs: 6 | unit-tests: 7 | name: "unit tests" 8 | 9 | runs-on: ${{ matrix.operating-system }} 10 | 11 | strategy: 12 | matrix: 13 | php-version: 14 | - "7.4" 15 | - "8.0" 16 | operating-system: 17 | - "ubuntu-latest" 18 | 19 | steps: 20 | - name: "checkout" 21 | uses: "actions/checkout@v2" 22 | 23 | - name: "installing PHP" 24 | uses: "shivammathur/setup-php@v2" 25 | with: 26 | php-version: "${{ matrix.php-version }}" 27 | ini-values: memory_limit=-1 28 | tools: composer:v2, cs2pr 29 | 30 | - name: "caching dependencies" 31 | uses: "actions/cache@v2" 32 | with: 33 | path: | 34 | ~/.composer/cache 35 | vendor 36 | key: "php-${{ matrix.php-version }}" 37 | restore-keys: "php-${{ matrix.php-version }}" 38 | 39 | - name: "installing dependencies" 40 | run: "composer install --no-interaction --no-progress --ignore-platform-req php" 41 | 42 | - name: "running unit tests ( phpunit )" 43 | run: "php vendor/bin/phpunit" 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tests/logs/* 2 | /vendor/ 3 | 4 | composer.lock 5 | 6 | .php_cs.cache 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /.phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | The coding standard for PHP Standard Library. 5 | 6 | src 7 | tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | error 15 | 16 | 17 | 18 | error 19 | 20 | 21 | 22 | error 23 | 24 | 25 | 26 | 27 | error 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at azjezz@protonmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 Saif Eddin Gmati 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Input Hydrator 2 | 3 | ![Unit tests status](https://github.com/azjezz/input-hydrator/workflows/unit%20tests/badge.svg?branch=develop) 4 | ![Static analysis status](https://github.com/azjezz/input-hydrator/workflows/static%20analysis/badge.svg?branch=develop) 5 | ![Security analysis status](https://github.com/azjezz/input-hydrator/workflows/security%20analysis/badge.svg?branch=develop) 6 | ![Coding standards status](https://github.com/azjezz/input-hydrator/workflows/coding%20standards/badge.svg?branch=develop) 7 | [![TravisCI Build Status](https://travis-ci.com/azjezz/input-hydrator.svg?branch=develop)](https://travis-ci.com/azjezz/input-hydrator) 8 | [![Coverage Status](https://coveralls.io/repos/github/azjezz/input-hydrator/badge.svg?branch=develop)](https://coveralls.io/github/azjezz/input-hydrator?branch=develop) 9 | [![Type Coverage](https://shepherd.dev/github/azjezz/input-hydrator/coverage.svg)](https://shepherd.dev/github/azjezz/input-hydrator) 10 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fazjezz%2Finput-hydrator%2Fdevelop)](https://dashboard.stryker-mutator.io/reports/github.com/azjezz/input-hydrator/develop) 11 | [![Total Downloads](https://poser.pugx.org/azjezz/input-hydrator/d/total.svg)](https://packagist.org/packages/azjezz/input-hydrator) 12 | [![Latest Stable Version](https://poser.pugx.org/azjezz/input-hydrator/v/stable.svg)](https://packagist.org/packages/azjezz/input-hydrator) 13 | [![License](https://poser.pugx.org/azjezz/input-hydrator/license.svg)](https://packagist.org/packages/azjezz/input-hydrator) 14 | 15 | Input hydrator is a simple hydrator made for the sole purpose of hydrating data-transfer input objects. 16 | 17 | 18 | ## Installation 19 | 20 | ```console 21 | $ composer require azjezz/input-hydrator 22 | ``` 23 | 24 | ## Example: 25 | 26 | ```php 27 | use AzJezz\Input; 28 | 29 | final class Search implements Input\InputInterface 30 | { 31 | public string $query; 32 | } 33 | 34 | /** 35 | * @var Search $search 36 | */ 37 | $search = (new Input\Hydrator())->hydrate(Search::class, $_GET); 38 | 39 | print $search->query; 40 | ``` 41 | 42 | While hydrating objects, some exceptions might be thrown: 43 | - `AzJezz\Input\Exception\TypeException`: this exception should result in 500 HTTP status code, 44 | as it represents an issue within the input class itself. such as the usage of a non-supported type, 45 | or missing type for a specific property. 46 | 47 | - `AzJezz\Input\Exception\BadInputException`: this exception should result in a 400 HTTP status code, 48 | as it means that the supplied request data doesn't match the input DTO structure. 49 | 50 | Currently, Input-Hydrator is limited to a small set of types: 51 | - `scalar` ( `string`, `int`, `float`, and `bool` ) 52 | - `null` 53 | - *any object that implements `AzJezz\Input\InputInterface`* 54 | 55 | Union types are supported for PHP >= 8.0, for example: 56 | 57 | ```php 58 | use AzJezz\Input; 59 | 60 | final class Filter implements Input\InputInterface 61 | { 62 | public ?int $maximumPrice; 63 | public ?int $minimumPrice; 64 | } 65 | 66 | final class Search implements Input\InputInterface 67 | { 68 | public string $query; 69 | public null|Filter|string $filter = null; 70 | } 71 | 72 | /** 73 | * $filter is optional, and is missing from the request, therefore it's gonna contain the default value. 74 | * 75 | * @var Search $search 76 | */ 77 | $search = (new Input\Hydrator())->hydrate(Search::class, [ 78 | 'query' => 'hello' 79 | ]); 80 | 81 | /** 82 | * $search->filter is now an instance of `Filter` 83 | * 84 | * @var Search $search 85 | */ 86 | $search = (new Input\Hydrator())->hydrate(Search::class, [ 87 | 'query' => 'hello', 88 | 'filter' => [ 89 | 'maximum_price' => 1000, 90 | 'minimum_price' => 10, // the field is optional ( nullable ), so we can remove this line. 91 | ] 92 | ]); 93 | 94 | /** 95 | * $search->filter is now a string 96 | * 97 | * @var Search $search 98 | */ 99 | $search = (new Input\Hydrator())->hydrate(Search::class, [ 100 | 'query' => 'hello', 101 | // this is okay as the `null|Filter|string` union contains `string` 102 | 'filter' => 'maximum_price=1000&minimum_price=10', 103 | ]); 104 | 105 | print $search->query; 106 | ``` 107 | 108 | ## License 109 | 110 | The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azjezz/input-hydrator", 3 | "type": "library", 4 | "description": "Hydrates input DTOs from request input.", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "azjezz", 9 | "email": "azjezz@protonmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.4" 14 | }, 15 | "require-dev": { 16 | "friendsofphp/php-cs-fixer": "^2.16", 17 | "infection/infection": "^0.19.2", 18 | "php-coveralls/php-coveralls": "^2.4", 19 | "phpunit/phpunit": "^9.4", 20 | "roave/security-advisories": "dev-master", 21 | "squizlabs/php_codesniffer": "^3.5", 22 | "vimeo/psalm": "^4.1" 23 | }, 24 | "config": { 25 | "process-timeout": 1200, 26 | "sort-packages": true 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "AzJezz\\Input\\": "src/AzJezz/Input" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "AzJezz\\Input\\Test\\": "tests/AzJezz/Input" 36 | } 37 | }, 38 | "minimum-stability": "dev", 39 | "prefer-stable": true, 40 | "scripts": { 41 | "check": [ 42 | "@cs:check", 43 | "@type:check", 44 | "@security:analysis", 45 | "@test:unit" 46 | ], 47 | "code:coverage": "php-coveralls -v", 48 | "cs:check": [ 49 | "phpcs", 50 | "php-cs-fixer fix --dry-run" 51 | ], 52 | "cs:fix": [ 53 | "phpcbf", 54 | "php-cs-fixer fix" 55 | ], 56 | "security:analysis": "psalm --taint-analysis", 57 | "test:unit": "phpunit", 58 | "type:check": "psalm", 59 | "type:coverage": "psalm --shepherd" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "tests\/logs\/infection.log", 9 | "badge": { 10 | "branch": "develop" 11 | } 12 | }, 13 | "timeout": 15 14 | } 15 | -------------------------------------------------------------------------------- /src/AzJezz/Input/Exception/BadInputException.php: -------------------------------------------------------------------------------- 1 | $input_class 23 | * @psalm-param array $request 24 | * 25 | * @psalm-return T 26 | * 27 | * @throws BadInputException If unable to construct the input class from the given request data. 28 | * @throws TypeException If the input class contains a property that is either untyped, 29 | * or of a non-supported type. 30 | */ 31 | public function hydrate(string $input_class, array $request): InputInterface 32 | { 33 | $reflection = new ReflectionClass($input_class); 34 | /** 35 | * @var InputInterface $instance 36 | * @psalm-var T $instance 37 | */ 38 | $instance = $reflection->newInstanceWithoutConstructor(); 39 | 40 | $properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC); 41 | $properties = array_filter( 42 | $properties, 43 | static fn(ReflectionProperty $property): bool => !$property->isStatic(), 44 | ); 45 | 46 | foreach ($properties as $property) { 47 | $value = $this->getFieldValue($instance, $request, $property); 48 | 49 | $property->setValue($instance, $value); 50 | } 51 | 52 | return $instance; 53 | } 54 | 55 | private function getFieldName(ReflectionProperty $property): string 56 | { 57 | $value = trim($property->getName()); 58 | $value = preg_replace('/[^a-zA-Z0-9_]/', '_', $value); 59 | $value = preg_replace('/(?<=\\w)([A-Z])/', '_$1', $value); 60 | $value = preg_replace('/_{2,}/', '_', $value); 61 | return strtolower($value); 62 | } 63 | 64 | /** 65 | * @throws TypeException If the input class contains a property that is untyped. 66 | */ 67 | private function getFieldType(ReflectionProperty $property): ReflectionType 68 | { 69 | $type = $property->getType(); 70 | if (null === $type) { 71 | throw TypeException::forMissingPropertyType( 72 | $property->getDeclaringClass()->getName(), 73 | $property->getName(), 74 | ); 75 | } 76 | 77 | return $type; 78 | } 79 | 80 | /** 81 | * @psalm-param array $request 82 | * 83 | * @psalm-return InputInterface|scalar|null 84 | * 85 | * @throws BadInputException If unable to construct the input class from the given request data. 86 | * @throws TypeException If the input class contains a property that is either untyped, 87 | * or of a non-supported type. 88 | */ 89 | private function getFieldValue(InputInterface $input, array $request, ReflectionProperty $property) 90 | { 91 | $field_name = $this->getFieldName($property); 92 | $field_type_reflection = $this->getFieldType($property); 93 | 94 | /** 95 | * Check if the field exists in the request data. 96 | */ 97 | if (!array_key_exists($field_name, $request)) { 98 | /** 99 | * In case we don't have a value, check if the property has a default value, which we can use. 100 | */ 101 | if ($property->isDefault() && $property->isInitialized($input)) { 102 | /** @psalm-var scalar $value */ 103 | $value = $property->getValue($input); 104 | 105 | return $value; 106 | } 107 | 108 | /** 109 | * Otherwise, Check if the property allows null values, in which case, 110 | * we can return null ( the field is optional ). 111 | */ 112 | if ($field_type_reflection->allowsNull()) { 113 | return null; 114 | } 115 | 116 | // The field is not optional, doesn't have a default value, and is missing from the request. 117 | // This means that we have a bad request in our hands. 118 | throw BadInputException::createForMissingField($field_name); 119 | } 120 | 121 | /** @var mixed $field_value */ 122 | $field_value = $request[$field_name]; 123 | // @codeCoverageIgnoreStart 124 | if (class_exists(ReflectionUnionType::class) && $field_type_reflection instanceof ReflectionUnionType) { 125 | /** @var list $inner_types */ 126 | $inner_types = $field_type_reflection->getTypes(); 127 | foreach ($inner_types as $inner_type) { 128 | try { 129 | return $this->coerceType($property, $field_name, $field_value, $inner_type); 130 | } catch (TypeException $exception) { 131 | throw $exception; 132 | } catch (BadInputException $exception) { 133 | } 134 | } 135 | 136 | throw BadInputException::createForInvalidFieldTypeFromValue( 137 | $field_name, 138 | (string)$field_type_reflection, 139 | $field_value, 140 | ); 141 | } 142 | // @codeCoverageIgnoreEnd 143 | 144 | // Now that we know the field exists, let's assert it's type. 145 | return $this->coerceType($property, $field_name, $field_value, $field_type_reflection); 146 | } 147 | 148 | /** 149 | * @param mixed $value 150 | * 151 | * @return mixed 152 | * @psalm-return InputInterface|scalar|null 153 | * 154 | * @throws BadInputException If unable to construct the input class from the given request data. 155 | * @throws TypeException If the input class contains a property that is of a non-supported type. 156 | */ 157 | private function coerceType(ReflectionProperty $property, string $name, $value, ReflectionType $type) 158 | { 159 | $type_as_string = $type instanceof ReflectionNamedType ? $type->getName() : (string)$type; 160 | if (class_exists($type_as_string)) { 161 | // Check if the type is a subclass of InputInterface 162 | if (is_subclass_of($type_as_string, InputInterface::class)) { 163 | // If the type is a subclass of input, we need to ensure that the value is an array. 164 | if (!is_array($value)) { 165 | // otherwise we throw a bad request exception 166 | throw BadInputException::createForInvalidFieldTypeFromValue($name, $type_as_string, $value); 167 | } 168 | 169 | /** 170 | * Since the value is an array, and the type is an input, we can try to map it. 171 | * 172 | * @var InputInterface 173 | */ 174 | return $this->hydrate($type_as_string, $value); 175 | } 176 | 177 | throw TypeException::forUnsupportedPropertyType( 178 | $property->getDeclaringClass()->getName(), 179 | $property->getName(), 180 | $type_as_string, 181 | ); 182 | } 183 | 184 | if ('array' === $type_as_string || 'iterable' === $type_as_string || 'object' === $type_as_string) { 185 | /** 186 | * we don't support array type as PHP doesn't support generics. 187 | */ 188 | throw TypeException::forUnsupportedPropertyType( 189 | $property->getDeclaringClass()->getName(), 190 | $property->getName(), 191 | $type_as_string 192 | ); 193 | } 194 | 195 | if ('string' === $type_as_string) { 196 | if (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) { 197 | return (string) $value; 198 | } 199 | } 200 | 201 | if ('int' === $type_as_string) { 202 | if (is_int($value)) { 203 | return $value; 204 | } 205 | 206 | if (is_string($value)) { 207 | $trimmed_value = ltrim($value, '0'); 208 | $integer_value = (int)$trimmed_value; 209 | if (((string)$integer_value) === $value) { 210 | return $integer_value; 211 | } 212 | 213 | if ('' === $trimmed_value && '' !== $value) { 214 | return 0; 215 | } 216 | } 217 | } 218 | 219 | if ('float' === $type_as_string) { 220 | if (is_float($value) || is_int($value)) { 221 | return (float)$value; 222 | } 223 | 224 | if (is_string($value) && '' !== $value) { 225 | if (ctype_digit($value)) { 226 | return (float)$value; 227 | } 228 | 229 | if (1 === preg_match("/^-?(?:\\d*\\.)?\\d+(?:[eE]\\d+)?$/", $value)) { 230 | return (float)$value; 231 | } 232 | } 233 | } 234 | 235 | if ('bool' === $type_as_string) { 236 | if (is_bool($value)) { 237 | return $value; 238 | } 239 | 240 | if ('1' === $value || 1 === $value) { 241 | return true; 242 | } 243 | 244 | if ('0' === $value || 0 === $value) { 245 | return false; 246 | } 247 | } 248 | 249 | if ($type->allowsNull()) { 250 | if ('' === $value || null === $value) { 251 | return null; 252 | } 253 | } 254 | 255 | throw BadInputException::createForInvalidFieldTypeFromValue($name, $type_as_string, $value); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/AzJezz/Input/HydratorInterface.php: -------------------------------------------------------------------------------- 1 | $input_class 18 | * @psalm-param array $request 19 | * 20 | * @psalm-return T 21 | * 22 | * @throws BadInputException If unable to construct the input class from the given request data. 23 | * @throws TypeException If the input class contains a property that is either untyped, 24 | * or of a non-supported type. 25 | */ 26 | public function hydrate(string $input_class, array $request): InputInterface; 27 | } 28 | -------------------------------------------------------------------------------- /src/AzJezz/Input/InputInterface.php: -------------------------------------------------------------------------------- 1 |