I was inspired to create this skeleton from: robiningelbrecht.
If you have problems with permissions please add sudo before make example:
sudo make install
sudo make start
make install
make start
make db-create
Please install packages makefile for Windows
make install
make start
make db-create
http://localhost
http://localhost/docs/v1
http://localhost:15672
make help
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");
};
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;
}
}
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.
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.
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.
class UserWasCreated extends DomainEvent
{
}
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.
}
}
class UserWasCreated extends DomainEvent
{
public function __construct(
private UserId $userId,
) {
}
public function getUserId(): UserId
{
return $this->userId;
}
}
The chosen AMQP implementation for this project is RabbitMQ, but it can be easily switched to for example Amazon's AMQP solution.
#[AsAmqpQueue(name: "user-command-queue", numberOfWorkers: 1)]
class UserEventQueue extends EventQueue
{
}
final readonly class UserEventsService
{
public function __construct(
private UserEventQueue $userEventQueue,
) {}
public function userWasCreated(User $user): void
{
$this->userEventQueue->queue(new UserWasCreated($user));
}
}
> docker-compose run --rm php bin/console.php app:amqp:consume user-command-queue
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.
To register a dependency or create a single configured global instance, you need to go to config/container.php
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);
},
...
];
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
To use swoole, just set DOCKER_TARGET_APP=swoole
in .env and rebuild the application container.
Learn more at these links: