├── .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 | 
4 | 
5 | 
6 | 
7 | [](https://travis-ci.com/azjezz/input-hydrator)
8 | [](https://coveralls.io/github/azjezz/input-hydrator?branch=develop)
9 | [](https://shepherd.dev/github/azjezz/input-hydrator)
10 | [](https://dashboard.stryker-mutator.io/reports/github.com/azjezz/input-hydrator/develop)
11 | [](https://packagist.org/packages/azjezz/input-hydrator)
12 | [](https://packagist.org/packages/azjezz/input-hydrator)
13 | [](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 |