feat: enhance service providers with provides method and integrate command bus in handlers
All checks were successful
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m40s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m42s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m53s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 2m55s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 2m42s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in 1m24s

This commit is contained in:
2025-12-22 13:18:13 -05:00
parent cae1de6ef3
commit de0c95db2a
17 changed files with 107 additions and 60 deletions

View File

@@ -101,7 +101,9 @@ services:
- ..:/app
build:
args:
KAFKA_ENABLED: "1"
KAFKA_ENABLED: "0"
UID: 0
USER: root
context: ..
dockerfile: Dockerfile
entrypoint: "/bin/sh -c 'while true; do sleep 30; done;'"

View File

@@ -1,5 +1,5 @@
# Use the RoadRunner image as a base for the first stage
FROM ghcr.io/roadrunner-server/roadrunner:2025.1.4 AS roadrunner
FROM ghcr.io/roadrunner-server/roadrunner:2025.1.6 AS roadrunner
# Use the official Composer image as the base for the library stage
FROM siteworxpro/composer AS library
@@ -15,6 +15,8 @@ RUN composer install --optimize-autoloader --ignore-platform-reqs --no-dev
FROM siteworxpro/php:8.5.0-cli-alpine AS php
ARG KAFKA_ENABLED=0
ARG USER=appuser
ARG UID=1000
# Move the production PHP configuration file to the default location
RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
@@ -49,8 +51,9 @@ ADD server.php cli.php grpc-worker.php .rr.yaml config.php ./
EXPOSE 9501 9001
RUN addgroup -g 1000 appuser && adduser -D -u 1000 -G appuser appuser && chown -R appuser:appuser /app
USER appuser
# Create a non-root user and set ownership of the /app directory
RUN if [ ! $UID -eq 0 ] ; then addgroup -g $UID $USER && adduser -D -u $UID -G $USER $USER && chown -R $USER:$USER /app ; fi
USER $USER
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD curl -f http://localhost:9501/healthz || exit 1

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands;
use Ahc\Cli\Input\Command;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus;
class DemoCommand extends Command implements CommandInterface
{
@@ -28,18 +30,14 @@ class DemoCommand extends Command implements CommandInterface
$pb->finish();
$this->writer()->boldBlue("Demo Command Executed!\n");
if ($this->values()['name']) {
$name = $this->values()['name'];
$greet = $this->values()['greet'] ?? false;
} else {
return 0;
}
if ($greet) {
$this->writer()->green("Hello, $name! Welcome to the CLI demo.\n");
} else {
$this->writer()->yellow("Name provided: {$name}\n");
$exampleCommand = new ExampleCommand($name);
$this->writer()->yellow(CommandBus::handle($exampleCommand));
}
return 0;

View File

@@ -7,11 +7,13 @@ namespace Siteworxpro\App\Controllers;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Siteworxpro\App\Attributes\Guards;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Docs\TokenSecurity;
use Siteworxpro\App\Docs\UnauthorizedResponse;
use Siteworxpro\App\Http\JsonResponseFactory;
use OpenApi\Attributes as OA;
use Siteworxpro\App\Http\Responses\GenericResponse;
use Siteworxpro\App\Services\Facades\CommandBus;
/**
* Class IndexController
@@ -37,7 +39,10 @@ class IndexController extends Controller
#[UnauthorizedResponse]
public function get(ServerRequest $request): ResponseInterface
{
return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running'));
$command = new ExampleCommand($request->getQueryParams()['name'] ?? 'Guest');
$greeting = CommandBus::handle($command);
return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running. ' . $greeting));
}
/**

View File

@@ -7,14 +7,18 @@ namespace Siteworxpro\App\GrpcHandlers;
use GRPC\Greeter\GreeterInterface;
use GRPC\Greeter\HelloReply;
use GRPC\Greeter\HelloRequest;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus;
use Spiral\RoadRunner\GRPC;
class GreeterHandler implements GreeterInterface
{
public function SayHello(GRPC\ContextInterface $ctx, HelloRequest $in): HelloReply // phpcs:ignore
{
$command = new ExampleCommand($in->getName());
$reply = new HelloReply();
$reply->setMessage('Hello ' . $in->getName());
$reply->setMessage(CommandBus::handle($command));
return $reply;
}

View File

@@ -55,24 +55,6 @@ class Facade
});
}
/**
* Convert the facade into a Mockery spy.
*
* @return HigherOrderTapProxy | MockInterface
*/
public static function spy(): HigherOrderTapProxy | MockInterface
{
if (! static::isMock()) {
$class = static::getMockableClass();
return tap($class ? Mockery::spy($class) : Mockery::spy(), function ($spy) {
static::swap($spy);
});
}
throw new \RuntimeException('Cannot spy on an existing mock instance.');
}
/**
* Initiate a partial mock on the facade.
*
@@ -189,19 +171,6 @@ class Facade
}
}
/**
* Determines whether a "fake" has been set as the facade instance.
*
* @return bool
*/
public static function isFake(): bool
{
$name = static::getFacadeAccessor();
return isset(static::$resolvedInstance[$name]) &&
static::$resolvedInstance[$name] instanceof Fake;
}
/**
* Get the root object behind the facade.
*

View File

@@ -6,6 +6,10 @@ namespace Siteworxpro\App\Services\ServiceProviders;
use Illuminate\Support\ServiceProvider;
use Siteworxpro\App\Async\Brokers\Broker;
use Siteworxpro\App\Async\Brokers\Kafka;
use Siteworxpro\App\Async\Brokers\RabbitMQ;
use Siteworxpro\App\Async\Brokers\Redis;
use Siteworxpro\App\Async\Brokers\Sqs;
use Siteworxpro\App\Services\Facades\Config;
/**
@@ -18,6 +22,11 @@ use Siteworxpro\App\Services\Facades\Config;
*/
class BrokerServiceProvider extends ServiceProvider
{
public function provides(): array
{
return [Kafka::class, RabbitMQ::class, Redis::class, Sqs::class];
}
/**
* Register services.
*

View File

@@ -18,6 +18,11 @@ use Siteworxpro\App\CommandBus\AttributeLocator;
*/
class CommandBusProvider extends ServiceProvider
{
public function provides(): array
{
return [CommandBus::class];
}
public function register(): void
{
$this->app->singleton(CommandBus::class, function () {

View File

@@ -14,6 +14,11 @@ use Siteworxpro\App\Events\Dispatcher;
*/
class DispatcherServiceProvider extends ServiceProvider
{
public function provides(): array
{
return [Dispatcher::class];
}
public function register(): void
{
$this->app->singleton(Dispatcher::class, function () {

View File

@@ -15,6 +15,11 @@ use Siteworxpro\App\Services\Facades\Config;
*/
class LoggerServiceProvider extends ServiceProvider
{
public function provides(): array
{
return [Logger::class];
}
public function register(): void
{
$this->app->singleton(Logger::class, function () {

View File

@@ -17,6 +17,11 @@ use Siteworxpro\App\Services\Facades\Config;
*/
class RedisServiceProvider extends ServiceProvider
{
public function provides(): array
{
return [Client::class];
}
public function register(): void
{
$this->app->singleton(Client::class, function () {

View File

@@ -4,23 +4,31 @@ declare(strict_types=1);
namespace Siteworxpro\Tests\Controllers;
use League\Tactician\CommandBus;
use Siteworxpro\App\Controllers\IndexController;
class IndexControllerTest extends AbstractController
{
/**
* @throws \JsonException
* @throws \JsonException|\ReflectionException
*/
public function testGet(): void
{
$this->assertTrue(true);
$this->getContainer()->bind(CommandBus::class, function () {
return \Mockery::mock(CommandBus::class)
->shouldReceive('handle')
->andReturn('Hello World')
->getMock();
});
$controller = new IndexController();
$response = $controller->get($this->getMockRequest());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"message":"Server is running"}', (string)$response->getBody());
$this->assertEquals('{"message":"Server is running. Hello World"}', (string)$response->getBody());
}
/**

View File

@@ -4,19 +4,32 @@ declare(strict_types=1);
namespace Siteworxpro\Tests\GrpcHandlers;
use GRPC\Greeter\HelloRequest;
use League\Tactician\CommandBus;
use Siteworxpro\App\GrpcHandlers\GreeterHandler;
use Siteworxpro\Tests\Unit;
use Spiral\RoadRunner\GRPC\ContextInterface;
class GreeterHandlerTest extends Unit
{
/**
* @throws \ReflectionException
*/
public function testSayHello(): void
{
$request = new \GRPC\Greeter\HelloRequest();
$this->getContainer()->bind(CommandBus::class, function () {
return \Mockery::mock(CommandBus::class)
->shouldReceive('handle')
->andReturn('Hello World')
->getMock();
});
$request = new HelloRequest();
$request->setName('World');
$context = \Mockery::mock(ContextInterface::class);
$handler = new \Siteworxpro\App\GrpcHandlers\GreeterHandler();
$handler = new GreeterHandler();
$response = $handler->SayHello($context, $request);
$this->assertEquals('Hello World', $response->getMessage());
}

View File

@@ -28,8 +28,9 @@ abstract class AbstractServiceProvider extends Unit
$this->assertInstanceOf($providerClass, $provider);
$provider->register();
$bindings = $provider->bindings;
foreach ($bindings as $abstract => $concrete) {
$abstract = $provider->provides()[0];
$concrete = get_class($container->make($abstract));
$this->assertTrue($container->bound($abstract), "The $abstract is not bound in the container.");
$this->assertNotNull($container->make($abstract), "The $abstract could not be resolved.");
@@ -40,4 +41,3 @@ abstract class AbstractServiceProvider extends Unit
);
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\ServiceProviders;
use Siteworxpro\App\Services\ServiceProviders\CommandBusProvider;
class CommandBusServiceProviderTest extends AbstractServiceProvider
{
protected function getProviderClass(): string
{
return CommandBusProvider::class;
}
}

View File

@@ -8,6 +8,7 @@ use Illuminate\Container\Container;
use Mockery;
use PHPUnit\Framework\TestCase;
use Siteworx\Config\Config as SWConfig;
use Siteworxpro\App\Kernel;
use Siteworxpro\App\Services\Facade;
use Siteworxpro\App\Services\Facades\Config;