diff --git a/README.md b/README.md index cb55fbc..23b5c9c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This _kind of_ works, but with several drawbacks: * One would always have to refer to the documentation (if one exists) for the "shape" of the array. This reduces both reusability and productivity. * Static code analysis and IDE auto-completion support are greatly hindered. -Now imagine instead of using an arbitrary array, we use an object with strongly-typed properties: +Now imagine instead of using an arbitrary array, we use an object with typed properties: ```php // UserCreationData.php @@ -68,6 +68,10 @@ composer require eve/dto This package requires PHP ≥7.4. +### Migrate from v1.x + +v1.x versions of this library include a strict type check—for example, assigning a string to a boolean property will throw an error. Though certainly useful, this feature doesn't belong in the scope of a DTO and has been removed from v2. You're encouraged to use a static analysis tool like [PHPStan](https://github.com/phpstan/phpstan) or [Psalm](https://psalm.dev) for the task instead. + ## Usage ### Basic Usage @@ -113,11 +117,10 @@ $data = UserCreationData::make() ]); ``` -If any of the passed properties doesn't exist in the class definition or if the types don't match (notice that a non-type public property e.g., `public $whatever` accepts all types), an exception will be thrown: +If any of the passed properties doesn't exist in the class definition, an exception will be thrown: ```php UserCreationData::make(['nope' => 'bar']); // throws "Public property $nope does not exist in class UserCreationData" -UserCreationData::make(['email' => new Foo()]); // throws 'UserCreationData::$email must be of type string, received a value of type Foo.', ``` Then we can call the `toArray()` method to transform the object into an associative array: @@ -126,7 +129,7 @@ Then we can call the `toArray()` method to transform the object into an associat $arr = $data->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow', 'age' => 30] ``` -Note that non-set properties will NOT be returned in the output array: +Note that non-set properties will NOT be included in the output array: ```php $data = UserCreationData::make(); @@ -139,26 +142,6 @@ $arr = $data->toArray(); // ['email' => 'alice@company.tld'] This is especially handy e.g., if you have a method to patch a database record, as it allows the operation to be totally flexible—you can patch all properties or only a subset of them. -### Type Annotations with DocBlock - -Instead of declaring your properties with built-in types, you can use type annotations with DockBlock. This is particularly useful if the property accepts multiple types—in fact, it's the only way to declare such in PHP<8. All type restrictions will be respected as normal: - -```php -use Carbon\Carbon; - -class NewOrderData extends \Eve\DTO\DataTransferObject -{ - /** - * @var string|Carbon - */ - public $order_date; -} - -NewOrderData::make(['order_date' => '2021-01-02 12:34:56']); // works -NewOrderData::make(['order_date' => Carbon::now()]); // works -NewOrderData::make(['order_date' => false]); // throws -``` - ### Nested DTOs Nested DTOs will be transformed into their corresponding arrays: @@ -223,10 +206,10 @@ $data->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow ## Differences from spatie/data-transfer-object -eve/dto is inspired by and shares some similarities with [spatie/data-transfer-object](https://github.com/spatie/data-transfer-object) but the two packages have certain differences, the most significant of which are as follows: +Though eve/dto is inspired by and shares some similarities with [spatie/data-transfer-object](https://github.com/spatie/data-transfer-object), the two packages have certain differences, the most significant of which are as follows: -* spatie/data-transfer-object requires all not-null properties to be supplied right from instantiation. This behavior is not always feasible or desirable (refer to the data patching example above). eve/dto opts for a much more forgiving approach, which allows a DTO to be created with any subset of properties. -* spatie/data-transfer-object uses a custom RegExp to parse the docblocks. This approach is prone to bugs and has some limitations. For example, the type-hinted class must be an FQCN (Fully Qualified Class Name) i.e. `\App\Models\Author` instead of `Author`. Meanwhile, eve/dto uses the official [ReflectionDocBlock](https://github.com/phpDocumentor/ReflectionDocBlock) and [TypeResolver](https://github.com/phpDocumentor/TypeResolver) packages from phpDocumentor to deal with docblocks and therefore doesn't have these issues. +* spatie/data-transfer-object requires all not-null properties to be supplied right from instantiation. This behavior is not always feasible or desirable (refer to the data patching example above). eve/dto opts for a much more forgiving approach, which allows a DTO to be created with any subset of properties. +* spatie/data-transfer-object can't detect or prevent you from assigning a non-existent property directly (e.g., `$userData->non_existent = 'foo'`), which is something eve/dto does to help ensure your object's integrity. * spatie/data-transfer-object implements such features as "Data Transfer Object Collection" and "Flexible Data Transfer Objects." To keep things simple and concise, eve/dto doesn't have these implementations. ## License diff --git a/composer.json b/composer.json index 8a71d84..c7bcdc2 100644 --- a/composer.json +++ b/composer.json @@ -3,9 +3,7 @@ "description": "Simplistic, flexible Data Transfer Object library", "type": "library", "require": { - "php": "^7.4|~8", - "phpdocumentor/reflection-docblock": "^5.2", - "phpdocumentor/type-resolver": "^1.4" + "php": "^7.4|~8" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/composer.lock b/composer.lock index 89b7700..df45e23 100644 --- a/composer.lock +++ b/composer.lock @@ -4,304 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cf9ade2974597cd008a0bf703a047d92", - "packages": [ - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" - }, - "time": "2020-09-03T19:13:55+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" - }, - "time": "2020-09-17T18:55:26+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.22.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-01-07T16:49:33+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" - } - ], + "content-hash": "543c333142b9f8b3638d65f1a924422e", + "packages": [], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -579,121 +283,279 @@ "php" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" + }, + "time": "2020-12-20T10:01:03+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2020-06-27T14:33:11+00:00" + }, + { + "name": "phar-io/version", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "bae7c545bef187884426f042434e561ab1ddb182" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", + "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.1.0" + }, + "time": "2021-02-23T14:00:09+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" }, - "time": "2020-12-20T10:01:03+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.1", + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "5.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2020-09-03T19:13:55+00:00" }, { - "name": "phar-io/version", - "version": "3.1.0", + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "bae7c545bef187884426f042434e561ab1ddb182" + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", - "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" } ], - "description": "Library for handling version information and constraints", + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.0" + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" }, - "time": "2021-02-23T14:00:09+00:00" + "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpspec/prophecy", @@ -2317,6 +2179,85 @@ }, "time": "2020-10-23T02:01:07+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.0", @@ -2366,6 +2307,64 @@ } ], "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], diff --git a/src/DataTransferObject.php b/src/DataTransferObject.php index bc028fc..687579b 100644 --- a/src/DataTransferObject.php +++ b/src/DataTransferObject.php @@ -2,31 +2,19 @@ namespace Eve\DTO; -use phpDocumentor\Reflection\DocBlockFactory; -use phpDocumentor\Reflection\TypeResolver; -use phpDocumentor\Reflection\Types\ContextFactory; use ReflectionProperty; abstract class DataTransferObject { private array $data = []; - private array $propertyMap = []; + private array $propertyNames = []; private array $excludedNames = []; private array $onlyNames = []; private function __construct(array $parameters = []) { foreach (static::getAssignableProperties() as $property) { - $this->propertyMap[$property->getName()] = [ - 'property' => $property, - 'validator' => new TypeValidator( - DocBlockFactory::createInstance(), - new TypeResolver(), - (new ContextFactory())->createFromReflector(ReflectionResolver::resolve(static::class)), - $property - ), - ]; - + $this->propertyNames[] = $property->getName(); unset($this->{$property->getName()}); } @@ -136,7 +124,7 @@ public function compact(): self private function assertPropertyExists(string $name): void { - if (!array_key_exists($name, $this->propertyMap)) { + if (!in_array($name, $this->propertyNames, true)) { throw DataTransferObjectException::nonexistentProperty(static::class, $name); } } @@ -144,8 +132,6 @@ private function assertPropertyExists(string $name): void public function __set($name, $value): void { $this->assertPropertyExists($name); - - $this->propertyMap[$name]['validator']->validate($value); $this->data[$name] = $value; } diff --git a/src/TypeValidator.php b/src/TypeValidator.php deleted file mode 100644 index 84281c7..0000000 --- a/src/TypeValidator.php +++ /dev/null @@ -1,120 +0,0 @@ - 'integer', - 'bool' => 'boolean', - 'float' => 'double', - ]; - - public function __construct( - DocBlockFactory $docBlockFactory, - TypeResolver $typeResolver, - Context $context, - ReflectionProperty $property - ) { - $this->docBlockFactory = $docBlockFactory; - $this->typeResolver = $typeResolver; - $this->context = $context; - $this->property = $property; - - $this->allowedTypes = array_unique( - array_merge( - $this->getNativeTypes($this->property->getType()), - $this->getDocBlockTypes() - ) - ); - } - - public function validate($value): void - { - if (!$this->allowedTypes) { - // an empty array of allowed types means all types are allowed - return; - } - - $actualType = gettype($value); - - foreach ($this->allowedTypes as $type) { - if (strtolower($actualType) === strtolower($type)) { - return; - } - - if (array_key_exists($type, self::TYPE_ALIASES) && self::TYPE_ALIASES[$type] === $actualType) { - return; - } - - if ($value instanceof $type) { - return; - } - } - - throw DataTransferObjectException::invalidType( - $this->property, - $actualType === 'object' ? get_class($value) : $actualType, - $this->allowedTypes - ); - } - - /** @return array */ - private function getNativeTypes(?ReflectionType $type): array - { - if (!$type) { - return []; - } - - if (method_exists($type, 'getName')) { - return $type->allowsNull() ? ['NULL', $type->getName()] : [$type->getName()]; - } - - // @see https://www.php.net/manual/en/reflectionuniontype.gettypes.php - if (method_exists($type, 'getTypes')) { - $typeNames = []; - - foreach ($type->getTypes() as $subType) { - $typeNames = array_merge($typeNames, $this->getNativeTypes($subType)); - } - - return $typeNames; - } - - return []; - } - - /** @return array */ - private function getDocBlockTypes(): array - { - if (!$this->property->getDocComment()) { - return []; - } - - $types = []; - - $docBlock = $this->docBlockFactory->create($this->property); - - /** @var Var_ $tag */ - foreach ($docBlock->getTagsByName('var') as $tag) { - foreach ($tag->getType() as $type) { - $types[] = (string) $this->typeResolver->resolve(ltrim($type, '\\'), $this->context); - } - } - - return $types; - } -} diff --git a/tests/Fixtures/SampleData.php b/tests/Fixtures/SampleData.php index 0a9e84c..5711c35 100644 --- a/tests/Fixtures/SampleData.php +++ b/tests/Fixtures/SampleData.php @@ -8,23 +8,8 @@ class SampleData extends DataTransferObject { public string $simple_prop; public ?string $nullable_prop; + public $mixed_prop; public array $array_prop; public Foo $object_prop; - - public $mixed_prop; - - public int $alias_prop_int; - public bool $alias_prop_bool; - public float $alias_prop_float; - - /** @var Foo|string */ - public string $union_prop; - - /** @var string|null */ - public $nullable_doctype_prop; - - /** @var Boolean|Int|String */ - public $mixed_case_type_prop; - public NestedData $nested; } diff --git a/tests/Fixtures/SampleData8.php b/tests/Fixtures/SampleData8.php deleted file mode 100644 index 473b37a..0000000 --- a/tests/Fixtures/SampleData8.php +++ /dev/null @@ -1,10 +0,0 @@ - 'Bob']); - self::assertSame(['compound_property' => 'Bob'], $data->toArray()); - - $data = SampleData8::make(['compound_property' => 10]); - self::assertSame(['compound_property' => 10], $data->toArray()); - - self::expectException(DataTransferObjectException::class); - SampleData8::make(['compound_property' => false]); - } -} diff --git a/tests/Unit/DataTransferObjectTest.php b/tests/Unit/DataTransferObjectTest.php index 01fcc6f..5eec429 100644 --- a/tests/Unit/DataTransferObjectTest.php +++ b/tests/Unit/DataTransferObjectTest.php @@ -39,97 +39,6 @@ public function testObjectProperty(): void self::assertEquals(['object_prop' => $foo], $data->toArray()); } - /** @return array */ - public function provideMixedTypedProperty(): array - { - return [ - [null], - ['foo'], - [true], - [new Foo()], - ]; - } - - /** @dataProvider provideMixedTypedProperty */ - public function testMixedPropertyCanBeAnything($value): void - { - $data = SampleData::make(); - $data->mixed_prop = $value; - - self::assertEquals(['mixed_prop' => $value], $data->toArray()); - } - - /** @return array */ - public function provideAliasedTypedProperty(): array - { - return [ - 'int/integer' => ['int', 10], - 'bool/boolean' => ['bool', true], - 'float/double' => ['float', 10.0], - ]; - } - - /** @dataProvider provideAliasedTypedProperty */ - public function testAliasTypedProperty(string $type, $value): void - { - $propName = "alias_prop_$type"; - $data = SampleData::make(); - $data->{$propName} = $value; - - self::assertEquals([$propName => $value], $data->toArray()); - } - - /** @return array */ - public function provideUnionTypedProperty(): array - { - return [ - 'string' => ['foo'], - 'Foo object' => [new Foo()], - ]; - } - - /** @dataProvider provideUnionTypedProperty */ - public function testUnionProperty($value): void - { - $data = SampleData::make(); - $data->union_prop = $value; - - self::assertEquals(['union_prop' => $value], $data->toArray()); - } - - public function testNullableTypeViaDocBlockProperty(): void - { - $data = SampleData::make(['nullable_doctype_prop' => null]); - - self::assertEquals(['nullable_doctype_prop' => null], $data->toArray()); - } - - /** @return array */ - public function provideMixedCaseTypeValues(): array - { - return [ - [false], - [100], - ['foo'], - ]; - } - - /** @dataProvider provideMixedCaseTypeValues */ - public function testMixedCaseTypeDocBlockProperty($value): void - { - $data = SampleData::make(['mixed_case_type_prop' => $value]); - - self::assertEquals(['mixed_case_type_prop' => $value], $data->toArray()); - } - - public function testNativeNullablePropertyAcceptsNull(): void - { - $data = SampleData::make(); - $data->nullable_prop = null; - - self::assertEquals(['nullable_prop' => null], $data->toArray()); - } - public function testSettingNonExistentPropertyThrows(): void { self::expectException(DataTransferObjectException::class); @@ -138,40 +47,6 @@ public function testSettingNonExistentPropertyThrows(): void SampleData::make(['nope' => 'bar']); } - /** @return array */ - public function provideInvalidType(): array - { - return [ - [null, 'NULL'], - [1, 'integer'], - [true, 'boolean'], - [1.0, 'double'], - [new Foo(), Foo::class], - ]; - } - - /** @dataProvider provideInvalidType */ - public function testSettingInvalidTypeThrows($value, string $type): void - { - self::expectException(DataTransferObjectException::class); - self::expectExceptionMessage( - sprintf( - 'Tests\Fixtures\SampleData::$simple_prop must be of type string, received a value of type %s.', - $type - ) - ); - - SampleData::make(['simple_prop' => $value]); - } - - public function testSettingInvalidTypeOnUnionTypeThrows(): void - { - self::expectException(DataTransferObjectException::class); - self::expectExceptionMessage('Tests\Fixtures\SampleData::$union_prop must be one of these types: string, \Tests\Fixtures\Foo; received a value of type boolean.'); //@phpcs-ignore - - SampleData::make(['union_prop' => false]); - } - public function testSet(): void { $data = SampleData::make();