You've already forked Php-Template
feat: implement command bus with attribute-based handler resolution and add example command and handler (#27)
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m13s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m24s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m58s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m20s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m0s
All checks were successful
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m13s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m24s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m58s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m20s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m5s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m0s
Reviewed-on: #27 Co-authored-by: Ron Rise <ron@siteworxpro.com> Co-committed-by: Ron Rise <ron@siteworxpro.com>
This commit was merged in pull request #27.
This commit is contained in:
@@ -27,7 +27,8 @@
|
|||||||
"react/async": "^4",
|
"react/async": "^4",
|
||||||
"guzzlehttp/guzzle": "^7.10",
|
"guzzlehttp/guzzle": "^7.10",
|
||||||
"zircote/swagger-php": "^5.7",
|
"zircote/swagger-php": "^5.7",
|
||||||
"spiral/roadrunner-grpc": "^3.5"
|
"spiral/roadrunner-grpc": "^3.5",
|
||||||
|
"league/tactician": "^1.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^12.4",
|
"phpunit/phpunit": "^12.4",
|
||||||
|
|||||||
57
composer.lock
generated
57
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "977f74570c671e4d59fd70d5e732c3d2",
|
"content-hash": "d027bee8e875c5542f7ff9612bfac4e2",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "adhocore/cli",
|
"name": "adhocore/cli",
|
||||||
@@ -1360,6 +1360,61 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-11-25T08:10:15+00:00"
|
"time": "2024-11-25T08:10:15+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "league/tactician",
|
||||||
|
"version": "v1.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/tactician.git",
|
||||||
|
"reference": "e79f763170f3d5922ec29e85cffca0bac5cd8975"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/tactician/zipball/e79f763170f3d5922ec29e85cffca0bac5cd8975",
|
||||||
|
"reference": "e79f763170f3d5922ec29e85cffca0bac5cd8975",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.3",
|
||||||
|
"phpunit/phpunit": "^7.5.20 || ^9.3.8",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5.8"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\Tactician\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ross Tuck",
|
||||||
|
"homepage": "http://tactician.thephpleague.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A small, flexible command bus. Handy for building service layers.",
|
||||||
|
"keywords": [
|
||||||
|
"command",
|
||||||
|
"command bus",
|
||||||
|
"service layer"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/thephpleague/tactician/issues",
|
||||||
|
"source": "https://github.com/thephpleague/tactician/tree/v1.1.0"
|
||||||
|
},
|
||||||
|
"time": "2021-02-14T15:29:04+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.9.0",
|
"version": "3.9.0",
|
||||||
|
|||||||
22
src/Attributes/CommandBus/HandlesCommand.php
Normal file
22
src/Attributes/CommandBus/HandlesCommand.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Attributes\CommandBus;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class HandlesCommand
|
||||||
|
* @package Siteworxpro\App\Attributes\CommandBus
|
||||||
|
*/
|
||||||
|
#[Attribute(Attribute::TARGET_CLASS)]
|
||||||
|
readonly class HandlesCommand
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param class-string $commandClass
|
||||||
|
*/
|
||||||
|
public function __construct(public string $commandClass)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/CommandBus/AttributeLocator.php
Normal file
49
src/CommandBus/AttributeLocator.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus;
|
||||||
|
|
||||||
|
use League\Tactician\Exception\CanNotInvokeHandlerException;
|
||||||
|
use League\Tactician\Handler\Locator\HandlerLocator;
|
||||||
|
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
|
||||||
|
|
||||||
|
class AttributeLocator implements HandlerLocator
|
||||||
|
{
|
||||||
|
private const string HANDLER_NAMESPACE = 'Siteworxpro\\App\\CommandBus\\Handlers\\';
|
||||||
|
|
||||||
|
private array $handlers;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$directory = __DIR__ . '/Handlers';
|
||||||
|
$files = scandir($directory);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
|
||||||
|
$className = pathinfo($file, PATHINFO_FILENAME);
|
||||||
|
$fullClassName = self::HANDLER_NAMESPACE . $className;
|
||||||
|
|
||||||
|
if (class_exists($fullClassName)) {
|
||||||
|
$reflectionClass = new \ReflectionClass($fullClassName);
|
||||||
|
$attributes = $reflectionClass->getAttributes(HandlesCommand::class);
|
||||||
|
|
||||||
|
foreach ($attributes as $attribute) {
|
||||||
|
$instance = $attribute->newInstance();
|
||||||
|
$commandClass = $instance->commandClass;
|
||||||
|
$this->handlers[$commandClass] = $fullClassName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHandlerForCommand($commandName)
|
||||||
|
{
|
||||||
|
if (isset($this->handlers[$commandName])) {
|
||||||
|
$handlerClass = $this->handlers[$commandName];
|
||||||
|
return new $handlerClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CanNotInvokeHandlerException("No handler found for command: " . $commandName);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/CommandBus/Commands/Command.php
Normal file
9
src/CommandBus/Commands/Command.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus\Commands;
|
||||||
|
|
||||||
|
readonly abstract class Command
|
||||||
|
{
|
||||||
|
}
|
||||||
18
src/CommandBus/Commands/ExampleCommand.php
Normal file
18
src/CommandBus/Commands/ExampleCommand.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus\Commands;
|
||||||
|
|
||||||
|
readonly class ExampleCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private string $name
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/CommandBus/Handlers/CommandHandler.php
Normal file
9
src/CommandBus/Handlers/CommandHandler.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus\Handlers;
|
||||||
|
|
||||||
|
abstract class CommandHandler implements CommandHandlerInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
12
src/CommandBus/Handlers/CommandHandlerInterface.php
Normal file
12
src/CommandBus/Handlers/CommandHandlerInterface.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus\Handlers;
|
||||||
|
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\Command;
|
||||||
|
|
||||||
|
interface CommandHandlerInterface
|
||||||
|
{
|
||||||
|
public function __invoke(Command $command): mixed;
|
||||||
|
}
|
||||||
30
src/CommandBus/Handlers/ExampleHandler.php
Normal file
30
src/CommandBus/Handlers/ExampleHandler.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\CommandBus\Handlers;
|
||||||
|
|
||||||
|
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\Command;
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
|
||||||
|
use Siteworxpro\App\Services\Facades\Logger;
|
||||||
|
|
||||||
|
#[HandlesCommand(ExampleCommand::class)]
|
||||||
|
class ExampleHandler extends CommandHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Command|ExampleCommand $command
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __invoke(Command|ExampleCommand $command): string
|
||||||
|
{
|
||||||
|
if (!method_exists($command, 'getName')) {
|
||||||
|
throw new \TypeError('Invalid command type provided to ExampleHandler.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $command->getName();
|
||||||
|
Logger::info('Handling ExampleCommand for name: ' . $name);
|
||||||
|
|
||||||
|
return 'Hello, ' . $name . '!';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ class HealthcheckController extends Controller
|
|||||||
'Healthcheck failed: ' . $e->getMessage(),
|
'Healthcheck failed: ' . $e->getMessage(),
|
||||||
['exception' => $e]
|
['exception' => $e]
|
||||||
);
|
);
|
||||||
|
|
||||||
return JsonResponseFactory::createJsonResponse(
|
return JsonResponseFactory::createJsonResponse(
|
||||||
new ServerErrorResponse($e),
|
new ServerErrorResponse($e),
|
||||||
CodesEnum::SERVICE_UNAVAILABLE
|
CodesEnum::SERVICE_UNAVAILABLE
|
||||||
|
|||||||
27
src/Services/Facades/CommandBus.php
Normal file
27
src/Services/Facades/CommandBus.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Services\Facades;
|
||||||
|
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\Command;
|
||||||
|
use Siteworxpro\App\Services\Facade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broker Facade
|
||||||
|
*
|
||||||
|
* @package Siteworxpro\App\Services\Facades
|
||||||
|
* @method static mixed handle(Command $command)
|
||||||
|
*/
|
||||||
|
class CommandBus extends Facade
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the registered name of the component.
|
||||||
|
*
|
||||||
|
* @return string The name of the component.
|
||||||
|
*/
|
||||||
|
protected static function getFacadeAccessor(): string
|
||||||
|
{
|
||||||
|
return \League\Tactician\CommandBus::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Services/ServiceProviders/CommandBusProvider.php
Normal file
33
src/Services/ServiceProviders/CommandBusProvider.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\App\Services\ServiceProviders;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use League\Tactician\CommandBus;
|
||||||
|
use League\Tactician\Handler\CommandHandlerMiddleware;
|
||||||
|
use League\Tactician\Handler\CommandNameExtractor\ClassNameExtractor;
|
||||||
|
use League\Tactician\Handler\MethodNameInflector\InvokeInflector;
|
||||||
|
use Siteworxpro\App\CommandBus\AttributeLocator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class CommandBusProvider
|
||||||
|
*
|
||||||
|
* @package Siteworxpro\App\Services\ServiceProviders
|
||||||
|
*/
|
||||||
|
class CommandBusProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->app->singleton(CommandBus::class, function () {
|
||||||
|
return new CommandBus([
|
||||||
|
new CommandHandlerMiddleware(
|
||||||
|
new ClassNameExtractor(),
|
||||||
|
new AttributeLocator(),
|
||||||
|
new InvokeInflector()
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
36
tests/CommandBus/AttributeLocatorTest.php
Normal file
36
tests/CommandBus/AttributeLocatorTest.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\CommandBus;
|
||||||
|
|
||||||
|
use League\Tactician\Exception\CanNotInvokeHandlerException;
|
||||||
|
use Siteworxpro\App\CommandBus\AttributeLocator;
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
|
||||||
|
use Siteworxpro\App\CommandBus\Handlers\ExampleHandler;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class AttributeLocatorTest extends Unit
|
||||||
|
{
|
||||||
|
private const array HANDLERS = [
|
||||||
|
ExampleCommand::class => ExampleHandler::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function testResolvesFiles(): void
|
||||||
|
{
|
||||||
|
$attributeLocator = new AttributeLocator();
|
||||||
|
|
||||||
|
foreach (self::HANDLERS as $command => $handler) {
|
||||||
|
$class = $attributeLocator->getHandlerForCommand($command);
|
||||||
|
$this->assertInstanceOf($handler, $class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsOnCannotResolve(): void
|
||||||
|
{
|
||||||
|
$attributeLocator = new AttributeLocator();
|
||||||
|
|
||||||
|
$this->expectException(CanNotInvokeHandlerException::class);
|
||||||
|
$attributeLocator->getHandlerForCommand('NonExistentCommand');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
tests/CommandBus/Handlers/ExampleHandlerTest.php
Normal file
32
tests/CommandBus/Handlers/ExampleHandlerTest.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\CommandBus\Handlers;
|
||||||
|
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\Command;
|
||||||
|
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
|
||||||
|
use Siteworxpro\App\CommandBus\Handlers\ExampleHandler;
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
|
||||||
|
class ExampleHandlerTest extends Unit
|
||||||
|
{
|
||||||
|
public function testExampleCommand(): void
|
||||||
|
{
|
||||||
|
$command = new ExampleCommand('test payload');
|
||||||
|
$this->assertEquals('test payload', $command->getName());
|
||||||
|
|
||||||
|
$handler = new ExampleHandler();
|
||||||
|
$result = $handler($command);
|
||||||
|
$this->assertEquals('Hello, test payload!', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsException(): void
|
||||||
|
{
|
||||||
|
$class = new readonly class extends Command
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->expectException(\TypeError::class);
|
||||||
|
$handler = new ExampleHandler();
|
||||||
|
$handler($class);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
tests/GrpcHandlers/GreeterHandlerTest.php
Normal file
23
tests/GrpcHandlers/GreeterHandlerTest.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Siteworxpro\Tests\GrpcHandlers;
|
||||||
|
|
||||||
|
use Siteworxpro\Tests\Unit;
|
||||||
|
use Spiral\RoadRunner\GRPC\ContextInterface;
|
||||||
|
|
||||||
|
class GreeterHandlerTest extends Unit
|
||||||
|
{
|
||||||
|
public function testSayHello(): void
|
||||||
|
{
|
||||||
|
$request = new \GRPC\Greeter\HelloRequest();
|
||||||
|
$request->setName('World');
|
||||||
|
|
||||||
|
$context = \Mockery::mock(ContextInterface::class);
|
||||||
|
|
||||||
|
$handler = new \Siteworxpro\App\GrpcHandlers\GreeterHandler();
|
||||||
|
$response = $handler->SayHello($context, $request);
|
||||||
|
$this->assertEquals('Hello World', $response->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user