From f7eb2c65647d5da3967c0de42b168afdff0a04c4 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 6 Mar 2024 17:11:32 +0100 Subject: [PATCH 01/28] Reimplement JsonMapper --- composer.json | 4 +- composer.lock | 491 ++---------------- json_string_compiling.php | 60 +++ src/WordPress/Blueprints/BlueprintMapper.php | 75 +-- src/WordPress/Blueprints/BlueprintParser.php | 220 +++----- .../Blueprints/Compile/BlueprintCompiler.php | 2 +- src/WordPress/Blueprints/ContainerBuilder.php | 14 - src/WordPress/Blueprints/Engine.php | 34 +- .../Blueprints/Model/BlueprintBuilder.php | 54 +- src/WordPress/JsonMapper/AnnotationMap.php | 64 +++ src/WordPress/JsonMapper/ArrayInformation.php | 61 +++ .../Evaluators/DocBlockAnnotations.php | 133 +++++ .../Evaluators/JsonEvaluatorInterface.php | 17 + .../Evaluators/NamespaceResolver.php | 161 ++++++ .../JsonMapper/Evaluators/PropertyMapper.php | 289 +++++++++++ src/WordPress/JsonMapper/Import.php | 29 ++ src/WordPress/JsonMapper/JsonMapper.php | 42 ++ .../JsonMapper/JsonMapperException.php | 7 + src/WordPress/JsonMapper/ObjectWrapper.php | 80 +++ .../JsonMapper/Property/Property.php | 64 +++ .../JsonMapper/Property/PropertyBuilder.php | 57 ++ .../JsonMapper/Property/PropertyMap.php | 78 +++ .../JsonMapper/Property/PropertyType.php | 42 ++ src/WordPress/JsonMapper/UseNodeVisitor.php | 38 ++ tests/JsonMapper/JsonMapperTest.php | 136 +++++ tests/Pest.php | 45 -- 26 files changed, 1536 insertions(+), 761 deletions(-) create mode 100644 json_string_compiling.php create mode 100644 src/WordPress/JsonMapper/AnnotationMap.php create mode 100644 src/WordPress/JsonMapper/ArrayInformation.php create mode 100644 src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php create mode 100644 src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php create mode 100644 src/WordPress/JsonMapper/Evaluators/NamespaceResolver.php create mode 100644 src/WordPress/JsonMapper/Evaluators/PropertyMapper.php create mode 100644 src/WordPress/JsonMapper/Import.php create mode 100644 src/WordPress/JsonMapper/JsonMapper.php create mode 100644 src/WordPress/JsonMapper/JsonMapperException.php create mode 100644 src/WordPress/JsonMapper/ObjectWrapper.php create mode 100644 src/WordPress/JsonMapper/Property/Property.php create mode 100644 src/WordPress/JsonMapper/Property/PropertyBuilder.php create mode 100644 src/WordPress/JsonMapper/Property/PropertyMap.php create mode 100644 src/WordPress/JsonMapper/Property/PropertyType.php create mode 100644 src/WordPress/JsonMapper/UseNodeVisitor.php create mode 100644 tests/JsonMapper/JsonMapperTest.php delete mode 100644 tests/Pest.php diff --git a/composer.json b/composer.json index 335c8bf5..90278d84 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,6 @@ { "prefer-stable": true, "require": { - "json-mapper/json-mapper": "*", "symfony/event-dispatcher": "*", "symfony/filesystem": "*", "symfony/process": "*", @@ -10,7 +9,8 @@ "pimple/pimple": "*", "psr/simple-cache": "*", "opis/json-schema": "*", - "ext-json": "*" + "ext-json": "*", + "nikic/php-parser": "v4.18.0" }, "require-dev": { "phpunit/phpunit": "*", diff --git a/composer.lock b/composer.lock index e2fb007b..d02b4b9d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,140 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b74762c3eed483809953e21464cf1760", + "content-hash": "bfa73126bb8c7534096d54d79774d82b", "packages": [ - { - "name": "json-mapper/json-mapper", - "version": "2.21.0", - "source": { - "type": "git", - "url": "https://github.com/JsonMapper/JsonMapper.git", - "reference": "df180e75e45f2d4224064eac948f6c9521262b49" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/JsonMapper/JsonMapper/zipball/df180e75e45f2d4224064eac948f6c9521262b49", - "reference": "df180e75e45f2d4224064eac948f6c9521262b49", - "shasum": "" - }, - "require": { - "ext-json": "*", - "myclabs/php-enum": "^1.7", - "nikic/php-parser": "^4.13", - "php": "^7.1 || ^8.0", - "psr/log": "^1.1 || ^2.0 || ^3.0", - "psr/simple-cache": " ^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.0 || ^6.0", - "symfony/polyfill-php73": "^1.18" - }, - "require-dev": { - "guzzlehttp/guzzle": "^6.5 || ^7.0", - "php-coveralls/php-coveralls": "^2.4", - "phpstan/phpstan": "^0.12.14", - "phpstan/phpstan-phpunit": "^0.12.17", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.5", - "symfony/console": "^2.1 || ^3.0 || ^4.0 || ^5.0", - "vimeo/psalm": "^4.10 || ^5.0" - }, - "suggest": { - "json-mapper/laravel-package": "Use JsonMapper directly with Laravel", - "json-mapper/symfony-bundle": "Use JsonMapper directly with Symfony" - }, - "type": "library", - "autoload": { - "psr-4": { - "JsonMapper\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Map JSON structures to PHP classes", - "homepage": "https://jsonmapper.net", - "keywords": [ - "json", - "jsonmapper", - "mapper", - "middleware" - ], - "support": { - "docs": "https://jsonmapper.net", - "issues": "https://github.com/JsonMapper/JsonMapper/issues", - "source": "https://github.com/JsonMapper/JsonMapper" - }, - "funding": [ - { - "url": "https://github.com/DannyvdSluijs", - "type": "github" - } - ], - "time": "2023-12-12T11:57:53+00:00" - }, - { - "name": "myclabs/php-enum", - "version": "1.8.4", - "source": { - "type": "git", - "url": "https://github.com/myclabs/php-enum.git", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483", - "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^4.6.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "MyCLabs\\Enum\\": "src/" - }, - "classmap": [ - "stubs/Stringable.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP Enum contributors", - "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" - } - ], - "description": "PHP Enum implementation", - "homepage": "http://github.com/myclabs/php-enum", - "keywords": [ - "enum" - ], - "support": { - "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.4" - }, - "funding": [ - { - "url": "https://github.com/mnapoli", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", - "type": "tidelift" - } - ], - "time": "2022-08-04T09:53:51+00:00" - }, { "name": "nikic/php-parser", "version": "v4.18.0", @@ -437,55 +305,6 @@ }, "time": "2021-10-28T11:13:42+00:00" }, - { - "name": "psr/cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/master" - }, - "time": "2016-08-06T20:24:11+00:00" - }, { "name": "psr/container", "version": "1.1.2", @@ -685,182 +504,6 @@ }, "time": "2017-10-23T01:57:42+00:00" }, - { - "name": "symfony/cache", - "version": "v5.4.36", - "source": { - "type": "git", - "url": "https://github.com/symfony/cache.git", - "reference": "a30f316214d908cf5874f700f3f3fb29ceee91ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/a30f316214d908cf5874f700f3f3fb29ceee91ba", - "reference": "a30f316214d908cf5874f700f3f3fb29ceee91ba", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0", - "psr/log": "^1.1|^2|^3", - "symfony/cache-contracts": "^1.1.7|^2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/var-exporter": "^4.4|^5.0|^6.0" - }, - "conflict": { - "doctrine/dbal": "<2.13.1", - "symfony/dependency-injection": "<4.4", - "symfony/http-kernel": "<4.4", - "symfony/var-dumper": "<4.4" - }, - "provide": { - "psr/cache-implementation": "1.0|2.0", - "psr/simple-cache-implementation": "1.0|2.0", - "symfony/cache-implementation": "1.0|2.0" - }, - "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/cache": "^1.6|^2.0", - "doctrine/dbal": "^2.13.1|^3|^4", - "predis/predis": "^1.1", - "psr/simple-cache": "^1.0|^2.0", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Cache\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", - "homepage": "https://symfony.com", - "keywords": [ - "caching", - "psr6" - ], - "support": { - "source": "https://github.com/symfony/cache/tree/v5.4.36" - }, - "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": "2024-02-19T13:08:14+00:00" - }, - { - "name": "symfony/cache-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/cache-contracts.git", - "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", - "reference": "64be4a7acb83b6f2bf6de9a02cee6dad41277ebc", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0|^3.0" - }, - "suggest": { - "symfony/cache-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Cache\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to caching", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v2.5.2" - }, - "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": "2022-01-02T09:53:40+00:00" - }, { "name": "symfony/deprecation-contracts", "version": "v2.5.2", @@ -1229,16 +872,16 @@ }, { "name": "symfony/http-client", - "version": "v5.4.36", + "version": "v5.4.37", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3e147c34ce44644f7bf7c2b8c8ecf76c0aac94b9" + "reference": "63d93fd99523b9608929a38172da3365a6c0821c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3e147c34ce44644f7bf7c2b8c8ecf76c0aac94b9", - "reference": "3e147c34ce44644f7bf7c2b8c8ecf76c0aac94b9", + "url": "https://api.github.com/repos/symfony/http-client/zipball/63d93fd99523b9608929a38172da3365a6c0821c", + "reference": "63d93fd99523b9608929a38172da3365a6c0821c", "shasum": "" }, "require": { @@ -1300,7 +943,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v5.4.36" + "source": "https://github.com/symfony/http-client/tree/v5.4.37" }, "funding": [ { @@ -1316,7 +959,7 @@ "type": "tidelift" } ], - "time": "2024-02-14T15:13:37+00:00" + "time": "2024-02-28T15:18:15+00:00" }, { "name": "symfony/http-client-contracts", @@ -1474,16 +1117,16 @@ }, { "name": "symfony/http-kernel", - "version": "v5.4.36", + "version": "v5.4.37", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "63a872e01fd70802b77023e2f5924170c99b2825" + "reference": "4ef7ed872564852b3c6c15fecf492975a52cbff3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/63a872e01fd70802b77023e2f5924170c99b2825", - "reference": "63a872e01fd70802b77023e2f5924170c99b2825", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4ef7ed872564852b3c6c15fecf492975a52cbff3", + "reference": "4ef7ed872564852b3c6c15fecf492975a52cbff3", "shasum": "" }, "require": { @@ -1566,7 +1209,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.36" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.37" }, "funding": [ { @@ -1582,7 +1225,7 @@ "type": "tidelift" } ], - "time": "2024-02-27T06:22:59+00:00" + "time": "2024-03-04T20:55:44+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2132,79 +1775,6 @@ } ], "time": "2024-02-15T11:19:14+00:00" - }, - { - "name": "symfony/var-exporter", - "version": "v5.4.35", - "source": { - "type": "git", - "url": "https://github.com/symfony/var-exporter.git", - "reference": "abb0a151b62d6b07e816487e20040464af96cae7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/abb0a151b62d6b07e816487e20040464af96cae7", - "reference": "abb0a151b62d6b07e816487e20040464af96cae7", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\VarExporter\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", - "homepage": "https://symfony.com", - "keywords": [ - "clone", - "construct", - "export", - "hydrate", - "instantiate", - "serialize" - ], - "support": { - "source": "https://github.com/symfony/var-exporter/tree/v5.4.35" - }, - "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": "2024-01-23T13:51:25+00:00" } ], "packages-dev": [ @@ -3027,20 +2597,21 @@ }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -3081,9 +2652,15 @@ "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/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -5736,16 +5313,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -5774,7 +5351,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -5782,7 +5359,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -5850,7 +5427,9 @@ "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, - "platform": [], + "platform": { + "ext-json": "*" + }, "platform-dev": [], "platform-overrides": { "php": "7.4" diff --git a/json_string_compiling.php b/json_string_compiling.php new file mode 100644 index 00000000..f493ce78 --- /dev/null +++ b/json_string_compiling.php @@ -0,0 +1,60 @@ + 'onProgress', + DoneEvent::class => 'onDone', + ); + } + + protected $progress_bar; + + public function __construct() { + ProgressBar::setFormatDefinition( 'custom', ' [%bar%] %current%/%max% -- %message%' ); + + $this->progress_bar = ( new SymfonyStyle( + new StringInput( '' ), + new ConsoleOutput() + ) )->createProgressBar( 100 ); + $this->progress_bar->setFormat( 'custom' ); + $this->progress_bar->setMessage( 'Start' ); + $this->progress_bar->start(); + } + + public function onProgress( ProgressEvent $event ) { + $this->progress_bar->setMessage( $event->caption ); + $this->progress_bar->setProgress( (int) $event->progress ); + } + + public function onDone( DoneEvent $event ) { + $this->progress_bar->finish(); + } +}; + +$results = run_blueprint( + $blueprint, + array( + 'environment' => ContainerBuilder::ENVIRONMENT_NATIVE, + 'documentRoot' => __DIR__ . '/new-wp', + 'progressSubscriber' => $subscriber, + ) +); diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index 146bc7e1..a0d6b0ea 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -2,86 +2,25 @@ namespace WordPress\Blueprints; -use InvalidArgumentException; -use JsonMapper\JsonMapperBuilder; + use WordPress\Blueprints\Model\DataClass\Blueprint; -use WordPress\Blueprints\Model\DataClass\ModelInfo; +use WordPress\JsonMapper\JsonMapper; class BlueprintMapper { - protected $mapper; public function __construct() { - $this->configureMapper(); + $this->mapper = new JsonMapper(); } - protected function configureMapper() { - $resourceMap = []; - foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resourceClass ) { - $resourceMap[ $resourceClass::DISCRIMINATOR ] = $resourceClass; - } - - $classFactoryRegistry = \JsonMapper\Handler\FactoryRegistry::WithNativePhpClassesAdded(); - $classFactoryRegistry->addFactory( - 'ResourceDefinitionInterface', - function ( $value ) use ( $resourceMap ) { - if ( is_string( $value ) ) { - return $value; - } - if ( ! isset( $value->resource ) ) { - throw new InvalidArgumentException( "Resource type must be defined" ); - } - if ( ! isset( $resourceMap[ $value->resource ] ) ) { - throw new InvalidArgumentException( "Resource type {$value->resource} is not implemented" ); - } - - return $this->mapper->mapToClass( $value, $resourceMap[ $value->resource ] ); - } - ); - - - $stepMap = []; - foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { - $stepMap[ $class::DISCRIMINATOR ] = $class; - } - $classFactoryRegistry->addFactory( - 'StepDefinitionInterface', - function ( $value ) use ( $stepMap ) { - if ( ! isset( $value->step ) ) { - throw new InvalidArgumentException( "Step must be defined" ); - } - if ( ! isset( $stepMap[ $value->step ] ) ) { - throw new InvalidArgumentException( "Step {$value->step} is not implemented" ); - } - - return $this->mapper->mapToClass( $value, $stepMap[ $value->step ] ); - } - ); - - $classFactoryRegistry->addFactory( - \ArrayObject::class, - function ( $value ) { - return new \ArrayObject( $value ); - } - ); - - $this->mapper = JsonMapperBuilder::new() - ->withPropertyMapper( new \JsonMapper\Handler\PropertyMapper( $classFactoryRegistry ) ) - ->withDocBlockAnnotationsMiddleware() - ->withTypedPropertiesMiddleware() - ->withNamespaceResolverMiddleware() - ->build(); - } /** * Maps a parsed and validated JSON object to a Blueprint class instance. * - * @param object $blueprint - * + * @param $blueprint * @return Blueprint */ - public function map( object $blueprint ): Blueprint { - return $this->mapper->mapToClass( $blueprint, Blueprint::class ); + public function map( $blueprint ) { + return $this->mapper->map_to_class( $blueprint, Blueprint::class ); } - -} +} \ No newline at end of file diff --git a/src/WordPress/Blueprints/BlueprintParser.php b/src/WordPress/Blueprints/BlueprintParser.php index d102ccb4..a40500a5 100644 --- a/src/WordPress/Blueprints/BlueprintParser.php +++ b/src/WordPress/Blueprints/BlueprintParser.php @@ -6,156 +6,76 @@ use WordPress\Blueprints\Model\BlueprintBuilder; use WordPress\Blueprints\Model\DataClass\Blueprint; -// TODO Review class -class BlueprintParser -{ - - protected BlueprintValidator $validator; - protected BlueprintMapper $mapper; - - public function __construct( - BlueprintValidator $validator, - BlueprintMapper $mapper - ) { - $this->mapper = $mapper; - $this->validator = $validator; - } - - public function parse($rawBlueprint) - { - if (is_string($rawBlueprint)) { - return $this->fromJson($rawBlueprint); - } elseif ($rawBlueprint instanceof Blueprint) { - return $this->fromBlueprint($rawBlueprint); - } elseif ($rawBlueprint instanceof BlueprintBuilder) { - return $this->fromBlueprint($rawBlueprint->toBlueprint()); - } elseif ($rawBlueprint instanceof \stdClass) { - return $this->fromObject($rawBlueprint); - } - throw new \InvalidArgumentException( - 'Unsupported $rawBlueprint type. Use a JSON string, a parsed JSON object, or a BlueprintBuilder instance.' - ); - } - - public function fromJson($json): ?Blueprint - { - return $this->fromObject(json_decode($json, false)); - } - - public function fromObject(object $data) - { - $result = $this->validator->validate($data); - if (!$result->isValid()) { - print_r((new ErrorFormatter())->format($result->error())); - die(); - } - - return $this->mapper->map($data); - } - - public function fromBlueprint(Blueprint $blueprint) - { - $result = $this->validator->validate($blueprint); - if (!$result->isValid()) { - print_r((new ErrorFormatter())->format($result->error())); -// $errorReport = [ -// "dataPointer" => $rootError->getDataPointer(), -// "schemaPointer" => $rootError->getSchemaPointer(), -// "Message" => $rootError->getMessage(), -// "Data" => $rootError->data, -// "Constraint" => $rootError->constraint, -// ]; - -// $specificError = $this->getSpecificAnyOfError( $rootError ); -// if ( $specificError ) { -// throw $specificError; -// } -// throw $rootError; - die(); - } - - return $blueprint; - } - -// /** -// * Narrows down ambiguous anyOf errors using the discriminator property. -// * -// * When one of the `anyOf` inputs doesn't match the schema, Swaggest\JsonSchema will return as many errors, -// * as there are `anyOf` options. Sometimes that means 26 errors to sieve through. For example: -// * -// * ``` -// * No valid results for oneOf { -// * 0: Enum failed, enum: ["a"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[0] -// * 1: Enum failed, enum: ["b"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[1] -// * 2: No valid results for anyOf { -// * 0: Enum failed, enum: ["c"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[0] -// * 1: Enum failed, enum: ["d"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[1] -// * 2: Enum failed, enum: ["e"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[2] -// * } at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde] -// * } at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo -// * ``` -// * -// * It's highly impractical to reason about that much output, so we narrow down the error to the specific `anyOf` option that failed. -// * -// * This function uses the discriminator property to find the specific `anyOf` option that failed. -// * For example, if the data looks like this: -// * -// * ``` -// * { -// * "steps": [ -// * { "step": "activatePlugin", "plugin": null }, -// * ] -// * } -// * ``` -// * -// * And the schema looks like this: -// * -// * ``` -// * "StepDefinition": { -// * "type": "object", -// * "discriminator": { -// * "propertyName": "step" -// * }, -// * "oneOf": [ -// * { "$ref": "#/definitions/ActivatePluginStep" }, -// * { "$ref": "#/definitions/ActivateThemeStep" }, -// * ``` -// * -// * This function will go through all the errors reported by Swaggest\JsonSchema and find the one associated -// * with the `ActivatePluginStep` definition, since its `step` property is set to `activatePlugin`. -// * -// * @param InvalidValue $e -// * -// * @return Error|void|null -// */ -// function getSpecificAnyOfError(InvalidValue $e): \Throwable|null -// { -// $subSchema = $this->getSubschema($e->getSchemaPointer()); -// -// if (property_exists($subSchema, '$ref')) { -// $discriminatedDefinition = $this->getSubschema($subSchema->{'$ref'}); -// if (property_exists($discriminatedDefinition, 'discriminator')) { -// $discriminatorField = $discriminatedDefinition->discriminator->propertyName; -// $discriminatorValue = $e->data->$discriminatorField; -// -// foreach ($discriminatedDefinition->oneOf as $discriminatorOption) { -// if (property_exists($discriminatorOption, '$ref')) { -// $optionDefinition = $this->getSubschema($discriminatorOption->{'$ref'}); -// $optionDiscriminatorValue = $optionDefinition->properties->{$discriminatorField}->const; -// if ($optionDiscriminatorValue === $discriminatorValue) { -// return $this->findSubErrorForSpecificAnyOfOption( -// $e->inspect(), -// $discriminatorOption->{'$ref'} -// ); -// } -// } -// } -// } -// } -// } -// - -// TODO Review logic in this method (might've been corrupted during downgrade) +class BlueprintParser { + + /** + * @var BlueprintValidator + */ + protected $validator; + + /** + * @var BlueprintMapper + */ + protected $mapper; + + public function __construct( + BlueprintValidator $validator, + BlueprintMapper $mapper + ) { + $this->validator = $validator; + $this->mapper = $mapper; + } + + function remove_utf8_bom( $text ) { + $bom = pack( 'H*', 'EFBBBF' ); + $text = preg_replace( "/^$bom/", '', $text ); + return $text; + } + + public function parse( $rawBlueprint ) { + if ( $rawBlueprint instanceof \stdClass ) { + return $this->fromObject( $rawBlueprint ); + } + if ( is_string( $rawBlueprint ) ) { + $data = json_decode( $rawBlueprint, false); + + if ( null === $data ) { + throw new InvalidArgumentException( 'Malformed JSON.' ); + } + + return $this->fromObject( $data ); + } + + if ( $rawBlueprint instanceof Blueprint ) { + return $this->fromBlueprint( $rawBlueprint ); + } + + if ( $rawBlueprint instanceof BlueprintBuilder ) { + return $this->fromBlueprint( $rawBlueprint->toBlueprint() ); + } + + throw new InvalidArgumentException( + 'Unsupported $rawBlueprint type. Use a JSON string, a parsed JSON object, or a BlueprintBuilder instance.' + ); + } + + public function fromObject( object $data ) { + $result = $this->validator->validate( $data ); + if ( ! $result->isValid() ) { + print_r( ( new ErrorFormatter() )->format( $result->error() ) ); + die(); + } + return $this->mapper->map( $data ); + } + + public function fromBlueprint( Blueprint $blueprint ) { + $result = $this->validator->validate( $blueprint ); + if ( ! $result->isValid() ) { + print_r( ( new ErrorFormatter() )->format( $result->error() ) ); + die(); + } + return $blueprint; + } // private function findSubErrorForSpecificAnyOfOption(Error $e, string $anyOfRef) // { diff --git a/src/WordPress/Blueprints/Compile/BlueprintCompiler.php b/src/WordPress/Blueprints/Compile/BlueprintCompiler.php index 84115b87..4e4a6df0 100644 --- a/src/WordPress/Blueprints/Compile/BlueprintCompiler.php +++ b/src/WordPress/Blueprints/Compile/BlueprintCompiler.php @@ -18,7 +18,7 @@ class BlueprintCompiler { protected $stepRunnerFactory; - protected ResourceResolverInterface $resourceResolver; + protected $resourceResolver; public function __construct( $stepRunnerFactory, diff --git a/src/WordPress/Blueprints/ContainerBuilder.php b/src/WordPress/Blueprints/ContainerBuilder.php index dcdf01f3..5fe51199 100644 --- a/src/WordPress/Blueprints/ContainerBuilder.php +++ b/src/WordPress/Blueprints/ContainerBuilder.php @@ -279,20 +279,6 @@ function ( $c ) { return new PlaygroundFetchSource(); }; - // Add a progress listener to all data sources -// foreach ( $container->keys() as $key ) { -// if ( str_starts_with( $key, 'data_source.' ) ) { -// $container->extend( $key, function ( $urlSource, $c ) { -// $urlSource->events->addListener( -// ProgressEvent::class, -// $c['progress_reporter'] -// ); -// -// return $urlSource; -// } ); -// } -// } - return $container; } } diff --git a/src/WordPress/Blueprints/Engine.php b/src/WordPress/Blueprints/Engine.php index c1f2af29..9f12130d 100644 --- a/src/WordPress/Blueprints/Engine.php +++ b/src/WordPress/Blueprints/Engine.php @@ -2,28 +2,44 @@ namespace WordPress\Blueprints; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; use WordPress\Blueprints\Compile\BlueprintCompiler; use WordPress\Blueprints\Compile\CompiledBlueprint; use WordPress\Blueprints\Runner\Blueprint\BlueprintRunner; class Engine { + /** + * @var BlueprintRunner + */ + public $runner; + + /** + * @var BlueprintParser + */ + protected $parser; + + /** + * @var BlueprintCompiler + */ + protected $compiler; + public function __construct( - protected BlueprintParser $parser, - protected BlueprintCompiler $compiler, - public readonly BlueprintRunner $runner + BlueprintParser $parser, + BlueprintCompiler $compiler, + BlueprintRunner $runner ) { + $this->runner = $runner; + $this->compiler = $compiler; + $this->parser = $parser; } - public function parseAndCompile( string|object $rawBlueprint ) { - $blueprint = $this->parser->parse( $rawBlueprint ); + public function parseAndCompile( $raw_blueprint ) { + $blueprint = $this->parser->parse( $raw_blueprint ); return $this->compiler->compile( $blueprint ); } - public function run( CompiledBlueprint $compiledBlueprint ) { - return $this->runner->run( $compiledBlueprint ); + public function run( CompiledBlueprint $compiled_blueprint ) { + return $this->runner->run( $compiled_blueprint ); } - } diff --git a/src/WordPress/Blueprints/Model/BlueprintBuilder.php b/src/WordPress/Blueprints/Model/BlueprintBuilder.php index 0253277d..70c93728 100644 --- a/src/WordPress/Blueprints/Model/BlueprintBuilder.php +++ b/src/WordPress/Blueprints/Model/BlueprintBuilder.php @@ -10,7 +10,9 @@ use WordPress\Blueprints\Model\DataClass\InstallPluginStep; use WordPress\Blueprints\Model\DataClass\InstallSqliteIntegrationStep; use WordPress\Blueprints\Model\DataClass\InstallThemeStep; +use WordPress\Blueprints\Model\DataClass\MkdirStep; use WordPress\Blueprints\Model\DataClass\ResourceDefinitionInterface; +use WordPress\Blueprints\Model\DataClass\RmStep; use WordPress\Blueprints\Model\DataClass\RunSQLStep; use WordPress\Blueprints\Model\DataClass\RunWordPressInstallerStep; use WordPress\Blueprints\Model\DataClass\SetSiteOptionsStep; @@ -22,13 +24,16 @@ class BlueprintBuilder { - private Blueprint $blueprint; + /** + * @var Blueprint + */ + private $blueprint; public function __construct() { $this->blueprint = new Blueprint(); } - static public function create() { + public static function create() { return new static(); } @@ -63,7 +68,7 @@ public function withPlugins( $pluginZips ) { } public function withPlugin( $pluginZip ) { - $this->withPlugins( [ $pluginZip ] ); + $this->withPlugins( array( $pluginZip ) ); return $this; } @@ -85,7 +90,7 @@ public function withTheme( $themeZip ) { public function withContent( $wxrs ) { if ( ! is_array( $wxrs ) ) { - $wxrs = [ $wxrs ]; + $wxrs = array( $wxrs ); } // @TODO: Should this automatically add the importer plugin if it's not already installed? foreach ( $wxrs as $wxr ) { @@ -123,19 +128,34 @@ public function withFiles( $files ) { public function withFile( $path, $data ) { return $this->addStep( ( new WriteFileStep() ) - ->setPath( 'wordpress.txt' ) + ->setPath( 'WordPress.txt' ) ->setData( $data ) ); } - public function downloadWordPress( string|ResourceDefinitionInterface $wpZip = null ) { - $this->prependStep( ( new DownloadWordPressStep() ) + public function remove( $path ) { + return $this->addStep( + ( new RmStep() ) + ->setPath( $path ) + ); + } + + public function makeDirectory( $path ) { + return $this->addStep( + ( new MkdirStep() ) + ->setPath( $path ) + ); + } + + public function downloadWordPress( $wpZip = null ) { + $this->prependStep( + ( new DownloadWordPressStep() ) ->setWordPressZip( $wpZip ?? 'https://wordpress.org/latest.zip' - ) ); + ) + ); return $this; - } public function runInstallationWizard() { @@ -145,21 +165,24 @@ public function runInstallationWizard() { } public function useSqlite( - string|ResourceDefinitionInterface $sqlitePluginSource = 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip' + $sqlitePluginSource = 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip' ) { - $this->addStep( ( new InstallSqliteIntegrationStep() ) + $this->addStep( + ( new InstallSqliteIntegrationStep() ) ->setSqlitePluginZip( $sqlitePluginSource - ) ); + ) + ); return $this; - } public function downloadWpCli() { - return $this->addStep( ( new WriteFileStep() ) + return $this->addStep( + ( new WriteFileStep() ) ->setPath( 'wp-cli.phar' ) - ->setData( ( new UrlResource() )->setUrl( 'https://playground.wordpress.net/wp-cli.phar' ) ) ); + ->setData( ( new UrlResource() )->setUrl( 'https://playground.wordpress.net/wp-cli.phar' ) ) + ); } private function prependStep( StepDefinitionInterface $builder ) { @@ -174,4 +197,3 @@ public function addStep( StepDefinitionInterface $builder ) { return $this; } } - diff --git a/src/WordPress/JsonMapper/AnnotationMap.php b/src/WordPress/JsonMapper/AnnotationMap.php new file mode 100644 index 00000000..18bf8c75 --- /dev/null +++ b/src/WordPress/JsonMapper/AnnotationMap.php @@ -0,0 +1,64 @@ +var = $var; + $this->params = $params; + $this->return = $return; + } + + public function hasVar(): bool + { + return ! \is_null($this->var); + } + + public function getVar(): string + { + if (\is_null($this->var)) { + throw new \Exception('Annotation map doesnt contain valid value for var'); + } + return $this->var; + } + + public function getParams(): array + { + return $this->params; + } + + public function hasParam(string $paramName): bool + { + return array_key_exists($paramName, $this->params); + } + + public function getParam(string $paramName): string + { + if (!$this->hasParam($paramName)) { + throw new \Exception("Annotation map doesnt contain param with name $paramName"); + } + return $this->params[$paramName]; + } + + public function hasReturn(): bool + { + return ! \is_null($this->return); + } + + public function getReturn(): string + { + if (\is_null($this->return)) { + throw new \Exception('Annotation map doesnt contain valid value for return'); + } + return $this->return; + } +} \ No newline at end of file diff --git a/src/WordPress/JsonMapper/ArrayInformation.php b/src/WordPress/JsonMapper/ArrayInformation.php new file mode 100644 index 00000000..e2254f5e --- /dev/null +++ b/src/WordPress/JsonMapper/ArrayInformation.php @@ -0,0 +1,61 @@ +isArray = $isArray; + $this->dimensions = $dimensions; + } + + public static function notAnArray(): self + { + return new self(false, 0); + } + + public static function singleDimension(): self + { + return new self(true, 1); + } + + public static function multiDimension(int $dimension): self + { + return new self(true, $dimension); + } + + public function isArray(): bool + { + return $this->isArray; + } + + public function getDimensions(): int + { + return $this->dimensions; + } + + public function isMultiDimensionalArray(): bool + { + return $this->isArray && $this->dimensions > 1; + } + + public function jsonSerialize(): array + { + return [ + 'isArray' => $this->isArray, + 'dimensions' => $this->dimensions + ]; + } + + public function equals(self $other): bool + { + return $this->isArray === $other->isArray + && $this->dimensions === $other->dimensions; + } +} diff --git a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php new file mode 100644 index 00000000..fae265f7 --- /dev/null +++ b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php @@ -0,0 +1,133 @@ +[A-Za-z_-]+)[ \t]+(?P[\w\[\]\\\\|]*).*$/m'; + + public function __construct() {} + + public function evaluate( + \stdClass $json, + ObjectWrapper $object_wrapper, + PropertyMap $property_map, + JsonMapper $mapper) { + $property_map->merge( $this->fetchPropertyMapForObject( $object_wrapper ) ); + } + + private function fetchPropertyMapForObject( ObjectWrapper $object ): PropertyMap { + $intermediatePropertyMap = new PropertyMap(); + foreach ( $this->getObjectPropertiesIncludingParents( $object ) as $property ) { + $name = $property->getName(); + $docBlock = $property->getDocComment(); + if ( $docBlock === false ) { + continue; + } + + $annotations = self::parseDocBlockToAnnotationMap( $docBlock ); + + if ( ! $annotations->hasVar() ) { + continue; + } + + $types = \explode( '|', $annotations->getVar() ); + $nullable = \in_array( 'null', $types, true ); + $types = \array_filter( + $types, + static function ( string $type ) { + return $type !== 'null'; + } + ); + + $builder = PropertyBuilder::new() + ->setName( $name ) + ->setIsNullable( $nullable ) + ->setVisibility( $this->fromReflectionProperty( $property ) ); + + /* A union type that has one of its types defined as array is to complex to understand */ + if ( \in_array( 'array', $types, true ) ) { + $property = $builder->addType( 'mixed', ArrayInformation::singleDimension() )->build(); + $intermediatePropertyMap->addProperty( $property ); + continue; + } + + foreach ( $types as $type ) { + $type = \trim( $type ); + $isAnArrayType = \substr( $type, -2 ) === '[]'; + + if ( ! $isAnArrayType ) { + $builder->addType( $type, ArrayInformation::notAnArray() ); + continue; + } + + $initialBracketPosition = strpos( $type, '[' ); + $dimensions = substr_count( $type, '[]' ); + + if ( $initialBracketPosition !== false ) { + $type = substr( $type, 0, $initialBracketPosition ); + } + + $builder->addType( $type, ArrayInformation::multiDimension( $dimensions ) ); + } + + $property = $builder->build(); + $intermediatePropertyMap->addProperty( $property ); + } + + return $intermediatePropertyMap; + } + + private function fromReflectionProperty( ReflectionProperty $property ): string { + + if ( $property->isPublic() ) { + return 'public'; + } + if ( $property->isProtected() ) { + return 'protected'; + } + return 'private'; + } + + public static function parseDocBlockToAnnotationMap( string $docBlock ): AnnotationMap { + // Strip away the start "/**' and ending "*/" + if ( strpos( $docBlock, '/**' ) === 0 ) { + $docBlock = \substr( $docBlock, 3 ); + } + if ( substr( $docBlock, -2 ) === '*/' ) { + $docBlock = \substr( $docBlock, 0, -2 ); + } + $docBlock = \trim( $docBlock ); + + $var = null; + if ( \preg_match_all( self::DOC_BLOCK_REGEX, $docBlock, $matches ) ) { + for ( $x = 0, $max = count( $matches[0] ); $x < $max; $x++ ) { + if ( $matches['name'][ $x ] === 'var' ) { + $var = $matches['value'][ $x ]; + } + } + } + + return new AnnotationMap( $var ?: null, array(), null ); + } + + /** @return \ReflectionProperty[] */ + public function getObjectPropertiesIncludingParents( ObjectWrapper $object ): array { + $properties = array(); + $reflectionClass = $object->getReflectedObject(); + do { + $properties = array_merge( $properties, $reflectionClass->getProperties() ); + } while ( $reflectionClass = $reflectionClass->getParentClass() ); + return $properties; + } +} diff --git a/src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php b/src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php new file mode 100644 index 00000000..58ea6bad --- /dev/null +++ b/src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php @@ -0,0 +1,17 @@ +fetchPropertyMapForObject( $object_wrapper, $property_map ) as $property ) { + $property_map->addProperty( $property ); + } + } + + private function fetchPropertyMapForObject( ObjectWrapper $object, PropertyMap $originalPropertyMap ): PropertyMap { + $intermediatePropertyMap = new PropertyMap(); + $imports = self::getImports( $object->getReflectedObject() ); + + /** @var Property $property */ + foreach ( $originalPropertyMap as $property ) { + $types = $property->get_property_types(); + foreach ( $types as $index => $type ) { + $types[ $index ] = $this->resolveSingleType( $type, $object, $imports ); + } + $intermediatePropertyMap->addProperty( $property->as_builder()->setTypes( ...$types )->build() ); + } + + return $intermediatePropertyMap; + } + + /** @return Import[] */ + private static function getImports( \ReflectionClass $class ): array { + if ( ! $class->isUserDefined() ) { + return array(); + } + + $filename = $class->getFileName(); + if ( $filename === false || \substr( $filename, -13 ) === "eval()'d code" ) { + throw new \RuntimeException( "Class {$class->getName()} has no filename available" ); + } + + if ( $class->getParentClass() === false ) { + return self::getImportsForFileName( $filename ); + } + + return array_unique( + array_merge( self::getImportsForFileName( $filename ), self::getImports( $class->getParentClass() ) ), + SORT_REGULAR + ); + } + + /** @return Import[] */ + private static function getImportsForFileName( string $filename ): array { + if ( ! \is_readable( $filename ) ) { + throw new \RuntimeException( "Unable to read {$filename}" ); + } + + $contents = \file_get_contents( $filename ); + if ( $contents === false ) { + throw new \RuntimeException( "Unable to read {$filename}" ); + } + + $parser = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 ); + + try { + $ast = $parser->parse( $contents ); + if ( \is_null( $ast ) ) { + throw new \Exception( "Failed to parse {$filename}" ); + } + } catch ( \Throwable $e ) { + throw new \Exception( "Failed to parse {$filename}" ); + } + + $traverser = new NodeTraverser(); + $visitor = new UseNodeVisitor(); + $traverser->addVisitor( $visitor ); + $traverser->traverse( $ast ); + + return $visitor->getImports(); + } + + + /** @param Import[] $imports */ + private function resolveSingleType( PropertyType $type, ObjectWrapper $object, array $imports ): PropertyType { + if ( $this->is_valid_scalar_type( $type ) ) { + return $type; + } + + $pos = strpos( $type->getType(), '\\' ); + if ( $pos === false ) { + $pos = strlen( $type->getType() ); + } + $nameSpacedFirstChunk = '\\' . substr( $type->getType(), 0, $pos ); + + $matches = \array_filter( + $imports, + static function ( Import $import ) use ( $nameSpacedFirstChunk ) { + if ( $import->hasAlias() && '\\' . $import->getAlias() === $nameSpacedFirstChunk ) { + return true; + } + + return $nameSpacedFirstChunk === \substr( $import->getImport(), -strlen( $nameSpacedFirstChunk ) ); + } + ); + + if ( count( $matches ) > 0 ) { + $match = \array_shift( $matches ); + if ( $match->hasAlias() ) { + $strippedType = \substr( $type->getType(), strlen( $nameSpacedFirstChunk ) ); + $fullyQualifiedType = $match->getImport() . '\\' . $strippedType; + } else { + $strippedMatch = \substr( $match->getImport(), 0, -strlen( $nameSpacedFirstChunk ) ); + $fullyQualifiedType = $strippedMatch . '\\' . $type->getType(); + } + + return new PropertyType( rtrim( $fullyQualifiedType, '\\' ), $type->getArrayInformation() ); + } + + $reflectedObject = $object->getReflectedObject(); + while ( true ) { + if ( class_exists( $reflectedObject->getNamespaceName() . '\\' . $type->getType() ) ) { + return new PropertyType( + $reflectedObject->getNamespaceName() . '\\' . $type->getType(), + $type->getArrayInformation() + ); + } + + $reflectedObject = $reflectedObject->getParentClass(); + if ( ! $reflectedObject ) { + break; + } + } + + return $type; + } + + /** + * @param PropertyType $type + * @return bool + */ + private function is_valid_scalar_type( PropertyType $type ): bool { + return in_array( $type->getType(), $this->scalar_types, true ); + } +} diff --git a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php new file mode 100644 index 00000000..f7272a7b --- /dev/null +++ b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php @@ -0,0 +1,289 @@ +mapper = $json_mapper; + $this->add_factories_for_native_php_classes(); + $this->add_factory_for_resources(); + $this->add_factory_for_steps(); + $this->add_factory_for_arrays(); + } + + public function evaluate( + \stdClass $json, + ObjectWrapper $object_wrapper, + PropertyMap $property_map, + JsonMapper $mapper + ) { + // If the type we are mapping has a last minute factory use it. + if ( $this->has_factory( $object_wrapper->getName() ) ) { + $result = $this->use_factory( $object_wrapper->getName(), $json ); + + $object_wrapper->setObject( $result ); + return; + } + + $values = (array) $json; + foreach ( $values as $key => $value ) { + if ( false === $property_map->has_property( $key ) ) { + continue; + } + + $property = $property_map->get_property( $key ); + + if ( false === $property->is_nullable() && null === $value ) { + throw new JsonMapperException( + "Null provided in json where \'{$object_wrapper->getName()}::{$key}\' doesn't allow null value" + ); + } + + if ( $property->is_nullable() && null === $value ) { + $this->set_value( $object_wrapper, $property, null ); + continue; + } + + $value = $this->map_value( $property, $value ); + $this->set_value( $object_wrapper, $property, $value ); + } + } + + private function map_value( Property $property, $value ) { + if ( null === $value && $property->is_nullable() ) { + return null; + } + // No match was found (or there was only one option) lets assume the first is the right one. + $types = $property->get_property_types(); + $type = \array_shift( $types ); + + if ( null === $type ) { + // Return the value as is as there is no type info. + return $value; + } + + if ( $this->is_valid_scalar_type( $type ) ) { + return $this->map_to_scalar( $type, $value ); + } + + if ( $this->has_factory( $type->getType() ) ) { + return $this->map_to_object_using_factory( $type, $value ); + } + + if ( ( class_exists( $type->getType() ) || interface_exists( $type->getType() ) ) ) { + return $this->map_to_object( $type, $value ); + } + + throw new JsonMapperException( "Unable to map to \'{$type->getType()}\'" ); + } + + /** + * @param PropertyType $type + * @return bool + */ + private function is_valid_scalar_type( PropertyType $type ): bool { + return in_array( $type->getType(), $this->scalar_types, true ); + } + + private function map_to_scalar( PropertyType $type, $value ) { + if ( false === is_array( $value ) ) { + return $this->cast_to_scalar_type( $type->getType(), $value ); + } + $mapped_scalars = array(); + foreach ( $value as $inner_value ) { + $mapped_scalars[] = $this->map_to_scalar( $type, $inner_value ); + } + return $mapped_scalars; + } + + private function cast_to_scalar_type( string $type, $value ) { + if ( 'string' === $type ) { + return (string) $value; + } + if ( 'boolean' === $type || 'bool' === $type ) { + return (bool) $value; + } + if ( 'integer' === $type || 'int' === $type ) { + return (int) $value; + } + if ( 'double' === $type || 'float' === $type ) { + return (float) $value; + } + + throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); + } + + private function map_to_object_using_factory( PropertyType $type, $value ) { + if ( false === is_array( $value ) ) { + return $this->use_factory( $type->getType(), $value ); + } + $mapped_objects = array(); + foreach ( $value as $inner_value ) { + $mapped_objects[] = $this->map_to_object_using_factory( $type, $inner_value ); + } + return $mapped_objects; + } + + private function map_to_object( PropertyType $type, $value ) { + if ( false === ( new ReflectionClass( $type->getType() ) )->isInstantiable() ) { + throw new JsonMapperException( "Unable to resolve uninstantiable \'{$type->getType()}\'." ); + } + if ( false === is_array( $value ) ) { + return $this->mapper->map_to_class( $value, $type->getType() ); + } + $mapped_objects = array(); + foreach ( $value as $inner_value ) { + $mapped_objects[] = $this->map_to_object( $type, $inner_value ); + } + return $mapped_objects; + } + + private function set_value( ObjectWrapper $object, Property $property, $value ) { + if ( 'public' === $property->visibility ) { + $object->getObject()->{$property->get_name()} = $value; + return; + } + + $method_name = 'set' . \ucfirst( $property->get_name() ); + if ( \method_exists( $object->getObject(), $method_name ) ) { + $method = new ReflectionMethod( $object->getObject(), $method_name ); + $parameters = $method->getParameters(); + + if ( \is_array( $value ) && \count( $parameters ) === 1 && $parameters[0]->isVariadic() ) { + $object->getObject()->$method_name( ...$value ); + return; + } + + $object->getObject()->$method_name( $value ); + return; + } + + throw new JsonMapperException( + "{$object->getName()}::{$property->get_name()} is non-public and no setter method was found" + ); + } + + private function sanitise_class_name( string $class_name ): string { + /* Erase leading slash as ::class doesn't contain leading slash */ + if ( strpos( $class_name, '\\' ) === 0 ) { + $class_name = substr( $class_name, 1 ); + } + + return $class_name; + } + + private function has_factory( string $class_name ): bool { + return array_key_exists( $this->sanitise_class_name( $class_name ), $this->factories ); + } + + private function use_factory( string $class_name, $params ) { + $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; + + return $factory( $params ); + } + + private function add_factory( string $class_name, callable $factory ) { + $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; + } + + private function add_factories_for_native_php_classes() { + $this->add_factory( + \DateTime::class, + static function ( string $value ) { + return new \DateTime( $value ); + } + ); + $this->add_factory( + \DateTimeImmutable::class, + static function ( string $value ) { + return new \DateTimeImmutable( $value ); + } + ); + $this->add_factory( + \stdClass::class, + static function ( $value ) { + return (object) $value; + } + ); + } + + private function add_factory_for_resources() { + $resourceMap = array(); + foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resourceClass ) { + $resourceMap[ $resourceClass::DISCRIMINATOR ] = $resourceClass; + } + $this->add_factory( + 'ResourceDefinitionInterface', + function ( $value ) use ( $resourceMap ) { + if ( is_string( $value ) ) { + return $value; + } + if ( ! isset( $value->resource ) ) { + throw new JsonMapperException( 'Resource type must be defined' ); + } + if ( ! isset( $resourceMap[ $value->resource ] ) ) { + throw new JsonMapperException( "Resource type {$value->resource} is not implemented" ); + } + + return $this->mapper->map_to_class( $value, $resourceMap[ $value->resource ] ); + } + ); + } + + private function add_factory_for_steps() { + $stepMap = array(); + foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { + $stepMap[ $class::DISCRIMINATOR ] = $class; + } + $this->add_factory( + 'StepDefinitionInterface', + function ( $value ) use ( $stepMap ) { + if ( ! isset( $value->step ) ) { + throw new JsonMapperException( 'Step must be defined' ); + } + if ( ! isset( $stepMap[ $value->step ] ) ) { + throw new JsonMapperException( "Step {$value->step} is not implemented" ); + } + + return $this->mapper->map_to_class( $value, $stepMap[ $value->step ] ); + } + ); + } + + private function add_factory_for_arrays() { + $this->add_factory( + \ArrayObject::class, + function ( $value ) { + return new \ArrayObject( $value ); + } + ); + } +} diff --git a/src/WordPress/JsonMapper/Import.php b/src/WordPress/JsonMapper/Import.php new file mode 100644 index 00000000..576f27f9 --- /dev/null +++ b/src/WordPress/JsonMapper/Import.php @@ -0,0 +1,29 @@ +import = $import; + $this->alias = $alias; + } + + public function getImport(): string { + return $this->import; + } + + public function getAlias(): string { + return $this->alias; + } + + public function hasAlias(): bool { + return ! \is_null( $this->alias ); + } +} diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php new file mode 100644 index 00000000..452a11ee --- /dev/null +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -0,0 +1,42 @@ +evaluators ) { + $this->evaluators = array( + new DocBlockAnnotations(), + new NamespaceResolver(), + new PropertyMapper( $this ), // PropertyMapper has to be the last evaluator. + ); + } + } + + public function map_to_class( \stdClass $json, string $class ) { + $this->configure_evaluators(); + + $object_wrapper = new ObjectWrapper( null, $class ); + $property_map = new PropertyMap(); + + foreach ( $this->evaluators as $evaluator ) { + $evaluator->evaluate( + $json, + $object_wrapper, + $property_map, + $this + ); + } + + return $object_wrapper->getObject(); + } +} diff --git a/src/WordPress/JsonMapper/JsonMapperException.php b/src/WordPress/JsonMapper/JsonMapperException.php new file mode 100644 index 00000000..f80b1350 --- /dev/null +++ b/src/WordPress/JsonMapper/JsonMapperException.php @@ -0,0 +1,7 @@ +object = $object; + $this->class_name = $class_name; + } + + /** @param object|null $object */ + public function setObject( $object ) { + $this->object = $object; + $this->reflected_object = null; + } + + /** @return object */ + public function getObject() { + if ( is_null( $this->object ) ) { + $constructor = $this->getReflectedObject()->getConstructor(); + if ( is_null( $constructor ) || $constructor->getNumberOfParameters() === 0 ) { + $this->object = $this->getReflectedObject()->newInstance(); + } else { + $this->object = $this->getReflectedObject()->newInstanceWithoutConstructor(); + } + } + + return $this->object; + } + + + /** @return class-string */ + public function getClassName(): string { + return $this->class_name; + } + + public function getReflectedObject(): ReflectionClass { + if ( $this->reflected_object === null ) { + $objectOrClass = ! \is_null( $this->object ) ? $this->object : $this->class_name; + $this->reflected_object = new ReflectionClass( $objectOrClass ); + } + + return $this->reflected_object; + } + + public function getName(): string { + return $this->getReflectedObject()->getName(); + } +} diff --git a/src/WordPress/JsonMapper/Property/Property.php b/src/WordPress/JsonMapper/Property/Property.php new file mode 100644 index 00000000..1aef3755 --- /dev/null +++ b/src/WordPress/JsonMapper/Property/Property.php @@ -0,0 +1,64 @@ +name = $name; + $this->visibility = $visibility; + $this->is_nullable = $is_nullable; + $this->property_types = $types; + } + + public function get_name(): string { + return $this->name; + } + + /** @return PropertyType[] */ + public function get_property_types(): array { + return $this->property_types; + } + + public function get_visibility(): string { + return $this->visibility; + } + + public function is_nullable(): bool { + return $this->is_nullable; + } + + public function as_builder(): PropertyBuilder { + return PropertyBuilder::new() + ->setName( $this->name ) + ->setTypes( ...$this->property_types ) + ->setIsNullable( $this->is_nullable() ) + ->setVisibility( $this->visibility ); + } + + // phpcs:ignore + public function jsonSerialize(): array { + return array( + 'name' => $this->name, + 'types' => $this->property_types, + 'visibility' => $this->visibility, + 'isNullable' => $this->is_nullable, + ); + } +} diff --git a/src/WordPress/JsonMapper/Property/PropertyBuilder.php b/src/WordPress/JsonMapper/Property/PropertyBuilder.php new file mode 100644 index 00000000..9d004263 --- /dev/null +++ b/src/WordPress/JsonMapper/Property/PropertyBuilder.php @@ -0,0 +1,57 @@ +name, + $this->visibility, + $this->isNullable, + ...$this->types + ); + } + + public function setName( string $name ): self { + $this->name = $name; + return $this; + } + + public function setTypes( PropertyType ...$types ): self { + $this->types = $types; + return $this; + } + + public function addType( string $type, ArrayInformation $arrayInformation ): self { + $this->types[] = new PropertyType( $type, $arrayInformation ); + return $this; + } + + public function setIsNullable( bool $isNullable ): self { + $this->isNullable = $isNullable; + return $this; + } + + public function setVisibility( string $visibility ): self { + $this->visibility = $visibility; + return $this; + } +} diff --git a/src/WordPress/JsonMapper/Property/PropertyMap.php b/src/WordPress/JsonMapper/Property/PropertyMap.php new file mode 100644 index 00000000..2d8040d5 --- /dev/null +++ b/src/WordPress/JsonMapper/Property/PropertyMap.php @@ -0,0 +1,78 @@ +map[ $property->get_name() ] = $property; + $this->iterator = null; + } + + public function has_property( string $name ): bool { + return array_key_exists( $name, $this->map ); + } + + public function get_property( string $key ): Property { + if ( false === $this->has_property( $key ) ) { + throw new JsonMapperException( "There is no property named $key" ); + } + + return $this->map[ $key ]; + } + + public function merge( self $other ) { + /** @var Property $property */ + foreach ( $other as $property ) { + if ( ! $this->has_property( $property->get_name() ) ) { + $this->addProperty( $property ); + continue; + } + + if ( $property == $this->get_property( $property->get_name() ) ) { + continue; + } + + $current = $this->get_property( $property->get_name() ); + $builder = $current->as_builder(); + + $builder->setIsNullable( $current->is_nullable() || $property->is_nullable() ); + foreach ( $property->get_property_types() as $propertyType ) { + $builder->addType( $propertyType->getType(), $propertyType->getArrayInformation() ); + } + + $this->addProperty( $builder->build() ); + } + $this->iterator = null; + } + + // phpcs:ignore + public function getIterator(): ArrayIterator { + if ( \is_null( $this->iterator ) ) { + $this->iterator = new ArrayIterator( $this->map ); + } + + return $this->iterator; + } + + // phpcs:ignore + public function jsonSerialize(): array { + return array( + 'properties' => $this->map, + ); + } + + public function toString(): string { + return (string) \json_encode( $this ); + } +} diff --git a/src/WordPress/JsonMapper/Property/PropertyType.php b/src/WordPress/JsonMapper/Property/PropertyType.php new file mode 100644 index 00000000..0a915e74 --- /dev/null +++ b/src/WordPress/JsonMapper/Property/PropertyType.php @@ -0,0 +1,42 @@ +type = $type; + $this->arrayInformation = $isArray; + } + + public function getType(): string { + return $this->type; + } + + public function isArray(): bool { + return $this->arrayInformation->isArray(); + } + + public function isMultiDimensionalArray(): bool { + return $this->arrayInformation->isMultiDimensionalArray(); + } + + public function getArrayInformation(): ArrayInformation { + return $this->arrayInformation; + } + + public function jsonSerialize(): array { + return array( + 'type' => $this->type, + 'isArray' => $this->arrayInformation->isArray(), + 'arrayInformation' => $this->arrayInformation, + ); + } +} diff --git a/src/WordPress/JsonMapper/UseNodeVisitor.php b/src/WordPress/JsonMapper/UseNodeVisitor.php new file mode 100644 index 00000000..e13c049d --- /dev/null +++ b/src/WordPress/JsonMapper/UseNodeVisitor.php @@ -0,0 +1,38 @@ +uses as $use ) { + $this->imports[] = new Import( $use->name->toString(), \is_null( $use->alias ) ? null : $use->alias->name ); + } + } elseif ( $node instanceof Stmt\GroupUse ) { + foreach ( $node->uses as $use ) { + $this->imports[] = new Import( + "{$node->prefix}\\{$use->name}", + \is_null( $use->alias ) ? null : $use->alias->name + ); + } + } + + return null; + } + + /** @return Import[] */ + public function getImports(): array { + return $this->imports; + } +} diff --git a/tests/JsonMapper/JsonMapperTest.php b/tests/JsonMapper/JsonMapperTest.php new file mode 100644 index 00000000..31081c4a --- /dev/null +++ b/tests/JsonMapper/JsonMapperTest.php @@ -0,0 +1,136 @@ +json_mapper = new JsonMapper(); + } + + public function testMapsEmptyBlueprint() { + $raw_json = '{}'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); + + $expected = BlueprintBuilder::create() + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } + + public function testMapsWordPressVersion() { + $raw_json = + '{ + "WordPressVersion":"https://wordpress.org/latest.zip" + }'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); + + $expected = BlueprintBuilder::create() + ->withWordPressVersion( 'https://wordpress.org/latest.zip' ) + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } + + public function testMapsMultiplePlugins() { + $raw_json = + '{ + "plugins": + [ + "https://downloads.wordpress.org/plugin/wordpress-importer.zip", + "https://downloads.wordpress.org/plugin/hello-dolly.zip", + "https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip" + ] + }'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); + + $expected = BlueprintBuilder::create() + ->withPlugins( + array( + 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', + 'https://downloads.wordpress.org/plugin/hello-dolly.zip', + 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', + ) + ) + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } + + public function testMapsMultiplePluginsSomeAsShorthandSomeAsSteps() { + $raw_json = + '{ + "plugins": + [ + "https://downloads.wordpress.org/plugin/wordpress-importer.zip", + "https://downloads.wordpress.org/plugin/hello-dolly.zip" + ], + "steps": + [ + {"step":"installPlugin","pluginZipFile":"https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip"} + ] + }'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); + + $expected = BlueprintBuilder::create() + ->withPlugins( + array( + 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', + 'https://downloads.wordpress.org/plugin/hello-dolly.zip', + 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', + ) + ) + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } + + public function testMapsWhenSpecificStepAppearsTwice() { + $raw_json = + '{ + "steps": + [ + {"step":"mkdir","path":"dir1"}, + {"step":"rm","path":"dir1"}, + {"step":"mkdir","path":"dir2"} + ] + }'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); + + $expected = BlueprintBuilder::create() + ->makeDirectory( 'dir1' ) + ->remove( 'dir1' ) + ->makeDirectory( 'dir2' ) + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } +} diff --git a/tests/Pest.php b/tests/Pest.php deleted file mode 100644 index 5949c617..00000000 --- a/tests/Pest.php +++ /dev/null @@ -1,45 +0,0 @@ -in('Feature'); - -/* -|-------------------------------------------------------------------------- -| Expectations -|-------------------------------------------------------------------------- -| -| When you're writing tests, you often need to check that values meet certain conditions. The -| "expect()" function gives you access to a set of "expectations" methods that you can use -| to assert different things. Of course, you may extend the Expectation API at any time. -| -*/ - -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - -/* -|-------------------------------------------------------------------------- -| Functions -|-------------------------------------------------------------------------- -| -| While Pest is very powerful out-of-the-box, you may have some testing code specific to your -| project that you don't want to repeat in every file. Here you can also expose helpers as -| global functions to help you to reduce the number of lines of code in your test files. -| -*/ - -function something() -{ - // .. -} From 3e04ddb09603a619e5c2f112b28ae01c7c5bca27 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 12:01:58 +0100 Subject: [PATCH 02/28] Remove `remove_utf8_bom()` --- src/WordPress/Blueprints/BlueprintParser.php | 126 +++++++++---------- 1 file changed, 61 insertions(+), 65 deletions(-) diff --git a/src/WordPress/Blueprints/BlueprintParser.php b/src/WordPress/Blueprints/BlueprintParser.php index a40500a5..58abfbea 100644 --- a/src/WordPress/Blueprints/BlueprintParser.php +++ b/src/WordPress/Blueprints/BlueprintParser.php @@ -2,6 +2,7 @@ namespace WordPress\Blueprints; +use InvalidArgumentException; use Opis\JsonSchema\Errors\ErrorFormatter; use WordPress\Blueprints\Model\BlueprintBuilder; use WordPress\Blueprints\Model\DataClass\Blueprint; @@ -26,18 +27,13 @@ public function __construct( $this->mapper = $mapper; } - function remove_utf8_bom( $text ) { - $bom = pack( 'H*', 'EFBBBF' ); - $text = preg_replace( "/^$bom/", '', $text ); - return $text; - } - - public function parse( $rawBlueprint ) { - if ( $rawBlueprint instanceof \stdClass ) { - return $this->fromObject( $rawBlueprint ); + public function parse( $raw_blueprint ) { + if ( $raw_blueprint instanceof \stdClass ) { + return $this->fromObject( $raw_blueprint ); } - if ( is_string( $rawBlueprint ) ) { - $data = json_decode( $rawBlueprint, false); + + if ( is_string( $raw_blueprint ) ) { + $data = json_decode( $raw_blueprint, false ); if ( null === $data ) { throw new InvalidArgumentException( 'Malformed JSON.' ); @@ -46,12 +42,12 @@ public function parse( $rawBlueprint ) { return $this->fromObject( $data ); } - if ( $rawBlueprint instanceof Blueprint ) { - return $this->fromBlueprint( $rawBlueprint ); + if ( $raw_blueprint instanceof Blueprint ) { + return $this->fromBlueprint( $raw_blueprint ); } - if ( $rawBlueprint instanceof BlueprintBuilder ) { - return $this->fromBlueprint( $rawBlueprint->toBlueprint() ); + if ( $raw_blueprint instanceof BlueprintBuilder ) { + return $this->fromBlueprint( $raw_blueprint->toBlueprint() ); } throw new InvalidArgumentException( @@ -59,7 +55,7 @@ public function parse( $rawBlueprint ) { ); } - public function fromObject( object $data ) { + public function fromObject( \stdClass $data ) { $result = $this->validator->validate( $data ); if ( ! $result->isValid() ) { print_r( ( new ErrorFormatter() )->format( $result->error() ) ); @@ -77,53 +73,53 @@ public function fromBlueprint( Blueprint $blueprint ) { return $blueprint; } -// private function findSubErrorForSpecificAnyOfOption(Error $e, string $anyOfRef) -// { -// if ($anyOfRef[0] === '#') { -// $anyOfRef = substr($anyOfRef, 1); -// } -// if ($e->schemaPointers) { -// foreach ($e->schemaPointers as $pointer) { -// if (str_starts_with($pointer, $anyOfRef)) { -// return $e; -// } -// } -// } -// if (!$e->subErrors) { -// return $e->error; -// } -// foreach ($e->subErrors as $subError) { -// $subError = findSubErrorForSpecificAnyOfOption($subError, $anyOfRef); -// if ($subError !== null) { -// return $subError; -// } -// } -// return $e; -// } - -// private function getSubschema($pointer) -// { -// if ($pointer[0] === '#') { -// $pointer = substr($pointer, 1); -// } -// if ($pointer[0] !== '/') { -// $pointer = substr($pointer, 1); -// } -// $path = explode('/', substr($pointer, 1)); -// $subSchema = $this->blueprintSchema; -// foreach ($path as $key) { -// if (is_numeric($key) && !property_exists($subSchema, $key)) { -// foreach ($subSchema->anyOf as $v) { -// if (is_object($v) && property_exists($v, '$ref')) { -// $subSchema = $v; -// break; -// } -// } -// } else { -// $subSchema = $subSchema->$key; -// } -// } -// -// return $subSchema; -// } + // private function findSubErrorForSpecificAnyOfOption(Error $e, string $anyOfRef) + // { + // if ($anyOfRef[0] === '#') { + // $anyOfRef = substr($anyOfRef, 1); + // } + // if ($e->schemaPointers) { + // foreach ($e->schemaPointers as $pointer) { + // if (str_starts_with($pointer, $anyOfRef)) { + // return $e; + // } + // } + // } + // if (!$e->subErrors) { + // return $e->error; + // } + // foreach ($e->subErrors as $subError) { + // $subError = findSubErrorForSpecificAnyOfOption($subError, $anyOfRef); + // if ($subError !== null) { + // return $subError; + // } + // } + // return $e; + // } + + // private function getSubschema($pointer) + // { + // if ($pointer[0] === '#') { + // $pointer = substr($pointer, 1); + // } + // if ($pointer[0] !== '/') { + // $pointer = substr($pointer, 1); + // } + // $path = explode('/', substr($pointer, 1)); + // $subSchema = $this->blueprintSchema; + // foreach ($path as $key) { + // if (is_numeric($key) && !property_exists($subSchema, $key)) { + // foreach ($subSchema->anyOf as $v) { + // if (is_object($v) && property_exists($v, '$ref')) { + // $subSchema = $v; + // break; + // } + // } + // } else { + // $subSchema = $subSchema->$key; + // } + // } + // + // return $subSchema; + // } } From da346af421e0c42c25d190799e03f60bee3456cb Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 12:49:07 +0100 Subject: [PATCH 03/28] Externalize custom factory configuration - generalizes Json Mapper - reworks tests to test the proper configuration of the BlueprintMapper as an instance of the generic JsonMapper - reworks tests to test generic JsonMapper features independently of custom configuration --- src/WordPress/Blueprints/BlueprintMapper.php | 52 ++++++++- .../JsonMapper/Evaluators/PropertyMapper.php | 57 ++-------- src/WordPress/JsonMapper/JsonMapper.php | 12 +- tests/Blueprints/BlueprintMapperTest.php | 106 ++++++++++++++++++ tests/JsonMapper/JsonMapperTest.php | 100 ----------------- 5 files changed, 171 insertions(+), 156 deletions(-) create mode 100644 tests/Blueprints/BlueprintMapperTest.php diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index a0d6b0ea..8064ccc3 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -4,16 +4,22 @@ use WordPress\Blueprints\Model\DataClass\Blueprint; +use WordPress\Blueprints\Model\DataClass\ModelInfo; use WordPress\JsonMapper\JsonMapper; +use WordPress\JsonMapper\JsonMapperException; class BlueprintMapper { - protected $mapper; + private $mapper; + + private $custom_factories = array(); public function __construct() { $this->mapper = new JsonMapper(); + $this->prepare_factory_for_resources(); + $this->prepare_factory_for_steps(); + $this->mapper->configure_evaluators( $this->custom_factories ); } - /** * Maps a parsed and validated JSON object to a Blueprint class instance. * @@ -23,4 +29,44 @@ public function __construct() { public function map( $blueprint ) { return $this->mapper->map_to_class( $blueprint, Blueprint::class ); } -} \ No newline at end of file + + private function prepare_factory_for_resources() { + $resource_map = array(); + foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resourceClass ) { + $resource_map[ $resourceClass::DISCRIMINATOR ] = $resourceClass; + } + + $this->custom_factories['ResourceDefinitionInterface'] = + function ( $value ) use ( $resource_map ) { + if ( is_string( $value ) ) { + return $value; + } + if ( ! isset( $value->resource ) ) { + throw new JsonMapperException( 'Resource type must be defined' ); + } + if ( ! isset( $resource_map[ $value->resource ] ) ) { + throw new JsonMapperException( "Resource type {$value->resource} is not implemented" ); + } + + return $this->mapper->map_to_class( $value, $resource_map[ $value->resource ] ); + }; + } + + private function prepare_factory_for_steps() { + $step_map = array(); + foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { + $step_map[ $class::DISCRIMINATOR ] = $class; + } + $this->custom_factories['StepDefinitionInterface'] = + function ( $value ) use ( $step_map ) { + if ( ! isset( $value->step ) ) { + throw new JsonMapperException( 'Step must be defined' ); + } + if ( ! isset( $step_map[ $value->step ] ) ) { + throw new JsonMapperException( "Step {$value->step} is not implemented" ); + } + + return $this->mapper->map_to_class( $value, $step_map[ $value->step ] ); + }; + } +} diff --git a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php index f7272a7b..b51eebf6 100644 --- a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php +++ b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php @@ -30,13 +30,15 @@ class PropertyMapper implements JsonEvaluatorInterface { private $mapper; public function __construct( - JsonMapper $json_mapper + JsonMapper $json_mapper, + array $custom_factories = null ) { $this->mapper = $json_mapper; $this->add_factories_for_native_php_classes(); - $this->add_factory_for_resources(); - $this->add_factory_for_steps(); $this->add_factory_for_arrays(); + if ( null !== $custom_factories ) { + $this->add_custom_factories( $custom_factories ); + } } public function evaluate( @@ -214,6 +216,12 @@ private function add_factory( string $class_name, callable $factory ) { $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; } + public function add_custom_factories( array $custom_factories ) { + foreach ( $custom_factories as $class_name => $custom_factory ) { + $this->add_factory( $class_name, $custom_factory ); + } + } + private function add_factories_for_native_php_classes() { $this->add_factory( \DateTime::class, @@ -235,49 +243,6 @@ static function ( $value ) { ); } - private function add_factory_for_resources() { - $resourceMap = array(); - foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resourceClass ) { - $resourceMap[ $resourceClass::DISCRIMINATOR ] = $resourceClass; - } - $this->add_factory( - 'ResourceDefinitionInterface', - function ( $value ) use ( $resourceMap ) { - if ( is_string( $value ) ) { - return $value; - } - if ( ! isset( $value->resource ) ) { - throw new JsonMapperException( 'Resource type must be defined' ); - } - if ( ! isset( $resourceMap[ $value->resource ] ) ) { - throw new JsonMapperException( "Resource type {$value->resource} is not implemented" ); - } - - return $this->mapper->map_to_class( $value, $resourceMap[ $value->resource ] ); - } - ); - } - - private function add_factory_for_steps() { - $stepMap = array(); - foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { - $stepMap[ $class::DISCRIMINATOR ] = $class; - } - $this->add_factory( - 'StepDefinitionInterface', - function ( $value ) use ( $stepMap ) { - if ( ! isset( $value->step ) ) { - throw new JsonMapperException( 'Step must be defined' ); - } - if ( ! isset( $stepMap[ $value->step ] ) ) { - throw new JsonMapperException( "Step {$value->step} is not implemented" ); - } - - return $this->mapper->map_to_class( $value, $stepMap[ $value->step ] ); - } - ); - } - private function add_factory_for_arrays() { $this->add_factory( \ArrayObject::class, diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 452a11ee..e629f1b0 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -10,21 +10,19 @@ class JsonMapper { private $evaluators; - public function __construct() {} + public function __construct() { + $this->configure_evaluators(); + } - private function configure_evaluators() { - if ( null === $this->evaluators ) { + public function configure_evaluators( array $custom_factories = null ) { $this->evaluators = array( new DocBlockAnnotations(), new NamespaceResolver(), - new PropertyMapper( $this ), // PropertyMapper has to be the last evaluator. + new PropertyMapper( $this, $custom_factories ), // PropertyMapper has to be the last evaluator. ); - } } public function map_to_class( \stdClass $json, string $class ) { - $this->configure_evaluators(); - $object_wrapper = new ObjectWrapper( null, $class ); $property_map = new PropertyMap(); diff --git a/tests/Blueprints/BlueprintMapperTest.php b/tests/Blueprints/BlueprintMapperTest.php new file mode 100644 index 00000000..4a320579 --- /dev/null +++ b/tests/Blueprints/BlueprintMapperTest.php @@ -0,0 +1,106 @@ +blueprint_mapper = new BlueprintMapper(); + } + + public function testMapsEmptyBlueprint() { + $raw_json = '{}'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->blueprint_mapper->map( $parsed_json ); + + $expected = BlueprintBuilder::create() + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } + + public function testMapsWordPressVersion() { + $raw_json = + '{ + "WordPressVersion":"https://wordpress.org/latest.zip" + }'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->blueprint_mapper->map( $parsed_json ); + + $expected = BlueprintBuilder::create() + ->withWordPressVersion( 'https://wordpress.org/latest.zip' ) + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } + + public function testMapsMultiplePlugins() { + $raw_json = + '{ + "plugins": + [ + "https://downloads.wordpress.org/plugin/wordpress-importer.zip", + "https://downloads.wordpress.org/plugin/hello-dolly.zip", + "https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip" + ] + }'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->blueprint_mapper->map( $parsed_json ); + + $expected = BlueprintBuilder::create() + ->withPlugins( + array( + 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', + 'https://downloads.wordpress.org/plugin/hello-dolly.zip', + 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', + ) + ) + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } + + public function testMapsWhenSpecificStepAppearsTwice() { + $raw_json = + '{ + "steps": + [ + {"step":"mkdir","path":"dir1"}, + {"step":"rm","path":"dir1"}, + {"step":"mkdir","path":"dir2"} + ] + }'; + + $parsed_json = json_decode( $raw_json, false ); + + $blueprint = $this->blueprint_mapper->map( $parsed_json ); + + $expected = BlueprintBuilder::create() + ->makeDirectory( 'dir1' ) + ->remove( 'dir1' ) + ->makeDirectory( 'dir2' ) + ->toBlueprint(); + + $this->assertEquals( $expected, $blueprint ); + } +} diff --git a/tests/JsonMapper/JsonMapperTest.php b/tests/JsonMapper/JsonMapperTest.php index 31081c4a..c8f8f71d 100644 --- a/tests/JsonMapper/JsonMapperTest.php +++ b/tests/JsonMapper/JsonMapperTest.php @@ -33,104 +33,4 @@ public function testMapsEmptyBlueprint() { $this->assertEquals( $expected, $blueprint ); } - - public function testMapsWordPressVersion() { - $raw_json = - '{ - "WordPressVersion":"https://wordpress.org/latest.zip" - }'; - - $parsed_json = json_decode( $raw_json, false ); - - $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); - - $expected = BlueprintBuilder::create() - ->withWordPressVersion( 'https://wordpress.org/latest.zip' ) - ->toBlueprint(); - - $this->assertEquals( $expected, $blueprint ); - } - - public function testMapsMultiplePlugins() { - $raw_json = - '{ - "plugins": - [ - "https://downloads.wordpress.org/plugin/wordpress-importer.zip", - "https://downloads.wordpress.org/plugin/hello-dolly.zip", - "https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip" - ] - }'; - - $parsed_json = json_decode( $raw_json, false ); - - $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); - - $expected = BlueprintBuilder::create() - ->withPlugins( - array( - 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', - 'https://downloads.wordpress.org/plugin/hello-dolly.zip', - 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', - ) - ) - ->toBlueprint(); - - $this->assertEquals( $expected, $blueprint ); - } - - public function testMapsMultiplePluginsSomeAsShorthandSomeAsSteps() { - $raw_json = - '{ - "plugins": - [ - "https://downloads.wordpress.org/plugin/wordpress-importer.zip", - "https://downloads.wordpress.org/plugin/hello-dolly.zip" - ], - "steps": - [ - {"step":"installPlugin","pluginZipFile":"https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip"} - ] - }'; - - $parsed_json = json_decode( $raw_json, false ); - - $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); - - $expected = BlueprintBuilder::create() - ->withPlugins( - array( - 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', - 'https://downloads.wordpress.org/plugin/hello-dolly.zip', - 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', - ) - ) - ->toBlueprint(); - - $this->assertEquals( $expected, $blueprint ); - } - - public function testMapsWhenSpecificStepAppearsTwice() { - $raw_json = - '{ - "steps": - [ - {"step":"mkdir","path":"dir1"}, - {"step":"rm","path":"dir1"}, - {"step":"mkdir","path":"dir2"} - ] - }'; - - $parsed_json = json_decode( $raw_json, false ); - - $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); - - $expected = BlueprintBuilder::create() - ->makeDirectory( 'dir1' ) - ->remove( 'dir1' ) - ->makeDirectory( 'dir2' ) - ->toBlueprint(); - - $this->assertEquals( $expected, $blueprint ); - } } From 55cbdba6c12ededa1e4ca79ee8d0bc700700214c Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 14:17:31 +0100 Subject: [PATCH 04/28] Drop AnnotationMap and other small changes --- src/WordPress/Blueprints/BlueprintMapper.php | 2 +- src/WordPress/JsonMapper/AnnotationMap.php | 64 ------------------- src/WordPress/JsonMapper/ArrayInformation.php | 10 --- .../Evaluators/DocBlockAnnotations.php | 46 +++++++------ .../JsonMapper/Evaluators/PropertyMapper.php | 1 - .../JsonMapper/Property/PropertyType.php | 4 -- 6 files changed, 23 insertions(+), 104 deletions(-) delete mode 100644 src/WordPress/JsonMapper/AnnotationMap.php diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index 8064ccc3..f4ca2361 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -26,7 +26,7 @@ public function __construct() { * @param $blueprint * @return Blueprint */ - public function map( $blueprint ) { + public function map( $blueprint ): Blueprint { return $this->mapper->map_to_class( $blueprint, Blueprint::class ); } diff --git a/src/WordPress/JsonMapper/AnnotationMap.php b/src/WordPress/JsonMapper/AnnotationMap.php deleted file mode 100644 index 18bf8c75..00000000 --- a/src/WordPress/JsonMapper/AnnotationMap.php +++ /dev/null @@ -1,64 +0,0 @@ -var = $var; - $this->params = $params; - $this->return = $return; - } - - public function hasVar(): bool - { - return ! \is_null($this->var); - } - - public function getVar(): string - { - if (\is_null($this->var)) { - throw new \Exception('Annotation map doesnt contain valid value for var'); - } - return $this->var; - } - - public function getParams(): array - { - return $this->params; - } - - public function hasParam(string $paramName): bool - { - return array_key_exists($paramName, $this->params); - } - - public function getParam(string $paramName): string - { - if (!$this->hasParam($paramName)) { - throw new \Exception("Annotation map doesnt contain param with name $paramName"); - } - return $this->params[$paramName]; - } - - public function hasReturn(): bool - { - return ! \is_null($this->return); - } - - public function getReturn(): string - { - if (\is_null($this->return)) { - throw new \Exception('Annotation map doesnt contain valid value for return'); - } - return $this->return; - } -} \ No newline at end of file diff --git a/src/WordPress/JsonMapper/ArrayInformation.php b/src/WordPress/JsonMapper/ArrayInformation.php index e2254f5e..9fba9a74 100644 --- a/src/WordPress/JsonMapper/ArrayInformation.php +++ b/src/WordPress/JsonMapper/ArrayInformation.php @@ -35,16 +35,6 @@ public function isArray(): bool return $this->isArray; } - public function getDimensions(): int - { - return $this->dimensions; - } - - public function isMultiDimensionalArray(): bool - { - return $this->isArray && $this->dimensions > 1; - } - public function jsonSerialize(): array { return [ diff --git a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php index fae265f7..01225041 100644 --- a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php @@ -1,11 +1,8 @@ merge( $this->fetchPropertyMapForObject( $object_wrapper ) ); + $property_map->merge( $this->compute_property_map( $object_wrapper ) ); } - private function fetchPropertyMapForObject( ObjectWrapper $object ): PropertyMap { - $intermediatePropertyMap = new PropertyMap(); - foreach ( $this->getObjectPropertiesIncludingParents( $object ) as $property ) { - $name = $property->getName(); + private function compute_property_map(ObjectWrapper $object ): PropertyMap { + $intermediate_property_map = new PropertyMap(); + foreach ( self::get_properties( $object ) as $property ) { $docBlock = $property->getDocComment(); - if ( $docBlock === false ) { + if ( false === $docBlock ) { continue; } - $annotations = self::parseDocBlockToAnnotationMap( $docBlock ); - - if ( ! $annotations->hasVar() ) { + $var = self::parse_var( $docBlock ); + if ( null === $var ) { continue; } - $types = \explode( '|', $annotations->getVar() ); + $types = \explode( '|', $var ); $nullable = \in_array( 'null', $types, true ); $types = \array_filter( $types, @@ -51,14 +46,14 @@ static function ( string $type ) { ); $builder = PropertyBuilder::new() - ->setName( $name ) + ->setName( $property->getName() ) ->setIsNullable( $nullable ) - ->setVisibility( $this->fromReflectionProperty( $property ) ); + ->setVisibility( $this->parse_visibility( $property ) ); /* A union type that has one of its types defined as array is to complex to understand */ if ( \in_array( 'array', $types, true ) ) { $property = $builder->addType( 'mixed', ArrayInformation::singleDimension() )->build(); - $intermediatePropertyMap->addProperty( $property ); + $intermediate_property_map->addProperty( $property ); continue; } @@ -82,14 +77,13 @@ static function ( string $type ) { } $property = $builder->build(); - $intermediatePropertyMap->addProperty( $property ); + $intermediate_property_map->addProperty( $property ); } - return $intermediatePropertyMap; + return $intermediate_property_map; } - private function fromReflectionProperty( ReflectionProperty $property ): string { - + private function parse_visibility( ReflectionProperty $property ): string { if ( $property->isPublic() ) { return 'public'; } @@ -99,7 +93,11 @@ private function fromReflectionProperty( ReflectionProperty $property ): string return 'private'; } - public static function parseDocBlockToAnnotationMap( string $docBlock ): AnnotationMap { + /** + * @param string $docBlock + * @return string|null + */ + private static function parse_var( string $docBlock ): string { // Strip away the start "/**' and ending "*/" if ( strpos( $docBlock, '/**' ) === 0 ) { $docBlock = \substr( $docBlock, 3 ); @@ -118,11 +116,11 @@ public static function parseDocBlockToAnnotationMap( string $docBlock ): Annotat } } - return new AnnotationMap( $var ?: null, array(), null ); + return $var; } - /** @return \ReflectionProperty[] */ - public function getObjectPropertiesIncludingParents( ObjectWrapper $object ): array { + /** @return ReflectionProperty[] */ + private static function get_properties( ObjectWrapper $object ): array { $properties = array(); $reflectionClass = $object->getReflectedObject(); do { diff --git a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php index b51eebf6..fd19978e 100644 --- a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php +++ b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php @@ -4,7 +4,6 @@ use ReflectionClass; use ReflectionMethod; -use WordPress\Blueprints\Model\DataClass\ModelInfo; use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\JsonMapperException; use WordPress\JsonMapper\ObjectWrapper; diff --git a/src/WordPress/JsonMapper/Property/PropertyType.php b/src/WordPress/JsonMapper/Property/PropertyType.php index 0a915e74..8d8c037b 100644 --- a/src/WordPress/JsonMapper/Property/PropertyType.php +++ b/src/WordPress/JsonMapper/Property/PropertyType.php @@ -24,10 +24,6 @@ public function isArray(): bool { return $this->arrayInformation->isArray(); } - public function isMultiDimensionalArray(): bool { - return $this->arrayInformation->isMultiDimensionalArray(); - } - public function getArrayInformation(): ArrayInformation { return $this->arrayInformation; } From 129d92157ec4e0877e81300466ddc763a8973f3c Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 15:14:52 +0100 Subject: [PATCH 05/28] Drop PropertyBuilder from DocBlockAnnotations --- .../Evaluators/DocBlockAnnotations.php | 48 +++++++++++-------- .../JsonMapper/Property/Property.php | 6 +-- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php index 01225041..7e9bd68a 100644 --- a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php @@ -6,8 +6,9 @@ use WordPress\JsonMapper\ArrayInformation; use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\ObjectWrapper; -use WordPress\JsonMapper\Property\PropertyBuilder; +use WordPress\JsonMapper\Property\Property; use WordPress\JsonMapper\Property\PropertyMap; +use WordPress\JsonMapper\Property\PropertyType; class DocBlockAnnotations implements JsonEvaluatorInterface { @@ -19,11 +20,12 @@ public function evaluate( \stdClass $json, ObjectWrapper $object_wrapper, PropertyMap $property_map, - JsonMapper $mapper) { + JsonMapper $mapper + ) { $property_map->merge( $this->compute_property_map( $object_wrapper ) ); } - private function compute_property_map(ObjectWrapper $object ): PropertyMap { + private function compute_property_map( ObjectWrapper $object ): PropertyMap { $intermediate_property_map = new PropertyMap(); foreach ( self::get_properties( $object ) as $property ) { $docBlock = $property->getDocComment(); @@ -36,33 +38,35 @@ private function compute_property_map(ObjectWrapper $object ): PropertyMap { continue; } - $types = \explode( '|', $var ); - $nullable = \in_array( 'null', $types, true ); - $types = \array_filter( + $name = $property->getName(); + $types = explode( '|', $var ); + $is_nullable = in_array( 'null', $types, true ); + $types = array_filter( $types, static function ( string $type ) { return $type !== 'null'; } ); - $builder = PropertyBuilder::new() - ->setName( $property->getName() ) - ->setIsNullable( $nullable ) - ->setVisibility( $this->parse_visibility( $property ) ); + $property = new Property( + $name, + self::parse_visibility( $property ), + $is_nullable + ); /* A union type that has one of its types defined as array is to complex to understand */ - if ( \in_array( 'array', $types, true ) ) { - $property = $builder->addType( 'mixed', ArrayInformation::singleDimension() )->build(); + if ( in_array( 'array', $types, true ) ) { + $property->property_types[] = new PropertyType( 'mixed', ArrayInformation::singleDimension() ); $intermediate_property_map->addProperty( $property ); continue; } foreach ( $types as $type ) { - $type = \trim( $type ); - $isAnArrayType = \substr( $type, -2 ) === '[]'; + $type = trim( $type ); + $isAnArrayType = substr( $type, -2 ) === '[]'; if ( ! $isAnArrayType ) { - $builder->addType( $type, ArrayInformation::notAnArray() ); + $property->property_types[] = new PropertyType( $type, ArrayInformation::notAnArray() ); continue; } @@ -73,17 +77,20 @@ static function ( string $type ) { $type = substr( $type, 0, $initialBracketPosition ); } - $builder->addType( $type, ArrayInformation::multiDimension( $dimensions ) ); + $property->property_types[] = new PropertyType( $type, ArrayInformation::multiDimension( $dimensions ) ); } - $property = $builder->build(); $intermediate_property_map->addProperty( $property ); } return $intermediate_property_map; } - private function parse_visibility( ReflectionProperty $property ): string { + /** + * @param ReflectionProperty $property + * @return string + */ + private static function parse_visibility( ReflectionProperty $property ): string { if ( $property->isPublic() ) { return 'public'; } @@ -119,7 +126,10 @@ private static function parse_var( string $docBlock ): string { return $var; } - /** @return ReflectionProperty[] */ + /** + * @param ObjectWrapper $object + * @return ReflectionProperty[] + */ private static function get_properties( ObjectWrapper $object ): array { $properties = array(); $reflectionClass = $object->getReflectedObject(); diff --git a/src/WordPress/JsonMapper/Property/Property.php b/src/WordPress/JsonMapper/Property/Property.php index 1aef3755..292d2d8a 100644 --- a/src/WordPress/JsonMapper/Property/Property.php +++ b/src/WordPress/JsonMapper/Property/Property.php @@ -4,16 +4,16 @@ class Property implements \JsonSerializable { /** @var string */ - private $name; + public $name; /** @var PropertyType[] */ - private $property_types; + public $property_types; /** @var string */ public $visibility; /** @var bool */ - private $is_nullable; + public $is_nullable; public function __construct( string $name, From 611627d764dd33f6f0b83e37ef82645836f7162d Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 15:15:06 +0100 Subject: [PATCH 06/28] Drop PropertyBuilder from DocBlockAnnotations --- src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php index 7e9bd68a..ed33a1b1 100644 --- a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php @@ -25,7 +25,11 @@ public function evaluate( $property_map->merge( $this->compute_property_map( $object_wrapper ) ); } - private function compute_property_map( ObjectWrapper $object ): PropertyMap { + /** + * @param ObjectWrapper $object + * @return PropertyMap + */ + private function compute_property_map(ObjectWrapper $object ): PropertyMap { $intermediate_property_map = new PropertyMap(); foreach ( self::get_properties( $object ) as $property ) { $docBlock = $property->getDocComment(); From ca12322bf04c14f05384431b67294bcd69611793 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 15:20:49 +0100 Subject: [PATCH 07/28] Clear DocBlockAnnotations --- .../Evaluators/DocBlockAnnotations.php | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php index ed33a1b1..d3572bbe 100644 --- a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php @@ -32,12 +32,12 @@ public function evaluate( private function compute_property_map(ObjectWrapper $object ): PropertyMap { $intermediate_property_map = new PropertyMap(); foreach ( self::get_properties( $object ) as $property ) { - $docBlock = $property->getDocComment(); - if ( false === $docBlock ) { + $doc_block = $property->getDocComment(); + if ( false === $doc_block ) { continue; } - $var = self::parse_var( $docBlock ); + $var = self::parse_var( $doc_block ); if ( null === $var ) { continue; } @@ -48,7 +48,7 @@ private function compute_property_map(ObjectWrapper $object ): PropertyMap { $types = array_filter( $types, static function ( string $type ) { - return $type !== 'null'; + return 'null' !== $type; } ); @@ -66,19 +66,19 @@ static function ( string $type ) { } foreach ( $types as $type ) { - $type = trim( $type ); - $isAnArrayType = substr( $type, -2 ) === '[]'; + $type = trim( $type ); + $is_array = substr( $type, -2 ) === '[]'; - if ( ! $isAnArrayType ) { + if ( ! $is_array ) { $property->property_types[] = new PropertyType( $type, ArrayInformation::notAnArray() ); continue; } - $initialBracketPosition = strpos( $type, '[' ); - $dimensions = substr_count( $type, '[]' ); + $first_bracket_index = strpos( $type, '[' ); + $dimensions = substr_count( $type, '[]' ); - if ( $initialBracketPosition !== false ) { - $type = substr( $type, 0, $initialBracketPosition ); + if ( false !== $first_bracket_index ) { + $type = substr( $type, 0, $first_bracket_index ); } $property->property_types[] = new PropertyType( $type, ArrayInformation::multiDimension( $dimensions ) ); @@ -105,23 +105,23 @@ private static function parse_visibility( ReflectionProperty $property ): string } /** - * @param string $docBlock + * @param string $doc_block * @return string|null */ - private static function parse_var( string $docBlock ): string { - // Strip away the start "/**' and ending "*/" - if ( strpos( $docBlock, '/**' ) === 0 ) { - $docBlock = \substr( $docBlock, 3 ); + private static function parse_var( string $doc_block ): string { + // Strip away the start "/**' and ending "*/". + if ( strpos( $doc_block, '/**' ) === 0 ) { + $doc_block = \substr( $doc_block, 3 ); } - if ( substr( $docBlock, -2 ) === '*/' ) { - $docBlock = \substr( $docBlock, 0, -2 ); + if ( substr( $doc_block, -2 ) === '*/' ) { + $doc_block = \substr( $doc_block, 0, -2 ); } - $docBlock = \trim( $docBlock ); + $doc_block = \trim( $doc_block ); $var = null; - if ( \preg_match_all( self::DOC_BLOCK_REGEX, $docBlock, $matches ) ) { + if ( \preg_match_all( self::DOC_BLOCK_REGEX, $doc_block, $matches ) ) { for ( $x = 0, $max = count( $matches[0] ); $x < $max; $x++ ) { - if ( $matches['name'][ $x ] === 'var' ) { + if ( 'var' === $matches['name'][ $x ] ) { $var = $matches['value'][ $x ]; } } @@ -135,11 +135,11 @@ private static function parse_var( string $docBlock ): string { * @return ReflectionProperty[] */ private static function get_properties( ObjectWrapper $object ): array { - $properties = array(); - $reflectionClass = $object->getReflectedObject(); + $properties = array(); + $reflection_class = $object->getReflectedObject(); do { - $properties = array_merge( $properties, $reflectionClass->getProperties() ); - } while ( $reflectionClass = $reflectionClass->getParentClass() ); + $properties = array_merge( $properties, $reflection_class->getProperties() ); + } while ( $reflection_class = $reflection_class->getParentClass() ); return $properties; } } From afb15758a539b1c7e5afa75d2b48025214c72073 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 16:10:08 +0100 Subject: [PATCH 08/28] Expose evaluators, pass factories as an array of factory_name to closure --- src/WordPress/Blueprints/BlueprintMapper.php | 97 ++++++++++++------- .../JsonMapper/Evaluators/PropertyMapper.php | 4 +- src/WordPress/JsonMapper/JsonMapper.php | 34 ++++--- tests/JsonMapper/JsonMapperTest.php | 2 +- 4 files changed, 83 insertions(+), 54 deletions(-) diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index f4ca2361..c182d2f7 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -2,71 +2,94 @@ namespace WordPress\Blueprints; - +use Closure; +use stdClass; use WordPress\Blueprints\Model\DataClass\Blueprint; use WordPress\Blueprints\Model\DataClass\ModelInfo; +use WordPress\JsonMapper\Evaluators\DocBlockAnnotations; +use WordPress\JsonMapper\Evaluators\NamespaceResolver; use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\JsonMapperException; class BlueprintMapper { + /** + * @var JsonMapper + */ private $mapper; - private $custom_factories = array(); - + /** + * + */ public function __construct() { - $this->mapper = new JsonMapper(); - $this->prepare_factory_for_resources(); - $this->prepare_factory_for_steps(); - $this->mapper->configure_evaluators( $this->custom_factories ); + $json_evaluators = array( + new DocBlockAnnotations(), + new NamespaceResolver(), + ); + $custom_factories = array_merge( + self::create_resource_factory(), + self::create_steps_factory() + ); + $this->mapper = new JsonMapper( $json_evaluators, $custom_factories ); } /** * Maps a parsed and validated JSON object to a Blueprint class instance. * - * @param $blueprint + * @param stdClass $blueprint a parsed and validated JSON object. * @return Blueprint */ - public function map( $blueprint ): Blueprint { - return $this->mapper->map_to_class( $blueprint, Blueprint::class ); + public function map( stdClass $blueprint ): Blueprint { + return $this->mapper->hydrate( $blueprint, Blueprint::class ); } - private function prepare_factory_for_resources() { + /** + * @return array{ResourceDefinitionInterface: Closure} + */ + private static function create_resource_factory(): array { $resource_map = array(); - foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resourceClass ) { - $resource_map[ $resourceClass::DISCRIMINATOR ] = $resourceClass; + foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resource_class ) { + $resource_map[ $resource_class::DISCRIMINATOR ] = $resource_class; } - $this->custom_factories['ResourceDefinitionInterface'] = - function ( $value ) use ( $resource_map ) { - if ( is_string( $value ) ) { - return $value; - } - if ( ! isset( $value->resource ) ) { - throw new JsonMapperException( 'Resource type must be defined' ); - } - if ( ! isset( $resource_map[ $value->resource ] ) ) { - throw new JsonMapperException( "Resource type {$value->resource} is not implemented" ); - } + return array( + 'ResourceDefinitionInterface' => + function ( $mapper, $value ) use ( $resource_map ) { + if ( is_string( $value ) ) { + return $value; + } + if ( ! isset( $value->resource ) ) { + throw new JsonMapperException( 'Resource type must be defined' ); + } + if ( ! isset( $resource_map[ $value->resource ] ) ) { + throw new JsonMapperException( "Resource type {$value->resource} is not implemented" ); + } - return $this->mapper->map_to_class( $value, $resource_map[ $value->resource ] ); - }; + return $mapper->hydrate( $value, $resource_map[ $value->resource ] ); + }, + ); } - private function prepare_factory_for_steps() { + + /** + * @return array{ResourceDefinitionInterface: Closure} + */ + private static function create_steps_factory(): array { $step_map = array(); foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { $step_map[ $class::DISCRIMINATOR ] = $class; } - $this->custom_factories['StepDefinitionInterface'] = - function ( $value ) use ( $step_map ) { - if ( ! isset( $value->step ) ) { - throw new JsonMapperException( 'Step must be defined' ); - } - if ( ! isset( $step_map[ $value->step ] ) ) { - throw new JsonMapperException( "Step {$value->step} is not implemented" ); - } + return array( + 'StepDefinitionInterface' => + function ( $mapper, $value ) use ( $step_map ) { + if ( ! isset( $value->step ) ) { + throw new JsonMapperException( 'Step must be defined' ); + } + if ( ! isset( $step_map[ $value->step ] ) ) { + throw new JsonMapperException( "Step {$value->step} is not implemented" ); + } - return $this->mapper->map_to_class( $value, $step_map[ $value->step ] ); - }; + return $mapper->hydrate( $value, $step_map[ $value->step ] ); + }, + ); } } diff --git a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php index fd19978e..0ff71c36 100644 --- a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php +++ b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php @@ -158,7 +158,7 @@ private function map_to_object( PropertyType $type, $value ) { throw new JsonMapperException( "Unable to resolve uninstantiable \'{$type->getType()}\'." ); } if ( false === is_array( $value ) ) { - return $this->mapper->map_to_class( $value, $type->getType() ); + return $this->mapper->hydrate( $value, $type->getType() ); } $mapped_objects = array(); foreach ( $value as $inner_value ) { @@ -208,7 +208,7 @@ private function has_factory( string $class_name ): bool { private function use_factory( string $class_name, $params ) { $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; - return $factory( $params ); + return $factory( $this->mapper, $params ); } private function add_factory( string $class_name, callable $factory ) { diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index e629f1b0..5544e9bd 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -3,27 +3,33 @@ namespace WordPress\JsonMapper; use WordPress\JsonMapper\Evaluators\DocBlockAnnotations; +use WordPress\JsonMapper\Evaluators\JsonEvaluatorInterface; use WordPress\JsonMapper\Evaluators\NamespaceResolver; use WordPress\JsonMapper\Evaluators\PropertyMapper; use WordPress\JsonMapper\Property\PropertyMap; class JsonMapper { - private $evaluators; - - public function __construct() { - $this->configure_evaluators(); - } - - public function configure_evaluators( array $custom_factories = null ) { - $this->evaluators = array( - new DocBlockAnnotations(), - new NamespaceResolver(), - new PropertyMapper( $this, $custom_factories ), // PropertyMapper has to be the last evaluator. - ); + /** + * @var JsonEvaluatorInterface[] + */ + private $evaluators = array(); + + /** + * @param array $evaluators + * @param array $custom_factories + */ + public function __construct( array $evaluators = null, array $custom_factories = null ) { + $this->evaluators[] = $evaluators; + $this->evaluators[] = new PropertyMapper( $this, $custom_factories ); } - public function map_to_class( \stdClass $json, string $class ) { - $object_wrapper = new ObjectWrapper( null, $class ); + /** + * @param \stdClass $json + * @param string $target + * @return object + */ + public function hydrate( \stdClass $json, string $target ) { + $object_wrapper = new ObjectWrapper( null, $target ); $property_map = new PropertyMap(); foreach ( $this->evaluators as $evaluator ) { diff --git a/tests/JsonMapper/JsonMapperTest.php b/tests/JsonMapper/JsonMapperTest.php index c8f8f71d..e7776f56 100644 --- a/tests/JsonMapper/JsonMapperTest.php +++ b/tests/JsonMapper/JsonMapperTest.php @@ -26,7 +26,7 @@ public function testMapsEmptyBlueprint() { $parsed_json = json_decode( $raw_json, false ); - $blueprint = $this->json_mapper->map_to_class( $parsed_json, Blueprint::class ); + $blueprint = $this->json_mapper->hydrate( $parsed_json, Blueprint::class ); $expected = BlueprintBuilder::create() ->toBlueprint(); From c63f5a3821849abe16c1232bb8ca753b0d016521 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 16:21:31 +0100 Subject: [PATCH 09/28] Pass mapper to factory only when it is required --- .../JsonMapper/Evaluators/PropertyMapper.php | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php index 0ff71c36..683aaed5 100644 --- a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php +++ b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php @@ -3,6 +3,7 @@ namespace WordPress\JsonMapper\Evaluators; use ReflectionClass; +use ReflectionException; use ReflectionMethod; use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\JsonMapperException; @@ -205,10 +206,18 @@ private function has_factory( string $class_name ): bool { return array_key_exists( $this->sanitise_class_name( $class_name ), $this->factories ); } - private function use_factory( string $class_name, $params ) { + /** + * @param string $class_name + * @param $params + * @return mixed + * @throws ReflectionException + */ + private function use_factory(string $class_name, $params ) { $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; - - return $factory( $this->mapper, $params ); + if (true === self::requires_json_mapper($factory)) { + return $factory( $this->mapper, $params ); + } + return $factory( $params ); } private function add_factory( string $class_name, callable $factory ) { @@ -250,4 +259,20 @@ function ( $value ) { } ); } + + /** + * @param callable $factory + * @return bool + * @throws ReflectionException + */ + public static function requires_json_mapper( callable $factory ): bool { + $reflection = new ReflectionMethod($factory); + $parameters = $reflection->getParameters(); + foreach ($parameters as $parameter) { + if ($parameter->getName() === 'mapper') { + return true; + } + } + return false; + } } From 457304f69218b46a9444b93ade019e99ae16bb79 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Tue, 12 Mar 2024 16:22:08 +0100 Subject: [PATCH 10/28] Clean up --- src/WordPress/JsonMapper/Evaluators/PropertyMapper.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php index 683aaed5..75fbed71 100644 --- a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php +++ b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php @@ -212,9 +212,9 @@ private function has_factory( string $class_name ): bool { * @return mixed * @throws ReflectionException */ - private function use_factory(string $class_name, $params ) { + private function use_factory( string $class_name, $params ) { $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; - if (true === self::requires_json_mapper($factory)) { + if ( true === self::requires_json_mapper( $factory ) ) { return $factory( $this->mapper, $params ); } return $factory( $params ); @@ -266,10 +266,10 @@ function ( $value ) { * @throws ReflectionException */ public static function requires_json_mapper( callable $factory ): bool { - $reflection = new ReflectionMethod($factory); + $reflection = new ReflectionMethod( $factory ); $parameters = $reflection->getParameters(); - foreach ($parameters as $parameter) { - if ($parameter->getName() === 'mapper') { + foreach ( $parameters as $parameter ) { + if ( $parameter->getName() === 'mapper' ) { return true; } } From 0eefbbd27839417080989ed2ef0df3c6962bce12 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 00:04:56 +0100 Subject: [PATCH 11/28] Drop PropertyMapper, Drop JsonEvaluatorInterface --- src/WordPress/Blueprints/BlueprintMapper.php | 69 ++--- .../Evaluators/DocBlockAnnotations.php | 12 +- .../Evaluators/JsonEvaluatorInterface.php | 17 -- .../Evaluators/NamespaceResolver.php | 9 +- .../JsonMapper/Evaluators/PropertyMapper.php | 278 ------------------ src/WordPress/JsonMapper/JsonMapper.php | 251 ++++++++++++++-- 6 files changed, 259 insertions(+), 377 deletions(-) delete mode 100644 src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php delete mode 100644 src/WordPress/JsonMapper/Evaluators/PropertyMapper.php diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index c182d2f7..10c63c44 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -2,7 +2,6 @@ namespace WordPress\Blueprints; -use Closure; use stdClass; use WordPress\Blueprints\Model\DataClass\Blueprint; use WordPress\Blueprints\Model\DataClass\ModelInfo; @@ -21,15 +20,15 @@ class BlueprintMapper { * */ public function __construct() { - $json_evaluators = array( + $property_mappers = array( new DocBlockAnnotations(), new NamespaceResolver(), ); - $custom_factories = array_merge( - self::create_resource_factory(), - self::create_steps_factory() + $custom_factories = array( + 'ResourceDefinitionInterface' => array( $this, 'resource_factory' ), + 'StepDefinitionInterface' => array( $this, 'step_factory' ), ); - $this->mapper = new JsonMapper( $json_evaluators, $custom_factories ); + $this->mapper = new JsonMapper( $property_mappers, $custom_factories ); } /** @@ -43,53 +42,47 @@ public function map( stdClass $blueprint ): Blueprint { } /** - * @return array{ResourceDefinitionInterface: Closure} + * @param $value + * @return object|string + * @throws JsonMapperException */ - private static function create_resource_factory(): array { + public function resource_factory( $value ) { $resource_map = array(); foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resource_class ) { $resource_map[ $resource_class::DISCRIMINATOR ] = $resource_class; } - return array( - 'ResourceDefinitionInterface' => - function ( $mapper, $value ) use ( $resource_map ) { - if ( is_string( $value ) ) { - return $value; - } - if ( ! isset( $value->resource ) ) { - throw new JsonMapperException( 'Resource type must be defined' ); - } - if ( ! isset( $resource_map[ $value->resource ] ) ) { - throw new JsonMapperException( "Resource type {$value->resource} is not implemented" ); - } + if ( is_string( $value ) ) { + return $value; + } + if ( ! isset( $value->resource ) ) { + throw new JsonMapperException( 'Resource type must be defined' ); + } + if ( ! isset( $resource_map[ $value->resource ] ) ) { + throw new JsonMapperException( "Resource type {$value->resource} is not implemented" ); + } - return $mapper->hydrate( $value, $resource_map[ $value->resource ] ); - }, - ); + return $this->mapper->hydrate( $value, $resource_map[ $value->resource ] ); } - /** - * @return array{ResourceDefinitionInterface: Closure} + * @param $value + * @return object + * @throws JsonMapperException */ - private static function create_steps_factory(): array { + public function step_factory( $value ) { $step_map = array(); foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { $step_map[ $class::DISCRIMINATOR ] = $class; } - return array( - 'StepDefinitionInterface' => - function ( $mapper, $value ) use ( $step_map ) { - if ( ! isset( $value->step ) ) { - throw new JsonMapperException( 'Step must be defined' ); - } - if ( ! isset( $step_map[ $value->step ] ) ) { - throw new JsonMapperException( "Step {$value->step} is not implemented" ); - } - return $mapper->hydrate( $value, $step_map[ $value->step ] ); - }, - ); + if ( ! isset( $value->step ) ) { + throw new JsonMapperException( 'Step must be defined' ); + } + if ( ! isset( $step_map[ $value->step ] ) ) { + throw new JsonMapperException( "Step {$value->step} is not implemented" ); + } + + return $this->mapper->hydrate( $value, $step_map[ $value->step ] ); } } diff --git a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php index d3572bbe..d61fb7ab 100644 --- a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php @@ -4,24 +4,18 @@ use ReflectionProperty; use WordPress\JsonMapper\ArrayInformation; -use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\ObjectWrapper; use WordPress\JsonMapper\Property\Property; use WordPress\JsonMapper\Property\PropertyMap; use WordPress\JsonMapper\Property\PropertyType; -class DocBlockAnnotations implements JsonEvaluatorInterface { +class DocBlockAnnotations { const DOC_BLOCK_REGEX = '/@(?P[A-Za-z_-]+)[ \t]+(?P[\w\[\]\\\\|]*).*$/m'; public function __construct() {} - public function evaluate( - \stdClass $json, - ObjectWrapper $object_wrapper, - PropertyMap $property_map, - JsonMapper $mapper - ) { + public function map_properties( ObjectWrapper $object_wrapper, PropertyMap $property_map ) { $property_map->merge( $this->compute_property_map( $object_wrapper ) ); } @@ -29,7 +23,7 @@ public function evaluate( * @param ObjectWrapper $object * @return PropertyMap */ - private function compute_property_map(ObjectWrapper $object ): PropertyMap { + private function compute_property_map( ObjectWrapper $object ): PropertyMap { $intermediate_property_map = new PropertyMap(); foreach ( self::get_properties( $object ) as $property ) { $doc_block = $property->getDocComment(); diff --git a/src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php b/src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php deleted file mode 100644 index 58ea6bad..00000000 --- a/src/WordPress/JsonMapper/Evaluators/JsonEvaluatorInterface.php +++ /dev/null @@ -1,17 +0,0 @@ -fetchPropertyMapForObject( $object_wrapper, $property_map ) as $property ) { $property_map->addProperty( $property ); } diff --git a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php b/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php deleted file mode 100644 index 75fbed71..00000000 --- a/src/WordPress/JsonMapper/Evaluators/PropertyMapper.php +++ /dev/null @@ -1,278 +0,0 @@ -mapper = $json_mapper; - $this->add_factories_for_native_php_classes(); - $this->add_factory_for_arrays(); - if ( null !== $custom_factories ) { - $this->add_custom_factories( $custom_factories ); - } - } - - public function evaluate( - \stdClass $json, - ObjectWrapper $object_wrapper, - PropertyMap $property_map, - JsonMapper $mapper - ) { - // If the type we are mapping has a last minute factory use it. - if ( $this->has_factory( $object_wrapper->getName() ) ) { - $result = $this->use_factory( $object_wrapper->getName(), $json ); - - $object_wrapper->setObject( $result ); - return; - } - - $values = (array) $json; - foreach ( $values as $key => $value ) { - if ( false === $property_map->has_property( $key ) ) { - continue; - } - - $property = $property_map->get_property( $key ); - - if ( false === $property->is_nullable() && null === $value ) { - throw new JsonMapperException( - "Null provided in json where \'{$object_wrapper->getName()}::{$key}\' doesn't allow null value" - ); - } - - if ( $property->is_nullable() && null === $value ) { - $this->set_value( $object_wrapper, $property, null ); - continue; - } - - $value = $this->map_value( $property, $value ); - $this->set_value( $object_wrapper, $property, $value ); - } - } - - private function map_value( Property $property, $value ) { - if ( null === $value && $property->is_nullable() ) { - return null; - } - // No match was found (or there was only one option) lets assume the first is the right one. - $types = $property->get_property_types(); - $type = \array_shift( $types ); - - if ( null === $type ) { - // Return the value as is as there is no type info. - return $value; - } - - if ( $this->is_valid_scalar_type( $type ) ) { - return $this->map_to_scalar( $type, $value ); - } - - if ( $this->has_factory( $type->getType() ) ) { - return $this->map_to_object_using_factory( $type, $value ); - } - - if ( ( class_exists( $type->getType() ) || interface_exists( $type->getType() ) ) ) { - return $this->map_to_object( $type, $value ); - } - - throw new JsonMapperException( "Unable to map to \'{$type->getType()}\'" ); - } - - /** - * @param PropertyType $type - * @return bool - */ - private function is_valid_scalar_type( PropertyType $type ): bool { - return in_array( $type->getType(), $this->scalar_types, true ); - } - - private function map_to_scalar( PropertyType $type, $value ) { - if ( false === is_array( $value ) ) { - return $this->cast_to_scalar_type( $type->getType(), $value ); - } - $mapped_scalars = array(); - foreach ( $value as $inner_value ) { - $mapped_scalars[] = $this->map_to_scalar( $type, $inner_value ); - } - return $mapped_scalars; - } - - private function cast_to_scalar_type( string $type, $value ) { - if ( 'string' === $type ) { - return (string) $value; - } - if ( 'boolean' === $type || 'bool' === $type ) { - return (bool) $value; - } - if ( 'integer' === $type || 'int' === $type ) { - return (int) $value; - } - if ( 'double' === $type || 'float' === $type ) { - return (float) $value; - } - - throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); - } - - private function map_to_object_using_factory( PropertyType $type, $value ) { - if ( false === is_array( $value ) ) { - return $this->use_factory( $type->getType(), $value ); - } - $mapped_objects = array(); - foreach ( $value as $inner_value ) { - $mapped_objects[] = $this->map_to_object_using_factory( $type, $inner_value ); - } - return $mapped_objects; - } - - private function map_to_object( PropertyType $type, $value ) { - if ( false === ( new ReflectionClass( $type->getType() ) )->isInstantiable() ) { - throw new JsonMapperException( "Unable to resolve uninstantiable \'{$type->getType()}\'." ); - } - if ( false === is_array( $value ) ) { - return $this->mapper->hydrate( $value, $type->getType() ); - } - $mapped_objects = array(); - foreach ( $value as $inner_value ) { - $mapped_objects[] = $this->map_to_object( $type, $inner_value ); - } - return $mapped_objects; - } - - private function set_value( ObjectWrapper $object, Property $property, $value ) { - if ( 'public' === $property->visibility ) { - $object->getObject()->{$property->get_name()} = $value; - return; - } - - $method_name = 'set' . \ucfirst( $property->get_name() ); - if ( \method_exists( $object->getObject(), $method_name ) ) { - $method = new ReflectionMethod( $object->getObject(), $method_name ); - $parameters = $method->getParameters(); - - if ( \is_array( $value ) && \count( $parameters ) === 1 && $parameters[0]->isVariadic() ) { - $object->getObject()->$method_name( ...$value ); - return; - } - - $object->getObject()->$method_name( $value ); - return; - } - - throw new JsonMapperException( - "{$object->getName()}::{$property->get_name()} is non-public and no setter method was found" - ); - } - - private function sanitise_class_name( string $class_name ): string { - /* Erase leading slash as ::class doesn't contain leading slash */ - if ( strpos( $class_name, '\\' ) === 0 ) { - $class_name = substr( $class_name, 1 ); - } - - return $class_name; - } - - private function has_factory( string $class_name ): bool { - return array_key_exists( $this->sanitise_class_name( $class_name ), $this->factories ); - } - - /** - * @param string $class_name - * @param $params - * @return mixed - * @throws ReflectionException - */ - private function use_factory( string $class_name, $params ) { - $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; - if ( true === self::requires_json_mapper( $factory ) ) { - return $factory( $this->mapper, $params ); - } - return $factory( $params ); - } - - private function add_factory( string $class_name, callable $factory ) { - $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; - } - - public function add_custom_factories( array $custom_factories ) { - foreach ( $custom_factories as $class_name => $custom_factory ) { - $this->add_factory( $class_name, $custom_factory ); - } - } - - private function add_factories_for_native_php_classes() { - $this->add_factory( - \DateTime::class, - static function ( string $value ) { - return new \DateTime( $value ); - } - ); - $this->add_factory( - \DateTimeImmutable::class, - static function ( string $value ) { - return new \DateTimeImmutable( $value ); - } - ); - $this->add_factory( - \stdClass::class, - static function ( $value ) { - return (object) $value; - } - ); - } - - private function add_factory_for_arrays() { - $this->add_factory( - \ArrayObject::class, - function ( $value ) { - return new \ArrayObject( $value ); - } - ); - } - - /** - * @param callable $factory - * @return bool - * @throws ReflectionException - */ - public static function requires_json_mapper( callable $factory ): bool { - $reflection = new ReflectionMethod( $factory ); - $parameters = $reflection->getParameters(); - foreach ( $parameters as $parameter ) { - if ( $parameter->getName() === 'mapper' ) { - return true; - } - } - return false; - } -} diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 5544e9bd..37026bed 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -2,45 +2,240 @@ namespace WordPress\JsonMapper; -use WordPress\JsonMapper\Evaluators\DocBlockAnnotations; -use WordPress\JsonMapper\Evaluators\JsonEvaluatorInterface; -use WordPress\JsonMapper\Evaluators\NamespaceResolver; -use WordPress\JsonMapper\Evaluators\PropertyMapper; +use stdClass; +use WordPress\JsonMapper\Property\Property; use WordPress\JsonMapper\Property\PropertyMap; +use WordPress\JsonMapper\Property\PropertyType; class JsonMapper { - /** - * @var JsonEvaluatorInterface[] - */ - private $evaluators = array(); + private $scalar_types = array( 'string', 'bool', 'boolean', 'int', 'integer', 'double', 'float' ); - /** - * @param array $evaluators - * @param array $custom_factories - */ - public function __construct( array $evaluators = null, array $custom_factories = null ) { - $this->evaluators[] = $evaluators; - $this->evaluators[] = new PropertyMapper( $this, $custom_factories ); + private $property_mappers; + + private $factories = array(); + + public function __construct( array $property_mappers = array(), array $custom_factories = array() ) { + $this->property_mappers = $property_mappers; + $this->add_factories_for_native_php_classes(); + $this->add_factory_for_arrays(); + $this->add_custom_factories( $custom_factories ); } - /** - * @param \stdClass $json - * @param string $target - * @return object - */ - public function hydrate( \stdClass $json, string $target ) { + public function hydrate( stdClass $json, string $target ) { $object_wrapper = new ObjectWrapper( null, $target ); $property_map = new PropertyMap(); - foreach ( $this->evaluators as $evaluator ) { - $evaluator->evaluate( - $json, - $object_wrapper, - $property_map, - $this - ); + foreach ( $this->property_mappers as $property_mapper ) { + $property_mapper->map_properties( $object_wrapper, $property_map ); } + $this->map_json_to_object( $json, $object_wrapper, $property_map ); + return $object_wrapper->getObject(); } + + public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper, PropertyMap $property_map ) { + // If the type we are mapping has a last minute factory use it. + if ( $this->has_factory( $object_wrapper->getName() ) ) { + $result = $this->use_factory( $object_wrapper->getName(), $json ); + + $object_wrapper->setObject( $result ); + return; + } + + $values = (array) $json; + foreach ( $values as $key => $value ) { + if ( false === $property_map->has_property( $key ) ) { + continue; + } + + $property = $property_map->get_property( $key ); + + if ( false === $property->is_nullable() && null === $value ) { + throw new JsonMapperException( + "Null provided in json where \'{$object_wrapper->getName()}::{$key}\' doesn't allow null value" + ); + } + + if ( $property->is_nullable() && null === $value ) { + $this->set_value( $object_wrapper, $property, null ); + continue; + } + + $value = $this->map_value( $property, $value ); + $this->set_value( $object_wrapper, $property, $value ); + } + } + + private function map_value( Property $property, $value ) { + if ( null === $value && $property->is_nullable() ) { + return null; + } + // No match was found (or there was only one option) lets assume the first is the right one. + $types = $property->get_property_types(); + $type = \array_shift( $types ); + + if ( null === $type ) { + // Return the value as is as there is no type info. + return $value; + } + + if ( $this->is_valid_scalar_type( $type ) ) { + return $this->map_to_scalar( $type, $value ); + } + + if ( $this->has_factory( $type->getType() ) ) { + return $this->map_to_object_using_factory( $type, $value ); + } + + if ( ( class_exists( $type->getType() ) || interface_exists( $type->getType() ) ) ) { + return $this->map_to_object( $type, $value ); + } + + throw new JsonMapperException( "Unable to map to \'{$type->getType()}\'" ); + } + + /** + * @param PropertyType $type + * @return bool + */ + private function is_valid_scalar_type( PropertyType $type ): bool { + return in_array( $type->getType(), $this->scalar_types, true ); + } + + private function map_to_scalar( PropertyType $type, $value ) { + if ( false === is_array( $value ) ) { + return $this->cast_to_scalar_type( $type->getType(), $value ); + } + $mapped_scalars = array(); + foreach ( $value as $inner_value ) { + $mapped_scalars[] = $this->map_to_scalar( $type, $inner_value ); + } + return $mapped_scalars; + } + + private function cast_to_scalar_type( string $type, $value ) { + if ( 'string' === $type ) { + return (string) $value; + } + if ( 'boolean' === $type || 'bool' === $type ) { + return (bool) $value; + } + if ( 'integer' === $type || 'int' === $type ) { + return (int) $value; + } + if ( 'double' === $type || 'float' === $type ) { + return (float) $value; + } + + throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); + } + + private function map_to_object_using_factory( PropertyType $type, $value ) { + if ( false === is_array( $value ) ) { + return $this->use_factory( $type->getType(), $value ); + } + $mapped_objects = array(); + foreach ( $value as $inner_value ) { + $mapped_objects[] = $this->map_to_object_using_factory( $type, $inner_value ); + } + return $mapped_objects; + } + + private function map_to_object( PropertyType $type, $value ) { + if ( false === ( new ReflectionClass( $type->getType() ) )->isInstantiable() ) { + throw new JsonMapperException( "Unable to resolve uninstantiable \'{$type->getType()}\'." ); + } + if ( false === is_array( $value ) ) { + return $this->mapper->hydrate( $value, $type->getType() ); + } + $mapped_objects = array(); + foreach ( $value as $inner_value ) { + $mapped_objects[] = $this->map_to_object( $type, $inner_value ); + } + return $mapped_objects; + } + + private function set_value( ObjectWrapper $object, Property $property, $value ) { + if ( 'public' === $property->visibility ) { + $object->getObject()->{$property->get_name()} = $value; + return; + } + + $method_name = 'set' . \ucfirst( $property->get_name() ); + if ( \method_exists( $object->getObject(), $method_name ) ) { + $method = new ReflectionMethod( $object->getObject(), $method_name ); + $parameters = $method->getParameters(); + + if ( \is_array( $value ) && \count( $parameters ) === 1 && $parameters[0]->isVariadic() ) { + $object->getObject()->$method_name( ...$value ); + return; + } + + $object->getObject()->$method_name( $value ); + return; + } + + throw new JsonMapperException( + "{$object->getName()}::{$property->get_name()} is non-public and no setter method was found" + ); + } + + private function sanitise_class_name( string $class_name ): string { + /* Erase leading slash as ::class doesn't contain leading slash */ + if ( strpos( $class_name, '\\' ) === 0 ) { + $class_name = substr( $class_name, 1 ); + } + + return $class_name; + } + + private function has_factory( string $class_name ): bool { + return array_key_exists( $this->sanitise_class_name( $class_name ), $this->factories ); + } + + private function use_factory( string $class_name, $params ) { + $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; + return $factory( $params ); + } + + private function add_factory( string $class_name, callable $factory ) { + $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; + } + + public function add_custom_factories( array $custom_factories ) { + foreach ( $custom_factories as $class_name => $custom_factory ) { + $this->add_factory( $class_name, $custom_factory ); + } + } + + private function add_factories_for_native_php_classes() { + $this->add_factory( + \DateTime::class, + static function ( string $value ) { + return new \DateTime( $value ); + } + ); + $this->add_factory( + \DateTimeImmutable::class, + static function ( string $value ) { + return new \DateTimeImmutable( $value ); + } + ); + $this->add_factory( + stdClass::class, + static function ( $value ) { + return (object) $value; + } + ); + } + + private function add_factory_for_arrays() { + $this->add_factory( + \ArrayObject::class, + function ( $value ) { + return new \ArrayObject( $value ); + } + ); + } } From 51610a432afc0d7b7a04f4bf755b73a1cc732585 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 12:02:44 +0100 Subject: [PATCH 12/28] Drop PropertyBuilder --- src/WordPress/Blueprints/BlueprintMapper.php | 4 +- src/WordPress/JsonMapper/ArrayInformation.php | 21 +------ src/WordPress/JsonMapper/JsonMapper.php | 25 +++++--- .../DocBlockAnnotations.php | 7 +-- .../NamespaceResolver.php | 13 ++--- .../JsonMapper/Property/Property.php | 39 +------------ .../JsonMapper/Property/PropertyBuilder.php | 57 ------------------- .../JsonMapper/Property/PropertyMap.php | 28 ++++----- .../Property/PropertyMapperInterface.php | 9 +++ .../JsonMapper/Property/PropertyType.php | 12 ---- 10 files changed, 50 insertions(+), 165 deletions(-) rename src/WordPress/JsonMapper/{Evaluators => Property}/DocBlockAnnotations.php (94%) rename src/WordPress/JsonMapper/{Evaluators => Property}/NamespaceResolver.php (91%) delete mode 100644 src/WordPress/JsonMapper/Property/PropertyBuilder.php create mode 100644 src/WordPress/JsonMapper/Property/PropertyMapperInterface.php diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index 10c63c44..e4d6befb 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -5,10 +5,10 @@ use stdClass; use WordPress\Blueprints\Model\DataClass\Blueprint; use WordPress\Blueprints\Model\DataClass\ModelInfo; -use WordPress\JsonMapper\Evaluators\DocBlockAnnotations; -use WordPress\JsonMapper\Evaluators\NamespaceResolver; use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\JsonMapperException; +use WordPress\JsonMapper\Property\DocBlockAnnotations; +use WordPress\JsonMapper\Property\NamespaceResolver; class BlueprintMapper { /** diff --git a/src/WordPress/JsonMapper/ArrayInformation.php b/src/WordPress/JsonMapper/ArrayInformation.php index 9fba9a74..4b45cbfa 100644 --- a/src/WordPress/JsonMapper/ArrayInformation.php +++ b/src/WordPress/JsonMapper/ArrayInformation.php @@ -2,7 +2,7 @@ namespace WordPress\JsonMapper; -class ArrayInformation implements \JsonSerializable +class ArrayInformation { /** @var bool */ private $isArray; @@ -29,23 +29,4 @@ public static function multiDimension(int $dimension): self { return new self(true, $dimension); } - - public function isArray(): bool - { - return $this->isArray; - } - - public function jsonSerialize(): array - { - return [ - 'isArray' => $this->isArray, - 'dimensions' => $this->dimensions - ]; - } - - public function equals(self $other): bool - { - return $this->isArray === $other->isArray - && $this->dimensions === $other->dimensions; - } } diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 37026bed..c1d9a851 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -5,13 +5,20 @@ use stdClass; use WordPress\JsonMapper\Property\Property; use WordPress\JsonMapper\Property\PropertyMap; +use WordPress\JsonMapper\Property\PropertyMapperInterface; use WordPress\JsonMapper\Property\PropertyType; class JsonMapper { private $scalar_types = array( 'string', 'bool', 'boolean', 'int', 'integer', 'double', 'float' ); + /** + * @var PropertyMapperInterface[] + */ private $property_mappers; + /** + * @var array{$class_name} + */ private $factories = array(); public function __construct( array $property_mappers = array(), array $custom_factories = array() ) { @@ -25,7 +32,9 @@ public function hydrate( stdClass $json, string $target ) { $object_wrapper = new ObjectWrapper( null, $target ); $property_map = new PropertyMap(); - foreach ( $this->property_mappers as $property_mapper ) { + + /** @var PropertyMapperInterface $property_mapper */ + foreach ($this->property_mappers as $property_mapper ) { $property_mapper->map_properties( $object_wrapper, $property_map ); } @@ -51,13 +60,13 @@ public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper $property = $property_map->get_property( $key ); - if ( false === $property->is_nullable() && null === $value ) { + if ( false === $property->is_nullable && null === $value ) { throw new JsonMapperException( "Null provided in json where \'{$object_wrapper->getName()}::{$key}\' doesn't allow null value" ); } - if ( $property->is_nullable() && null === $value ) { + if ( $property->is_nullable && null === $value ) { $this->set_value( $object_wrapper, $property, null ); continue; } @@ -68,11 +77,11 @@ public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper } private function map_value( Property $property, $value ) { - if ( null === $value && $property->is_nullable() ) { + if ( null === $value && $property->is_nullable ) { return null; } // No match was found (or there was only one option) lets assume the first is the right one. - $types = $property->get_property_types(); + $types = $property->property_types; $type = \array_shift( $types ); if ( null === $type ) { @@ -158,11 +167,11 @@ private function map_to_object( PropertyType $type, $value ) { private function set_value( ObjectWrapper $object, Property $property, $value ) { if ( 'public' === $property->visibility ) { - $object->getObject()->{$property->get_name()} = $value; + $object->getObject()->{$property->name} = $value; return; } - $method_name = 'set' . \ucfirst( $property->get_name() ); + $method_name = 'set' . \ucfirst( $property->name ); if ( \method_exists( $object->getObject(), $method_name ) ) { $method = new ReflectionMethod( $object->getObject(), $method_name ); $parameters = $method->getParameters(); @@ -177,7 +186,7 @@ private function set_value( ObjectWrapper $object, Property $property, $value ) } throw new JsonMapperException( - "{$object->getName()}::{$property->get_name()} is non-public and no setter method was found" + "{$object->getName()}::{$property->name} is non-public and no setter method was found" ); } diff --git a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php similarity index 94% rename from src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php rename to src/WordPress/JsonMapper/Property/DocBlockAnnotations.php index d61fb7ab..fdc69167 100644 --- a/src/WordPress/JsonMapper/Evaluators/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php @@ -1,15 +1,12 @@ [A-Za-z_-]+)[ \t]+(?P[\w\[\]\\\\|]*).*$/m'; diff --git a/src/WordPress/JsonMapper/Evaluators/NamespaceResolver.php b/src/WordPress/JsonMapper/Property/NamespaceResolver.php similarity index 91% rename from src/WordPress/JsonMapper/Evaluators/NamespaceResolver.php rename to src/WordPress/JsonMapper/Property/NamespaceResolver.php index c1eed2b3..74ed0c0e 100644 --- a/src/WordPress/JsonMapper/Evaluators/NamespaceResolver.php +++ b/src/WordPress/JsonMapper/Property/NamespaceResolver.php @@ -1,18 +1,14 @@ get_property_types(); + $types = $property->property_types; foreach ( $types as $index => $type ) { $types[ $index ] = $this->resolveSingleType( $type, $object, $imports ); } - $intermediatePropertyMap->addProperty( $property->as_builder()->setTypes( ...$types )->build() ); + $property->property_types = $types; + $intermediatePropertyMap->addProperty( $property ); } return $intermediatePropertyMap; diff --git a/src/WordPress/JsonMapper/Property/Property.php b/src/WordPress/JsonMapper/Property/Property.php index 292d2d8a..6443e986 100644 --- a/src/WordPress/JsonMapper/Property/Property.php +++ b/src/WordPress/JsonMapper/Property/Property.php @@ -2,11 +2,11 @@ namespace WordPress\JsonMapper\Property; -class Property implements \JsonSerializable { +class Property { /** @var string */ public $name; - /** @var PropertyType[] */ + /** @var string[] */ public $property_types; /** @var string */ @@ -26,39 +26,4 @@ public function __construct( $this->is_nullable = $is_nullable; $this->property_types = $types; } - - public function get_name(): string { - return $this->name; - } - - /** @return PropertyType[] */ - public function get_property_types(): array { - return $this->property_types; - } - - public function get_visibility(): string { - return $this->visibility; - } - - public function is_nullable(): bool { - return $this->is_nullable; - } - - public function as_builder(): PropertyBuilder { - return PropertyBuilder::new() - ->setName( $this->name ) - ->setTypes( ...$this->property_types ) - ->setIsNullable( $this->is_nullable() ) - ->setVisibility( $this->visibility ); - } - - // phpcs:ignore - public function jsonSerialize(): array { - return array( - 'name' => $this->name, - 'types' => $this->property_types, - 'visibility' => $this->visibility, - 'isNullable' => $this->is_nullable, - ); - } } diff --git a/src/WordPress/JsonMapper/Property/PropertyBuilder.php b/src/WordPress/JsonMapper/Property/PropertyBuilder.php deleted file mode 100644 index 9d004263..00000000 --- a/src/WordPress/JsonMapper/Property/PropertyBuilder.php +++ /dev/null @@ -1,57 +0,0 @@ -name, - $this->visibility, - $this->isNullable, - ...$this->types - ); - } - - public function setName( string $name ): self { - $this->name = $name; - return $this; - } - - public function setTypes( PropertyType ...$types ): self { - $this->types = $types; - return $this; - } - - public function addType( string $type, ArrayInformation $arrayInformation ): self { - $this->types[] = new PropertyType( $type, $arrayInformation ); - return $this; - } - - public function setIsNullable( bool $isNullable ): self { - $this->isNullable = $isNullable; - return $this; - } - - public function setVisibility( string $visibility ): self { - $this->visibility = $visibility; - return $this; - } -} diff --git a/src/WordPress/JsonMapper/Property/PropertyMap.php b/src/WordPress/JsonMapper/Property/PropertyMap.php index 2d8040d5..f0faaaea 100644 --- a/src/WordPress/JsonMapper/Property/PropertyMap.php +++ b/src/WordPress/JsonMapper/Property/PropertyMap.php @@ -15,8 +15,8 @@ class PropertyMap implements \IteratorAggregate, \JsonSerializable { public function __construct() {} public function addProperty( Property $property ) { - $this->map[ $property->get_name() ] = $property; - $this->iterator = null; + $this->map[ $property->name ] = $property; + $this->iterator = null; } public function has_property( string $name ): bool { @@ -32,26 +32,22 @@ public function get_property( string $key ): Property { } public function merge( self $other ) { - /** @var Property $property */ - foreach ( $other as $property ) { - if ( ! $this->has_property( $property->get_name() ) ) { - $this->addProperty( $property ); + /** @var Property $other_property */ + foreach ( $other as $other_property ) { + $other_name = $other_property->name; + if ( false === $this->has_property( $other_name ) ) { + $this->addProperty( $other_property ); continue; } - if ( $property == $this->get_property( $property->get_name() ) ) { + if ( $other_property === $this->get_property( $other_name ) ) { continue; } - $current = $this->get_property( $property->get_name() ); - $builder = $current->as_builder(); - - $builder->setIsNullable( $current->is_nullable() || $property->is_nullable() ); - foreach ( $property->get_property_types() as $propertyType ) { - $builder->addType( $propertyType->getType(), $propertyType->getArrayInformation() ); - } - - $this->addProperty( $builder->build() ); + $property = $this->get_property( $other_name ); + $property->is_nullable = $property->is_nullable || $other_property->is_nullable; + $property->property_types = array_merge( $property->property_types, $other_property->property_types ); + $this->addProperty( $property ); } $this->iterator = null; } diff --git a/src/WordPress/JsonMapper/Property/PropertyMapperInterface.php b/src/WordPress/JsonMapper/Property/PropertyMapperInterface.php new file mode 100644 index 00000000..f2d2decd --- /dev/null +++ b/src/WordPress/JsonMapper/Property/PropertyMapperInterface.php @@ -0,0 +1,9 @@ +type; } - public function isArray(): bool { - return $this->arrayInformation->isArray(); - } - public function getArrayInformation(): ArrayInformation { return $this->arrayInformation; } - - public function jsonSerialize(): array { - return array( - 'type' => $this->type, - 'isArray' => $this->arrayInformation->isArray(), - 'arrayInformation' => $this->arrayInformation, - ); - } } From aaf5f01a4fc5e7e2b1bc482eadb23cbc880018b3 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 13:44:29 +0100 Subject: [PATCH 13/28] Drop ArrayInformation, PropertyType --- src/WordPress/JsonMapper/ArrayInformation.php | 32 ------------- src/WordPress/JsonMapper/JsonMapper.php | 46 +++++++++---------- .../Property/DocBlockAnnotations.php | 7 ++- .../JsonMapper/Property/NamespaceResolver.php | 33 ++++++------- .../JsonMapper/Property/Property.php | 2 +- .../JsonMapper/Property/PropertyType.php | 26 ----------- 6 files changed, 42 insertions(+), 104 deletions(-) delete mode 100644 src/WordPress/JsonMapper/ArrayInformation.php delete mode 100644 src/WordPress/JsonMapper/Property/PropertyType.php diff --git a/src/WordPress/JsonMapper/ArrayInformation.php b/src/WordPress/JsonMapper/ArrayInformation.php deleted file mode 100644 index 4b45cbfa..00000000 --- a/src/WordPress/JsonMapper/ArrayInformation.php +++ /dev/null @@ -1,32 +0,0 @@ -isArray = $isArray; - $this->dimensions = $dimensions; - } - - public static function notAnArray(): self - { - return new self(false, 0); - } - - public static function singleDimension(): self - { - return new self(true, 1); - } - - public static function multiDimension(int $dimension): self - { - return new self(true, $dimension); - } -} diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index c1d9a851..54eb3153 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -82,43 +82,43 @@ private function map_value( Property $property, $value ) { } // No match was found (or there was only one option) lets assume the first is the right one. $types = $property->property_types; - $type = \array_shift( $types ); + $property_type = \array_shift( $types ); - if ( null === $type ) { + if ( null === $property_type ) { // Return the value as is as there is no type info. return $value; } - if ( $this->is_valid_scalar_type( $type ) ) { - return $this->map_to_scalar( $type, $value ); + if ( $this->is_valid_scalar_type( $property_type ) ) { + return $this->map_to_scalar( $property_type, $value ); } - if ( $this->has_factory( $type->getType() ) ) { - return $this->map_to_object_using_factory( $type, $value ); + if ( $this->has_factory( $property_type ) ) { + return $this->map_to_object_using_factory( $property_type, $value ); } - if ( ( class_exists( $type->getType() ) || interface_exists( $type->getType() ) ) ) { - return $this->map_to_object( $type, $value ); + if ( ( class_exists( $property_type ) || interface_exists( $property_type ) ) ) { + return $this->map_to_object( $property_type, $value ); } - throw new JsonMapperException( "Unable to map to \'{$type->getType()}\'" ); + throw new JsonMapperException( "Unable to map to \'{$property_type}\'" ); } /** - * @param PropertyType $type + * @param string $property_type * @return bool */ - private function is_valid_scalar_type( PropertyType $type ): bool { - return in_array( $type->getType(), $this->scalar_types, true ); + private function is_valid_scalar_type( string $property_type ): bool { + return in_array( $property_type, $this->scalar_types, true ); } - private function map_to_scalar( PropertyType $type, $value ) { + private function map_to_scalar(string $property_type, $value ) { if ( false === is_array( $value ) ) { - return $this->cast_to_scalar_type( $type->getType(), $value ); + return $this->cast_to_scalar_type( $property_type, $value ); } $mapped_scalars = array(); foreach ( $value as $inner_value ) { - $mapped_scalars[] = $this->map_to_scalar( $type, $inner_value ); + $mapped_scalars[] = $this->map_to_scalar( $property_type, $inner_value ); } return $mapped_scalars; } @@ -140,27 +140,27 @@ private function cast_to_scalar_type( string $type, $value ) { throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); } - private function map_to_object_using_factory( PropertyType $type, $value ) { + private function map_to_object_using_factory(string $property_type, $value ) { if ( false === is_array( $value ) ) { - return $this->use_factory( $type->getType(), $value ); + return $this->use_factory( $property_type, $value ); } $mapped_objects = array(); foreach ( $value as $inner_value ) { - $mapped_objects[] = $this->map_to_object_using_factory( $type, $inner_value ); + $mapped_objects[] = $this->map_to_object_using_factory( $property_type, $inner_value ); } return $mapped_objects; } - private function map_to_object( PropertyType $type, $value ) { - if ( false === ( new ReflectionClass( $type->getType() ) )->isInstantiable() ) { - throw new JsonMapperException( "Unable to resolve uninstantiable \'{$type->getType()}\'." ); + private function map_to_object(string $property_type, $value ) { + if ( false === ( new ReflectionClass( $property_type ) )->isInstantiable() ) { + throw new JsonMapperException( "Unable to resolve uninstantiable \'{$property_type}\'." ); } if ( false === is_array( $value ) ) { - return $this->mapper->hydrate( $value, $type->getType() ); + return $this->hydrate( $value, $property_type ); } $mapped_objects = array(); foreach ( $value as $inner_value ) { - $mapped_objects[] = $this->map_to_object( $type, $inner_value ); + $mapped_objects[] = $this->map_to_object( $property_type, $inner_value ); } return $mapped_objects; } diff --git a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php index fdc69167..9d24e57a 100644 --- a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php @@ -51,7 +51,7 @@ static function ( string $type ) { /* A union type that has one of its types defined as array is to complex to understand */ if ( in_array( 'array', $types, true ) ) { - $property->property_types[] = new PropertyType( 'mixed', ArrayInformation::singleDimension() ); + $property->property_types[] = 'mixed'; $intermediate_property_map->addProperty( $property ); continue; } @@ -61,18 +61,17 @@ static function ( string $type ) { $is_array = substr( $type, -2 ) === '[]'; if ( ! $is_array ) { - $property->property_types[] = new PropertyType( $type, ArrayInformation::notAnArray() ); + $property->property_types[] = $type; continue; } $first_bracket_index = strpos( $type, '[' ); - $dimensions = substr_count( $type, '[]' ); if ( false !== $first_bracket_index ) { $type = substr( $type, 0, $first_bracket_index ); } - $property->property_types[] = new PropertyType( $type, ArrayInformation::multiDimension( $dimensions ) ); + $property->property_types[] = $type; } $intermediate_property_map->addProperty( $property ); diff --git a/src/WordPress/JsonMapper/Property/NamespaceResolver.php b/src/WordPress/JsonMapper/Property/NamespaceResolver.php index 74ed0c0e..9ff53b5b 100644 --- a/src/WordPress/JsonMapper/Property/NamespaceResolver.php +++ b/src/WordPress/JsonMapper/Property/NamespaceResolver.php @@ -90,16 +90,16 @@ private static function getImportsForFileName( string $filename ): array { /** @param Import[] $imports */ - private function resolveSingleType( PropertyType $type, ObjectWrapper $object, array $imports ): PropertyType { - if ( $this->is_valid_scalar_type( $type ) ) { - return $type; + private function resolveSingleType(string $property_type, ObjectWrapper $object, array $imports ): string { + if ( $this->is_valid_scalar_type( $property_type ) ) { + return $property_type; } - $pos = strpos( $type->getType(), '\\' ); + $pos = strpos( $property_type, '\\' ); if ( $pos === false ) { - $pos = strlen( $type->getType() ); + $pos = strlen( $property_type ); } - $nameSpacedFirstChunk = '\\' . substr( $type->getType(), 0, $pos ); + $nameSpacedFirstChunk = '\\' . substr( $property_type, 0, $pos ); $matches = \array_filter( $imports, @@ -115,23 +115,20 @@ static function ( Import $import ) use ( $nameSpacedFirstChunk ) { if ( count( $matches ) > 0 ) { $match = \array_shift( $matches ); if ( $match->hasAlias() ) { - $strippedType = \substr( $type->getType(), strlen( $nameSpacedFirstChunk ) ); + $strippedType = \substr( $property_type, strlen( $nameSpacedFirstChunk ) ); $fullyQualifiedType = $match->getImport() . '\\' . $strippedType; } else { $strippedMatch = \substr( $match->getImport(), 0, -strlen( $nameSpacedFirstChunk ) ); - $fullyQualifiedType = $strippedMatch . '\\' . $type->getType(); + $fullyQualifiedType = $strippedMatch . '\\' . $property_type; } - return new PropertyType( rtrim( $fullyQualifiedType, '\\' ), $type->getArrayInformation() ); + return rtrim( $fullyQualifiedType, '\\' ); } $reflectedObject = $object->getReflectedObject(); while ( true ) { - if ( class_exists( $reflectedObject->getNamespaceName() . '\\' . $type->getType() ) ) { - return new PropertyType( - $reflectedObject->getNamespaceName() . '\\' . $type->getType(), - $type->getArrayInformation() - ); + if ( class_exists( $reflectedObject->getNamespaceName() . '\\' . $property_type ) ) { + return $reflectedObject->getNamespaceName() . '\\' . $property_type; } $reflectedObject = $reflectedObject->getParentClass(); @@ -140,14 +137,14 @@ static function ( Import $import ) use ( $nameSpacedFirstChunk ) { } } - return $type; + return $property_type; } /** - * @param PropertyType $type + * @param string $property_type * @return bool */ - private function is_valid_scalar_type( PropertyType $type ): bool { - return in_array( $type->getType(), $this->scalar_types, true ); + private function is_valid_scalar_type( string $property_type ): bool { + return in_array( $property_type, $this->scalar_types, true ); } } diff --git a/src/WordPress/JsonMapper/Property/Property.php b/src/WordPress/JsonMapper/Property/Property.php index 6443e986..a589969c 100644 --- a/src/WordPress/JsonMapper/Property/Property.php +++ b/src/WordPress/JsonMapper/Property/Property.php @@ -19,7 +19,7 @@ public function __construct( string $name, string $visibility, bool $is_nullable, - PropertyType ...$types + array $types = array() ) { $this->name = $name; $this->visibility = $visibility; diff --git a/src/WordPress/JsonMapper/Property/PropertyType.php b/src/WordPress/JsonMapper/Property/PropertyType.php deleted file mode 100644 index 30e6822c..00000000 --- a/src/WordPress/JsonMapper/Property/PropertyType.php +++ /dev/null @@ -1,26 +0,0 @@ -type = $type; - $this->arrayInformation = $isArray; - } - - public function getType(): string { - return $this->type; - } - - public function getArrayInformation(): ArrayInformation { - return $this->arrayInformation; - } -} From 328d1a5b9188168a8d0e6086a632ba757a073409 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 14:37:24 +0100 Subject: [PATCH 14/28] Clean up --- src/WordPress/JsonMapper/JsonMapper.php | 8 +------- .../JsonMapper/Property/DocBlockAnnotations.php | 3 ++- src/WordPress/JsonMapper/Property/PropertyMap.php | 9 +-------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 54eb3153..9042eb9f 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -2,11 +2,11 @@ namespace WordPress\JsonMapper; +use ReflectionClass; use stdClass; use WordPress\JsonMapper\Property\Property; use WordPress\JsonMapper\Property\PropertyMap; use WordPress\JsonMapper\Property\PropertyMapperInterface; -use WordPress\JsonMapper\Property\PropertyType; class JsonMapper { private $scalar_types = array( 'string', 'bool', 'boolean', 'int', 'integer', 'double', 'float' ); @@ -24,7 +24,6 @@ class JsonMapper { public function __construct( array $property_mappers = array(), array $custom_factories = array() ) { $this->property_mappers = $property_mappers; $this->add_factories_for_native_php_classes(); - $this->add_factory_for_arrays(); $this->add_custom_factories( $custom_factories ); } @@ -32,7 +31,6 @@ public function hydrate( stdClass $json, string $target ) { $object_wrapper = new ObjectWrapper( null, $target ); $property_map = new PropertyMap(); - /** @var PropertyMapperInterface $property_mapper */ foreach ($this->property_mappers as $property_mapper ) { $property_mapper->map_properties( $object_wrapper, $property_map ); @@ -44,7 +42,6 @@ public function hydrate( stdClass $json, string $target ) { } public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper, PropertyMap $property_map ) { - // If the type we are mapping has a last minute factory use it. if ( $this->has_factory( $object_wrapper->getName() ) ) { $result = $this->use_factory( $object_wrapper->getName(), $json ); @@ -237,9 +234,6 @@ static function ( $value ) { return (object) $value; } ); - } - - private function add_factory_for_arrays() { $this->add_factory( \ArrayObject::class, function ( $value ) { diff --git a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php index 9d24e57a..ff16d21a 100644 --- a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php @@ -2,6 +2,7 @@ namespace WordPress\JsonMapper\Property; +use ReflectionClass; use ReflectionProperty; use WordPress\JsonMapper\ArrayInformation; use WordPress\JsonMapper\ObjectWrapper; @@ -126,7 +127,7 @@ private static function parse_var( string $doc_block ): string { */ private static function get_properties( ObjectWrapper $object ): array { $properties = array(); - $reflection_class = $object->getReflectedObject(); + $reflection_class = new ReflectionClass( $object->getObject() ); do { $properties = array_merge( $properties, $reflection_class->getProperties() ); } while ( $reflection_class = $reflection_class->getParentClass() ); diff --git a/src/WordPress/JsonMapper/Property/PropertyMap.php b/src/WordPress/JsonMapper/Property/PropertyMap.php index f0faaaea..8f2063e8 100644 --- a/src/WordPress/JsonMapper/Property/PropertyMap.php +++ b/src/WordPress/JsonMapper/Property/PropertyMap.php @@ -5,7 +5,7 @@ use ArrayIterator; use function array_key_exists; -class PropertyMap implements \IteratorAggregate, \JsonSerializable { +class PropertyMap implements \IteratorAggregate { /** @var Property[] */ private $map = array(); @@ -61,13 +61,6 @@ public function getIterator(): ArrayIterator { return $this->iterator; } - // phpcs:ignore - public function jsonSerialize(): array { - return array( - 'properties' => $this->map, - ); - } - public function toString(): string { return (string) \json_encode( $this ); } From 91ae08b851bd3b655d134466c5ebd80f6b94f2fe Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 15:41:37 +0100 Subject: [PATCH 15/28] Add tests --- src/WordPress/Blueprints/BlueprintMapper.php | 3 +- .../Blueprints/Model/BlueprintBuilder.php | 14 --- .../Blueprints/Model/DataClass/Blueprint.php | 8 +- .../Property/DocBlockAnnotations.php | 1 - tests/Blueprints/BlueprintMapperTest.php | 117 ++++++++++++------ 5 files changed, 88 insertions(+), 55 deletions(-) diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index e4d6befb..6c92580f 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -21,8 +21,9 @@ class BlueprintMapper { */ public function __construct() { $property_mappers = array( - new DocBlockAnnotations(), new NamespaceResolver(), + new DocBlockAnnotations(), + ); $custom_factories = array( 'ResourceDefinitionInterface' => array( $this, 'resource_factory' ), diff --git a/src/WordPress/Blueprints/Model/BlueprintBuilder.php b/src/WordPress/Blueprints/Model/BlueprintBuilder.php index 70c93728..f6e8b92a 100644 --- a/src/WordPress/Blueprints/Model/BlueprintBuilder.php +++ b/src/WordPress/Blueprints/Model/BlueprintBuilder.php @@ -133,20 +133,6 @@ public function withFile( $path, $data ) { ); } - public function remove( $path ) { - return $this->addStep( - ( new RmStep() ) - ->setPath( $path ) - ); - } - - public function makeDirectory( $path ) { - return $this->addStep( - ( new MkdirStep() ) - ->setPath( $path ) - ); - } - public function downloadWordPress( $wpZip = null ) { $this->prependStep( ( new DownloadWordPressStep() ) diff --git a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php index 1abda4aa..f6bda88a 100644 --- a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php +++ b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php @@ -2,6 +2,8 @@ namespace WordPress\Blueprints\Model\DataClass; +use ArrayObject; + class Blueprint { /** @@ -18,7 +20,7 @@ class Blueprint /** * Slot for runtime–specific options, schema must be provided by the runtime. - * @var \ArrayObject + * @var ArrayObject */ public $runtime; @@ -27,7 +29,7 @@ class Blueprint /** * PHP Constants to define on every request - * @var \ArrayObject + * @var ArrayObject */ public $constants; @@ -39,7 +41,7 @@ class Blueprint /** * WordPress site options to define - * @var \ArrayObject + * @var ArrayObject */ public $siteOptions; diff --git a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php index ff16d21a..90e70909 100644 --- a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php @@ -4,7 +4,6 @@ use ReflectionClass; use ReflectionProperty; -use WordPress\JsonMapper\ArrayInformation; use WordPress\JsonMapper\ObjectWrapper; class DocBlockAnnotations implements PropertyMapperInterface { diff --git a/tests/Blueprints/BlueprintMapperTest.php b/tests/Blueprints/BlueprintMapperTest.php index 4a320579..30eccb4f 100644 --- a/tests/Blueprints/BlueprintMapperTest.php +++ b/tests/Blueprints/BlueprintMapperTest.php @@ -2,11 +2,13 @@ namespace Blueprints; +use ArrayObject; use WordPress\Blueprints\BlueprintMapper; use PHPUnit\Framework\TestCase; use WordPress\Blueprints\Model\BlueprintBuilder; use WordPress\Blueprints\Model\DataClass\Blueprint; -use WordPress\JsonMapper\JsonMapper; +use WordPress\Blueprints\Model\DataClass\MkdirStep; +use WordPress\Blueprints\Model\DataClass\RmStep; class BlueprintMapperTest extends TestCase { @@ -23,37 +25,35 @@ public function before() { } public function testMapsEmptyBlueprint() { - $raw_json = '{}'; + $raw_blueprint = '{}'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_blueprint, false ); - $blueprint = $this->blueprint_mapper->map( $parsed_json ); + $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = BlueprintBuilder::create() - ->toBlueprint(); + $expected = new Blueprint(); - $this->assertEquals( $expected, $blueprint ); + $this->assertEquals( $expected, $result ); } public function testMapsWordPressVersion() { - $raw_json = + $raw_blueprint = '{ "WordPressVersion":"https://wordpress.org/latest.zip" }'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_blueprint, false ); - $blueprint = $this->blueprint_mapper->map( $parsed_json ); + $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = BlueprintBuilder::create() - ->withWordPressVersion( 'https://wordpress.org/latest.zip' ) - ->toBlueprint(); + $expected = new Blueprint(); + $expected->WordPressVersion = 'https://wordpress.org/latest.zip'; - $this->assertEquals( $expected, $blueprint ); + $this->assertEquals( $expected, $result ); } public function testMapsMultiplePlugins() { - $raw_json = + $raw_blueprint = '{ "plugins": [ @@ -63,25 +63,22 @@ public function testMapsMultiplePlugins() { ] }'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_blueprint, false ); - $blueprint = $this->blueprint_mapper->map( $parsed_json ); + $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = BlueprintBuilder::create() - ->withPlugins( - array( - 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', - 'https://downloads.wordpress.org/plugin/hello-dolly.zip', - 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', - ) - ) - ->toBlueprint(); + $expected = new Blueprint(); + $expected->plugins = array( + 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', + 'https://downloads.wordpress.org/plugin/hello-dolly.zip', + 'https://downloads.wordpress.org/plugin/gutenberg.17.7.0.zip', + ); - $this->assertEquals( $expected, $blueprint ); + $this->assertEquals( $expected, $result ); } public function testMapsWhenSpecificStepAppearsTwice() { - $raw_json = + $raw_blueprint = '{ "steps": [ @@ -91,16 +88,64 @@ public function testMapsWhenSpecificStepAppearsTwice() { ] }'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_blueprint, false ); - $blueprint = $this->blueprint_mapper->map( $parsed_json ); + $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = BlueprintBuilder::create() - ->makeDirectory( 'dir1' ) - ->remove( 'dir1' ) - ->makeDirectory( 'dir2' ) - ->toBlueprint(); + $expected = new Blueprint(); + $expected->steps = array( + 0 => ( new MkdirStep() )->setPath('dir1'), + 1 => ( new RmStep() )->setPath('dir1'), + 2 => ( new MkdirStep() )->setPath('dir2') + ); - $this->assertEquals( $expected, $blueprint ); + $this->assertEquals( $expected, $result ); + } + + public function testMapsWpConfigConstants() { + $raw_blueprint = + '{ + "constants": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": true, + "WP_CACHE": true + } + }'; + + $parsed_json = json_decode( $raw_blueprint, false ); + + $result = $this->blueprint_mapper->map( $parsed_json ); + + $expected = new Blueprint(); + $expected->constants = new ArrayObject(array( + 'WP_DEBUG' => true, + 'WP_DEBUG_LOG' => true, + 'WP_DEBUG_DISPLAY' => true, + 'WP_CACHE' => true + )); + + $this->assertEquals( $expected, $result ); + } + public function testMapsSiteOptions() { + $raw_blueprint = + '{ + "siteOptions": { + "blogname": "My Blog", + "blogdescription": "A great blog" + } + }'; + + $parsed_json = json_decode( $raw_blueprint, false ); + + $result = $this->blueprint_mapper->map( $parsed_json ); + + $expected = new Blueprint(); + $expected->siteOptions = new \ArrayObject(array( + 'blogname' => 'My Blog', + 'blogdescription' => 'A great blog' + )); + + $this->assertEquals( $expected, $result ); } } From 9663b17c56c2457b2a27e244eabcd2cb8bb1a6ec Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 16:35:14 +0100 Subject: [PATCH 16/28] Delete NamespaceResolver, PropertyMap, PropertyMapperInterface --- src/WordPress/Blueprints/BlueprintMapper.php | 9 +- .../{Property => }/DocBlockAnnotations.php | 23 ++- src/WordPress/JsonMapper/JsonMapper.php | 45 +++--- .../JsonMapper/{Property => }/Property.php | 2 +- .../JsonMapper/Property/NamespaceResolver.php | 150 ------------------ .../JsonMapper/Property/PropertyMap.php | 67 -------- .../Property/PropertyMapperInterface.php | 9 -- 7 files changed, 36 insertions(+), 269 deletions(-) rename src/WordPress/JsonMapper/{Property => }/DocBlockAnnotations.php (81%) rename src/WordPress/JsonMapper/{Property => }/Property.php (91%) delete mode 100644 src/WordPress/JsonMapper/Property/NamespaceResolver.php delete mode 100644 src/WordPress/JsonMapper/Property/PropertyMap.php delete mode 100644 src/WordPress/JsonMapper/Property/PropertyMapperInterface.php diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index 6c92580f..fc3501cb 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -7,8 +7,6 @@ use WordPress\Blueprints\Model\DataClass\ModelInfo; use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\JsonMapperException; -use WordPress\JsonMapper\Property\DocBlockAnnotations; -use WordPress\JsonMapper\Property\NamespaceResolver; class BlueprintMapper { /** @@ -20,16 +18,11 @@ class BlueprintMapper { * */ public function __construct() { - $property_mappers = array( - new NamespaceResolver(), - new DocBlockAnnotations(), - - ); $custom_factories = array( 'ResourceDefinitionInterface' => array( $this, 'resource_factory' ), 'StepDefinitionInterface' => array( $this, 'step_factory' ), ); - $this->mapper = new JsonMapper( $property_mappers, $custom_factories ); + $this->mapper = new JsonMapper( $custom_factories ); } /** diff --git a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php b/src/WordPress/JsonMapper/DocBlockAnnotations.php similarity index 81% rename from src/WordPress/JsonMapper/Property/DocBlockAnnotations.php rename to src/WordPress/JsonMapper/DocBlockAnnotations.php index 90e70909..a12859d4 100644 --- a/src/WordPress/JsonMapper/Property/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/DocBlockAnnotations.php @@ -1,27 +1,22 @@ [A-Za-z_-]+)[ \t]+(?P[\w\[\]\\\\|]*).*$/m'; - public function __construct() {} - - public function map_properties( ObjectWrapper $object_wrapper, PropertyMap $property_map ) { - $property_map->merge( $this->compute_property_map( $object_wrapper ) ); - } + private function __construct() {} /** * @param ObjectWrapper $object - * @return PropertyMap + * @return array */ - private function compute_property_map( ObjectWrapper $object ): PropertyMap { - $intermediate_property_map = new PropertyMap(); + public static function compute_property_map( ObjectWrapper $object ): array { + $property_map = array(); foreach ( self::get_properties( $object ) as $property ) { $doc_block = $property->getDocComment(); if ( false === $doc_block ) { @@ -52,7 +47,7 @@ static function ( string $type ) { /* A union type that has one of its types defined as array is to complex to understand */ if ( in_array( 'array', $types, true ) ) { $property->property_types[] = 'mixed'; - $intermediate_property_map->addProperty( $property ); + $property_map[] = $property ; continue; } @@ -74,10 +69,10 @@ static function ( string $type ) { $property->property_types[] = $type; } - $intermediate_property_map->addProperty( $property ); + $property_map[] = $property ; } - return $intermediate_property_map; + return $property_map; } /** diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 9042eb9f..305daff5 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -4,44 +4,38 @@ use ReflectionClass; use stdClass; -use WordPress\JsonMapper\Property\Property; -use WordPress\JsonMapper\Property\PropertyMap; -use WordPress\JsonMapper\Property\PropertyMapperInterface; class JsonMapper { private $scalar_types = array( 'string', 'bool', 'boolean', 'int', 'integer', 'double', 'float' ); - /** - * @var PropertyMapperInterface[] - */ - private $property_mappers; - /** * @var array{$class_name} */ private $factories = array(); - public function __construct( array $property_mappers = array(), array $custom_factories = array() ) { - $this->property_mappers = $property_mappers; + public function __construct( array $custom_factories = array() ) { $this->add_factories_for_native_php_classes(); $this->add_custom_factories( $custom_factories ); } public function hydrate( stdClass $json, string $target ) { $object_wrapper = new ObjectWrapper( null, $target ); - $property_map = new PropertyMap(); - /** @var PropertyMapperInterface $property_mapper */ - foreach ($this->property_mappers as $property_mapper ) { - $property_mapper->map_properties( $object_wrapper, $property_map ); - } + $property_map = DocBlockAnnotations::compute_property_map( $object_wrapper ); $this->map_json_to_object( $json, $object_wrapper, $property_map ); return $object_wrapper->getObject(); } - public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper, PropertyMap $property_map ) { + /** + * @param stdClass $json + * @param ObjectWrapper $object_wrapper + * @param Property[] $property_map + * @return void + * @throws JsonMapperException + */ + public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper, array $property_map ) { if ( $this->has_factory( $object_wrapper->getName() ) ) { $result = $this->use_factory( $object_wrapper->getName(), $json ); @@ -51,12 +45,9 @@ public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper $values = (array) $json; foreach ( $values as $key => $value ) { - if ( false === $property_map->has_property( $key ) ) { + if ( null === $property = self::get_property( $property_map, $key ) ) { continue; } - - $property = $property_map->get_property( $key ); - if ( false === $property->is_nullable && null === $value ) { throw new JsonMapperException( "Null provided in json where \'{$object_wrapper->getName()}::{$key}\' doesn't allow null value" @@ -241,4 +232,18 @@ function ( $value ) { } ); } + + /** + * @param array $property_map + * @param string $property_name + * @return null|Property + */ + public static function get_property ( array $property_map, string $property_name ) { + foreach ( $property_map as $property ) { + if ( $property->name === $property_name ) { + return $property; + } + } + return null; + } } diff --git a/src/WordPress/JsonMapper/Property/Property.php b/src/WordPress/JsonMapper/Property.php similarity index 91% rename from src/WordPress/JsonMapper/Property/Property.php rename to src/WordPress/JsonMapper/Property.php index a589969c..e0ecf983 100644 --- a/src/WordPress/JsonMapper/Property/Property.php +++ b/src/WordPress/JsonMapper/Property.php @@ -1,6 +1,6 @@ fetchPropertyMapForObject( $object_wrapper, $property_map ) as $property ) { - $property_map->addProperty( $property ); - } - } - - private function fetchPropertyMapForObject( ObjectWrapper $object, PropertyMap $originalPropertyMap ): PropertyMap { - $intermediatePropertyMap = new PropertyMap(); - $imports = self::getImports( $object->getReflectedObject() ); - - /** @var Property $property */ - foreach ( $originalPropertyMap as $property ) { - $types = $property->property_types; - foreach ( $types as $index => $type ) { - $types[ $index ] = $this->resolveSingleType( $type, $object, $imports ); - } - $property->property_types = $types; - $intermediatePropertyMap->addProperty( $property ); - } - - return $intermediatePropertyMap; - } - - /** @return Import[] */ - private static function getImports( \ReflectionClass $class ): array { - if ( ! $class->isUserDefined() ) { - return array(); - } - - $filename = $class->getFileName(); - if ( $filename === false || \substr( $filename, -13 ) === "eval()'d code" ) { - throw new \RuntimeException( "Class {$class->getName()} has no filename available" ); - } - - if ( $class->getParentClass() === false ) { - return self::getImportsForFileName( $filename ); - } - - return array_unique( - array_merge( self::getImportsForFileName( $filename ), self::getImports( $class->getParentClass() ) ), - SORT_REGULAR - ); - } - - /** @return Import[] */ - private static function getImportsForFileName( string $filename ): array { - if ( ! \is_readable( $filename ) ) { - throw new \RuntimeException( "Unable to read {$filename}" ); - } - - $contents = \file_get_contents( $filename ); - if ( $contents === false ) { - throw new \RuntimeException( "Unable to read {$filename}" ); - } - - $parser = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 ); - - try { - $ast = $parser->parse( $contents ); - if ( \is_null( $ast ) ) { - throw new \Exception( "Failed to parse {$filename}" ); - } - } catch ( \Throwable $e ) { - throw new \Exception( "Failed to parse {$filename}" ); - } - - $traverser = new NodeTraverser(); - $visitor = new UseNodeVisitor(); - $traverser->addVisitor( $visitor ); - $traverser->traverse( $ast ); - - return $visitor->getImports(); - } - - - /** @param Import[] $imports */ - private function resolveSingleType(string $property_type, ObjectWrapper $object, array $imports ): string { - if ( $this->is_valid_scalar_type( $property_type ) ) { - return $property_type; - } - - $pos = strpos( $property_type, '\\' ); - if ( $pos === false ) { - $pos = strlen( $property_type ); - } - $nameSpacedFirstChunk = '\\' . substr( $property_type, 0, $pos ); - - $matches = \array_filter( - $imports, - static function ( Import $import ) use ( $nameSpacedFirstChunk ) { - if ( $import->hasAlias() && '\\' . $import->getAlias() === $nameSpacedFirstChunk ) { - return true; - } - - return $nameSpacedFirstChunk === \substr( $import->getImport(), -strlen( $nameSpacedFirstChunk ) ); - } - ); - - if ( count( $matches ) > 0 ) { - $match = \array_shift( $matches ); - if ( $match->hasAlias() ) { - $strippedType = \substr( $property_type, strlen( $nameSpacedFirstChunk ) ); - $fullyQualifiedType = $match->getImport() . '\\' . $strippedType; - } else { - $strippedMatch = \substr( $match->getImport(), 0, -strlen( $nameSpacedFirstChunk ) ); - $fullyQualifiedType = $strippedMatch . '\\' . $property_type; - } - - return rtrim( $fullyQualifiedType, '\\' ); - } - - $reflectedObject = $object->getReflectedObject(); - while ( true ) { - if ( class_exists( $reflectedObject->getNamespaceName() . '\\' . $property_type ) ) { - return $reflectedObject->getNamespaceName() . '\\' . $property_type; - } - - $reflectedObject = $reflectedObject->getParentClass(); - if ( ! $reflectedObject ) { - break; - } - } - - return $property_type; - } - - /** - * @param string $property_type - * @return bool - */ - private function is_valid_scalar_type( string $property_type ): bool { - return in_array( $property_type, $this->scalar_types, true ); - } -} diff --git a/src/WordPress/JsonMapper/Property/PropertyMap.php b/src/WordPress/JsonMapper/Property/PropertyMap.php deleted file mode 100644 index 8f2063e8..00000000 --- a/src/WordPress/JsonMapper/Property/PropertyMap.php +++ /dev/null @@ -1,67 +0,0 @@ -map[ $property->name ] = $property; - $this->iterator = null; - } - - public function has_property( string $name ): bool { - return array_key_exists( $name, $this->map ); - } - - public function get_property( string $key ): Property { - if ( false === $this->has_property( $key ) ) { - throw new JsonMapperException( "There is no property named $key" ); - } - - return $this->map[ $key ]; - } - - public function merge( self $other ) { - /** @var Property $other_property */ - foreach ( $other as $other_property ) { - $other_name = $other_property->name; - if ( false === $this->has_property( $other_name ) ) { - $this->addProperty( $other_property ); - continue; - } - - if ( $other_property === $this->get_property( $other_name ) ) { - continue; - } - - $property = $this->get_property( $other_name ); - $property->is_nullable = $property->is_nullable || $other_property->is_nullable; - $property->property_types = array_merge( $property->property_types, $other_property->property_types ); - $this->addProperty( $property ); - } - $this->iterator = null; - } - - // phpcs:ignore - public function getIterator(): ArrayIterator { - if ( \is_null( $this->iterator ) ) { - $this->iterator = new ArrayIterator( $this->map ); - } - - return $this->iterator; - } - - public function toString(): string { - return (string) \json_encode( $this ); - } -} diff --git a/src/WordPress/JsonMapper/Property/PropertyMapperInterface.php b/src/WordPress/JsonMapper/Property/PropertyMapperInterface.php deleted file mode 100644 index f2d2decd..00000000 --- a/src/WordPress/JsonMapper/Property/PropertyMapperInterface.php +++ /dev/null @@ -1,9 +0,0 @@ - Date: Wed, 13 Mar 2024 16:50:27 +0100 Subject: [PATCH 17/28] Clean up --- .../JsonMapper/DocBlockAnnotations.php | 8 ++++---- src/WordPress/JsonMapper/JsonMapper.php | 20 ++++++++++++------- src/WordPress/JsonMapper/ObjectWrapper.php | 1 - 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/WordPress/JsonMapper/DocBlockAnnotations.php b/src/WordPress/JsonMapper/DocBlockAnnotations.php index a12859d4..34666c09 100644 --- a/src/WordPress/JsonMapper/DocBlockAnnotations.php +++ b/src/WordPress/JsonMapper/DocBlockAnnotations.php @@ -47,7 +47,7 @@ static function ( string $type ) { /* A union type that has one of its types defined as array is to complex to understand */ if ( in_array( 'array', $types, true ) ) { $property->property_types[] = 'mixed'; - $property_map[] = $property ; + $property_map[] = $property; continue; } @@ -69,7 +69,7 @@ static function ( string $type ) { $property->property_types[] = $type; } - $property_map[] = $property ; + $property_map[] = $property; } return $property_map; @@ -94,7 +94,7 @@ private static function parse_visibility( ReflectionProperty $property ): string * @return string|null */ private static function parse_var( string $doc_block ): string { - // Strip away the start "/**' and ending "*/". + // Strip away the start "/**" and ending "*/". if ( strpos( $doc_block, '/**' ) === 0 ) { $doc_block = \substr( $doc_block, 3 ); } @@ -121,7 +121,7 @@ private static function parse_var( string $doc_block ): string { */ private static function get_properties( ObjectWrapper $object ): array { $properties = array(); - $reflection_class = new ReflectionClass( $object->getObject() ); + $reflection_class = $object->getReflectedObject(); do { $properties = array_merge( $properties, $reflection_class->getProperties() ); } while ( $reflection_class = $reflection_class->getParentClass() ); diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 305daff5..95b96320 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -3,6 +3,7 @@ namespace WordPress\JsonMapper; use ReflectionClass; +use ReflectionMethod; use stdClass; class JsonMapper { @@ -18,8 +19,14 @@ public function __construct( array $custom_factories = array() ) { $this->add_custom_factories( $custom_factories ); } - public function hydrate( stdClass $json, string $target ) { - $object_wrapper = new ObjectWrapper( null, $target ); + /** + * @param stdClass $json + * @param string $class_name + * @return object + * @throws JsonMapperException + */ + public function hydrate( stdClass $json, string $class_name ) { + $object_wrapper = new ObjectWrapper( null, $class_name ); $property_map = DocBlockAnnotations::compute_property_map( $object_wrapper ); @@ -35,7 +42,7 @@ public function hydrate( stdClass $json, string $target ) { * @return void * @throws JsonMapperException */ - public function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper, array $property_map ) { + private function map_json_to_object( stdClass $json, ObjectWrapper $object_wrapper, array $property_map ) { if ( $this->has_factory( $object_wrapper->getName() ) ) { $result = $this->use_factory( $object_wrapper->getName(), $json ); @@ -128,7 +135,7 @@ private function cast_to_scalar_type( string $type, $value ) { throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); } - private function map_to_object_using_factory(string $property_type, $value ) { + private function map_to_object_using_factory( string $property_type, $value ): array{ if ( false === is_array( $value ) ) { return $this->use_factory( $property_type, $value ); } @@ -183,7 +190,6 @@ private function sanitise_class_name( string $class_name ): string { if ( strpos( $class_name, '\\' ) === 0 ) { $class_name = substr( $class_name, 1 ); } - return $class_name; } @@ -200,7 +206,7 @@ private function add_factory( string $class_name, callable $factory ) { $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; } - public function add_custom_factories( array $custom_factories ) { + private function add_custom_factories( array $custom_factories ) { foreach ( $custom_factories as $class_name => $custom_factory ) { $this->add_factory( $class_name, $custom_factory ); } @@ -238,7 +244,7 @@ function ( $value ) { * @param string $property_name * @return null|Property */ - public static function get_property ( array $property_map, string $property_name ) { + private static function get_property ( array $property_map, string $property_name ) { foreach ( $property_map as $property ) { if ( $property->name === $property_name ) { return $property; diff --git a/src/WordPress/JsonMapper/ObjectWrapper.php b/src/WordPress/JsonMapper/ObjectWrapper.php index c53e473e..6dbd5b7d 100644 --- a/src/WordPress/JsonMapper/ObjectWrapper.php +++ b/src/WordPress/JsonMapper/ObjectWrapper.php @@ -59,7 +59,6 @@ public function getObject() { return $this->object; } - /** @return class-string */ public function getClassName(): string { return $this->class_name; From 12bbf4856be970a4e7ec7f00065581fce2f960aa Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 17:57:56 +0100 Subject: [PATCH 18/28] Fix union types issues --- .../Model/DataClass/UrlResource.php | 7 +- src/WordPress/JsonMapper/JsonMapper.php | 342 +++++++++++++----- tests/Blueprints/BlueprintMapperTest.php | 23 ++ 3 files changed, 273 insertions(+), 99 deletions(-) diff --git a/src/WordPress/Blueprints/Model/DataClass/UrlResource.php b/src/WordPress/Blueprints/Model/DataClass/UrlResource.php index f4dd6d4c..75c952ea 100644 --- a/src/WordPress/Blueprints/Model/DataClass/UrlResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/UrlResource.php @@ -32,10 +32,11 @@ public function setResource( string $resource ) { public function setUrl( string $url ) { + // @TODO mapper supposedly sets values with setters, so why does this not set the caption? $this->url = $url; - if ( ! $this->caption ) { - $this->caption = 'Downloading ' . $url; - } +// if ( ! $this->caption ) { +// $this->caption = 'Downloading ' . $url; +// } return $this; } diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 95b96320..c69441ce 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -6,17 +6,19 @@ use ReflectionMethod; use stdClass; -class JsonMapper { - private $scalar_types = array( 'string', 'bool', 'boolean', 'int', 'integer', 'double', 'float' ); +class JsonMapper +{ + private $scalar_types = array('string', 'bool', 'boolean', 'int', 'integer', 'double', 'float'); /** - * @var array{$class_name} + * @var array{ $class_name } */ private $factories = array(); - public function __construct( array $custom_factories = array() ) { + public function __construct(array $custom_factories = array()) + { $this->add_factories_for_native_php_classes(); - $this->add_custom_factories( $custom_factories ); + $this->add_custom_factories($custom_factories); } /** @@ -25,12 +27,13 @@ public function __construct( array $custom_factories = array() ) { * @return object * @throws JsonMapperException */ - public function hydrate( stdClass $json, string $class_name ) { - $object_wrapper = new ObjectWrapper( null, $class_name ); + public function hydrate(stdClass $json, string $class_name) + { + $object_wrapper = new ObjectWrapper(null, $class_name); - $property_map = DocBlockAnnotations::compute_property_map( $object_wrapper ); + $property_map = DocBlockAnnotations::compute_property_map($object_wrapper); - $this->map_json_to_object( $json, $object_wrapper, $property_map ); + $this->map_json_to_object($json, $object_wrapper, $property_map); return $object_wrapper->getObject(); } @@ -42,141 +45,281 @@ public function hydrate( stdClass $json, string $class_name ) { * @return void * @throws JsonMapperException */ - private function map_json_to_object( stdClass $json, ObjectWrapper $object_wrapper, array $property_map ) { - if ( $this->has_factory( $object_wrapper->getName() ) ) { - $result = $this->use_factory( $object_wrapper->getName(), $json ); + private function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper, array $property_map) + { + if ($this->has_factory($object_wrapper->getName())) { + $result = $this->use_factory($object_wrapper->getName(), $json); - $object_wrapper->setObject( $result ); + $object_wrapper->setObject($result); return; } - $values = (array) $json; - foreach ( $values as $key => $value ) { - if ( null === $property = self::get_property( $property_map, $key ) ) { + $values = (array)$json; + foreach ($values as $key => $value) { + if (null === $property = self::get_property($property_map, $key)) { continue; } - if ( false === $property->is_nullable && null === $value ) { + if (false === $property->is_nullable && null === $value) { throw new JsonMapperException( "Null provided in json where \'{$object_wrapper->getName()}::{$key}\' doesn't allow null value" ); } - - if ( $property->is_nullable && null === $value ) { - $this->set_value( $object_wrapper, $property, null ); + if ($property->is_nullable && null === $value) { + $this->set_value($object_wrapper, $property, null); continue; } - - $value = $this->map_value( $property, $value ); - $this->set_value( $object_wrapper, $property, $value ); + $value = $this->map_value($property, $value); + $this->set_value($object_wrapper, $property, $value); } } - private function map_value( Property $property, $value ) { - if ( null === $value && $property->is_nullable ) { + private function map_value(Property $property, $value) + { + // For union types, loop through and see if value is a match with the type + if (\count($property->property_types) > 1) { + foreach ($property->property_types as $property_type) { + + $property_type = trim($property_type); + $is_array = substr($property_type, -2) === '[]'; +// +// if ( ! $is_array ) { +// $property->property_types[] = $type; +// continue; +// } +// +// $first_bracket_index = strpos( $type, '[' ); +// +// if ( false !== $first_bracket_index ) { +// $type = substr( $type, 0, $first_bracket_index ); +// } + + + if (\is_array($value) && $is_array && count($value) === 0) { + return []; + } + + if (\is_array($value) && $is_array) { + $copy = $value; + $firstValue = \array_shift($copy); + + /* Array of scalar values */ + if ($this->propertyTypeAndValueTypeAreScalarAndSameType($property_type, $firstValue)) { + + + $scalarType = new ScalarType($property_type->getType()); + return \array_map(function ($v) use ($scalarType) { + return $this->scalarCaster->cast($scalarType, $v); + }, $value); + } + +// if ( $this->is_valid_scalar_type( $property_type ) ) { +// return $this->map_to_scalar( $property_type, $value ); +// } + + // Array of registered class @todo how do you know it was the correct type? +// if ($this->has_factory($type->getType())) { +// return $this->mapToObjectsUsingFactory($type, $value); +// } + if ($this->has_factory($property_type)) { + return $this->map_to_object_using_factory($property_type, $value); + } + + // Array of existing class @todo how do you know it was the correct type? +// if ((class_exists($type->getType()) || interface_exists($type->getType()))) { +// return $this->mapToObjects($type, $value, $mapper); +// } + if ((class_exists($property_type) || interface_exists($property_type))) { + return $this->map_to_object($property_type, $value); + } + + continue; + } + + // If the type we are mapping has a last minute factory use it. +// if ($this->classFactoryRegistry->hasFactory($type->getType())) { +// return $this->mapToObjectsUsingFactory($type, $value); +// } + if ($this->has_factory($property_type)) { + return $this->map_to_object_using_factory($property_type, $value); + } + + // Single scalar value + if ($this->propertyTypeAndValueTypeAreScalarAndSameType($property_type, $value)) { + return $this->scalarCaster->cast(new ScalarType($property_type->getType()), $value); + } + + // Single existing class @todo how do you know it was the correct type? +// if (\class_exists($property_type->getType())) { +// return $this->mapToSingleObject($property_type->getType(), $value, $mapper); +// } + if ((class_exists($property_type) || interface_exists($property_type))) { + return $this->map_to_object($property_type, $value); + } + } + } + + if (\is_null($value) && $property->isNullable()) { return null; } // No match was found (or there was only one option) lets assume the first is the right one. $types = $property->property_types; - $property_type = \array_shift( $types ); + $property_type = \array_shift($types); - if ( null === $property_type ) { + if ($property_type === null) { // Return the value as is as there is no type info. return $value; } - if ( $this->is_valid_scalar_type( $property_type ) ) { - return $this->map_to_scalar( $property_type, $value ); + if ($this->is_valid_scalar_type($property_type)) { + return $this->map_to_scalar($property_type, $value); + } + + if ($this->has_factory($property_type)) { + return $this->map_to_object_using_factory($property_type, $value); } - if ( $this->has_factory( $property_type ) ) { - return $this->map_to_object_using_factory( $property_type, $value ); +// if ($this->classFactoryRegistry->hasFactory($property_type)) { +// return $this->mapToObjectsUsingFactory($property_type, $value); +// } + +// if ((class_exists($type->getType()) || interface_exists($type->getType()))) { +// return $this->mapToObjects($type, $value, $mapper); +// } + if ((class_exists($property_type) || interface_exists($property_type))) { + return $this->map_to_object($property_type, $value); + } + + throw new \Exception("Unable to map to {$type->getType()}"); + } +// if ( null === $value && $property->is_nullable ) { +// return null; +// } +// // No match was found (or there was only one option) lets assume the first is the right one. +// $types = $property->property_types; +// $property_type = \array_shift( $types ); +// +// if ( null === $property_type ) { +// // Return the value as is as there is no type info. +// return $value; +// } +// +// if ( $this->is_valid_scalar_type( $property_type ) ) { +// return $this->map_to_scalar( $property_type, $value ); +// } +// +// if ( $this->has_factory( $property_type ) ) { +// return $this->map_to_object_using_factory( $property_type, $value ); +// } +// +// if ( ( class_exists( $property_type ) || interface_exists( $property_type ) ) ) { +// return $this->map_to_object( $property_type, $value ); +// } +// +// throw new JsonMapperException( "Unable to map to \'{$property_type}\'" ); +// } + + /** + * @param mixed $value + * @psalm-assert-if-true scalar $value + */ + private function propertyTypeAndValueTypeAreScalarAndSameType(string $type, $value): bool + { + if (!\is_scalar($value) || !$this->is_valid_scalar_type($type)) { + return false; } - if ( ( class_exists( $property_type ) || interface_exists( $property_type ) ) ) { - return $this->map_to_object( $property_type, $value ); + $valueType = \gettype($value); + if ($valueType === 'double') { + $valueType = 'float'; } - throw new JsonMapperException( "Unable to map to \'{$property_type}\'" ); + return $type === $valueType; } /** * @param string $property_type * @return bool */ - private function is_valid_scalar_type( string $property_type ): bool { - return in_array( $property_type, $this->scalar_types, true ); + private function is_valid_scalar_type(string $property_type): bool + { + return in_array($property_type, $this->scalar_types, true); } - private function map_to_scalar(string $property_type, $value ) { - if ( false === is_array( $value ) ) { - return $this->cast_to_scalar_type( $property_type, $value ); + private function map_to_scalar(string $property_type, $value) + { + if (false === is_array($value)) { + return $this->cast_to_scalar_type($property_type, $value); } $mapped_scalars = array(); - foreach ( $value as $inner_value ) { - $mapped_scalars[] = $this->map_to_scalar( $property_type, $inner_value ); + foreach ($value as $inner_value) { + $mapped_scalars[] = $this->map_to_scalar($property_type, $inner_value); } return $mapped_scalars; } - private function cast_to_scalar_type( string $type, $value ) { - if ( 'string' === $type ) { - return (string) $value; + private function cast_to_scalar_type(string $type, $value) + { + if ('string' === $type) { + return (string)$value; } - if ( 'boolean' === $type || 'bool' === $type ) { - return (bool) $value; + if ('boolean' === $type || 'bool' === $type) { + return (bool)$value; } - if ( 'integer' === $type || 'int' === $type ) { - return (int) $value; + if ('integer' === $type || 'int' === $type) { + return (int)$value; } - if ( 'double' === $type || 'float' === $type ) { - return (float) $value; + if ('double' === $type || 'float' === $type) { + return (float)$value; } - throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); + throw new JsonMapperException("Casting to scalar type \'$type\' failed."); } - private function map_to_object_using_factory( string $property_type, $value ): array{ - if ( false === is_array( $value ) ) { - return $this->use_factory( $property_type, $value ); + private function map_to_object_using_factory(string $property_type, $value) + { + if (false === is_array($value)) { + return $this->use_factory($property_type, $value); } $mapped_objects = array(); - foreach ( $value as $inner_value ) { - $mapped_objects[] = $this->map_to_object_using_factory( $property_type, $inner_value ); + foreach ($value as $inner_value) { + $mapped_objects[] = $this->map_to_object_using_factory($property_type, $inner_value); } return $mapped_objects; } - private function map_to_object(string $property_type, $value ) { - if ( false === ( new ReflectionClass( $property_type ) )->isInstantiable() ) { - throw new JsonMapperException( "Unable to resolve uninstantiable \'{$property_type}\'." ); + private function map_to_object(string $property_type, $value) + { + if (false === (new ReflectionClass($property_type))->isInstantiable()) { + throw new JsonMapperException("Unable to resolve uninstantiable \'{$property_type}\'."); } - if ( false === is_array( $value ) ) { - return $this->hydrate( $value, $property_type ); + if (false === is_array($value)) { + return $this->hydrate($value, $property_type); } $mapped_objects = array(); - foreach ( $value as $inner_value ) { - $mapped_objects[] = $this->map_to_object( $property_type, $inner_value ); + foreach ($value as $inner_value) { + $mapped_objects[] = $this->map_to_object($property_type, $inner_value); } return $mapped_objects; } - private function set_value( ObjectWrapper $object, Property $property, $value ) { - if ( 'public' === $property->visibility ) { + private function set_value(ObjectWrapper $object, Property $property, $value) + { + if ('public' === $property->visibility) { $object->getObject()->{$property->name} = $value; return; } - $method_name = 'set' . \ucfirst( $property->name ); - if ( \method_exists( $object->getObject(), $method_name ) ) { - $method = new ReflectionMethod( $object->getObject(), $method_name ); + $method_name = 'set' . \ucfirst($property->name); + if (\method_exists($object->getObject(), $method_name)) { + $method = new ReflectionMethod($object->getObject(), $method_name); $parameters = $method->getParameters(); - if ( \is_array( $value ) && \count( $parameters ) === 1 && $parameters[0]->isVariadic() ) { - $object->getObject()->$method_name( ...$value ); + if (\is_array($value) && \count($parameters) === 1 && $parameters[0]->isVariadic()) { + $object->getObject()->$method_name(...$value); return; } - $object->getObject()->$method_name( $value ); + $object->getObject()->$method_name($value); return; } @@ -185,56 +328,62 @@ private function set_value( ObjectWrapper $object, Property $property, $value ) ); } - private function sanitise_class_name( string $class_name ): string { + private function sanitise_class_name(string $class_name): string + { /* Erase leading slash as ::class doesn't contain leading slash */ - if ( strpos( $class_name, '\\' ) === 0 ) { - $class_name = substr( $class_name, 1 ); + if (strpos($class_name, '\\') === 0) { + $class_name = substr($class_name, 1); } return $class_name; } - private function has_factory( string $class_name ): bool { - return array_key_exists( $this->sanitise_class_name( $class_name ), $this->factories ); + private function has_factory(string $class_name): bool + { + return array_key_exists($this->sanitise_class_name($class_name), $this->factories); } - private function use_factory( string $class_name, $params ) { - $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; - return $factory( $params ); + private function use_factory(string $class_name, $params) + { + $factory = $this->factories[$this->sanitise_class_name($class_name)]; + return $factory($params); } - private function add_factory( string $class_name, callable $factory ) { - $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; + private function add_factory(string $class_name, callable $factory) + { + $this->factories[$this->sanitise_class_name($class_name)] = $factory; } - private function add_custom_factories( array $custom_factories ) { - foreach ( $custom_factories as $class_name => $custom_factory ) { - $this->add_factory( $class_name, $custom_factory ); + private function add_custom_factories(array $custom_factories) + { + foreach ($custom_factories as $class_name => $custom_factory) { + $this->add_factory($class_name, $custom_factory); } } - private function add_factories_for_native_php_classes() { + private function add_factories_for_native_php_classes() + { $this->add_factory( \DateTime::class, - static function ( string $value ) { - return new \DateTime( $value ); + static function (string $value) { + return new \DateTime($value); } ); $this->add_factory( \DateTimeImmutable::class, - static function ( string $value ) { - return new \DateTimeImmutable( $value ); + static function (string $value) { + return new \DateTimeImmutable($value); } ); $this->add_factory( stdClass::class, - static function ( $value ) { - return (object) $value; + static function ($value) { + return (object)$value; } ); $this->add_factory( \ArrayObject::class, - function ( $value ) { - return new \ArrayObject( $value ); + function ($value) { + return new \ArrayObject($value); } ); } @@ -244,9 +393,10 @@ function ( $value ) { * @param string $property_name * @return null|Property */ - private static function get_property ( array $property_map, string $property_name ) { - foreach ( $property_map as $property ) { - if ( $property->name === $property_name ) { + private static function get_property(array $property_map, string $property_name) + { + foreach ($property_map as $property) { + if ($property->name === $property_name) { return $property; } } diff --git a/tests/Blueprints/BlueprintMapperTest.php b/tests/Blueprints/BlueprintMapperTest.php index 30eccb4f..542dbaa3 100644 --- a/tests/Blueprints/BlueprintMapperTest.php +++ b/tests/Blueprints/BlueprintMapperTest.php @@ -9,6 +9,7 @@ use WordPress\Blueprints\Model\DataClass\Blueprint; use WordPress\Blueprints\Model\DataClass\MkdirStep; use WordPress\Blueprints\Model\DataClass\RmStep; +use WordPress\Blueprints\Model\DataClass\UrlResource; class BlueprintMapperTest extends TestCase { @@ -77,6 +78,28 @@ public function testMapsMultiplePlugins() { $this->assertEquals( $expected, $result ); } + public function testMapsPluginsWithDifferentDataTypes() { + $raw_blueprint = + '{ + "plugins": [ + "https://downloads.wordpress.org/plugin/wordpress-importer.zip", + { "resource": "url", "url": "https://mysite.com" } + ] + }'; + + $parsed_json = json_decode( $raw_blueprint, false ); + + $result = $this->blueprint_mapper->map( $parsed_json ); + + $expected = new Blueprint(); + $expected->plugins = array( + 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', + ( new UrlResource() ) ->setUrl( "https://mysite.com" ), + ); + + $this->assertEquals( $expected, $result ); + } + public function testMapsWhenSpecificStepAppearsTwice() { $raw_blueprint = '{ From 1be7bc58dbbc3ae3d5654426ec1ea488665df5c7 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Wed, 13 Mar 2024 18:04:51 +0100 Subject: [PATCH 19/28] Add test --- tests/Blueprints/BlueprintMapperTest.php | 60 ++++++++++++++++-------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/tests/Blueprints/BlueprintMapperTest.php b/tests/Blueprints/BlueprintMapperTest.php index 542dbaa3..9b6f1f57 100644 --- a/tests/Blueprints/BlueprintMapperTest.php +++ b/tests/Blueprints/BlueprintMapperTest.php @@ -10,6 +10,7 @@ use WordPress\Blueprints\Model\DataClass\MkdirStep; use WordPress\Blueprints\Model\DataClass\RmStep; use WordPress\Blueprints\Model\DataClass\UrlResource; +use WordPress\JsonMapper\JsonMapperException; class BlueprintMapperTest extends TestCase { @@ -47,7 +48,7 @@ public function testMapsWordPressVersion() { $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = new Blueprint(); + $expected = new Blueprint(); $expected->WordPressVersion = 'https://wordpress.org/latest.zip'; $this->assertEquals( $expected, $result ); @@ -68,7 +69,7 @@ public function testMapsMultiplePlugins() { $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = new Blueprint(); + $expected = new Blueprint(); $expected->plugins = array( 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', 'https://downloads.wordpress.org/plugin/hello-dolly.zip', @@ -91,15 +92,30 @@ public function testMapsPluginsWithDifferentDataTypes() { $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = new Blueprint(); + $expected = new Blueprint(); $expected->plugins = array( 'https://downloads.wordpress.org/plugin/wordpress-importer.zip', - ( new UrlResource() ) ->setUrl( "https://mysite.com" ), + ( new UrlResource() )->setUrl( 'https://mysite.com' ), ); $this->assertEquals( $expected, $result ); } + public function testFailsWhenPluginsWithInvalidDataTypes() { + $raw_blueprint = + '{ + "plugins": [ + "https://downloads.wordpress.org/plugin/wordpress-importer.zip", + 123 + ] + }'; + + $parsed_json = json_decode( $raw_blueprint, false ); + + $this->expectException( JsonMapperException::class ); + $this->blueprint_mapper->map( $parsed_json ); + } + public function testMapsWhenSpecificStepAppearsTwice() { $raw_blueprint = '{ @@ -115,11 +131,11 @@ public function testMapsWhenSpecificStepAppearsTwice() { $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = new Blueprint(); + $expected = new Blueprint(); $expected->steps = array( - 0 => ( new MkdirStep() )->setPath('dir1'), - 1 => ( new RmStep() )->setPath('dir1'), - 2 => ( new MkdirStep() )->setPath('dir2') + 0 => ( new MkdirStep() )->setPath( 'dir1' ), + 1 => ( new RmStep() )->setPath( 'dir1' ), + 2 => ( new MkdirStep() )->setPath( 'dir2' ), ); $this->assertEquals( $expected, $result ); @@ -140,13 +156,15 @@ public function testMapsWpConfigConstants() { $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = new Blueprint(); - $expected->constants = new ArrayObject(array( - 'WP_DEBUG' => true, - 'WP_DEBUG_LOG' => true, - 'WP_DEBUG_DISPLAY' => true, - 'WP_CACHE' => true - )); + $expected = new Blueprint(); + $expected->constants = new ArrayObject( + array( + 'WP_DEBUG' => true, + 'WP_DEBUG_LOG' => true, + 'WP_DEBUG_DISPLAY' => true, + 'WP_CACHE' => true, + ) + ); $this->assertEquals( $expected, $result ); } @@ -163,11 +181,13 @@ public function testMapsSiteOptions() { $result = $this->blueprint_mapper->map( $parsed_json ); - $expected = new Blueprint(); - $expected->siteOptions = new \ArrayObject(array( - 'blogname' => 'My Blog', - 'blogdescription' => 'A great blog' - )); + $expected = new Blueprint(); + $expected->siteOptions = new \ArrayObject( + array( + 'blogname' => 'My Blog', + 'blogdescription' => 'A great blog', + ) + ); $this->assertEquals( $expected, $result ); } From 271a88ed4370e78a2de932ddb918893c71fd5ae9 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Sat, 16 Mar 2024 09:53:59 +0100 Subject: [PATCH 20/28] Fix model autogeneration bug, test mapper and parser --- composer.json | 5 + .../Model/DataClass/ActivatePluginStep.php | 6 +- .../Model/DataClass/ActivateThemeStep.php | 6 +- .../Blueprints/Model/DataClass/Blueprint.php | 20 +- .../Model/DataClass/BlueprintOnBoot.php | 4 +- .../Model/DataClass/CorePluginResource.php | 4 +- .../Model/DataClass/CoreThemeResource.php | 4 +- .../Blueprints/Model/DataClass/CpStep.php | 8 +- .../Model/DataClass/DefineSiteUrlStep.php | 6 +- .../DataClass/DefineWpConfigConstsStep.php | 6 +- .../Model/DataClass/DownloadWordPressStep.php | 4 +- .../Model/DataClass/EnableMultisiteStep.php | 4 +- .../Model/DataClass/EvalPHPCallbackStep.php | 6 +- .../Blueprints/Model/DataClass/FileInfo.php | 8 +- .../Model/DataClass/FileInfoData.php | 10 +- .../Model/DataClass/FileInfoDataBuffer.php | 2 +- .../Model/DataClass/FilesystemResource.php | 4 +- .../Model/DataClass/ImportFileStep.php | 4 +- .../Model/DataClass/InlineResource.php | 4 +- .../Model/DataClass/InstallPluginStep.php | 4 +- .../InstallSqliteIntegrationStep.php | 4 +- .../Model/DataClass/InstallThemeStep.php | 4 +- .../Blueprints/Model/DataClass/MkdirStep.php | 6 +- .../Blueprints/Model/DataClass/MvStep.php | 8 +- .../Blueprints/Model/DataClass/Progress.php | 4 +- .../DataClass/ResourceDefinitionInterface.php | 3 +- .../Blueprints/Model/DataClass/RmStep.php | 6 +- .../Blueprints/Model/DataClass/RunPHPStep.php | 6 +- .../Blueprints/Model/DataClass/RunSQLStep.php | 4 +- .../DataClass/RunWordPressInstallerStep.php | 4 +- .../Model/DataClass/SetSiteOptionsStep.php | 6 +- .../Blueprints/Model/DataClass/UnzipStep.php | 6 +- .../Model/DataClass/UrlResource.php | 25 +- .../Blueprints/Model/DataClass/WPCLIStep.php | 6 +- .../WordPressInstallationOptions.php | 2 +- .../Model/DataClass/WriteFileStep.php | 8 +- .../Blueprints/bin/autogenerate_models.php | 6 +- .../JsonMapper/DocBlockAnnotations.php | 130 ----- src/WordPress/JsonMapper/Import.php | 29 -- src/WordPress/JsonMapper/JsonMapper.php | 482 +++++++----------- src/WordPress/JsonMapper/ObjectWrapper.php | 79 --- src/WordPress/JsonMapper/Property.php | 5 - src/WordPress/JsonMapper/PropertyParser.php | 134 +++++ src/WordPress/JsonMapper/UseNodeVisitor.php | 38 -- tests/E2E/JsonBlueprintTest.php | 83 +++ tests/JsonMapper/JsonMapperTest.php | 89 +++- tests/JsonMapper/PropertyParserTest.php | 179 +++++++ .../resources/TestResourceClassSetValue.php | 23 + 48 files changed, 802 insertions(+), 696 deletions(-) delete mode 100644 src/WordPress/JsonMapper/DocBlockAnnotations.php delete mode 100644 src/WordPress/JsonMapper/Import.php delete mode 100644 src/WordPress/JsonMapper/ObjectWrapper.php create mode 100644 src/WordPress/JsonMapper/PropertyParser.php delete mode 100644 src/WordPress/JsonMapper/UseNodeVisitor.php create mode 100644 tests/E2E/JsonBlueprintTest.php create mode 100644 tests/JsonMapper/PropertyParserTest.php create mode 100644 tests/JsonMapper/resources/TestResourceClassSetValue.php diff --git a/composer.json b/composer.json index 90278d84..0f5d244b 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,11 @@ "src/WordPress/Streams/stream_str_replace.php" ] }, + "autoload-dev": { + "classmap": [ + "tests/" + ] + }, "scripts": { "phpcs": "phpcs --standard=WordPress" } diff --git a/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php b/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php index d8dd8dcb..4f71ac02 100644 --- a/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php @@ -4,13 +4,13 @@ class ActivatePluginStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'activatePlugin'; + const DISCRIMINATOR = 'activatePlugin'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'activatePlugin'; @@ -19,7 +19,7 @@ class ActivatePluginStep implements StepDefinitionInterface * Plugin slug, like 'gutenberg' or 'hello-dolly'. * @var string */ - public $slug; + public $slug = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php b/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php index e0a2b89d..04f44688 100644 --- a/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php @@ -4,13 +4,13 @@ class ActivateThemeStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'activateTheme'; + const DISCRIMINATOR = 'activateTheme'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'activateTheme'; @@ -19,7 +19,7 @@ class ActivateThemeStep implements StepDefinitionInterface * Theme slug, like 'twentytwentythree'. * @var string */ - public $slug; + public $slug = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php index f6bda88a..ae7ff841 100644 --- a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php +++ b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php @@ -2,36 +2,34 @@ namespace WordPress\Blueprints\Model\DataClass; -use ArrayObject; - class Blueprint { /** * Optional description. It doesn't do anything but is exposed as a courtesy to developers who may want to document which blueprint file does what. * @var string */ - public $description; + public $description = ''; /** * Version of WordPress to use. Also accepts URL to a WordPress zip file. * @var string */ - public $WordPressVersion; + public $WordPressVersion = null; /** * Slot for runtime–specific options, schema must be provided by the runtime. - * @var ArrayObject + * @var \ArrayObject */ - public $runtime; + public $runtime = null; /** @var BlueprintOnBoot */ - public $onBoot; + public $onBoot = null; /** * PHP Constants to define on every request - * @var ArrayObject + * @var \ArrayObject */ - public $constants; + public $constants = []; /** * WordPress plugins to install and activate @@ -41,9 +39,9 @@ class Blueprint /** * WordPress site options to define - * @var ArrayObject + * @var \ArrayObject */ - public $siteOptions; + public $siteOptions = []; /** * The steps to run after every other operation in this Blueprint was executed. diff --git a/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php b/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php index a091a052..52ebd1ae 100644 --- a/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php +++ b/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php @@ -8,10 +8,10 @@ class BlueprintOnBoot * The URL to navigate to after the blueprint has been run. * @var string */ - public $openUrl; + public $openUrl = null; /** @var bool */ - public $login; + public $login = null; public function setOpenUrl(string $openUrl) diff --git a/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php b/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php index 9772a304..0328fcb2 100644 --- a/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php @@ -4,7 +4,7 @@ class CorePluginResource implements ResourceDefinitionInterface { - public const DISCRIMINATOR = 'wordpress.org/plugins'; + const DISCRIMINATOR = 'wordpress.org/plugins'; /** * Identifies the file resource as a WordPress Core plugin @@ -16,7 +16,7 @@ class CorePluginResource implements ResourceDefinitionInterface * The slug of the WordPress Core plugin * @var string */ - public $slug; + public $slug = null; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php b/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php index bf4bd3c9..5c139df4 100644 --- a/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php @@ -4,7 +4,7 @@ class CoreThemeResource implements ResourceDefinitionInterface { - public const DISCRIMINATOR = 'wordpress.org/themes'; + const DISCRIMINATOR = 'wordpress.org/themes'; /** * Identifies the file resource as a WordPress Core theme @@ -16,7 +16,7 @@ class CoreThemeResource implements ResourceDefinitionInterface * The slug of the WordPress Core theme * @var string */ - public $slug; + public $slug = null; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/CpStep.php b/src/WordPress/Blueprints/Model/DataClass/CpStep.php index b86ad3a6..3d26c3f4 100644 --- a/src/WordPress/Blueprints/Model/DataClass/CpStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/CpStep.php @@ -4,13 +4,13 @@ class CpStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'cp'; + const DISCRIMINATOR = 'cp'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'cp'; @@ -19,13 +19,13 @@ class CpStep implements StepDefinitionInterface * Source path * @var string */ - public $fromPath; + public $fromPath = null; /** * Target path * @var string */ - public $toPath; + public $toPath = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php b/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php index 47b2b327..0e58f971 100644 --- a/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php @@ -4,13 +4,13 @@ class DefineSiteUrlStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'defineSiteUrl'; + const DISCRIMINATOR = 'defineSiteUrl'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'defineSiteUrl'; @@ -19,7 +19,7 @@ class DefineSiteUrlStep implements StepDefinitionInterface * The URL * @var string */ - public $siteUrl; + public $siteUrl = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php b/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php index f2059f42..c6310512 100644 --- a/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php @@ -4,13 +4,13 @@ class DefineWpConfigConstsStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'defineWpConfigConsts'; + const DISCRIMINATOR = 'defineWpConfigConsts'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'defineWpConfigConsts'; @@ -19,7 +19,7 @@ class DefineWpConfigConstsStep implements StepDefinitionInterface * The constants to define * @var \ArrayObject */ - public $consts; + public $consts = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/DownloadWordPressStep.php b/src/WordPress/Blueprints/Model/DataClass/DownloadWordPressStep.php index 9c76df93..432fb9c3 100644 --- a/src/WordPress/Blueprints/Model/DataClass/DownloadWordPressStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/DownloadWordPressStep.php @@ -4,13 +4,13 @@ class DownloadWordPressStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'downloadWordPress'; + const DISCRIMINATOR = 'downloadWordPress'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'downloadWordPress'; diff --git a/src/WordPress/Blueprints/Model/DataClass/EnableMultisiteStep.php b/src/WordPress/Blueprints/Model/DataClass/EnableMultisiteStep.php index a93e2d2e..e47d2be2 100644 --- a/src/WordPress/Blueprints/Model/DataClass/EnableMultisiteStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/EnableMultisiteStep.php @@ -4,13 +4,13 @@ class EnableMultisiteStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'enableMultisite'; + const DISCRIMINATOR = 'enableMultisite'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'enableMultisite'; diff --git a/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php b/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php index dc4c1b61..ed27e156 100644 --- a/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php @@ -4,13 +4,13 @@ class EvalPHPCallbackStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'evalPHPCallback'; + const DISCRIMINATOR = 'evalPHPCallback'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** * The step identifier. @@ -22,7 +22,7 @@ class EvalPHPCallbackStep implements StepDefinitionInterface * The PHP function. * @var mixed */ - public $callback; + public $callback = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/FileInfo.php b/src/WordPress/Blueprints/Model/DataClass/FileInfo.php index 0ed00e2a..e89b1972 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FileInfo.php +++ b/src/WordPress/Blueprints/Model/DataClass/FileInfo.php @@ -5,16 +5,16 @@ class FileInfo { /** @var string */ - public $key; + public $key = null; /** @var string */ - public $name; + public $name = null; /** @var string */ - public $type; + public $type = null; /** @var FileInfoData */ - public $data; + public $data = null; public function setKey(string $key) diff --git a/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php b/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php index 9638f0e9..64417c02 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php +++ b/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php @@ -5,19 +5,19 @@ class FileInfoData { /** @var float */ - public $BYTES_PER_ELEMENT; + public $BYTES_PER_ELEMENT = null; /** @var FileInfoDataBuffer */ - public $buffer; + public $buffer = null; /** @var float */ - public $byteLength; + public $byteLength = null; /** @var float */ - public $byteOffset; + public $byteOffset = null; /** @var float */ - public $length; + public $length = null; public function setBYTES_PER_ELEMENT(float $BYTES_PER_ELEMENT) diff --git a/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php b/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php index 9bb4dfc8..6868d18b 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php +++ b/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php @@ -5,7 +5,7 @@ class FileInfoDataBuffer { /** @var float */ - public $byteLength; + public $byteLength = null; public function setByteLength(float $byteLength) diff --git a/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php b/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php index d222c5b8..3959f701 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php @@ -4,7 +4,7 @@ class FilesystemResource implements ResourceDefinitionInterface { - public const DISCRIMINATOR = 'filesystem'; + const DISCRIMINATOR = 'filesystem'; /** * Identifies the file resource as Virtual File System (VFS) @@ -16,7 +16,7 @@ class FilesystemResource implements ResourceDefinitionInterface * The path to the file in the VFS * @var string */ - public $path; + public $path = null; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/ImportFileStep.php b/src/WordPress/Blueprints/Model/DataClass/ImportFileStep.php index 9bc5dc4c..c4dd3310 100644 --- a/src/WordPress/Blueprints/Model/DataClass/ImportFileStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/ImportFileStep.php @@ -4,13 +4,13 @@ class ImportFileStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'importFile'; + const DISCRIMINATOR = 'importFile'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'importFile'; diff --git a/src/WordPress/Blueprints/Model/DataClass/InlineResource.php b/src/WordPress/Blueprints/Model/DataClass/InlineResource.php index 82d7d0cd..7433f865 100644 --- a/src/WordPress/Blueprints/Model/DataClass/InlineResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/InlineResource.php @@ -4,7 +4,7 @@ class InlineResource implements ResourceDefinitionInterface { - public const DISCRIMINATOR = 'inline'; + const DISCRIMINATOR = 'inline'; /** * Identifies the file resource as an inline string @@ -16,7 +16,7 @@ class InlineResource implements ResourceDefinitionInterface * The contents of the file * @var string */ - public $contents; + public $contents = null; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/InstallPluginStep.php b/src/WordPress/Blueprints/Model/DataClass/InstallPluginStep.php index c2e8a072..d4ea14ca 100644 --- a/src/WordPress/Blueprints/Model/DataClass/InstallPluginStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/InstallPluginStep.php @@ -4,13 +4,13 @@ class InstallPluginStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'installPlugin'; + const DISCRIMINATOR = 'installPlugin'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** * The step identifier. diff --git a/src/WordPress/Blueprints/Model/DataClass/InstallSqliteIntegrationStep.php b/src/WordPress/Blueprints/Model/DataClass/InstallSqliteIntegrationStep.php index 82489e15..a96aef15 100644 --- a/src/WordPress/Blueprints/Model/DataClass/InstallSqliteIntegrationStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/InstallSqliteIntegrationStep.php @@ -4,13 +4,13 @@ class InstallSqliteIntegrationStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'installSqliteIntegration'; + const DISCRIMINATOR = 'installSqliteIntegration'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'installSqliteIntegration'; diff --git a/src/WordPress/Blueprints/Model/DataClass/InstallThemeStep.php b/src/WordPress/Blueprints/Model/DataClass/InstallThemeStep.php index 49e233d5..a862fad6 100644 --- a/src/WordPress/Blueprints/Model/DataClass/InstallThemeStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/InstallThemeStep.php @@ -4,13 +4,13 @@ class InstallThemeStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'installTheme'; + const DISCRIMINATOR = 'installTheme'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** * The step identifier. diff --git a/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php b/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php index 3e05a3f5..bde0e187 100644 --- a/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php @@ -4,13 +4,13 @@ class MkdirStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'mkdir'; + const DISCRIMINATOR = 'mkdir'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'mkdir'; @@ -19,7 +19,7 @@ class MkdirStep implements StepDefinitionInterface * The path of the directory you want to create * @var string */ - public $path; + public $path = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/MvStep.php b/src/WordPress/Blueprints/Model/DataClass/MvStep.php index 6dacc734..19023fb6 100644 --- a/src/WordPress/Blueprints/Model/DataClass/MvStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/MvStep.php @@ -4,13 +4,13 @@ class MvStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'mv'; + const DISCRIMINATOR = 'mv'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'mv'; @@ -19,13 +19,13 @@ class MvStep implements StepDefinitionInterface * Source path * @var string */ - public $fromPath; + public $fromPath = null; /** * Target path * @var string */ - public $toPath; + public $toPath = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/Progress.php b/src/WordPress/Blueprints/Model/DataClass/Progress.php index 4ae54958..7f117261 100644 --- a/src/WordPress/Blueprints/Model/DataClass/Progress.php +++ b/src/WordPress/Blueprints/Model/DataClass/Progress.php @@ -5,10 +5,10 @@ class Progress { /** @var float */ - public $weight; + public $weight = null; /** @var string */ - public $caption; + public $caption = null; public function setWeight(float $weight) diff --git a/src/WordPress/Blueprints/Model/DataClass/ResourceDefinitionInterface.php b/src/WordPress/Blueprints/Model/DataClass/ResourceDefinitionInterface.php index 269ff53b..8bee1256 100644 --- a/src/WordPress/Blueprints/Model/DataClass/ResourceDefinitionInterface.php +++ b/src/WordPress/Blueprints/Model/DataClass/ResourceDefinitionInterface.php @@ -2,5 +2,6 @@ namespace WordPress\Blueprints\Model\DataClass; -interface ResourceDefinitionInterface { +interface ResourceDefinitionInterface +{ } diff --git a/src/WordPress/Blueprints/Model/DataClass/RmStep.php b/src/WordPress/Blueprints/Model/DataClass/RmStep.php index 4e9aa2a4..5acc1dab 100644 --- a/src/WordPress/Blueprints/Model/DataClass/RmStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/RmStep.php @@ -4,13 +4,13 @@ class RmStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'rm'; + const DISCRIMINATOR = 'rm'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'rm'; @@ -19,7 +19,7 @@ class RmStep implements StepDefinitionInterface * The path to remove * @var string */ - public $path; + public $path = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php b/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php index 1bcb96b1..420ab0f0 100644 --- a/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php @@ -4,13 +4,13 @@ class RunPHPStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'runPHP'; + const DISCRIMINATOR = 'runPHP'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** * The step identifier. @@ -22,7 +22,7 @@ class RunPHPStep implements StepDefinitionInterface * The PHP code to run. * @var string */ - public $code; + public $code = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/RunSQLStep.php b/src/WordPress/Blueprints/Model/DataClass/RunSQLStep.php index 68d0f2ca..a63ad67e 100644 --- a/src/WordPress/Blueprints/Model/DataClass/RunSQLStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/RunSQLStep.php @@ -4,13 +4,13 @@ class RunSQLStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'runSql'; + const DISCRIMINATOR = 'runSql'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** * The step identifier. diff --git a/src/WordPress/Blueprints/Model/DataClass/RunWordPressInstallerStep.php b/src/WordPress/Blueprints/Model/DataClass/RunWordPressInstallerStep.php index 604fe982..de109909 100644 --- a/src/WordPress/Blueprints/Model/DataClass/RunWordPressInstallerStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/RunWordPressInstallerStep.php @@ -4,13 +4,13 @@ class RunWordPressInstallerStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'runWpInstallationWizard'; + const DISCRIMINATOR = 'runWpInstallationWizard'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'runWpInstallationWizard'; diff --git a/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php b/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php index f1399ff8..8a5ee1bb 100644 --- a/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php @@ -4,13 +4,13 @@ class SetSiteOptionsStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'setSiteOptions'; + const DISCRIMINATOR = 'setSiteOptions'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** * The name of the step. Must be "setSiteOptions". @@ -22,7 +22,7 @@ class SetSiteOptionsStep implements StepDefinitionInterface * The options to set on the site. * @var \ArrayObject */ - public $options; + public $options = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php b/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php index 7f292e18..212ca120 100644 --- a/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php @@ -4,13 +4,13 @@ class UnzipStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'unzip'; + const DISCRIMINATOR = 'unzip'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'unzip'; @@ -22,7 +22,7 @@ class UnzipStep implements StepDefinitionInterface * The path to extract the zip file to * @var string */ - public $extractToPath; + public $extractToPath = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/UrlResource.php b/src/WordPress/Blueprints/Model/DataClass/UrlResource.php index 75c952ea..6198b64c 100644 --- a/src/WordPress/Blueprints/Model/DataClass/UrlResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/UrlResource.php @@ -2,8 +2,9 @@ namespace WordPress\Blueprints\Model\DataClass; -class UrlResource implements ResourceDefinitionInterface { - public const DISCRIMINATOR = 'url'; +class UrlResource implements ResourceDefinitionInterface +{ + const DISCRIMINATOR = 'url'; /** * Identifies the file resource as a URL @@ -15,36 +16,32 @@ class UrlResource implements ResourceDefinitionInterface { * The URL of the file * @var string */ - public $url; + public $url = null; /** * Optional caption for displaying a progress message * @var string */ - public $caption; + public $caption = null; - public function setResource( string $resource ) { + public function setResource(string $resource) + { $this->resource = $resource; - return $this; } - public function setUrl( string $url ) { - // @TODO mapper supposedly sets values with setters, so why does this not set the caption? + public function setUrl(string $url) + { $this->url = $url; -// if ( ! $this->caption ) { -// $this->caption = 'Downloading ' . $url; -// } - return $this; } - public function setCaption( string $caption ) { + public function setCaption(string $caption) + { $this->caption = $caption; - return $this; } } diff --git a/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php b/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php index 399839fb..2be9b508 100644 --- a/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php @@ -4,13 +4,13 @@ class WPCLIStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'wp-cli'; + const DISCRIMINATOR = 'wp-cli'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** * The step identifier. @@ -22,7 +22,7 @@ class WPCLIStep implements StepDefinitionInterface * The WP CLI command to run. * @var string[] */ - public $command; + public $command = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php b/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php index 7793e3db..9ed65307 100644 --- a/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php +++ b/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php @@ -5,7 +5,7 @@ class WordPressInstallationOptions { /** @var string */ - public $adminUsername; + public $adminUsername = null; /** @var string */ public $adminPassword = 'admin'; diff --git a/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php b/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php index 9785296c..5ec66f42 100644 --- a/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php @@ -4,13 +4,13 @@ class WriteFileStep implements StepDefinitionInterface { - public const DISCRIMINATOR = 'writeFile'; + const DISCRIMINATOR = 'writeFile'; /** @var Progress */ public $progress; /** @var bool */ - public $continueOnError; + public $continueOnError = false; /** @var string */ public $step = 'writeFile'; @@ -19,13 +19,13 @@ class WriteFileStep implements StepDefinitionInterface * The path of the file to write to * @var string */ - public $path; + public $path = null; /** * The data to write * @var string|ResourceDefinitionInterface */ - public $data; + public $data = null; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/bin/autogenerate_models.php b/src/WordPress/Blueprints/bin/autogenerate_models.php index ebd2139b..b8f205f2 100644 --- a/src/WordPress/Blueprints/bin/autogenerate_models.php +++ b/src/WordPress/Blueprints/bin/autogenerate_models.php @@ -196,6 +196,7 @@ function fixTypeHint( string $typeHint, array $replacements ): string { $schema = $janeProperty->getObject(); if ( $schema instanceof JsonSchema ) { + $property->setValue( $schema->getDefault() ); if ( $schema->getConst() ) { $property->setValue( $schema->getConst() ); // Assume that a class with an interface uses a const property @@ -205,9 +206,10 @@ function fixTypeHint( string $typeHint, array $replacements ): string { // so let's keep it simple for now. if ( $hasInterface ) { $class->addConstant( 'DISCRIMINATOR', $schema->getConst() ); + // Method 'addConstant' by default sets const visibility to 'public', but PHP 7.0 does not like it. + // So, we have to manually set it back to null. + $class->getConstants()['DISCRIMINATOR']->setVisibility( null ); } - } elseif ( $schema->getDefault() ) { - $property->setValue( $schema->getDefault() ); } elseif ( $schema->getType() === 'array' ) { $property->setValue( [] ); } diff --git a/src/WordPress/JsonMapper/DocBlockAnnotations.php b/src/WordPress/JsonMapper/DocBlockAnnotations.php deleted file mode 100644 index 34666c09..00000000 --- a/src/WordPress/JsonMapper/DocBlockAnnotations.php +++ /dev/null @@ -1,130 +0,0 @@ -[A-Za-z_-]+)[ \t]+(?P[\w\[\]\\\\|]*).*$/m'; - - private function __construct() {} - - /** - * @param ObjectWrapper $object - * @return array - */ - public static function compute_property_map( ObjectWrapper $object ): array { - $property_map = array(); - foreach ( self::get_properties( $object ) as $property ) { - $doc_block = $property->getDocComment(); - if ( false === $doc_block ) { - continue; - } - - $var = self::parse_var( $doc_block ); - if ( null === $var ) { - continue; - } - - $name = $property->getName(); - $types = explode( '|', $var ); - $is_nullable = in_array( 'null', $types, true ); - $types = array_filter( - $types, - static function ( string $type ) { - return 'null' !== $type; - } - ); - - $property = new Property( - $name, - self::parse_visibility( $property ), - $is_nullable - ); - - /* A union type that has one of its types defined as array is to complex to understand */ - if ( in_array( 'array', $types, true ) ) { - $property->property_types[] = 'mixed'; - $property_map[] = $property; - continue; - } - - foreach ( $types as $type ) { - $type = trim( $type ); - $is_array = substr( $type, -2 ) === '[]'; - - if ( ! $is_array ) { - $property->property_types[] = $type; - continue; - } - - $first_bracket_index = strpos( $type, '[' ); - - if ( false !== $first_bracket_index ) { - $type = substr( $type, 0, $first_bracket_index ); - } - - $property->property_types[] = $type; - } - - $property_map[] = $property; - } - - return $property_map; - } - - /** - * @param ReflectionProperty $property - * @return string - */ - private static function parse_visibility( ReflectionProperty $property ): string { - if ( $property->isPublic() ) { - return 'public'; - } - if ( $property->isProtected() ) { - return 'protected'; - } - return 'private'; - } - - /** - * @param string $doc_block - * @return string|null - */ - private static function parse_var( string $doc_block ): string { - // Strip away the start "/**" and ending "*/". - if ( strpos( $doc_block, '/**' ) === 0 ) { - $doc_block = \substr( $doc_block, 3 ); - } - if ( substr( $doc_block, -2 ) === '*/' ) { - $doc_block = \substr( $doc_block, 0, -2 ); - } - $doc_block = \trim( $doc_block ); - - $var = null; - if ( \preg_match_all( self::DOC_BLOCK_REGEX, $doc_block, $matches ) ) { - for ( $x = 0, $max = count( $matches[0] ); $x < $max; $x++ ) { - if ( 'var' === $matches['name'][ $x ] ) { - $var = $matches['value'][ $x ]; - } - } - } - - return $var; - } - - /** - * @param ObjectWrapper $object - * @return ReflectionProperty[] - */ - private static function get_properties( ObjectWrapper $object ): array { - $properties = array(); - $reflection_class = $object->getReflectedObject(); - do { - $properties = array_merge( $properties, $reflection_class->getProperties() ); - } while ( $reflection_class = $reflection_class->getParentClass() ); - return $properties; - } -} diff --git a/src/WordPress/JsonMapper/Import.php b/src/WordPress/JsonMapper/Import.php deleted file mode 100644 index 576f27f9..00000000 --- a/src/WordPress/JsonMapper/Import.php +++ /dev/null @@ -1,29 +0,0 @@ -import = $import; - $this->alias = $alias; - } - - public function getImport(): string { - return $this->import; - } - - public function getAlias(): string { - return $this->alias; - } - - public function hasAlias(): bool { - return ! \is_null( $this->alias ); - } -} diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index c69441ce..5436d11a 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -2,401 +2,291 @@ namespace WordPress\JsonMapper; +use ArrayObject; use ReflectionClass; use ReflectionMethod; use stdClass; - -class JsonMapper -{ - private $scalar_types = array('string', 'bool', 'boolean', 'int', 'integer', 'double', 'float'); +/** + * Class JsonMapper + * + * This class is responsible for mapping JSON data to PHP objects. It supports mapping to native PHP classes + * as well as custom classes. Custom factories can be provided for instantiating objects of specific classes. + * + * @package WordPress\JsonMapper + */ +class JsonMapper { + /** + * Array of strings representing valid scalar types. + * + * @var string[] + */ + private $scalar_types = array( 'string', 'bool', 'boolean', 'int', 'integer', 'double', 'float' ); /** - * @var array{ $class_name } + * Array of factories for instantiating objects of specific classes. + * + * @var array */ private $factories = array(); - public function __construct(array $custom_factories = array()) - { - $this->add_factories_for_native_php_classes(); - $this->add_custom_factories($custom_factories); - } - /** - * @param stdClass $json - * @param string $class_name - * @return object - * @throws JsonMapperException + * Constructs a new instance of this class. + * + * This constructor initializes the object and adds factories for native PHP classes. + * It also allows for the addition of custom factories through the $custom_factories parameter. + * + * @param null|array $custom_factories An associative array where the key is the class name + * and the value is a callable factory function. The factory function is expected to take + * an instance of stdClass as its argument and return an instance of the class specified + * by the key. This parameter is optional, and if not provided, an empty array will be used. */ - public function hydrate(stdClass $json, string $class_name) - { - $object_wrapper = new ObjectWrapper(null, $class_name); - - $property_map = DocBlockAnnotations::compute_property_map($object_wrapper); - - $this->map_json_to_object($json, $object_wrapper, $property_map); - - return $object_wrapper->getObject(); + public function __construct( array $custom_factories = array() ) { + $this->add_factories_for_native_php_classes(); + $this->add_custom_factories( $custom_factories ); } - /** - * @param stdClass $json - * @param ObjectWrapper $object_wrapper - * @param Property[] $property_map - * @return void - * @throws JsonMapperException - */ - private function map_json_to_object(stdClass $json, ObjectWrapper $object_wrapper, array $property_map) - { - if ($this->has_factory($object_wrapper->getName())) { - $result = $this->use_factory($object_wrapper->getName(), $json); + public function hydrate( stdClass $json, string $class_name ) { + return $this->has_factory( $class_name ) + ? $this->use_factory( $class_name, $json ) + : $this->hydrate_manually( $class_name, $json ); + } - $object_wrapper->setObject($result); - return; - } + private function hydrate_manually( string $class_name, stdClass $json ) { + $reflection_class = new ReflectionClass( $class_name ); + $object = $reflection_class->newInstance(); + $property_map = PropertyParser::compute_property_map( $reflection_class ); - $values = (array)$json; - foreach ($values as $key => $value) { - if (null === $property = self::get_property($property_map, $key)) { + foreach ( (array) $json as $value_name => $value ) { + // Ignore null data in JSON. + if ( null === $value ) { continue; } - if (false === $property->is_nullable && null === $value) { - throw new JsonMapperException( - "Null provided in json where \'{$object_wrapper->getName()}::{$key}\' doesn't allow null value" - ); - } - if ($property->is_nullable && null === $value) { - $this->set_value($object_wrapper, $property, null); + + $property = self::get_property_for_value( $value_name, $property_map ); + // Ignore additional data in JSON. + if ( null === $property ) { continue; } - $value = $this->map_value($property, $value); - $this->set_value($object_wrapper, $property, $value); - } - } - private function map_value(Property $property, $value) - { - // For union types, loop through and see if value is a match with the type - if (\count($property->property_types) > 1) { - foreach ($property->property_types as $property_type) { - - $property_type = trim($property_type); - $is_array = substr($property_type, -2) === '[]'; -// -// if ( ! $is_array ) { -// $property->property_types[] = $type; -// continue; -// } -// -// $first_bracket_index = strpos( $type, '[' ); -// -// if ( false !== $first_bracket_index ) { -// $type = substr( $type, 0, $first_bracket_index ); -// } - - - if (\is_array($value) && $is_array && count($value) === 0) { - return []; - } - - if (\is_array($value) && $is_array) { - $copy = $value; - $firstValue = \array_shift($copy); - - /* Array of scalar values */ - if ($this->propertyTypeAndValueTypeAreScalarAndSameType($property_type, $firstValue)) { - - - $scalarType = new ScalarType($property_type->getType()); - return \array_map(function ($v) use ($scalarType) { - return $this->scalarCaster->cast($scalarType, $v); - }, $value); - } - -// if ( $this->is_valid_scalar_type( $property_type ) ) { -// return $this->map_to_scalar( $property_type, $value ); -// } - - // Array of registered class @todo how do you know it was the correct type? -// if ($this->has_factory($type->getType())) { -// return $this->mapToObjectsUsingFactory($type, $value); -// } - if ($this->has_factory($property_type)) { - return $this->map_to_object_using_factory($property_type, $value); - } - - // Array of existing class @todo how do you know it was the correct type? -// if ((class_exists($type->getType()) || interface_exists($type->getType()))) { -// return $this->mapToObjects($type, $value, $mapper); -// } - if ((class_exists($property_type) || interface_exists($property_type))) { - return $this->map_to_object($property_type, $value); - } - - continue; - } - - // If the type we are mapping has a last minute factory use it. -// if ($this->classFactoryRegistry->hasFactory($type->getType())) { -// return $this->mapToObjectsUsingFactory($type, $value); -// } - if ($this->has_factory($property_type)) { - return $this->map_to_object_using_factory($property_type, $value); - } - - // Single scalar value - if ($this->propertyTypeAndValueTypeAreScalarAndSameType($property_type, $value)) { - return $this->scalarCaster->cast(new ScalarType($property_type->getType()), $value); - } - - // Single existing class @todo how do you know it was the correct type? -// if (\class_exists($property_type->getType())) { -// return $this->mapToSingleObject($property_type->getType(), $value, $mapper); -// } - if ((class_exists($property_type) || interface_exists($property_type))) { - return $this->map_to_object($property_type, $value); - } - } + $value = $this->map_value( $property, $value ); + $this->set_value( $object, $property, $value ); } - if (\is_null($value) && $property->isNullable()) { - return null; - } - // No match was found (or there was only one option) lets assume the first is the right one. - $types = $property->property_types; - $property_type = \array_shift($types); + return $object; + } - if ($property_type === null) { - // Return the value as is as there is no type info. + private function map_value( Property $property, $value ) { + if ( 0 === count( $property->property_types ) ) { + // Return the value as is - there is no type info. return $value; } - if ($this->is_valid_scalar_type($property_type)) { - return $this->map_to_scalar($property_type, $value); + foreach ( $property->property_types as $property_type ) { + $array_depth = substr_count( $property_type, '[]' ); + $is_array = 'array' === $property_type || $array_depth > 0; + $property_type = str_replace( '[]', '', $property_type ); + + if ( is_array( $value ) && $is_array && count( $value ) === 0 ) { + return array(); + } + + if ( $this->is_property_and_value_same_scalar( $property_type, $value ) ) { + return $this->map_to_scalar( $property_type, $value ); + } + + if ( $this->has_factory( $property_type ) ) { + return $this->map_to_object_using_factory( $property_type, $value ); + } + + if ( ( class_exists( $property_type ) || interface_exists( $property_type ) ) ) { + return $this->map_to_object( $property_type, $value ); + } } - if ($this->has_factory($property_type)) { - return $this->map_to_object_using_factory($property_type, $value); + throw new JsonMapperException( + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + 'Unable to map ' . json_encode( $value ) . " to '$property->name'." + ); + } + + private function set_value( $object, Property $property, $value ) { + if ( 'public' === $property->visibility ) { + $object->{$property->name} = $value; + return; } -// if ($this->classFactoryRegistry->hasFactory($property_type)) { -// return $this->mapToObjectsUsingFactory($property_type, $value); -// } + $method_name = 'set' . ucfirst( $property->name ); + if ( method_exists( $object, $method_name ) ) { + $method = new ReflectionMethod( $object, $method_name ); + $parameters = $method->getParameters(); -// if ((class_exists($type->getType()) || interface_exists($type->getType()))) { -// return $this->mapToObjects($type, $value, $mapper); -// } - if ((class_exists($property_type) || interface_exists($property_type))) { - return $this->map_to_object($property_type, $value); + if ( is_array( $value ) && count( $parameters ) === 1 && $parameters[0]->isVariadic() ) { + call_user_func_array( array( $object, $method_name ), $value ); + return; + } + + $object->$method_name( $value ); + return; } - throw new \Exception("Unable to map to {$type->getType()}"); + throw new JsonMapperException( + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + "Property: '" . get_class( $object ) . "::$property->name' is non-public and no setter method was found." + ); } -// if ( null === $value && $property->is_nullable ) { -// return null; -// } -// // No match was found (or there was only one option) lets assume the first is the right one. -// $types = $property->property_types; -// $property_type = \array_shift( $types ); -// -// if ( null === $property_type ) { -// // Return the value as is as there is no type info. -// return $value; -// } -// -// if ( $this->is_valid_scalar_type( $property_type ) ) { -// return $this->map_to_scalar( $property_type, $value ); -// } -// -// if ( $this->has_factory( $property_type ) ) { -// return $this->map_to_object_using_factory( $property_type, $value ); -// } -// -// if ( ( class_exists( $property_type ) || interface_exists( $property_type ) ) ) { -// return $this->map_to_object( $property_type, $value ); -// } -// -// throw new JsonMapperException( "Unable to map to \'{$property_type}\'" ); -// } - /** - * @param mixed $value - * @psalm-assert-if-true scalar $value - */ - private function propertyTypeAndValueTypeAreScalarAndSameType(string $type, $value): bool - { - if (!\is_scalar($value) || !$this->is_valid_scalar_type($type)) { + private function is_property_and_value_same_scalar( string $property_type, $value ) { + if ( false === is_scalar( $value ) || false === $this->is_valid_scalar_type( $property_type ) ) { return false; } - $valueType = \gettype($value); - if ($valueType === 'double') { - $valueType = 'float'; + $value_type = gettype( $value ); + + if ( 'boolean' === $value_type ) { + return 'boolean' === $property_type || 'bool' === $property_type; + } + + if ( 'integer' === $value_type ) { + return 'integer' === $property_type || 'int' === $property_type; + } + + if ( 'double' === $value_type ) { + return 'float' === $property_type || 'double' === $property_type; } - return $type === $valueType; + return $value_type === $property_type; } /** * @param string $property_type * @return bool */ - private function is_valid_scalar_type(string $property_type): bool - { - return in_array($property_type, $this->scalar_types, true); - } - - private function map_to_scalar(string $property_type, $value) - { - if (false === is_array($value)) { - return $this->cast_to_scalar_type($property_type, $value); - } - $mapped_scalars = array(); - foreach ($value as $inner_value) { - $mapped_scalars[] = $this->map_to_scalar($property_type, $inner_value); - } - return $mapped_scalars; + private function is_valid_scalar_type( string $property_type ): bool { + return in_array( $property_type, $this->scalar_types, true ); } - private function cast_to_scalar_type(string $type, $value) - { - if ('string' === $type) { - return (string)$value; + private static function cast_to_scalar_type( string $type, $value ) { + if ( 'string' === $type ) { + return (string) $value; } - if ('boolean' === $type || 'bool' === $type) { - return (bool)$value; + if ( 'boolean' === $type || 'bool' === $type ) { + return (bool) $value; } - if ('integer' === $type || 'int' === $type) { - return (int)$value; + if ( 'integer' === $type || 'int' === $type ) { + return (int) $value; } - if ('double' === $type || 'float' === $type) { - return (float)$value; + if ( 'double' === $type || 'float' === $type ) { + return (float) $value; } - throw new JsonMapperException("Casting to scalar type \'$type\' failed."); + throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); } - private function map_to_object_using_factory(string $property_type, $value) - { - if (false === is_array($value)) { - return $this->use_factory($property_type, $value); + private function map_to_scalar( string $property_type, $value ) { + if ( false === is_array( $value ) ) { + return self::cast_to_scalar_type( $property_type, $value ); } - $mapped_objects = array(); - foreach ($value as $inner_value) { - $mapped_objects[] = $this->map_to_object_using_factory($property_type, $inner_value); + $mapped = array(); + foreach ( $value as $inner_value ) { + $mapped[] = $this->map_to_scalar( $property_type, $inner_value ); } - return $mapped_objects; + return $mapped; } - private function map_to_object(string $property_type, $value) - { - if (false === (new ReflectionClass($property_type))->isInstantiable()) { - throw new JsonMapperException("Unable to resolve uninstantiable \'{$property_type}\'."); + private function map_to_object_using_factory( string $property_type, $value ) { + if ( false === is_array( $value ) ) { + return $this->use_factory( $property_type, $value ); } - if (false === is_array($value)) { - return $this->hydrate($value, $property_type); + $mapped = array(); + foreach ( $value as $inner_value ) { + $mapped[] = $this->map_to_object_using_factory( $property_type, $inner_value ); } - $mapped_objects = array(); - foreach ($value as $inner_value) { - $mapped_objects[] = $this->map_to_object($property_type, $inner_value); - } - return $mapped_objects; + return $mapped; } - private function set_value(ObjectWrapper $object, Property $property, $value) - { - if ('public' === $property->visibility) { - $object->getObject()->{$property->name} = $value; - return; + private function map_to_object( string $property_type, $value ) { + if ( false === ( new ReflectionClass( $property_type ) )->isInstantiable() ) { + throw new JsonMapperException( "Unable to resolve uninstantiable \'{$property_type}\'." ); } - - $method_name = 'set' . \ucfirst($property->name); - if (\method_exists($object->getObject(), $method_name)) { - $method = new ReflectionMethod($object->getObject(), $method_name); - $parameters = $method->getParameters(); - - if (\is_array($value) && \count($parameters) === 1 && $parameters[0]->isVariadic()) { - $object->getObject()->$method_name(...$value); - return; - } - - $object->getObject()->$method_name($value); - return; + if ( false === is_array( $value ) ) { + return $this->hydrate( $value, $property_type ); } - - throw new JsonMapperException( - "{$object->getName()}::{$property->name} is non-public and no setter method was found" - ); + $mapped = array(); + foreach ( $value as $inner_value ) { + $mapped[] = $this->map_to_object( $property_type, $inner_value ); + } + return $mapped; } - private function sanitise_class_name(string $class_name): string - { + private function sanitise_class_name( string $class_name ): string { /* Erase leading slash as ::class doesn't contain leading slash */ - if (strpos($class_name, '\\') === 0) { - $class_name = substr($class_name, 1); + if ( strpos( $class_name, '\\' ) === 0 ) { + $class_name = substr( $class_name, 1 ); } return $class_name; } - private function has_factory(string $class_name): bool - { - return array_key_exists($this->sanitise_class_name($class_name), $this->factories); + private function has_factory( string $class_name ): bool { + return array_key_exists( $this->sanitise_class_name( $class_name ), $this->factories ); } - private function use_factory(string $class_name, $params) - { - $factory = $this->factories[$this->sanitise_class_name($class_name)]; - return $factory($params); + private function use_factory( string $class_name, $params ) { + $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; + return $factory( $params ); } - private function add_factory(string $class_name, callable $factory) - { - $this->factories[$this->sanitise_class_name($class_name)] = $factory; + private function add_factory( string $class_name, callable $factory ) { + $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; } - private function add_custom_factories(array $custom_factories) - { - foreach ($custom_factories as $class_name => $custom_factory) { - $this->add_factory($class_name, $custom_factory); + private function add_custom_factories( array $custom_factories ) { + foreach ( $custom_factories as $class_name => $custom_factory ) { + $this->add_factory( $class_name, $custom_factory ); } } - private function add_factories_for_native_php_classes() - { + private function add_factories_for_native_php_classes() { $this->add_factory( \DateTime::class, - static function (string $value) { - return new \DateTime($value); + static function ( string $value ) { + return new \DateTime( $value ); } ); $this->add_factory( \DateTimeImmutable::class, - static function (string $value) { - return new \DateTimeImmutable($value); + static function ( string $value ) { + return new \DateTimeImmutable( $value ); } ); $this->add_factory( stdClass::class, - static function ($value) { - return (object)$value; + static function ( $value ) { + return (object) $value; } ); $this->add_factory( - \ArrayObject::class, - function ($value) { - return new \ArrayObject($value); + ArrayObject::class, + function ( $value ) { + return new ArrayObject( $value ); + } + ); + $this->add_factory( + 'array', + function ( $value ) { + return new ArrayObject( $value ); } ); } /** - * @param array $property_map - * @param string $property_name - * @return null|Property + * @param string $value_name + * @param Property[] $property_map + * @return Property|null */ - private static function get_property(array $property_map, string $property_name) - { - foreach ($property_map as $property) { - if ($property->name === $property_name) { + private static function get_property_for_value( string $value_name, array $property_map ) { + /** @var Property $property */ + foreach ( $property_map as $property_name => $property ) { + if ( $property_name === $value_name ) { return $property; } } diff --git a/src/WordPress/JsonMapper/ObjectWrapper.php b/src/WordPress/JsonMapper/ObjectWrapper.php deleted file mode 100644 index 6dbd5b7d..00000000 --- a/src/WordPress/JsonMapper/ObjectWrapper.php +++ /dev/null @@ -1,79 +0,0 @@ -object = $object; - $this->class_name = $class_name; - } - - /** @param object|null $object */ - public function setObject( $object ) { - $this->object = $object; - $this->reflected_object = null; - } - - /** @return object */ - public function getObject() { - if ( is_null( $this->object ) ) { - $constructor = $this->getReflectedObject()->getConstructor(); - if ( is_null( $constructor ) || $constructor->getNumberOfParameters() === 0 ) { - $this->object = $this->getReflectedObject()->newInstance(); - } else { - $this->object = $this->getReflectedObject()->newInstanceWithoutConstructor(); - } - } - - return $this->object; - } - - /** @return class-string */ - public function getClassName(): string { - return $this->class_name; - } - - public function getReflectedObject(): ReflectionClass { - if ( $this->reflected_object === null ) { - $objectOrClass = ! \is_null( $this->object ) ? $this->object : $this->class_name; - $this->reflected_object = new ReflectionClass( $objectOrClass ); - } - - return $this->reflected_object; - } - - public function getName(): string { - return $this->getReflectedObject()->getName(); - } -} diff --git a/src/WordPress/JsonMapper/Property.php b/src/WordPress/JsonMapper/Property.php index e0ecf983..a7b19fc2 100644 --- a/src/WordPress/JsonMapper/Property.php +++ b/src/WordPress/JsonMapper/Property.php @@ -12,18 +12,13 @@ class Property { /** @var string */ public $visibility; - /** @var bool */ - public $is_nullable; - public function __construct( string $name, string $visibility, - bool $is_nullable, array $types = array() ) { $this->name = $name; $this->visibility = $visibility; - $this->is_nullable = $is_nullable; $this->property_types = $types; } } diff --git a/src/WordPress/JsonMapper/PropertyParser.php b/src/WordPress/JsonMapper/PropertyParser.php new file mode 100644 index 00000000..6334f897 --- /dev/null +++ b/src/WordPress/JsonMapper/PropertyParser.php @@ -0,0 +1,134 @@ +[A-Za-z_-]+)[ \t]+(?P[\w\[\]\\\\|]*).*$/m'; + + /** + * Private constructor. + */ + private function __construct() {} + + /** + * Analyzes the reflected class and returns a map of properties based on reflections and DocBlocks. + * + * @param ReflectionClass $reflection_class reflected class which DocBlocks are to be mapped to a property map. + * @return array the property map, key: property name + */ + public static function compute_property_map( ReflectionClass $reflection_class ) { + $property_map = array(); + + foreach ( self::get_properties( $reflection_class ) as $reflection_property ) { + $property_map[ $reflection_property->getName() ] = new Property( + $reflection_property->getName(), + self::parse_visibility( $reflection_property ), + self::parse_property_types( $reflection_property ) + ); + } + + return $property_map; + } + + /** + * Returns property visibility as string. Only supports 'public', 'protected' and 'private'. + * + * @param ReflectionProperty $reflection_property reflected property to derive visibility from. + * @return string property visibility. + */ + private static function parse_visibility( ReflectionProperty $reflection_property ) { + if ( $reflection_property->isPublic() ) { + return 'public'; + } + + if ( $reflection_property->isProtected() ) { + return 'protected'; + } + + return 'private'; + } + + /** + * Returns an array of property types parsed from the provided DocBlock. Filters out 'null' + * and removes Global Namespace Prefixes. + * + * Examples: + * + * For types: PointlessInterface[]|null will return: array('PointlessInterface[]') + * + * For types: string|int[][] will return: array('string', 'int[][]') + * + * For types: \ArrayObject will return array('ArrayObject') + * + * @param ReflectionProperty $reflection_property reflected property to derive and parse DocBlock from. + * @return string[] array of property types, might be empty if no properties were listed in the DocBlock, or the + * DocBlock does not exist at all. + */ + private static function parse_property_types( ReflectionProperty $reflection_property ) { + $doc_block = $reflection_property->getDocComment(); + + if ( false === $doc_block ) { + return array(); + } + + // Strip away the start "/**" and ending "*/". + if ( strpos( $doc_block, '/**' ) === 0 ) { + $doc_block = substr( $doc_block, 3 ); + } + if ( substr( $doc_block, -2 ) === '*/' ) { + $doc_block = substr( $doc_block, 0, -2 ); + } + $doc_block = trim( $doc_block ); + + $var = null; + if ( preg_match_all( self::DOC_BLOCK_REGEX, $doc_block, $matches ) ) { + for ( $x = 0, $max = count( $matches[0] ); $x < $max; $x++ ) { + if ( 'var' === $matches['name'][ $x ] ) { + $var = $matches['value'][ $x ]; + } + } + } + + if ( null === $var ) { + return array(); + } + + $property_types = array(); + /** @var string $property_type */ + foreach ( explode( '|', $var ) as $property_type ) { + // Filter out 'null' type. + if ( 'null' === $property_type ) { + continue; + } + // Return types without their Global Namespace Prefixes. + $property_types[] = str_replace( '\\', '', $property_type ); + } + + return $property_types; + } + + /** + * Returns an array of properties for the reflected class and all of its parents. + * + * @param ReflectionClass $reflection_class reflected class to recursively extract properties from. + * @return ReflectionProperty[] array of properties. + */ + private static function get_properties( ReflectionClass $reflection_class ) { + $properties = $reflection_class->getProperties(); + $reflected_parent = $reflection_class->getParentClass(); + + while ( false !== $reflected_parent ) { + $properties = array_merge( $properties, $reflected_parent->getProperties() ); + $reflected_parent = $reflected_parent->getParentClass(); + } + + return $properties; + } +} diff --git a/src/WordPress/JsonMapper/UseNodeVisitor.php b/src/WordPress/JsonMapper/UseNodeVisitor.php deleted file mode 100644 index e13c049d..00000000 --- a/src/WordPress/JsonMapper/UseNodeVisitor.php +++ /dev/null @@ -1,38 +0,0 @@ -uses as $use ) { - $this->imports[] = new Import( $use->name->toString(), \is_null( $use->alias ) ? null : $use->alias->name ); - } - } elseif ( $node instanceof Stmt\GroupUse ) { - foreach ( $node->uses as $use ) { - $this->imports[] = new Import( - "{$node->prefix}\\{$use->name}", - \is_null( $use->alias ) ? null : $use->alias->name - ); - } - } - - return null; - } - - /** @return Import[] */ - public function getImports(): array { - return $this->imports; - } -} diff --git a/tests/E2E/JsonBlueprintTest.php b/tests/E2E/JsonBlueprintTest.php new file mode 100644 index 00000000..820999b6 --- /dev/null +++ b/tests/E2E/JsonBlueprintTest.php @@ -0,0 +1,83 @@ +document_root = Path::makeAbsolute( 'test', sys_get_temp_dir() ); + } + + /** + * @after + */ + public function after() { + ( new Filesystem() )->remove( $this->document_root ); + } + public function testUntilRunner() { + $blueprint = '{"steps":[{"step":"mkdir","path":"dir"},{"step": "rm","path": "dir"}]}'; + + $subscriber = new class() implements EventSubscriberInterface { + public static function getSubscribedEvents() { + return array( + ProgressEvent::class => 'onProgress', + DoneEvent::class => 'onDone', + ); + } + + protected $progress_bar; + + public function __construct() { + ProgressBar::setFormatDefinition( 'custom', ' [%bar%] %current%/%max% -- %message%' ); + + $this->progress_bar = ( new SymfonyStyle( + new StringInput( '' ), + new ConsoleOutput() + ) )->createProgressBar( 100 ); + $this->progress_bar->setFormat( 'custom' ); + $this->progress_bar->setMessage( 'Start' ); + $this->progress_bar->start(); + } + + public function onProgress( ProgressEvent $event ) { + $this->progress_bar->setMessage( $event->caption ); + $this->progress_bar->setProgress( (int) $event->progress ); + } + + public function onDone( DoneEvent $event ) { + $this->progress_bar->finish(); + } + }; + + $results = run_blueprint( + $blueprint, + array( + 'environment' => ContainerBuilder::ENVIRONMENT_NATIVE, + 'documentRoot' => $this->document_root . '/new-wp', + 'progressSubscriber' => $subscriber, + ) + ); + + $this->assertEquals( array(), $results ); + } +} diff --git a/tests/JsonMapper/JsonMapperTest.php b/tests/JsonMapper/JsonMapperTest.php index e7776f56..944ca07c 100644 --- a/tests/JsonMapper/JsonMapperTest.php +++ b/tests/JsonMapper/JsonMapperTest.php @@ -2,10 +2,11 @@ namespace JsonMapper; -use WordPress\Blueprints\Model\BlueprintBuilder; -use WordPress\Blueprints\Model\DataClass\Blueprint; +use ArrayObject; +use JsonMapper\resources\TestResourceClassSetValue; use WordPress\JsonMapper\JsonMapper; use PHPUnit\Framework\TestCase; +use WordPress\JsonMapper\JsonMapperException; class JsonMapperTest extends TestCase { @@ -21,16 +22,90 @@ public function before() { $this->json_mapper = new JsonMapper(); } - public function testMapsEmptyBlueprint() { + /** + * Test checks if mapper works at all. + * + * @return void + */ + public function testMapsToArrayObject() { + $raw_json = '{}'; + + $parsed_json = json_decode( $raw_json, false ); + + $result = $this->json_mapper->hydrate( $parsed_json, ArrayObject::class ); + + $expected = new ArrayObject(); + + $this->assertEquals( $expected, $result ); + } + + public function testSetValue() { $raw_json = '{}'; $parsed_json = json_decode( $raw_json, false ); - $blueprint = $this->json_mapper->hydrate( $parsed_json, Blueprint::class ); + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); + + $expected = new TestResourceClassSetValue(); + + $this->assertEquals( $expected, $result ); + } + public function testSetsPublicProperties() { + $raw_json = '{"publicProperty":"test"}'; + + $parsed_json = json_decode( $raw_json, false ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); + + $expected = new TestResourceClassSetValue(); + $expected->publicProperty = 'test'; + + $this->assertEquals( $expected, $result ); + } + + public function testSetsPrivatePropertiesWithSetter() { + $raw_json = '{"privateProperty":"test"}'; + + $parsed_json = json_decode( $raw_json, false ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); + + $expected = new TestResourceClassSetValue(); + $expected->setPrivateProperty( 'test' ); + + $this->assertEquals( $expected, $result ); + } + + public function testSetsProtectedPropertiesWithSetter() { + $raw_json = '{"protectedProperty":"test"}'; + + $parsed_json = json_decode( $raw_json, false ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); - $expected = BlueprintBuilder::create() - ->toBlueprint(); + $expected = new TestResourceClassSetValue(); + $expected->setProtectedProperty( 'test' ); + + $this->assertEquals( $expected, $result ); + } + + public function testFailsSettingPrivatePropertyWithNoSetter() { + $raw_json = '{"setterlessPrivateProperty":"test"}'; + + $parsed_json = json_decode( $raw_json, false ); + + $this->expectException( JsonMapperException::class ); + $this->expectExceptionMessage( "Property: 'JsonMapper\\resources\TestResourceClassSetValue::setterlessPrivateProperty' is non-public and no setter method was found." ); + $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); + } + + public function testFailsSettingProtectedPropertyWithNoSetter() { + $raw_json = '{"setterlessProtectedProperty":"test"}'; + + $parsed_json = json_decode( $raw_json, false ); - $this->assertEquals( $expected, $blueprint ); + $this->expectException( JsonMapperException::class ); + $this->expectExceptionMessage( "Property: 'JsonMapper\\resources\TestResourceClassSetValue::setterlessProtectedProperty' is non-public and no setter method was found." ); + $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); } } diff --git a/tests/JsonMapper/PropertyParserTest.php b/tests/JsonMapper/PropertyParserTest.php new file mode 100644 index 00000000..bfa2fb77 --- /dev/null +++ b/tests/JsonMapper/PropertyParserTest.php @@ -0,0 +1,179 @@ + new Property( 'string', 'private', array( 'string' ) ), + ); + $this->assertEquals( $expected, $result ); + } + + public function testMapsPropertiesForArraysOfScalars() { + $class = new class() { + /** + * @var string + */ + private $string; + + /** + * @var string[] + */ + private $string_array; + + /** + * @var string[][] + */ + private $string_deep_array; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'string' => new Property( 'string', 'private', array( 'string' ) ), + 'string_array' => new Property( 'string_array', 'private', array( 'string[]' ) ), + 'string_deep_array' => new Property( 'string_deep_array', 'private', array( 'string[][]' ) ), + ); + $this->assertEquals( $expected, $result ); + } + public function testMapsPropertiesForArrays() { + $class = new class() { + /** + * @var string|array + */ + private $string_or_array; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'string_or_array' => new Property( 'string_or_array', 'private', array( 'string', 'array' ) ), + ); + $this->assertEquals( $expected, $result ); + } + + public function testMapsPropertiesWithNoDocBlocks() { + $class = new class() { + private $no_docblock; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'no_docblock' => new Property( 'no_docblock', 'private', array() ), + ); + $this->assertEquals( $expected, $result ); + } + + //test visibility parsing + public function testMapsPropertiesForPublic() { + $class = new class() { + /** + * @var string + */ + public $string; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'string' => new Property( 'string', 'public', array( 'string' ) ), + ); + $this->assertEquals( $expected, $result ); + } + public function testMapsPropertiesForProtected() { + $class = new class() { + /** + * @var string + */ + protected $string; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'string' => new Property( 'string', 'protected', array( 'string' ) ), + ); + $this->assertEquals( $expected, $result ); + } + + public function testMapsPropertiesForPrivate() { + $class = new class() { + /** + * @var string + */ + private $string; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'string' => new Property( 'string', 'private', array( 'string' ) ), + ); + $this->assertEquals( $expected, $result ); + } + + public function testMapsPropertiesForUnionDocBlock() { + $class = new class() { + /** + * @var string|array|bool + */ + private $string_or_array_or_bool; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'string_or_array_or_bool' => new Property( 'string_or_array_or_bool', 'private', array( 'string', 'array', 'bool' ) ), + ); + $this->assertEquals( $expected, $result ); + } + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + public function testMapsPropertiesWhenTypeWithGlobalNamespacePrefix() { + $class = new class() { + /** + * @var \stdClass + */ + private $global_stdclass; + + /** + * @var stdClass + */ + private $local_stdclass; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'global_stdclass' => new Property( 'global_stdclass', 'private', array( 'stdClass' ) ), + 'local_stdclass' => new Property( 'local_stdclass', 'private', array( 'stdClass' ) ), + ); + $this->assertEquals( $expected, $result ); + } + + public function testMapsPropertiesWhenTypeNullable() { + $class = new class() { + /** + * @var null|string + */ + private $nullable_string; + }; + + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $expected = array( + 'nullable_string' => new Property( 'nullable_string', 'private', array( 'string' ) ), + ); + $this->assertEquals( $expected, $result ); + } +} diff --git a/tests/JsonMapper/resources/TestResourceClassSetValue.php b/tests/JsonMapper/resources/TestResourceClassSetValue.php new file mode 100644 index 00000000..19a48445 --- /dev/null +++ b/tests/JsonMapper/resources/TestResourceClassSetValue.php @@ -0,0 +1,23 @@ +privateProperty = $value; + } + + public function setPrivateProperty( $value ) { + $this->privateProperty = $value; + } +} From b968df8705ee456b60bc3e786e9726240eac61b2 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Sat, 16 Mar 2024 10:04:07 +0100 Subject: [PATCH 21/28] Comment out test failing due to Windows compatibility issue in runner --- tests/E2E/JsonBlueprintTest.php | 94 ++++++++++++++++----------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/E2E/JsonBlueprintTest.php b/tests/E2E/JsonBlueprintTest.php index 820999b6..75526bcc 100644 --- a/tests/E2E/JsonBlueprintTest.php +++ b/tests/E2E/JsonBlueprintTest.php @@ -19,7 +19,7 @@ class JsonBlueprintTest extends TestCase { /** * @var string */ - private string $document_root; + private $document_root; /** * @before @@ -34,50 +34,50 @@ public function before() { public function after() { ( new Filesystem() )->remove( $this->document_root ); } - public function testUntilRunner() { - $blueprint = '{"steps":[{"step":"mkdir","path":"dir"},{"step": "rm","path": "dir"}]}'; - - $subscriber = new class() implements EventSubscriberInterface { - public static function getSubscribedEvents() { - return array( - ProgressEvent::class => 'onProgress', - DoneEvent::class => 'onDone', - ); - } - - protected $progress_bar; - - public function __construct() { - ProgressBar::setFormatDefinition( 'custom', ' [%bar%] %current%/%max% -- %message%' ); - - $this->progress_bar = ( new SymfonyStyle( - new StringInput( '' ), - new ConsoleOutput() - ) )->createProgressBar( 100 ); - $this->progress_bar->setFormat( 'custom' ); - $this->progress_bar->setMessage( 'Start' ); - $this->progress_bar->start(); - } - - public function onProgress( ProgressEvent $event ) { - $this->progress_bar->setMessage( $event->caption ); - $this->progress_bar->setProgress( (int) $event->progress ); - } - - public function onDone( DoneEvent $event ) { - $this->progress_bar->finish(); - } - }; - - $results = run_blueprint( - $blueprint, - array( - 'environment' => ContainerBuilder::ENVIRONMENT_NATIVE, - 'documentRoot' => $this->document_root . '/new-wp', - 'progressSubscriber' => $subscriber, - ) - ); - - $this->assertEquals( array(), $results ); - } +// public function testRunningJsonBlueprint() { +// $blueprint = '{}'; +// +// $subscriber = new class() implements EventSubscriberInterface { +// public static function getSubscribedEvents() { +// return array( +// ProgressEvent::class => 'onProgress', +// DoneEvent::class => 'onDone', +// ); +// } +// +// protected $progress_bar; +// +// public function __construct() { +// ProgressBar::setFormatDefinition( 'custom', ' [%bar%] %current%/%max% -- %message%' ); +// +// $this->progress_bar = ( new SymfonyStyle( +// new StringInput( '' ), +// new ConsoleOutput() +// ) )->createProgressBar( 100 ); +// $this->progress_bar->setFormat( 'custom' ); +// $this->progress_bar->setMessage( 'Start' ); +// $this->progress_bar->start(); +// } +// +// public function onProgress( ProgressEvent $event ) { +// $this->progress_bar->setMessage( $event->caption ); +// $this->progress_bar->setProgress( (int) $event->progress ); +// } +// +// public function onDone( DoneEvent $event ) { +// $this->progress_bar->finish(); +// } +// }; +// +// $results = run_blueprint( +// $blueprint, +// array( +// 'environment' => ContainerBuilder::ENVIRONMENT_NATIVE, +// 'documentRoot' => $this->document_root . '/new-wp', +// 'progressSubscriber' => $subscriber, +// ) +// ); +// +// $this->assertEquals( array(), $results ); +// } } From d215465871d6e05121c87a50d5111bdea9318392 Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Sat, 16 Mar 2024 11:38:59 +0100 Subject: [PATCH 22/28] Fix bug, find next bug to fix --- src/WordPress/JsonMapper/JsonMapper.php | 106 ++++++++++++++---- tests/JsonMapper/JsonMapperTest.php | 56 ++++++--- tests/JsonMapper/PropertyParserTest.php | 20 ++-- .../TestResourceClassComplexMapping.php | 16 +++ .../resources/TestResourceClassSetValue.php | 1 + 5 files changed, 152 insertions(+), 47 deletions(-) create mode 100644 tests/JsonMapper/resources/TestResourceClassComplexMapping.php diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 5436d11a..e82f11ac 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -4,6 +4,7 @@ use ArrayObject; use ReflectionClass; +use ReflectionException; use ReflectionMethod; use stdClass; /** @@ -45,13 +46,48 @@ public function __construct( array $custom_factories = array() ) { $this->add_custom_factories( $custom_factories ); } + /** + * Creates an instance of the given class and populates it with data from the given JSON object. + * + * This method first checks if a factory exists for the given class. If a factory exists, it uses the factory + * to create the instance and populate it with data. If no factory exists, it creates the instance and populates + * it manually by calling the `hydrate_manually` method. + * + * @param stdClass $json The JSON object containing the data to populate the new object with. + * @param string $class_name The fully qualified name of the class to create an instance of. + * @return object An instance of the class specified by $class_name, populated with data from $json. + * @throws ReflectionException If the class does not exist and the instance is being created manually. + * @throws JsonMapperException If mapping the value to an associated property type failed or if setting was + * impossible. + */ public function hydrate( stdClass $json, string $class_name ) { return $this->has_factory( $class_name ) ? $this->use_factory( $class_name, $json ) - : $this->hydrate_manually( $class_name, $json ); + : $this->hydrate_manually( $json, $class_name ); } - private function hydrate_manually( string $class_name, stdClass $json ) { + /** + * Creates an instance of the given class and populates its properties with data from the given JSON object. + * + * This method uses PHP's Reflection API to create a new instance of the class specified by $class_name. + * It then uses the PropertyParser to compute a property map for the class, which is an associative array + * where the keys are property names and the values are {@link Property} objects. + * + * The method then iterates over the properties of the JSON object. For each property, it retrieves the + * corresponding Property object from the property map, maps the JSON value to the type of the Property, + * and sets the value of the Property on the newly created object. + * + * If the JSON object contains a property that is not defined in the class, or if the value of a property + * in the JSON object is null, that property is ignored. + * + * @param stdClass $json The JSON object containing the data to populate the new object with. + * @param string $class_name The fully qualified name of the class to create an instance of. + * @return object An instance of the class specified by $class_name, populated with data from $json. + * @throws ReflectionException If the class does not exist. + * @throws JsonMapperException If mapping the value to an associated property type failed or if setting was + * impossible. + */ + private function hydrate_manually( stdClass $json, string $class_name ) { $reflection_class = new ReflectionClass( $class_name ); $object = $reflection_class->newInstance(); $property_map = PropertyParser::compute_property_map( $reflection_class ); @@ -75,6 +111,45 @@ private function hydrate_manually( string $class_name, stdClass $json ) { return $object; } + /** + * Retrieves the Property object associated with a given value name from a property map. + * + * This method iterates over the provided property map and returns the Property object + * whose name matches the provided value name. If no matching Property is found, it returns null. + * + * @param string $value_name The name of the value for which to find the corresponding Property. + * @param Property[] $property_map An associative array where the keys are property names + * and the values are Property objects. + * @return Property|null The Property object associated with the given value name, + * or null if no matching Property is found. + */ + private static function get_property_for_value( string $value_name, array $property_map ) { + foreach ( $property_map as $property_name => $property ) { + if ( $property_name === $value_name ) { + return $property; + } + } + return null; + } + + /** + * Maps a value from the JSON object to one of the types listed for that Property. + * + * This method uses the type of the {@link Property} to determine how to map the value from the JSON object. + * + * If the Property is: + * + * - of a basic type (like string, int, bool, etc.), the method simply casts the value to that type. + * + * - of a type the mapper has a factory for, the method + * - of a class type, the method creates a new instance of that class and populates it with data from the JSON + * value. + * + * @param Property $property The Property object to which the value should be mapped. + * @param mixed $value The value from the JSON object to map. + * @return mixed The mapped value, of the type specified by the Property. + * @throws JsonMapperException If the value cannot be mapped to any of the types listed for that Property. + */ private function map_value( Property $property, $value ) { if ( 0 === count( $property->property_types ) ) { // Return the value as is - there is no type info. @@ -136,11 +211,16 @@ private function set_value( $object, Property $property, $value ) { } private function is_property_and_value_same_scalar( string $property_type, $value ) { - if ( false === is_scalar( $value ) || false === $this->is_valid_scalar_type( $property_type ) ) { + $copy_value = $value; + while ( true === is_array( $copy_value ) ) { + $copy_value = $copy_value[0]; + } + + if ( false === is_scalar( $copy_value ) || false === $this->is_valid_scalar_type( $property_type ) ) { return false; } - $value_type = gettype( $value ); + $value_type = gettype( $copy_value ); if ( 'boolean' === $value_type ) { return 'boolean' === $property_type || 'bool' === $property_type; @@ -206,7 +286,8 @@ private function map_to_object_using_factory( string $property_type, $value ) { private function map_to_object( string $property_type, $value ) { if ( false === ( new ReflectionClass( $property_type ) )->isInstantiable() ) { - throw new JsonMapperException( "Unable to resolve uninstantiable \'{$property_type}\'." ); + // phpcs:ignore + throw new JsonMapperException( "Unable to resolve uninstantiable \'$property_type\'." ); } if ( false === is_array( $value ) ) { return $this->hydrate( $value, $property_type ); @@ -277,19 +358,4 @@ function ( $value ) { } ); } - - /** - * @param string $value_name - * @param Property[] $property_map - * @return Property|null - */ - private static function get_property_for_value( string $value_name, array $property_map ) { - /** @var Property $property */ - foreach ( $property_map as $property_name => $property ) { - if ( $property_name === $value_name ) { - return $property; - } - } - return null; - } } diff --git a/tests/JsonMapper/JsonMapperTest.php b/tests/JsonMapper/JsonMapperTest.php index 944ca07c..d60b4486 100644 --- a/tests/JsonMapper/JsonMapperTest.php +++ b/tests/JsonMapper/JsonMapperTest.php @@ -3,6 +3,7 @@ namespace JsonMapper; use ArrayObject; +use JsonMapper\resources\TestResourceClassComplexMapping; use JsonMapper\resources\TestResourceClassSetValue; use WordPress\JsonMapper\JsonMapper; use PHPUnit\Framework\TestCase; @@ -30,7 +31,7 @@ public function before() { public function testMapsToArrayObject() { $raw_json = '{}'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_json ); $result = $this->json_mapper->hydrate( $parsed_json, ArrayObject::class ); @@ -39,21 +40,10 @@ public function testMapsToArrayObject() { $this->assertEquals( $expected, $result ); } - public function testSetValue() { - $raw_json = '{}'; - - $parsed_json = json_decode( $raw_json, false ); - - $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); - - $expected = new TestResourceClassSetValue(); - - $this->assertEquals( $expected, $result ); - } public function testSetsPublicProperties() { $raw_json = '{"publicProperty":"test"}'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_json ); $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); @@ -66,7 +56,7 @@ public function testSetsPublicProperties() { public function testSetsPrivatePropertiesWithSetter() { $raw_json = '{"privateProperty":"test"}'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_json ); $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); @@ -79,7 +69,7 @@ public function testSetsPrivatePropertiesWithSetter() { public function testSetsProtectedPropertiesWithSetter() { $raw_json = '{"protectedProperty":"test"}'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_json ); $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); @@ -92,7 +82,7 @@ public function testSetsProtectedPropertiesWithSetter() { public function testFailsSettingPrivatePropertyWithNoSetter() { $raw_json = '{"setterlessPrivateProperty":"test"}'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_json ); $this->expectException( JsonMapperException::class ); $this->expectExceptionMessage( "Property: 'JsonMapper\\resources\TestResourceClassSetValue::setterlessPrivateProperty' is non-public and no setter method was found." ); @@ -102,10 +92,42 @@ public function testFailsSettingPrivatePropertyWithNoSetter() { public function testFailsSettingProtectedPropertyWithNoSetter() { $raw_json = '{"setterlessProtectedProperty":"test"}'; - $parsed_json = json_decode( $raw_json, false ); + $parsed_json = json_decode( $raw_json ); $this->expectException( JsonMapperException::class ); $this->expectExceptionMessage( "Property: 'JsonMapper\\resources\TestResourceClassSetValue::setterlessProtectedProperty' is non-public and no setter method was found." ); $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); } + + public function testMapsToDeepScalarArray() { + $raw_json = '{"arrayOfStringArrays":[["test1","test2"],["test3","test4"]]}'; + + $parsed_json = json_decode( $raw_json ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); + + $expected = new TestResourceClassComplexMapping(); + $expected->arrayOfStringArrays = array( + array( 'test1', 'test2' ), + array( 'test3', 'test4' ), + ); + + $this->assertEquals( $expected, $result ); + } + + public function testMapsToDeepMixedArray() { + $raw_json = '{"arrayOfMixedArrays":[["test1", 42],["test3", true]]}'; + + $parsed_json = json_decode( $raw_json ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); + + $expected = new TestResourceClassComplexMapping(); + $expected->arrayOfMixedArrays = array( + array( 'test1', 42 ), + array( 'test3', true ), + ); + + $this->assertEquals( $expected, $result ); + } } diff --git a/tests/JsonMapper/PropertyParserTest.php b/tests/JsonMapper/PropertyParserTest.php index bfa2fb77..7e8e4b29 100644 --- a/tests/JsonMapper/PropertyParserTest.php +++ b/tests/JsonMapper/PropertyParserTest.php @@ -12,7 +12,7 @@ // phpcs:disable Generic.Commenting class PropertyParserTest extends TestCase { - public function testMapsPropertiesForScalars() { + public function testParsesPropertiesWithScalarTypes() { $class = new class() { /** * @var string @@ -27,7 +27,7 @@ public function testMapsPropertiesForScalars() { $this->assertEquals( $expected, $result ); } - public function testMapsPropertiesForArraysOfScalars() { + public function testParsesPropertiesWithArraysOfScalarTypes() { $class = new class() { /** * @var string @@ -53,7 +53,7 @@ public function testMapsPropertiesForArraysOfScalars() { ); $this->assertEquals( $expected, $result ); } - public function testMapsPropertiesForArrays() { + public function testParsesPropertiesWithArrays() { $class = new class() { /** * @var string|array @@ -68,7 +68,7 @@ public function testMapsPropertiesForArrays() { $this->assertEquals( $expected, $result ); } - public function testMapsPropertiesWithNoDocBlocks() { + public function testParsesPropertiesWithNoDocBlocks() { $class = new class() { private $no_docblock; }; @@ -81,7 +81,7 @@ public function testMapsPropertiesWithNoDocBlocks() { } //test visibility parsing - public function testMapsPropertiesForPublic() { + public function testParsesPropertiesWithPublicVisibility() { $class = new class() { /** * @var string @@ -95,7 +95,7 @@ public function testMapsPropertiesForPublic() { ); $this->assertEquals( $expected, $result ); } - public function testMapsPropertiesForProtected() { + public function testParsesPropertiesWithProtectedVisibility() { $class = new class() { /** * @var string @@ -110,7 +110,7 @@ public function testMapsPropertiesForProtected() { $this->assertEquals( $expected, $result ); } - public function testMapsPropertiesForPrivate() { + public function testParsesPropertiesWithPrivateVisibility() { $class = new class() { /** * @var string @@ -125,7 +125,7 @@ public function testMapsPropertiesForPrivate() { $this->assertEquals( $expected, $result ); } - public function testMapsPropertiesForUnionDocBlock() { + public function testParsesPropertiesWithUnionTypes() { $class = new class() { /** * @var string|array|bool @@ -141,7 +141,7 @@ public function testMapsPropertiesForUnionDocBlock() { } /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - public function testMapsPropertiesWhenTypeWithGlobalNamespacePrefix() { + public function testParsesPropertiesWithGlobalNamespacePrefixedType() { $class = new class() { /** * @var \stdClass @@ -162,7 +162,7 @@ public function testMapsPropertiesWhenTypeWithGlobalNamespacePrefix() { $this->assertEquals( $expected, $result ); } - public function testMapsPropertiesWhenTypeNullable() { + public function testParsesPropertiesWithNullType() { $class = new class() { /** * @var null|string diff --git a/tests/JsonMapper/resources/TestResourceClassComplexMapping.php b/tests/JsonMapper/resources/TestResourceClassComplexMapping.php new file mode 100644 index 00000000..aac44a25 --- /dev/null +++ b/tests/JsonMapper/resources/TestResourceClassComplexMapping.php @@ -0,0 +1,16 @@ + Date: Sat, 16 Mar 2024 16:07:43 +0100 Subject: [PATCH 23/28] Fix bug, add bug for later --- src/WordPress/JsonMapper/JsonMapper.php | 83 ++++++++++--------- tests/JsonMapper/JsonMapperTest.php | 49 +++++++++++ .../TestResourceClassComplexMapping.php | 15 ++++ 3 files changed, 106 insertions(+), 41 deletions(-) diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index e82f11ac..df8e5a96 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -16,6 +16,8 @@ * @package WordPress\JsonMapper */ class JsonMapper { + const ARRAY_TYPE = '/^array(\[])*$/'; + const MIXED_ARRAY_TYPE = '/^mixed(\[])+$/'; /** * Array of strings representing valid scalar types. * @@ -132,24 +134,7 @@ private static function get_property_for_value( string $value_name, array $prope return null; } - /** - * Maps a value from the JSON object to one of the types listed for that Property. - * - * This method uses the type of the {@link Property} to determine how to map the value from the JSON object. - * - * If the Property is: - * - * - of a basic type (like string, int, bool, etc.), the method simply casts the value to that type. - * - * - of a type the mapper has a factory for, the method - * - of a class type, the method creates a new instance of that class and populates it with data from the JSON - * value. - * - * @param Property $property The Property object to which the value should be mapped. - * @param mixed $value The value from the JSON object to map. - * @return mixed The mapped value, of the type specified by the Property. - * @throws JsonMapperException If the value cannot be mapped to any of the types listed for that Property. - */ + private function map_value( Property $property, $value ) { if ( 0 === count( $property->property_types ) ) { // Return the value as is - there is no type info. @@ -158,8 +143,8 @@ private function map_value( Property $property, $value ) { foreach ( $property->property_types as $property_type ) { $array_depth = substr_count( $property_type, '[]' ); - $is_array = 'array' === $property_type || $array_depth > 0; $property_type = str_replace( '[]', '', $property_type ); + $is_array = 'array' === $property_type || $array_depth > 0; if ( is_array( $value ) && $is_array && count( $value ) === 0 ) { return array(); @@ -178,12 +163,29 @@ private function map_value( Property $property, $value ) { } } + // If nothing more precise worked, value is an array, and one of the types is an array or mixed[], try it. + // Will work for deeper arrays. Does not check if depth matches. + if ( is_array( $value ) + && ( $this->is_matching_property( $property->property_types, self::ARRAY_TYPE ) ) + || $this->is_matching_property( $property->property_types, self::MIXED_ARRAY_TYPE ) ) { + return $value; + } + throw new JsonMapperException( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'Unable to map ' . json_encode( $value ) . " to '$property->name'." ); } + private function is_matching_property( $property_types, $pattern ) { + foreach ( $property_types as $property_type ) { + if ( preg_match( $pattern, $property_type ) ) { + return true; + } + } + return false; + } + private function set_value( $object, Property $property, $value ) { if ( 'public' === $property->visibility ) { $object->{$property->name} = $value; @@ -211,30 +213,35 @@ private function set_value( $object, Property $property, $value ) { } private function is_property_and_value_same_scalar( string $property_type, $value ) { - $copy_value = $value; - while ( true === is_array( $copy_value ) ) { - $copy_value = $copy_value[0]; - } + if ( false === is_array( $value ) ) { + if ( false === is_scalar( $value ) || false === $this->is_valid_scalar_type( $property_type ) ) { + return false; + } - if ( false === is_scalar( $copy_value ) || false === $this->is_valid_scalar_type( $property_type ) ) { - return false; - } + $value_type = gettype( $value ); - $value_type = gettype( $copy_value ); + if ( 'boolean' === $value_type ) { + return 'boolean' === $property_type || 'bool' === $property_type; + } - if ( 'boolean' === $value_type ) { - return 'boolean' === $property_type || 'bool' === $property_type; - } + if ( 'integer' === $value_type ) { + return 'integer' === $property_type || 'int' === $property_type; + } + + if ( 'double' === $value_type ) { + return 'float' === $property_type || 'double' === $property_type; + } - if ( 'integer' === $value_type ) { - return 'integer' === $property_type || 'int' === $property_type; + return $value_type === $property_type; } - if ( 'double' === $value_type ) { - return 'float' === $property_type || 'double' === $property_type; + foreach ( $value as $inner_value ) { + if ( false === $this->is_property_and_value_same_scalar( $property_type, $inner_value ) ) { + return false; + } } - return $value_type === $property_type; + return true; } /** @@ -351,11 +358,5 @@ function ( $value ) { return new ArrayObject( $value ); } ); - $this->add_factory( - 'array', - function ( $value ) { - return new ArrayObject( $value ); - } - ); } } diff --git a/tests/JsonMapper/JsonMapperTest.php b/tests/JsonMapper/JsonMapperTest.php index d60b4486..2f0b2be3 100644 --- a/tests/JsonMapper/JsonMapperTest.php +++ b/tests/JsonMapper/JsonMapperTest.php @@ -130,4 +130,53 @@ public function testMapsToDeepMixedArray() { $this->assertEquals( $expected, $result ); } + + public function testFailsWhenArrayWrongScalarType() { + $raw_json = '{"arrayOfStringArrays":[["test1", 42],["test3", true]]}'; + + $parsed_json = json_decode( $raw_json ); + + $this->expectException( JsonMapperException::class ); + $this->expectExceptionMessage( "Unable to map [[\"test1\",42],[\"test3\",true]] to 'arrayOfStringArrays'." ); + $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); + } + + public function testFailsWhenWrongScalarType() { + $raw_json = '{"string":42}'; + + $parsed_json = json_decode( $raw_json ); + + $this->expectException( JsonMapperException::class ); + $this->expectExceptionMessage( "Unable to map 42 to 'string'." ); + $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); + } + + public function testMapsToArrayOfArrays() { + $raw_json = '{"arrayOfArrays":[["test1", 42],["test3", true]]}'; + + $parsed_json = json_decode( $raw_json ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); + + $expected = new TestResourceClassComplexMapping(); + $expected->arrayOfArrays = array( + array( 'test1', 42 ), + array( 'test3', true ), + ); + + $this->assertEquals( $expected, $result ); + } + + public function testMapsToArray() { + $raw_json = '{"array":["test1","test2"]}'; + + $parsed_json = json_decode( $raw_json ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); + + $expected = new TestResourceClassComplexMapping(); + $expected->array = array('test1','test2'); + + $this->assertEquals( $expected, $result ); + } } diff --git a/tests/JsonMapper/resources/TestResourceClassComplexMapping.php b/tests/JsonMapper/resources/TestResourceClassComplexMapping.php index aac44a25..510d2468 100644 --- a/tests/JsonMapper/resources/TestResourceClassComplexMapping.php +++ b/tests/JsonMapper/resources/TestResourceClassComplexMapping.php @@ -3,6 +3,11 @@ namespace JsonMapper\resources; // phpcs:disable class TestResourceClassComplexMapping { + /** + * @var string; + */ + public $string; + /** * @var string[][] */ @@ -13,4 +18,14 @@ class TestResourceClassComplexMapping { * @var mixed[][] */ public $arrayOfMixedArrays; + + /** + * @var array[] + */ + public $arrayOfArrays; + + /** + * @var array + */ + public $array; } \ No newline at end of file From f9d67a6711273f6676691cbf6a642b9609db81af Mon Sep 17 00:00:00 2001 From: Michael Reichardt Date: Sun, 17 Mar 2024 09:53:18 +0100 Subject: [PATCH 24/28] Apply review suggestions --- src/WordPress/JsonMapper/JsonMapper.php | 21 ++++-- src/WordPress/JsonMapper/PropertyParser.php | 3 +- tests/E2E/JsonBlueprintTest.php | 83 --------------------- 3 files changed, 16 insertions(+), 91 deletions(-) delete mode 100644 tests/E2E/JsonBlueprintTest.php diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index df8e5a96..8e254dc9 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -134,7 +134,16 @@ private static function get_property_for_value( string $value_name, array $prope return null; } - + /** + * Maps a value from the JSON object to the type of the given Property. + * + * Warning - array depths are not fully checked during mapping. + * + * @param Property $property The Property object with a list of possible types the value could be mapped to. + * @param mixed $value The value from the JSON object. + * @return mixed The mapped value, of the type specified by the Property. + * @throws JsonMapperException If the Property type is not supported, or if the value cannot be mapped to the Property type. + */ private function map_value( Property $property, $value ) { if ( 0 === count( $property->property_types ) ) { // Return the value as is - there is no type info. @@ -155,7 +164,7 @@ private function map_value( Property $property, $value ) { } if ( $this->has_factory( $property_type ) ) { - return $this->map_to_object_using_factory( $property_type, $value ); + return $this->map_using_factory( $property_type, $value ); } if ( ( class_exists( $property_type ) || interface_exists( $property_type ) ) ) { @@ -163,8 +172,8 @@ private function map_value( Property $property, $value ) { } } - // If nothing more precise worked, value is an array, and one of the types is an array or mixed[], try it. - // Will work for deeper arrays. Does not check if depth matches. + // If nothing more precise worked, the value is an array, and one of the types is an array or mixed[], + // just return the values as is. This will work for arrays of any depth. if ( is_array( $value ) && ( $this->is_matching_property( $property->property_types, self::ARRAY_TYPE ) ) || $this->is_matching_property( $property->property_types, self::MIXED_ARRAY_TYPE ) ) { @@ -280,13 +289,13 @@ private function map_to_scalar( string $property_type, $value ) { return $mapped; } - private function map_to_object_using_factory( string $property_type, $value ) { + private function map_using_factory(string $property_type, $value ) { if ( false === is_array( $value ) ) { return $this->use_factory( $property_type, $value ); } $mapped = array(); foreach ( $value as $inner_value ) { - $mapped[] = $this->map_to_object_using_factory( $property_type, $inner_value ); + $mapped[] = $this->map_using_factory( $property_type, $inner_value ); } return $mapped; } diff --git a/src/WordPress/JsonMapper/PropertyParser.php b/src/WordPress/JsonMapper/PropertyParser.php index 6334f897..74ead84e 100644 --- a/src/WordPress/JsonMapper/PropertyParser.php +++ b/src/WordPress/JsonMapper/PropertyParser.php @@ -61,7 +61,7 @@ private static function parse_visibility( ReflectionProperty $reflection_propert * * Examples: * - * For types: PointlessInterface[]|null will return: array('PointlessInterface[]') + * For types: AnInterface[]|null will return: array('AnInterface[]') * * For types: string|int[][] will return: array('string', 'int[][]') * @@ -101,7 +101,6 @@ private static function parse_property_types( ReflectionProperty $reflection_pro } $property_types = array(); - /** @var string $property_type */ foreach ( explode( '|', $var ) as $property_type ) { // Filter out 'null' type. if ( 'null' === $property_type ) { diff --git a/tests/E2E/JsonBlueprintTest.php b/tests/E2E/JsonBlueprintTest.php deleted file mode 100644 index 75526bcc..00000000 --- a/tests/E2E/JsonBlueprintTest.php +++ /dev/null @@ -1,83 +0,0 @@ -document_root = Path::makeAbsolute( 'test', sys_get_temp_dir() ); - } - - /** - * @after - */ - public function after() { - ( new Filesystem() )->remove( $this->document_root ); - } -// public function testRunningJsonBlueprint() { -// $blueprint = '{}'; -// -// $subscriber = new class() implements EventSubscriberInterface { -// public static function getSubscribedEvents() { -// return array( -// ProgressEvent::class => 'onProgress', -// DoneEvent::class => 'onDone', -// ); -// } -// -// protected $progress_bar; -// -// public function __construct() { -// ProgressBar::setFormatDefinition( 'custom', ' [%bar%] %current%/%max% -- %message%' ); -// -// $this->progress_bar = ( new SymfonyStyle( -// new StringInput( '' ), -// new ConsoleOutput() -// ) )->createProgressBar( 100 ); -// $this->progress_bar->setFormat( 'custom' ); -// $this->progress_bar->setMessage( 'Start' ); -// $this->progress_bar->start(); -// } -// -// public function onProgress( ProgressEvent $event ) { -// $this->progress_bar->setMessage( $event->caption ); -// $this->progress_bar->setProgress( (int) $event->progress ); -// } -// -// public function onDone( DoneEvent $event ) { -// $this->progress_bar->finish(); -// } -// }; -// -// $results = run_blueprint( -// $blueprint, -// array( -// 'environment' => ContainerBuilder::ENVIRONMENT_NATIVE, -// 'documentRoot' => $this->document_root . '/new-wp', -// 'progressSubscriber' => $subscriber, -// ) -// ); -// -// $this->assertEquals( array(), $results ); -// } -} From 8ddb3c8b783edc4a03f686b735d0cf8b3dca6f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 17 Mar 2024 22:12:53 +0100 Subject: [PATCH 25/28] Do not initialized generated model properties as "null" since they're already null by default --- .../Blueprints/Model/DataClass/ActivatePluginStep.php | 2 +- .../Blueprints/Model/DataClass/ActivateThemeStep.php | 2 +- src/WordPress/Blueprints/Model/DataClass/Blueprint.php | 10 +++++----- .../Blueprints/Model/DataClass/BlueprintOnBoot.php | 4 ++-- .../Blueprints/Model/DataClass/CorePluginResource.php | 2 +- .../Blueprints/Model/DataClass/CoreThemeResource.php | 2 +- src/WordPress/Blueprints/Model/DataClass/CpStep.php | 4 ++-- .../Blueprints/Model/DataClass/DefineSiteUrlStep.php | 2 +- .../Model/DataClass/DefineWpConfigConstsStep.php | 2 +- .../Blueprints/Model/DataClass/EvalPHPCallbackStep.php | 2 +- src/WordPress/Blueprints/Model/DataClass/FileInfo.php | 8 ++++---- .../Blueprints/Model/DataClass/FileInfoData.php | 10 +++++----- .../Blueprints/Model/DataClass/FileInfoDataBuffer.php | 2 +- .../Blueprints/Model/DataClass/FilesystemResource.php | 2 +- .../Blueprints/Model/DataClass/InlineResource.php | 2 +- src/WordPress/Blueprints/Model/DataClass/MkdirStep.php | 2 +- src/WordPress/Blueprints/Model/DataClass/MvStep.php | 4 ++-- src/WordPress/Blueprints/Model/DataClass/Progress.php | 4 ++-- src/WordPress/Blueprints/Model/DataClass/RmStep.php | 2 +- .../Blueprints/Model/DataClass/RunPHPStep.php | 2 +- .../Blueprints/Model/DataClass/SetSiteOptionsStep.php | 2 +- src/WordPress/Blueprints/Model/DataClass/UnzipStep.php | 2 +- .../Blueprints/Model/DataClass/UrlResource.php | 4 ++-- src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php | 4 ++-- .../Model/DataClass/WordPressInstallationOptions.php | 2 +- .../Blueprints/Model/DataClass/WriteFileStep.php | 4 ++-- src/WordPress/Blueprints/bin/autogenerate_models.php | 6 +++++- 27 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php b/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php index 4f71ac02..4538d564 100644 --- a/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php @@ -19,7 +19,7 @@ class ActivatePluginStep implements StepDefinitionInterface * Plugin slug, like 'gutenberg' or 'hello-dolly'. * @var string */ - public $slug = null; + public $slug; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php b/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php index 04f44688..8d93b861 100644 --- a/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php @@ -19,7 +19,7 @@ class ActivateThemeStep implements StepDefinitionInterface * Theme slug, like 'twentytwentythree'. * @var string */ - public $slug = null; + public $slug; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php index ae7ff841..6fde22ab 100644 --- a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php +++ b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php @@ -14,16 +14,16 @@ class Blueprint * Version of WordPress to use. Also accepts URL to a WordPress zip file. * @var string */ - public $WordPressVersion = null; + public $WordPressVersion; /** * Slot for runtime–specific options, schema must be provided by the runtime. * @var \ArrayObject */ - public $runtime = null; + public $runtime; /** @var BlueprintOnBoot */ - public $onBoot = null; + public $onBoot; /** * PHP Constants to define on every request @@ -33,7 +33,7 @@ class Blueprint /** * WordPress plugins to install and activate - * @var string[]|ResourceDefinitionInterface[] + * @var list|list|list|list|list|list */ public $plugins = []; @@ -45,7 +45,7 @@ class Blueprint /** * The steps to run after every other operation in this Blueprint was executed. - * @var StepDefinitionInterface[] + * @var list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list */ public $steps = []; diff --git a/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php b/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php index 52ebd1ae..a091a052 100644 --- a/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php +++ b/src/WordPress/Blueprints/Model/DataClass/BlueprintOnBoot.php @@ -8,10 +8,10 @@ class BlueprintOnBoot * The URL to navigate to after the blueprint has been run. * @var string */ - public $openUrl = null; + public $openUrl; /** @var bool */ - public $login = null; + public $login; public function setOpenUrl(string $openUrl) diff --git a/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php b/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php index 0328fcb2..086d45c1 100644 --- a/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php @@ -16,7 +16,7 @@ class CorePluginResource implements ResourceDefinitionInterface * The slug of the WordPress Core plugin * @var string */ - public $slug = null; + public $slug; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php b/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php index 5c139df4..e7015a0a 100644 --- a/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php @@ -16,7 +16,7 @@ class CoreThemeResource implements ResourceDefinitionInterface * The slug of the WordPress Core theme * @var string */ - public $slug = null; + public $slug; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/CpStep.php b/src/WordPress/Blueprints/Model/DataClass/CpStep.php index 3d26c3f4..08d81a7d 100644 --- a/src/WordPress/Blueprints/Model/DataClass/CpStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/CpStep.php @@ -19,13 +19,13 @@ class CpStep implements StepDefinitionInterface * Source path * @var string */ - public $fromPath = null; + public $fromPath; /** * Target path * @var string */ - public $toPath = null; + public $toPath; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php b/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php index 0e58f971..5ade7609 100644 --- a/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php @@ -19,7 +19,7 @@ class DefineSiteUrlStep implements StepDefinitionInterface * The URL * @var string */ - public $siteUrl = null; + public $siteUrl; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php b/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php index c6310512..d75f533b 100644 --- a/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php @@ -19,7 +19,7 @@ class DefineWpConfigConstsStep implements StepDefinitionInterface * The constants to define * @var \ArrayObject */ - public $consts = null; + public $consts; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php b/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php index ed27e156..0bf34c2d 100644 --- a/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/EvalPHPCallbackStep.php @@ -22,7 +22,7 @@ class EvalPHPCallbackStep implements StepDefinitionInterface * The PHP function. * @var mixed */ - public $callback = null; + public $callback; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/FileInfo.php b/src/WordPress/Blueprints/Model/DataClass/FileInfo.php index e89b1972..0ed00e2a 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FileInfo.php +++ b/src/WordPress/Blueprints/Model/DataClass/FileInfo.php @@ -5,16 +5,16 @@ class FileInfo { /** @var string */ - public $key = null; + public $key; /** @var string */ - public $name = null; + public $name; /** @var string */ - public $type = null; + public $type; /** @var FileInfoData */ - public $data = null; + public $data; public function setKey(string $key) diff --git a/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php b/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php index 64417c02..9638f0e9 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php +++ b/src/WordPress/Blueprints/Model/DataClass/FileInfoData.php @@ -5,19 +5,19 @@ class FileInfoData { /** @var float */ - public $BYTES_PER_ELEMENT = null; + public $BYTES_PER_ELEMENT; /** @var FileInfoDataBuffer */ - public $buffer = null; + public $buffer; /** @var float */ - public $byteLength = null; + public $byteLength; /** @var float */ - public $byteOffset = null; + public $byteOffset; /** @var float */ - public $length = null; + public $length; public function setBYTES_PER_ELEMENT(float $BYTES_PER_ELEMENT) diff --git a/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php b/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php index 6868d18b..9bb4dfc8 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php +++ b/src/WordPress/Blueprints/Model/DataClass/FileInfoDataBuffer.php @@ -5,7 +5,7 @@ class FileInfoDataBuffer { /** @var float */ - public $byteLength = null; + public $byteLength; public function setByteLength(float $byteLength) diff --git a/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php b/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php index 3959f701..2c72c182 100644 --- a/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php @@ -16,7 +16,7 @@ class FilesystemResource implements ResourceDefinitionInterface * The path to the file in the VFS * @var string */ - public $path = null; + public $path; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/InlineResource.php b/src/WordPress/Blueprints/Model/DataClass/InlineResource.php index 7433f865..fa3bde2f 100644 --- a/src/WordPress/Blueprints/Model/DataClass/InlineResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/InlineResource.php @@ -16,7 +16,7 @@ class InlineResource implements ResourceDefinitionInterface * The contents of the file * @var string */ - public $contents = null; + public $contents; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php b/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php index bde0e187..911ee9da 100644 --- a/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/MkdirStep.php @@ -19,7 +19,7 @@ class MkdirStep implements StepDefinitionInterface * The path of the directory you want to create * @var string */ - public $path = null; + public $path; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/MvStep.php b/src/WordPress/Blueprints/Model/DataClass/MvStep.php index 19023fb6..736d18aa 100644 --- a/src/WordPress/Blueprints/Model/DataClass/MvStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/MvStep.php @@ -19,13 +19,13 @@ class MvStep implements StepDefinitionInterface * Source path * @var string */ - public $fromPath = null; + public $fromPath; /** * Target path * @var string */ - public $toPath = null; + public $toPath; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/Progress.php b/src/WordPress/Blueprints/Model/DataClass/Progress.php index 7f117261..4ae54958 100644 --- a/src/WordPress/Blueprints/Model/DataClass/Progress.php +++ b/src/WordPress/Blueprints/Model/DataClass/Progress.php @@ -5,10 +5,10 @@ class Progress { /** @var float */ - public $weight = null; + public $weight; /** @var string */ - public $caption = null; + public $caption; public function setWeight(float $weight) diff --git a/src/WordPress/Blueprints/Model/DataClass/RmStep.php b/src/WordPress/Blueprints/Model/DataClass/RmStep.php index 5acc1dab..c4a791ed 100644 --- a/src/WordPress/Blueprints/Model/DataClass/RmStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/RmStep.php @@ -19,7 +19,7 @@ class RmStep implements StepDefinitionInterface * The path to remove * @var string */ - public $path = null; + public $path; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php b/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php index 420ab0f0..ff2fb327 100644 --- a/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php @@ -22,7 +22,7 @@ class RunPHPStep implements StepDefinitionInterface * The PHP code to run. * @var string */ - public $code = null; + public $code; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php b/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php index 8a5ee1bb..d9f1dd36 100644 --- a/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/SetSiteOptionsStep.php @@ -22,7 +22,7 @@ class SetSiteOptionsStep implements StepDefinitionInterface * The options to set on the site. * @var \ArrayObject */ - public $options = null; + public $options; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php b/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php index 212ca120..f107951d 100644 --- a/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php @@ -22,7 +22,7 @@ class UnzipStep implements StepDefinitionInterface * The path to extract the zip file to * @var string */ - public $extractToPath = null; + public $extractToPath; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/UrlResource.php b/src/WordPress/Blueprints/Model/DataClass/UrlResource.php index 6198b64c..cd03093a 100644 --- a/src/WordPress/Blueprints/Model/DataClass/UrlResource.php +++ b/src/WordPress/Blueprints/Model/DataClass/UrlResource.php @@ -16,13 +16,13 @@ class UrlResource implements ResourceDefinitionInterface * The URL of the file * @var string */ - public $url = null; + public $url; /** * Optional caption for displaying a progress message * @var string */ - public $caption = null; + public $caption; public function setResource(string $resource) diff --git a/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php b/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php index 2be9b508..8183f8f4 100644 --- a/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php @@ -20,9 +20,9 @@ class WPCLIStep implements StepDefinitionInterface /** * The WP CLI command to run. - * @var string[] + * @var list */ - public $command = null; + public $command; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php b/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php index 9ed65307..7793e3db 100644 --- a/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php +++ b/src/WordPress/Blueprints/Model/DataClass/WordPressInstallationOptions.php @@ -5,7 +5,7 @@ class WordPressInstallationOptions { /** @var string */ - public $adminUsername = null; + public $adminUsername; /** @var string */ public $adminPassword = 'admin'; diff --git a/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php b/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php index 5ec66f42..dc1a05f8 100644 --- a/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php @@ -19,13 +19,13 @@ class WriteFileStep implements StepDefinitionInterface * The path of the file to write to * @var string */ - public $path = null; + public $path; /** * The data to write * @var string|ResourceDefinitionInterface */ - public $data = null; + public $data; public function setProgress(Progress $progress) diff --git a/src/WordPress/Blueprints/bin/autogenerate_models.php b/src/WordPress/Blueprints/bin/autogenerate_models.php index b8f205f2..ba1f5605 100644 --- a/src/WordPress/Blueprints/bin/autogenerate_models.php +++ b/src/WordPress/Blueprints/bin/autogenerate_models.php @@ -196,7 +196,11 @@ function fixTypeHint( string $typeHint, array $replacements ): string { $schema = $janeProperty->getObject(); if ( $schema instanceof JsonSchema ) { - $property->setValue( $schema->getDefault() ); + // Don't set "null" as the default value since it's already a default + // value of all class properties. + if ( $schema->getDefault() !== null ) { + $property->setValue( $schema->getDefault() ); + } if ( $schema->getConst() ) { $property->setValue( $schema->getConst() ); // Assume that a class with an interface uses a const property From 7174d723277bbd95e3f86ddcc912b6a9255eff29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 17 Mar 2024 22:16:56 +0100 Subject: [PATCH 26/28] Restore the original if/elseif structure in autogenerate_models --- src/WordPress/Blueprints/bin/autogenerate_models.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/WordPress/Blueprints/bin/autogenerate_models.php b/src/WordPress/Blueprints/bin/autogenerate_models.php index ba1f5605..c03ca0da 100644 --- a/src/WordPress/Blueprints/bin/autogenerate_models.php +++ b/src/WordPress/Blueprints/bin/autogenerate_models.php @@ -196,11 +196,6 @@ function fixTypeHint( string $typeHint, array $replacements ): string { $schema = $janeProperty->getObject(); if ( $schema instanceof JsonSchema ) { - // Don't set "null" as the default value since it's already a default - // value of all class properties. - if ( $schema->getDefault() !== null ) { - $property->setValue( $schema->getDefault() ); - } if ( $schema->getConst() ) { $property->setValue( $schema->getConst() ); // Assume that a class with an interface uses a const property @@ -214,6 +209,10 @@ function fixTypeHint( string $typeHint, array $replacements ): string { // So, we have to manually set it back to null. $class->getConstants()['DISCRIMINATOR']->setVisibility( null ); } + } elseif ( $schema->getDefault() !== null ) { + // Don't set "null" as the default value since it's already a default + // value of all class properties. + $property->setValue( $schema->getDefault() ); } elseif ( $schema->getType() === 'array' ) { $property->setValue( [] ); } From 8aeb4f3cfa0179f6b2ff025bf4c3e144f46ddf45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sun, 17 Mar 2024 22:35:47 +0100 Subject: [PATCH 27/28] Replace list<> with regular PHP type annotations --- src/WordPress/Blueprints/Model/DataClass/Blueprint.php | 4 ++-- src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php index 6fde22ab..6257788e 100644 --- a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php +++ b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php @@ -33,7 +33,7 @@ class Blueprint /** * WordPress plugins to install and activate - * @var list|list|list|list|list|list + * @var string[]|ResourceDefinitionInterface[] */ public $plugins = []; @@ -45,7 +45,7 @@ class Blueprint /** * The steps to run after every other operation in this Blueprint was executed. - * @var list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list|list + * @var StepDefinitionInterface[] */ public $steps = []; diff --git a/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php b/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php index 8183f8f4..3949a622 100644 --- a/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php +++ b/src/WordPress/Blueprints/Model/DataClass/WPCLIStep.php @@ -20,7 +20,7 @@ class WPCLIStep implements StepDefinitionInterface /** * The WP CLI command to run. - * @var list + * @var string[] */ public $command; From c98093cc586ce880e20320a2c7c869fd07ab9fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 18 Mar 2024 00:48:24 +0100 Subject: [PATCH 28/28] Clean up structure, formatting, comments --- src/WordPress/Blueprints/BlueprintMapper.php | 11 +- src/WordPress/Blueprints/Engine.php | 4 +- src/WordPress/Blueprints/functions.php | 12 +- src/WordPress/JsonMapper/JsonMapper.php | 315 ++++++++----------- src/WordPress/JsonMapper/PropertyParser.php | 78 ++++- src/WordPress/JsonMapper/Utils.php | 13 + tests/JsonMapper/JsonMapperTest.php | 46 ++- tests/JsonMapper/PropertyParserTest.php | 30 +- tests/JsonMapper/resources/Bag.php | 17 + tests/JsonMapper/resources/Item.php | 12 + 10 files changed, 309 insertions(+), 229 deletions(-) create mode 100644 src/WordPress/JsonMapper/Utils.php create mode 100644 tests/JsonMapper/resources/Bag.php create mode 100644 tests/JsonMapper/resources/Item.php diff --git a/src/WordPress/Blueprints/BlueprintMapper.php b/src/WordPress/Blueprints/BlueprintMapper.php index fc3501cb..029dc79b 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -5,6 +5,8 @@ use stdClass; use WordPress\Blueprints\Model\DataClass\Blueprint; use WordPress\Blueprints\Model\DataClass\ModelInfo; +use WordPress\Blueprints\Model\DataClass\ResourceDefinitionInterface; +use WordPress\Blueprints\Model\DataClass\StepDefinitionInterface; use WordPress\JsonMapper\JsonMapper; use WordPress\JsonMapper\JsonMapperException; @@ -19,16 +21,17 @@ class BlueprintMapper { */ public function __construct() { $custom_factories = array( - 'ResourceDefinitionInterface' => array( $this, 'resource_factory' ), - 'StepDefinitionInterface' => array( $this, 'step_factory' ), + ResourceDefinitionInterface::class => array( $this, 'resource_factory' ), + StepDefinitionInterface::class => array( $this, 'step_factory' ), ); - $this->mapper = new JsonMapper( $custom_factories ); + $this->mapper = new JsonMapper( $custom_factories ); } /** * Maps a parsed and validated JSON object to a Blueprint class instance. * * @param stdClass $blueprint a parsed and validated JSON object. + * * @return Blueprint */ public function map( stdClass $blueprint ): Blueprint { @@ -37,6 +40,7 @@ public function map( stdClass $blueprint ): Blueprint { /** * @param $value + * * @return object|string * @throws JsonMapperException */ @@ -61,6 +65,7 @@ public function resource_factory( $value ) { /** * @param $value + * * @return object * @throws JsonMapperException */ diff --git a/src/WordPress/Blueprints/Engine.php b/src/WordPress/Blueprints/Engine.php index 9f12130d..97c99ad3 100644 --- a/src/WordPress/Blueprints/Engine.php +++ b/src/WordPress/Blueprints/Engine.php @@ -28,9 +28,9 @@ public function __construct( BlueprintCompiler $compiler, BlueprintRunner $runner ) { - $this->runner = $runner; + $this->runner = $runner; $this->compiler = $compiler; - $this->parser = $parser; + $this->parser = $parser; } public function parseAndCompile( $raw_blueprint ) { diff --git a/src/WordPress/Blueprints/functions.php b/src/WordPress/Blueprints/functions.php index c740f4d2..66a661eb 100644 --- a/src/WordPress/Blueprints/functions.php +++ b/src/WordPress/Blueprints/functions.php @@ -20,7 +20,7 @@ function run_blueprint( $json, $options = [] ) { $engine = $c['blueprint.engine']; $compiledBlueprint = $engine->parseAndCompile( $json ); - + /** @var $engine Engine */ if ( $progressSubscriber ) { if ( $progressType === 'steps' ) { @@ -50,12 +50,12 @@ function move_files_from_directory_to_directory( string $from, string $to ) { if ( '.' === $file || '..' === $file ) { continue; } - $fromPath = Path::canonicalize($from . '/' . $file); - $toPath = Path::canonicalize($to . '/' . $file); + $fromPath = Path::canonicalize( $from . '/' . $file ); + $toPath = Path::canonicalize( $to . '/' . $file ); try { - $fs->rename($fromPath, $toPath); - } catch (IOException $exception) { - throw new BlueprintException("Failed to move the file from {$fromPath} at {$toPath}", 0, $exception); + $fs->rename( $fromPath, $toPath ); + } catch ( IOException $exception ) { + throw new BlueprintException( "Failed to move the file from {$fromPath} at {$toPath}", 0, $exception ); } } } diff --git a/src/WordPress/JsonMapper/JsonMapper.php b/src/WordPress/JsonMapper/JsonMapper.php index 8e254dc9..e001d22a 100644 --- a/src/WordPress/JsonMapper/JsonMapper.php +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -7,41 +7,27 @@ use ReflectionException; use ReflectionMethod; use stdClass; + /** * Class JsonMapper * - * This class is responsible for mapping JSON data to PHP objects. It supports mapping to native PHP classes - * as well as custom classes. Custom factories can be provided for instantiating objects of specific classes. + * This class is responsible for mapping JSON data to PHP class instances. + * Custom factories can be provided for instantiating objects of specific classes. * * @package WordPress\JsonMapper */ class JsonMapper { - const ARRAY_TYPE = '/^array(\[])*$/'; - const MIXED_ARRAY_TYPE = '/^mixed(\[])+$/'; - /** - * Array of strings representing valid scalar types. - * - * @var string[] - */ - private $scalar_types = array( 'string', 'bool', 'boolean', 'int', 'integer', 'double', 'float' ); - /** - * Array of factories for instantiating objects of specific classes. + * Factories for instantiating specific classes. * * @var array */ private $factories = array(); /** - * Constructs a new instance of this class. - * - * This constructor initializes the object and adds factories for native PHP classes. - * It also allows for the addition of custom factories through the $custom_factories parameter. - * - * @param null|array $custom_factories An associative array where the key is the class name - * and the value is a callable factory function. The factory function is expected to take - * an instance of stdClass as its argument and return an instance of the class specified - * by the key. This parameter is optional, and if not provided, an empty array will be used. + * @param null|array $custom_factories A map of class name to an instance factory function. + * The function takes a single stdClass argument with parsed + * JSON data and returns a class instance. */ public function __construct( array $custom_factories = array() ) { $this->add_factories_for_native_php_classes(); @@ -49,59 +35,45 @@ public function __construct( array $custom_factories = array() ) { } /** - * Creates an instance of the given class and populates it with data from the given JSON object. - * - * This method first checks if a factory exists for the given class. If a factory exists, it uses the factory - * to create the instance and populate it with data. If no factory exists, it creates the instance and populates - * it manually by calling the `hydrate_manually` method. + * Creates an instance of $class_name based on parsed JSON data. * * @param stdClass $json The JSON object containing the data to populate the new object with. - * @param string $class_name The fully qualified name of the class to create an instance of. + * @param string $class_name The fully qualified name of the class to create an instance of. + * * @return object An instance of the class specified by $class_name, populated with data from $json. * @throws ReflectionException If the class does not exist and the instance is being created manually. * @throws JsonMapperException If mapping the value to an associated property type failed or if setting was - * impossible. + * impossible. */ public function hydrate( stdClass $json, string $class_name ) { return $this->has_factory( $class_name ) - ? $this->use_factory( $class_name, $json ) - : $this->hydrate_manually( $json, $class_name ); + ? $this->run_factory( $class_name, $json ) + : $this->create_and_hydrate( $class_name, $json ); } /** - * Creates an instance of the given class and populates its properties with data from the given JSON object. - * - * This method uses PHP's Reflection API to create a new instance of the class specified by $class_name. - * It then uses the PropertyParser to compute a property map for the class, which is an associative array - * where the keys are property names and the values are {@link Property} objects. - * - * The method then iterates over the properties of the JSON object. For each property, it retrieves the - * corresponding Property object from the property map, maps the JSON value to the type of the Property, - * and sets the value of the Property on the newly created object. - * - * If the JSON object contains a property that is not defined in the class, or if the value of a property - * in the JSON object is null, that property is ignored. + * Populates an instance of $class_name created via ReflectionClass::newInstance. * * @param stdClass $json The JSON object containing the data to populate the new object with. - * @param string $class_name The fully qualified name of the class to create an instance of. + * @param string $class_name The fully qualified name of the class to create an instance of. + * * @return object An instance of the class specified by $class_name, populated with data from $json. * @throws ReflectionException If the class does not exist. * @throws JsonMapperException If mapping the value to an associated property type failed or if setting was - * impossible. + * impossible. */ - private function hydrate_manually( stdClass $json, string $class_name ) { + private function create_and_hydrate( string $class_name, stdClass $json ) { $reflection_class = new ReflectionClass( $class_name ); - $object = $reflection_class->newInstance(); - $property_map = PropertyParser::compute_property_map( $reflection_class ); + $object = $reflection_class->newInstance(); + $property_map = PropertyParser::compute_property_map( $reflection_class ); - foreach ( (array) $json as $value_name => $value ) { + foreach ( (array) $json as $key => $value ) { // Ignore null data in JSON. if ( null === $value ) { continue; } - $property = self::get_property_for_value( $value_name, $property_map ); - // Ignore additional data in JSON. + $property = $property_map[ $key ] ?? null; if ( null === $property ) { continue; } @@ -113,34 +85,14 @@ private function hydrate_manually( stdClass $json, string $class_name ) { return $object; } - /** - * Retrieves the Property object associated with a given value name from a property map. - * - * This method iterates over the provided property map and returns the Property object - * whose name matches the provided value name. If no matching Property is found, it returns null. - * - * @param string $value_name The name of the value for which to find the corresponding Property. - * @param Property[] $property_map An associative array where the keys are property names - * and the values are Property objects. - * @return Property|null The Property object associated with the given value name, - * or null if no matching Property is found. - */ - private static function get_property_for_value( string $value_name, array $property_map ) { - foreach ( $property_map as $property_name => $property ) { - if ( $property_name === $value_name ) { - return $property; - } - } - return null; - } - /** * Maps a value from the JSON object to the type of the given Property. * * Warning - array depths are not fully checked during mapping. * * @param Property $property The Property object with a list of possible types the value could be mapped to. - * @param mixed $value The value from the JSON object. + * @param mixed $value The value from the JSON object. + * * @return mixed The mapped value, of the type specified by the Property. * @throws JsonMapperException If the Property type is not supported, or if the value cannot be mapped to the Property type. */ @@ -151,196 +103,175 @@ private function map_value( Property $property, $value ) { } foreach ( $property->property_types as $property_type ) { - $array_depth = substr_count( $property_type, '[]' ); - $property_type = str_replace( '[]', '', $property_type ); - $is_array = 'array' === $property_type || $array_depth > 0; + $array_dimensions = PropertyParser::get_array_dimensions( $property_type ); + $property_type = PropertyParser::without_dimensions( $property_type ); + $type_is_array = 'array' === $property_type || $array_dimensions > 0; - if ( is_array( $value ) && $is_array && count( $value ) === 0 ) { + if ( is_array( $value ) && $type_is_array && count( $value ) === 0 ) { return array(); } - if ( $this->is_property_and_value_same_scalar( $property_type, $value ) ) { - return $this->map_to_scalar( $property_type, $value ); + if ( $this->is_scalar_recursive( $value, $property_type ) ) { + return $this->map_to_scalar_recursive( $value, $property_type ); } if ( $this->has_factory( $property_type ) ) { - return $this->map_using_factory( $property_type, $value ); + return $this->map_using_factory( $value, $property_type ); } - if ( ( class_exists( $property_type ) || interface_exists( $property_type ) ) ) { - return $this->map_to_object( $property_type, $value ); + if ( class_exists( $property_type ) || interface_exists( $property_type ) ) { + return $this->map_to_object( $value, $property_type ); } } - // If nothing more precise worked, the value is an array, and one of the types is an array or mixed[], - // just return the values as is. This will work for arrays of any depth. - if ( is_array( $value ) - && ( $this->is_matching_property( $property->property_types, self::ARRAY_TYPE ) ) - || $this->is_matching_property( $property->property_types, self::MIXED_ARRAY_TYPE ) ) { - return $value; + // If nothing more precise worked, the value is an array, and it can be mapped to an array type, + // just return the values as is. This ignores the array dimensions. + // @TODO: Take array dimensions into account. + if ( is_array( $value ) ) { + foreach ( $property->property_types as $property_type ) { + if ( preg_match( '/^(array|mixed|object)(\[\])*$/', $property_type ) ) { + return $value; + } + } } throw new JsonMapperException( - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 'Unable to map ' . json_encode( $value ) . " to '$property->name'." ); } - private function is_matching_property( $property_types, $pattern ) { - foreach ( $property_types as $property_type ) { - if ( preg_match( $pattern, $property_type ) ) { - return true; - } - } - return false; - } private function set_value( $object, Property $property, $value ) { - if ( 'public' === $property->visibility ) { - $object->{$property->name} = $value; - return; - } - + // Use a setter if it exists. $method_name = 'set' . ucfirst( $property->name ); if ( method_exists( $object, $method_name ) ) { - $method = new ReflectionMethod( $object, $method_name ); + $method = new ReflectionMethod( $object, $method_name ); $parameters = $method->getParameters(); if ( is_array( $value ) && count( $parameters ) === 1 && $parameters[0]->isVariadic() ) { call_user_func_array( array( $object, $method_name ), $value ); + return; } $object->$method_name( $value ); + + return; + } + + // Use a public property if it exists. + if ( 'public' === $property->visibility ) { + $object->{$property->name} = $value; + return; } throw new JsonMapperException( - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped "Property: '" . get_class( $object ) . "::$property->name' is non-public and no setter method was found." ); + } - private function is_property_and_value_same_scalar( string $property_type, $value ) { - if ( false === is_array( $value ) ) { - if ( false === is_scalar( $value ) || false === $this->is_valid_scalar_type( $property_type ) ) { - return false; + private function is_scalar_recursive( $value, string $property_type ) { + if ( is_array( $value ) ) { + foreach ( $value as $inner_value ) { + if ( ! $this->is_scalar_recursive( $inner_value, $property_type ) ) { + return false; + } } - $value_type = gettype( $value ); + return true; + } - if ( 'boolean' === $value_type ) { - return 'boolean' === $property_type || 'bool' === $property_type; - } + if ( ! is_scalar( $value ) || ! Utils::is_type_scalar( $property_type ) ) { + return false; + } - if ( 'integer' === $value_type ) { - return 'integer' === $property_type || 'int' === $property_type; - } + $value_type = gettype( $value ); - if ( 'double' === $value_type ) { - return 'float' === $property_type || 'double' === $property_type; - } + if ( 'boolean' === $value_type ) { + return 'boolean' === $property_type || 'bool' === $property_type; + } - return $value_type === $property_type; + if ( 'integer' === $value_type ) { + return 'integer' === $property_type || 'int' === $property_type; } - foreach ( $value as $inner_value ) { - if ( false === $this->is_property_and_value_same_scalar( $property_type, $inner_value ) ) { - return false; - } + if ( 'double' === $value_type ) { + return 'float' === $property_type || 'double' === $property_type; } - return true; + return $value_type === $property_type; } - /** - * @param string $property_type - * @return bool - */ - private function is_valid_scalar_type( string $property_type ): bool { - return in_array( $property_type, $this->scalar_types, true ); - } + private function map_to_scalar_recursive( $value, string $property_type ) { + if ( is_array( $value ) ) { + $mapped = array(); + foreach ( $value as $inner_value ) { + $mapped[] = $this->map_to_scalar_recursive( $inner_value, $property_type ); + } + + return $mapped; + } - private static function cast_to_scalar_type( string $type, $value ) { - if ( 'string' === $type ) { + if ( 'string' === $property_type ) { return (string) $value; } - if ( 'boolean' === $type || 'bool' === $type ) { + if ( 'boolean' === $property_type || 'bool' === $property_type ) { return (bool) $value; } - if ( 'integer' === $type || 'int' === $type ) { + if ( 'integer' === $property_type || 'int' === $property_type ) { return (int) $value; } - if ( 'double' === $type || 'float' === $type ) { + if ( 'double' === $property_type || 'float' === $property_type ) { return (float) $value; } - throw new JsonMapperException( "Casting to scalar type \'$type\' failed." ); + throw new \InvalidArgumentException( "\'$property_type\' is not a scalar value so it could not be cast to a scalar type." ); } - private function map_to_scalar( string $property_type, $value ) { - if ( false === is_array( $value ) ) { - return self::cast_to_scalar_type( $property_type, $value ); - } - $mapped = array(); - foreach ( $value as $inner_value ) { - $mapped[] = $this->map_to_scalar( $property_type, $inner_value ); - } - return $mapped; - } + private function map_using_factory( $value, string $property_type ) { + if ( is_array( $value ) ) { + $mapped = array(); + foreach ( $value as $inner_value ) { + $mapped[] = $this->map_using_factory( $inner_value, $property_type ); + } - private function map_using_factory(string $property_type, $value ) { - if ( false === is_array( $value ) ) { - return $this->use_factory( $property_type, $value ); - } - $mapped = array(); - foreach ( $value as $inner_value ) { - $mapped[] = $this->map_using_factory( $property_type, $inner_value ); + return $mapped; } - return $mapped; + + return $this->run_factory( $property_type, $value ); } - private function map_to_object( string $property_type, $value ) { - if ( false === ( new ReflectionClass( $property_type ) )->isInstantiable() ) { + private function map_to_object( $value, string $property_type ) { + if ( ! ( new ReflectionClass( $property_type ) )->isInstantiable() ) { // phpcs:ignore throw new JsonMapperException( "Unable to resolve uninstantiable \'$property_type\'." ); } - if ( false === is_array( $value ) ) { - return $this->hydrate( $value, $property_type ); - } - $mapped = array(); - foreach ( $value as $inner_value ) { - $mapped[] = $this->map_to_object( $property_type, $inner_value ); + + if ( is_array( $value ) ) { + $mapped = array(); + foreach ( $value as $inner_value ) { + $mapped[] = $this->map_to_object( $inner_value, $property_type ); + } + + return $mapped; } - return $mapped; + + return $this->hydrate( $value, $property_type ); } - private function sanitise_class_name( string $class_name ): string { + private function sanitize_class_name( string $class_name ): string { /* Erase leading slash as ::class doesn't contain leading slash */ if ( strpos( $class_name, '\\' ) === 0 ) { $class_name = substr( $class_name, 1 ); } - return $class_name; - } - - private function has_factory( string $class_name ): bool { - return array_key_exists( $this->sanitise_class_name( $class_name ), $this->factories ); - } - private function use_factory( string $class_name, $params ) { - $factory = $this->factories[ $this->sanitise_class_name( $class_name ) ]; - return $factory( $params ); - } - - private function add_factory( string $class_name, callable $factory ) { - $this->factories[ $this->sanitise_class_name( $class_name ) ] = $factory; + return $class_name; } - private function add_custom_factories( array $custom_factories ) { - foreach ( $custom_factories as $class_name => $custom_factory ) { - $this->add_factory( $class_name, $custom_factory ); - } - } private function add_factories_for_native_php_classes() { $this->add_factory( @@ -362,10 +293,28 @@ static function ( $value ) { } ); $this->add_factory( - ArrayObject::class, + \ArrayObject::class, function ( $value ) { return new ArrayObject( $value ); } ); } + + private function add_custom_factories( array $custom_factories ) { + foreach ( $custom_factories as $class_name => $custom_factory ) { + $this->add_factory( $class_name, $custom_factory ); + } + } + + private function has_factory( string $class_name ): bool { + return array_key_exists( $this->sanitize_class_name( $class_name ), $this->factories ); + } + + private function run_factory( string $class_name, $params ) { + return $this->factories[$this->sanitize_class_name( $class_name )]( $params ); + } + + private function add_factory( string $class_name, callable $factory ) { + $this->factories[ $this->sanitize_class_name( $class_name ) ] = $factory; + } } diff --git a/src/WordPress/JsonMapper/PropertyParser.php b/src/WordPress/JsonMapper/PropertyParser.php index 74ead84e..fcfebfea 100644 --- a/src/WordPress/JsonMapper/PropertyParser.php +++ b/src/WordPress/JsonMapper/PropertyParser.php @@ -15,12 +15,29 @@ class PropertyParser { /** * Private constructor. */ - private function __construct() {} + private function __construct() { + } + + public static function without_dimensions( $type ) { + return substr( $type, 0, strlen( $type ) - self::get_array_dimensions( $type ) * 2 ); + } + + public static function get_array_dimensions( $type ) { + $dimension = 0; + $at = strlen( $type ); + while ( substr( $type, $at - 2, 2 ) === '[]' ) { + $dimension ++; + $at -= 2; + } + + return $dimension; + } /** * Analyzes the reflected class and returns a map of properties based on reflections and DocBlocks. * * @param ReflectionClass $reflection_class reflected class which DocBlocks are to be mapped to a property map. + * * @return array the property map, key: property name */ public static function compute_property_map( ReflectionClass $reflection_class ) { @@ -41,6 +58,7 @@ public static function compute_property_map( ReflectionClass $reflection_class ) * Returns property visibility as string. Only supports 'public', 'protected' and 'private'. * * @param ReflectionProperty $reflection_property reflected property to derive visibility from. + * * @return string property visibility. */ private static function parse_visibility( ReflectionProperty $reflection_property ) { @@ -67,7 +85,8 @@ private static function parse_visibility( ReflectionProperty $reflection_propert * * For types: \ArrayObject will return array('ArrayObject') * - * @param ReflectionProperty $reflection_property reflected property to derive and parse DocBlock from. + * @param ReflectionProperty $reflection_property reflected property to derive and parse DocBlock from. + * * @return string[] array of property types, might be empty if no properties were listed in the DocBlock, or the * DocBlock does not exist at all. */ @@ -82,14 +101,14 @@ private static function parse_property_types( ReflectionProperty $reflection_pro if ( strpos( $doc_block, '/**' ) === 0 ) { $doc_block = substr( $doc_block, 3 ); } - if ( substr( $doc_block, -2 ) === '*/' ) { - $doc_block = substr( $doc_block, 0, -2 ); + if ( substr( $doc_block, - 2 ) === '*/' ) { + $doc_block = substr( $doc_block, 0, - 2 ); } $doc_block = trim( $doc_block ); $var = null; if ( preg_match_all( self::DOC_BLOCK_REGEX, $doc_block, $matches ) ) { - for ( $x = 0, $max = count( $matches[0] ); $x < $max; $x++ ) { + for ( $x = 0, $max = count( $matches[0] ); $x < $max; $x ++ ) { if ( 'var' === $matches['name'][ $x ] ) { $var = $matches['value'][ $x ]; } @@ -101,30 +120,67 @@ private static function parse_property_types( ReflectionProperty $reflection_pro } $property_types = array(); - foreach ( explode( '|', $var ) as $property_type ) { + foreach ( explode( '|', $var ) as $original_property_type ) { // Filter out 'null' type. - if ( 'null' === $property_type ) { + if ( 'null' === $original_property_type ) { continue; } - // Return types without their Global Namespace Prefixes. - $property_types[] = str_replace( '\\', '', $property_type ); + $array_brackets = ''; + $type = $original_property_type; + preg_match( '/(\[\])+$/', $type, $matches ); + if ( ! empty( $matches ) ) { + $array_brackets = $matches[0]; + $type = substr( $type, 0, - strlen( $array_brackets ) ); + } + + if ( Utils::is_type_scalar( $type ) || $type === 'array' || $type === 'object' || $type === 'mixed' ) { + $property_types[] = $type . $array_brackets; + continue; + } + + // $type is a class name, check if it exists. + if ( class_exists( '\\' . $type ) ) { + $property_types[] = '\\' . $type . $array_brackets; + continue; + } + + if ( class_exists( $type ) ) { + $property_types[] = $type . $array_brackets; + continue; + } + + $ns = $reflection_property->getDeclaringClass()->getNamespaceName(); + + if ( ! empty( $ns ) ) { + $namespaced_type = '\\' . $ns . '\\' . $type; + if ( + class_exists( $namespaced_type ) + || interface_exists( $namespaced_type ) + ) { + $property_types[] = $namespaced_type . $array_brackets; + continue; + } + } + throw new JsonMapperException( "Property type {$original_property_type} cannot be mapped to any known scalar or class name." ); } return $property_types; } + /** * Returns an array of properties for the reflected class and all of its parents. * * @param ReflectionClass $reflection_class reflected class to recursively extract properties from. + * * @return ReflectionProperty[] array of properties. */ private static function get_properties( ReflectionClass $reflection_class ) { - $properties = $reflection_class->getProperties(); + $properties = $reflection_class->getProperties(); $reflected_parent = $reflection_class->getParentClass(); while ( false !== $reflected_parent ) { - $properties = array_merge( $properties, $reflected_parent->getProperties() ); + $properties = array_merge( $properties, $reflected_parent->getProperties() ); $reflected_parent = $reflected_parent->getParentClass(); } diff --git a/src/WordPress/JsonMapper/Utils.php b/src/WordPress/JsonMapper/Utils.php new file mode 100644 index 00000000..e186cde5 --- /dev/null +++ b/src/WordPress/JsonMapper/Utils.php @@ -0,0 +1,13 @@ +json_mapper = new JsonMapper(); } + public function testCustomFactory() { + $mapper = new JsonMapper( array( + Item::class => function ( $json ) { + $item = new Item(); + $item->name = $json->name; + + return $item; + }, + ) ); + + $result = $mapper->hydrate( + json_decode( '{"name":"test","items":[{"name":"test"}]}' ), + Bag::class + ); + + $expected = new Bag(); + $expected->name = 'test'; + $expected->items = [ new Item() ]; + $expected->items[0]->name = 'test'; + + $this->assertEquals( $expected, $result ); + } + /** * Test checks if mapper works at all. * @@ -47,7 +72,7 @@ public function testSetsPublicProperties() { $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); - $expected = new TestResourceClassSetValue(); + $expected = new TestResourceClassSetValue(); $expected->publicProperty = 'test'; $this->assertEquals( $expected, $result ); @@ -100,13 +125,12 @@ public function testFailsSettingProtectedPropertyWithNoSetter() { } public function testMapsToDeepScalarArray() { - $raw_json = '{"arrayOfStringArrays":[["test1","test2"],["test3","test4"]]}'; - - $parsed_json = json_decode( $raw_json ); - - $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); + $result = $this->json_mapper->hydrate( + json_decode( '{"arrayOfStringArrays":[["test1","test2"],["test3","test4"]]}' ), + TestResourceClassComplexMapping::class + ); - $expected = new TestResourceClassComplexMapping(); + $expected = new TestResourceClassComplexMapping(); $expected->arrayOfStringArrays = array( array( 'test1', 'test2' ), array( 'test3', 'test4' ), @@ -122,7 +146,7 @@ public function testMapsToDeepMixedArray() { $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); - $expected = new TestResourceClassComplexMapping(); + $expected = new TestResourceClassComplexMapping(); $expected->arrayOfMixedArrays = array( array( 'test1', 42 ), array( 'test3', true ), @@ -158,7 +182,7 @@ public function testMapsToArrayOfArrays() { $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); - $expected = new TestResourceClassComplexMapping(); + $expected = new TestResourceClassComplexMapping(); $expected->arrayOfArrays = array( array( 'test1', 42 ), array( 'test3', true ), @@ -174,8 +198,8 @@ public function testMapsToArray() { $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassComplexMapping::class ); - $expected = new TestResourceClassComplexMapping(); - $expected->array = array('test1','test2'); + $expected = new TestResourceClassComplexMapping(); + $expected->array = array( 'test1', 'test2' ); $this->assertEquals( $expected, $result ); } diff --git a/tests/JsonMapper/PropertyParserTest.php b/tests/JsonMapper/PropertyParserTest.php index 7e8e4b29..645ce6c1 100644 --- a/tests/JsonMapper/PropertyParserTest.php +++ b/tests/JsonMapper/PropertyParserTest.php @@ -20,7 +20,7 @@ public function testParsesPropertiesWithScalarTypes() { private $string; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'string' => new Property( 'string', 'private', array( 'string' ) ), ); @@ -45,7 +45,7 @@ public function testParsesPropertiesWithArraysOfScalarTypes() { private $string_deep_array; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'string' => new Property( 'string', 'private', array( 'string' ) ), 'string_array' => new Property( 'string_array', 'private', array( 'string[]' ) ), @@ -53,6 +53,7 @@ public function testParsesPropertiesWithArraysOfScalarTypes() { ); $this->assertEquals( $expected, $result ); } + public function testParsesPropertiesWithArrays() { $class = new class() { /** @@ -61,7 +62,7 @@ public function testParsesPropertiesWithArrays() { private $string_or_array; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'string_or_array' => new Property( 'string_or_array', 'private', array( 'string', 'array' ) ), ); @@ -73,7 +74,7 @@ public function testParsesPropertiesWithNoDocBlocks() { private $no_docblock; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'no_docblock' => new Property( 'no_docblock', 'private', array() ), ); @@ -89,12 +90,13 @@ public function testParsesPropertiesWithPublicVisibility() { public $string; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'string' => new Property( 'string', 'public', array( 'string' ) ), ); $this->assertEquals( $expected, $result ); } + public function testParsesPropertiesWithProtectedVisibility() { $class = new class() { /** @@ -103,7 +105,7 @@ public function testParsesPropertiesWithProtectedVisibility() { protected $string; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'string' => new Property( 'string', 'protected', array( 'string' ) ), ); @@ -118,7 +120,7 @@ public function testParsesPropertiesWithPrivateVisibility() { private $string; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'string' => new Property( 'string', 'private', array( 'string' ) ), ); @@ -133,9 +135,11 @@ public function testParsesPropertiesWithUnionTypes() { private $string_or_array_or_bool; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( - 'string_or_array_or_bool' => new Property( 'string_or_array_or_bool', 'private', array( 'string', 'array', 'bool' ) ), + 'string_or_array_or_bool' => new Property( 'string_or_array_or_bool', + 'private', + array( 'string', 'array', 'bool' ) ), ); $this->assertEquals( $expected, $result ); } @@ -154,10 +158,10 @@ public function testParsesPropertiesWithGlobalNamespacePrefixedType() { private $local_stdclass; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( - 'global_stdclass' => new Property( 'global_stdclass', 'private', array( 'stdClass' ) ), - 'local_stdclass' => new Property( 'local_stdclass', 'private', array( 'stdClass' ) ), + 'global_stdclass' => new Property( 'global_stdclass', 'private', array( '\\stdClass' ) ), + 'local_stdclass' => new Property( 'local_stdclass', 'private', array( '\\stdClass' ) ), ); $this->assertEquals( $expected, $result ); } @@ -170,7 +174,7 @@ public function testParsesPropertiesWithNullType() { private $nullable_string; }; - $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); + $result = PropertyParser::compute_property_map( new ReflectionClass( $class ) ); $expected = array( 'nullable_string' => new Property( 'nullable_string', 'private', array( 'string' ) ), ); diff --git a/tests/JsonMapper/resources/Bag.php b/tests/JsonMapper/resources/Bag.php new file mode 100644 index 00000000..3498c971 --- /dev/null +++ b/tests/JsonMapper/resources/Bag.php @@ -0,0 +1,17 @@ +