diff --git a/CHANGELOG.md b/CHANGELOG.md index 85969e33..42481ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ To get the diff between two versions, go to https://github.com/SonsOfPHP/sonsofp ## [Unreleased] +* [PR #226](https://github.com/SonsOfPHP/sonsofphp/pull/226) [Registry] New Component and Contract * [PR #225](https://github.com/SonsOfPHP/sonsofphp/pull/225) Maintenance * [PR #222](https://github.com/SonsOfPHP/sonsofphp/pull/222) [Assert] New Component * [PR #221](https://github.com/SonsOfPHP/sonsofphp/pull/221) [StateMachine] New Contract and Component diff --git a/bard.json b/bard.json index 52498537..729bb11f 100644 --- a/bard.json +++ b/bard.json @@ -177,6 +177,14 @@ "path": "src/SonsOfPHP/Contract/Pager", "repository": "git@github.com:SonsOfPHP/pager-contract.git" }, + { + "path": "src/SonsOfPHP/Component/Registry", + "repository": "git@github.com:SonsOfPHP/registry.git" + }, + { + "path": "src/SonsOfPHP/Contract/Registry", + "repository": "git@github.com:SonsOfPHP/registry-contract.git" + }, { "path": "src/SonsOfPHP/Contract/Version", "repository": "git@github.com:SonsOfPHP/version-contract.git" diff --git a/composer.json b/composer.json index a459bf7b..552ef0a1 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,8 @@ "psr/http-server-middleware-implementation": "^1.0", "sonsofphp/http-handler-implementation": "0.3.x-dev", "sonsofphp/mailer-implementation": "0.3.x-dev", - "sonsofphp/state-machine-implementation": "0.3.x-dev" + "sonsofphp/state-machine-implementation": "0.3.x-dev", + "sonsofphp/registry-implementation": "0.3.x-dev" }, "require": { "php": ">=8.2", @@ -87,7 +88,8 @@ "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", "aws/aws-sdk-php": "^3.0", - "liip/imagine-bundle": "^2.0" + "liip/imagine-bundle": "^2.0", + "sonsofphp/registry-contract": "0.3.x-dev" }, "replace": { "sonsofphp/bard": "self.version", @@ -135,7 +137,9 @@ "sonsofphp/filesystem-liip-imagine": "self.version", "sonsofphp/state-machine": "self.version", "sonsofphp/state-machine-contract": "self.version", - "sonsofphp/assert": "self.version" + "sonsofphp/assert": "self.version", + "sonsofphp/registry": "self.version", + "sonsofphp/registry-contract": "self.version" }, "autoload": { "psr-4": { @@ -183,6 +187,8 @@ "SonsOfPHP\\Contract\\Logger\\": "src/SonsOfPHP/Contract/Logger", "SonsOfPHP\\Contract\\Money\\": "src/SonsOfPHP/Contract/Money", "SonsOfPHP\\Contract\\Pager\\": "src/SonsOfPHP/Contract/Pager", + "SonsOfPHP\\Component\\Registry\\": "src/SonsOfPHP/Component/Registry", + "SonsOfPHP\\Contract\\Registry\\": "src/SonsOfPHP/Contract/Registry", "SonsOfPHP\\Contract\\Version\\": "src/SonsOfPHP/Contract/Version" }, "exclude-from-classmap": [ @@ -217,7 +223,8 @@ "src/SonsOfPHP/Bridge/Doctrine/Collections/Pager/Tests", "src/SonsOfPHP/Bridge/Doctrine/DBAL/Pager/Tests", "src/SonsOfPHP/Bridge/Doctrine/ORM/Pager/Tests", - "src/SonsOfPHP/Component/Version/Tests" + "src/SonsOfPHP/Component/Version/Tests", + "src/SonsOfPHP/Component/Registry/Tests" ] }, "extra": { @@ -240,4 +247,4 @@ "SonsOfPHP\\Bridge\\Symfony\\Cqrs\\Tests\\": "src/SonsOfPHP/Bridge/Symfony/Cqrs/Tests" } } -} +} \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 5e719959..0f3d1625 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -51,6 +51,7 @@ * [Queries](components/money/queries.md) * [Pager](components/pager/README.md) * [Adapters](components/pager/adapters.md) +* [Registry](components/registry.md) * [State Machine](components/state-machine.md) * [Version](components/version.md) @@ -63,6 +64,7 @@ * [Filesystem](contracts/filesystem.md) * [Mailer](contracts/mailer.md) * [Pager](contracts/pager.md) +* [Registry](contracts/registry.md) * [State Machine](contracts/state-machine.md) ## 💁 Contributing diff --git a/docs/components/registry.md b/docs/components/registry.md new file mode 100644 index 00000000..1d864316 --- /dev/null +++ b/docs/components/registry.md @@ -0,0 +1,32 @@ +--- +title: Registry +--- + +## Installation + +```shell +composer require sonsofphp/registry +``` + +## Usage + +```php +register('service.id', $service); +$service = $registry->get('service.id'); + +if ($registry->has('service.id')) { + // ... +} + +$registry->unregister('service.id'); + +foreach ($registry->all() as $identifier => $service) { + // ... +} +``` diff --git a/docs/contracts/registry.md b/docs/contracts/registry.md new file mode 100644 index 00000000..5fe64d9f --- /dev/null +++ b/docs/contracts/registry.md @@ -0,0 +1,9 @@ +--- +title: Registry Contract +--- + +## Installation + +```shell +composer require sonsofphp/registry-contract +``` diff --git a/src/SonsOfPHP/Component/Registry/.gitattributes b/src/SonsOfPHP/Component/Registry/.gitattributes new file mode 100644 index 00000000..84c7add0 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/SonsOfPHP/Component/Registry/.gitignore b/src/SonsOfPHP/Component/Registry/.gitignore new file mode 100644 index 00000000..5414c2c6 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor/ diff --git a/src/SonsOfPHP/Component/Registry/Exception/ExistingServiceException.php b/src/SonsOfPHP/Component/Registry/Exception/ExistingServiceException.php new file mode 100644 index 00000000..610c0b34 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/Exception/ExistingServiceException.php @@ -0,0 +1,12 @@ + + */ +class ExistingServiceException extends \Exception implements ExistingServiceExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Registry/Exception/NonExistingServiceException.php b/src/SonsOfPHP/Component/Registry/Exception/NonExistingServiceException.php new file mode 100644 index 00000000..946f2189 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/Exception/NonExistingServiceException.php @@ -0,0 +1,12 @@ + + */ +class NonExistingServiceException extends \Exception implements NonExistingServiceExceptionInterface {} diff --git a/src/SonsOfPHP/Component/Registry/LICENSE b/src/SonsOfPHP/Component/Registry/LICENSE new file mode 100644 index 00000000..39238382 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/LICENSE @@ -0,0 +1,19 @@ +Copyright 2022 to Present Joshua Estes + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/SonsOfPHP/Component/Registry/README.md b/src/SonsOfPHP/Component/Registry/README.md new file mode 100644 index 00000000..0b666b97 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/README.md @@ -0,0 +1,52 @@ +Sons of PHP - State Machine +=========================== + +```php + 'order', + 'supports' => [ + OrderInterface::class, + ], + 'transitions' => [ + 'create' => [ + 'from' => 'draft', + 'to' => 'new', + ], + 'fulfill' => [ + 'from' => 'new', + 'to' => 'fulfilled', + ], + 'cancel' => [ + 'from' => ['draft', 'new', 'fulfilled'], + 'to' => 'fulfilled', + ], + ], +]); + +// Check if state can change +$sm->can($order, 'create'); + +// Apply transition +$sm->apply($order, 'fulfil'); + +// Get Current State +$sm->getState($order); +``` + +## Learn More + +* [Documentation][docs] +* [Contributing][contributing] +* [Report Issues][issues] and [Submit Pull Requests][pull-requests] in the [Mother Repository][mother-repo] +* Get Help & Support using [Discussions][discussions] + +[discussions]: https://github.com/orgs/SonsOfPHP/discussions +[mother-repo]: https://github.com/SonsOfPHP/sonsofphp +[contributing]: https://docs.sonsofphp.com/contributing/ +[docs]: https://docs.sonsofphp.com/components/state-machine/ +[issues]: https://github.com/SonsOfPHP/sonsofphp/issues?q=is%3Aopen+is%3Aissue+label%3AStateMachine +[pull-requests]: https://github.com/SonsOfPHP/sonsofphp/pulls?q=is%3Aopen+is%3Apr+label%3AStateMachine diff --git a/src/SonsOfPHP/Component/Registry/ServiceRegistry.php b/src/SonsOfPHP/Component/Registry/ServiceRegistry.php new file mode 100644 index 00000000..ea941c35 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/ServiceRegistry.php @@ -0,0 +1,77 @@ + + */ +class ServiceRegistry implements ServiceRegistryInterface +{ + private array $services = []; + + public function __construct( + private string $interface, + ) {} + + /** + * {@inheritdoc} + */ + public function all(): iterable + { + return $this->services; + } + + /** + * {@inheritdoc} + */ + public function register(string $identifier, object $service): void + { + if ($this->has($identifier)) { + throw new ExistingServiceException(); + } + + if (!$service instanceof $this->interface) { + throw new \InvalidArgumentException(); + } + + $this->services[$identifier] = $service; + } + + /** + * {@inheritdoc} + */ + public function unregister(string $identifier): void + { + if (!$this->has($identifier)) { + throw new NonExistingServiceException(); + } + + unset($this->services[$identifier]); + } + + /** + * {@inheritdoc} + */ + public function has(string $identifier): bool + { + return array_key_exists($identifier, $this->services); + } + + /** + * {@inheritdoc} + */ + public function get(string $identifier): object + { + if (!$this->has($identifier)) { + throw new NonExistingServiceException(); + } + + return $this->services[$identifier]; + } +} diff --git a/src/SonsOfPHP/Component/Registry/Tests/ServiceRegistryTest.php b/src/SonsOfPHP/Component/Registry/Tests/ServiceRegistryTest.php new file mode 100644 index 00000000..d2d17af6 --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/Tests/ServiceRegistryTest.php @@ -0,0 +1,88 @@ +registry = new ServiceRegistry('Exception'); + } + + public function testItHasTheCorrectInterface(): void + { + $this->assertInstanceOf(ServiceRegistryInterface::class, $this->registry); + } + + public function testItCanRegisterNewServices(): void + { + $identifier = 'exception'; + $this->assertCount(0, $this->registry->all()); + $this->assertFalse($this->registry->has($identifier)); + $this->registry->register($identifier, new \Exception()); + $this->assertCount(1, $this->registry->all()); + $this->assertTrue($this->registry->has($identifier)); + } + + public function testItWillThrowCorrectExceptionWhenServiceIdentifierExists(): void + { + $this->registry->register('exception', new \Exception()); + $this->expectException(ExistingServiceExceptionInterface::class); + $this->registry->register('exception', new \Exception()); + } + + public function testItWillThrowCorrectExceptionWhenServiceIsWrongType(): void + { + $this->expectException('InvalidArgumentException'); + $this->registry->register('stdclass', new \stdClass()); + } + + public function testItWillReturnAllServicesInTheCorrectFormat(): void + { + $this->registry->register('exception', new \Exception()); + $services = $this->registry->all(); + $this->assertArrayHasKey('exception', $services); + } + + public function testItCanUnregisterServices(): void + { + $identifier = 'exception'; + $this->registry->register($identifier, new \Exception()); + $this->assertTrue($this->registry->has($identifier)); + $this->registry->unregister($identifier); + $this->assertFalse($this->registry->has($identifier)); + } + + public function testItWillThrowCorrectExceptionWhenUnregisteringNonExistingService(): void + { + $this->expectException(NonExistingServiceExceptionInterface::class); + $this->registry->unregister('stdclass'); + } + + public function testItIsAbleToRetrieveRegisteredService(): void + { + $identifier = 'exception'; + $service = new \Exception(); + $this->registry->register($identifier, $service); + $this->assertSame($service, $this->registry->get($identifier)); + } + + public function testItWillThrowCorrectExceptionWhenGettingNonExistingService(): void + { + $this->expectException(NonExistingServiceExceptionInterface::class); + $this->registry->get('stdclass'); + } +} diff --git a/src/SonsOfPHP/Component/Registry/composer.json b/src/SonsOfPHP/Component/Registry/composer.json new file mode 100644 index 00000000..9d2fc34d --- /dev/null +++ b/src/SonsOfPHP/Component/Registry/composer.json @@ -0,0 +1,55 @@ +{ + "name": "sonsofphp/registry", + "type": "library", + "description": "", + "keywords": [ + "service-registry", + "registry" + ], + "homepage": "https://github.com/SonsOfPHP/registry", + "license": "MIT", + "authors": [ + { + "name": "Joshua Estes", + "email": "joshua@sonsofphp.com" + } + ], + "support": { + "issues": "https://github.com/SonsOfPHP/sonsofphp/issues", + "forum": "https://github.com/orgs/SonsOfPHP/discussions", + "docs": "https://docs.sonsofphp.com" + }, + "autoload": { + "psr-4": { + "SonsOfPHP\\Component\\Registry\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": ">=8.2", + "sonsofphp/registry-contract": "0.3.x-dev" + }, + "provide": { + "sonsofphp/registry-implementation": "0.3.x-dev" + }, + "extra": { + "sort-packages": true, + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/JoshuaEstes" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/packagist-sonsofphp-sonsofphp" + } + ] +} \ No newline at end of file diff --git a/src/SonsOfPHP/Contract/Registry/ExistingServiceExceptionInterface.php b/src/SonsOfPHP/Contract/Registry/ExistingServiceExceptionInterface.php new file mode 100644 index 00000000..9cfa619a --- /dev/null +++ b/src/SonsOfPHP/Contract/Registry/ExistingServiceExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface ExistingServiceExceptionInterface extends \Throwable {} diff --git a/src/SonsOfPHP/Contract/Registry/NonExistingServiceExceptionInterface.php b/src/SonsOfPHP/Contract/Registry/NonExistingServiceExceptionInterface.php new file mode 100644 index 00000000..50dcfafa --- /dev/null +++ b/src/SonsOfPHP/Contract/Registry/NonExistingServiceExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface NonExistingServiceExceptionInterface extends \Throwable {} diff --git a/src/SonsOfPHP/Contract/Registry/composer.json b/src/SonsOfPHP/Contract/Registry/composer.json index b32989db..9049e003 100644 --- a/src/SonsOfPHP/Contract/Registry/composer.json +++ b/src/SonsOfPHP/Contract/Registry/composer.json @@ -51,4 +51,4 @@ "url": "https://tidelift.com/subscription/pkg/packagist-sonsofphp-sonsofphp" } ] -} +} \ No newline at end of file