You've already forked Php-Template
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e971d32f9d
|
|||
|
2a060fb972
|
|||
|
b53a95ebcf
|
|||
|
de0c95db2a
|
|||
|
cae1de6ef3
|
|||
|
84c3b392ba
|
@@ -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;'"
|
||||
|
||||
11
Dockerfile
11
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
|
||||
@@ -12,9 +12,11 @@ RUN composer install --optimize-autoloader --ignore-platform-reqs --no-dev
|
||||
|
||||
|
||||
# Use the official PHP CLI image with Alpine Linux for the second stage
|
||||
FROM siteworxpro/php:8.5.0-cli-alpine AS php
|
||||
FROM siteworxpro/php:8.5.1-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
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"react/async": "^4",
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"zircote/swagger-php": "^5.7",
|
||||
"spiral/roadrunner-grpc": "^3.5"
|
||||
"spiral/roadrunner-grpc": "^3.5",
|
||||
"league/tactician": "^1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "977f74570c671e4d59fd70d5e732c3d2",
|
||||
"content-hash": "d027bee8e875c5542f7ff9612bfac4e2",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/cli",
|
||||
@@ -1360,6 +1360,61 @@
|
||||
],
|
||||
"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",
|
||||
"version": "3.9.0",
|
||||
|
||||
27
makefile
27
makefile
@@ -43,6 +43,26 @@ help: ## Show this help
|
||||
@echo "$(SPARK) Available commands:"
|
||||
@awk -F':|##' '/^[a-zA-Z0-9._-]+:.*##/ {printf " %-$(HELP_COL_WIDTH)s - %s\n", $$1, $$3}' $(MAKEFILE_LIST) | sort
|
||||
|
||||
docker-build: ## Build Docker image
|
||||
@push_flag=""; \
|
||||
if [ -n "$(push)" ]; then \
|
||||
push="--push"; \
|
||||
fi
|
||||
@if [ -z "$(image)" ]; then \
|
||||
echo "image variable is required: make docker-build image=your-image-name tag=your"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "tag variable is required: make docker-build image=your-image-name tag=your"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@platform_flag=""; \
|
||||
if [ -n "$(platform)" ]; then \
|
||||
platform_flag="--platform=$(platform)"; \
|
||||
fi; \
|
||||
printf "$(YELLOW)$(SPARK) Building Docker image: $(image):$(tag)$(RESET)\n"; \
|
||||
docker buildx build $$platform_flag --provenance=true --sbom=true $$push_flag --tag $(image):$(tag) .
|
||||
|
||||
start: ## Start the development runtime container
|
||||
@printf "$(GREEN)$(ROCKET) Starting $(DEV_RUNTIME)$(RESET)\n"
|
||||
$(DOCKER) up $(DEV_RUNTIME) -d --no-recreate
|
||||
@@ -82,6 +102,11 @@ composer-install: ## Install PHP dependencies in the composer runtime container
|
||||
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||
$(COMPOSER) "composer install --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs"
|
||||
|
||||
composer-install-no-dev: ## Install PHP dependencies without dev packages in the composer runtime container
|
||||
@printf "$(COMPOSE) $(GREEN)Installing PHP dependencies (no-dev) in $(COMPOSER_RUNTIME)$(RESET)\n"
|
||||
@$(DOCKER) up $(COMPOSER_RUNTIME) -d --no-recreate
|
||||
$(COMPOSER) "composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-reqs"
|
||||
|
||||
composer-require: ## Require a PHP package in the composer runtime container (usage: make composer-require package=vendor/package)
|
||||
ifndef package
|
||||
$(error package variable is required: make composer-require package=vendor/package)
|
||||
@@ -159,4 +184,4 @@ ci: composer-install migrate license-check lint test ## CI-like local flow
|
||||
down: stop ## Alias for stop
|
||||
up: start ## Alias for start
|
||||
|
||||
.PONY: help start sh run stop restart rebuild ps migrate composer-install composer-require composer-require-dev composer-update enable-debug enable-coverage protoc license-check lint fmt test test-coverage dev ci down up
|
||||
.PHONY: help start sh run stop docker-build restart rebuild ps migrate composer-install composer-require composer-require-dev composer-update enable-debug enable-coverage protoc license-check lint fmt test test-coverage dev ci down up
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
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 . '!';
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ use Nyholm\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Responses\GenericResponse;
|
||||
use Siteworxpro\App\Http\Responses\ServerErrorResponse;
|
||||
use Siteworxpro\App\Models\Model;
|
||||
use Siteworxpro\App\Services\Facades\Logger;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
use OpenApi\Attributes as OA;
|
||||
@@ -43,18 +45,19 @@ class HealthcheckController extends Controller
|
||||
throw new \Exception('Redis ping failed');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Logger::emergency(
|
||||
'Healthcheck failed: ' . $e->getMessage(),
|
||||
['exception' => $e]
|
||||
);
|
||||
|
||||
return JsonResponseFactory::createJsonResponse(
|
||||
[
|
||||
'status_code' => CodesEnum::SERVICE_UNAVAILABLE->value,
|
||||
'message' => 'Healthcheck Failed',
|
||||
'error' => $e->getMessage(),
|
||||
],
|
||||
new ServerErrorResponse($e),
|
||||
CodesEnum::SERVICE_UNAVAILABLE
|
||||
);
|
||||
}
|
||||
|
||||
return JsonResponseFactory::createJsonResponse(
|
||||
new GenericResponse('Healthcheck OK', CodesEnum::OK->value)
|
||||
new GenericResponse('Healthcheck OK')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,6 @@ class UnauthorizedResponse extends OA\Response
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
properties: [
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 401),
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Unauthorized'),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,12 @@ use OpenApi\Attributes as OA;
|
||||
schema: 'GenericResponse',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Operation completed successfully.'),
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 200),
|
||||
]
|
||||
)]
|
||||
readonly class GenericResponse implements Arrayable
|
||||
{
|
||||
public function __construct(
|
||||
private string $message = '',
|
||||
private int $statusCode = 200
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -26,7 +24,6 @@ readonly class GenericResponse implements Arrayable
|
||||
{
|
||||
return [
|
||||
'message' => $this->message,
|
||||
'status_code' => $this->statusCode,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use OpenApi\Attributes as OA;
|
||||
type: 'string',
|
||||
example: 'The requested resource /api/resource was not found.'
|
||||
),
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 404),
|
||||
new OA\Property(
|
||||
property: 'context',
|
||||
description: 'Additional context about the not found error.',
|
||||
@@ -32,7 +31,6 @@ readonly class NotFoundResponse implements Arrayable
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'status_code' => CodesEnum::NOT_FOUND->value,
|
||||
'message' => 'The requested resource ' . $this->uri . ' was not found.',
|
||||
'context' => $this->context,
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ use OpenApi\Attributes as OA;
|
||||
schema: 'ServerErrorResponse',
|
||||
properties: array(
|
||||
new OA\Property(property: 'message', type: 'string', example: 'An internal server error occurred.'),
|
||||
new OA\Property(property: 'status_code', type: 'integer', example: 500),
|
||||
new OA\Property(property: 'code', type: 'integer', example: 500),
|
||||
new OA\Property(
|
||||
property: 'file',
|
||||
type: 'string',
|
||||
@@ -35,7 +35,7 @@ readonly class ServerErrorResponse implements Arrayable
|
||||
{
|
||||
if (Config::get('app.dev_mode')) {
|
||||
return [
|
||||
'status_code' => $this->e->getCode() != 0 ?
|
||||
'code' => $this->e->getCode() != 0 ?
|
||||
$this->e->getCode() :
|
||||
CodesEnum::INTERNAL_SERVER_ERROR->value,
|
||||
'message' => $this->e->getMessage(),
|
||||
@@ -47,7 +47,7 @@ readonly class ServerErrorResponse implements Arrayable
|
||||
}
|
||||
|
||||
return [
|
||||
'status_code' => $this->e->getCode() != 0 ?
|
||||
'code' => $this->e->getCode() != 0 ?
|
||||
$this->e->getCode() :
|
||||
CodesEnum::INTERNAL_SERVER_ERROR->value,
|
||||
'message' => 'An internal server error occurred.',
|
||||
|
||||
@@ -10,6 +10,7 @@ use Siteworxpro\App\Services\Facade;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Dispatcher;
|
||||
use Siteworxpro\App\Services\ServiceProviders\BrokerServiceProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\CommandBusProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\DispatcherServiceProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\LoggerServiceProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\RedisServiceProvider;
|
||||
@@ -33,7 +34,8 @@ class Kernel
|
||||
LoggerServiceProvider::class,
|
||||
RedisServiceProvider::class,
|
||||
DispatcherServiceProvider::class,
|
||||
BrokerServiceProvider::class
|
||||
BrokerServiceProvider::class,
|
||||
CommandBusProvider::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
38
src/Services/ServiceProviders/CommandBusProvider.php
Normal file
38
src/Services/ServiceProviders/CommandBusProvider.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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 provides(): array
|
||||
{
|
||||
return [CommandBus::class];
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(CommandBus::class, function () {
|
||||
return new CommandBus([
|
||||
new CommandHandlerMiddleware(
|
||||
new ClassNameExtractor(),
|
||||
new AttributeLocator(),
|
||||
new InvokeInflector()
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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","status_code":200}', (string)$response->getBody());
|
||||
$this->assertEquals('{"message":"Server is running. Hello World"}', (string)$response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,6 +43,6 @@ class IndexControllerTest extends AbstractController
|
||||
$response = $controller->post($this->getMockRequest());
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('{"message":"POST request received","status_code":200}', (string)$response->getBody());
|
||||
$this->assertEquals('{"message":"POST request received"}', (string)$response->getBody());
|
||||
}
|
||||
}
|
||||
|
||||
36
tests/GrpcHandlers/GreeterHandlerTest.php
Normal file
36
tests/GrpcHandlers/GreeterHandlerTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
$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 GreeterHandler();
|
||||
$response = $handler->SayHello($context, $request);
|
||||
$this->assertEquals('Hello World', $response->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ class NotFoundResponseTest extends Unit
|
||||
$response = new NotFoundResponse('/api/resource', ['key' => 'value']);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 404,
|
||||
'message' => 'The requested resource /api/resource was not found.',
|
||||
'context' => ['key' => 'value'],
|
||||
];
|
||||
|
||||
@@ -19,7 +19,7 @@ class ServerErrorResponseTest extends Unit
|
||||
$response = new ServerErrorResponse($e, ['operation' => 'data_processing']);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 500,
|
||||
'code' => 500,
|
||||
'message' => 'A Test Error occurred.',
|
||||
'context' => [
|
||||
'operation' => 'data_processing'
|
||||
@@ -35,13 +35,15 @@ class ServerErrorResponseTest extends Unit
|
||||
|
||||
public function testToArrayNotInDevMode(): void
|
||||
{
|
||||
Config::set('app.dev_mode', false);
|
||||
|
||||
try {
|
||||
throw new \Exception('A Test Error occurred.');
|
||||
} catch (\Exception $exception) {
|
||||
$response = new ServerErrorResponse($exception);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 500,
|
||||
'code' => 500,
|
||||
'message' => 'An internal server error occurred.',
|
||||
];
|
||||
|
||||
@@ -51,13 +53,15 @@ class ServerErrorResponseTest extends Unit
|
||||
|
||||
public function testToArrayIfCodeIsSet(): void
|
||||
{
|
||||
Config::set('app.dev_mode', false);
|
||||
|
||||
try {
|
||||
throw new \Exception('A Test Error occurred.', 1234);
|
||||
} catch (\Exception $exception) {
|
||||
$response = new ServerErrorResponse($exception);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 1234,
|
||||
'code' => 1234,
|
||||
'message' => 'An internal server error occurred.',
|
||||
];
|
||||
|
||||
@@ -75,7 +79,7 @@ class ServerErrorResponseTest extends Unit
|
||||
$response = new ServerErrorResponse($exception);
|
||||
|
||||
$expected = [
|
||||
'status_code' => 1234,
|
||||
'code' => 1234,
|
||||
'message' => 'A Test Error occurred.',
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
|
||||
@@ -9,7 +9,9 @@ use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
use RoadRunner\Logger\Logger as RRLogger;
|
||||
use Siteworxpro\App\Log\Logger;
|
||||
use Siteworxpro\App\Services\Facades\Logger as LoggerFacade;
|
||||
use Siteworxpro\Tests\Unit;
|
||||
|
||||
class LoggerRpcTest extends Unit
|
||||
@@ -36,8 +38,8 @@ class LoggerRpcTest extends Unit
|
||||
->with('message', ['key' => 'value'])
|
||||
->times(1);
|
||||
|
||||
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
|
||||
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
|
||||
LoggerFacade::getFacadeContainer()
|
||||
->bind(RRLogger::class, function () use ($mock) {
|
||||
return $mock;
|
||||
});
|
||||
|
||||
@@ -63,8 +65,8 @@ class LoggerRpcTest extends Unit
|
||||
->with('message', ['key' => 'value'])
|
||||
->times(2);
|
||||
|
||||
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
|
||||
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
|
||||
LoggerFacade::getFacadeContainer()
|
||||
->bind(RRLogger::class, function () use ($mock) {
|
||||
return $mock;
|
||||
});
|
||||
|
||||
@@ -91,8 +93,8 @@ class LoggerRpcTest extends Unit
|
||||
->with('message', ['key' => 'value'])
|
||||
->times(1);
|
||||
|
||||
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
|
||||
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
|
||||
LoggerFacade::getFacadeContainer()
|
||||
->bind(RRLogger::class, function () use ($mock) {
|
||||
return $mock;
|
||||
});
|
||||
|
||||
@@ -118,8 +120,8 @@ class LoggerRpcTest extends Unit
|
||||
->with('message', ['key' => 'value'])
|
||||
->times(4);
|
||||
|
||||
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
|
||||
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
|
||||
LoggerFacade::getFacadeContainer()
|
||||
->bind(RRLogger::class, function () use ($mock) {
|
||||
return $mock;
|
||||
});
|
||||
|
||||
@@ -147,8 +149,8 @@ class LoggerRpcTest extends Unit
|
||||
$mock->expects('log')
|
||||
->with('notaloglevel', 'message', ['key' => 'value']);
|
||||
|
||||
\Siteworxpro\App\Services\Facades\Logger::getFacadeContainer()
|
||||
->bind(\RoadRunner\Logger\Logger::class, function () use ($mock) {
|
||||
LoggerFacade::getFacadeContainer()
|
||||
->bind(RRLogger::class, function () use ($mock) {
|
||||
return $mock;
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
tests/ServiceProviders/CommandBusServiceProviderTest.php
Normal file
15
tests/ServiceProviders/CommandBusServiceProviderTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user