diff --git a/composer.json b/composer.json index 335c8bf5..0f5d244b 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": "*", @@ -46,6 +46,11 @@ "src/WordPress/Streams/stream_str_replace.php" ] }, + "autoload-dev": { + "classmap": [ + "tests/" + ] + }, "scripts": { "phpcs": "phpcs --standard=WordPress" } 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..029dc79b 100644 --- a/src/WordPress/Blueprints/BlueprintMapper.php +++ b/src/WordPress/Blueprints/BlueprintMapper.php @@ -2,86 +2,86 @@ namespace WordPress\Blueprints; -use InvalidArgumentException; -use JsonMapper\JsonMapperBuilder; +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; class BlueprintMapper { + /** + * @var JsonMapper + */ + private $mapper; - protected $mapper; - + /** + * + */ public function __construct() { - $this->configureMapper(); - } - - 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 ] ); - } + $custom_factories = array( + ResourceDefinitionInterface::class => array( $this, 'resource_factory' ), + StepDefinitionInterface::class => array( $this, 'step_factory' ), ); + $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 { + return $this->mapper->hydrate( $blueprint, Blueprint::class ); + } - $stepMap = []; - foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { - $stepMap[ $class::DISCRIMINATOR ] = $class; + /** + * @param $value + * + * @return object|string + * @throws JsonMapperException + */ + public function resource_factory( $value ) { + $resource_map = array(); + foreach ( ModelInfo::getResourceDefinitionInterfaceImplementations() as $resource_class ) { + $resource_map[ $resource_class::DISCRIMINATOR ] = $resource_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 ); - } - ); + 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" ); + } - $this->mapper = JsonMapperBuilder::new() - ->withPropertyMapper( new \JsonMapper\Handler\PropertyMapper( $classFactoryRegistry ) ) - ->withDocBlockAnnotationsMiddleware() - ->withTypedPropertiesMiddleware() - ->withNamespaceResolverMiddleware() - ->build(); + return $this->mapper->hydrate( $value, $resource_map[ $value->resource ] ); } /** - * Maps a parsed and validated JSON object to a Blueprint class instance. + * @param $value * - * @param object $blueprint - * - * @return Blueprint + * @return object + * @throws JsonMapperException */ - public function map( object $blueprint ): Blueprint { - return $this->mapper->mapToClass( $blueprint, Blueprint::class ); - } + public function step_factory( $value ) { + $step_map = array(); + foreach ( ModelInfo::getStepDefinitionInterfaceImplementations() as $class ) { + $step_map[ $class::DISCRIMINATOR ] = $class; + } + 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/Blueprints/BlueprintParser.php b/src/WordPress/Blueprints/BlueprintParser.php index d102ccb4..58abfbea 100644 --- a/src/WordPress/Blueprints/BlueprintParser.php +++ b/src/WordPress/Blueprints/BlueprintParser.php @@ -2,208 +2,124 @@ namespace WordPress\Blueprints; +use InvalidArgumentException; use Opis\JsonSchema\Errors\ErrorFormatter; 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) - -// 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; -// } +class BlueprintParser { + + /** + * @var BlueprintValidator + */ + protected $validator; + + /** + * @var BlueprintMapper + */ + protected $mapper; + + public function __construct( + BlueprintValidator $validator, + BlueprintMapper $mapper + ) { + $this->validator = $validator; + $this->mapper = $mapper; + } + + public function parse( $raw_blueprint ) { + if ( $raw_blueprint instanceof \stdClass ) { + return $this->fromObject( $raw_blueprint ); + } + + if ( is_string( $raw_blueprint ) ) { + $data = json_decode( $raw_blueprint, false ); + + if ( null === $data ) { + throw new InvalidArgumentException( 'Malformed JSON.' ); + } + + return $this->fromObject( $data ); + } + + if ( $raw_blueprint instanceof Blueprint ) { + return $this->fromBlueprint( $raw_blueprint ); + } + + if ( $raw_blueprint instanceof BlueprintBuilder ) { + return $this->fromBlueprint( $raw_blueprint->toBlueprint() ); + } + + throw new InvalidArgumentException( + 'Unsupported $rawBlueprint type. Use a JSON string, a parsed JSON object, or a BlueprintBuilder instance.' + ); + } + + public function fromObject( \stdClass $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) + // { + // 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; + // } } 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..97c99ad3 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..f6e8b92a 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,20 @@ 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 downloadWordPress( $wpZip = null ) { + $this->prependStep( + ( new DownloadWordPressStep() ) ->setWordPressZip( $wpZip ?? 'https://wordpress.org/latest.zip' - ) ); + ) + ); return $this; - } public function runInstallationWizard() { @@ -145,21 +151,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 +183,3 @@ public function addStep( StepDefinitionInterface $builder ) { return $this; } } - diff --git a/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php b/src/WordPress/Blueprints/Model/DataClass/ActivatePluginStep.php index d8dd8dcb..4538d564 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'; diff --git a/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php b/src/WordPress/Blueprints/Model/DataClass/ActivateThemeStep.php index e0a2b89d..8d93b861 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'; diff --git a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php index 1abda4aa..6257788e 100644 --- a/src/WordPress/Blueprints/Model/DataClass/Blueprint.php +++ b/src/WordPress/Blueprints/Model/DataClass/Blueprint.php @@ -8,7 +8,7 @@ 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. @@ -29,7 +29,7 @@ class Blueprint * PHP Constants to define on every request * @var \ArrayObject */ - public $constants; + public $constants = []; /** * WordPress plugins to install and activate @@ -41,7 +41,7 @@ class Blueprint * WordPress site options to define * @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/CorePluginResource.php b/src/WordPress/Blueprints/Model/DataClass/CorePluginResource.php index 9772a304..086d45c1 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 diff --git a/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php b/src/WordPress/Blueprints/Model/DataClass/CoreThemeResource.php index bf4bd3c9..e7015a0a 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 diff --git a/src/WordPress/Blueprints/Model/DataClass/CpStep.php b/src/WordPress/Blueprints/Model/DataClass/CpStep.php index b86ad3a6..08d81a7d 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'; diff --git a/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php b/src/WordPress/Blueprints/Model/DataClass/DefineSiteUrlStep.php index 47b2b327..5ade7609 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'; diff --git a/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php b/src/WordPress/Blueprints/Model/DataClass/DefineWpConfigConstsStep.php index f2059f42..d75f533b 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'; 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..0bf34c2d 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. diff --git a/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php b/src/WordPress/Blueprints/Model/DataClass/FilesystemResource.php index d222c5b8..2c72c182 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) 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..fa3bde2f 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 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..911ee9da 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'; diff --git a/src/WordPress/Blueprints/Model/DataClass/MvStep.php b/src/WordPress/Blueprints/Model/DataClass/MvStep.php index 6dacc734..736d18aa 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'; 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..c4a791ed 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'; diff --git a/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php b/src/WordPress/Blueprints/Model/DataClass/RunPHPStep.php index 1bcb96b1..ff2fb327 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. 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..d9f1dd36 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". diff --git a/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php b/src/WordPress/Blueprints/Model/DataClass/UnzipStep.php index 7f292e18..f107951d 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'; diff --git a/src/WordPress/Blueprints/Model/DataClass/UrlResource.php b/src/WordPress/Blueprints/Model/DataClass/UrlResource.php index f4dd6d4c..cd03093a 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 @@ -24,26 +25,23 @@ class UrlResource implements ResourceDefinitionInterface { public $caption; - public function setResource( string $resource ) { + public function setResource(string $resource) + { $this->resource = $resource; - return $this; } - public function setUrl( string $url ) { + 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..3949a622 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. diff --git a/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php b/src/WordPress/Blueprints/Model/DataClass/WriteFileStep.php index 9785296c..dc1a05f8 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'; diff --git a/src/WordPress/Blueprints/bin/autogenerate_models.php b/src/WordPress/Blueprints/bin/autogenerate_models.php index ebd2139b..c03ca0da 100644 --- a/src/WordPress/Blueprints/bin/autogenerate_models.php +++ b/src/WordPress/Blueprints/bin/autogenerate_models.php @@ -205,8 +205,13 @@ 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() ) { + } 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( [] ); 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 new file mode 100644 index 00000000..e001d22a --- /dev/null +++ b/src/WordPress/JsonMapper/JsonMapper.php @@ -0,0 +1,320 @@ + + */ + private $factories = array(); + + /** + * @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(); + $this->add_custom_factories( $custom_factories ); + } + + /** + * 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. + * + * @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->run_factory( $class_name, $json ) + : $this->create_and_hydrate( $class_name, $json ); + } + + /** + * 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. + * + * @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 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 ); + + foreach ( (array) $json as $key => $value ) { + // Ignore null data in JSON. + if ( null === $value ) { + continue; + } + + $property = $property_map[ $key ] ?? null; + if ( null === $property ) { + continue; + } + + $value = $this->map_value( $property, $value ); + $this->set_value( $object, $property, $value ); + } + + return $object; + } + + /** + * 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. + return $value; + } + + foreach ( $property->property_types as $property_type ) { + $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 ) && $type_is_array && count( $value ) === 0 ) { + return array(); + } + + 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( $value, $property_type ); + } + + 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 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 + 'Unable to map ' . json_encode( $value ) . " to '$property->name'." + ); + } + + + private function set_value( $object, Property $property, $value ) { + // Use a setter if it exists. + $method_name = 'set' . ucfirst( $property->name ); + if ( method_exists( $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 + "Property: '" . get_class( $object ) . "::$property->name' is non-public and no setter method was found." + ); + + } + + 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; + } + } + + return true; + } + + if ( ! is_scalar( $value ) || ! Utils::is_type_scalar( $property_type ) ) { + return false; + } + + $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 $value_type === $property_type; + } + + 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; + } + + if ( 'string' === $property_type ) { + return (string) $value; + } + if ( 'boolean' === $property_type || 'bool' === $property_type ) { + return (bool) $value; + } + if ( 'integer' === $property_type || 'int' === $property_type ) { + return (int) $value; + } + if ( 'double' === $property_type || 'float' === $property_type ) { + return (float) $value; + } + + throw new \InvalidArgumentException( "\'$property_type\' is not a scalar value so it could not be cast to a scalar type." ); + } + + 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 ); + } + + return $mapped; + } + + return $this->run_factory( $property_type, $value ); + } + + 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 ( is_array( $value ) ) { + $mapped = array(); + foreach ( $value as $inner_value ) { + $mapped[] = $this->map_to_object( $inner_value, $property_type ); + } + + return $mapped; + } + + return $this->hydrate( $value, $property_type ); + } + + 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 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; + } + ); + $this->add_factory( + \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/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 @@ +name = $name; + $this->visibility = $visibility; + $this->property_types = $types; + } +} diff --git a/src/WordPress/JsonMapper/PropertyParser.php b/src/WordPress/JsonMapper/PropertyParser.php new file mode 100644 index 00000000..fcfebfea --- /dev/null +++ b/src/WordPress/JsonMapper/PropertyParser.php @@ -0,0 +1,189 @@ +[A-Za-z_-]+)[ \t]+(?P[\w\[\]\\\\|]*).*$/m'; + + /** + * Private constructor. + */ + 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 ) { + $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: AnInterface[]|null will return: array('AnInterface[]') + * + * 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(); + foreach ( explode( '|', $var ) as $original_property_type ) { + // Filter out 'null' type. + if ( 'null' === $original_property_type ) { + continue; + } + $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(); + $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/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 @@ +blueprint_mapper = new BlueprintMapper(); + } + + public function testMapsEmptyBlueprint() { + $raw_blueprint = '{}'; + + $parsed_json = json_decode( $raw_blueprint, false ); + + $result = $this->blueprint_mapper->map( $parsed_json ); + + $expected = new Blueprint(); + + $this->assertEquals( $expected, $result ); + } + + public function testMapsWordPressVersion() { + $raw_blueprint = + '{ + "WordPressVersion":"https://wordpress.org/latest.zip" + }'; + + $parsed_json = json_decode( $raw_blueprint, false ); + + $result = $this->blueprint_mapper->map( $parsed_json ); + + $expected = new Blueprint(); + $expected->WordPressVersion = 'https://wordpress.org/latest.zip'; + + $this->assertEquals( $expected, $result ); + } + + public function testMapsMultiplePlugins() { + $raw_blueprint = + '{ + "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_blueprint, false ); + + $result = $this->blueprint_mapper->map( $parsed_json ); + + $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, $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 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 = + '{ + "steps": + [ + {"step":"mkdir","path":"dir1"}, + {"step":"rm","path":"dir1"}, + {"step":"mkdir","path":"dir2"} + ] + }'; + + $parsed_json = json_decode( $raw_blueprint, false ); + + $result = $this->blueprint_mapper->map( $parsed_json ); + + $expected = new Blueprint(); + $expected->steps = array( + 0 => ( new MkdirStep() )->setPath( 'dir1' ), + 1 => ( new RmStep() )->setPath( 'dir1' ), + 2 => ( new MkdirStep() )->setPath( 'dir2' ), + ); + + $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 ); + } +} diff --git a/tests/JsonMapper/JsonMapperTest.php b/tests/JsonMapper/JsonMapperTest.php new file mode 100644 index 00000000..989ee229 --- /dev/null +++ b/tests/JsonMapper/JsonMapperTest.php @@ -0,0 +1,206 @@ +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. + * + * @return void + */ + public function testMapsToArrayObject() { + $raw_json = '{}'; + + $parsed_json = json_decode( $raw_json ); + + $result = $this->json_mapper->hydrate( $parsed_json, ArrayObject::class ); + + $expected = new ArrayObject(); + + $this->assertEquals( $expected, $result ); + } + + public function testSetsPublicProperties() { + $raw_json = '{"publicProperty":"test"}'; + + $parsed_json = json_decode( $raw_json ); + + $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 ); + + $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 ); + + $result = $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); + + $expected = new TestResourceClassSetValue(); + $expected->setProtectedProperty( 'test' ); + + $this->assertEquals( $expected, $result ); + } + + public function testFailsSettingPrivatePropertyWithNoSetter() { + $raw_json = '{"setterlessPrivateProperty":"test"}'; + + $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." ); + $this->json_mapper->hydrate( $parsed_json, TestResourceClassSetValue::class ); + } + + public function testFailsSettingProtectedPropertyWithNoSetter() { + $raw_json = '{"setterlessProtectedProperty":"test"}'; + + $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() { + $result = $this->json_mapper->hydrate( + json_decode( '{"arrayOfStringArrays":[["test1","test2"],["test3","test4"]]}' ), + 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 ); + } + + 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/PropertyParserTest.php b/tests/JsonMapper/PropertyParserTest.php new file mode 100644 index 00000000..645ce6c1 --- /dev/null +++ b/tests/JsonMapper/PropertyParserTest.php @@ -0,0 +1,183 @@ + new Property( 'string', 'private', array( 'string' ) ), + ); + $this->assertEquals( $expected, $result ); + } + + public function testParsesPropertiesWithArraysOfScalarTypes() { + $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 testParsesPropertiesWithArrays() { + $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 testParsesPropertiesWithNoDocBlocks() { + $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 testParsesPropertiesWithPublicVisibility() { + $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 testParsesPropertiesWithProtectedVisibility() { + $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 testParsesPropertiesWithPrivateVisibility() { + $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 testParsesPropertiesWithUnionTypes() { + $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 testParsesPropertiesWithGlobalNamespacePrefixedType() { + $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 testParsesPropertiesWithNullType() { + $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/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 @@ +privateProperty = $value; + } + + public function setPrivateProperty( $value ) { + $this->privateProperty = $value; + } +} 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() -{ - // .. -}