diff --git a/.dev/docker-compose.yml b/.dev/docker-compose.yml index 3c6e0ef..32f6ac5 100644 --- a/.dev/docker-compose.yml +++ b/.dev/docker-compose.yml @@ -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;'" diff --git a/Dockerfile b/Dockerfile index 7a0ff70..df8bf9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/makefile b/makefile index 693603f..2354957 100644 --- a/makefile +++ b/makefile @@ -45,7 +45,7 @@ help: ## Show this help start: ## Start the development runtime container @printf "$(GREEN)$(ROCKET) Starting $(DEV_RUNTIME)$(RESET)\n" - $(DOCKER) up $(DEV_RUNTIME) -d --no-recreate + $(DOCKER) up $(DEV_RUNTIME) -d --no-recreate sh: ## Open a shell in the development runtime container @$(MAKE) start diff --git a/src/Cli/Commands/DemoCommand.php b/src/Cli/Commands/DemoCommand.php index c4a66c9..7742a73 100644 --- a/src/Cli/Commands/DemoCommand.php +++ b/src/Cli/Commands/DemoCommand.php @@ -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; - } + $name = $this->values()['name']; + $greet = $this->values()['greet'] ?? false; 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; diff --git a/src/Controllers/IndexController.php b/src/Controllers/IndexController.php index e9a9059..51385bf 100644 --- a/src/Controllers/IndexController.php +++ b/src/Controllers/IndexController.php @@ -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)); } /** diff --git a/src/GrpcHandlers/GreeterHandler.php b/src/GrpcHandlers/GreeterHandler.php index 72028d7..3c124ab 100644 --- a/src/GrpcHandlers/GreeterHandler.php +++ b/src/GrpcHandlers/GreeterHandler.php @@ -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; } diff --git a/src/Services/Facade.php b/src/Services/Facade.php index ffae566..e9e883f 100644 --- a/src/Services/Facade.php +++ b/src/Services/Facade.php @@ -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. * diff --git a/src/Services/ServiceProviders/BrokerServiceProvider.php b/src/Services/ServiceProviders/BrokerServiceProvider.php index 28db53b..03d4d13 100644 --- a/src/Services/ServiceProviders/BrokerServiceProvider.php +++ b/src/Services/ServiceProviders/BrokerServiceProvider.php @@ -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. * diff --git a/src/Services/ServiceProviders/CommandBusProvider.php b/src/Services/ServiceProviders/CommandBusProvider.php index 52ea31d..93567b8 100644 --- a/src/Services/ServiceProviders/CommandBusProvider.php +++ b/src/Services/ServiceProviders/CommandBusProvider.php @@ -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 () { diff --git a/src/Services/ServiceProviders/DispatcherServiceProvider.php b/src/Services/ServiceProviders/DispatcherServiceProvider.php index 081ab11..43435d0 100644 --- a/src/Services/ServiceProviders/DispatcherServiceProvider.php +++ b/src/Services/ServiceProviders/DispatcherServiceProvider.php @@ -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 () { diff --git a/src/Services/ServiceProviders/LoggerServiceProvider.php b/src/Services/ServiceProviders/LoggerServiceProvider.php index 1745bdb..8285ea3 100644 --- a/src/Services/ServiceProviders/LoggerServiceProvider.php +++ b/src/Services/ServiceProviders/LoggerServiceProvider.php @@ -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 () { diff --git a/src/Services/ServiceProviders/RedisServiceProvider.php b/src/Services/ServiceProviders/RedisServiceProvider.php index 748deb8..555e84d 100644 --- a/src/Services/ServiceProviders/RedisServiceProvider.php +++ b/src/Services/ServiceProviders/RedisServiceProvider.php @@ -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 () { diff --git a/tests/Controllers/IndexControllerTest.php b/tests/Controllers/IndexControllerTest.php index 47d03b5..842c55d 100644 --- a/tests/Controllers/IndexControllerTest.php +++ b/tests/Controllers/IndexControllerTest.php @@ -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()); } /** diff --git a/tests/GrpcHandlers/GreeterHandlerTest.php b/tests/GrpcHandlers/GreeterHandlerTest.php index f0d5a76..c507870 100644 --- a/tests/GrpcHandlers/GreeterHandlerTest.php +++ b/tests/GrpcHandlers/GreeterHandlerTest.php @@ -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()); } diff --git a/tests/ServiceProviders/AbstractServiceProvider.php b/tests/ServiceProviders/AbstractServiceProvider.php index a5e8c59..ffd7f38 100644 --- a/tests/ServiceProviders/AbstractServiceProvider.php +++ b/tests/ServiceProviders/AbstractServiceProvider.php @@ -28,16 +28,16 @@ abstract class AbstractServiceProvider extends Unit $this->assertInstanceOf($providerClass, $provider); $provider->register(); - $bindings = $provider->bindings; - foreach ($bindings as $abstract => $concrete) { - $this->assertTrue($container->bound($abstract), "The $abstract is not bound in the container."); - $this->assertNotNull($container->make($abstract), "The $abstract could not be resolved."); + $abstract = $provider->provides()[0]; + $concrete = get_class($container->make($abstract)); - $this->assertInstanceOf( - $concrete, - $container->make($abstract), - "The $abstract is not an instance of $concrete." - ); - } + $this->assertTrue($container->bound($abstract), "The $abstract is not bound in the container."); + $this->assertNotNull($container->make($abstract), "The $abstract could not be resolved."); + + $this->assertInstanceOf( + $concrete, + $container->make($abstract), + "The $abstract is not an instance of $concrete." + ); } } diff --git a/tests/ServiceProviders/CommandBusServiceProviderTest.php b/tests/ServiceProviders/CommandBusServiceProviderTest.php new file mode 100644 index 0000000..f899bc2 --- /dev/null +++ b/tests/ServiceProviders/CommandBusServiceProviderTest.php @@ -0,0 +1,15 @@ +