├── LICENSE
├── README.md
├── composer.json
├── integrations
└── respect-validation
│ ├── README.md
│ └── src
│ ├── Api
│ └── Validation.php
│ ├── Context
│ ├── ValidationContext.php
│ └── ValidationContextFactory.php
│ ├── Processor
│ └── Validate.php
│ ├── ValidationContextFactoryInterface.php
│ ├── ValidationContextInterface.php
│ └── Value
│ └── ValidationFailedValue.php
├── psalm.xml
└── src
├── Api
├── Fields.php
├── Helpers.php
├── Main.php
└── Processors.php
├── AutoMapper.php
├── Context
└── Context.php
├── ContextInterface.php
├── Exception
├── NotFoundException.php
├── UnexpectedValueException.php
└── UnknownPartException.php
├── ExceptionValueInterface.php
├── Extractor
├── FromArrayKey.php
├── FromArrayKeyFirst.php
├── FromArrayKeyLast.php
├── FromObjectMethod.php
├── FromObjectProp.php
└── FromSelf.php
├── ExtractorInterface.php
├── ExtractorResolver.php
├── ExtractorResolverInterface.php
├── Field
├── ToArrayKey.php
├── ToObjectMethod.php
├── ToObjectProp.php
└── ToSelf.php
├── FieldInterface.php
├── Mapper.php
├── MapperExceptionInterface.php
├── MapperFactory.php
├── MapperFactoryInterface.php
├── MapperInterface.php
├── ObjectFieldInterface.php
├── Path
├── Parser.php
├── ParserInterface.php
├── Part
│ ├── ArrayKey.php
│ ├── ArrayKeyFirst.php
│ ├── ArrayKeyLast.php
│ ├── ObjectMethod.php
│ ├── ObjectProp.php
│ └── SelfPointer.php
├── PartInterface.php
├── Path.php
└── PathInterface.php
├── Processor
├── AssertType.php
├── Call.php
├── CallWithContext.php
├── Condition.php
├── ConditionWithContext.php
├── Find.php
├── FindWithContext.php
├── Get.php
├── GetFromContext.php
├── Ignore.php
├── MapIterable.php
├── MarshalNestedArray.php
├── MarshalNestedObject.php
├── NotFound.php
├── Pass.php
├── Pipeline.php
└── Value.php
├── ProcessorInterface.php
├── Value
├── IgnoreValue.php
├── NotFoundValue.php
└── UserValue.php
└── ValueInterface.php
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-present, Valeriy Protopopov
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ℹ️ You are on a branch with the second version of the `acelot/automapper`.
2 | If you want a previous version, then proceed to [1.x](https://github.com/acelot/automapper/tree/1.x) branch.
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | **AutoMapper** is a powerful declarative data mapper for PHP 8.
20 | AutoMapper can map data from any source data (usually array/object) to the existing array/object or marshal a new ones.
21 |
22 | ## 💿 Install
23 |
24 | ```bash
25 | composer require acelot/automapper:^2.0
26 | ```
27 |
28 | ## 🗿 Usage
29 |
30 | ### How to marshal new array from the another?
31 |
32 | ```php
33 | use Acelot\AutoMapper\Context;
34 | use Acelot\AutoMapper\AutoMapper as a;
35 |
36 | $source = [
37 | 'id' => '99',
38 | 'name' => [
39 | 'lastname' => 'Doe',
40 | 'firstname' => 'John',
41 | ],
42 | 'skills' => 'Php, CSS,JS,html, MySql, brainfuck,'
43 | ];
44 |
45 | $result = a::marshalArray(
46 | new Context(),
47 | $source,
48 | a::toKey('id', a::pipe(
49 | a::get('[id]'),
50 | a::toInt()
51 | )),
52 | a::toKey('fullname', a::pipe(
53 | a::marshalNestedArray(
54 | a::toKey(0, a::get('[name][firstname]')),
55 | a::toKey(1, a::get('[name][lastname]')),
56 | ),
57 | a::joinArray(' ')
58 | )),
59 | a::toKey('skills', a::pipe(
60 | a::get('[skills]'),
61 | a::explodeString(','),
62 | a::mapIterable(a::pipe(
63 | a::trimString(),
64 | a::ifEmpty(a::ignore()),
65 | a::call('strtolower')
66 | )),
67 | a::toArray(),
68 | a::sortArray()
69 | ))
70 | );
71 |
72 | // Output of `var_export($result)`
73 | array(
74 | 'id' => 99,
75 | 'fullname' => 'John Doe',
76 | 'skills' => [
77 | 0 => 'brainfuck',
78 | 1 => 'css',
79 | 2 => 'html',
80 | 3 => 'js',
81 | 4 => 'mysql',
82 | 5 => 'php',
83 | ],
84 | )
85 | ```
86 |
87 | ### How to map data from source to the existing array?
88 |
89 | Show the code
90 |
91 | ```php
92 | use Acelot\AutoMapper\Context;
93 | use Acelot\AutoMapper\AutoMapper as a;
94 |
95 | $source = [
96 | 'title' => ' Product title ',
97 | 'desc' => [
98 | 'Product short description',
99 | 'Product regular description',
100 | 'Product descriptive description',
101 | ]
102 | ];
103 |
104 | $target = [
105 | 'id' => 5,
106 | 'title' => 'Current title',
107 | ];
108 |
109 | $result = a::map(
110 | new Context(),
111 | $source,
112 | $target,
113 | a::toKey('title', a::pipe(
114 | a::get('[title]'),
115 | a::trimString()
116 | )),
117 | a::toKey('description', a::get('[desc][#last]')),
118 | );
119 |
120 | // Output of `var_export($result)`
121 | array (
122 | 'id' => 5,
123 | 'title' => 'Product title',
124 | 'description' => 'Product descriptive description',
125 | )
126 | ```
127 |
128 |
129 |
130 | ## 📌 Examples
131 |
132 | All examples can be found in [`tests/Functional`](tests/Functional) directory.
133 |
134 | ## 🗄️ Reference
135 |
136 | No need to use concrete classes, it's better to use the AutoMapper API [static functions](src/AutoMapper.php).
137 | It is very convenient to import the AutoMapper as a short alias, for example `use Acelot\AutoMapper\AutoMapper as a`.
138 |
139 | ### Main functions
140 |
141 | The main functions of AutoMapper.
142 |
143 | | Function | Description |
144 | |-----------------------------------------------------------|--------------------------------------------------------------------------|
145 | | [`map`](tests/Functional/mapTest.php) | Maps data from the source to the target. Target is passing by reference. |
146 | | [`marshalArray`](tests/Functional/marshalArrayTest.php) | Maps data from the source to the keys of the new array. |
147 | | [`marshalObject`](tests/Functional/marshalObjectTest.php) | Maps data from the source to the properties of the new `stdClass`. |
148 |
149 | ### Field definitions
150 |
151 | Definitions that helps you to shape the target structure.
152 |
153 | | Function | Description |
154 | |----------------------------------------------------|--------------------------------------------------------------------------------|
155 | | [`toKey`](tests/Functional/marshalArrayTest.php) | Sets/creates the value by key with given name in the target array. |
156 | | [`toProp`](tests/Functional/marshalObjectTest.php) | Sets/creates the value by property with given name in the target object. |
157 | | [`toMethod`](tests/Functional/toMethodTest.php) | Calls a method with given name with value as an argument in the target object. |
158 | | [`toSelf`](tests/Functional/toSelfTest.php) | Assigns a value to the target. |
159 |
160 | ### Processors
161 |
162 | Core value processors. The purpose of processors is to retrieve the value or mutate the incoming value and pass it to the next one.
163 |
164 | | Function | Description |
165 | |-----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
166 | | [`assertType`](tests/Functional/assertTypeTest.php) | Asserts the type of value. Throws `UnexpectedValueException` if assert is failed. |
167 | | [`call`](tests/Functional/callTest.php) | Process the value by user defined function. |
168 | | [`callCtx`](tests/Functional/callCtxTest.php) | Same as `call` but [context](#what-is-context) will be passed as a first argument. |
169 | | [`condition`](tests/Functional/conditionTest.php) | Condition processor. If the user-defined function returns `true`, then the value will be passed to the first processor, otherwise to the second. |
170 | | [`conditionCtx`](tests/Functional/conditionCtxTest.php) | Same as `condition` but [context](#what-is-context) will be passed to the user-defined function. |
171 | | [`find`](tests/Functional/findTest.php) | Finds the element in iterable by the given predicate function. |
172 | | [`findCtx`](tests/Functional/findCtxTest.php) | Same as `find` but [context](#what-is-context) will be passed to the predicate function. |
173 | | [`get`](tests/Functional/getTest.php) | Most useful processor. Fetches the value from the source by given [path](#how-to-use-get-processor). |
174 | | [`getFromCtx`](tests/Functional/getFromCtxTest.php) | Fetches the value from the [context](#what-is-context). |
175 | | [`ignore`](tests/Functional/ignoreTest.php) | Always returns the `IgnoreValue`. This value will be ignored by field definition, `mapArray` and `mapIterator` |
176 | | [`mapIterable`](tests/Functional/mapIterableTest.php) | Iterates over elements of an iterable and applies the given sub-processor. ℹ️ Produces values by `yield` operator, so in order to retrieve them you should to iterate the result or call `toArray` helper. |
177 | | [`marshalNestedArray`](tests/Functional/marshalNestedArrayTest.php) | The same function as `mapArray` only in the form of a processor. Accepts the value from the previous processor as a source. |
178 | | [`marshalNestedObject`](tests/Functional/marshalNestedObjectTest.php) | Same as `marshalNestedArray` only produces object. |
179 | | [`notFound`](tests/Functional/notFoundTest.php) | Always returns the `NotFoundValue`. |
180 | | [`pass`](tests/Functional/passTest.php) | This processor do nothing and just returns the value untouched. |
181 | | [`pipe`](tests/Functional/marshalNestedArrayTest.php) | Conveyor processor. Accepts multiple sub-processors and pass the value to the first sub-processor, then pass the result of the first to the second, then to the third and so on. |
182 | | [`value`](tests/Functional/conditionTest.php) | Just returns the given value. |
183 |
184 | ### Helpers
185 |
186 | Helpers are the processors that built on top of the another processors. Some helpers are just a shorthands to the core processors with specific arguments, some of them are combination of the multiple processors.
187 |
188 | | Function | Description |
189 | |-----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
190 | | [`joinArray`](tests/Functional/joinArrayTest.php) | Joins the incoming array using the given separator. Throws `UnexpectedValueException` if incoming value is not an array. |
191 | | [`sortArray`](tests/Functional/sortArrayTest.php) | Sorts the incoming array using built-in `sort` function. Throws `UnexpectedValueException` if incoming value is not an array. |
192 | | [`uniqueArray`](tests/Functional/uniqueArrayTest.php) | Returns only unique elements of the incoming array. Throws `UnexpectedValueException` if incoming value is not an array. |
193 | | [`ifNotFound`](tests/Functional/notFoundTest.php) | Checks if the incoming value is `NotFoundValue` and passes the value to other processors depending on the result. |
194 | | [`ifEmpty`](tests/Functional/ifEmptyTest.php) | Same as `ifNotFound` but checks if the value is `empty`. |
195 | | [`ifNull`](tests/Functional/ifNullTest.php) | Same as `ifNotFound` but checks if the value `is_null`. |
196 | | [`IfEqual`](tests/Functional/ifEqualTest.php) | Checks if the incoming value is equal to the given value. |
197 | | [`ifNotEqual`](tests/Functional/ifEqualTest.php) | Checks if the incoming value is not equal to the given value. |
198 | | [`explodeString`](tests/Functional/explodeStringTest.php) | Splits the incoming string into parts using built-in `explode` function. Throws `UnexpectedValueException` if incoming value is not a string. |
199 | | [`trimString`](tests/Functional/trimStringTest.php) | Trims the incoming string using built-in `trim` function. Throws `UnexpectedValueException` if incoming value is not a string. |
200 | | [`toBool`](tests/Functional/toBoolTest.php) | Converts the incoming value to boolean type. |
201 | | [`toFloat`](tests/Functional/toFloatTest.php) | Converts the incoming value to float type. Throws `UnexpectedValueException` if incoming value is not a scalar. |
202 | | [`toInt`](tests/Functional/toIntTest.php) | Converts the incoming value to integer type. Throws `UnexpectedValueException` if incoming value is not a scalar. |
203 | | [`toString`](tests/Functional/toStringTest.php) | Converts the incoming value to string type. Throws `UnexpectedValueException` if incoming value is not a scalar or an object that not implements `__toString`. |
204 | | [`toArray`](tests/Functional/toArrayTest.php) | Converts the incoming value to array. Usually used with `mapIterable` processor. |
205 |
206 | ## 🧩 Integrations
207 |
208 | | Name | Description |
209 | |--------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
210 | | [`RespectValidation`](integrations/respect-validation) | Provides validation processor via [`respect/validation`](https://github.com/Respect/Validation) library. |
211 |
212 | ## 🤨 FAQ
213 |
214 | ### What is Context?
215 |
216 | The `Context` is a special DTO class for storing any kind of data: configs, DB connections, fixtures, etc.
217 | This DTO is passed to the mapper, and you can use your data inside the processors.
218 | Processors capable of working with the context end with `Ctx` suffix, [`callCtx`](tests/Functional/callCtxTest.php) for example.
219 |
220 | ### How to use `get` processor?
221 |
222 | You can obtain any key/prop/method from the source using the [`get`](tests/Functional/getTest.php) processor which accepts a special path string.
223 | The processor parses the given path and divides it into parts, then pulls out the data following the parts of the path.
224 |
225 | Available path parts:
226 |
227 | | Part | Description |
228 | |---------------------|------------------------------------------------------|
229 | | `@` | "Self Pointer" – returns the source itself |
230 | | `[0]` | Returns an array value by index |
231 | | `[key]` | Returns an array value by key |
232 | | `[some key]` | Returns an array value by key with spaces |
233 | | `[#first]` | Returns an array first value |
234 | | `[#last]` | Returns an array last value |
235 | | `->property` | Returns an object value by property |
236 | | `->{some property}` | Returns an object value by property name with spaces |
237 | | `->someMethod()` | Calls an object method and returns the value |
238 |
239 | You can combine the parts to obtain the deep values:
240 |
241 | ```
242 | [array_key][array key with spaces][#first][#last]->property->{property with spaces}->someMethod()
243 | ```
244 |
245 | If any part of the path is not found, then the processor will return `NotFoundValue` value.
246 | This value throws an `NotFoundException` but you can recover it using [`ifNotFound`](tests/Functional/notFoundTest.php) helper.
247 |
248 | ## 🖋️ License
249 |
250 | Licensed under [MIT](LICENSE). Copyright (c) 2017-present, Valeriy Protopopov
251 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "acelot/automapper",
3 | "description": "Powerful declarative data mapper for PHP 8",
4 | "keywords": [
5 | "mapper",
6 | "automapper",
7 | "data-mapper",
8 | "marshaller"
9 | ],
10 | "type": "library",
11 | "homepage": "https://github.com/acelot/automapper",
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "Valeriy Protopopov",
16 | "email": "provaleriy@gmail.com"
17 | }
18 | ],
19 | "require": {
20 | "php": "^8.0"
21 | },
22 | "require-dev": {
23 | "phpunit/phpunit": "^9.0",
24 | "nikic/php-parser": "^4.15",
25 | "friendsofphp/php-cs-fixer": "^3.13",
26 | "respect/validation": "^2",
27 | "giggsey/libphonenumber-for-php": "*",
28 | "vimeo/psalm": "^5.26"
29 | },
30 | "suggest": {
31 | "respect/validation": "Enables respect-validation integration"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "Acelot\\AutoMapper\\": "src/",
36 | "Acelot\\AutoMapper\\Integrations\\RespectValidation\\": "integrations/respect-validation/src/"
37 | }
38 | },
39 | "autoload-dev": {
40 | "psr-4": {
41 | "Acelot\\AutoMapper\\Tests\\": "tests/",
42 | "Acelot\\AutoMapper\\Integrations\\RespectValidation\\Tests\\": "integrations/respect-validation/tests/"
43 | }
44 | },
45 | "bin": ["bin/generate-static-api"],
46 | "scripts": {
47 | "psalm": "psalm",
48 | "test": "phpunit",
49 | "generate-static-api": "generate-static-api",
50 | "php-cs-fixer": "php-cs-fixer"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/integrations/respect-validation/README.md:
--------------------------------------------------------------------------------
1 | # Respect Validation Integration for AutoMapper
2 |
3 | To use this integration you should install the [`respect/validation`](https://packagist.org/packages/respect/validation) composer package first.
4 |
5 | ## Usage
6 |
7 | ### How to validate source data?
8 |
9 | `\Respect\Validation\Exceptions\ValidationException` will be thrown on error.
10 |
11 | ```php
12 | use Acelot\AutoMapper\Context;
13 | use Acelot\AutoMapper\Integrations\RespectValidation\Context\ValidationContext;
14 | use Acelot\AutoMapper\AutoMapper as a;
15 | use Respect\Validation\Validator as v;
16 |
17 | $source = [
18 | 'email' => 'press@google.com',
19 | 'phone' => '1-650-253-0000'
20 | ];
21 |
22 | $result = marshalArray(
23 | new Context(),
24 | $source,
25 | a::toKey('email', a::pipe(
26 | a::get('[email]'),
27 | a::validate(v::email())
28 | ))
29 | a::toKey('phone', a::pipe(
30 | a::get('[phone]'),
31 | a::validate(v::phone())
32 | )),
33 | );
34 |
35 | // Output of `var_export($result)`
36 | array(
37 | 'email' => 'press@google.com',
38 | 'phone' => '1-650-253-0000',
39 | )
40 | ```
41 |
42 | ### How to recover a validation exception?
43 |
44 | ```php
45 | use Acelot\AutoMapper\Context;
46 | use Acelot\AutoMapper\AutoMapper as a;
47 | use Acelot\AutoMapper\Integrations\RespectValidation\ValidationContextInterface;
48 | use Respect\Validation\Validator as v;
49 |
50 | $source = [
51 | 'email' => 'bademail@',
52 | 'phone' => '1-650-253-0000'
53 | ];
54 |
55 | $context = new Context();
56 |
57 | $result = marshalArray(
58 | $context,
59 | $source,
60 | a::toKey('email', a::pipe(
61 | a::get('[email]'),
62 | a::validate(v::email()),
63 | a::ifValidationFailed(a::ignore())
64 | )),
65 | a::toKey('phone', a::pipe(
66 | a::get('[phone]'),
67 | a::validate(v::phone()),
68 | a::ifValidationFailed(a::ignore())
69 | ))
70 | );
71 |
72 | // Output of `var_export($result)`
73 | array(
74 | 'phone' => '1-650-253-0000',
75 | )
76 |
77 | // Validation errors can be found in Context
78 | $validationErrors = $context->get(ValidationContextInterface::class)->getErrors();
79 | ```
80 |
81 | ### Processors
82 |
83 | | Function | Description |
84 | |-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
85 | | [`validate`](tests/Functional/validateTest.php) | Validates the value and pass it if it's valid or returns the [`ValidationFailedValue`](src/Value/ValidationFailedValue.php) |
86 |
87 | ### Helpers
88 |
89 | | Function | Description |
90 | |-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
91 | | [`ifValidationFailed`](tests/Functional/validateTest.php) | Checks if the incoming value is `ValidationFailedValue` and passes the value to other processors depending on the result. |
92 |
--------------------------------------------------------------------------------
/integrations/respect-validation/src/Api/Validation.php:
--------------------------------------------------------------------------------
1 | $value instanceof ValidationFailedValue, $true, $false ?? new Pass());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/integrations/respect-validation/src/Context/ValidationContext.php:
--------------------------------------------------------------------------------
1 | errors[] = $e;
18 | }
19 |
20 | public function hasErrors(): bool
21 | {
22 | return count($this->errors) > 0;
23 | }
24 |
25 | public function getErrors(): array
26 | {
27 | return $this->errors;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/integrations/respect-validation/src/Context/ValidationContextFactory.php:
--------------------------------------------------------------------------------
1 | validator;
25 | }
26 |
27 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
28 | {
29 | if (!$value instanceof UserValue) {
30 | return $value;
31 | }
32 |
33 | try {
34 | $this->validator->assert($value->getValue());
35 | } catch (ValidationException $e) {
36 | $this->getValidationContext($context)->addError($e);
37 |
38 | return new ValidationFailedValue($e);
39 | }
40 |
41 | return $value;
42 | }
43 |
44 | private function getValidationContext(ContextInterface $context): ValidationContextInterface
45 | {
46 | if (!$context->has(ValidationContextInterface::class)) {
47 | $context->set(ValidationContextInterface::class, $this->validationContextFactory->create());
48 | }
49 |
50 | return $context->get(ValidationContextInterface::class);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/integrations/respect-validation/src/ValidationContextFactoryInterface.php:
--------------------------------------------------------------------------------
1 | exception;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Api/Fields.php:
--------------------------------------------------------------------------------
1 | implode($separator, $value))
22 | );
23 | }
24 |
25 | public function sortArray(bool $descending = false, int $options = SORT_REGULAR): Pipeline
26 | {
27 | return new Pipeline(
28 | new AssertType(AssertType::ARRAY),
29 | new Call(function (array $value) use ($descending, $options) {
30 | if ($descending) {
31 | rsort($value, $options);
32 | } else {
33 | sort($value, $options);
34 | }
35 |
36 | return $value;
37 | })
38 | );
39 | }
40 |
41 | public function uniqueArray(bool $keepKeys = false, int $options = SORT_STRING): Pipeline
42 | {
43 | return new Pipeline(
44 | new AssertType(AssertType::ARRAY),
45 | new Call(function (array $value) use ($keepKeys, $options) {
46 | $items = array_unique($value, $options);
47 |
48 | return $keepKeys ? $items : array_values($items);
49 | })
50 | );
51 | }
52 |
53 | public function ifNotFound(ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
54 | {
55 | return new Condition(fn(mixed $value) => $value instanceof NotFoundValue, $true, $false ?? new Pass());
56 | }
57 |
58 | public function ifEmpty(ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
59 | {
60 | return new Condition(fn(mixed $value) => empty($value), $true, $false ?? new Pass());
61 | }
62 |
63 | public function ifNull(ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
64 | {
65 | return new Condition('is_null', $true, $false ?? new Pass());
66 | }
67 |
68 | public function ifEqual(mixed $to, ProcessorInterface $true, ?ProcessorInterface $false = null, bool $strict = true): Condition
69 | {
70 | return new Condition(fn(mixed $value) => $strict ? $value === $to : $value == $to, $true, $false ?? new Pass());
71 | }
72 |
73 | public function ifNotEqual(mixed $to, ProcessorInterface $true, ?ProcessorInterface $false = null, bool $strict = true): Condition
74 | {
75 | return new Condition(fn(mixed $value) => $strict ? $value !== $to : $value != $to, $true, $false ?? new Pass());
76 | }
77 |
78 | /**
79 | * @param non-empty-string $separator
80 | * @return Pipeline
81 | */
82 | public function explodeString(string $separator): Pipeline
83 | {
84 | return new Pipeline(
85 | new AssertType(AssertType::STRING),
86 | new Call(fn(string $value) => explode($separator, $value))
87 | );
88 | }
89 |
90 | public function trimString(string $characters = " \t\n\r\0\x0B"): Pipeline
91 | {
92 | return new Pipeline(
93 | new AssertType(AssertType::STRING),
94 | new Call(fn(string $value) => trim($value, $characters))
95 | );
96 | }
97 |
98 | public function toBool(): Call
99 | {
100 | return new Call('boolval');
101 | }
102 |
103 | public function toFloat(): Pipeline
104 | {
105 | return new Pipeline(
106 | new AssertType(AssertType::NULL, AssertType::SCALAR),
107 | new Call(fn(mixed $value) => is_null($value) ? 0.0 : floatval($value))
108 | );
109 | }
110 |
111 | public function toInt(): Pipeline
112 | {
113 | return new Pipeline(
114 | new AssertType(AssertType::NULL, AssertType::SCALAR),
115 | new Call(fn(mixed $value) => is_null($value) ? 0 : intval($value))
116 | );
117 | }
118 |
119 | public function toString(): Pipeline
120 | {
121 | return new Pipeline(
122 | new AssertType(AssertType::NULL, AssertType::SCALAR, AssertType::TO_STRING),
123 | new Call('strval')
124 | );
125 | }
126 |
127 | public function toArray(): Call
128 | {
129 | return new Call(function (mixed $value) {
130 | if ($value instanceof Traversable) {
131 | return iterator_to_array($value);
132 | }
133 |
134 | if (is_array($value)) {
135 | return $value;
136 | }
137 |
138 | return [$value];
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/Api/Main.php:
--------------------------------------------------------------------------------
1 | mapperFactory = $mapperFactory;
24 | }
25 |
26 | public function map(ContextInterface $context, mixed $source, mixed &$target, FieldInterface ...$fields): void
27 | {
28 | $mapper = $this->mapperFactory->create($context, ...$fields);
29 | $mapper->map($source, $target);
30 | }
31 |
32 | public function marshalArray(ContextInterface $context, mixed $source, ToArrayKey ...$fields): array
33 | {
34 | $target = [];
35 |
36 | $mapper = $this->mapperFactory->create($context, ...$fields);
37 | $mapper->map($source, $target);
38 |
39 | /** @var array $target */
40 | return $target;
41 | }
42 |
43 | public function marshalObject(ContextInterface $context, mixed $source, ObjectFieldInterface ...$fields): stdClass
44 | {
45 | $target = new stdClass();
46 |
47 | $mapper = $this->mapperFactory->create($context, ...$fields);
48 | $mapper->map($source, $target);
49 |
50 | /** @var \stdClass $target */
51 | return $target;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Api/Processors.php:
--------------------------------------------------------------------------------
1 | map($context, $source, $target, ...$fields);
45 | }
46 |
47 | public static function marshalArray(ContextInterface $context, mixed $source, ToArrayKey ...$fields): array
48 | {
49 | return self::getInstance(Main::class)->marshalArray($context, $source, ...$fields);
50 | }
51 |
52 | public static function marshalObject(ContextInterface $context, mixed $source, ObjectFieldInterface ...$fields): stdClass
53 | {
54 | return self::getInstance(Main::class)->marshalObject($context, $source, ...$fields);
55 | }
56 |
57 | public static function toKey(int|string $key, ProcessorInterface $processor): ToArrayKey
58 | {
59 | return self::getInstance(Fields::class)->toKey($key, $processor);
60 | }
61 |
62 | public static function toProp(string $property, ProcessorInterface $processor): ToObjectProp
63 | {
64 | return self::getInstance(Fields::class)->toProp($property, $processor);
65 | }
66 |
67 | public static function toMethod(string $method, ProcessorInterface $processor): ToObjectMethod
68 | {
69 | return self::getInstance(Fields::class)->toMethod($method, $processor);
70 | }
71 |
72 | public static function toSelf(ProcessorInterface $processor): ToSelf
73 | {
74 | return self::getInstance(Fields::class)->toSelf($processor);
75 | }
76 |
77 | public static function assertType(string $oneOfType, string ...$oneOfTypes): AssertType
78 | {
79 | return self::getInstance(Processors::class)->assertType($oneOfType, ...$oneOfTypes);
80 | }
81 |
82 | public static function call(callable $callable): Call
83 | {
84 | return self::getInstance(Processors::class)->call($callable);
85 | }
86 |
87 | public static function callCtx(callable $callable): CallWithContext
88 | {
89 | return self::getInstance(Processors::class)->callCtx($callable);
90 | }
91 |
92 | /**
93 | * @param callable(mixed): bool $condition
94 | * @param ProcessorInterface $true
95 | * @param ProcessorInterface|null $false
96 | * @return Condition
97 | */
98 | public static function condition(callable $condition, ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
99 | {
100 | return self::getInstance(Processors::class)->condition($condition, $true, $false);
101 | }
102 |
103 | /**
104 | * @param callable(ContextInterface, mixed): bool $condition
105 | * @param ProcessorInterface $true
106 | * @param ProcessorInterface|null $false
107 | * @return ConditionWithContext
108 | */
109 | public static function conditionCtx(callable $condition, ProcessorInterface $true, ?ProcessorInterface $false = null): ConditionWithContext
110 | {
111 | return self::getInstance(Processors::class)->conditionCtx($condition, $true, $false);
112 | }
113 |
114 | /**
115 | * @param callable(mixed, int|string): bool $predicate
116 | * @return Find
117 | */
118 | public static function find(callable $predicate): Find
119 | {
120 | return self::getInstance(Processors::class)->find($predicate);
121 | }
122 |
123 | /**
124 | * @param callable(ContextInterface, mixed, int|string): bool $predicate
125 | * @return FindWithContext
126 | */
127 | public static function findCtx(callable $predicate): FindWithContext
128 | {
129 | return self::getInstance(Processors::class)->findCtx($predicate);
130 | }
131 |
132 | public static function get(string $path): Get
133 | {
134 | return self::getInstance(Processors::class)->get($path);
135 | }
136 |
137 | public static function getFromCtx(string $key): GetFromContext
138 | {
139 | return self::getInstance(Processors::class)->getFromCtx($key);
140 | }
141 |
142 | public static function ignore(): Ignore
143 | {
144 | return self::getInstance(Processors::class)->ignore();
145 | }
146 |
147 | public static function mapIterable(ProcessorInterface $processor, bool $keepKeys = false): MapIterable
148 | {
149 | return self::getInstance(Processors::class)->mapIterable($processor, $keepKeys);
150 | }
151 |
152 | public static function marshalNestedArray(ToArrayKey $firstField, ToArrayKey ...$restFields): MarshalNestedArray
153 | {
154 | return self::getInstance(Processors::class)->marshalNestedArray($firstField, ...$restFields);
155 | }
156 |
157 | public static function marshalNestedObject(ToObjectProp $firstField, ToObjectProp ...$restFields): MarshalNestedObject
158 | {
159 | return self::getInstance(Processors::class)->marshalNestedObject($firstField, ...$restFields);
160 | }
161 |
162 | public static function notFound(string $path): NotFound
163 | {
164 | return self::getInstance(Processors::class)->notFound($path);
165 | }
166 |
167 | public static function value(mixed $value): Value
168 | {
169 | return self::getInstance(Processors::class)->value($value);
170 | }
171 |
172 | public static function pass(): Pass
173 | {
174 | return self::getInstance(Processors::class)->pass();
175 | }
176 |
177 | public static function pipe(ProcessorInterface ...$processors): Pipeline
178 | {
179 | return self::getInstance(Processors::class)->pipe(...$processors);
180 | }
181 |
182 | public static function joinArray(string $separator = ''): Pipeline
183 | {
184 | return self::getInstance(Helpers::class)->joinArray($separator);
185 | }
186 |
187 | public static function sortArray(bool $descending = false, int $options = SORT_REGULAR): Pipeline
188 | {
189 | return self::getInstance(Helpers::class)->sortArray($descending, $options);
190 | }
191 |
192 | public static function uniqueArray(bool $keepKeys = false, int $options = SORT_STRING): Pipeline
193 | {
194 | return self::getInstance(Helpers::class)->uniqueArray($keepKeys, $options);
195 | }
196 |
197 | public static function ifNotFound(ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
198 | {
199 | return self::getInstance(Helpers::class)->ifNotFound($true, $false);
200 | }
201 |
202 | public static function ifEmpty(ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
203 | {
204 | return self::getInstance(Helpers::class)->ifEmpty($true, $false);
205 | }
206 |
207 | public static function ifNull(ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
208 | {
209 | return self::getInstance(Helpers::class)->ifNull($true, $false);
210 | }
211 |
212 | public static function ifEqual(mixed $to, ProcessorInterface $true, ?ProcessorInterface $false = null, bool $strict = true): Condition
213 | {
214 | return self::getInstance(Helpers::class)->ifEqual($to, $true, $false, $strict);
215 | }
216 |
217 | public static function ifNotEqual(mixed $to, ProcessorInterface $true, ?ProcessorInterface $false = null, bool $strict = true): Condition
218 | {
219 | return self::getInstance(Helpers::class)->ifNotEqual($to, $true, $false, $strict);
220 | }
221 |
222 | /**
223 | * @param non-empty-string $separator
224 | * @return Pipeline
225 | */
226 | public static function explodeString(string $separator): Pipeline
227 | {
228 | return self::getInstance(Helpers::class)->explodeString($separator);
229 | }
230 |
231 | public static function trimString(string $characters = " \t\n\r\x00\v"): Pipeline
232 | {
233 | return self::getInstance(Helpers::class)->trimString($characters);
234 | }
235 |
236 | public static function toBool(): Call
237 | {
238 | return self::getInstance(Helpers::class)->toBool();
239 | }
240 |
241 | public static function toFloat(): Pipeline
242 | {
243 | return self::getInstance(Helpers::class)->toFloat();
244 | }
245 |
246 | public static function toInt(): Pipeline
247 | {
248 | return self::getInstance(Helpers::class)->toInt();
249 | }
250 |
251 | public static function toString(): Pipeline
252 | {
253 | return self::getInstance(Helpers::class)->toString();
254 | }
255 |
256 | public static function toArray(): Call
257 | {
258 | return self::getInstance(Helpers::class)->toArray();
259 | }
260 |
261 | public static function validate(Validatable $validator): Validate
262 | {
263 | return self::getInstance(Validation::class)->validate($validator);
264 | }
265 |
266 | public static function ifValidationFailed(ProcessorInterface $true, ?ProcessorInterface $false = null): Condition
267 | {
268 | return self::getInstance(Validation::class)->ifValidationFailed($true, $false);
269 | }
270 |
271 | /**
272 | * @template T
273 | * @param class-string $class
274 | * @return T
275 | */
276 | private static function getInstance(string $class)
277 | {
278 | if (!isset(self::$instances[$class])) {
279 | self::$instances[$class] = new $class();
280 | }
281 |
282 | return self::$instances[$class];
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/src/Context/Context.php:
--------------------------------------------------------------------------------
1 | $items
12 | */
13 | public function __construct(
14 | private array $items = []
15 | ) {}
16 |
17 | public function has(int|string $key): bool
18 | {
19 | return array_key_exists($key, $this->items);
20 | }
21 |
22 | /**
23 | * @template T
24 | * @param int|string|class-string $key
25 | * @return mixed|T
26 | */
27 | public function get(int|string $key): mixed
28 | {
29 | if (!$this->has($key)) {
30 | throw new OutOfBoundsException('Key does not exists in context');
31 | }
32 |
33 | return $this->items[$key];
34 | }
35 |
36 | public function set(int|string $key, mixed $value): void
37 | {
38 | $this->items[$key] = $value;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/ContextInterface.php:
--------------------------------------------------------------------------------
1 | $key
12 | * @return mixed|T
13 | */
14 | public function get(int|string $key): mixed;
15 |
16 | public function set(int|string $key, mixed $value): void;
17 | }
18 |
--------------------------------------------------------------------------------
/src/Exception/NotFoundException.php:
--------------------------------------------------------------------------------
1 | path;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exception/UnexpectedValueException.php:
--------------------------------------------------------------------------------
1 | getActualType())
17 | );
18 | }
19 |
20 | public function getExpectedType(): string
21 | {
22 | return $this->expectedType;
23 | }
24 |
25 | public function getActualValue(): mixed
26 | {
27 | return $this->actualValue;
28 | }
29 |
30 | public function getActualType(): string
31 | {
32 | return is_object($this->actualValue)
33 | ? get_class($this->actualValue)
34 | : gettype($this->actualValue);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Exception/UnknownPartException.php:
--------------------------------------------------------------------------------
1 | part;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ExceptionValueInterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class FromArrayKey implements ExtractorInterface
11 | {
12 | /**
13 | * @param array-key $key
14 | */
15 | public function __construct(
16 | private mixed $key
17 | ) {}
18 |
19 | /**
20 | * @return array-key
21 | */
22 | public function getKey(): mixed
23 | {
24 | return $this->key;
25 | }
26 |
27 | public function isExtractable(mixed $source): bool
28 | {
29 | return (
30 | (is_string($source) && is_int($this->key) && isset($source[$this->key])) ||
31 | (is_array($source) && array_key_exists($this->key, $source))
32 | );
33 | }
34 |
35 | public function extract(mixed $source): mixed
36 | {
37 | return is_string($source) ? $source[(int) $this->key] : $source[$this->key];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Extractor/FromArrayKeyFirst.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class FromArrayKeyFirst implements ExtractorInterface
11 | {
12 | public function isExtractable(mixed $source): bool
13 | {
14 | return is_array($source) && !empty($source);
15 | }
16 |
17 | public function extract(mixed $source): mixed
18 | {
19 | return $source[array_key_first($source)];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Extractor/FromArrayKeyLast.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class FromArrayKeyLast implements ExtractorInterface
11 | {
12 | public function isExtractable(mixed $source): bool
13 | {
14 | return is_array($source) && !empty($source);
15 | }
16 |
17 | public function extract(mixed $source): mixed
18 | {
19 | return $source[array_key_last($source)];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Extractor/FromObjectMethod.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class FromObjectMethod implements ExtractorInterface
11 | {
12 | public function __construct(
13 | private string $method
14 | ) {}
15 |
16 | public function getMethod(): string
17 | {
18 | return $this->method;
19 | }
20 |
21 | public function isExtractable(mixed $source): bool
22 | {
23 | return is_object($source) && method_exists($source, $this->method);
24 | }
25 |
26 | public function extract(mixed $source): mixed
27 | {
28 | return call_user_func([$source, $this->method]);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Extractor/FromObjectProp.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class FromObjectProp implements ExtractorInterface
11 | {
12 | public function __construct(
13 | private string $property
14 | ) {}
15 |
16 | public function getProperty(): string
17 | {
18 | return $this->property;
19 | }
20 |
21 | public function isExtractable(mixed $source): bool
22 | {
23 | return is_object($source) && property_exists($source, $this->property);
24 | }
25 |
26 | public function extract(mixed $source): mixed
27 | {
28 | return $source->{$this->property};
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Extractor/FromSelf.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class FromSelf implements ExtractorInterface
11 | {
12 | public function isExtractable(mixed $source): bool
13 | {
14 | return true;
15 | }
16 |
17 | public function extract(mixed $source): mixed
18 | {
19 | return $source;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ExtractorInterface.php:
--------------------------------------------------------------------------------
1 | getKey());
16 | }
17 |
18 | if ($part instanceof Part\ArrayKeyFirst) {
19 | return new Extractor\FromArrayKeyFirst();
20 | }
21 |
22 | if ($part instanceof Part\ArrayKeyLast) {
23 | return new Extractor\FromArrayKeyLast();
24 | }
25 |
26 | if ($part instanceof Part\ObjectMethod) {
27 | return new Extractor\FromObjectMethod($part->getMethod());
28 | }
29 |
30 | if ($part instanceof Part\ObjectProp) {
31 | return new Extractor\FromObjectProp($part->getProperty());
32 | }
33 |
34 | if ($part instanceof Part\SelfPointer) {
35 | return new Extractor\FromSelf();
36 | }
37 |
38 | throw new UnknownPartException($part);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/ExtractorResolverInterface.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | final class ToArrayKey implements FieldInterface
12 | {
13 | public function __construct(
14 | private int|string $key,
15 | private ProcessorInterface $processor
16 | ) {}
17 |
18 | public function getKey(): int|string
19 | {
20 | return $this->key;
21 | }
22 |
23 | public function getProcessor(): ProcessorInterface
24 | {
25 | return $this->processor;
26 | }
27 |
28 | public function writeValue(mixed &$target, mixed $value): void
29 | {
30 | /** @psalm-suppress MixedAssignment,MixedArrayAssignment */
31 | $target[$this->key] = $value;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Field/ToObjectMethod.php:
--------------------------------------------------------------------------------
1 | method;
18 | }
19 |
20 | public function getProcessor(): ProcessorInterface
21 | {
22 | return $this->processor;
23 | }
24 |
25 | public function writeValue(mixed &$target, mixed $value): void
26 | {
27 | call_user_func([$target, $this->method], $value);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Field/ToObjectProp.php:
--------------------------------------------------------------------------------
1 | property;
18 | }
19 |
20 | public function getProcessor(): ProcessorInterface
21 | {
22 | return $this->processor;
23 | }
24 |
25 | public function writeValue(mixed &$target, mixed $value): void
26 | {
27 | $target->{$this->property} = $value;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Field/ToSelf.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | final class ToSelf implements FieldInterface
12 | {
13 | public function __construct(
14 | private ProcessorInterface $processor
15 | ) {}
16 |
17 | public function getProcessor(): ProcessorInterface
18 | {
19 | return $this->processor;
20 | }
21 |
22 | public function writeValue(mixed &$target, mixed $value): void
23 | {
24 | /** @psalm-suppress MixedAssignment */
25 | $target = $value;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/FieldInterface.php:
--------------------------------------------------------------------------------
1 | context = $context;
20 | $this->fields = $fields;
21 | }
22 |
23 | public function getContext(): ContextInterface
24 | {
25 | return $this->context;
26 | }
27 |
28 | /**
29 | * @return FieldInterface[]
30 | */
31 | public function getFields(): array
32 | {
33 | return $this->fields;
34 | }
35 |
36 | public function map(mixed $source, mixed &$target): void
37 | {
38 | foreach ($this->fields as $field) {
39 | $value = $field->getProcessor()->process($this->context, new UserValue($source));
40 |
41 | if ($value instanceof IgnoreValue) {
42 | continue;
43 | }
44 |
45 | if ($value instanceof ExceptionValueInterface) {
46 | throw $value->getException();
47 | }
48 |
49 | if ($value instanceof UserValue) {
50 | $field->writeValue($target, $value->getValue());
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/MapperExceptionInterface.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | interface ObjectFieldInterface extends FieldInterface
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/src/Path/Parser.php:
--------------------------------------------------------------------------------
1 | @)|' . // pointer to self `@`
28 | '->(?<' . Parser::GROUP_METHOD . '>\w+)\(\)|' . // object method `->methodName()`
29 | '->(?<' . Parser::GROUP_PROP . '>\w+)|' . // object property `->property`
30 | '->{(?<' . Parser::GROUP_SPROP . '>[^}]+)}|' . // object property with spaces `->{some weird property}`
31 | '\[#(?<' . Parser::GROUP_POS . '>first|last)\]|' . // array position `[#first]` or `[#last]`
32 | '\[(?<' . Parser::GROUP_INDEX . '>\d+)\]|' . // array index `[0]`
33 | '\[(?<' . Parser::GROUP_KEY . '>[^]]+)\]|' . // array key `[key]` or `[key with spaces]`
34 | '(?<' . Parser::GROUP_ERROR . '>.+)' . // error indicator
35 | '/u';
36 |
37 | private const GROUPS = [
38 | Parser::GROUP_SELF,
39 | Parser::GROUP_METHOD,
40 | Parser::GROUP_PROP,
41 | Parser::GROUP_SPROP,
42 | Parser::GROUP_POS,
43 | Parser::GROUP_INDEX,
44 | Parser::GROUP_KEY,
45 | Parser::GROUP_ERROR,
46 | ];
47 |
48 | public function parse(string $path): Path
49 | {
50 | if (preg_match_all(self::PATH_PATTERN, $path, $matches) === 0) {
51 | throw new InvalidArgumentException('Invalid path');
52 | }
53 |
54 | if (!empty(array_filter($matches[Parser::GROUP_ERROR]))) {
55 | throw new InvalidArgumentException('Invalid path');
56 | }
57 |
58 | $tokens = $this->getTokens($matches);
59 |
60 | return new Path(...$this->convertToParts($tokens));
61 | }
62 |
63 | /**
64 | * @param string[][] $matches
65 | * @return array
66 | */
67 | private function getTokens(array $matches): array
68 | {
69 | $tokens = [];
70 |
71 | for ($i = 0; $i < count($matches[0]); $i++) {
72 | foreach (self::GROUPS as $group) {
73 | /** @psalm-suppress RiskyTruthyFalsyComparison */
74 | if (!empty($matches[$group][$i])) {
75 | $tokens[$i] = [$group, $matches[$group][$i]];
76 | continue 2;
77 | }
78 | }
79 | }
80 |
81 | return $tokens;
82 | }
83 |
84 | /**
85 | * @param array $tokens
86 | * @return PartInterface[]
87 | */
88 | private function convertToParts(array $tokens): array
89 | {
90 | $parts = [];
91 |
92 | foreach ($tokens as [$group, $value]) {
93 | $parts[] = match ($group) {
94 | self::GROUP_SELF => new SelfPointer(),
95 | self::GROUP_METHOD => new ObjectMethod($value),
96 | self::GROUP_PROP, self::GROUP_SPROP => new ObjectProp($value),
97 | self::GROUP_KEY => new ArrayKey($value),
98 | self::GROUP_INDEX => new ArrayKey(intval($value)),
99 | self::GROUP_POS => $value === 'first' ? new ArrayKeyFirst() : new ArrayKeyLast(),
100 | default => throw new UnknownPartException($group)
101 | };
102 | }
103 |
104 | return $parts;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Path/ParserInterface.php:
--------------------------------------------------------------------------------
1 | key;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Path/Part/ArrayKeyFirst.php:
--------------------------------------------------------------------------------
1 | method;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Path/Part/ObjectProp.php:
--------------------------------------------------------------------------------
1 | property;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Path/Part/SelfPointer.php:
--------------------------------------------------------------------------------
1 | parts = $parts;
15 | }
16 |
17 | /**
18 | * @return PartInterface[]
19 | */
20 | public function getParts(): array
21 | {
22 | return $this->parts;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Path/PathInterface.php:
--------------------------------------------------------------------------------
1 | oneOfTypes = $oneOfTypes;
37 | }
38 |
39 | /**
40 | * @return string[]
41 | */
42 | public function getOneOfTypes(): array
43 | {
44 | return $this->oneOfTypes;
45 | }
46 |
47 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
48 | {
49 | if (!$value instanceof UserValue) {
50 | return $value;
51 | }
52 |
53 | $asserts = array_map(fn(string $type) => $this->getAssert($type), $this->oneOfTypes);
54 |
55 | foreach ($asserts as $assert) {
56 | if ($assert($value->getValue())) {
57 | return $value;
58 | }
59 | }
60 |
61 | throw new UnexpectedValueException(join('|', $this->oneOfTypes), $value->getValue());
62 | }
63 |
64 | private function getAssert(string $type): callable
65 | {
66 | return match ($type) {
67 | self::BOOL => static fn(mixed $v) => is_bool($v),
68 | self::INT => static fn(mixed $v) => is_int($v),
69 | self::FLOAT => static fn(mixed $v) => is_float($v),
70 | self::SCALAR => static fn(mixed $v) => is_scalar($v),
71 | self::STRING => static fn(mixed $v) => is_string($v),
72 | self::TO_STRING => static fn(mixed $v) => is_object($v) && method_exists($v, '__toString'),
73 | self::ITERABLE => static fn(mixed $v) => is_iterable($v),
74 | self::ARRAY => static fn(mixed $v) => is_array($v),
75 | self::OBJECT => static fn(mixed $v) => is_object($v),
76 | self::CALLABLE => static fn(mixed $v) => is_callable($v),
77 | self::NUMERIC => static fn(mixed $v) => is_numeric($v),
78 | self::COUNTABLE => static fn(mixed $v) => is_countable($v),
79 | self::RESOURCE => static fn(mixed $v) => is_resource($v),
80 | self::NULL => static fn(mixed $v) => is_null($v),
81 | default => throw new InvalidArgumentException("Unknown assert type `$type`")
82 | };
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Processor/Call.php:
--------------------------------------------------------------------------------
1 | callable = $callable;
20 | }
21 |
22 | public function getCallable(): callable
23 | {
24 | return $this->callable;
25 | }
26 |
27 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
28 | {
29 | if (!$value instanceof UserValue) {
30 | return $value;
31 | }
32 |
33 | /** @var mixed $newValue */
34 | $newValue = ($this->callable)($value->getValue());
35 |
36 | if (!$newValue instanceof ValueInterface) {
37 | $newValue = new UserValue($newValue);
38 | }
39 |
40 | return $newValue;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Processor/CallWithContext.php:
--------------------------------------------------------------------------------
1 | callable = $callable;
20 | }
21 |
22 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
23 | {
24 | if (!$value instanceof UserValue) {
25 | return $value;
26 | }
27 |
28 | /** @var mixed $newValue */
29 | $newValue = ($this->callable)($context, $value->getValue());
30 |
31 | if (!$newValue instanceof ValueInterface) {
32 | $newValue = new UserValue($newValue);
33 | }
34 |
35 | return $newValue;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Processor/Condition.php:
--------------------------------------------------------------------------------
1 | condition = $condition;
29 | }
30 |
31 | public function getCondition(): callable
32 | {
33 | return $this->condition;
34 | }
35 |
36 | public function getTrueProcessor(): ProcessorInterface
37 | {
38 | return $this->true;
39 | }
40 |
41 | public function getFalseProcessor(): ProcessorInterface
42 | {
43 | return $this->false;
44 | }
45 |
46 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
47 | {
48 | if (($this->condition)($value instanceof UserValue ? $value->getValue() : $value)) {
49 | return $this->true->process($context, $value);
50 | }
51 |
52 | return $this->false->process($context, $value);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Processor/ConditionWithContext.php:
--------------------------------------------------------------------------------
1 | condition = $condition;
29 | }
30 |
31 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
32 | {
33 | $result = ($this->condition)(
34 | $context,
35 | $value instanceof UserValue ? $value->getValue() : $value
36 | );
37 |
38 | if ($result) {
39 | return $this->ifTrue->process($context, $value);
40 | }
41 |
42 | return $this->ifFalse->process($context, $value);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Processor/Find.php:
--------------------------------------------------------------------------------
1 | predicate = $predicate;
25 | }
26 |
27 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
28 | {
29 | if (!$value instanceof UserValue) {
30 | return $value;
31 | }
32 |
33 | $innerValue = $value->getValue();
34 |
35 | if (!is_iterable($innerValue)) {
36 | throw new UnexpectedValueException('array|Traversable', $innerValue);
37 | }
38 |
39 | /**
40 | * @var array-key $key
41 | * @var mixed $item
42 | */
43 | foreach ($innerValue as $key => $item) {
44 | if (($this->predicate)($item, $key)) {
45 | return new UserValue($item);
46 | }
47 | }
48 |
49 | return new NotFoundValue('by predicate');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Processor/FindWithContext.php:
--------------------------------------------------------------------------------
1 | predicate = $predicate;
25 | }
26 |
27 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
28 | {
29 | if (!$value instanceof UserValue) {
30 | return $value;
31 | }
32 |
33 | $innerValue = $value->getValue();
34 |
35 | if (!is_iterable($innerValue)) {
36 | throw new UnexpectedValueException('array|Traversable', $innerValue);
37 | }
38 |
39 | /**
40 | * @var array-key $key
41 | * @var mixed $item
42 | */
43 | foreach ($innerValue as $key => $item) {
44 | if (($this->predicate)($context, $item, $key)) {
45 | return new UserValue($item);
46 | }
47 | }
48 |
49 | return new NotFoundValue('by predicate');
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Processor/Get.php:
--------------------------------------------------------------------------------
1 | parser->parse($this->path);
28 |
29 | /** @var mixed $currentValue */
30 | $currentValue = $value->getValue();
31 |
32 | foreach ($path->getParts() as $part) {
33 | $extractor = $this->extractorResolver->resolve($part);
34 |
35 | if (!$extractor->isExtractable($currentValue)) {
36 | return new NotFoundValue($this->path);
37 | }
38 |
39 | /** @var mixed $currentValue */
40 | $currentValue = $extractor->extract($currentValue);
41 | }
42 |
43 | return new UserValue($currentValue);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Processor/GetFromContext.php:
--------------------------------------------------------------------------------
1 | has($this->key)) {
20 | return new NotFoundValue($this->key);
21 | }
22 |
23 | return new UserValue($context->get($this->key));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Processor/Ignore.php:
--------------------------------------------------------------------------------
1 | getValue();
28 |
29 | if (!is_iterable($innerValue)) {
30 | throw new UnexpectedValueException('array|Traversable', $innerValue);
31 | }
32 |
33 | return new UserValue($this->map($context, $innerValue));
34 | }
35 |
36 | private function map(ContextInterface $context, iterable $iterator): Generator
37 | {
38 | /**
39 | * @var array-key $key
40 | * @var mixed $item
41 | */
42 | foreach ($iterator as $key => $item) {
43 | $processed = $this->processor->process($context, new UserValue($item));
44 |
45 | if ($processed instanceof IgnoreValue) {
46 | continue;
47 | }
48 |
49 | if ($processed instanceof ExceptionValueInterface) {
50 | throw $processed->getException();
51 | }
52 |
53 | if ($processed instanceof UserValue) {
54 | if ($this->keepKeys) {
55 | yield $key => $processed->getValue();
56 | } else {
57 | yield $processed->getValue();
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Processor/MarshalNestedArray.php:
--------------------------------------------------------------------------------
1 | fields = $fields;
25 | }
26 |
27 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
28 | {
29 | if (!$value instanceof UserValue) {
30 | return $value;
31 | }
32 |
33 | $target = [];
34 |
35 | $mapper = $this->mapperFactory->create($context, ...$this->fields);
36 | $mapper->map($value->getValue(), $target);
37 |
38 | return new UserValue($target);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Processor/MarshalNestedObject.php:
--------------------------------------------------------------------------------
1 | fields = $fields;
26 | }
27 |
28 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
29 | {
30 | if (!$value instanceof UserValue) {
31 | return $value;
32 | }
33 |
34 | $target = new stdClass();
35 |
36 | $mapper = $this->mapperFactory->create($context, ...$this->fields);
37 | $mapper->map($value->getValue(), $target);
38 |
39 | return new UserValue($target);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Processor/NotFound.php:
--------------------------------------------------------------------------------
1 | path);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Processor/Pass.php:
--------------------------------------------------------------------------------
1 | processors = $processors;
19 | }
20 |
21 | /**
22 | * @return ProcessorInterface[]
23 | */
24 | public function getProcessors(): array
25 | {
26 | return $this->processors;
27 | }
28 |
29 | public function process(ContextInterface $context, ValueInterface $value): ValueInterface
30 | {
31 | foreach ($this->processors as $processor) {
32 | $value = $processor->process($context, $value);
33 | }
34 |
35 | return $value;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Processor/Value.php:
--------------------------------------------------------------------------------
1 | value instanceof ValueInterface) {
19 | return $this->value;
20 | }
21 |
22 | return new UserValue($this->value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/ProcessorInterface.php:
--------------------------------------------------------------------------------
1 | path;
18 | }
19 |
20 | public function getException(): Throwable
21 | {
22 | return new NotFoundException($this->path);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Value/UserValue.php:
--------------------------------------------------------------------------------
1 | value;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/ValueInterface.php:
--------------------------------------------------------------------------------
1 |