diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index dbe1613..341f2d5 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -17,6 +17,7 @@ 'phpdoc_align' => false, 'phpdoc_annotation_without_dot' => false, 'single_import_per_statement' => false, + 'global_namespace_import' => false, ]) ; diff --git a/composer.json b/composer.json index 85d03a5..af78835 100755 --- a/composer.json +++ b/composer.json @@ -4,8 +4,11 @@ "license": "BSD-3-Clause", "require": { "php": "^8.1", + "cebe/php-openapi": "^1.7.0", "illuminate/database": "^9.0 || ^10.0", "illuminate/support": "^9.0 || ^10.0", + "laminas/laminas-config": "^3.7.0", + "laminas/laminas-config-aggregator": "^1.7.0", "laminas/laminas-servicemanager": "^3.20.0", "laminas/laminas-stdlib": "^3.1", "league/flysystem": "^3.0.0", @@ -19,14 +22,17 @@ "symfony/messenger": "^6.0", "symfony/property-access": "^6.0", "symfony/property-info": "^6.0", - "symfony/serializer": "^6.0" + "symfony/serializer": "^6.0", + "symfony/yaml": "^6.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.3.2", - "laminas/laminas-config-aggregator": "^1.1", + "laminas/laminas-diactoros": "^2.7", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-ftp": "^3.0", "league/flysystem-memory": "^3.0", + "league/openapi-psr7-validator": "^0.18", + "mezzio/mezzio-router": "^3.8", "nesbot/carbon": "^2.9.1", "phpspec/prophecy": "^1.8.0", "phpspec/prophecy-phpunit": "^2.0", @@ -37,6 +43,7 @@ }, "autoload": { "classmap": [ + "packages/api/src/", "packages/cache/src/", "packages/console/src/", "packages/db-eloquent/src/", @@ -47,6 +54,7 @@ "packages/service-manager/src/" ], "psr-4": { + "AftDev\\Api\\": "packages/api/src/", "AftDev\\Cache\\": "packages/cache/src/", "AftDev\\Console\\": "packages/console/src/", "AftDev\\DbEloquent\\": "packages/db-eloquent/src/", @@ -59,6 +67,7 @@ }, "autoload-dev": { "psr-4": { + "AftDevTest\\Api\\": "packages/api/tests/", "AftDevTest\\Cache\\": "packages/cache/tests/", "AftDevTest\\Console\\": "packages/console/tests/", "AftDevTest\\DbEloquent\\": "packages/db-eloquent/tests/", @@ -71,6 +80,7 @@ } }, "replace": { + "aftdev/api": "self.version", "aftdev/cache-manager": "1.0.0", "aftdev/console-manager": "1.0.0", "aftdev/db-eloquent": "1.0.0", @@ -100,6 +110,7 @@ "extra": { "laminas": { "config-provider": [ + "AftDev\\Api\\ConfigProvider", "AftDev\\Cache\\ConfigProvider", "AftDev\\Console\\ConfigProvider", "AftDev\\Db\\ConfigProvider", diff --git a/composer.lock b/composer.lock index 35268fb..32f2ae1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fed863c4f94050a13a9d96d18bf068b3", + "content-hash": "9be4d3b7053708200aceb4f222d8d923", "packages": [ { "name": "brick/math", @@ -61,6 +61,55 @@ ], "time": "2023-01-15T23:15:59+00:00" }, + { + "name": "brick/varexporter", + "version": "0.3.8", + "source": { + "type": "git", + "url": "https://github.com/brick/varexporter.git", + "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/varexporter/zipball/b5853edea6204ff8fa10633c3a4cccc4058410ed", + "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5 || ^9.0", + "vimeo/psalm": "4.23.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\VarExporter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", + "keywords": [ + "var_export" + ], + "support": { + "issues": "https://github.com/brick/varexporter/issues", + "source": "https://github.com/brick/varexporter/tree/0.3.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-01-21T23:05:38+00:00" + }, { "name": "cakephp/core", "version": "4.4.15", @@ -294,6 +343,75 @@ }, "time": "2023-02-24T22:07:16+00:00" }, + { + "name": "cebe/php-openapi", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/cebe/php-openapi.git", + "reference": "020d72b8e3a9a60bc229953e93eda25c49f46f45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cebe/php-openapi/zipball/020d72b8e3a9a60bc229953e93eda25c49f46f45", + "reference": "020d72b8e3a9a60bc229953e93eda25c49f46f45", + "shasum": "" + }, + "require": { + "ext-json": "*", + "justinrainbow/json-schema": "^5.2", + "php": ">=7.1.0", + "symfony/yaml": "^3.4 || ^4 || ^5 || ^6" + }, + "conflict": { + "symfony/yaml": "3.4.0 - 3.4.4 || 4.0.0 - 4.4.17 || 5.0.0 - 5.1.9 || 5.2.0" + }, + "require-dev": { + "apis-guru/openapi-directory": "1.0.0", + "cebe/indent": "*", + "mermade/openapi3-examples": "1.0.0", + "nexmo/api-specification": "1.0.0", + "oai/openapi-specification": "3.0.3", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5 || ^9.4" + }, + "bin": [ + "bin/php-openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "cebe\\openapi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "https://cebe.cc/", + "role": "Creator" + } + ], + "description": "Read and write OpenAPI yaml/json files and make the content accessable in PHP objects.", + "homepage": "https://github.com/cebe/php-openapi#readme", + "keywords": [ + "openapi" + ], + "support": { + "issues": "https://github.com/cebe/php-openapi/issues", + "source": "https://github.com/cebe/php-openapi" + }, + "time": "2022-04-20T14:46:44+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.8", @@ -771,6 +889,213 @@ }, "time": "2023-06-27T18:33:36+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.12", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", + "reference": "ad87d5a5ca981228e0e205c2bc7dfb8e24559b60", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.12" + }, + "time": "2022-04-13T08:02:27+00:00" + }, + { + "name": "laminas/laminas-config", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-config.git", + "reference": "46baad58d0b12cf98539e04334eff40a1fdfb9a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-config/zipball/46baad58d0b12cf98539e04334eff40a1fdfb9a0", + "reference": "46baad58d0b12cf98539e04334eff40a1fdfb9a0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laminas/laminas-stdlib": "^3.6", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "psr/container": "^1.0" + }, + "conflict": { + "container-interop/container-interop": "<1.2.0", + "zendframework/zend-config": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.4.0", + "laminas/laminas-filter": "~2.23.0", + "laminas/laminas-i18n": "~2.19.0", + "laminas/laminas-servicemanager": "~3.19.0", + "phpunit/phpunit": "~9.5.25" + }, + "suggest": { + "laminas/laminas-filter": "^2.7.2; install if you want to use the Filter processor", + "laminas/laminas-i18n": "^2.7.4; install if you want to use the Translator processor", + "laminas/laminas-servicemanager": "^2.7.8 || ^3.3; if you need an extensible plugin manager for use with the Config Factory" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Config\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides a nested object property based user interface for accessing this configuration data within application code", + "homepage": "https://laminas.dev", + "keywords": [ + "config", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-config/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-config/issues", + "rss": "https://github.com/laminas/laminas-config/releases.atom", + "source": "https://github.com/laminas/laminas-config" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-10-16T14:21:22+00:00" + }, + { + "name": "laminas/laminas-config-aggregator", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-config-aggregator.git", + "reference": "5c445bbe9afabb7fd7c38382f27930f11632dd90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-config-aggregator/zipball/5c445bbe9afabb7fd7c38382f27930f11632dd90", + "reference": "5c445bbe9afabb7fd7c38382f27930f11632dd90", + "shasum": "" + }, + "require": { + "brick/varexporter": "^0.3.7", + "laminas/laminas-stdlib": "^3.10.1", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "webimpress/safe-writer": "^2.2.0" + }, + "conflict": { + "nikic/php-parser": "<4.12", + "zendframework/zend-config-aggregator": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.4.0", + "laminas/laminas-config": "^3.8.0", + "laminas/laminas-servicemanager": "^3.19", + "phpunit/phpunit": "^9.5.26", + "psalm/plugin-phpunit": "^0.18.0", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "laminas/laminas-config": "Allows loading configuration from XML, INI, YAML, and JSON files", + "laminas/laminas-config-aggregator-modulemanager": "Allows loading configuration from laminas-mvc Module classes", + "laminas/laminas-config-aggregator-parameters": "Allows usage of templated parameters within your configuration" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ConfigAggregator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Lightweight library for collecting and merging configuration from different sources", + "homepage": "https://laminas.dev", + "keywords": [ + "config-aggregator", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-config-aggregator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-config-aggregator/issues", + "rss": "https://github.com/laminas/laminas-config-aggregator/releases.atom", + "source": "https://github.com/laminas/laminas-config-aggregator" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-12-03T21:22:49+00:00" + }, { "name": "laminas/laminas-servicemanager", "version": "3.21.0", @@ -1329,17 +1654,73 @@ "time": "2023-06-20T18:29:04+00:00" }, { - "name": "ocramius/package-versions", - "version": "2.7.0", + "name": "nikic/php-parser", + "version": "v4.16.0", "source": { "type": "git", - "url": "https://github.com/Ocramius/PackageVersions.git", - "reference": "065921ed7cb2a6861443d91138d0a4378316af8d" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/065921ed7cb2a6861443d91138d0a4378316af8d", - "reference": "065921ed7cb2a6861443d91138d0a4378316af8d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + }, + "time": "2023-06-25T14:52:30+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "065921ed7cb2a6861443d91138d0a4378316af8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/065921ed7cb2a6861443d91138d0a4378316af8d", + "reference": "065921ed7cb2a6861443d91138d0a4378316af8d", "shasum": "" }, "require": { @@ -1537,16 +1918,16 @@ }, { "name": "psr/http-message", - "version": "2.0", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { @@ -1555,7 +1936,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -1570,7 +1951,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -1584,9 +1965,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/http-server-handler", @@ -3594,6 +3975,77 @@ ], "time": "2023-04-21T08:48:44+00:00" }, + { + "name": "symfony/yaml", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a9a8337aa641ef2aa39c3e028f9107ec391e5927" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a9a8337aa641ef2aa39c3e028f9107ec391e5927", + "reference": "a9a8337aa641ef2aa39c3e028f9107ec391e5927", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.3.0" + }, + "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": "2023-04-28T13:28:14+00:00" + }, { "name": "voku/portable-ascii", "version": "2.0.1", @@ -3667,6 +4119,65 @@ } ], "time": "2022-03-08T17:03:00+00:00" + }, + { + "name": "webimpress/safe-writer", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.4", + "vimeo/psalm": "^4.7", + "webimpress/coding-standard": "^1.2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev", + "dev-develop": "2.3.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "support": { + "issues": "https://github.com/webimpress/safe-writer/issues", + "source": "https://github.com/webimpress/safe-writer/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2021-04-19T16:34:45+00:00" } ], "packages-dev": [ @@ -3726,16 +4237,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.269.0", + "version": "3.275.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78" + "reference": "6cf6aacecda1dec52bf4a70d8e1503b5bc56e924" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78", - "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6cf6aacecda1dec52bf4a70d8e1503b5bc56e924", + "reference": "6cf6aacecda1dec52bf4a70d8e1503b5bc56e924", "shasum": "" }, "require": { @@ -3747,7 +4258,8 @@ "guzzlehttp/promises": "^1.4.0", "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "mtdowling/jmespath.php": "^2.6", - "php": ">=5.5" + "php": ">=5.5", + "psr/http-message": "^1.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -3764,7 +4276,6 @@ "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", "psr/cache": "^1.0", - "psr/http-message": "^1.0", "psr/simple-cache": "^1.0", "sebastian/comparator": "^1.2.3 || ^4.0", "yoast/phpunit-polyfills": "^1.0" @@ -3815,58 +4326,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.269.0" - }, - "time": "2023-04-26T18:21:04+00:00" - }, - { - "name": "brick/varexporter", - "version": "0.3.8", - "source": { - "type": "git", - "url": "https://github.com/brick/varexporter.git", - "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/brick/varexporter/zipball/b5853edea6204ff8fa10633c3a4cccc4058410ed", - "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.0", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^8.5 || ^9.0", - "vimeo/psalm": "4.23.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Brick\\VarExporter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", - "keywords": [ - "var_export" - ], - "support": { - "issues": "https://github.com/brick/varexporter/issues", - "source": "https://github.com/brick/varexporter/tree/0.3.8" + "source": "https://github.com/aws/aws-sdk-php/tree/3.275.1" }, - "funding": [ - { - "url": "https://github.com/BenMorel", - "type": "github" - } - ], - "time": "2023-01-21T23:05:38+00:00" + "time": "2023-06-30T18:23:40+00:00" }, { "name": "composer/pcre", @@ -4356,6 +4818,62 @@ ], "time": "2022-12-15T16:57:16+00:00" }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, { "name": "friendsofphp/php-cs-fixer", "version": "v3.20.0", @@ -4774,65 +5292,93 @@ "time": "2023-04-17T16:11:26+00:00" }, { - "name": "laminas/laminas-config-aggregator", - "version": "1.13.0", + "name": "laminas/laminas-diactoros", + "version": "2.25.2", "source": { "type": "git", - "url": "https://github.com/laminas/laminas-config-aggregator.git", - "reference": "5c445bbe9afabb7fd7c38382f27930f11632dd90" + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-config-aggregator/zipball/5c445bbe9afabb7fd7c38382f27930f11632dd90", - "reference": "5c445bbe9afabb7fd7c38382f27930f11632dd90", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e", + "reference": "9f3f4bf5b99c9538b6f1dbcc20f6fec357914f9e", "shasum": "" }, "require": { - "brick/varexporter": "^0.3.7", - "laminas/laminas-stdlib": "^3.10.1", "php": "~8.0.0 || ~8.1.0 || ~8.2.0", - "webimpress/safe-writer": "^2.2.0" + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1" }, "conflict": { - "nikic/php-parser": "<4.12", - "zendframework/zend-config-aggregator": "*" + "zendframework/zend-diactoros": "*" }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.4.0", - "laminas/laminas-config": "^3.8.0", - "laminas/laminas-servicemanager": "^3.19", - "phpunit/phpunit": "^9.5.26", - "psalm/plugin-phpunit": "^0.18.0", - "vimeo/psalm": "^5.0" + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" }, - "suggest": { - "laminas/laminas-config": "Allows loading configuration from XML, INI, YAML, and JSON files", - "laminas/laminas-config-aggregator-modulemanager": "Allows loading configuration from laminas-mvc Module classes", - "laminas/laminas-config-aggregator-parameters": "Allows usage of templated parameters within your configuration" + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^0.9.0", + "laminas/laminas-coding-standard": "^2.5", + "php-http/psr7-integration-tests": "^1.2", + "phpunit/phpunit": "^9.5.28", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.6" }, "type": "library", + "extra": { + "laminas": { + "config-provider": "Laminas\\Diactoros\\ConfigProvider", + "module": "Laminas\\Diactoros" + } + }, "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php", + "src/functions/create_uploaded_file.legacy.php", + "src/functions/marshal_headers_from_sapi.legacy.php", + "src/functions/marshal_method_from_sapi.legacy.php", + "src/functions/marshal_protocol_version_from_sapi.legacy.php", + "src/functions/marshal_uri_from_sapi.legacy.php", + "src/functions/normalize_server.legacy.php", + "src/functions/normalize_uploaded_files.legacy.php", + "src/functions/parse_cookie_header.legacy.php" + ], "psr-4": { - "Laminas\\ConfigAggregator\\": "src/" + "Laminas\\Diactoros\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "description": "Lightweight library for collecting and merging configuration from different sources", + "description": "PSR HTTP Message implementations", "homepage": "https://laminas.dev", "keywords": [ - "config-aggregator", - "laminas" + "http", + "laminas", + "psr", + "psr-17", + "psr-7" ], "support": { "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-config-aggregator/", + "docs": "https://docs.laminas.dev/laminas-diactoros/", "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-config-aggregator/issues", - "rss": "https://github.com/laminas/laminas-config-aggregator/releases.atom", - "source": "https://github.com/laminas/laminas-config-aggregator" + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" }, "funding": [ { @@ -4840,7 +5386,7 @@ "type": "community_bridge" } ], - "time": "2022-12-03T21:22:49+00:00" + "time": "2023-04-17T15:44:17+00:00" }, { "name": "league/flysystem-aws-s3-v3", @@ -4990,42 +5536,356 @@ "type": "library", "autoload": { "psr-4": { - "League\\Flysystem\\InMemory\\": "" + "League\\Flysystem\\InMemory\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "In-memory filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "memory" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-memory/issues", + "source": "https://github.com/thephpleague/flysystem-memory/tree/3.15.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-05-02T20:02:14+00:00" + }, + { + "name": "league/openapi-psr7-validator", + "version": "0.18", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/openapi-psr7-validator.git", + "reference": "5f98f98abf37f4533473699ef2ff2b4dc9b8d52e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/openapi-psr7-validator/zipball/5f98f98abf37f4533473699ef2ff2b4dc9b8d52e", + "reference": "5f98f98abf37f4533473699ef2ff2b4dc9b8d52e", + "shasum": "" + }, + "require": { + "cebe/php-openapi": "^1.6", + "ext-json": "*", + "league/uri": "^6.3", + "php": ">=7.2", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/http-message": "^1.0", + "psr/http-server-middleware": "^1.0", + "respect/validation": "^1.1.3 || ^2.0", + "riverline/multipart-parser": "^2.0.3", + "webmozart/assert": "^1.4" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "guzzlehttp/psr7": "^1.5", + "hansott/psr7-cookies": "^3.0.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-webmozart-assert": "^1", + "phpunit/phpunit": "^7 || ^8 || ^9", + "symfony/cache": "^5.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OpenAPIValidation\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Validate PSR-7 messages against OpenAPI (3.0.2) specifications expressed in YAML or JSON", + "homepage": "https://github.com/thephpleague/openapi-psr7-validator", + "keywords": [ + "http", + "openapi", + "psr7", + "validation" + ], + "support": { + "issues": "https://github.com/thephpleague/openapi-psr7-validator/issues", + "source": "https://github.com/thephpleague/openapi-psr7-validator/tree/0.18" + }, + "time": "2022-03-01T10:34:32+00:00" + }, + { + "name": "league/uri", + "version": "6.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/a700b4656e4c54371b799ac61e300ab25a2d1d39", + "reference": "a700b4656e4c54371b799ac61e300ab25a2d1d39", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/uri-interfaces": "^2.3", + "php": "^8.1", + "psr/http-message": "^1.0.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.9.5", + "nyholm/psr7": "^1.5.1", + "php-http/psr7-integration-tests": "^1.1.1", + "phpbench/phpbench": "^1.2.6", + "phpstan/phpstan": "^1.8.5", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.4.3", + "phpunit/phpunit": "^9.5.24", + "psr/http-factory": "^1.0.1" + }, + "suggest": { + "ext-fileinfo": "Needed to create Data URI from a filepath", + "ext-intl": "Needed to improve host validation", + "league/uri-components": "Needed to easily manipulate URI objects", + "psr/http-factory": "Needed to use the URI factory" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri/issues", + "source": "https://github.com/thephpleague/uri/tree/6.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2022-09-13T19:58:47+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "reference": "00e7e2943f76d8cb50c7dfdc2f6dee356e15e383", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "phpstan/phpstan": "^0.12.90", + "phpstan/phpstan-phpunit": "^0.12.19", + "phpstan/phpstan-strict-rules": "^0.12.9", + "phpunit/phpunit": "^8.5.15 || ^9.5" + }, + "suggest": { + "ext-intl": "to use the IDNA feature", + "symfony/intl": "to use the IDNA feature via Symfony Polyfill" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interface for URI representation", + "homepage": "http://github.com/thephpleague/uri-interfaces", + "keywords": [ + "rfc3986", + "rfc3987", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/thephpleague/uri-interfaces/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2021-06-28T04:27:21+00:00" + }, + { + "name": "mezzio/mezzio-router", + "version": "3.16.1", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-router.git", + "reference": "b83d61a728fdc2c62c6d20d16b73414901b36070" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-router/zipball/b83d61a728fdc2c62c6d20d16b73414901b36070", + "reference": "b83d61a728fdc2c62c6d20d16b73414901b36070", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.2", + "php": "~8.1.0 || ~8.2.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0.1 || ^2.0.0", + "psr/http-server-middleware": "^1.0", + "webmozart/assert": "^1.10" + }, + "conflict": { + "mezzio/mezzio": "<3.5", + "zendframework/zend-expressive-router": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-diactoros": "^2.25.2", + "laminas/laminas-servicemanager": "^3.20.0", + "laminas/laminas-stratigility": "^3.9.0", + "phpunit/phpunit": "^10.1.2", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.9" + }, + "suggest": { + "mezzio/mezzio-aurarouter": "^3.0 to use the Aura.Router routing adapter", + "mezzio/mezzio-fastroute": "^3.0 to use the FastRoute routing adapter", + "mezzio/mezzio-laminasrouter": "^3.0 to use the laminas-router routing adapter" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Router\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Router\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frankdejonge.nl" - } + "BSD-3-Clause" ], - "description": "In-memory filesystem adapter for Flysystem.", + "description": "Router subcomponent for Mezzio", + "homepage": "https://mezzio.dev", "keywords": [ - "Flysystem", - "file", - "files", - "filesystem", - "memory" + "http", + "laminas", + "mezzio", + "middleware", + "psr", + "psr-7" ], "support": { - "issues": "https://github.com/thephpleague/flysystem-memory/issues", - "source": "https://github.com/thephpleague/flysystem-memory/tree/3.15.0" + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio/features/router/intro/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-router/issues", + "rss": "https://github.com/mezzio/mezzio-router/releases.atom", + "source": "https://github.com/mezzio/mezzio-router" }, "funding": [ { - "url": "https://ecologi.com/frankdejonge", - "type": "custom" - }, - { - "url": "https://github.com/frankdejonge", - "type": "github" + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" } ], - "time": "2023-05-02T20:02:14+00:00" + "time": "2023-04-24T14:33:22+00:00" }, { "name": "mtdowling/jmespath.php", @@ -5147,62 +6007,6 @@ ], "time": "2023-03-08T13:26:56+00:00" }, - { - "name": "nikic/php-parser", - "version": "v4.16.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" - }, - "time": "2023-06-25T14:52:30+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.3", @@ -6273,6 +7077,183 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "respect/stringifier", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/Respect/Stringifier.git", + "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/Stringifier/zipball/e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59", + "reference": "e55af3c8aeaeaa2abb5fa47a58a8e9688cc23b59", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.8", + "malukenho/docheader": "^0.1.7", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/stringify.php" + ], + "psr-4": { + "Respect\\Stringifier\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Respect/Stringifier Contributors", + "homepage": "https://github.com/Respect/Stringifier/graphs/contributors" + } + ], + "description": "Converts any value to a string", + "homepage": "http://respect.github.io/Stringifier/", + "keywords": [ + "respect", + "stringifier", + "stringify" + ], + "support": { + "issues": "https://github.com/Respect/Stringifier/issues", + "source": "https://github.com/Respect/Stringifier/tree/0.2.0" + }, + "time": "2017-12-29T19:39:25+00:00" + }, + { + "name": "respect/validation", + "version": "2.2.4", + "source": { + "type": "git", + "url": "https://github.com/Respect/Validation.git", + "reference": "d304ace5325efd7180daffb1f8627bb0affd4e3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/Validation/zipball/d304ace5325efd7180daffb1f8627bb0affd4e3a", + "reference": "d304ace5325efd7180daffb1f8627bb0affd4e3a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0 || ^8.1 || ^8.2", + "respect/stringifier": "^0.2.0", + "symfony/polyfill-mbstring": "^1.2" + }, + "require-dev": { + "egulias/email-validator": "^3.0", + "malukenho/docheader": "^0.1", + "mikey179/vfsstream": "^1.6", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.6", + "psr/http-message": "^1.0", + "respect/coding-standard": "^3.0", + "squizlabs/php_codesniffer": "^3.7", + "symfony/validator": "^3.0||^4.0" + }, + "suggest": { + "egulias/email-validator": "Strict (RFC compliant) email validation", + "ext-bcmath": "Arbitrary Precision Mathematics", + "ext-fileinfo": "File Information", + "ext-mbstring": "Multibyte String Functions" + }, + "type": "library", + "autoload": { + "psr-4": { + "Respect\\Validation\\": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Respect/Validation Contributors", + "homepage": "https://github.com/Respect/Validation/graphs/contributors" + } + ], + "description": "The most awesome validation engine ever created for PHP", + "homepage": "http://respect.github.io/Validation/", + "keywords": [ + "respect", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/Respect/Validation/issues", + "source": "https://github.com/Respect/Validation/tree/2.2.4" + }, + "time": "2023-02-15T01:05:24+00:00" + }, + { + "name": "riverline/multipart-parser", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Riverline/multipart-parser.git", + "reference": "2418bdfc2eab01e39bcffee808b1a365c166292a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Riverline/multipart-parser/zipball/2418bdfc2eab01e39bcffee808b1a365c166292a", + "reference": "2418bdfc2eab01e39bcffee808b1a365c166292a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "laminas/laminas-diactoros": "^1.8.7 || ^2.11.1", + "phpunit/phpunit": "^5.7 || ^9.0", + "psr/http-message": "^1.0", + "symfony/psr-http-message-bridge": "^1.1 || ^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Riverline\\MultiPartParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Romain Cambien", + "email": "romain@cambien.net" + }, + { + "name": "Riverline", + "homepage": "http://www.riverline.fr" + } + ], + "description": "One class library to parse multipart content with encoding and charset support.", + "keywords": [ + "http", + "multipart", + "parser" + ], + "support": { + "issues": "https://github.com/Riverline/multipart-parser/issues", + "source": "https://github.com/Riverline/multipart-parser/tree/2.1.1" + }, + "time": "2023-04-28T18:53:59+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.0", @@ -7923,65 +8904,6 @@ ], "time": "2021-07-28T10:34:58+00:00" }, - { - "name": "webimpress/safe-writer", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/webimpress/safe-writer.git", - "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", - "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", - "shasum": "" - }, - "require": { - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.5.4", - "vimeo/psalm": "^4.7", - "webimpress/coding-standard": "^1.2.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev", - "dev-develop": "2.3.x-dev", - "dev-release-1.0": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Webimpress\\SafeWriter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "description": "Tool to write files safely, to avoid race conditions", - "keywords": [ - "concurrent write", - "file writer", - "race condition", - "safe writer", - "webimpress" - ], - "support": { - "issues": "https://github.com/webimpress/safe-writer/issues", - "source": "https://github.com/webimpress/safe-writer/tree/2.2.0" - }, - "funding": [ - { - "url": "https://github.com/michalbundyra", - "type": "github" - } - ], - "time": "2021-04-19T16:34:45+00:00" - }, { "name": "webmozart/assert", "version": "1.11.0", diff --git a/config/autoload/.gitignore b/config/autoload/.gitignore deleted file mode 100755 index 1a83fda..0000000 --- a/config/autoload/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -local.php -*.local.php diff --git a/config/autoload/api.global.php b/config/autoload/api.global.php new file mode 100644 index 0000000..86b7474 --- /dev/null +++ b/config/autoload/api.global.php @@ -0,0 +1,9 @@ + [ + 'spec' => realpath('tests/data/openapi/petstore.yaml'), + ], +]; diff --git a/config/autoload/database.local.php b/config/autoload/database.local.php new file mode 100755 index 0000000..bf7e2d1 --- /dev/null +++ b/config/autoload/database.local.php @@ -0,0 +1,14 @@ + [ + 'connections' => [ + 'test' => [ + 'hostname' => 'framework-mysql', + 'username' => 'root', + 'password' => 'root', + 'port' => 3306, + ], + ], + ], +]; diff --git a/config/autoload/development/.gitignore b/config/autoload/development/.gitignore new file mode 100755 index 0000000..94548af --- /dev/null +++ b/config/autoload/development/.gitignore @@ -0,0 +1,3 @@ +* +*/ +!.gitignore diff --git a/config/autoload/local/.gitignore b/config/autoload/local/.gitignore new file mode 100755 index 0000000..94548af --- /dev/null +++ b/config/autoload/local/.gitignore @@ -0,0 +1,3 @@ +* +*/ +!.gitignore diff --git a/config/config.php b/config/config.php index af6ff80..94e236c 100755 --- a/config/config.php +++ b/config/config.php @@ -7,6 +7,9 @@ $processors = []; +$env = getenv('APP_ENV') ?: 'production'; +$envFolders = [$env, 'local']; + $aggregator = new ConfigAggregator( [ \AftDev\ServiceManager\ConfigProvider::class, @@ -17,8 +20,9 @@ \AftDev\Db\ConfigProvider::class, \AftDev\DbEloquent\ConfigProvider::class, \AftDev\Messenger\ConfigProvider::class, + \AftDev\Api\ConfigProvider::class, - new PhpFileProvider(realpath(__DIR__).'/autoload/{{,*.}global,{,*.}local}.php'), + new PhpFileProvider(realpath(__DIR__).'/autoload/{{,*.}global,{'.join(',', $envFolders).'}/{,*}}.php'), ], null, $processors diff --git a/docker-compose.yml b/docker-compose.yml index 2b92460..b795396 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: # Map local user to container user for file permissions. user: '${UID:-1000}:${GID:-1000}' environment: + APP_ENV: ${APP_ENV:-production} PHP_VERSION: ${PHP_VERSION:-80} volumes: - './:/data' diff --git a/env/php/Dockerfile b/env/php/Dockerfile index 00fc5b6..efb48e3 100755 --- a/env/php/Dockerfile +++ b/env/php/Dockerfile @@ -24,4 +24,6 @@ COPY ["env/php/config.ini", "/etc/php.d/zzzzz_docker.ini"] WORKDIR /data +ENV APP_ENV production + CMD ["php", "-a"] diff --git a/packages/api/README.md b/packages/api/README.md new file mode 100644 index 0000000..4fc6dc9 --- /dev/null +++ b/packages/api/README.md @@ -0,0 +1,172 @@ +# Api Manager + +The main philosophy of this package is that your openAPI specification is the +source of truth of your application, it is the contract provided to your +consumers, and thus can be used to generate other things like routes or request +validator. + +## Configuration + +```php +[ + 'spec' => '/path/to/openapi.yml', + 'namespace' => 'App\Api\Controller', + 'mutations' => [], + 'versions' => [], + 'servers' => [ + 'url' => 'https://api.server.com', + ], +] +``` + +## Versioning and Mutations + +Versioning comes in handy when you introduce breaking changes. Our +recommendation is that every time it happens you would create a new version +based on a calendar date. + +The main idea is that the spec file should always contains the most up to date +information and each version only contain mutations to make it backward +compatible. + +Because the versions are in chronological order we can apply them recursively. + +```php +[ + 'spec' => '/path/to/openapi.yml', + 'current_version' => '2022-05-10', + 'versions' => [ + '2022-05-10' => [], + '2017-01-03' => [ + MutationOne::class, + ], + '2016-11-24' => [ + MutationTwo::class, + ], + '2012-04-03' => [ + MutationThree::class, + ], + ], +]; +``` + +e.g: to generate version `2016-11-03`, we would start from the `spec` file Then +apply all version rollbacks up to that version. In our example that means be +`MutationOne::class` and `MutationTwo::class` + +Mutations can apply to any version so you could even add mutations to the +current versions This could be useful if you want to test a new feature or a +change before making it public. + +```php +[ + 'spec' => '/path/to/openapi.yml' + 'current_version' => '2022-05-10', + 'versions' => [ + '2022-05-10' => [ + ChangeSpecIfFeatureToggleIsOn::class + ], + // ... + ], +] +``` + +Each mutations should be invokables classes. The OpenApi object will be passed +as argument so you can freely edit it. + +example: + +```php +use cebe\openapi\spec\OpenApi; + +class ChangeSpecIfFeatureToggleIsOn implements OpenApiMutation +{ + public function __invoke(OpenApi $spec, FeatureToggleService $featureToggle): void + { + $toggleOn = $featureToggle->isEnabled('You Cool new feature'); + + if (false === $toggleOn) { + // Do nothing. + return; + } + + $spec->paths['/new-path'] = new PathItem([...]); + } +} +``` + +Note: If configured properly, each mutation classes will be invoked via the +Service manager resolver. [TODO ADD LINK TO RESOLVER] It means that the class +dependencies will be automatically injected. + +### Checking version in your controllers / business logic services. + +From you controllers or other services you would just need to check the current +version via the `OpenApiManager->getVersion()` method. + +```php +use cebe\openapi\spec\OpenApi; + +class PathController +{ + public function __construct( + private OpenApi $openApi, + ) + {} + + public function __invoke() + { + if ($openApi->getVersion() > `2016-11-24`) { + return ['a', 'b', 'c']; + } + + return 'a'; + } +} +``` + +## Routing + +This package comes with a router helper that will autogenerate route +configuration from an openapi spec object. + +This configuration can then be used by your router of choice (like FastRouter or +Mezzio Application) + +```php + + +``` + +### Route parameters + +Each router have a different parameter format, by default our +OpenApiRouteGenerator is setup to use the FastRouter format. + +To override this, simply create a new translator and inject it to the Route +Generator (via container dependency injection or manually) + +```php + 'aliases' => [ + ParamTranslatorInterface::class => YourTranslator::class + ], +``` + +## OpenAPI request validator via middleware (PSR-15 Middleware Factory) + +This package provides a middleware factory that returns a PSR-15 compatible +middleware that will validate that all incoming requests matches the format +defined by your openapi spec. + +This middleware also validate that whatever data is sent back from your +controllers match the schemas defined in the spec + +The middleware uses the openapi validator from +https://github.com/thephpleague/openapi-psr7-validator + +```php +$openApiRequestValidatorMiddleware = $container->get(\League\OpenAPIValidation\PSR15\ValidationMiddleware::class); + +// Use middleware when defining your openapi routes +// Actual implementation will depending on your router. +``` diff --git a/packages/api/composer.json b/packages/api/composer.json new file mode 100755 index 0000000..d010ecf --- /dev/null +++ b/packages/api/composer.json @@ -0,0 +1,45 @@ +{ + "name": "aftdev/api", + "description": "Api Manager", + "license": "BSD-3-Clause", + "keywords": [ + "api", + "openapi" + ], + "require": { + "php": "^8.1", + "cebe/php-openapi": "^1.7.0", + "illuminate/support": "^9.0" + }, + "require-dev": { + "psr/http-server-middleware": "^1.0", + "league/openapi-psr7-validator": "^0.18", + "phpspec/prophecy-phpunit": "^2.0", + "laminas/laminas-diactoros": "^2.7", + "mezzio/mezzio-router": "^3.8" + }, + "autoload": { + "psr-4": { + "AftDev\\Api\\": "src/" + }, + "classmap": [ + "src/" + ] + }, + "autoload-dev": { + "psr-4": { + "AftDevTest\\Api\\": "tests/" + } + }, + "suggest": { + "aftdev/service-manager": "To autoload spec invokable classes", + "aftdev/cache": "To automatically cache generated specs and routes", + "psr/http-server-middleware": "To use the middleware", + "league/openapi-psr7-validator": "To automatically validate requests" + }, + "extra": { + "laminas": { + "config-provider": "AftDev\\Api\\ConfigProvider" + } + } +} diff --git a/packages/api/src/ConfigProvider.php b/packages/api/src/ConfigProvider.php new file mode 100644 index 0000000..6cbe528 --- /dev/null +++ b/packages/api/src/ConfigProvider.php @@ -0,0 +1,59 @@ + $this->getDependencies(), + self::CONFIG_KEY => $this->getApiConfig(), + ]; + } + + public function getDependencies() + { + return [ + 'factories' => [ + OpenApi::class => Factory\CurrentOpenApiVersionFactory::class, + OpenApiManager::class => Factory\OpenApiManagerFactory::class, + OpenApiRouteGenerator::class => Factory\OpenApiRouteGeneratorFactory::class, + HandlerMapper::class => InvokableFactory::class, + FastRouterParamTranslator::class => InvokableFactory::class, + \League\OpenAPIValidation\PSR15\ValidationMiddleware::class => Factory\RequestValidationMiddlewareFactory::class, + ], + 'aliases' => [ + ParamTranslatorInterface::class => FastRouterParamTranslator::class, + ], + ]; + } + + public function getApiConfig() + { + return [ + 'servers' => [ + 'default' => [ + 'url' => 'https://api.your-site.com', + ], + ], + 'spec' => 'config/openapi/openapi.yml', + 'namespace' => 'App\Api\Controller', + 'version' => null, + 'versions' => [], + // Cache store to use (requires aftdev/cache-manager) + 'cache' => null, + ]; + } +} diff --git a/packages/api/src/Exception/UnknownVersionException.php b/packages/api/src/Exception/UnknownVersionException.php new file mode 100644 index 0000000..4c94171 --- /dev/null +++ b/packages/api/src/Exception/UnknownVersionException.php @@ -0,0 +1,9 @@ +get(OpenApiManager::class); + + return $apiManager->getCurrentVersion(); + } +} diff --git a/packages/api/src/Factory/InteractsWithCacheTrait.php b/packages/api/src/Factory/InteractsWithCacheTrait.php new file mode 100644 index 0000000..c63380c --- /dev/null +++ b/packages/api/src/Factory/InteractsWithCacheTrait.php @@ -0,0 +1,22 @@ +has(CacheManager::class)) { + return $container->get(CacheManager::class)->store($store); + } + + return null; + } +} diff --git a/packages/api/src/Factory/OpenApiManagerFactory.php b/packages/api/src/Factory/OpenApiManagerFactory.php new file mode 100644 index 0000000..1715000 --- /dev/null +++ b/packages/api/src/Factory/OpenApiManagerFactory.php @@ -0,0 +1,53 @@ +get('config')[ConfigProvider::CONFIG_KEY]; + $resolver = $container->has(Resolver::class) ? $container->get(Resolver::class) : null; + + $version = $this->getApiVersionFromHeader($container) ?? $config['version'] ?? null; + $servers = $config['servers'] ?? []; + + $cache = $this->getCache($container, $config); + + return new OpenApiManager( + specFile: $config['spec'], + servers: $servers, + currentVersion: $version, + versions: $config['versions'] ?? [], + resolver: $resolver, + cache: $cache ?? null, + ); + } + + private function getApiVersionFromHeader(ContainerInterface $container): ?string + { + $request = $container->has(ServerRequestInterface::class) ? $container->get(ServerRequestInterface::class) : null; + + // Mezzio for some reason use a callback for the service. + if ($request instanceof Closure) { + $request = $request(); + } + + if (!$request) { + return null; + } + + return $request->getHeader(OpenApiManager::VERSION_HEADER_NAME, null)[0] ?? null; + } +} diff --git a/packages/api/src/Factory/OpenApiRouteGeneratorFactory.php b/packages/api/src/Factory/OpenApiRouteGeneratorFactory.php new file mode 100644 index 0000000..e89f85e --- /dev/null +++ b/packages/api/src/Factory/OpenApiRouteGeneratorFactory.php @@ -0,0 +1,30 @@ +get(ParamTranslatorInterface::class); + + $config = $container->get('config')[ConfigProvider::CONFIG_KEY]; + $namespace = $config['namespace'] ?? 'App\Controller'; + $cache = $this->getCache($container, $config); + + return new OpenApiRouteGenerator( + namespace: $namespace, + paramTranslator: $paramTranslator, + cache: $cache, + ); + } +} diff --git a/packages/api/src/Factory/RequestValidationMiddlewareFactory.php b/packages/api/src/Factory/RequestValidationMiddlewareFactory.php new file mode 100644 index 0000000..5a8d045 --- /dev/null +++ b/packages/api/src/Factory/RequestValidationMiddlewareFactory.php @@ -0,0 +1,24 @@ +get(OpenApiManager::class); + + $currentVersion = $apiManager->getCurrentVersion(); + + return (new ValidationMiddlewareBuilder()) + ->fromSchema($currentVersion) + ->getValidationMiddleware() + ; + } +} diff --git a/packages/api/src/Mutation/OpenApiMutation.php b/packages/api/src/Mutation/OpenApiMutation.php new file mode 100644 index 0000000..a75e7be --- /dev/null +++ b/packages/api/src/Mutation/OpenApiMutation.php @@ -0,0 +1,13 @@ +setVersions($versions); + $this->setCurrentVersion($currentVersion ?? self::BASE_VERSION); + } + + /** + * Set the default version. + */ + public function setCurrentVersion(string $version): void + { + $this->currentVersion = $version; + } + + public function hasVersion(string $version): bool + { + return isset($this->versions[$version]); + } + + public function getVersions(): array + { + return array_keys($this->versions); + } + + /** + * @throws UnknownVersionException + */ + public function getCurrentVersion(): OpenApi + { + return $this->getVersion($this->currentVersion); + } + + /** + * @throws UnknownVersionException + */ + public function getVersion(string $version): OpenApi + { + if (isset($this->cachedVersions[$version])) { + return $this->cachedVersions[$version]; + } + + $cache = $this->getCache($version); + if ($cache && $cache->isHit()) { + $versionOpenApi = $cache->get(); + } + + if (!isset($versionOpenApi)) { + $versionMutations = $this->getVersionMutations($version); + + $versionOpenApi = $this->getBase(); + + // Override Version. + if (self::BASE_VERSION != $version) { + $versionOpenApi->info->version = $version; + } + + // Add Servers. + $servers = array_map( + fn ($s) => new Server($s), + $this->servers, + ); + + if ($servers) { + $versionOpenApi->servers = $servers; + } + + $this->applyMutations($versionOpenApi, $versionMutations); + + if ($this->cache) { + $cache->set($versionOpenApi); + $this->cache->save($cache); + } + } + + return $this->cachedVersions[$version] = $versionOpenApi; + } + + public function getCache(string $version = 'default'): ?CacheItemInterface + { + if (null === $this->cache) { + return null; + } + + return $this->cache->getItem( + Str::swap( + [ + '{:version}' => preg_replace('/[^a-zA-Z0-9\.-_]+/', '', $version), + ], + self::CACHE_NAME, + ) + ); + } + + /** + * Set the openapi versions. + * + * The most recent version will be set as the current version. + */ + private function setVersions(array $versions): void + { + $this->versions = $versions; + krsort($this->versions); + + $lastVersion = key($this->versions); + if ($lastVersion) { + $this->setCurrentVersion($lastVersion); + } + } + + /** + * Get the base openapi. + */ + private function getBase(): OpenApi + { + $info = new SplFileInfo($this->specFile); + if (!$info->isFile()) { + throw new \ValueError('Could not find the openapi spec file'); + } + + switch ($info->getExtension()) { + case 'yaml': + case 'yml': + $helper = 'readFromYamlFile'; + + break; + + case 'json': + $helper = 'readFromJsonFile'; + + break; + + default: + throw new \ValueError('Invalid file extension'); + } + + return $this->base = Reader::$helper($this->specFile); + } + + /** + * @throws UnknownVersionException + */ + private function getVersionMutations(string $version): array + { + if (self::BASE_VERSION != $version && !$this->hasVersion($version)) { + throw new UnknownVersionException(sprintf('Unknown version %s', $version)); + } + + $mutations = []; + foreach ($this->versions as $loopVersion => $versionMutations) { + $mutations = array_merge($mutations, $versionMutations); + + if ($loopVersion === $version) { + break; + } + } + + return $mutations; + } + + private function applyMutations(OpenApi $openApi, array $mutations = []) + { + foreach ($mutations as $mutation) { + if ($this->resolver) { + $this->resolver->call( + $mutation, + [ + OpenApi::class => $openApi, + '$openApi' => $openApi, + ] + ); + } elseif (is_callable($mutation)) { + $mutation($openApi); + } + } + } +} diff --git a/packages/api/src/Route/FastRouterParamTranslator.php b/packages/api/src/Route/FastRouterParamTranslator.php new file mode 100644 index 0000000..578d637 --- /dev/null +++ b/packages/api/src/Route/FastRouterParamTranslator.php @@ -0,0 +1,31 @@ + '', + 'integer' => '\d+', + 'number' => '', + 'boolean' => 'true|false', + ]; + + public function translate(string $value, bool $required, string $type, ?string $format = null): string + { + $name = trim($value, '/{}'); + + $reg = $this->mapping[$type] ?? false; + if ($reg) { + $name .= ':'.$reg; + } + + if (!$required) { + return sprintf('[/{%s}]', $name); + } + + return sprintf('/{%s}', $name) ?: $value; + } +} diff --git a/packages/api/src/Route/OpenApiRouteGenerator.php b/packages/api/src/Route/OpenApiRouteGenerator.php new file mode 100644 index 0000000..8092f65 --- /dev/null +++ b/packages/api/src/Route/OpenApiRouteGenerator.php @@ -0,0 +1,212 @@ + 'index', // when no path params. + 'getWithParams' => 'show', // where there is a param + 'post' => 'create', + 'put' => 'update', + 'patch' => 'update', + ]; + + public function __construct( + private string $namespace = 'App\Controller', + private ?ParamTranslatorInterface $paramTranslator = null, + private ?CacheItemPoolInterface $cache = null, + ) { + } + + public function getCache($openApi): ?CacheItemInterface + { + if (null === $this->cache) { + return null; + } + + $version = $openApi->info->version ?? 'default'; + + return $this->cache->getItem( + Str::swap( + [ + '{:version}' => preg_replace('/[^a-zA-Z0-9]+/', '', $version), + ], + self::CACHE_NAME, + ) + ); + } + + /** + * Get routes form the given spec. + * + * Will fetch from cache if it exists. + * + * @return Route[] + */ + public function getRoutes(OpenApi $openApi): array + { + $routeCache = $this->getCache($openApi); + if ($routeCache && $routeCache->isHit()) { + return $routeCache->get(); + } + + $generator = $this->generateRoutes($openApi); + $routeArray = iterator_to_array($generator, false); + + if ($routeCache) { + $routeCache->set($routeArray); + $this->cache->save($routeCache); + } + + return $routeArray; + } + + /** + * Returns routes from the openapi spec. + */ + public function generateRoutes(OpenApi $openApi): \Generator + { + foreach ($openApi->paths as $path => $pathItem) { + yield from $this->getRoutesForPath($path, $pathItem); + } + } + + private function getRoutesForPath(string $uri, PathItem $path): \Generator + { + $pathParams = $this->getPathParameters($path->parameters); + + foreach ($path->getOperations() as $method => $operation) { + $params = [ + 'uri' => $this->swapParameters( + $uri, + array_merge( + $pathParams, + $this->getPathParameters($operation->parameters) + ), + ), + 'method' => $method, + 'name' => 'api.'.($operation->operationId ?? Str::camel("{$method}{$uri}")), + 'handler' => $this->getHandlerNameForOperation($uri, $method), + ]; + + yield new Route(...$params); + } + } + + private function swapParameters(string $uri, array $parameters) + { + if (!$this->paramTranslator) { + return $uri; + } + + // replacement list. + $translations = []; + foreach ($parameters as $param => $options) { + $translations["/{{$param}}"] = + $this->paramTranslator->translate( + "/{{$param}}", + ...$options, + ); + } + + return Str::swap($translations, $uri); + } + + private function getPathParameters(array $parameters): array + { + $pathParams = array_filter($parameters, fn ($param) => 'path' == $param->in); + $paramInfo = []; + foreach ($pathParams as $param) { + $paramInfo[$param->name] = [ + 'required' => $param->required, + 'type' => $param->schema->type, + 'format' => $param->schema->format, + ]; + } + + return $paramInfo; + } + + private function getHandlerNameForOperation(string $path, string $method): string + { + $pathInfo = Str::of($path)->trim('/')->explode('/'); + + $routeParams = $pathInfo->filter(fn ($str) => Str::match('/^\{.+\}$/', $str)); + $routeResources = $pathInfo->diff($routeParams); + + $controllerPosition = $routeResources->keys()->last(); + $routeParams = $routeParams->skipWhile(fn ($i, $key) => $key <= $controllerPosition); + + // Controller is always the last nonParam + $controller = $this->getControllerForPath((string) $routeResources->pop()); + $methodName = $this->getMethodName($method, $routeResources, $routeParams, $controller); + + return $controller.'@'.$methodName; + } + + private function getControllerForPath(string $controller): string + { + return $this->namespace.'\\'.Str::of($controller)->ucfirst()->pluralStudly(); + } + + private function getMethodName(string $method, Collection $routeResources, Collection $routeParams, string $controller): string + { + $hasParams = $routeParams->count(); + + // From Mapping + $verb = ($hasParams && isset($this->methodMapping[$method.'WithParams']) ? $this->methodMapping[$method.'WithParams'] : null) + ?? $this->methodMapping[$method] + ?? $method; + + $scopedBy = $routeResources->map(fn ($s) => Str::of($s)->singular()->studly()->ucfirst()); + $filterBy = $routeParams->map(fn ($s) => Str::of($s)->trim('{}')->ucfirst()); + + $methodName = $verb; + $glue = 'By'; + + if ($scopedBy->count()) { + $methodName .= $glue.$scopedBy->join(''); + $glue = 'And'; + } + + // If we have only one param do not use it if it matches the controller name + $appendFilterBy = + $hasParams + && (1 !== $hasParams || !$this->paramMatchesController($filterBy->first(), $controller)); + + if ($appendFilterBy) { + $methodName .= $glue.$filterBy->join(''); + } + + return $methodName; + } + + private function paramMatchesController($param, $controller): bool + { + $potentialMatches = [ + 'id', + 'uuid', + $controller, + $controller.'Id', + $controller.'Uuid', + ]; + + return Str::contains( + needles: $potentialMatches, + haystack: $param, + ignoreCase: true, + ); + } +} diff --git a/packages/api/src/Route/ParamTranslatorInterface.php b/packages/api/src/Route/ParamTranslatorInterface.php new file mode 100644 index 0000000..19b1f4b --- /dev/null +++ b/packages/api/src/Route/ParamTranslatorInterface.php @@ -0,0 +1,10 @@ +container = $this->prophesize(ContainerInterface::class); + $this->resolver = $this->createStub(Resolver::class); + + $self = $this; + $this->container->get('config')->will(fn () => $self->config); + + $this->container->has(Resolver::class)->willReturn(false); + $this->container->has(ServerRequestInterface::class)->willReturn(false); + } + + public function testWithResolver() + { + $this->config = [ + 'api' => [ + 'spec' => 'x', + 'versions' => [], + 'version' => null, + ], + ]; + + $this->container->has(Resolver::class)->willReturn(true); + $this->container->get(Resolver::class)->shouldBeCalled()->willReturn($this->resolver); + + (new OpenApiManagerFactory())($this->container->reveal()); + } + + public function testWithVersionFromHeader() + { + $this->config = [ + 'api' => [ + 'spec' => realpath(__DIR__.'/../specs/petstore.yaml'), + 'versions' => [ + 'from-header' => [], + 'from-config' => [], + ], + 'version' => 'from-config', + ], + ]; + + $version = 'from-header'; + + $request = new ServerRequest(headers: [OpenApiManager::VERSION_HEADER_NAME => $version]); + $this->container->has(ServerRequestInterface::class)->willReturn(true); + $this->container->get(ServerRequestInterface::class)->willReturn($request); + + $openApiManager = (new OpenApiManagerFactory())($this->container->reveal()); + + $versionSpec = $openApiManager->getCurrentVersion(); + $this->assertEquals($versionSpec->info->version, $version); + } + + public function testWithVersionFromHeaderCallback() + { + $this->config = [ + 'api' => [ + 'spec' => realpath(__DIR__.'/../specs/petstore.yaml'), + 'versions' => [ + 'from-header' => [], + 'from-config' => [], + ], + 'version' => 'from-config', + ], + ]; + + $version = 'from-header'; + + $request = new ServerRequest(headers: [OpenApiManager::VERSION_HEADER_NAME => $version]); + $this->container->has(ServerRequestInterface::class)->willReturn(true); + $this->container->get(ServerRequestInterface::class)->willReturn(fn () => $request); + + $openApiManager = (new OpenApiManagerFactory())($this->container->reveal()); + + $versionSpec = $openApiManager->getCurrentVersion(); + $this->assertEquals($versionSpec->info->version, $version); + } + + public function testWithVersionFromConfig() + { + $this->config = [ + 'api' => [ + 'spec' => realpath(__DIR__.'/../specs/petstore.yaml'), + 'versions' => [ + 'from-header' => [], + 'from-config' => [], + ], + 'version' => 'from-config', + ], + ]; + + $openApiManager = (new OpenApiManagerFactory())($this->container->reveal()); + + $versionSpec = $openApiManager->getCurrentVersion(); + $this->assertEquals($versionSpec->info->version, 'from-config'); + } + + public function testWithoutVersion() + { + $this->config = [ + 'api' => [ + 'spec' => realpath(__DIR__.'/../specs/petstore.yaml'), + ], + ]; + + $openApiManager = (new OpenApiManagerFactory())($this->container->reveal()); + + $versionSpec = $openApiManager->getCurrentVersion(); + $this->assertEquals($versionSpec->info->version, '1.0.0'); + } +} diff --git a/packages/api/tests/OpenApiManagerTest.php b/packages/api/tests/OpenApiManagerTest.php new file mode 100644 index 0000000..d0df255 --- /dev/null +++ b/packages/api/tests/OpenApiManagerTest.php @@ -0,0 +1,213 @@ +expectException(UnknownVersionException::class); + + $openApiManager = new OpenApiManager('specFile', [ + '2018-01-01' => [], + '2011-01-01' => [], + '2012-01-01' => [], + ]); + + $openApiManager->getVersion('v1'); + } + + public function testUnknownSpec() + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('Could not find the openapi spec file'); + + $openApiManager = new OpenApiManager('invalid.file'); + + $openApiManager->getCurrentVersion(); + } + + public function testUnknownExtension() + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage('Invalid file extension'); + + $openApiManager = new OpenApiManager(realpath(__DIR__.'/specs/not-supported.php')); + + $openApiManager->getCurrentVersion(); + } + + public function testVersions() + { + $openApiManager = new OpenApiManager( + realpath(__DIR__.'/specs/petstore.yaml'), + [ + '2018-01-01' => [], + '2011-01-01' => [], + '2012-01-01' => [], + ] + ); + + $versions = $openApiManager->getVersions(); + + $this->assertEquals([ + '2018-01-01', + '2012-01-01', + '2011-01-01', + ], $versions); + } + + public function testVersionMutations() + { + $container = $this->prophesize(ContainerInterface::class); + $resolver = new Resolver($container->reveal()); + + $openApiManager = new OpenApiManager( + realpath(__DIR__.'/specs/petstore.yaml'), + [ + '2018-01-01' => [], + '2011-01-01' => [ + fn (OpenApi $openApi) => $openApi->info->description .= '|Mutation2011A', + fn (OpenApi $openApi) => $openApi->info->description .= '|Mutation2011B', + ], + '2012-01-01' => [ + fn (OpenApi $openApi) => $openApi->info->description .= '|Mutation2012', + ], + ], + resolver: $resolver + ); + + $openApi = $openApiManager->getVersion('2011-01-01'); + + $this->assertEquals('2011-01-01', $openApi->info->version); + $this->assertEquals( + 'description_value|Mutation2012|Mutation2011A|Mutation2011B', + $openApi->info->description + ); + + $openApi = $openApiManager->getVersion('2012-01-01'); + + $this->assertEquals('2012-01-01', $openApi->info->version); + $this->assertEquals( + 'description_value|Mutation2012', + $openApi->info->description + ); + } + + public function testJsonSpec() + { + $openApiManager = new OpenApiManager(realpath(__DIR__.'/specs/petstore.json')); + $openApi = $openApiManager->getCurrentVersion(); + + $this->assertEquals( + 'description_value_json', + $openApi->info->description + ); + } + + public function testVersionCache() + { + $openApiManager = new OpenApiManager( + realpath(__DIR__.'/specs/petstore.yaml'), + ['2012-01-01' => []], + ); + + $openApi = $openApiManager->getVersion('2012-01-01'); + $openApi2 = $openApiManager->getVersion('2012-01-01'); + + $this->assertSame($openApi, $openApi2); + } + + public function testMutatationsNoContainer() + { + $openApiManager = new OpenApiManager( + realpath(__DIR__.'/specs/petstore.yaml'), + [ + '2012-01-01' => [ + fn (OpenApi $openApi) => $openApi->info->description .= '|Mutation2012', + ], + ] + ); + + $openApi = $openApiManager->getVersion('2012-01-01'); + $this->assertEquals( + 'description_value|Mutation2012', + $openApi->info->description + ); + } + + public function testNoVersion() + { + $openApiManager = new OpenApiManager(realpath(__DIR__.'/specs/petstore.yaml')); + $openApi = $openApiManager->getCurrentVersion(); + + $this->assertEquals( + 'description_value', + $openApi->info->description + ); + } + + public function testGetCurrentVersion() + { + $openApiManager = new OpenApiManager( + realpath(__DIR__.'/specs/petstore.yaml'), + versions: [ + 'test-version' => [ + fn (OpenApi $openApi) => $openApi->info->version = 'test-version', + ], + ], + ); + $openApiManager->setCurrentVersion('test-version'); + + $openApi = $openApiManager->getCurrentVersion(); + $this->assertEquals( + 'test-version', + $openApi->info->version + ); + } + + public function testServers() + { + $openApiManager = new OpenApiManager( + realpath(__DIR__.'/specs/petstore.yaml'), + servers: [ + 'server_a' => [ + 'url' => 'https://serverA.com', + ], + 'server_b' => [ + 'url' => 'https://serverB.com', + ], + ], + ); + + $openApi = $openApiManager->getCurrentVersion(); + + $this->assertCount(2, $openApi->servers); + $serverA = current($openApi->servers); + $serverB = last($openApi->servers); + + $this->assertInstanceOf(Server::class, $serverA); + $this->assertInstanceOf(Server::class, $serverB); + + $this->assertSame('https://serverA.com', $serverA->url); + $this->assertSame('https://serverB.com', $serverB->url); + + $this->assertTrue($openApi->validate()); + } +} diff --git a/packages/api/tests/Route/FastRouterParamTranslatorTest.php b/packages/api/tests/Route/FastRouterParamTranslatorTest.php new file mode 100644 index 0000000..f849369 --- /dev/null +++ b/packages/api/tests/Route/FastRouterParamTranslatorTest.php @@ -0,0 +1,73 @@ +translate('/{id}', ...$options); + $this->assertEquals($expected, $transformed); + } + + static public function dataProvider() + { + return [ + 'string' => [ + 'options' => [ + 'required' => true, + 'type' => 'string', + 'format' => '', + ], + 'expected' => '/{id}', + ], + 'string-optional' => [ + 'options' => [ + 'required' => false, + 'type' => 'string', + 'format' => '', + ], + 'expected' => '[/{id}]', + ], + 'integer' => [ + 'options' => [ + 'required' => true, + 'type' => 'integer', + 'format' => '', + ], + 'expected' => '/{id:\d+}', + ], + 'number' => [ + 'options' => [ + 'required' => true, + 'type' => 'number', + 'format' => '', + ], + 'expected' => '/{id}', + ], + 'boolean' => [ + 'options' => [ + 'required' => true, + 'type' => 'boolean', + 'format' => '', + ], + 'expected' => '/{id:true|false}', + ], + ]; + } +} diff --git a/packages/api/tests/Route/OpenApiRouteGeneratorTest.php b/packages/api/tests/Route/OpenApiRouteGeneratorTest.php new file mode 100644 index 0000000..6b1226a --- /dev/null +++ b/packages/api/tests/Route/OpenApiRouteGeneratorTest.php @@ -0,0 +1,108 @@ +getRoutes($this->getOpenApi()); + + $this->assertIsArray($routes); + } + + public function testGenerator() + { + $generator = new OpenApiRouteGenerator(); + $collection = new LazyCollection(fn () => $generator->generateRoutes($this->getOpenApi())); + + $found = []; + foreach ($collection as $i) { + $found[$i->uri][$i->method] = $i->handler; + } + + $expected = [ + '/companies' => [ + 'get' => 'App\Controller\Companies@index', + 'post' => 'App\Controller\Companies@create', + 'options' => 'App\Controller\Companies@options', + 'head' => 'App\Controller\Companies@head', + 'trace' => 'App\Controller\Companies@trace', + ], + '/companies/{companyId}' => [ + 'get' => 'App\Controller\Companies@show', + 'put' => 'App\Controller\Companies@update', + 'delete' => 'App\Controller\Companies@delete', + ], + '/companies/{companyId}/employees' => [ + 'get' => 'App\Controller\Employees@indexByCompany', + ], + '/companies/{companyId}/employees/{employeeId}' => [ + 'get' => 'App\Controller\Employees@showByCompany', + ], + '/companies/{companyId}/employees/{employeeId}/salary' => [ + 'get' => 'App\Controller\Salaries@indexByCompanyEmployee', + ], + '/companies/{companyId}/employees/{employeeId}/salary/{category}' => [ + 'get' => 'App\Controller\Salaries@showByCompanyEmployeeAndCategory', + ], + '/test/param/{intParam}/{stringParam}/{dateParam}/{optionalParam}' => [ + 'get' => 'App\Controller\Params@showByTestAndIntParamStringParamDateParamOptionalParam', + ], + ]; + + $this->assertEquals($expected, $found); + } + + public function testRouteParamTranslation() + { + $paramTranslator = $this->prophesize(ParamTranslatorInterface::class); + $paramTranslator->translate(Argument::cetera())->willReturn('/{:translated}'); + + $generator = new OpenApiRouteGenerator( + paramTranslator: $paramTranslator->reveal(), + ); + + $collection = $generator->generateRoutes($this->getOpenApi()); + $found = []; + foreach ($collection as $i) { + if (!in_array($i->uri, $found)) { + $found[] = $i->uri; + } + } + + $expected = [ + '/companies', + '/companies/{:translated}', + '/companies/{:translated}/employees', + '/companies/{:translated}/employees/{:translated}', + '/companies/{:translated}/employees/{:translated}/salary', + '/companies/{:translated}/employees/{:translated}/salary/{:translated}', + '/test/param/{:translated}/{:translated}/{:translated}/{:translated}', + ]; + + $this->assertEquals($expected, $found); + } + + protected function getOpenApi(): OpenApi + { + return Reader::readFromYamlFile(realpath(__DIR__.'/../specs/routes.yaml')); + } +} diff --git a/packages/api/tests/specs/not-supported.php b/packages/api/tests/specs/not-supported.php new file mode 100644 index 0000000..0b67a5f --- /dev/null +++ b/packages/api/tests/specs/not-supported.php @@ -0,0 +1,3 @@ + + Browsers send an HTTP OPTIONS request to find out the supported HTTP + methods and other options supported for the target resource before + sending the actual request. + head: + summary: > + HEAD method requests HTTP headers from the server as if the document was + requested using the HTTP GET method. The only difference between HTTP + HEAD and GET requests is that for HTTP HEAD, the server only returns + headers without body. + operationId: deleteCompany + trace: + operationId: traceCompany + /companies/{companyId}: + parameters: + - $ref: '#/components/parameters/company' + get: + summary: Info for a specific company + operationId: showCompanyById + delete: + summary: Delete a company + operationId: deleteCompany + put: + summary: Update company + operationId: updateCompanyById + parameters: + - name: companyId + in: path + required: true + description: The id of the company to retrieve + schema: + type: string + /companies/{companyId}/employees: + parameters: + - $ref: '#/components/parameters/company' + get: + summary: List all company employees + operationId: listEmployeesByCompany + /companies/{companyId}/employees/{employeeId}: + parameters: + - $ref: '#/components/parameters/company' + - $ref: '#/components/parameters/employee' + get: + summary: Get one company employee by id + description: should rarely exist + operationId: getEmployeeByCompany + /companies/{companyId}/employees/{employeeId}/salary: + parameters: + - $ref: '#/components/parameters/company' + - $ref: '#/components/parameters/employee' + get: + summary: Get salary of a company employee + description: This is getting ridiculous + operationId: getSalaryByCompanyEmployee + /companies/{companyId}/employees/{employeeId}/salary/{category}: + parameters: + - $ref: '#/components/parameters/company' + - $ref: '#/components/parameters/employee' + - name: category + in: path + required: true + schema: + type: string + get: + summary: Get salary of a company employee by category + description: This is just silly now + operationId: getSalaryByCompanyEmployeeAndCategory + /test/param/{intParam}/{stringParam}/{dateParam}/{optionalParam}: + parameters: + - $ref: '#/components/parameters/intParam' + - $ref: '#/components/parameters/stringParam' + - $ref: '#/components/parameters/numberParam' + - $ref: '#/components/parameters/dateParam' + - $ref: '#/components/parameters/optionalParam' + get: + operationId: sdfsdf +components: + parameters: + company: + name: companyId + in: path + required: true + schema: + type: string + employee: + name: employeeId + in: path + required: true + schema: + type: string + stringParam: + name: stringParam + in: path + required: true + schema: + type: string + intParam: + name: intParam + in: path + required: true + schema: + type: integer + format: int32 + numberParam: + name: numberParam + in: path + required: true + schema: + type: number + format: double + dateParam: + name: dateParam + in: path + required: true + schema: + type: string + format: date + optionalParam: + name: optionalParam + in: path + required: false + schema: + type: string diff --git a/packages/cache/src/ConfigProvider.php b/packages/cache/src/ConfigProvider.php index 78f4f1a..4f004c4 100755 --- a/packages/cache/src/ConfigProvider.php +++ b/packages/cache/src/ConfigProvider.php @@ -2,7 +2,7 @@ namespace AftDev\Cache; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use Psr\Cache\CacheItemPoolInterface; use Psr\SimpleCache\CacheInterface as SimpleCacheInterface; use Symfony\Contracts\Cache\CacheInterface; @@ -52,7 +52,7 @@ public function getCacheManagerConfig() ], ], 'abstract_factories' => [ - 'default' => ReflectionAbstractFactory::class, + 'default' => ResolverAbstractFactory::class, ], ]; } diff --git a/packages/cache/src/Factory/ChainAdapterFactory.php b/packages/cache/src/Factory/ChainAdapterFactory.php index 45bf6f5..7787b22 100755 --- a/packages/cache/src/Factory/ChainAdapterFactory.php +++ b/packages/cache/src/Factory/ChainAdapterFactory.php @@ -3,12 +3,12 @@ namespace AftDev\Cache\Factory; use AftDev\Cache\CacheManager; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerInterface; use Symfony\Component\Cache\Adapter\ChainAdapter; -class ChainAdapterFactory extends ReflectionAbstractFactory +class ChainAdapterFactory implements FactoryInterface { public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { diff --git a/packages/cache/src/Factory/MemcachedAdapterFactory.php b/packages/cache/src/Factory/MemcachedAdapterFactory.php index d73d5ba..20fe5d4 100755 --- a/packages/cache/src/Factory/MemcachedAdapterFactory.php +++ b/packages/cache/src/Factory/MemcachedAdapterFactory.php @@ -2,11 +2,11 @@ namespace AftDev\Cache\Factory; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use Psr\Container\ContainerInterface; use Symfony\Component\Cache\Adapter\MemcachedAdapter; -class MemcachedAdapterFactory extends ReflectionAbstractFactory +class MemcachedAdapterFactory extends ResolverAbstractFactory { public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { diff --git a/packages/cache/src/Factory/RedisAdapterFactory.php b/packages/cache/src/Factory/RedisAdapterFactory.php index 805247c..0aff101 100755 --- a/packages/cache/src/Factory/RedisAdapterFactory.php +++ b/packages/cache/src/Factory/RedisAdapterFactory.php @@ -2,11 +2,11 @@ namespace AftDev\Cache\Factory; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use Psr\Container\ContainerInterface; use Symfony\Component\Cache\Adapter\RedisAdapter; -class RedisAdapterFactory extends ReflectionAbstractFactory +class RedisAdapterFactory extends ResolverAbstractFactory { public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { diff --git a/packages/cache/src/Factory/TagAwareAdapterFactory.php b/packages/cache/src/Factory/TagAwareAdapterFactory.php index de9f1e2..334cd61 100755 --- a/packages/cache/src/Factory/TagAwareAdapterFactory.php +++ b/packages/cache/src/Factory/TagAwareAdapterFactory.php @@ -3,12 +3,12 @@ namespace AftDev\Cache\Factory; use AftDev\Cache\CacheManager; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Psr\Container\ContainerInterface; use Symfony\Component\Cache\Adapter\TagAwareAdapter; -class TagAwareAdapterFactory extends ReflectionAbstractFactory +class TagAwareAdapterFactory extends ResolverAbstractFactory { public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { diff --git a/packages/console/src/ConfigProvider.php b/packages/console/src/ConfigProvider.php index ab59195..b321ec2 100755 --- a/packages/console/src/ConfigProvider.php +++ b/packages/console/src/ConfigProvider.php @@ -2,7 +2,7 @@ namespace AftDev\Console; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; class ConfigProvider { @@ -52,7 +52,7 @@ public function getManagerConfig(): array { return [ 'abstract_factories' => [ - 'default' => ReflectionAbstractFactory::class, + 'default' => ResolverAbstractFactory::class, ], ]; } diff --git a/packages/filesystem/src/Factory/DiskAbstractFactory.php b/packages/filesystem/src/Factory/DiskAbstractFactory.php index 71a1eab..5541eeb 100755 --- a/packages/filesystem/src/Factory/DiskAbstractFactory.php +++ b/packages/filesystem/src/Factory/DiskAbstractFactory.php @@ -2,11 +2,11 @@ namespace AftDev\Filesystem\Factory; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use League\Flysystem\Filesystem; use Psr\Container\ContainerInterface; -class DiskAbstractFactory extends ReflectionAbstractFactory +class DiskAbstractFactory extends ResolverAbstractFactory { use GetConfigTrait; diff --git a/packages/filesystem/src/Factory/S3AdapterFactory.php b/packages/filesystem/src/Factory/S3AdapterFactory.php index 10084d4..70a2f20 100644 --- a/packages/filesystem/src/Factory/S3AdapterFactory.php +++ b/packages/filesystem/src/Factory/S3AdapterFactory.php @@ -2,13 +2,13 @@ namespace AftDev\Filesystem\Factory; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use Aws\S3\S3Client; use Illuminate\Support\Arr; use League\Flysystem\Filesystem; use Psr\Container\ContainerInterface; -class S3AdapterFactory extends ReflectionAbstractFactory +class S3AdapterFactory extends ResolverAbstractFactory { use GetConfigTrait; diff --git a/packages/log/src/Factory/ChannelAbstractFactory.php b/packages/log/src/Factory/ChannelAbstractFactory.php index b1b8fe3..f63efc4 100755 --- a/packages/log/src/Factory/ChannelAbstractFactory.php +++ b/packages/log/src/Factory/ChannelAbstractFactory.php @@ -2,13 +2,13 @@ namespace AftDev\Log\Factory; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Monolog\Logger; use Psr\Container\ContainerInterface; -class ChannelAbstractFactory extends ReflectionAbstractFactory +class ChannelAbstractFactory extends ResolverAbstractFactory { public function __invoke(ContainerInterface $container, $handlerName, array $options = null) { diff --git a/packages/service-manager/README.MD b/packages/service-manager/README.MD deleted file mode 100755 index 66d3b85..0000000 --- a/packages/service-manager/README.MD +++ /dev/null @@ -1,108 +0,0 @@ -# Service Manager - -This package contains extra goodies built on top of the [laminas service -manager](https://docs.laminas.dev/laminas-servicemanager/). - -## Configurable Service Manager - -Extends the Laminas Plugin Manager. -Allow definition of plugin configuration/settings. -To easily create configurable services/plugins. - -```php - [ - 'default' => 'adapter_1', - 'default_options' => [ - 'option_default' => 'Default', - ], - 'plugins' => [ - 'adapter_1' => [ - 'service' => AdapterClass::class, // What service to use, - 'options' => [ - 'option_1' => 'Option1', - 'option_2' => 'Option2', - ], - ], - 'adapter_2' => [ - 'service' => OtherAdapterClass::class, - 'options' => [ - 'option_1' => 'Option1', - 'option_2' => 'Option2', - ], - ], - // Short notation. (key is the name of the service to use) - 'service_name' => [ - 'option_1' => 'Option1', - ] - ], - // Laminas Plugin service manager configuration like factories or aliases. - 'factories' => [ - AdapterClass::class => Invokable::class, - OtherAdapterClass::class => Invokable::class, - ], - 'aliases' => [ - 'service_name' => AdapterClass::class, - ], - ], -]; -``` - -## Service Resolver - -Use the Resolver to automatically inject dependencies when calling function. -This is based on the laravel container [functionality](https://laravel.com/docs/8.x/container#method-invocation-and-injection). - -```php -call(function (Dependency $dependency) { - $dependency->doSomething(); -}); - -// With classes. -class ServiceA -{ - protected $dependencyA; - function __construct(DependencyA $dependencyA) - { - $this->dependencyA = $dependencyA; - } - - function handle(DependencyB $dependencyB) - { - $this->dependencyA->doSomething(); - $dependencyB->doSomething(); - } -} - -$serviceA = $resolver->resolveClass(ServiceA::class); -$resolver->call([$serviceA, 'handle']); - -// Create service and call handle function shortcut. -$resolver->call(ServiceA::class.'@handle'); - -``` - -## Reflection Abstract Factory - -Abstract factory that will auto inject dependencies of your services. -Add it to your laminas service manager configuration under - -```php - [], - 'aliases' => [], - 'abstract_factories' => [ - 'default' => ReflectionAbstractFactory::class, - ], -]; - -``` diff --git a/packages/service-manager/README.md b/packages/service-manager/README.md new file mode 100755 index 0000000..0aac96a --- /dev/null +++ b/packages/service-manager/README.md @@ -0,0 +1,233 @@ +# Service Manager + +This package contains extra goodies built on top of the +[laminas service manager](https://docs.laminas.dev/laminas-servicemanager/). + +## Configurable Service Manager + +Extends the Laminas Plugin Manager. Allows definition of plugin +configuration/settings. To easily create configurable services/plugins. + +```php + [ + 'default' => 'adapter_1', + 'default_options' => [ + 'option_default' => 'Default', + ], + 'plugins' => [ + 'adapter_1' => [ + 'service' => AdapterClass::class, // What service to use, + 'options' => [ + 'option_1' => 'Option1', + 'option_2' => 'Option2', + ], + ], + 'adapter_2' => [ + 'service' => OtherAdapterClass::class, + 'options' => [ + 'option_1' => 'Option1', + 'option_2' => 'Option2', + ], + ], + // Short notation. (key is the name of the service to use) + 'service_name' => [ + 'option_1' => 'Option1', + ] + ], + // Laminas Plugin service manager configuration like factories or aliases. + 'factories' => [ + AdapterClass::class => Invokable::class, + OtherAdapterClass::class => Invokable::class, + ], + 'aliases' => [ + 'service_name' => AdapterClass::class, + ], + ], +]; +``` + +```php +class YourManager extends AbstractManager { + +} + +$pluginManager = new YourManager($container, $config['manager_x']); +$pluginManager->get('adapter_2'); +``` + +## Resolver + +### Service Resolver + +This service automatically resolve services dependencies. + +```php +$container = new class implements Psr\Container\ContainerInterface; + +$resolver = new Resolver($container); + +// With closures. +$resolver->call(function (Dependency $dependency) { + $dependency->doSomething(); +}); + +// With classes. +class ServiceA +{ + protected $dependencyA; + + public function __construct(DependencyA $dependencyA) + { + $this->dependencyA = $dependencyA; + } +} + +// Instantiate class only +$serviceA = $resolver->resolveClass(ServiceA::class); +``` + +### Resolver Abstract Factory + +A Laminas abstract factory that will auto inject dependencies of your services +by using the resolver. + +Add it to your laminas service manager configuration in the 'abstract_factories' +section. That way any unregistered services will be automatically created with +all of their dependencies injected. + +```php + [], + 'aliases' => [], + 'abstract_factories' => [ + 'default' => ResolverAbstractFactory::class, + ], +]; +``` + +Note: This factory is automatically added if you are using this package service +ConfigProvider. + +### Contextual Binding / Binding Primitives + +Primitives variables cannot be auto-discovered by the service manager and thus +would require context binding. + +```php + +class ServiceA +{ + public function __construct( + private DependencyClass $dependency, + private array $primitiveArray, + private int $primitiveInt + ) +} + +$resolver->when(ServiceA::class)->needs(DependencyClass::class)->give( new DependencyClass()) +$resolver->when(ServiceA::class)->needs('primitiveArray')->give(['a','b','c']) +$resolver->when(ServiceA::class)->needs('primitiveInt')->give(1) + + +$serviceA = $resolver->resolveClass(ServiceA::class); + +$serviceA->dependency; // DependencyClass +$serviceA->primitiveArray; // ['a','b','c'] +$serviceA->primitiveInt; // 1 +``` + +### Method Invocation & Injection + +Use the Resolver to automatically inject dependencies when calling functions. +This is based on the laravel container +[functionality](https://laravel.com/docs/9.x/container#method-invocation-and-injection). + +```php +$resolver->call(function(Dependency $dependency) { + return $dependency->doSomething(); +}); +``` + +Automatically fetch a service from the container and invoke its function +(default is \_\_invoke) but you can customize the function to use: + +```php +class ServiceA +{ + protected $dependencyA; + + public function __construct(DependencyA $dependencyA) + { + $this->dependencyA = $dependencyA; + } + + public function __invoke(DependencyB $dependencyB) + { + $this->dependencyA->doSomething(); + $dependencyB->doSomething(); + } + + public function handle(DependencyC $dependencyC) + { + // ... + } +} + + +$resolver->call(ServiceA::class); // uses __invoke +$resolver->call([ServiceA::class, 'handle']); // uses handle +$resolver->call(ServiceA::class.'@handle'); // uses handle +``` + +### PSR-15 Resolve Middleware + +If your application uses PSR-15 Middleware - like +[Mezzio](https://docs.mezzio.dev/mezzio/) you could potentially use the provided +Resolve Middleware to automatically inject dependencies in your handlers +constructor but also handler actions. + +Example when using the Mezzio router. + +```php +// config/routes.php + +return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void { + + // By Manually creating the middleware + $resolver = $container->get(Resolver::class); + $pingMiddleware = new ResolveMiddleware($container, App\Handler\PingHandler::class.'@myCustomAction'); + + $app->get('/api/ping', $pingMiddleware, 'api.ping'); + + // Or by using the factory + + $resolveMiddlewareFactory = $container->get(ResolveMiddlewareFactory::class); + + $app->get('/api/ping', $resolveMiddlewareFactory->prepare(App\Handler\PingHandler::class.'@otherAction')); +} +``` + +Note: By using this middleware, your handlers will not be able to implements the +`Psr\Http\Server\RequestHandlerInterface` anymore. They should nonetheless +return a `Psr\Http\Message\ResponseInterface` + +```php +use Psr\Http\Message\ResponseInterface; + +class PingHandler +{ + public function myCustomAction(DependencyOne $dep1, DependencyTwo $dep2): ResponseInterface + { + } + + public function otherAction(DependencyThree $dep3): ResponseInterface + { + } +} +``` diff --git a/packages/service-manager/composer.json b/packages/service-manager/composer.json index b34be41..21e1b8f 100755 --- a/packages/service-manager/composer.json +++ b/packages/service-manager/composer.json @@ -12,6 +12,9 @@ "laminas/laminas-servicemanager": "^3.20.0", "psr/container": "^1.1 || ^2.0" }, + "require-dev": { + "laminas/laminas-diactoros": "^2.7" + }, "autoload": { "psr-4": { "AftDev\\ServiceManager\\": "src/" diff --git a/packages/service-manager/src/AbstractManager.php b/packages/service-manager/src/AbstractManager.php index cb7b253..2b6eac1 100755 --- a/packages/service-manager/src/AbstractManager.php +++ b/packages/service-manager/src/AbstractManager.php @@ -94,7 +94,7 @@ public function get($name, array $options = null) /** * Return true if the manager can create the service. * - * @param mixed $name + * {@inheritdoc} */ public function has($name) { diff --git a/packages/service-manager/src/ConfigProvider.php b/packages/service-manager/src/ConfigProvider.php index 2476322..006b3ce 100755 --- a/packages/service-manager/src/ConfigProvider.php +++ b/packages/service-manager/src/ConfigProvider.php @@ -2,6 +2,9 @@ namespace AftDev\ServiceManager; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; +use AftDev\ServiceManager\Middleware\ResolveMiddlewareFactory; + class ConfigProvider { public function __invoke() @@ -16,6 +19,10 @@ public function getDependencies() return [ 'factories' => [ Resolver::class => Resolver\ResolverFactory::class, + ResolveMiddlewareFactory::class => Middleware\ResolveMiddlewareFactoryFactory::class, + ], + 'abstract_factories' => [ + 'resolver' => ResolverAbstractFactory::class, ], ]; } diff --git a/packages/service-manager/src/Factory/ReflectionAbstractFactory.php b/packages/service-manager/src/Factory/ResolverAbstractFactory.php similarity index 92% rename from packages/service-manager/src/Factory/ReflectionAbstractFactory.php rename to packages/service-manager/src/Factory/ResolverAbstractFactory.php index e9e21af..23bb5d1 100755 --- a/packages/service-manager/src/Factory/ReflectionAbstractFactory.php +++ b/packages/service-manager/src/Factory/ResolverAbstractFactory.php @@ -7,7 +7,7 @@ use Laminas\ServiceManager\Factory\AbstractFactoryInterface; use Psr\Container\ContainerInterface; -class ReflectionAbstractFactory implements AbstractFactoryInterface +class ResolverAbstractFactory implements AbstractFactoryInterface { public function __invoke(ContainerInterface $container, $requestedName, array $options = null) { @@ -28,6 +28,9 @@ public function __invoke(ContainerInterface $container, $requestedName, array $o } } + /** + * @return bool + */ public function canCreate(ContainerInterface $container, $requestedName) { return class_exists($requestedName) && $this->canCallConstructor($requestedName); diff --git a/packages/service-manager/src/Middleware/ResolveMiddleware.php b/packages/service-manager/src/Middleware/ResolveMiddleware.php new file mode 100644 index 0000000..06536d1 --- /dev/null +++ b/packages/service-manager/src/Middleware/ResolveMiddleware.php @@ -0,0 +1,28 @@ +getAttributes(); + $attributes[ServerRequestInterface::class] = $request; + + return $this->resolver->call($this->callable, $attributes); + } +} diff --git a/packages/service-manager/src/Middleware/ResolveMiddlewareFactory.php b/packages/service-manager/src/Middleware/ResolveMiddlewareFactory.php new file mode 100755 index 0000000..af3d164 --- /dev/null +++ b/packages/service-manager/src/Middleware/ResolveMiddlewareFactory.php @@ -0,0 +1,20 @@ +resolver, $callable); + } +} diff --git a/packages/service-manager/src/Middleware/ResolveMiddlewareFactoryFactory.php b/packages/service-manager/src/Middleware/ResolveMiddlewareFactoryFactory.php new file mode 100644 index 0000000..ace1b5b --- /dev/null +++ b/packages/service-manager/src/Middleware/ResolveMiddlewareFactoryFactory.php @@ -0,0 +1,14 @@ +get(Resolver::class)); + } +} diff --git a/packages/service-manager/src/Resolver.php b/packages/service-manager/src/Resolver.php index 486f29c..b5270f7 100755 --- a/packages/service-manager/src/Resolver.php +++ b/packages/service-manager/src/Resolver.php @@ -4,27 +4,21 @@ use AftDev\ServiceManager\Resolver\RuleBuilder; use Psr\Container\ContainerInterface; +use ReflectionClass; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionException; class Resolver { - /** - * @var ContainerInterface - */ - protected $container; - /** * Array of rules on how to resolve parameters. - * - * @var array */ - protected $rules; + protected array $rules = []; - /** - * Resolver constructor. - */ - public function __construct(ContainerInterface $container) - { - $this->container = $container; + public function __construct( + protected ContainerInterface $container + ) { } /** @@ -32,28 +26,21 @@ public function __construct(ContainerInterface $container) * * @throws \ReflectionException If a parameter cannot be resolved. */ - public function resolveClass(string $requestedName, array $parameters = []): object + public function resolveClass(string $requestedName, array $params = []): object { - $reflectionClass = new \ReflectionClass($requestedName); - - if (null === ($constructor = $reflectionClass->getConstructor())) { - return new $requestedName(); - } - - $reflectionParameters = $constructor->getParameters(); + $reflectionClass = new ReflectionClass($requestedName); + $constructor = $reflectionClass->getConstructor(); + $constructorParams = $constructor ? $constructor->getParameters() : []; - if (empty($reflectionParameters)) { + if (empty($constructorParams)) { return new $requestedName(); } - $constructorParameters = array_map(function (\ReflectionParameter $parameter) use ($requestedName, $parameters) { - $parameterName = $parameter->getName(); - if (array_key_exists($parameterName, $parameters)) { - return $this->getParameterValue($parameters[$parameterName]); - } - - return $this->getServiceParameter($requestedName, $parameter); - }, $reflectionParameters); + $parameterValues = array_merge($params, $this->rules[$requestedName] ?? []); + $constructorParameters = array_map( + fn (ReflectionParameter $parameter) => $this->mapParameter($parameter, $parameterValues), + $constructorParams + ); return new $requestedName(...$constructorParameters); } @@ -62,11 +49,11 @@ public function resolveClass(string $requestedName, array $parameters = []): obj * Automatically call a function with all parameters injected from the container. * * @param array|callable|string $function - The function name or an array class,function name. - * @param array $parameters - List of Hard coded values. + * @param array $parameters - List of hard coded values. * * @throws \ReflectionException If a parameter cannot be resolved. */ - public function call($function, array $parameters = []) + public function call(array|callable|string $function, array $parameters = []): mixed { // Check if callable if (is_callable($function)) { @@ -81,20 +68,16 @@ public function call($function, array $parameters = []) $className = $exploded[0]; $functionName = $exploded[1] ?? '__invoke'; - $reflection = new \ReflectionMethod($className, $functionName); - $function = [$this->resolveClass($className, $parameters), $functionName]; + $reflection = new ReflectionMethod($className, $functionName); + $function = [$this->container->get($className), $functionName]; } $reflectionParameters = $reflection->getParameters(); - $parameters = array_map(function (\ReflectionParameter $parameter) use ($parameters) { - $parameterName = $parameter->getName(); - if (array_key_exists($parameterName, $parameters)) { - return $this->getParameterValue($parameters[$parameterName]); - } - - return $this->resolveParameter($parameter); - }, $reflectionParameters); + $parameters = array_map( + fn (ReflectionParameter $parameter) => $this->mapParameter($parameter, $parameters), + $reflectionParameters + ); return call_user_func($function, ...$parameters); } @@ -112,59 +95,65 @@ public function when(string $serviceName): RuleBuilder * * @param mixed $implementation */ - public function addServiceRule(string $serviceName, string $parameter, $implementation) + public function addServiceRule(string $serviceName, string $parameter, $implementation): void { $this->rules[$serviceName][$parameter] = $implementation; } /** - * Get value for a service parameter. - * - * @return mixed + * Map function parameter to the given list. * - * @throws \ReflectionException If parameter cannot be resolved. + * @throws ReflectionException If parameter cannot be resolved. */ - protected function getServiceParameter(string $serviceName, \ReflectionParameter $parameter) + protected function mapParameter(ReflectionParameter $parameter, array $parameters = []) { $parameterName = $parameter->getName(); - if (isset($this->rules[$serviceName]) && array_key_exists($parameterName, $this->rules[$serviceName])) { - return $this->getParameterValue($this->rules[$serviceName][$parameterName]); + + // Primitives. + $primitiveName = $parameterName; + if (array_key_exists($primitiveName, $parameters)) { + return $this->getParameterValue($parameters[$primitiveName]); } - return $this->resolveParameter($parameter); - } + // By TypeHint. + $type = $parameter->hasType() ? $parameter->getType() : null; - protected function getParameterValue($value) - { - return $value instanceof \Closure ? $value() : $value; - } + $types = $type + ? ($type instanceof (\ReflectionUnionType::class) ? $type->getTypes() : [$type]) + : []; - /** - * @throws \ReflectionException If parameter cannot be resolved. - */ - protected function resolveParameter(\ReflectionParameter $parameter) - { - $parameterName = $parameter->getName(); + foreach ($types as $type) { + if ($type->isBuiltin()) { + continue; + } - // Check that we have the value in the container. - $type = $parameter->getType() ?? null; - $notBuildIn = $type && !$type->isBuiltin(); - if ($type && $notBuildIn && $this->container->has($type->getName())) { - return $this->container->get($type->getName()); + // Rule on type? + $parameterTypeName = $type->getName(); + if (array_key_exists($parameterTypeName, $parameters)) { + return $this->getParameterValue($parameters[$parameterTypeName]); + } + + // Can the type be fetched from container? + if ($this->container->has($parameterTypeName)) { + return $this->container->get($parameterTypeName); + } } - try { - // Finally check for default value. - return $parameter->getDefaultValue(); - } catch (\ReflectionException $e) { - throw new \ReflectionException( - sprintf( + // Finally check for default value. + if (!$parameter->isOptional()) { + throw new ReflectionException( + message: sprintf( 'Unable to resolve parameter "%s"', $parameterName ), - 0, - $e ); } + + return $parameter->getDefaultValue(); + } + + protected function getParameterValue($value) + { + return $value instanceof \Closure ? $value() : $value; } } diff --git a/packages/service-manager/tests/Factory/ReflectionAbstractFactoryTest.php b/packages/service-manager/tests/Factory/ResolverAbstractFactoryTest.php similarity index 83% rename from packages/service-manager/tests/Factory/ReflectionAbstractFactoryTest.php rename to packages/service-manager/tests/Factory/ResolverAbstractFactoryTest.php index a9814db..67da89d 100755 --- a/packages/service-manager/tests/Factory/ReflectionAbstractFactoryTest.php +++ b/packages/service-manager/tests/Factory/ResolverAbstractFactoryTest.php @@ -2,7 +2,7 @@ namespace AftDevTest\ServiceManager\Factory; -use AftDev\ServiceManager\Factory\ReflectionAbstractFactory; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use AftDev\ServiceManager\Resolver; use AftDev\Test\TestCase; use Laminas\ServiceManager\Exception\ServiceNotFoundException; @@ -11,10 +11,10 @@ /** * @internal * - * @covers \AftDev\ServiceManager\Factory\ReflectionAbstractFactory + * @covers \AftDev\ServiceManager\Factory\ResolverAbstractFactory * @covers \AftDev\ServiceManager\Resolver */ -class ReflectionAbstractFactoryTest extends TestCase +class ResolverAbstractFactoryTest extends TestCase { public function testFactory() { @@ -34,7 +34,7 @@ public function testFactory() ->willReturn($resolver->reveal()) ; - $built = (new ReflectionAbstractFactory())($container->reveal(), PublicConstructor::class, []); + $built = (new ResolverAbstractFactory())($container->reveal(), PublicConstructor::class, []); $this->assertSame($returned, $built); } @@ -59,7 +59,7 @@ public function testInvalidOption() ; $this->expectException(ServiceNotFoundException::class); - (new ReflectionAbstractFactory())($container->reveal(), PublicConstructor::class); + (new ResolverAbstractFactory())($container->reveal(), PublicConstructor::class); } /** @@ -68,7 +68,7 @@ public function testInvalidOption() public function testCanCreate() { $container = $this->prophesize(ContainerInterface::class); - $factory = new ReflectionAbstractFactory(); + $factory = new ResolverAbstractFactory(); $canCreate = $factory->canCreate($container->reveal(), PublicConstructor::class); $this->assertTrue($canCreate); diff --git a/packages/service-manager/tests/Middleware/ResolveMiddlewareFactoryTest.php b/packages/service-manager/tests/Middleware/ResolveMiddlewareFactoryTest.php new file mode 100755 index 0000000..f9fa071 --- /dev/null +++ b/packages/service-manager/tests/Middleware/ResolveMiddlewareFactoryTest.php @@ -0,0 +1,27 @@ +prophesize(Resolver::class); + + $factory = new ResolveMiddlewareFactory($resolver->reveal()); + + $middleware = $factory->prepare('Test'); + + $this->assertInstanceOf(MiddlewareInterface::class, $middleware); + } +} diff --git a/packages/service-manager/tests/Middleware/ResolveMiddlewareTest.php b/packages/service-manager/tests/Middleware/ResolveMiddlewareTest.php new file mode 100755 index 0000000..8efec0b --- /dev/null +++ b/packages/service-manager/tests/Middleware/ResolveMiddlewareTest.php @@ -0,0 +1,46 @@ + 'testValue', + ]; + + foreach ($attributes as $key => $value) { + $request = $request->withAttribute($key, $value); + } + + $response = $this->prophesize(ResponseInterface::class); + $requestHandler = $this->prophesize(RequestHandlerInterface::class); + + $handlerName = 'XXXX'; + $resolver = $this->prophesize(Resolver::class); + $resolver + ->call($handlerName, $attributes + [ServerRequestInterface::class => $request]) + ->shouldBeCalledOnce() + ->willReturn($response->reveal()) + ; + + $resolveMiddleware = new ResolveMiddleware($resolver->reveal(), $handlerName); + $resolveMiddleware->process($request, $requestHandler->reveal()); + } +} diff --git a/packages/service-manager/tests/ResolverTest.php b/packages/service-manager/tests/ResolverTest.php index e40cc79..ff7987e 100755 --- a/packages/service-manager/tests/ResolverTest.php +++ b/packages/service-manager/tests/ResolverTest.php @@ -2,12 +2,10 @@ namespace AftDevTest\ServiceManager; +use AftDev\ServiceManager\Factory\ResolverAbstractFactory; use AftDev\ServiceManager\Resolver; use AftDev\Test\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; +use Laminas\ServiceManager\ServiceManager; /** * @internal @@ -16,63 +14,64 @@ */ class ResolverTest extends TestCase { - use ProphecyTrait; + protected ServiceManager $container; - /** - * @var ObjectProphecy - */ - protected $container; - - /** - * @var Resolver - */ - protected $resolver; + protected Resolver $resolver; public function setUp(): void { - $this->container = $this->prophesize(ContainerInterface::class); - - $this->resolver = new Resolver($this->container->reveal()); + $container = new \Laminas\ServiceManager\ServiceManager([ + 'services' => [ + ExistingServiceB::class => new ExistingServiceB(), + ExistingServiceC::class => new ExistingServiceC(), + ], + 'factories' => [ + Resolver::class => Resolver\ResolverFactory::class, + ], + 'abstract_factories' => [ + ResolverAbstractFactory::class, + ], + ]); + + $this->resolver = $container->get(Resolver::class); + $this->container = $container; } public function testResolveClass() { - $this->container->has(Argument::any()) - ->will(function ($params) { - if (ExistingServiceB::class === $params[0]) { - return true; - } - - return false; - }) - ; - - $serviceB = new ExistingServiceB(); - $this->container - ->get(ExistingServiceB::class) - ->willReturn($serviceB) - ; - - $options = [ - 'optionsA' => 'A', - 'optionsB' => ['a', 'b', 'c'], - 'optionsC' => 1, - 'optionsD' => 'mooh2', - 'optionCallable' => function () { - return 'fromCallable'; - }, - ]; + $this->resolver->when(TestService::class)->needs('optionsA')->give('options'); + $this->resolver->when(TestService::class)->needs('optionsB')->give(['a', 'b']); + $this->resolver->when(TestService::class)->needs('optionsC')->give(1); + $this->resolver->when(TestService::class)->needs('optionCallable')->give(function () { + return 'fromCallable'; + }); + + $testClass = $this->resolver->resolveClass(TestService::class); - $class = $this->resolver->resolveClass(TestService::class, $options); + $this->assertSame($this->container->get(ExistingServiceB::class), $testClass->serviceB); + $this->assertSame('options', $testClass->optionsA); + $this->assertSame(['a', 'b'], $testClass->optionsB); + $this->assertSame(1, $testClass->optionsC); + $this->assertSame('fromCallable', $testClass->optionCallable); + + // Test type-hint Override. + $overrideServiceB = new ExistingServiceB(); + $this->resolver->when(TestService::class)->needs(ExistingServiceB::class)->give($overrideServiceB); + + $withOverride = $this->resolver->resolveClass(TestService::class); + $this->assertSame($overrideServiceB, $withOverride->serviceB); + } + + public function testMultipleType() + { + $serviceB = $this->container->get(ExistingServiceB::class); + $this->resolver->when(TestServiceComplex::class)->needs('intOrArray')->give(1); - $this->assertInstanceOf(TestService::class, $class); + $testClass = $this->resolver->resolveClass(TestServiceComplex::class); - $this->assertSame($serviceB, $class->serviceB); - $this->assertSame($options['optionsA'], $class->optionsA); - $this->assertSame($options['optionsB'], $class->optionsB); - $this->assertSame($options['optionsC'], $class->optionsC); - $this->assertSame($options['optionsD'], $class->optionsD); - $this->assertSame('fromCallable', $class->optionCallable); + $this->assertSame($serviceB, $testClass->serviceB); + $this->assertSame(1, $testClass->intOrArray); + $this->assertSame($serviceB, $testClass->intOrExistingServiceB); } /** @@ -80,23 +79,29 @@ public function testResolveClass() */ public function testNoConstructorAndConstructorWithoutParams() { - $options = []; - - $service = $this->resolver->resolveClass(ExistingServiceB::class, $options); + $service = $this->resolver->resolveClass(ExistingServiceB::class); $this->assertInstanceOf(ExistingServiceB::class, $service); - $serviceNoParams = $this->resolver->resolveClass(ExistingServiceC::class, $options); + $serviceNoParams = $this->resolver->resolveClass(ExistingServiceC::class); $this->assertInstanceOf(ExistingServiceC::class, $serviceNoParams); } public function testCallFunction() { $serviceC = new ExistingServiceC(); - $this->container->has(ExistingServiceC::class)->willReturn(true); - $this->container - ->get(ExistingServiceC::class) - ->willReturn($serviceC) - ; + $container = new \Laminas\ServiceManager\ServiceManager([ + 'services' => [ + ExistingServiceC::class => $serviceC, + ], + 'factories' => [ + Resolver::class => Resolver\ResolverFactory::class, + ], + 'abstract_factories' => [ + ResolverAbstractFactory::class, + ], + ]); + + $this->resolver = $container->get(Resolver::class); $testClass = $this->resolver->resolveClass(ExistingServiceB::class); @@ -137,30 +142,6 @@ public function testCallFunction() ], $returnValue3); } - public function testWhen() - { - $serviceB = new ExistingServiceB(); - $serviceC = new ExistingServiceC(); - - $this->resolver->when(TestService::class)->needs('serviceB')->give($serviceB); - $this->resolver->when(TestService::class)->needs('optionsA')->give('options'); - $this->resolver->when(TestService::class)->needs('optionsB')->give(['a', 'b']); - $this->resolver->when(TestService::class)->needs('optionsC')->give(1); - $this->resolver->when(TestService::class)->needs('optionCallable')->give(function () { - return 'callable'; - }); - - $this->resolver->when(ExistingServiceB::class)->needs('serviceC')->give($serviceC); - - $testClass = $this->resolver->resolveClass(TestService::class); - - $this->assertSame($serviceB, $testClass->serviceB); - $this->assertSame('options', $testClass->optionsA); - $this->assertSame(['a', 'b'], $testClass->optionsB); - $this->assertSame(1, $testClass->optionsC); - $this->assertSame('callable', $testClass->optionCallable); - } - /** * Test that the resolver will throw an exception. */ @@ -169,31 +150,34 @@ public function testUnknownDependency() $this->expectException(\ReflectionException::class); $this->resolver->resolveClass(NotAutodiscoverable::class); } + + public function testNoType() + { + $this->expectException(\ReflectionException::class); + $this->resolver->resolveClass(NoType::class); + } } class TestService { - public $serviceB; - public $optionsA; - public $optionsB; - public $optionsC; - public $optionsD; - public $optionCallable; + public function __construct( + public ExistingServiceB $serviceB, + public string $optionsA, + public array $optionsB, + public int $optionsC, + public string $optionsD = 'mooh', + public $optionCallable = null + ) { + } +} +class TestServiceComplex +{ public function __construct( - ExistingServiceB $serviceB, - string $optionsA, - array $optionsB, - int $optionsC, - string $optionsD = 'mooh', - $optionCallable = null + public ExistingServiceB $serviceB, + public int|array $intOrArray, + public int|ExistingServiceB $intOrExistingServiceB, ) { - $this->serviceB = $serviceB; - $this->optionsA = $optionsA; - $this->optionsB = $optionsB; - $this->optionsC = $optionsC; - $this->optionsD = $optionsD; - $this->optionCallable = $optionCallable; } } @@ -229,3 +213,10 @@ public function __construct(array $test) { } } + +class NoType +{ + public function __construct($test) + { + } +} diff --git a/tests/Feature/Api/OpenApiManagerTest.php b/tests/Feature/Api/OpenApiManagerTest.php new file mode 100644 index 0000000..2e4a016 --- /dev/null +++ b/tests/Feature/Api/OpenApiManagerTest.php @@ -0,0 +1,149 @@ +container->get(OpenApiManager::class); + + $this->assertInstanceOf(OpenApiManager::class, $openApiManager); + } + + /** + * @covers \AftDev\Api\Factory\CurrentOpenApiVersionFactory + */ + public function testCurrentOpenApi() + { + /** @var OpenApi $openApi */ + $openApi = $this->container->get(OpenApi::class); + + $this->assertInstanceOf(OpenApi::class, $openApi); + $this->assertEquals('Swagger Petstore', $openApi->info->title); + $this->assertEquals('1.0.0', $openApi->info->version); + } + + /** + * @covers \AftDev\Api\Factory\CurrentOpenApiVersionFactory + * @covers \AftDev\Api\Factory\OpenApiManagerFactory + */ + public function testCurrentVersion() + { + $this->overrideConfig([ConfigProvider::CONFIG_KEY, 'versions'], ['zz' => [], 'test-version' => [ + fn (OpenApi $openApi) => $openApi->info->version = 'test-version', + ]]); + $this->overrideConfig(join('.', [ConfigProvider::CONFIG_KEY, 'version']), 'test-version'); + + $openApi = $this->container->get(OpenApi::class); + + $this->assertInstanceOf(OpenApi::class, $openApi); + $this->assertEquals('test-version', $openApi->info->version); + } + + public function testMutationDependenciesInjection() + { + $apiManagerClass = false; + $this->overrideConfig([ConfigProvider::CONFIG_KEY, 'versions'], ['2019-01-02' => [ + function (OpenApi $openApi, OpenApiManager $apiManager) use (&$apiManagerClass) { + $apiManagerClass = get_class($apiManager); + }, + ]]); + + $openApiManager = $this->container->get(OpenApiManager::class); + + $openApiManager->getVersion('2019-01-02'); + + $this->assertEquals(OpenApiManager::class, $apiManagerClass); + } + + /** + * @covers \AftDev\Api\Factory\OpenApiManagerFactory + */ + public function testCache() + { + $this->overrideConfig([ConfigProvider::CONFIG_KEY, 'cache'], 'php'); + $this->overrideConfig([ConfigProvider::CONFIG_KEY, 'versions'], ['2019-01-02' => []]); + + $cacheManager = $this->container->get(CacheManager::class); + + /** @var CacheItemPoolInterface|\Symfony\Component\Cache\Adapter\PhpFilesAdapter $phpStore */ + $phpStore = $cacheManager->get('php'); + $phpStore->clear(); + + $openApiManager = $this->container->get(OpenApiManager::class); + $openApiManager->getCurrentVersion(); + $openApiManager->getVersion('2019-01-02'); + + $this->assertTrue($phpStore->hasItem('api.specs._base')); + $this->assertTrue($phpStore->hasItem('api.specs.20190102')); + } + + /** + * Make sure the cache is being reused. + * + * @depends testCache + * Previous function will add items into the cache. This test make sure they + * are used + * + * @param mixed $phpStore + */ + public function testCacheFound($phpStore) + { + $this->overrideConfig([ConfigProvider::CONFIG_KEY, 'cache'], 'php'); + $this->overrideConfig([ConfigProvider::CONFIG_KEY, 'versions'], ['2019-01-02' => []]); + + $cacheManager = $this->container->get(CacheManager::class); + $phpStore = $cacheManager->get('php'); + $this->assertTrue($phpStore->hasItem('api.specs._base')); + + $openApiManager = $this->container->get(OpenApiManager::class); + $spec = $openApiManager->getCurrentVersion(); + + $phpStore->clear(); + $this->assertInstanceOf(OpenApi::class, $spec); + } + + /** + * @covers \AftDev\Api\Factory\OpenApiManagerFactory + */ + public function testServers() + { + $this->overrideConfig( + [ConfigProvider::CONFIG_KEY, 'servers'], + [ + 'serverA' => [ + 'url' => 'https://test.com', + 'description' => 'xyz', + ], + ], + ); + + $openApiManager = $this->container->get(OpenApiManager::class); + + $servers = $openApiManager->getCurrentVersion()->servers; + + $this->assertInstanceOf(Server::class, current($servers)); + $this->assertEquals('https://test.com', current($servers)->url); + $this->assertEquals('xyz', current($servers)->description); + } +} diff --git a/tests/Feature/Api/OpenApiRouteGeneratorTest.php b/tests/Feature/Api/OpenApiRouteGeneratorTest.php new file mode 100644 index 0000000..d8a3183 --- /dev/null +++ b/tests/Feature/Api/OpenApiRouteGeneratorTest.php @@ -0,0 +1,62 @@ +container->get(CacheManager::class); + $this->cache = $cacheManager->store('php'); + } + + protected function tearDown(): void + { + parent::tearDown(); + $this->cache->clear(); + } + + /** + * @covers \AftDev\Api\Factory\OpenApiRouteGeneratorFactory + */ + public function testFactory() + { + $generator = $this->container->get(OpenApiRouteGenerator::class); + + $this->assertInstanceOf(OpenApiRouteGenerator::class, $generator); + } + + public function testCache() + { + $generator = new OpenApiRouteGenerator(cache: $this->cache); + $spec = Reader::readFromYamlFile(realpath(__DIR__.'/../../../packages/api/tests/specs/routes.yaml')); + + $routes = $generator->getRoutes($spec); + + $this->assertIsArray($routes); + + $cache = $generator->getCache($spec); + $this->assertTrue($cache->isHit()); + + // When cache exists. + $routes = $generator->getRoutes($spec); + $this->assertIsArray($routes); + } +} diff --git a/tests/Feature/Api/ParamTranslatorInterfaceTest.php b/tests/Feature/Api/ParamTranslatorInterfaceTest.php new file mode 100644 index 0000000..72b5581 --- /dev/null +++ b/tests/Feature/Api/ParamTranslatorInterfaceTest.php @@ -0,0 +1,28 @@ +container->get(ParamTranslatorInterface::class); + + $this->assertInstanceOf(ParamTranslatorInterface::class, $translator); + $this->assertInstanceOf(FastRouterParamTranslator::class, $translator); + } +} diff --git a/tests/Feature/Api/RequestValidationMiddlewareTest.php b/tests/Feature/Api/RequestValidationMiddlewareTest.php new file mode 100644 index 0000000..1d26272 --- /dev/null +++ b/tests/Feature/Api/RequestValidationMiddlewareTest.php @@ -0,0 +1,23 @@ +container->get(ValidationMiddleware::class); + + $this->assertInstanceOf(ValidationMiddleware::class, $middleware); + } +} diff --git a/tests/Feature/Messenger/MessengerTest.php b/tests/Feature/Messenger/MessengerTest.php index 252b4a6..0c74c66 100755 --- a/tests/Feature/Messenger/MessengerTest.php +++ b/tests/Feature/Messenger/MessengerTest.php @@ -10,7 +10,6 @@ use AftDev\Messenger\Queue\QueueManager; use AftDev\Test\Feature\Messenger\Messages\TestQueuableCommand; use AftDev\Test\FeatureTestCase; -use Illuminate\Support\Arr; use Prophecy\Argument; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Stamp\DelayStamp; @@ -43,10 +42,8 @@ public function setUp(): void { parent::setUp(); - $config = $this->container->get('config'); - // Create another transport for tests. - Arr::set($config, 'messenger.queues.plugins.memory_two.service', 'memory'); + $this->overrideConfig('messenger.queues.plugins.memory_two.service', 'memory'); $this->messenger = $this->container->get(Messenger::class); } diff --git a/tests/Feature/Messenger/Queues/TransportsTest.php b/tests/Feature/Messenger/Queues/TransportsTest.php index 9b7db93..bc36c7c 100755 --- a/tests/Feature/Messenger/Queues/TransportsTest.php +++ b/tests/Feature/Messenger/Queues/TransportsTest.php @@ -5,7 +5,6 @@ use AftDev\Messenger\Queue\QueueManager; use AftDev\Test\Feature\Messenger\Messages\TestCommand; use AftDev\Test\FeatureTestCase; -use Illuminate\Support\Arr; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Transport\TransportInterface; @@ -51,9 +50,7 @@ public static function transportProviders() public function testInvalidTransport(): void { - $config = $this->container->get('config'); - - Arr::set($config, 'messenger.queues.plugins.invalid.dsn', 'invalid://sdfdf'); + $this->overrideConfig('messenger.queues.plugins.invalid.dsn', 'invalid://sdfdf'); $queueManager = $this->container->get(QueueManager::class); diff --git a/tests/Feature/ServiceManager/ResolveMiddlewareTest.php b/tests/Feature/ServiceManager/ResolveMiddlewareTest.php new file mode 100755 index 0000000..a6f59ce --- /dev/null +++ b/tests/Feature/ServiceManager/ResolveMiddlewareTest.php @@ -0,0 +1,27 @@ +container->get(ResolveMiddlewareFactory::class); + + $this->assertInstanceOf(ResolveMiddlewareFactory::class, $factory); + } +} diff --git a/tests/FeatureTestCase.php b/tests/FeatureTestCase.php index 0274352..f7c1207 100755 --- a/tests/FeatureTestCase.php +++ b/tests/FeatureTestCase.php @@ -4,6 +4,7 @@ use AftDev\Db\Migration\PhinxApplication; use AftDev\DbEloquent\Capsule\CapsuleManager; +use Illuminate\Support\Arr; use Laminas\ServiceManager\ServiceManager; use Prophecy\Prophecy\ObjectProphecy; @@ -98,4 +99,15 @@ protected function mockService(string $name, object $mock) return $mock; } + + protected function overrideConfig(string|array $configName, $configValue) + { + if (is_array($configName)) { + $configName = join('.', $configName); + } + + $config = $this->container->get('config'); + + Arr::set($config, $configName, $configValue); + } } diff --git a/tests/data/openapi/petstore.yaml b/tests/data/openapi/petstore.yaml new file mode 100644 index 0000000..760877b --- /dev/null +++ b/tests/data/openapi/petstore.yaml @@ -0,0 +1,111 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - petsVersion1 + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string