mrhdolek / slim4-boirlerplate
An Slim 4 Framework skeleton using AMQP,DDD,Doctrine
Requires
- php: ^8.2
- ext-json: *
- ext-pcntl: *
- ext-sockets: *
- awurth/slim-validation: ^5.0
- doctrine/data-fixtures: ^1.6
- doctrine/migrations: ^3.6
- doctrine/orm: ^2.15
- fakerphp/faker: ^1.23
- laminas/laminas-diactoros: ^3.3
- lcobucci/clock: ^3.1
- monolog/monolog: ^2.8
- peppeocchi/php-cron-scheduler: ^4.0
- php-amqplib/php-amqplib: ^3.2
- php-di/php-di: ^7.0
- php-di/slim-bridge: ^3.4
- predis/predis: ^2.2
- ramsey/uuid: ^4.7
- slim/psr7: ^1.5
- slim/slim: ^4.10
- slim/twig-view: ^3.3
- swoole/ide-helper: ^5.1
- symfony/cache: ^6.3
- symfony/console: ^6.3
- symfony/doctrine-bridge: ^6.3
- symfony/finder: ^6.3
- symfony/yaml: ^6.2
- thecodingmachine/safe: ^2.5
- vlucas/phpdotenv: ^5.5
- zircote/swagger-php: ^4.7
Requires (Dev)
- blumilksoftware/codestyle: *
- jangregor/phpstan-prophecy: ^1.0.0
- league/openapi-psr7-validator: ^0.22.0
- phpspec/prophecy-phpunit: ^2.0
- phpstan/extension-installer: ^1.2.0
- phpstan/phpstan: ^1.8
- phpunit/phpunit: ^9.5.26
- qossmic/deptrac-shim: ^1.0
- spatie/phpunit-snapshot-assertions: ^4.2
This package is auto-updated.
Last update: 2025-02-18 14:40:52 UTC
README
An Slim 4 Framework skeleton using AMQP and DDD
I was inspired to create this skeleton from: robiningelbrecht.
Project setup
Development
If you have problems with permissions please add sudo before make example:
sudo make install
sudo make start
Run env for Mac/Linux
make install
make start
make db-create
Run env for Windows
Please install packages makefile for Windows
make install
make start
make db-create
Address where the environment is available
http://localhost
Documentation for a Rest Api
http://localhost/docs/v1
RabbitMq dashboard
http://localhost:15672
All commands
make help
Some examples
Registering a new route
namespace App\Application\Actions\User; class GetAllUsersAction extends UserAction { public function __construct( private readonly UserService $userService, protected LoggerInterface $logger, ) { parent::__construct($logger); } protected function action(): Response { $user = $this->userService->getAllUsers(); return $this->respondWithJson(new UsersResponseDto($user)); } }
Head over to config/routes.php
and add a route for your RequestHandler:
return function (App $app) { $group->get("/users", GetAllUsersAction::class) ->setName("getAllUsers"); };
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; } }
Scheduling Commands
To schedule a command, we use the GO\Scheduler class. This class allows us to define the timing and frequency of command execution. Here's an example of how to schedule a command to run daily:
$scheduler = new GO\Scheduler(); $scheduler->php('/path/to/command app:user:create')->daily(); $scheduler->run();
In this example, the app:user:create command is scheduled to run every day.
Running the Scheduler
The scheduler should be triggered by a system cron job to ensure it runs at regular intervals. Typically, you would set up a cron job to execute a PHP script that initializes and runs the scheduler.
For instance, a cron job running every minute might look like this:
* * * * * ./bin/console.php schedule
This setup ensures that your scheduled commands are executed reliably and on time.
Domain event and event handlers
The framework implements the amqp protocol with handlers that allow events to be easily pushed onto the queue. Each event must have a handler implemented that consumes the event.
Creating a new event
class UserWasCreated extends DomainEvent { }
Creating the corresponding event handler
namespace App\Domain\Entity\User\DomainEvents; #[AsEventHandler] class UserWasCreatedEventHandler implements EventHandler { public function __construct( ) { } public function handle(DomainEvent $event): void { assert($event instanceof UserWasCreated); // Do stuff. } }
Eventing
Create a new event
class UserWasCreated extends DomainEvent { public function __construct( private UserId $userId, ) { } public function getUserId(): UserId { return $this->userId; } }
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
#[AsAmqpQueue(name: "user-command-queue", numberOfWorkers: 1)] class UserEventQueue extends EventQueue { }
Queueing events
final readonly class UserEventsService { public function __construct( private UserEventQueue $userEventQueue, ) {} public function userWasCreated(User $user): void { $this->userEventQueue->queue(new UserWasCreated($user)); } }
Consuming your queue
> docker-compose run --rm php bin/console.php app:amqp:consume user-command-queue
Create new entity
If you have created a new entity and want to map it to a database you must create a xml in src/Infrastructure/Persistence/Doctrine/Mapping . It must be named so as to indicate where exactly the entity to be mapped is located.
Binding of interfaces/Registry global objects
To register a dependency or create a single configured global instance, you need to go to config/container.php
Mapping database data to custom objects
To map data from a database to a custom object you need to extend something from Doctrine/DBAL/Types .
use App\Domain\ValueObject\UserType; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\StringType; class UserTypeType extends StringType { const TYPE_NAME = 'UserType'; public function convertToPHPValue($value, AbstractPlatform $platform): ?UserType { return null !== $value ? UserType::fromString($value) : null; } public function getName() { return self::TYPE_NAME; } }
After extending, you need to add a note in the xml that you are mapping a field to an object.
<field name="type" type="UserType" column="type" nullable="true"/>
Finally, you must add the new type in config/container.php where doctrine is configured
use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Tools\Setup; return [ ... EntityManager::class => function (Settings $settings): EntityManager { $config = Setup::createXMLMetadataConfiguration( $settings->get("doctrine.metadata_dirs"), $settings->get("doctrine.dev_mode"), ); if (!Type::hasType('UserType')) { Type::addType('UserType', UserTypeType::class); } return EntityManager::create($settings->get("doctrine.connection"), $config); }, ... ];
Database migrations
To manage database migrations, the doctrine/migrations package is used.
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> <entity name="App\Domain\Entity\User\User" table="users"> <id name="id" type="integer" column="id"> <generator strategy="SEQUENCE"/> <sequence-generator sequence-name="user_id_seq" allocation-size="1" initial-value="1"/> </id> <field name="username" type="string" column="name" length="64" nullable="true"/> <field name="firstName" type="string" column="surname" length="64" nullable="true"/> <field name="lastName" type="string" column="email" length="64" nullable="true"/> <options> <option name="collate">utf8mb4_polish_ci</option> </options> </entity> </doctrine-mapping>
The mapping is done using a yaml which maps your entities from the domain to a structure in the database . If you change something in yaml, you can use the commands below to generate a migration based on the difference.
> docker-compose run --rm php vendor/bin/doctrine-migrations diff > docker-compose run --rm php vendor/bin/doctrine-migrations migrate
Swoole
To use swoole, just set DOCKER_TARGET_APP=swoole
in .env and rebuild the application container.
Documentations
Learn more at these links: