robiningelbrecht / php-slim-skeleton
Requires
- php: ^8.2
- ext-json: *
- ext-pcntl: *
- ext-sockets: *
- doctrine/dbal: ^3.6
- doctrine/migrations: ^3.5
- doctrine/orm: ^2.12
- filp/whoops: ^2.15
- lcobucci/clock: ^2.2
- php-amqplib/php-amqplib: ^3.2
- php-di/php-di: ^7.0
- php-di/slim-bridge: ^3.3
- ramsey/uuid: ^4.7
- slim/psr7: ^1.6
- slim/slim: ^4.11
- symfony/cache: ^6.1
- symfony/console: ^6.1
- symfony/finder: ^6.1
- thecodingmachine/safe: ^2.2
- twig/twig: ^3.4
- vlucas/phpdotenv: ^5.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.16
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.1
- robiningelbrecht/phpunit-pretty-print: ^1.2.0
- spatie/phpunit-snapshot-assertions: ^5.0
This package is auto-updated.
Last update: 2024-10-29 15:46:19 UTC
README
An event-driven Slim 4 Framework skeleton using AMQP and CQRS
Installation
Default installation profile
The default installation profile has no examples. You should be using this profile if you know what's up and want to start with a clean slate.
> composer create-project robiningelbrecht/php-slim-skeleton [app-name] --no-install --ignore-platform-reqs --stability=dev # Build docker containers > docker-compose up -d --build # Install dependencies > docker-compose run --rm php-cli composer install
Full installation profile
The full installation profile has a complete working example.
> composer create-project robiningelbrecht/php-slim-skeleton:dev-master-with-examples [app-name] --no-install --ignore-platform-reqs --stability=dev # Build docker containers > docker-compose up -d --build # Install dependencies > docker-compose run --rm php-cli composer install # Initialize example > docker-compose run --rm php-cli composer example:init # Start consuming the voting example queue > docker-compose run --rm php-cli bin/console app:amqp:consume add-vote-command-queue
Some examples
Registering a new route
namespace App\Controller; class UserOverviewRequestHandler { public function __construct( private readonly UserOverviewRepository $userOverviewRepository, ) { } public function handle( ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $users = $this->userOverviewRepository->findonyBy(/*...*/); $response->getBody()->write(/*...*/); return $response; } }
Head over to config/routes.php
and add a route for your RequestHandler:
return function (App $app) { // Set default route strategy. $routeCollector = $app->getRouteCollector(); $routeCollector->setDefaultInvocationStrategy(new RequestResponseArgs()); $app->get('/user/overview', UserOverviewRequestHandler::class.':handle'); };
Console commands
The console application uses the Symfony console component to leverage CLI functionality.
#[AsCommand(name: 'app:user:create')] class CreateUserConsoleCommand extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { // ... return Command::SUCCESS; } }
Domain commands and command handlers
The skeleton allows you to use commands and command handlers to perform actions. These 2 always come in pairs, when creating a new command in the write model, a corresponding command handler has to be created as well.
Creating a new command
namespace App\Domain\WriteModel\User\CreateUser; class CreateUser extends DomainCommand { }
Creating the corresponding command handler
namespace App\Domain\WriteModel\User\CreateUser; #[AsCommandHandler] class CreateUserCommandHandler implements CommandHandler { public function __construct( ) { } public function handle(DomainCommand $command): void { assert($command instanceof CreateUser); // Do stuff. } }
Eventing
The idea of this project is that everything is, or can be, event-driven. Event sourcing is not provided by default.
Create a new event
class UserWasCreated extends DomainEvent { public function __construct( private UserId $userId, ) { } public function getUserId(): UserId { return $this->userId; } }
Record the event
class User extends AggregateRoot { private function __construct( private UserId $userId, ) { } public static function create( UserId $userId, ): self { $user = new self($userId); $user->recordThat(new UserWasCreated($userId)); return $user; } }
Publish the event
class UserRepository extends DbalAggregateRootRepository { public function add(User $user): void { $this->connection->insert(/*...*/); $this->publishEvents($user->getRecordedEvents()); } }
Listen to the event
#[AsEventListener(type: EventListenerType::PROCESS_MANAGER)] class UserNotificationManager extends ConventionBasedEventListener { public function reactToUserWasCreated(UserWasCreated $event): void { // Send out some notifications. } }
Async processing of commands with RabbitMQ
The chosen AMQP implementation for this project is RabbitMQ, but it can be easily switched to for example Amazon's AMQP solution.
Registering new queues
#[AsEventListener(type: EventListenerType::PROCESS_MANAGER)] class UserCommandQueue extends CommandQueue { }
Queueing commands
class YourService { public function __construct( private readonly UserCommandQueue $userCommandQueue ) { } public function aMethod(): void { $this->userCommandQueue->queue(new CreateUser(/*...*/)); } }
Consuming your queue
> docker-compose run --rm php-cli bin/console app:amqp:consume user-command-queue
Database migrations
To manage database migrations, the doctrine/migrations package is used.
#[Entity] class User extends AggregateRoot { private function __construct( #[Id, Column(type: 'string', unique: true, nullable: false)] private readonly UserId $userId, #[Column(type: 'string', nullable: false)] private readonly Name $name, ) { } // ... }
You can have Doctrine generate a migration for you by comparing the current state of your database schema to the mapping information that is defined by using the ORM and then execute that migration.
> docker-compose run --rm php-cli vendor/bin/doctrine-migrations diff > docker-compose run --rm php-cli vendor/bin/doctrine-migrations migrate
Templating engine
The template engine of choice for this project is Twig and can be used to render anything HTML related.
Create a template
<h1>Users</h1> <ul> {% for user in users %} <li>{{ user.username|e }}</li> {% endfor %} </ul>
Render the template
class UserOverviewRequestHandler { public function __construct( private readonly Environment $twig, ) { } public function handle( ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { $template = $this->twig->load('users.html.twig'); $response->getBody()->write($template->render(/*...*/)); return $response; } }
Documentation
Learn more at these links:
Projects using this skeleton
- Unofficial World Cube Association (WCA) Public API
- Database of newly generated Pokemon cards using GPT and Stable Diffusion
- A PHP app that generates Pokemon cards by using GPT and Stable Diffusion
- Generate Full 3D pictures of a Rubiks cube
Contributing
Please see CONTRIBUTING for details.