diff --git a/.buildpath b/.buildpath new file mode 100644 index 0000000..a4ae71b --- /dev/null +++ b/.buildpath @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..6945617 --- /dev/null +++ b/.project @@ -0,0 +1,33 @@ + + + di + + + + + + org.eclipse.php.composer.core.builder.buildPathManagementBuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.eclipse.dltk.core.scriptbuilder + + + + + + org.eclipse.php.core.PHPNature + org.eclipse.wst.common.project.facet.core.nature + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f101a3d --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name" : "pluf/di", + "description" : "Pluf DI library", + "type" : "library", + "homepage" : "https://github.com/pluf/di", + "license" : "GPL-3.0-with-GCC-exception", + "authors" : [{ + "name" : "Mostafa Barmshory", + "email" : "mostafa.barmshory@gmail.com", + "role" : "author", + "homepage" : "https://viraweb123.ir" + } + ], + "support" : { + "email" : "info@pluf.ir", + "issues" : "https://github.com/pluf/di/issues", + "source" : "https://github.com/pluf/di", + "wiki" : "https://github.com/pluf/di/wiki" + }, + "keywords" : [ + "pluf", + "di" + ], + "autoload" : { + "psr-4" : { + "Pluf\\Di\\" : "src" + } + }, + "autoload-dev" : { + "psr-4" : { + "Pluf\\Tests\\" : "tests" + } + }, + "require" : { + "psr/container" : "1.*" + }, + "require-dev" : { + "phpunit/phpunit" : "~7.5" + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..1389fec --- /dev/null +++ b/composer.lock @@ -0,0 +1,1578 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ae21efcfb2b595fd14a62cadd1ab432d", + "packages": [ + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-05-29T17:27:14+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-06-29T13:22:24+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-09-03T19:13:55+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-09-17T18:55:26+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0 || ^9.0 <9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-09-29T09:10:42+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-10-31T16:06:48+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "050bedf145a257b1ff02746c31894800e5122946" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2018-09-13T20:33:42+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2019-06-07T04:22:29+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "abandoned": true, + "time": "2019-09-17T06:23:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2020-01-08T08:45:45+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "shasum": "" + }, + "require": { + "php": "^7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-07-12T15:12:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "time": "2019-02-04T06:01:07+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2019-11-20T08:46:58+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2019-09-14T09:02:43+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2018-10-04T04:07:39+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "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": "2020-07-14T12:35:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d2bb63e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,41 @@ + + + + tests/ + + + + + + + ./src + + + + + + + + + + + + + + + + + + + + diff --git a/src/CallableResolver.php b/src/CallableResolver.php new file mode 100644 index 0000000..ea13d0d --- /dev/null +++ b/src/CallableResolver.php @@ -0,0 +1,134 @@ + + */ +class CallableResolver +{ + + /** + * PSR11 Container to resolbe dependencies + * + * @var ContainerInterface + */ + private ContainerInterface $container; + + /** + * Creates new instance of resolber + * + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * Resolve the given callable into a real PHP callable. + * + * @param callable|string|array $callable + * + * @return callable Real PHP callable. + * + * @throws NotCallableException|\ReflectionException + */ + public function resolve($callable): callable + { + if (is_string($callable) && strpos($callable, '::') !== false) { + $callable = explode('::', $callable, 2); + } + + $callable = $this->resolveFromContainer($callable); + + if (! is_callable($callable)) { + throw NotCallableException::fromInvalidCallable($callable, true); + } + + return $callable; + } + + /** + * + * @param callable|string|array $callable + * @return callable|mixed + * @throws NotCallableException|\ReflectionException + */ + private function resolveFromContainer($callable) + { + // Shortcut for a very common use case + if ($callable instanceof Closure) { + return $callable; + } + + // If it's already a callable there is nothing to do + if (is_callable($callable)) { + // TODO with PHP 8 that should not be necessary to check this anymore + if (! $this->isStaticCallToNonStaticMethod($callable)) { + return $callable; + } + } + + // The callable is a container entry name + if (is_string($callable)) { + try { + return $this->container->get($callable); + } catch (NotFoundExceptionInterface $e) { + if ($this->container->has($callable)) { + throw $e; + } + throw NotCallableException::fromInvalidCallable($callable, true); + } + } + + // The callable is an array whose first item is a container entry name + // e.g. ['some-container-entry', 'methodToCall'] + if (is_array($callable) && is_string($callable[0])) { + try { + // Replace the container entry name by the actual object + $callable[0] = $this->container->get($callable[0]); + return $callable; + } catch (NotFoundExceptionInterface $e) { + if ($this->container->has($callable[0])) { + throw $e; + } + throw new NotCallableException(sprintf('Cannot call %s() on %s because it is not a class nor a valid container entry', $callable[1], $callable[0])); + } + } + + // Unrecognized stuff, we let it fail later + return $callable; + } + + /** + * Check if the callable represents a static call to a non-static method. + * + * @param mixed $callable + * + * @throws \ReflectionException + */ + private function isStaticCallToNonStaticMethod($callable): bool + { + if (is_array($callable) && is_string($callable[0])) { + [ + $class, + $method + ] = $callable; + $reflection = new ReflectionMethod($class, $method); + + return ! $reflection->isStatic(); + } + + return false; + } +} diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..25f201a --- /dev/null +++ b/src/Container.php @@ -0,0 +1,247 @@ + + */ +class Container implements ArrayAccess, ContainerInterface +{ + + private array $factories = []; + + private array $frozen = []; + + /** + * Stores list of all keys in this container + * + * @var array + */ + private array $keys = []; + + /** + * Parent container is used to resolve services hericically. + * + * @var Container + */ + private ?Container $parent = null; + + /** + * The invoker is used to call the factories + * + * @var Invoker + */ + private ?Invoker $internalInvoker = null; + + /** + * Instantiates the container. + * + * Objects and parameters can be passed as argument to the constructor. + * + * @param array $values + * The parameters or objects + */ + public function __construct(?Container $parent = null) + { + $this->parent = $parent; + // Create an invoker + $this->internalInvoker = new Invoker(new ResolverChain([ + new ParameterNameContainerResolver($this), + new DefaultValueResolver() + ])); + + // register the container as service + $this->offsetSet('container', Container::value($this)); + } + + /** + * Sets a factory to provide an object + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id + * The unique identifier for the parameter or object + * @param mixed $factory + * The factory to define an object + * + * @throws FrozenServiceException Prevent override of a frozen service + */ + public function offsetSet($id, $factory) + { + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + // TODO: maso, 2020: check the $value + if (! is_callable($factory)) { + throw new ExpectedInvokableException('Factory is not a Closure or invokable object.'); + } + + $this->factories[$id] = $factory; + $this->keys[$id] = true; + } + + /** + * Gets a parameter or an object. + * + * @param string $id + * The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + public function offsetGet($id) + { + if (! isset($this->keys[$id])) { + // TODO: maso, 2020: fetch from parent + throw new UnknownIdentifierException($id); + } + + $factory = $this->factories[$id]; + $val = $this->internalInvoker->call($factory); + // value is used and you are not allowd to overrid + $this->frozen[$id] = true; + return $val; + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id + * The unique identifier for the parameter or object + * + * @return bool + */ + public function offsetExists($id) + { + return isset($this->keys[$id]); + } + + /** + * Unsets a parameter or an object. + * + * @param string $id + * The unique identifier for the parameter or object + */ + public function offsetUnset($id) + { + if (isset($this->keys[$id])) { + unset($this->factories[$id], $this->keys[$id], $this->frozen); + } + } + + /** + * + * {@inheritdoc} + * @see \Psr\Container\ContainerInterface::get() + */ + public function get($id) + { + return $this->offsetGet($id); + } + + /** + * + * {@inheritdoc} + * @see \Psr\Container\ContainerInterface::has() + */ + public function has($id) + { + return $this->offsetExists($id); + } + + /** + * Returns all defined value names. + * + * @return array An array of value names + */ + public function keys() + { + return array_keys($this->factories); + } + + public function raw($id): Closure + { + if (! $this->has($id)) { + throw new UnknownIdentifierException($id); + } + return $this->factories[$id]; + } + + /** + * Marks a callable as being a factory service. + * + * @param callable $callable + * A service definition to be used as a factory + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public static function factory(Closure $callable): Closure + { + return $callable; + } + + /** + * Returns a closure that stores the result of the given service definition + * for uniqueness in the scope of this instance of Pimple. + * + * @param callable $callable + * A service definition to wrap for uniqueness + * + * @return Closure The wrapped closure + */ + public static function service($callable): Closure + { + if (! is_callable($callable)) { + throw new InvalidArgumentException('A factory must use to create a service.'); + } + return function ($container) use ($callable) { + static $service; + + if (null === $service) { + // TODO: invoke the callable + $invoker = new Invoker(new ParameterNameContainerResolver($container)); + $service = $invoker->call($callable); + } + + return $service; + }; + } + + /** + * Protects a callable from being interpreted as a service. + * + * This is useful when you want to store a callable or value as a service. So the + * value is not a factory. + * + * @param mixed $callable + * A callable to protect from being evaluated + * + * @return Closure The protected closure + */ + public static function value($value) + { + return function () use ($value) { + return $value; + }; + } +} diff --git a/src/Exception/ExpectedInvokableException.php b/src/Exception/ExpectedInvokableException.php new file mode 100644 index 0000000..a0ce028 --- /dev/null +++ b/src/Exception/ExpectedInvokableException.php @@ -0,0 +1,13 @@ + + */ +class ExpectedInvokableException extends \InvalidArgumentException implements ContainerExceptionInterface +{ +} diff --git a/src/Exception/FrozenServiceException.php b/src/Exception/FrozenServiceException.php new file mode 100644 index 0000000..a45de49 --- /dev/null +++ b/src/Exception/FrozenServiceException.php @@ -0,0 +1,24 @@ + + */ +class FrozenServiceException extends RuntimeException implements ContainerExceptionInterface +{ + + /** + * + * @param string $id + * Identifier of the frozen service + */ + public function __construct($id) + { + parent::__construct(sprintf('Cannot override frozen service "%s".', $id)); + } +} diff --git a/src/Exception/InvalidServiceIdentifierException.php b/src/Exception/InvalidServiceIdentifierException.php new file mode 100644 index 0000000..ddb589c --- /dev/null +++ b/src/Exception/InvalidServiceIdentifierException.php @@ -0,0 +1,24 @@ + + */ +class InvalidServiceIdentifierException extends InvalidArgumentException implements NotFoundExceptionInterface +{ + + /** + * + * @param string $id + * The invalid identifier + */ + public function __construct($id) + { + parent::__construct(sprintf('Identifier "%s" does not contain an object definition.', $id)); + } +} diff --git a/src/Exception/InvocationException.php b/src/Exception/InvocationException.php new file mode 100644 index 0000000..0b16f6d --- /dev/null +++ b/src/Exception/InvocationException.php @@ -0,0 +1,11 @@ + + */ +class InvocationException extends \Exception +{ +} diff --git a/src/Exception/NotCallableException.php b/src/Exception/NotCallableException.php new file mode 100644 index 0000000..1651164 --- /dev/null +++ b/src/Exception/NotCallableException.php @@ -0,0 +1,30 @@ + + */ +class NotCallableException extends InvocationException +{ + /** + * @param mixed $value + */ + public static function fromInvalidCallable($value, bool $containerEntry = false): self + { + if (is_object($value)) { + $message = sprintf('Instance of %s is not a callable', get_class($value)); + } elseif (is_array($value) && isset($value[0], $value[1])) { + $class = is_object($value[0]) ? get_class($value[0]) : $value[0]; + $extra = method_exists($class, '__call') ? ' A __call() method exists but magic methods are not supported.' : ''; + $message = sprintf('%s::%s() is not a callable.%s', $class, $value[1], $extra); + } elseif ($containerEntry) { + $message = var_export($value, true) . ' is neither a callable nor a valid container entry'; + } else { + $message = var_export($value, true) . ' is not a callable'; + } + + return new self($message); + } +} diff --git a/src/Exception/NotEnoughParametersException.php b/src/Exception/NotEnoughParametersException.php new file mode 100644 index 0000000..8244da5 --- /dev/null +++ b/src/Exception/NotEnoughParametersException.php @@ -0,0 +1,11 @@ + + */ +class NotEnoughParametersException extends InvocationException +{ +} diff --git a/src/Exception/UnknownIdentifierException.php b/src/Exception/UnknownIdentifierException.php new file mode 100644 index 0000000..524bc83 --- /dev/null +++ b/src/Exception/UnknownIdentifierException.php @@ -0,0 +1,23 @@ + + */ +class UnknownIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + + /** + * + * @param string $id + * The unknown identifier + */ + public function __construct($id) + { + parent::__construct(\sprintf('Identifier "%s" is not defined.', $id)); + } +} diff --git a/src/Invoker.php b/src/Invoker.php new file mode 100644 index 0000000..4dae263 --- /dev/null +++ b/src/Invoker.php @@ -0,0 +1,116 @@ + + */ +class Invoker +{ + + /** + * + * @var CallableResolver|null + */ + private ?CallableResolver $callableResolver = null; + + /** + * + * @var ParameterResolver + */ + private ParameterResolver $parameterResolver; + + /** + * + * @var ContainerInterface|null + */ + private $container; + + public function __construct(?ParameterResolver $parameterResolver = null, ?ContainerInterface $container = null) + { + $this->parameterResolver = $parameterResolver ?: $this->createParameterResolver(); + + $this->container = $container; + if ($container) { + $this->callableResolver = new CallableResolver($container); + } + } + + /** + * + * {@inheritdoc} + */ + public function call($callable, array $parameters = []) + { + if ($this->callableResolver) { + $callable = $this->callableResolver->resolve($callable); + } + + if (! is_callable($callable)) { + throw new NotCallableException(sprintf('%s is not a callable', is_object($callable) ? 'Instance of ' . get_class($callable) : var_export($callable, true))); + } + + $callableReflection = CallableReflection::create($callable); + + $args = $this->parameterResolver->getParameters($callableReflection, $parameters, array()); + + // Sort by array key because call_user_func_array ignores numeric keys + ksort($args); + + // Check all parameters are resolved + $diff = array_diff_key($callableReflection->getParameters(), $args); + if (! empty($diff)) { + /** @var \ReflectionParameter $parameter */ + $parameter = reset($diff); + throw new NotEnoughParametersException(sprintf('Unable to invoke the callable because no value was given for parameter %d ($%s)', $parameter->getPosition() + 1, $parameter->name)); + } + + return call_user_func_array($callable, $args); + } + + /** + * Create the default parameter resolver. + */ + private function createParameterResolver(): ParameterResolver + { + return new ResolverChain(array( + new NumericArrayResolver(), + new AssociativeArrayResolver(), + new DefaultValueResolver() + )); + } + + /** + * + * @return ParameterResolver By default it's a ResolverChain + */ + public function getParameterResolver(): ParameterResolver + { + return $this->parameterResolver; + } + + public function getContainer(): ?ContainerInterface + { + return $this->container; + } + + /** + * + * @return CallableResolver|null Returns null if no container was given in the constructor. + */ + public function getCallableResolver(): ?CallableResolver + { + return $this->callableResolver; + } +} \ No newline at end of file diff --git a/src/ParameterResolver/AssociativeArrayResolver.php b/src/ParameterResolver/AssociativeArrayResolver.php new file mode 100644 index 0000000..842c31e --- /dev/null +++ b/src/ParameterResolver/AssociativeArrayResolver.php @@ -0,0 +1,38 @@ +call($callable, ['foo' => 'bar'])` will inject the string `'bar'` + * in the parameter named `$foo`. + * + * Parameters that are not indexed by a string are ignored. + * + * @author Mostafa Barmshory + */ +class AssociativeArrayResolver implements ParameterResolver +{ + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ) { + $parameters = $reflection->getParameters(); + + // Skip parameters already resolved + if (! empty($resolvedParameters)) { + $parameters = array_diff_key($parameters, $resolvedParameters); + } + + foreach ($parameters as $index => $parameter) { + if (array_key_exists($parameter->name, $providedParameters)) { + $resolvedParameters[$index] = $providedParameters[$parameter->name]; + } + } + + return $resolvedParameters; + } +} diff --git a/src/ParameterResolver/Container/ParameterNameContainerResolver.php b/src/ParameterResolver/Container/ParameterNameContainerResolver.php new file mode 100644 index 0000000..a6d5087 --- /dev/null +++ b/src/ParameterResolver/Container/ParameterNameContainerResolver.php @@ -0,0 +1,50 @@ + + */ +class ParameterNameContainerResolver implements ParameterResolver +{ + /** + * @var ContainerInterface + */ + private $container; + + /** + * @param ContainerInterface $container The container to get entries from. + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ) { + $parameters = $reflection->getParameters(); + + // Skip parameters already resolved + if (! empty($resolvedParameters)) { + $parameters = array_diff_key($parameters, $resolvedParameters); + } + + foreach ($parameters as $index => $parameter) { + $name = $parameter->name; + + if ($name && $this->container->has($name)) { + $resolvedParameters[$index] = $this->container->get($name); + } + } + + return $resolvedParameters; + } +} diff --git a/src/ParameterResolver/Container/TypeHintContainerResolver.php b/src/ParameterResolver/Container/TypeHintContainerResolver.php new file mode 100644 index 0000000..3061ab7 --- /dev/null +++ b/src/ParameterResolver/Container/TypeHintContainerResolver.php @@ -0,0 +1,65 @@ + + */ +class TypeHintContainerResolver implements ParameterResolver +{ + /** + * @var ContainerInterface + */ + private $container; + + /** + * @param ContainerInterface $container The container to get entries from. + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ) { + $parameters = $reflection->getParameters(); + + // Skip parameters already resolved + if (! empty($resolvedParameters)) { + $parameters = array_diff_key($parameters, $resolvedParameters); + } + + foreach ($parameters as $index => $parameter) { + $parameterType = $parameter->getType(); + if (!$parameterType) { + // No type + continue; + } + if ($parameterType->isBuiltin()) { + // Primitive types are not supported + continue; + } + if (!$parameterType instanceof ReflectionNamedType) { + // Union types are not supported + continue; + } + + $parameterClass = $parameterType->getName(); + + if ($this->container->has($parameterClass)) { + $resolvedParameters[$index] = $this->container->get($parameterClass); + } + } + + return $resolvedParameters; + } +} diff --git a/src/ParameterResolver/DefaultValueResolver.php b/src/ParameterResolver/DefaultValueResolver.php new file mode 100644 index 0000000..51ada64 --- /dev/null +++ b/src/ParameterResolver/DefaultValueResolver.php @@ -0,0 +1,39 @@ + + */ +class DefaultValueResolver implements ParameterResolver +{ + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ) { + $parameters = $reflection->getParameters(); + + // Skip parameters already resolved + if (! empty($resolvedParameters)) { + $parameters = array_diff_key($parameters, $resolvedParameters); + } + + foreach ($parameters as $index => $parameter) { + /** @var \ReflectionParameter $parameter */ + if ($parameter->isOptional()) { + try { + $resolvedParameters[$index] = $parameter->getDefaultValue(); + } catch (ReflectionException $e) { + // Can't get default values from PHP internal classes and functions + } + } + } + + return $resolvedParameters; + } +} diff --git a/src/ParameterResolver/NumericArrayResolver.php b/src/ParameterResolver/NumericArrayResolver.php new file mode 100644 index 0000000..2effa63 --- /dev/null +++ b/src/ParameterResolver/NumericArrayResolver.php @@ -0,0 +1,38 @@ +call($callable, ['foo', 'bar'])` will simply resolve the parameters + * to `['foo', 'bar']`. + * + * Parameters that are not indexed by a number (i.e. parameter position) + * will be ignored. + * + * @author Mostafa Barmshory + */ +class NumericArrayResolver implements ParameterResolver +{ + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ) { + // Skip parameters already resolved + if (! empty($resolvedParameters)) { + $providedParameters = array_diff_key($providedParameters, $resolvedParameters); + } + + foreach ($providedParameters as $key => $value) { + if (is_int($key)) { + $resolvedParameters[$key] = $value; + } + } + + return $resolvedParameters; + } +} diff --git a/src/ParameterResolver/ParameterResolver.php b/src/ParameterResolver/ParameterResolver.php new file mode 100644 index 0000000..cf0b089 --- /dev/null +++ b/src/ParameterResolver/ParameterResolver.php @@ -0,0 +1,32 @@ + + */ +interface ParameterResolver +{ + /** + * Resolves the parameters to use to call the callable. + * + * `$resolvedParameters` contains parameters that have already been resolved. + * + * Each ParameterResolver must resolve parameters that are not already + * in `$resolvedParameters`. That allows to chain multiple ParameterResolver. + * + * @param ReflectionFunctionAbstract $reflection Reflection object for the callable. + * @param array $providedParameters Parameters provided by the caller. + * @param array $resolvedParameters Parameters resolved (indexed by parameter position). + * + * @return array + */ + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ); +} diff --git a/src/ParameterResolver/ResolverChain.php b/src/ParameterResolver/ResolverChain.php new file mode 100644 index 0000000..57f3e65 --- /dev/null +++ b/src/ParameterResolver/ResolverChain.php @@ -0,0 +1,65 @@ + + */ +class ResolverChain implements ParameterResolver +{ + /** + * @var ParameterResolver[] + */ + private $resolvers; + + public function __construct(array $resolvers = array()) + { + $this->resolvers = $resolvers; + } + + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ) { + $reflectionParameters = $reflection->getParameters(); + + foreach ($this->resolvers as $resolver) { + $resolvedParameters = $resolver->getParameters( + $reflection, + $providedParameters, + $resolvedParameters + ); + + $diff = array_diff_key($reflectionParameters, $resolvedParameters); + if (empty($diff)) { + // Stop traversing: all parameters are resolved + return $resolvedParameters; + } + } + + return $resolvedParameters; + } + + /** + * Push a parameter resolver after the ones already registered. + */ + public function appendResolver(ParameterResolver $resolver): void + { + $this->resolvers[] = $resolver; + } + + /** + * Insert a parameter resolver before the ones already registered. + */ + public function prependResolver(ParameterResolver $resolver): void + { + array_unshift($this->resolvers, $resolver); + } +} diff --git a/src/ParameterResolver/TypeHintResolver.php b/src/ParameterResolver/TypeHintResolver.php new file mode 100644 index 0000000..182f486 --- /dev/null +++ b/src/ParameterResolver/TypeHintResolver.php @@ -0,0 +1,52 @@ + + */ +class TypeHintResolver implements ParameterResolver +{ + public function getParameters( + ReflectionFunctionAbstract $reflection, + array $providedParameters, + array $resolvedParameters + ) { + $parameters = $reflection->getParameters(); + + // Skip parameters already resolved + if (! empty($resolvedParameters)) { + $parameters = array_diff_key($parameters, $resolvedParameters); + } + + foreach ($parameters as $index => $parameter) { + $parameterType = $parameter->getType(); + if (!$parameterType) { + // No type + continue; + } + if ($parameterType->isBuiltin()) { + // Primitive types are not supported + continue; + } + if (!$parameterType instanceof ReflectionNamedType) { + // Union types are not supported + continue; + } + + $parameterClass = $parameterType->getName(); + + if (array_key_exists($parameterClass, $providedParameters)) { + $resolvedParameters[$index] = $providedParameters[$parameterClass]; + } + } + + return $resolvedParameters; + } +} diff --git a/src/Reflection/CallableReflection.php b/src/Reflection/CallableReflection.php new file mode 100644 index 0000000..fb97998 --- /dev/null +++ b/src/Reflection/CallableReflection.php @@ -0,0 +1,62 @@ + + */ +class CallableReflection +{ + /** + * @param callable $callable + * + * @throws NotCallableException|\ReflectionException + * + * TODO Use the `callable` type-hint once support for PHP 5.4 and up. + */ + public static function create($callable): ReflectionFunctionAbstract + { + // Closure + if ($callable instanceof \Closure) { + return new ReflectionFunction($callable); + } + + // Array callable + if (is_array($callable)) { + [$class, $method] = $callable; + + if (! method_exists($class, $method)) { + throw NotCallableException::fromInvalidCallable($callable); + } + + return new ReflectionMethod($class, $method); + } + + // Callable object (i.e. implementing __invoke()) + if (is_object($callable) && method_exists($callable, '__invoke')) { + return new ReflectionMethod($callable, '__invoke'); + } + + // Callable class (i.e. implementing __invoke()) + if (is_string($callable) && class_exists($callable) && method_exists($callable, '__invoke')) { + return new ReflectionMethod($callable, '__invoke'); + } + + // Standard function + if (is_string($callable) && function_exists($callable)) { + return new ReflectionFunction($callable); + } + + throw new NotCallableException(sprintf( + '%s is not a callable', + is_string($callable) ? $callable : 'Instance of ' . get_class($callable) + )); + } +} diff --git a/src/ServiceIterator.php b/src/ServiceIterator.php new file mode 100644 index 0000000..3ced8f6 --- /dev/null +++ b/src/ServiceIterator.php @@ -0,0 +1,48 @@ + + */ +final class ServiceIterator implements Iterator +{ + + private $container; + + private $ids; + + public function __construct(Container $container, array $ids) + { + $this->container = $container; + $this->ids = $ids; + } + + public function rewind() + { + reset($this->ids); + } + + public function current() + { + return $this->container[current($this->ids)]; + } + + public function key() + { + return current($this->ids); + } + + public function next() + { + next($this->ids); + } + + public function valid() + { + return null !== key($this->ids); + } +} diff --git a/src/ServiceLocator.php b/src/ServiceLocator.php new file mode 100644 index 0000000..10f8640 --- /dev/null +++ b/src/ServiceLocator.php @@ -0,0 +1,56 @@ + + */ +class ServiceLocator implements ContainerInterface +{ + + private $container; + + private $aliases = []; + + /** + * + * @param Container $container + * The Container instance used to locate services + * @param array $ids + * Array of service ids that can be located. String keys can be used to define aliases + */ + public function __construct(Container $container, array $ids) + { + $this->container = $container; + + foreach ($ids as $key => $id) { + $this->aliases[\is_int($key) ? $id : $key] = $id; + } + } + + /** + * + * {@inheritdoc} + */ + public function get($id) + { + if (! isset($this->aliases[$id])) { + throw new UnknownIdentifierException($id); + } + + return $this->container[$this->aliases[$id]]; + } + + /** + * + * {@inheritdoc} + */ + public function has($id) + { + return isset($this->aliases[$id]) && isset($this->container[$this->aliases[$id]]); + } +} diff --git a/tests/CallabelResolverTest.php b/tests/CallabelResolverTest.php new file mode 100644 index 0000000..18c7f3a --- /dev/null +++ b/tests/CallabelResolverTest.php @@ -0,0 +1,201 @@ +container = new Container(); + $this->resolver = new CallableResolver($this->container); + } + + /** + * + * @test + */ + public function resolves_function() + { + $result = $this->resolver->resolve('strlen'); + + $this->assertSame(strlen('Hello world!'), $result('Hello world!')); + } + + /** + * + * @test + */ + public function resolves_namespaced_function() + { + $result = $this->resolver->resolve(__NAMESPACE__ . '\foo'); + + $this->assertEquals('bar', $result()); + } + + /** + * + * @test + */ + public function resolves_callable_from_container() + { + $callable = function () {}; + $this->container['thing-to-call'] = function () use (&$callable) { + return $callable; + }; + + $this->assertSame($callable, $this->resolver->resolve('thing-to-call')); + } + + /** + * + * @test + */ + public function resolves_invokable_class() + { + $callable = new CallableSpy(); + $this->container[CallableSpy::class] = function () use (&$callable) { + return $callable; + }; + + $this->assertSame($callable, $this->resolver->resolve(CallableSpy::class)); + } + + /** + * + * @test + */ + public function resolve_array_method_call() + { + $fixture = new InvokerTestFixture(); + $this->container[InvokerTestFixture::class] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->resolver->resolve(array( + InvokerTestFixture::class, + 'foo' + )); + + $result(); + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function resolve_string_method_call() + { + $fixture = new InvokerTestFixture(); + $this->container[InvokerTestFixture::class] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->resolver->resolve(InvokerTestFixture::class . '::foo'); + + $result(); + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function resolves_array_method_call_with_service() + { + $fixture = new InvokerTestFixture(); + $this->container['thing-to-call'] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->resolver->resolve(array( + 'thing-to-call', + 'foo' + )); + + $result(); + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function resolves_string_method_call_with_service() + { + $fixture = new InvokerTestFixture(); + $this->container['thing-to-call'] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->resolver->resolve('thing-to-call::foo'); + + $result(); + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function throws_resolving_non_callable_from_container() + { + $this->expectExceptionMessage("'foo' is neither a callable nor a valid container entry"); + $this->expectException(NotCallableException::class); + $resolver = new CallableResolver(new Container()); + $resolver->resolve('foo'); + } + + /** + * + * @test + */ + public function handles_objects_correctly_in_exception_message() + { + $this->expectExceptionMessage("Instance of stdClass is not a callable"); + $this->expectException(NotCallableException::class); + $resolver = new CallableResolver(new Container()); + $resolver->resolve(new stdClass()); + } + + /** + * + * @test + */ + public function handles_method_calls_correctly_in_exception_message() + { + $this->expectExceptionMessage("stdClass::test() is not a callable"); + $this->expectException(NotCallableException::class); + $resolver = new CallableResolver(new Container()); + $resolver->resolve(array( + new stdClass(), + 'test' + )); + } +} + +function foo() +{ + return 'bar'; +} \ No newline at end of file diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php new file mode 100644 index 0000000..3ab6c52 --- /dev/null +++ b/tests/ContainerTest.php @@ -0,0 +1,616 @@ + + */ +class ContainerTest extends TestCase +{ + + public function testWithString() + { + $container = new Container(); + $container['param'] = Container::value('value'); + + $this->assertEquals('value', $container['param']); + } + + public function testWithClosure() + { + $container = new Container(); + $container['service'] = function () { + return new Fixtures\Service(); + }; + + $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $container['service']); + } + + public function testServicesShouldBeDifferent() + { + $container = new Container(); + $container['item'] = Container::factory(function () { + return new Fixtures\Service(); + }); + + $serviceOne = $container['item']; + $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $container['item']; + $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + public function testShouldPassContainerAsParameter() + { + $container = new Container(); + // Default is factory + $container['factory'] = function () { + return new Fixtures\Service(); + }; + + // Container is registerd by default + // $container['container'] = function ($container) { + // return $container; + // }; + + $this->assertNotSame($container, $container['factory']); + $this->assertSame($container, $container['container']); + } + + public function testIsset() + { + $container = new Container(); + $container['param'] = Container::value('value'); + $container['service'] = Container::service(function () { + return new Fixtures\Service(); + }); + + $container['null'] = Container::value(null); + + $this->assertTrue(isset($container['param'])); + $this->assertTrue(isset($container['service'])); + $this->assertTrue(isset($container['null'])); + $this->assertFalse(isset($container['non_existent'])); + } + + // public function testConstructorInjection() + // { + // $params = [ + // 'param' => 'value' + // ]; + // $container = new Container(); + + // $this->assertSame($params['param'], $container['param']); + // } + public function testOffsetGetValidatesKeyIsPresent() + { + $this->expectException(UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $container = new Container(); + echo $container['foo']; + } + + /** + * + * @group legacy + */ + public function testLegacyOffsetGetValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $container = new Container(); + echo $container['foo']; + } + + public function testOffsetGetHonorsNullValues() + { + $container = new Container(); + $container['foo'] = Container::value(null); + $this->assertNull($container['foo']); + } + + public function testUnset() + { + $container = new Container(); + $container['param'] = Container::value('value'); + $container['service'] = Container::service(function () { + return new Fixtures\Service(); + }); + + unset($container['param'], $container['service']); + $this->assertFalse(isset($container['param'])); + $this->assertFalse(isset($container['service'])); + } + + /** + * + * @dataProvider serviceDefinitionProvider + */ + public function testShare($service) + { + $container = new Container(); + $container['shared_service'] = Container::service($service); + $container['value'] = Container::value('value'); + + $serviceOne = $container['shared_service']; + $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceOne); + + $serviceTwo = $container['shared_service']; + $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceTwo); + + $this->assertSame($serviceOne, $serviceTwo); + } + + /** + * + * @dataProvider serviceDefinitionProvider + */ + public function testProtect($service) + { + $container = new Container(); + $container['protected'] = Container::value($service); + + $this->assertSame($service, $container['protected']); + } + + public function testGlobalFunctionNameAsParameterValue() + { + $container = new Container(); + $container['global_function'] = Container::value('strlen'); + $this->assertSame('strlen', $container['global_function']); + } + + public function testRaw() + { + $container = new Container(); + $container['factory'] = $definition = function () { + return 'foo'; + }; + $this->assertSame($definition, $container->raw('factory')); + } + + /** + * + * @test + */ + public function testRawValidatesKeyIsPresent() + { + $this->expectException(UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $container = new Container(); + $container->raw('foo'); + } + + /** + * + * @group legacy + */ + public function testLegacyRawValidatesKeyIsPresent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $container = new Container(); + $container->raw('foo'); + } + + // /** + // * + // * @dataProvider serviceDefinitionProvider + // */ + // public function testExtend($service) + // { + // $container = new Container(); + // $container['shared_service'] = Container::service(function () { + // return new Fixtures\Service(); + // }); + // $container['factory_service'] = function () { + // return new Fixtures\Service(); + // }; + + // $container->extend('shared_service', $service); + // $serviceOne = $container['shared_service']; + // $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceOne); + // $serviceTwo = $container['shared_service']; + // $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceTwo); + // $this->assertSame($serviceOne, $serviceTwo); + // $this->assertSame($serviceOne->value, $serviceTwo->value); + + // $container->extend('factory_service', $service); + // $serviceOne = $container['factory_service']; + // $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceOne); + // $serviceTwo = $container['factory_service']; + // $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $serviceTwo); + // $this->assertNotSame($serviceOne, $serviceTwo); + // $this->assertNotSame($serviceOne->value, $serviceTwo->value); + // } + + // public function testExtendDoesNotLeakWithFactories() + // { + // if (\extension_loaded('pimple')) { + // $this->markTestSkipped('Pimple extension does not support this test'); + // } + // $container = new Container(); + + // $container['foo'] = $container->factory(function () { + // return; + // }); + // $container['foo'] = $container->extend('foo', function ($foo, $container) { + // return; + // }); + // unset($container['foo']); + + // $p = new \ReflectionProperty($container, 'values'); + // $p->setAccessible(true); + // $this->assertEmpty($p->getValue($container)); + + // $p = new \ReflectionProperty($container, 'factories'); + // $p->setAccessible(true); + // $this->assertCount(0, $p->getValue($container)); + // } + + // public function testExtendValidatesKeyIsPresent() + // { + // $this->expectException(UnknownIdentifierException::class); + // $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + // $container = new Container(); + // $container->extend('foo', function () {}); + // } + + // /** + // * + // * @group legacy + // */ + // public function testLegacyExtendValidatesKeyIsPresent() + // { + // $this->expectException(\InvalidArgumentException::class); + // $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + // $container = new Container(); + // $container->extend('foo', function () {}); + // } + public function testKeys() + { + $containr = new Container(); + $containr['foo'] = Container::value(123); + $containr['bar'] = Container::value(123); + + $this->assertEquals([ + 'container', + 'foo', + 'bar' + ], $containr->keys()); + } + + /** + * + * @test + */ + public function settingAnInvokableObjectShouldTreatItAsFactory() + { + $container = new Container(); + $container['invokable'] = new Fixtures\Invokable(); + + $this->assertInstanceOf('Pluf\Tests\Fixtures\Service', $container['invokable']); + } + + /** + * + * @test + */ + public function settingNonInvokableObjectShouldTreatItAsParameter() + { + $container = new Container(); + $container['non_invokable'] = Container::value(new Fixtures\NonInvokable()); + + $this->assertInstanceOf('Pluf\Tests\Fixtures\NonInvokable', $container['non_invokable']); + } + + /** + * + * @dataProvider badServiceDefinitionProvider + */ + public function testFactoryFailsForInvalidServiceDefinitions($service) + { + $this->expectException(ExpectedInvokableException::class); +// $this->expectExceptionMessage('Service definition is not a Closure or invokable object.'); + + $container = new Container(); + $container['key'] = $service; + return $container; + } + + /** + * + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyFactoryFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); +// $this->expectExceptionMessage('Service definition is not a Closure or invokable object.'); + + $container = new Container(); + $container['key'] = $service; + return $container; + } + + /** + * + * @dataProvider badServiceDefinitionProvider + */ + public function testProtectFailsForInvalidServiceDefinitions($service) + { + $this->expectException(ExpectedInvokableException::class); +// $this->expectExceptionMessage('Factory is not a Closure or invokable object.'); + + $container = new Container(); + $container['key'] = $service; + return $container; + } + + /** + * + * @group legacy + * @dataProvider badServiceDefinitionProvider + */ + public function testLegacyProtectFailsForInvalidServiceDefinitions($service) + { + $this->expectException(\InvalidArgumentException::class); +// $this->expectExceptionMessage('Callable is not a Closure or invokable object.'); + + $container = new Container(); + $container['key'] = $service; + return $container; + } + +// /** +// * +// * @dataProvider badServiceDefinitionProvider +// */ +// public function testExtendFailsForKeysNotContainingServiceDefinitions($service) +// { +// $this->expectException(InvalidServiceIdentifierException::class); +// $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); + +// $container = new Container(); +// $container['foo'] = $service; +// $container->extend('foo', function () {}); +// } + +// /** +// * +// * @group legacy +// * @dataProvider badServiceDefinitionProvider +// */ +// public function testLegacyExtendFailsForKeysNotContainingServiceDefinitions($service) +// { +// $this->expectException(\InvalidArgumentException::class); +// $this->expectExceptionMessage('Identifier "foo" does not contain an object definition.'); + +// $container = new Container(); +// $container['foo'] = $service; +// $container->extend('foo', function () {}); +// } + +// /** +// * +// * @group legacy +// * @expectedDeprecation How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "foo" should be protected? +// */ +// public function testExtendingProtectedClosureDeprecation() +// { +// $container = new Container(); +// $container['foo'] = $container->protect(function () { +// return 'bar'; +// }); + +// $container->extend('foo', function ($value) { +// return $value . '-baz'; +// }); + +// $this->assertSame('bar-baz', $container['foo']); +// } + +// /** +// * +// * @dataProvider badServiceDefinitionProvider +// */ +// public function testExtendFailsForInvalidServiceDefinitions($service) +// { +// $this->expectException(ExpectedInvokableException::class); +// $this->expectExceptionMessage('Extension service definition is not a Closure or invokable object.'); + +// $container = new Container(); +// $container['foo'] = function () {}; +// $container->extend('foo', $service); +// } + +// /** +// * +// * @group legacy +// * @dataProvider badServiceDefinitionProvider +// */ +// public function testLegacyExtendFailsForInvalidServiceDefinitions($service) +// { +// $this->expectException(\InvalidArgumentException::class); +// $this->expectExceptionMessage('Extension service definition is not a Closure or invokable object.'); + +// $container = new Container(); +// $container['foo'] = function () {}; +// $container->extend('foo', $service); +// } + +// public function testExtendFailsIfFrozenServiceIsNonInvokable() +// { +// $this->expectException(FrozenServiceException::class); +// $this->expectExceptionMessage('Cannot override frozen service "foo".'); + +// $container = new Container(); +// $container['foo'] = function () { +// return new Fixtures\NonInvokable(); +// }; +// $foo = $container['foo']; + +// $container->extend('foo', function () {}); +// } + +// public function testExtendFailsIfFrozenServiceIsInvokable() +// { +// $this->expectException(FrozenServiceException::class); +// $this->expectExceptionMessage('Cannot override frozen service "foo".'); + +// $container = new Container(); +// $container['foo'] = function () { +// return new Fixtures\Invokable(); +// }; +// $foo = $container['foo']; + +// $container->extend('foo', function () {}); +// } + + /** + * Provider for invalid service definitions. + */ + public function badServiceDefinitionProvider() + { + return [ + [ + 123 + ], + [ + new Fixtures\NonInvokable() + ] + ]; + } + + /** + * Provider for service definitions. + */ + public function serviceDefinitionProvider() + { + return [ + [ + function ($value) { + $service = new Fixtures\Service(); + $service->value = $value; + + return $service; + } + ], + [ + new Fixtures\Invokable() + ] + ]; + } + + public function testDefiningNewServiceAfterFreeze() + { + $container = new Container(); + $container['foo'] = function () { + return 'foo'; + }; + $foo = $container['foo']; + + $container['bar'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $container['bar']); + } + + public function testOverridingServiceAfterFreeze() + { + $this->expectException(FrozenServiceException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $container = new Container(); + $container['foo'] = function () { + return 'foo'; + }; + $foo = $container['foo']; + + $container['foo'] = function () { + return 'bar'; + }; + } + + /** + * + * @group legacy + */ + public function testLegacyOverridingServiceAfterFreeze() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot override frozen service "foo".'); + + $container = new Container(); + $container['foo'] = function () { + return 'foo'; + }; + $foo = $container['foo']; + + $container['foo'] = function () { + return 'bar'; + }; + } + + public function testRemovingServiceAfterFreeze() + { + $container = new Container(); + $container['foo'] = function () { + return 'foo'; + }; + $foo = $container['foo']; + + unset($container['foo']); + $container['foo'] = function () { + return 'bar'; + }; + $this->assertSame('bar', $container['foo']); + } + +// public function testExtendingService() +// { +// $container = new Container(); +// $container['foo'] = function () { +// return 'foo'; +// }; +// $container['foo'] = $container->extend('foo', function ($foo, $app) { +// return "$foo.bar"; +// }); +// $container['foo'] = $container->extend('foo', function ($foo, $app) { +// return "$foo.baz"; +// }); +// $this->assertSame('foo.bar.baz', $container['foo']); +// } + +// public function testExtendingServiceAfterOtherServiceFreeze() +// { +// $container = new Container(); +// $container['foo'] = function () { +// return 'foo'; +// }; +// $container['bar'] = function () { +// return 'bar'; +// }; +// $foo = $container['foo']; + +// $container['bar'] = $container->extend('bar', function ($bar, $app) { +// return "$bar.baz"; +// }); +// $this->assertSame('bar.baz', $container['bar']); +// } +} diff --git a/tests/Fixtures/Invokable.php b/tests/Fixtures/Invokable.php new file mode 100644 index 0000000..927551d --- /dev/null +++ b/tests/Fixtures/Invokable.php @@ -0,0 +1,19 @@ + + * + */ +class Invokable +{ + + public function __invoke($value = null) + { + $service = new Service(); + $service->value = $value; + + return $service; + } +} diff --git a/tests/Fixtures/InvokerTestFixture.php b/tests/Fixtures/InvokerTestFixture.php new file mode 100644 index 0000000..a974d24 --- /dev/null +++ b/tests/Fixtures/InvokerTestFixture.php @@ -0,0 +1,20 @@ + + * + */ +class InvokerTestFixture +{ + + public $wasCalled = false; + + public function foo() + { + // Use this to make sure we are not called from a static context + $this->wasCalled = true; + return 'bar'; + } +} \ No newline at end of file diff --git a/tests/Fixtures/InvokerTestMagicMethodFixture.php b/tests/Fixtures/InvokerTestMagicMethodFixture.php new file mode 100644 index 0000000..0dbcc1b --- /dev/null +++ b/tests/Fixtures/InvokerTestMagicMethodFixture.php @@ -0,0 +1,24 @@ + + * + */ +class InvokerTestMagicMethodFixture +{ + + public $wasCalled = false; + + public function __call($name, $args) + { + if ($name === 'foo') { + $this->wasCalled = true; + return 'bar'; + } + throw new Exception('Unknown method'); + } +} \ No newline at end of file diff --git a/tests/Fixtures/InvokerTestStaticFixture.php b/tests/Fixtures/InvokerTestStaticFixture.php new file mode 100644 index 0000000..3040007 --- /dev/null +++ b/tests/Fixtures/InvokerTestStaticFixture.php @@ -0,0 +1,16 @@ + + * + */ +class InvokerTestStaticFixture +{ + + public static function foo() + { + return 'bar'; + } +} \ No newline at end of file diff --git a/tests/Fixtures/NonInvokable.php b/tests/Fixtures/NonInvokable.php new file mode 100644 index 0000000..0d9582d --- /dev/null +++ b/tests/Fixtures/NonInvokable.php @@ -0,0 +1,14 @@ + + * + */ +class NonInvokable +{ + + public function __call($a, $b) + {} +} diff --git a/tests/Fixtures/Service.php b/tests/Fixtures/Service.php new file mode 100644 index 0000000..c193895 --- /dev/null +++ b/tests/Fixtures/Service.php @@ -0,0 +1,12 @@ + + */ +class Service +{ + + public $value; +} diff --git a/tests/InvokerTest.php b/tests/InvokerTest.php new file mode 100644 index 0000000..b2fadd1 --- /dev/null +++ b/tests/InvokerTest.php @@ -0,0 +1,509 @@ + + * + */ +class InvokerTest extends TestCase +{ + + /** + * + * @var Invoker + */ + private $invoker; + + /** + * + * @var Container + */ + private $container; + + public function setUp(): void + { + parent::setUp(); + $this->container = new Container(); + $this->invoker = new Invoker(null, $this->container); + } + + /** + * + * @test + */ + public function should_invoke_closure() + { + $callable = new CallableSpy(); + + $this->invoker->call($callable); + + $this->assertWasCalled($callable); + } + + /** + * + * @test + */ + public function should_invoke_method() + { + $fixture = new InvokerTestFixture(); + + $this->invoker->call([ + $fixture, + 'foo' + ]); + + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function cannot_invoke_unknown_method() + { + $this->expectExceptionMessage("Pluf\Tests\Fixtures\InvokerTestFixture::bar() is not a callable."); + $this->expectException(NotCallableException::class); + $this->invoker->call([ + new InvokerTestFixture(), + 'bar' + ]); + } + + /** + * + * @test + */ + public function cannot_invoke_magic_method() + { + $this->expectExceptionMessage("Pluf\Tests\Fixtures\InvokerTestMagicMethodFixture::foo() is not a callable. A __call() method exists but magic methods are not supported."); + $this->expectException(NotCallableException::class); + $this->invoker->call(array( + new InvokerTestMagicMethodFixture(), + 'foo' + )); + } + + /** + * + * @test + */ + public function should_invoke_static_method() + { + $result = $this->invoker->call([ + InvokerTestStaticFixture::class, + 'foo' + ]); + + $this->assertEquals('bar', $result); + } + + /** + * + * @test + */ + public function should_invoke_static_method_with_scope_resolution_syntax() + { + $result = $this->invoker->call('Pluf\Tests\Fixtures\InvokerTestStaticFixture::foo'); + + $this->assertEquals('bar', $result); + } + + /** + * + * @test + */ + public function should_return_the_callable_return_value() + { + $result = $this->invoker->call(function () { + return 42; + }); + + $this->assertEquals(42, $result); + } + + /** + * + * @test + */ + public function should_throw_if_no_value_for_parameter() + { + $this->expectExceptionMessage('Unable to invoke the callable because no value was given for parameter 2 ($bar)'); + $this->expectException(NotEnoughParametersException::class); + $this->invoker->call(function ($foo, $bar, $baz) {}, [ + 'foo' => 'foo', + 'baz' => 'baz' + ]); + } + + /** + * + * @test + */ + public function should_throw_if_no_value_for_parameter_even_with_trailing_optional_parameters() + { + $this->expectExceptionMessage('Unable to invoke the callable because no value was given for parameter 2 ($bar)'); + $this->expectException(NotEnoughParametersException::class); + $this->invoker->call(function ($foo, $bar, $baz = null) {}, [ + 'foo' => 'foo', + 'baz' => 'baz' + ]); + } + + /** + * + * @test + */ + public function should_invoke_callable_with_parameters_indexed_by_position() + { + $callable = new CallableSpy(); + + $this->invoker->call($callable, [ + 'foo', + 'bar' + ]); + + $this->assertWasCalledWith($callable, [ + 'foo', + 'bar' + ]); + } + + /** + * + * @test + */ + public function should_invoke_callable_with_parameters_indexed_by_name() + { + $parameters = [ + 'foo' => 'foo', + 'bar' => 'bar' + ]; + + $result = $this->invoker->call(function ($foo, $bar) { + return $foo . $bar; + }, $parameters); + + $this->assertEquals('foobar', $result); + } + + /** + * + * @test + */ + public function should_invoke_callable_with_default_value_for_undefined_parameters() + { + $parameters = [ + 'foo', // Positioned parameter + 'baz' => 'baz' // Named parameter + ]; + + $result = $this->invoker->call(function ($foo, $bar = 'bar', $baz = null) { + return $foo . $bar . $baz; + }, $parameters); + + $this->assertEquals('foobarbaz', $result); + } + + /** + * + * @test + */ + public function should_do_dependency_injection_with_typehint_container_resolver() + { + $resolver = new TypeHintContainerResolver($this->container); + $this->invoker->getParameterResolver()->prependResolver($resolver); + + $expected = new stdClass(); + $this->container['stdClass'] = function () use (&$expected) { + return $expected; + }; + + $result = $this->invoker->call(function (stdClass $foo) { + return $foo; + }); + + $this->assertSame($expected, $result); + } + + /** + * + * @test + */ + public function should_do_dependency_injection_with_parameter_name_container_resolver() + { + $resolver = new ParameterNameContainerResolver($this->container); + $this->invoker->getParameterResolver()->prependResolver($resolver); + + $expected = new stdClass(); + $this->container['foo'] = function () use (&$expected) { + return $expected; + }; + + $result = $this->invoker->call(function ($foo) { + return $foo; + }); + + $this->assertSame($expected, $result); + } + + /** + * + * @test + */ + public function should_resolve_callable_from_container() + { + $callable = new CallableSpy(); + $this->container['thing-to-call'] = function () use (&$callable) { + return $callable; + }; + + $this->invoker->call('thing-to-call'); + + $this->assertWasCalled($callable); + } + + /** + * + * @test + */ + public function should_resolve_array_callable_from_container() + { + $fixture = new InvokerTestFixture(); + $this->container['thing-to-call'] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->invoker->call([ + 'thing-to-call', + 'foo' + ]); + + $this->assertEquals('bar', $result); + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function should_resolve_callable_from_container_with_scope_resolution_syntax() + { + $fixture = new InvokerTestFixture(); + $this->container['thing-to-call'] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->invoker->call('thing-to-call::foo'); + + $this->assertEquals('bar', $result); + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function should_resolve_array_callable_from_container_with_class_name() + { + $fixture = new InvokerTestFixture(); + $this->container[InvokerTestFixture::class] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->invoker->call([ + InvokerTestFixture::class, + 'foo' + ]); + + $this->assertEquals('bar', $result); + $this->assertTrue($fixture->wasCalled); + } + + /** + * + * @test + */ + public function should_resolve_callable_from_container_with_class_name_in_scope_resolution_syntax() + { + $fixture = new InvokerTestFixture(); + $this->container[InvokerTestFixture::class] = function () use (&$fixture) { + return $fixture; + }; + + $result = $this->invoker->call('Pluf\Tests\Fixtures\InvokerTestFixture::foo'); + + $this->assertEquals('bar', $result); + $this->assertTrue($fixture->wasCalled); + } + + /** + * Mixing named parameters with positioned parameters is a really bad idea. + * When that happens, the positioned parameters have the highest priority and will + * override named parameters in case of conflicts. + * + * Note that numeric array indexes ignore string indexes. In our example, the + * 'bar' value has the position `0`, which overrides the 'foo' value. + * + * @test + */ + public function positioned_parameters_have_the_highest_priority() + { + $factory = function ($foo, $bar = 300) { + return [ + $foo, + $bar + ]; + }; + $result = $this->invoker->call($factory, [ + 'foo' => 'foo', + 'bar' + ]); + + $this->assertEquals([ + 'bar', + 300 + ], $result); + } + + /** + * + * @test + */ + public function should_not_invoke_statically_a_non_static_method() + { + $this->expectExceptionMessage("Cannot call foo() on Pluf\Tests\Fixtures\InvokerTestFixture because it is not a class nor a valid container entry"); + $this->expectException(NotCallableException::class); + $this->invoker->call([ + InvokerTestFixture::class, + 'foo' + ]); + } + + /** + * + * @test + */ + public function should_throw_if_calling_non_callable_without_container() + { + $this->expectExceptionMessage("'foo' is not a callable"); + $this->expectException(NotCallableException::class); + $invoker = new Invoker(); + $invoker->call('foo'); + } + + /** + * + * @test + */ + public function should_throw_if_calling_non_callable_without_container_2() + { + $this->expectExceptionMessage("NULL is not a callable"); + $this->expectException(NotCallableException::class); + $invoker = new Invoker(); + $invoker->call(null); + } + + /** + * + * @test + */ + public function should_throw_if_calling_non_callable_with_container() + { + $this->expectExceptionMessage("'foo' is neither a callable nor a valid container entry"); + $this->expectException(NotCallableException::class); + $invoker = new Invoker(null, new Container()); + $invoker->call('foo'); + } + + /** + * + * @test + */ + public function should_throw_if_calling_non_callable_object() + { + $this->expectExceptionMessage('Instance of stdClass is not a callable'); + $this->expectException(NotCallableException::class); + $invoker = new Invoker(); + $invoker->call(new stdClass()); + } + + /** + * + * @test + */ + public function should_invoke_static_method_rather_than_resolving_entry_from_container() + { + // Register a non-callable so that test fails if we try to invoke that + $this->container[InvokerTestStaticFixture::class] = function () { + return 'foobar'; + }; + + // Call the static method: shouldn't get from the container even though the + // entry exist (because we are calling a static method) + $result = $this->invoker->call([ + InvokerTestStaticFixture::class, + 'foo' + ]); + $this->assertEquals('bar', $result); + } + + /** + * + * @test + */ + public function should_throw_if_no_value_for_optional_parameter_1() + { + $this->expectExceptionMessage('Unable to invoke the callable because no value was given for parameter 2 ($bar)'); + $this->expectException(NotEnoughParametersException::class); + // Create without the DefaultValueResolver + $this->invoker = new Invoker(new AssociativeArrayResolver(), $this->container); + $this->invoker->call(function ($foo, $bar = null) {}, [ + 'foo' => 'foo' + ]); + } + + /** + * + * @test + */ + public function should_throw_if_no_value_for_optional_parameter_2() + { + $this->expectExceptionMessage('Unable to invoke the callable because no value was given for parameter 2 ($bar)'); + $this->expectException(NotEnoughParametersException::class); + // Create without the DefaultValueResolver + $this->invoker = new Invoker(new AssociativeArrayResolver(), $this->container); + $this->invoker->call(function ($foo, $bar = null, $baz = null) {}, [ + 'foo' => 'foo', + 'baz' => 'baz' + ]); + } + + private function assertWasCalled(CallableSpy $callableSpy) + { + $this->assertEquals(1, $callableSpy->getCallCount(), 'The callable should be called once'); + } + + private function assertWasCalledWith(CallableSpy $callableSpy, array $parameters) + { + $this->assertWasCalled($callableSpy); + $this->assertEquals($parameters, $callableSpy->getLastCallParameters()); + } +} + + diff --git a/tests/Mock/CallableSpy.php b/tests/Mock/CallableSpy.php new file mode 100644 index 0000000..9990735 --- /dev/null +++ b/tests/Mock/CallableSpy.php @@ -0,0 +1,69 @@ + + */ +class CallableSpy +{ + + /** + * + * @var callable|null + */ + private $callable; + + /** + * + * @var int + */ + private $callCount = 0; + + /** + * + * @var array + */ + private $parameters = []; + + public static function mock($callable) + { + return new self($callable); + } + + public function __construct($callable = null) + { + $this->callable = $callable; + } + + public function __invoke() + { + $this->callCount ++; + $this->parameters = func_get_args(); + + if ($this->callable === null) { + return null; + } + + return call_user_func_array($this->callable, func_get_args()); + } + + /** + * + * @return int + */ + public function getCallCount() + { + return $this->callCount; + } + + /** + * + * @return array + */ + public function getLastCallParameters() + { + return $this->parameters; + } +} \ No newline at end of file diff --git a/tests/Mock/Notfound.php b/tests/Mock/Notfound.php new file mode 100644 index 0000000..658e416 --- /dev/null +++ b/tests/Mock/Notfound.php @@ -0,0 +1,12 @@ + + */ +class NotFound extends \Exception implements NotFoundExceptionInterface +{ +} \ No newline at end of file diff --git a/tests/Mock/StaticCallableSpy.php b/tests/Mock/StaticCallableSpy.php new file mode 100644 index 0000000..6f0bdd6 --- /dev/null +++ b/tests/Mock/StaticCallableSpy.php @@ -0,0 +1,22 @@ + + */ +class StaticCallableSpy +{ + + /** + * + * @var int + */ + public static int $callCount = 0; + + public function __invoke() + { + StaticCallableSpy::$callCount ++; + } +} \ No newline at end of file diff --git a/tests/Psr11/ContainerTest.php b/tests/Psr11/ContainerTest.php new file mode 100644 index 0000000..95527f3 --- /dev/null +++ b/tests/Psr11/ContainerTest.php @@ -0,0 +1,51 @@ + + * + */ +class ContainerTest extends TestCase +{ + + public function testGetReturnsExistingService() + { + $psr = new Container(); + $psr['service'] = Container::service(function () { + return new Service(); + }); + + $this->assertSame($psr['service'], $psr->get('service')); + } + + public function testGetThrowsExceptionIfServiceIsNotFound() + { + $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); + $this->expectExceptionMessage('Identifier "service" is not defined.'); + + $psr = new Container(); + + $psr->get('service'); + } + + public function testHasReturnsTrueIfServiceExists() + { + $psr = new Container(); + $psr['service'] = function () { + return new Service(); + }; + + $this->assertTrue($psr->has('service')); + } + + public function testHasReturnsFalseIfServiceDoesNotExist() + { + $psr = new Container(); + $this->assertFalse($psr->has('service')); + } +} diff --git a/tests/Psr11/ServiceLocatorTest.php b/tests/Psr11/ServiceLocatorTest.php new file mode 100644 index 0000000..3112e2e --- /dev/null +++ b/tests/Psr11/ServiceLocatorTest.php @@ -0,0 +1,123 @@ + + */ +class ServiceLocatorTest extends TestCase +{ + + public function testCanAccessServices() + { + $container = new Container(); + $container['service'] = Container::service(function () { + return new Fixtures\Service(); + }); + $locator = new ServiceLocator($container, [ + 'service' + ]); + + $this->assertSame($container['service'], $locator->get('service')); + } + + public function testCanAccessAliasedServices() + { + $container = new Container(); + $container['service'] = Container::service(function () { + return new Fixtures\Service(); + }); + $locator = new ServiceLocator($container, [ + 'alias' => 'service' + ]); + + $this->assertSame($container['service'], $locator->get('alias')); + } + + public function testCannotAccessAliasedServiceUsingRealIdentifier() + { + $this->expectException(UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "service" is not defined.'); + + $container = new Container(); + $container['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($container, [ + 'alias' => 'service' + ]); + + $service = $locator->get('service'); + } + + public function testGetValidatesServiceCanBeLocated() + { + $this->expectException(UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "foo" is not defined.'); + + $container = new Container(); + $container['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($container, [ + 'alias' => 'service' + ]); + + $service = $locator->get('foo'); + } + + public function testGetValidatesTargetServiceExists() + { + $this->expectException(UnknownIdentifierException::class); + $this->expectExceptionMessage('Identifier "invalid" is not defined.'); + + $container = new Container(); + $container['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($container, [ + 'alias' => 'invalid' + ]); + + $service = $locator->get('alias'); + } + + public function testHasValidatesServiceCanBeLocated() + { + $container = new Container(); + $container['service1'] = function () { + return new Fixtures\Service(); + }; + $container['service2'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($container, [ + 'service1' + ]); + + $this->assertTrue($locator->has('service1')); + $this->assertFalse($locator->has('service2')); + } + + public function testHasChecksIfTargetServiceExists() + { + $container = new Container(); + $container['service'] = function () { + return new Fixtures\Service(); + }; + $locator = new ServiceLocator($container, [ + 'foo' => 'service', + 'bar' => 'invalid' + ]); + + $this->assertTrue($locator->has('foo')); + $this->assertFalse($locator->has('bar')); + } +} diff --git a/tests/ServiceIteratorTest.php b/tests/ServiceIteratorTest.php new file mode 100644 index 0000000..08537b4 --- /dev/null +++ b/tests/ServiceIteratorTest.php @@ -0,0 +1,39 @@ + + * + */ +class ServiceIteratorTest extends TestCase +{ + + public function testIsIterable() + { + $container = new Container(); + $container['service1'] = Container::service(function () { + return new Service(); + }); + $container['service2'] = Container::service(function () { + return new Service(); + }); + $container['service3'] = Container::service(function () { + return new Service(); + }); + $iterator = new ServiceIterator($container, [ + 'service1', + 'service2' + ]); + + $this->assertSame([ + 'service1' => $container['service1'], + 'service2' => $container['service2'] + ], iterator_to_array($iterator)); + } +}