├── 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 | AutoMapper 8 | 9 |

10 | 11 |

12 | build 13 | coverage 14 | packagist 15 | dependencies 16 | MIT 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 |