├── .github ├── FUNDING.yml └── workflows │ ├── style.yml │ ├── update-changelog.yml │ └── test.yml ├── src ├── Caster.php ├── Attributes │ ├── Strict.php │ ├── MapTo.php │ ├── MapFrom.php │ ├── CastWith.php │ └── DefaultCast.php ├── Validator.php ├── Exceptions │ ├── UnknownProperties.php │ ├── InvalidCasterClass.php │ └── ValidationException.php ├── Casters │ ├── DataTransferObjectCaster.php │ ├── EnumCaster.php │ └── ArrayCaster.php ├── Validation │ └── ValidationResult.php ├── Reflection │ ├── DataTransferObjectClass.php │ └── DataTransferObjectProperty.php ├── Arr.php └── DataTransferObject.php ├── LICENSE.md ├── composer.json ├── .php-cs-fixer.dist.php ├── CHANGELOG.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: spatie 2 | custom: https://spatie.be/open-source/support-us 3 | -------------------------------------------------------------------------------- /src/Caster.php: -------------------------------------------------------------------------------- 1 | casterClass, Caster::class)) { 19 | throw new InvalidCasterClass($this->casterClass); 20 | } 21 | 22 | $this->args = $args; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Casters/DataTransferObjectCaster.php: -------------------------------------------------------------------------------- 1 | classNames as $className) { 18 | if ($value instanceof $className) { 19 | return $value; 20 | } 21 | } 22 | 23 | return new $this->classNames[0]($value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: Check & fix styling 2 | 3 | on: [push] 4 | 5 | jobs: 6 | php-cs-fixer: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | 15 | - name: Run PHP CS Fixer 16 | uses: docker://oskarstark/php-cs-fixer-ga 17 | with: 18 | args: --config=.php-cs-fixer.dist.php --allow-risky=yes 19 | 20 | - name: Commit changes 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Fix styling 24 | -------------------------------------------------------------------------------- /src/Validation/ValidationResult.php: -------------------------------------------------------------------------------- 1 | $errorsForField) { 19 | /** @var \Spatie\DataTransferObject\Validation\ValidationResult $errorForField */ 20 | foreach ($errorsForField as $errorForField) { 21 | $messages[] = "\t - `{$className}->{$fieldName}`: {$errorForField->message}"; 22 | } 23 | } 24 | 25 | parent::__construct("Validation errors:" . PHP_EOL . implode(PHP_EOL, $messages)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Casters/EnumCaster.php: -------------------------------------------------------------------------------- 1 | enumType) { 19 | return $value; 20 | } 21 | 22 | if (! is_subclass_of($this->enumType, 'BackedEnum')) { 23 | throw new LogicException("Caster [EnumCaster] may only be used to cast backed enums. Received [$this->enumType]."); 24 | } 25 | 26 | $castedValue = $this->enumType::tryFrom($value); 27 | 28 | if ($castedValue === null) { 29 | throw new LogicException("Couldn't cast enum [$this->enumType] with value [$value]"); 30 | } 31 | 32 | return $castedValue; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.0, 8.1] 13 | dependency-version: [prefer-lowest, prefer-stable] 14 | 15 | name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | extensions: mbstring, sqlite, pdo_sqlite, iconv 26 | coverage: none 27 | 28 | - name: Setup problem matchers 29 | run: | 30 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 31 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 32 | 33 | - name: Install dependencies 34 | run: | 35 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 36 | 37 | - name: Execute tests 38 | run: vendor/bin/phpunit 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/data-transfer-object", 3 | "description": "Data transfer objects with batteries included", 4 | "keywords": [ 5 | "spatie", 6 | "data-transfer-object" 7 | ], 8 | "homepage": "https://github.com/spatie/data-transfer-object", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Brent Roose", 13 | "email": "brent@spatie.be", 14 | "homepage": "https://spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0" 20 | }, 21 | "require-dev": { 22 | "illuminate/collections": "^8.36", 23 | "larapack/dd": "^1.1", 24 | "phpunit/phpunit": "^9.5.5", 25 | "jetbrains/phpstorm-attributes": "^1.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Spatie\\DataTransferObject\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Spatie\\DataTransferObject\\Tests\\": "tests" 35 | } 36 | }, 37 | "scripts": { 38 | "test": "vendor/bin/phpunit", 39 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 40 | }, 41 | "config": { 42 | "sort-packages": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Attributes/DefaultCast.php: -------------------------------------------------------------------------------- 1 | getType(); 26 | 27 | /** @var \ReflectionNamedType[]|null $types */ 28 | $types = match ($type::class) { 29 | ReflectionNamedType::class => [$type], 30 | ReflectionUnionType::class => $type->getTypes(), 31 | default => null, 32 | }; 33 | 34 | if (! $types) { 35 | return false; 36 | } 37 | 38 | foreach ($types as $type) { 39 | if ($type->getName() !== $this->targetClass) { 40 | continue; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | public function resolveCaster(): Caster 50 | { 51 | return new $this->casterClass(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | notPath('docs/*') 5 | ->notPath('vendor') 6 | ->in([ 7 | __DIR__.'/src', 8 | __DIR__.'/tests', 9 | ]) 10 | ->name('*.php') 11 | ->ignoreDotFiles(true) 12 | ->ignoreVCS(true); 13 | 14 | return (new PhpCsFixer\Config()) 15 | ->setRules([ 16 | '@PSR12' => true, 17 | 'array_syntax' => ['syntax' => 'short'], 18 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 19 | 'no_unused_imports' => true, 20 | 'not_operator_with_successor_space' => true, 21 | 'trailing_comma_in_multiline' => true, 22 | 'phpdoc_scalar' => true, 23 | 'unary_operator_spaces' => true, 24 | 'binary_operator_spaces' => true, 25 | 'logical_operators' => true, 26 | 'blank_line_before_statement' => [ 27 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 28 | ], 29 | 'phpdoc_single_line_var_spacing' => true, 30 | 'phpdoc_var_without_name' => true, 31 | 'class_attributes_separation' => [ 32 | 'elements' => [ 33 | 'method' => 'one' 34 | ], 35 | ], 36 | 'method_argument_space' => [ 37 | 'on_multiline' => 'ensure_fully_multiline', 38 | 'keep_multiple_spaces_after_comma' => true, 39 | ], 40 | ]) 41 | ->setFinder($finder); 42 | -------------------------------------------------------------------------------- /src/Casters/ArrayCaster.php: -------------------------------------------------------------------------------- 1 | types as $type) { 21 | if ($type == 'array') { 22 | return $this->mapInto( 23 | destination: [], 24 | items: $value 25 | ); 26 | } 27 | 28 | if (is_subclass_of($type, ArrayAccess::class)) { 29 | return $this->mapInto( 30 | destination: new $type(), 31 | items: $value 32 | ); 33 | } 34 | } 35 | 36 | throw new LogicException( 37 | "Caster [ArrayCaster] may only be used to cast arrays or objects that implement ArrayAccess." 38 | ); 39 | } 40 | 41 | private function mapInto(array | ArrayAccess $destination, mixed $items): array | ArrayAccess 42 | { 43 | if ($destination instanceof ArrayAccess && ! is_subclass_of($destination, Traversable::class)) { 44 | throw new LogicException( 45 | "Caster [ArrayCaster] may only be used to cast ArrayAccess objects that are traversable." 46 | ); 47 | } 48 | 49 | foreach ($items as $key => $item) { 50 | $destination[$key] = $this->castItem($item); 51 | } 52 | 53 | return $destination; 54 | } 55 | 56 | private function castItem(mixed $data) 57 | { 58 | if ($data instanceof $this->itemType) { 59 | return $data; 60 | } 61 | 62 | if (is_array($data)) { 63 | return new $this->itemType(...$data); 64 | } 65 | 66 | throw new LogicException( 67 | "Caster [ArrayCaster] each item must be an array or an instance of the specified item type [{$this->itemType}]." 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Reflection/DataTransferObjectClass.php: -------------------------------------------------------------------------------- 1 | reflectionClass = new ReflectionClass($dataTransferObject); 22 | $this->dataTransferObject = $dataTransferObject; 23 | } 24 | 25 | /** 26 | * @return \Spatie\DataTransferObject\Reflection\DataTransferObjectProperty[] 27 | */ 28 | public function getProperties(): array 29 | { 30 | $publicProperties = array_filter( 31 | $this->reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC), 32 | fn (ReflectionProperty $property) => ! $property->isStatic() 33 | ); 34 | 35 | return array_map( 36 | fn (ReflectionProperty $property) => new DataTransferObjectProperty( 37 | $this->dataTransferObject, 38 | $property 39 | ), 40 | $publicProperties 41 | ); 42 | } 43 | 44 | public function validate(): void 45 | { 46 | $validationErrors = []; 47 | 48 | foreach ($this->getProperties() as $property) { 49 | $validators = $property->getValidators(); 50 | 51 | foreach ($validators as $validator) { 52 | $result = $validator->validate($property->getValue()); 53 | 54 | if ($result->isValid) { 55 | continue; 56 | } 57 | 58 | $validationErrors[$property->name][] = $result; 59 | } 60 | } 61 | 62 | if (count($validationErrors)) { 63 | throw new ValidationException($this->dataTransferObject, $validationErrors); 64 | } 65 | } 66 | 67 | public function isStrict(): bool 68 | { 69 | if (! isset($this->isStrict)) { 70 | $attribute = null; 71 | 72 | $reflectionClass = $this->reflectionClass; 73 | while ($attribute === null && $reflectionClass !== false) { 74 | $attribute = $reflectionClass->getAttributes(Strict::class)[0] ?? null; 75 | 76 | $reflectionClass = $reflectionClass->getParentClass(); 77 | } 78 | 79 | $this->isStrict = $attribute !== null; 80 | } 81 | 82 | return $this->isStrict; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Arr.php: -------------------------------------------------------------------------------- 1 | offsetExists($key); 94 | } 95 | 96 | return array_key_exists($key, $array); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/DataTransferObject.php: -------------------------------------------------------------------------------- 1 | getProperties() as $property) { 29 | $property->setValue(Arr::get($args, $property->name, $property->getDefaultValue())); 30 | 31 | $args = Arr::forget($args, $property->name); 32 | } 33 | 34 | if ($class->isStrict() && count($args)) { 35 | throw UnknownProperties::new(static::class, array_keys($args)); 36 | } 37 | 38 | $class->validate(); 39 | } 40 | 41 | public static function arrayOf(array $arrayOfParameters): array 42 | { 43 | return array_map( 44 | fn (mixed $parameters) => new static($parameters), 45 | $arrayOfParameters 46 | ); 47 | } 48 | 49 | public function all(): array 50 | { 51 | $data = []; 52 | 53 | $class = new ReflectionClass(static::class); 54 | 55 | $properties = $class->getProperties(ReflectionProperty::IS_PUBLIC); 56 | 57 | foreach ($properties as $property) { 58 | if ($property->isStatic()) { 59 | continue; 60 | } 61 | 62 | $mapToAttribute = $property->getAttributes(MapTo::class); 63 | $name = count($mapToAttribute) ? $mapToAttribute[0]->newInstance()->name : $property->getName(); 64 | 65 | $data[$name] = $property->getValue($this); 66 | } 67 | 68 | return $data; 69 | } 70 | 71 | public function only(string ...$keys): static 72 | { 73 | $dataTransferObject = clone $this; 74 | 75 | $dataTransferObject->onlyKeys = [...$this->onlyKeys, ...$keys]; 76 | 77 | return $dataTransferObject; 78 | } 79 | 80 | public function except(string ...$keys): static 81 | { 82 | $dataTransferObject = clone $this; 83 | 84 | $dataTransferObject->exceptKeys = [...$this->exceptKeys, ...$keys]; 85 | 86 | return $dataTransferObject; 87 | } 88 | 89 | public function clone(...$args): static 90 | { 91 | return new static(...array_merge($this->toArray(), $args)); 92 | } 93 | 94 | public function toArray(): array 95 | { 96 | if (count($this->onlyKeys)) { 97 | $array = Arr::only($this->all(), $this->onlyKeys); 98 | } else { 99 | $array = Arr::except($this->all(), $this->exceptKeys); 100 | } 101 | 102 | $array = $this->parseArray($array); 103 | 104 | return $array; 105 | } 106 | 107 | protected function parseArray(array $array): array 108 | { 109 | foreach ($array as $key => $value) { 110 | if ($value instanceof DataTransferObject) { 111 | $array[$key] = $value->toArray(); 112 | 113 | continue; 114 | } 115 | 116 | if (! is_array($value)) { 117 | continue; 118 | } 119 | 120 | $array[$key] = $this->parseArray($value); 121 | } 122 | 123 | return $array; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Reflection/DataTransferObjectProperty.php: -------------------------------------------------------------------------------- 1 | dataTransferObject = $dataTransferObject; 35 | $this->reflectionProperty = $reflectionProperty; 36 | 37 | $this->name = $this->resolveMappedProperty(); 38 | 39 | $this->caster = $this->resolveCaster(); 40 | } 41 | 42 | public function setValue(mixed $value): void 43 | { 44 | if ($this->caster && $value !== null) { 45 | $value = $this->caster->cast($value); 46 | } 47 | 48 | $this->reflectionProperty->setValue($this->dataTransferObject, $value); 49 | } 50 | 51 | /** 52 | * @return \Spatie\DataTransferObject\Validator[] 53 | */ 54 | public function getValidators(): array 55 | { 56 | $attributes = $this->reflectionProperty->getAttributes( 57 | Validator::class, 58 | ReflectionAttribute::IS_INSTANCEOF 59 | ); 60 | 61 | return array_map( 62 | fn (ReflectionAttribute $attribute) => $attribute->newInstance(), 63 | $attributes 64 | ); 65 | } 66 | 67 | public function getValue(): mixed 68 | { 69 | return $this->reflectionProperty->getValue($this->dataTransferObject); 70 | } 71 | 72 | public function getDefaultValue(): mixed 73 | { 74 | return $this->reflectionProperty->getDefaultValue(); 75 | } 76 | 77 | private function resolveCaster(): ?Caster 78 | { 79 | $attributes = $this->reflectionProperty->getAttributes(CastWith::class); 80 | 81 | if (! count($attributes)) { 82 | $attributes = $this->resolveCasterFromType(); 83 | } 84 | 85 | if (! count($attributes)) { 86 | return $this->resolveCasterFromDefaults(); 87 | } 88 | 89 | /** @var \Spatie\DataTransferObject\Attributes\CastWith $attribute */ 90 | $attribute = $attributes[0]->newInstance(); 91 | 92 | return new $attribute->casterClass( 93 | array_map(fn ($type) => $this->resolveTypeName($type), $this->extractTypes()), 94 | ...$attribute->args 95 | ); 96 | } 97 | 98 | private function resolveCasterFromType(): array 99 | { 100 | foreach ($this->extractTypes() as $type) { 101 | $name = $this->resolveTypeName($type); 102 | 103 | if (! class_exists($name)) { 104 | continue; 105 | } 106 | 107 | $reflectionClass = new ReflectionClass($name); 108 | 109 | do { 110 | $attributes = $reflectionClass->getAttributes(CastWith::class); 111 | 112 | $reflectionClass = $reflectionClass->getParentClass(); 113 | } while (! count($attributes) && $reflectionClass); 114 | 115 | if (count($attributes) > 0) { 116 | return $attributes; 117 | } 118 | } 119 | 120 | return []; 121 | } 122 | 123 | private function resolveCasterFromDefaults(): ?Caster 124 | { 125 | $defaultCastAttributes = []; 126 | 127 | $class = $this->reflectionProperty->getDeclaringClass(); 128 | 129 | do { 130 | array_push($defaultCastAttributes, ...$class->getAttributes(DefaultCast::class)); 131 | 132 | $class = $class->getParentClass(); 133 | } while ($class !== false); 134 | 135 | if (! count($defaultCastAttributes)) { 136 | return null; 137 | } 138 | 139 | foreach ($defaultCastAttributes as $defaultCastAttribute) { 140 | /** @var \Spatie\DataTransferObject\Attributes\DefaultCast $defaultCast */ 141 | $defaultCast = $defaultCastAttribute->newInstance(); 142 | 143 | if ($defaultCast->accepts($this->reflectionProperty)) { 144 | return $defaultCast->resolveCaster(); 145 | } 146 | } 147 | 148 | return null; 149 | } 150 | 151 | private function resolveMappedProperty(): string | int 152 | { 153 | $attributes = $this->reflectionProperty->getAttributes(MapFrom::class); 154 | 155 | if (! count($attributes)) { 156 | return $this->reflectionProperty->name; 157 | } 158 | 159 | return $attributes[0]->newInstance()->name; 160 | } 161 | 162 | /** 163 | * @return ReflectionNamedType[] 164 | */ 165 | private function extractTypes(): array 166 | { 167 | $type = $this->reflectionProperty->getType(); 168 | 169 | if (! $type) { 170 | return []; 171 | } 172 | 173 | return match ($type::class) { 174 | ReflectionNamedType::class => [$type], 175 | ReflectionUnionType::class => $type->getTypes(), 176 | }; 177 | } 178 | 179 | private function resolveTypeName(ReflectionType $type): string 180 | { 181 | return match ($type->getName()) { 182 | 'self' => $this->dataTransferObject::class, 183 | 'parent' => get_parent_class($this->dataTransferObject), 184 | default => $type->getName(), 185 | }; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `data-transfer-object` will be documented in this file 4 | 5 | ## 3.9.1 - 2022-09-16 6 | 7 | ### What's Changed 8 | 9 | - Add test for "Cannot use positional argument after named argument during unpacking" error by @Kehet in https://github.com/spatie/data-transfer-object/pull/302 10 | - Fix positional after named by @aidan-casey in https://github.com/spatie/data-transfer-object/pull/304 11 | 12 | ### New Contributors 13 | 14 | - @Kehet made their first contribution in https://github.com/spatie/data-transfer-object/pull/302 15 | 16 | **Full Changelog**: https://github.com/spatie/data-transfer-object/compare/3.9.0...3.9.1 17 | 18 | ## 3.9.0 - 2022-09-14 19 | 20 | ### What's Changed 21 | 22 | - allow enum casting not only enum value casting by @regnerisch in https://github.com/spatie/data-transfer-object/pull/301 23 | 24 | ### New Contributors 25 | 26 | - @regnerisch made their first contribution in https://github.com/spatie/data-transfer-object/pull/301 27 | 28 | **Full Changelog**: https://github.com/spatie/data-transfer-object/compare/3.8.1...3.9.0 29 | 30 | ## 3.8.1 - 2022-06-02 31 | 32 | ### What's Changed 33 | 34 | - Fixed bug #249 by @gerpo in https://github.com/spatie/data-transfer-object/pull/251 35 | 36 | **Full Changelog**: https://github.com/spatie/data-transfer-object/compare/3.8.0...3.8.1 37 | 38 | ## 3.8.0 - 2022-06-02 39 | 40 | ### What's Changed 41 | 42 | - Added enum caster by @Elnadrion in https://github.com/spatie/data-transfer-object/pull/277 43 | 44 | ### New Contributors 45 | 46 | - @Raja-Omer-Mustafa made their first contribution in https://github.com/spatie/data-transfer-object/pull/271 47 | - @Elnadrion made their first contribution in https://github.com/spatie/data-transfer-object/pull/277 48 | 49 | **Full Changelog**: https://github.com/spatie/data-transfer-object/compare/3.7.3...3.8.0 50 | 51 | ## 3.7.3 - 2022-01-10 52 | 53 | ## What's Changed 54 | 55 | - Stop suggesting phpstan/phpstan by @ymilin in https://github.com/spatie/data-transfer-object/pull/264 56 | 57 | ## New Contributors 58 | 59 | - @sergiy-petrov made their first contribution in https://github.com/spatie/data-transfer-object/pull/246 60 | - @damms005 made their first contribution in https://github.com/spatie/data-transfer-object/pull/255 61 | - @ymilin made their first contribution in https://github.com/spatie/data-transfer-object/pull/264 62 | 63 | **Full Changelog**: https://github.com/spatie/data-transfer-object/compare/3.7.2...3.7.3 64 | 65 | ## 3.7.2 - 2021-09-17 66 | 67 | - `#[Strict]` is passed down the inheritance chain so children are strict when parent is strict (#239) 68 | 69 | ## 3.7.1 - 2021-09-09 70 | 71 | - Cast properties with self or parent type (#236) 72 | 73 | ## 3.7.0 - 2021-08-26 74 | 75 | - Add `#[MapTo]` support (#233) 76 | 77 | ## 3.6.2 - 2021-08-25 78 | 79 | - Correct behavior of Arr::forget with dot keys (#231) 80 | 81 | ## 3.6.1 - 2021-08-17 82 | 83 | - Fix array assignment bug with strict dto's (#225) 84 | 85 | ## 3.6.0 - 2021-08-12 86 | 87 | - Support mapped properties (#224) 88 | 89 | ## 3.5.0 - 2021-08-11 90 | 91 | - Support union types in casters (#210) 92 | 93 | ## 3.4.0 - 2021-08-10 94 | 95 | - Fix for an empty value being created when casting `ArrayAccess` objects (#216) 96 | - Add logic exception when attempting to cast `ArrayAccess` objects that are not traversable (#216) 97 | - Allow the `ArrayCaster` to retain values that are already instances of the `itemType` (#217) 98 | 99 | ## 3.3.0 - 2021-06-01 100 | 101 | - Expose DTO and validation error array in ValidationException (#213) 102 | 103 | ## 3.2.0 - 2021-05-31 104 | 105 | - Support generic casters (#199) 106 | - Add `ArrayCaster` 107 | - Add casting of objects that implement `ArrayAccess` to the `ArrayCaster` (#206) 108 | - Fix for caster subclass check (#204) 109 | 110 | ## 3.1.1 - 2021-04-26 111 | 112 | - Make `DefaultCast` repeatable (#202) 113 | 114 | ## 3.1.0 - 2021-04-21 115 | 116 | - Add `DataTransferObject::clone(...$args)` 117 | 118 | ## 3.0.4 - 2021-04-14 119 | 120 | - Support union types (#185) 121 | - Resolve default cast from parent classes (#189) 122 | - Support default values (#191) 123 | 124 | ## 3.0.3 - 2021-04-08 125 | 126 | - Fix when nested DTO have casted field (#178) 127 | 128 | ## 3.0.2 - 2021-04-02 129 | 130 | - Allow valid DTOs to be passed to caster (#177) 131 | 132 | ## 3.0.1 - 2021-04-02 133 | 134 | - Fix for null values with casters 135 | 136 | ## 3.0.0 - 2021-04-02 137 | 138 | This package now focuses only on object creation by adding easy-to-use casting and data validation functionality. All runtime type checks are gone in favour of the improved type system in PHP 8. 139 | 140 | - Require `php:^8.0` 141 | - Removed all runtime type checking functionality, you should use typed properties and a static analysis tool like Psalm or PhpStan 142 | - Removed `Spatie\DataTransferObject\DataTransferObjectCollection` 143 | - Removed `Spatie\DataTransferObject\FlexibleDataTransferObject`, all DTOs are now considered flexible 144 | - Removed runtime immutable DTOs, you should use static analysis instead 145 | - Added `Spatie\DataTransferObject\Validator` 146 | - Added `Spatie\DataTransferObject\Validation\ValidationResult` 147 | - Added `#[DefaultCast]` 148 | - Added `#[CastWith]` 149 | - Added `Spatie\DataTransferObject\Caster` 150 | - Added `#[Strict]` 151 | 152 | ## 2.8.3 - 2021-02-12 153 | 154 | - Add support for using `collection` internally 155 | 156 | ## 2.8.2 - 2021-02-11 157 | 158 | This might be a breaking change, but it was required for a bugfix 159 | 160 | - Prevent DataTransferObjectCollection from iterating over array copy (#166) 161 | 162 | ## 2.8.1 - 2021-02-10 163 | 164 | - Fix for incorrect return type (#164) 165 | 166 | ## 2.8.0 - 2021-01-27 167 | 168 | - Allow the traversal of collections with string keys 169 | 170 | ## 2.7.0 - 2021-01-21 171 | 172 | - Cast nested collections (#117) 173 | 174 | ## 2.6.0 - 2020-11-26 175 | 176 | - Support PHP 8 177 | 178 | ## 2.5.0 - 2020-08-28 179 | 180 | - Group type errors (#130) 181 | 182 | ## 2.4.0 - 2020-08-28 183 | 184 | - Support for `array` syntax (#136) 185 | 186 | ## 2.3.0 - 2020-08-19 187 | 188 | - Add PHPStan extension to support `checkUninitializedProperties: true` (#135) 189 | 190 | ## 2.2.1 - 2020-05-13 191 | 192 | - Validator for typed 7.4 properties (#109) 193 | 194 | ## 2.2.0 - 2020-05-08 195 | 196 | - Add support for typed properties to DTO casting in PHP 7.4 197 | 198 | ## 2.0.0 - 2020-04-28 199 | 200 | - Bump minimum required PHP version to 7.4 201 | - Support for nested immutable DTOs (#86) 202 | 203 | ## 1.13.3 - 2020-01-29 204 | 205 | - Ignore static properties when serializing (#88) 206 | 207 | ## 1.13.2 - 2020-01-08 208 | 209 | - DataTransferObjectError::invalidType : get actual type before mutating $value for the error message (#81) 210 | 211 | ## 1.13.1 - 2020-01-08 212 | 213 | - Improve extendability of DTOs (#80) 214 | 215 | ## 1.13.0 - 2020-01-08 216 | 217 | - Ignore static properties (#82) 218 | - Add `DataTransferObject::arrayOf` (#83) 219 | 220 | ## 1.12.0 - 2019-12-19 221 | 222 | - Improved performance by adding a cache (#79) 223 | - Add `FlexibleDataTransferObject` which allows for unknown properties to be ignored 224 | 225 | ## 1.11.0 - 2019-11-28 (#71) 226 | 227 | - Add `iterable` and `iterable<\Type>` support 228 | 229 | ## 1.10.0 - 2019-10-16 230 | 231 | - Allow a DTO to be constructed without an array (#68) 232 | 233 | ## 1.9.1 - 2019-10-03 234 | 235 | - Improve type error message 236 | 237 | ## 1.9.0 - 2019-08-30 238 | 239 | - Add DataTransferObjectCollection::items() 240 | 241 | ## 1.8.0 - 2019-03-18 242 | 243 | - Support immutability 244 | 245 | ## 1.7.1 - 2019-02-11 246 | 247 | - Fixes #47, allowing empty dto's to be cast to using an empty array. 248 | 249 | ## 1.7.0 - 2019-02-04 250 | 251 | - Nested array DTO casting supported. 252 | 253 | ## 1.6.6 - 2018-12-04 254 | 255 | - Properly support `float`. 256 | 257 | ## 1.6.5 - 2018-11-20 258 | 259 | - Fix uninitialised error with default value. 260 | 261 | ## 1.6.4 - 2018-11-15 262 | 263 | - Don't use `allValues` anymore. 264 | 265 | ## 1.6.3 - 2018-11-14 266 | 267 | - Support nested collections in collections 268 | - Cleanup code 269 | 270 | ## 1.6.2 - 2018-11-14 271 | 272 | - Remove too much magic in nested array casting 273 | 274 | ## 1.6.1 - 2018-11-14 275 | 276 | - Support nested `toArray` in collections. 277 | 278 | ## 1.6.0 - 2018-11-14 279 | 280 | - Support nested `toArray`. 281 | 282 | ## 1.5.1 - 2018-11-07 283 | 284 | - Add strict type declarations 285 | 286 | ## 1.5.0 - 2018-11-07 287 | 288 | - Add auto casting of nested DTOs 289 | 290 | ## 1.4.0 - 2018-11-05 291 | 292 | - Rename to data-transfer-object 293 | 294 | ## 1.2.0 - 2018-10-30 295 | 296 | - Add uninitialized errors. 297 | 298 | ## 1.1.1 - 2018-10-25 299 | 300 | - Support instanceof on interfaces when type checking 301 | 302 | ## 1.1.0 - 2018-10-24 303 | 304 | - proper support for collections of value objects 305 | 306 | ## 1.0.0 - 2018-10-24 307 | 308 | - initial release 309 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning** 2 | > We [have decided](https://stitcher.io/blog/deprecating-spatie-dto) to stop maintaining this package. 3 | > 4 | > Consider migrating to [spatie/laravel-data](https://spatie.be/docs/laravel-data) or [cuyz/valinor](https://github.com/cuyz/valinor). 5 | > 6 | > Feel free to fork our code and adapt it to your needs. 7 | 8 | # Data transfer objects with batteries included 9 | 10 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/data-transfer-object.svg?style=flat-square)](https://packagist.org/packages/spatie/data-transfer-object) 11 | ![Test](https://github.com/spatie/data-transfer-object/workflows/Test/badge.svg) 12 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/data-transfer-object.svg?style=flat-square)](https://packagist.org/packages/spatie/data-transfer-object) 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require spatie/data-transfer-object 20 | ``` 21 | 22 | * **Note**: v3 of this package only supports `php:^8.0`. If you're looking for the older version, check out [v2](https://github.com/spatie/data-transfer-object/tree/v2). 23 | 24 | ## Support us 25 | 26 | [](https://spatie.be/github-ad-click/data-transfer-object) 27 | 28 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 29 | 30 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 31 | 32 | ## Usage 33 | 34 | The goal of this package is to make constructing objects from arrays of (serialized) data as easy as possible. Here's what a DTO looks like: 35 | 36 | ```php 37 | use Spatie\DataTransferObject\Attributes\MapFrom; 38 | use Spatie\DataTransferObject\DataTransferObject; 39 | 40 | class MyDTO extends DataTransferObject 41 | { 42 | public OtherDTO $otherDTO; 43 | 44 | public OtherDTOCollection $collection; 45 | 46 | #[CastWith(ComplexObjectCaster::class)] 47 | public ComplexObject $complexObject; 48 | 49 | public ComplexObjectWithCast $complexObjectWithCast; 50 | 51 | #[NumberBetween(1, 100)] 52 | public int $a; 53 | 54 | #[MapFrom('address.city')] 55 | public string $city; 56 | } 57 | ``` 58 | 59 | You could construct this DTO like so: 60 | 61 | ```php 62 | $dto = new MyDTO( 63 | a: 5, 64 | collection: [ 65 | ['id' => 1], 66 | ['id' => 2], 67 | ['id' => 3], 68 | ], 69 | complexObject: [ 70 | 'name' => 'test', 71 | ], 72 | complexObjectWithCast: [ 73 | 'name' => 'test', 74 | ], 75 | otherDTO: ['id' => 5], 76 | ); 77 | ``` 78 | 79 | Let's discuss all possibilities one by one. 80 | 81 | ## Named arguments 82 | 83 | Constructing a DTO can be done with named arguments. It's also possible to still use the old array notation. This example is equivalent to the one above. 84 | 85 | ```php 86 | $dto = new MyDTO([ 87 | 'a' => 5, 88 | 'collection' => [ 89 | ['id' => 1], 90 | ['id' => 2], 91 | ['id' => 3], 92 | ], 93 | 'complexObject' => [ 94 | 'name' => 'test', 95 | ], 96 | 'complexObjectWithCast' => [ 97 | 'name' => 'test', 98 | ], 99 | 'otherDTO' => ['id' => 5], 100 | ]); 101 | ``` 102 | 103 | ## Value casts 104 | 105 | If a DTO has a property that is another DTO or a DTO collection, the package will take care of automatically casting arrays of data to those DTOs: 106 | 107 | ```php 108 | $dto = new MyDTO( 109 | collection: [ // This will become an object of class OtherDTOCollection 110 | ['id' => 1], 111 | ['id' => 2], // Each item will be an instance of OtherDTO 112 | ['id' => 3], 113 | ], 114 | otherDTO: ['id' => 5], // This data will be cast to OtherDTO 115 | ); 116 | ``` 117 | 118 | ### Custom casters 119 | 120 | You can build your own caster classes, which will take whatever input they are given, and will cast that input to the desired result. 121 | 122 | Take a look at the `ComplexObject`: 123 | 124 | ```php 125 | class ComplexObject 126 | { 127 | public string $name; 128 | } 129 | ``` 130 | 131 | And its caster `ComplexObjectCaster`: 132 | 133 | ```php 134 | use Spatie\DataTransferObject\Caster; 135 | 136 | class ComplexObjectCaster implements Caster 137 | { 138 | /** 139 | * @param array|mixed $value 140 | * 141 | * @return mixed 142 | */ 143 | public function cast(mixed $value): ComplexObject 144 | { 145 | return new ComplexObject( 146 | name: $value['name'] 147 | ); 148 | } 149 | } 150 | ``` 151 | 152 | ### Class-specific casters 153 | 154 | Instead of specifying which caster should be used for each property, you can also define that caster on the target class itself: 155 | 156 | ```php 157 | class MyDTO extends DataTransferObject 158 | { 159 | public ComplexObjectWithCast $complexObjectWithCast; 160 | } 161 | ``` 162 | 163 | ```php 164 | #[CastWith(ComplexObjectWithCastCaster::class)] 165 | class ComplexObjectWithCast 166 | { 167 | public string $name; 168 | } 169 | ``` 170 | 171 | ### Default casters 172 | 173 | It's possible to define default casters on a DTO class itself. These casters will be used whenever a property with a given type is encountered within the DTO class. 174 | 175 | ```php 176 | #[ 177 | DefaultCast(DateTimeImmutable::class, DateTimeImmutableCaster::class), 178 | DefaultCast(MyEnum::class, EnumCaster::class), 179 | ] 180 | abstract class BaseDataTransferObject extends DataTransferObject 181 | { 182 | public MyEnum $status; // EnumCaster will be used 183 | 184 | public DateTimeImmutable $date; // DateTimeImmutableCaster will be used 185 | } 186 | ``` 187 | 188 | ### Using custom caster arguments 189 | 190 | Any caster can be passed custom arguments, the built-in [`ArrayCaster` implementation](https://github.com/spatie/data-transfer-object/blob/master/src/Casters/ArrayCaster.php) is a good example of how this may be used. 191 | 192 | Using named arguments when passing input to your caster will help make your code more clear, but they are not required. 193 | 194 | For example: 195 | 196 | ```php 197 | /** @var \Spatie\DataTransferObject\Tests\Foo[] */ 198 | #[CastWith(ArrayCaster::class, itemType: Foo::class)] 199 | public array $collectionWithNamedArguments; 200 | 201 | /** @var \Spatie\DataTransferObject\Tests\Foo[] */ 202 | #[CastWith(ArrayCaster::class, Foo::class)] 203 | public array $collectionWithoutNamedArguments; 204 | ``` 205 | 206 | Note that the first argument passed to the caster constructor is always the array with type(s) of the value being casted. 207 | All other arguments will be the ones passed as extra arguments in the `CastWith` attribute. 208 | 209 | ## Validation 210 | 211 | This package doesn't offer any specific validation functionality, but it does give you a way to build your own validation attributes. For example, `NumberBetween` is a user-implemented validation attribute: 212 | 213 | ```php 214 | class MyDTO extends DataTransferObject 215 | { 216 | #[NumberBetween(1, 100)] 217 | public int $a; 218 | } 219 | ``` 220 | 221 | It works like this under the hood: 222 | 223 | ```php 224 | #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] 225 | class NumberBetween implements Validator 226 | { 227 | public function __construct( 228 | private int $min, 229 | private int $max 230 | ) { 231 | } 232 | 233 | public function validate(mixed $value): ValidationResult 234 | { 235 | if ($value < $this->min) { 236 | return ValidationResult::invalid("Value should be greater than or equal to {$this->min}"); 237 | } 238 | 239 | if ($value > $this->max) { 240 | return ValidationResult::invalid("Value should be less than or equal to {$this->max}"); 241 | } 242 | 243 | return ValidationResult::valid(); 244 | } 245 | } 246 | ``` 247 | 248 | ## Mapping 249 | 250 | You can map a DTO property from a source property with a different name using the `#[MapFrom]` attribute. 251 | 252 | It works with a "dot" notation property name or an index. 253 | 254 | ```php 255 | class PostDTO extends DataTransferObject 256 | { 257 | #[MapFrom('postTitle')] 258 | public string $title; 259 | 260 | #[MapFrom('user.name')] 261 | public string $author; 262 | } 263 | 264 | $dto = new PostDTO([ 265 | 'postTitle' => 'Hello world', 266 | 'user' => [ 267 | 'name' => 'John Doe' 268 | ] 269 | ]); 270 | ``` 271 | 272 | ```php 273 | class UserDTO extends DataTransferObject 274 | { 275 | 276 | #[MapFrom(0)] 277 | public string $firstName; 278 | 279 | #[MapFrom(1)] 280 | public string $lastName; 281 | } 282 | 283 | $dto = new UserDTO(['John', 'Doe']); 284 | ``` 285 | 286 | Sometimes you also want to map them during the transformation to Array. 287 | A typical usecase would be transformation from camel case to snake case. 288 | For that you can use the `#[MapTo]` attribute. 289 | 290 | ```php 291 | class UserDTO extends DataTransferObject 292 | { 293 | 294 | #[MapFrom(0)] 295 | #[MapTo('first_name')] 296 | public string $firstName; 297 | 298 | #[MapFrom(1)] 299 | #[MapTo('last_name')] 300 | public string $lastName; 301 | } 302 | 303 | $dto = new UserDTO(['John', 'Doe']); 304 | $dto->toArray() // ['first_name' => 'John', 'last_name'=> 'Doe']; 305 | $dto->only('first_name')->toArray() // ['first_name' => 'John']; 306 | ``` 307 | 308 | ## Strict DTOs 309 | 310 | The previous version of this package added the `FlexibleDataTransferObject` class which allowed you to ignore properties that didn't exist on the DTO. This behaviour has been changed, all DTOs are flexible now by default, but you can make them strict by using the `#[Strict]` attribute: 311 | 312 | 313 | ```php 314 | class NonStrictDto extends DataTransferObject 315 | { 316 | public string $name; 317 | } 318 | 319 | // This works 320 | new NonStrictDto( 321 | name: 'name', 322 | unknown: 'unknown' 323 | ); 324 | ``` 325 | 326 | ```php 327 | use \Spatie\DataTransferObject\Attributes\Strict; 328 | 329 | #[Strict] 330 | class StrictDto extends DataTransferObject 331 | { 332 | public string $name; 333 | } 334 | 335 | // This throws a \Spatie\DataTransferObject\Exceptions\UnknownProperties exception 336 | new StrictDto( 337 | name: 'name', 338 | unknown: 'unknown' 339 | ); 340 | ``` 341 | 342 | ## Helper functions 343 | 344 | There are also some helper functions provided for working with multiple properties at once. 345 | 346 | ```php 347 | $postData->all(); 348 | 349 | $postData 350 | ->only('title', 'body') 351 | ->toArray(); 352 | 353 | $postData 354 | ->except('author') 355 | ->toArray(); 356 | ``` 357 | 358 | Note that `all()` will simply return all properties, while `toArray()` will cast nested DTOs to arrays as well. 359 | 360 | You can chain the `except()` and `only()` methods: 361 | 362 | ```php 363 | $postData 364 | ->except('title') 365 | ->except('body') 366 | ->toArray(); 367 | ``` 368 | 369 | It's important to note that `except()` and `only()` are immutable, they won't change the original data transfer object. 370 | 371 | ## Immutable DTOs and cloning 372 | 373 | This package doesn't force immutable objects since PHP doesn't support them, but you're always encouraged to keep your DTOs immutable. To help you, there's a `clone` method on every DTO which accepts data to override: 374 | 375 | ```php 376 | $clone = $original->clone(other: ['name' => 'a']); 377 | ``` 378 | 379 | Note that no data in `$original` is changed. 380 | 381 | ## Collections of DTOs 382 | 383 | This version removes the `DataTransferObjectCollection` class. Instead you can use simple casters and your own collection classes. 384 | 385 | Here's an example of casting a collection of DTOs to an array of DTOs: 386 | 387 | ```php 388 | class Bar extends DataTransferObject 389 | { 390 | /** @var \Spatie\DataTransferObject\Tests\Foo[] */ 391 | #[CastWith(FooArrayCaster::class)] 392 | public array $collectionOfFoo; 393 | } 394 | 395 | class Foo extends DataTransferObject 396 | { 397 | public string $name; 398 | } 399 | ``` 400 | 401 | ```php 402 | class FooArrayCaster implements Caster 403 | { 404 | public function cast(mixed $value): array 405 | { 406 | if (! is_array($value)) { 407 | throw new Exception("Can only cast arrays to Foo"); 408 | } 409 | 410 | return array_map( 411 | fn (array $data) => new Foo(...$data), 412 | $value 413 | ); 414 | } 415 | } 416 | ``` 417 | 418 | If you don't want the redundant typehint, or want extended collection functionality; you could create your own collection classes using any collection implementation. In this example, we use Laravel's: 419 | 420 | ```php 421 | class Bar extends DataTransferObject 422 | { 423 | #[CastWith(FooCollectionCaster::class)] 424 | public CollectionOfFoo $collectionOfFoo; 425 | } 426 | 427 | class Foo extends DataTransferObject 428 | { 429 | public string $name; 430 | } 431 | ``` 432 | 433 | ```php 434 | use Illuminate\Support\Collection; 435 | 436 | class CollectionOfFoo extends Collection 437 | { 438 | // Add the correct return type here for static analyzers to know which type of array this is 439 | public function offsetGet($key): Foo 440 | { 441 | return parent::offsetGet($key); 442 | } 443 | } 444 | ``` 445 | 446 | ```php 447 | class FooCollectionCaster implements Caster 448 | { 449 | public function cast(mixed $value): CollectionOfFoo 450 | { 451 | return new CollectionOfFoo(array_map( 452 | fn (array $data) => new Foo(...$data), 453 | $value 454 | )); 455 | } 456 | } 457 | ``` 458 | 459 | ## Simple arrays of DTOs 460 | 461 | For a simple array of DTOs, or an object that implements PHP's built-in `ArrayAccess`, consider using the `ArrayCaster` which requires an item type to be provided: 462 | 463 | ```php 464 | class Bar extends DataTransferObject 465 | { 466 | /** @var \Spatie\DataTransferObject\Tests\Foo[] */ 467 | #[CastWith(ArrayCaster::class, itemType: Foo::class)] 468 | public array $collectionOfFoo; 469 | } 470 | ``` 471 | 472 | ## Testing 473 | 474 | ``` bash 475 | composer test 476 | ``` 477 | 478 | ### Changelog 479 | 480 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 481 | 482 | ## Contributing 483 | 484 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 485 | 486 | ### Security 487 | 488 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 489 | 490 | ## Postcardware 491 | 492 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 493 | 494 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 495 | 496 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 497 | 498 | ## External tools 499 | 500 | - [json2dto](https://json2dto.atymic.dev): a GUI to convert JSON objects to DTO classes (with nesting support). Also provides a [CLI tool](https://github.com/atymic/json2dto#cli-tool) for local usage. 501 | - [Data Transfer Object Factory](https://github.com/anteris-dev/data-transfer-object-factory): Intelligently generates a DTO instance using the correct content for your properties based on its name and type. 502 | 503 | ## Credits 504 | 505 | - [Brent Roose](https://github.com/brendt) 506 | - [All Contributors](../../contributors) 507 | 508 | Our `Arr` class contains functions copied from Laravels `Arr` helper. 509 | 510 | ## License 511 | 512 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 513 | --------------------------------------------------------------------------------