An event-driven Slim 4 Framework skeleton using AMQP and CQRS
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
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
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');
};
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;
}
}
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.
namespace App\Domain\WriteModel\User\CreateUser;
class CreateUser extends DomainCommand
{
}
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.
}
}
The idea of this project is that everything is, or can be, event-driven. Event sourcing is not provided by default.
class UserWasCreated extends DomainEvent
{
public function __construct(
private UserId $userId,
) {
}
public function getUserId(): UserId
{
return $this->userId;
}
}
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;
}
}
class UserRepository extends DbalAggregateRootRepository
{
public function add(User $user): void
{
$this->connection->insert(/*...*/);
$this->publishEvents($user->getRecordedEvents());
}
}
#[AsEventListener(type: EventListenerType::PROCESS_MANAGER)]
class UserNotificationManager extends ConventionBasedEventListener
{
public function reactToUserWasCreated(UserWasCreated $event): void
{
// Send out some notifications.
}
}
The chosen AMQP implementation for this project is RabbitMQ, but it can be easily switched to for example Amazon's AMQP solution.
#[AsEventListener(type: EventListenerType::PROCESS_MANAGER)]
class UserCommandQueue extends CommandQueue
{
}
class YourService
{
public function __construct(
private readonly UserCommandQueue $userCommandQueue
) {
}
public function aMethod(): void
{
$this->userCommandQueue->queue(new CreateUser(/*...*/));
}
}
> docker-compose run --rm php-cli bin/console app:amqp:consume user-command-queue
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
The template engine of choice for this project is Twig and can be used to render anything HTML related.
<h1>Users</h1>
<ul>
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
</ul>
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;
}
}
Learn more at these links:
- 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
Please see CONTRIBUTING for details.