Krótka historia o tym jak projektuję aplikacje przy pomocy TDD
Kilka słów o mnie
@l3l0
Powiedzmy że chcemy zarejestrować użytkownika
Jak byście zaimplementowali funkcje tworzenia użytkownika w systemie?
<?php
declare(strict_types=1);
namespace Cocoders\SymfonyLiveWarsaw;
class User
{
private $id;
private $email;
private $password;
private $firstName;
private $lastName;
public function id(): int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(string $firstName): void
{
$this->firstName = $firstName;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(string $lastName): void
{
$this->lastName = $lastName;
}
}
DAO - gdzie problem?
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends AbstractController
{
/**
* @Route("/users", name="users", methods={"GET", "POST"})
*/
public function registerUser(Request $request, UserPasswordEncoder $encoder): void
{
$form = $this->createForm(RegisterUserType::class, new User());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$user->setPassword($encoder->encodePassword($user, $user->getPlainPassword()));
$this->getDoctrine()->persist($user);
$this->getDoctrine()->flush();
return $this->redirectToRoute('user', ['id' => $user->getId()]);
}
return $this->render();
}
}
<?php
class UserController extends AbstractController
{
/**
* @Route("/users", name="users", methods={"GET", "POST"})
*/
public function registerUser(Request $request): void
{
$form = $this->createForm(RegisterUserType::class, new User());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($this->getDoctrine()->getRepository(User::class)
->findOneByEmail($form->getData()->getEmail())) {
// form error
return $this->render();
}
$user = $form->getData();
$user->setPassword($passwordEncoder->encodePassword($user, $user->getPlainPassword());
$this->getDoctrine()->persist($user);
$this->getDoctrine()->flush();
// additional logic
if ($this->someChecks) {
$this->emailSender->send();
}
$this->firestoreUpdater->update();
return $this->redirectToRoute('user', ['id' => $user->getId()]);
}
return $this->render();
}
}
Kontroler - gdzie problem?
<?php
class CreateUserCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'app:create-user';
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($this->getDoctrine()->getRepository(User::class)->findOneByEmail($form->getData()->getEmail())) {
// form error
$output->writeln('User already exists');
return;
}
$user = new User();
if ($input->getOption('email')) {
$user->setEmail($input->getOption('email'));
}
if ($input->setPassword('password')) {
$user->setPassword($passwordEncoder->encodePassword(
$user,
$input->getOption('password')
));
}
$this->getDoctrine()->persist($user);
$this->getDoctrine()->flush();
// additional logic
if ($this->someChecks) {
$this->emailSender->send();
}
$this->firestoreUpdater->update()
}
}
<?php
declare(strict_types=1);
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
foreach (['leszek.prabucki@gmail.com', 'contact@cocoders.com'] as $email) {
$user = new User();
$user->setEmail($email);
$user->setPassword($passwordEncoder->encodePassword(
$user,
'secretPasswd'
));
$this->getDoctrine()->persist($user);
$this->getDoctrine()->flush();
$this->firestoreUpdater->update();
}
}
}
Gdyby tak skupić się na rozwiązaniu problemu, a potem podłączyć pod to framework?
Ok, zaprojektujmy sobie nasz projekt od nowa zostawiając framework na później
TDD
vendor/bin/phpspec desc "SymfonyLiveWarsaw\Domain\User"
<?php
declare(strict_types=1);
namespace spec\SymfonyLiveWarsaw\Domain;
use SymfonyLiveWarsaw\Domain\User;
use SymfonyLiveWarsaw\Domain\Email;
use SymfonyLiveWarsaw\Domain\PasswordHash;
use PhpSpec\ObjectBehavior;
/**
* @mixin User
*/
class UserSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedWith(
User\Id::fromString('35f15ad1-b764-4f22-a655-e2ef3873dfc3'),
Email::fromString('leszek.prabucki@gmail.com'),
PasswordHash::fromString('$argon2id$v=19$m=1024,t=2,p=2$WXl6Wk5zWWpPY3RvWFloMA$emM94iLGzIMjqDC/uk6UFlRIhXQQ72t1f67L+LZEVQA')
);
}
function it_is_initialized_with_required_data_like_id_email_and_username()
{
$this->shouldBeAnInstanceOf(User::class);
}
function it_allows_to_get_email()
{
$this->email()->shouldBeLike(Email::fromString('leszek.prabucki@gmail.com'));
}
function it_allows_to_get_id()
{
$this->id()->shouldBeLike(User\Id::fromString('35f15ad1-b764-4f22-a655-e2ef3873dfc3'));
}
}
<?php
declare(strict_types=1);
namespace SymfonyLiveWarsaw\Domain;
use SymfonyLiveWarsaw\Domain\User\PasswordHash;
class User
{
/**
* @var User\Id
*/
private $id;
/**
* @var Email
*/
private $email;
/**
* @var PasswordHash
*/
private $passwordHash;
public function __construct(User\Id $id, Email $email, PasswordHash $passwordHash)
{
$this->id = $id;
$this->email = $email;
$this->passwordHash = $passwordHash;
}
public function email(): Email
{
return $this->email;
}
public function id(): User\Id
{
return $this->id;
}
}
<?php
declare(strict_types=1);
namespace spec\SymfonyLiveWarsaw\Domain;
use SymfonyLiveWarsaw\Domain\User;
use SymfonyLiveWarsaw\Domain\Email;
use SymfonyLiveWarsaw\Domain\User\PasswordHash;
use SymfonyLiveWarsaw\Domain\User\Fullname;
use PhpSpec\ObjectBehavior;
/**
* @mixin User
*/
class UserSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedWith(
User\Id::fromString('35f15ad1-b764-4f22-a655-e2ef3873dfc3'),
Email::fromString('leszek.prabucki@gmail.com'),
PasswordHash::fromString('$argon2id$v=19$m=1024,t=2,p=2$WXl6Wk5zWWpPY3RvWFloMA$emM94iLGzIMjqDC/uk6UFlRIhXQQ72t1f67L+LZEVQA')
);
}
//...
function it_can_have_full_name()
{
$this->fullname()->shouldBe(null);
$this->changeFullname(Fullname::fromFirstAndLastName('Leszek', 'Prabucki'));
$this->fullname()->shouldBeLike(Fullname::fromFirstAndLastName('Leszek', 'Prabucki'));
}
}
<?php
namespace spec\SymfonyLiveWarsaw\Domain;
use SymfonyLiveWarsaw\Domain\Email;
use PhpSpec\ObjectBehavior;
/**
* @mixin Email
*/
class EmailSpec extends ObjectBehavior
{
function it_do_not_initialize_with_value_which_is_not_in_html5_allowed_format()
{
$this->beConstructedThrough(
'fromString',
[
'lesze@k'
]
);
$this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation();
}
function it_is_initialized_for_valid_value()
{
$this->beConstructedThrough('fromString', ['contact@cocoders.com']);
$this->toString()->shouldBe('contact@cocoders.com');
}
function it_normalize_given_value()
{
$this->beConstructedThrough('fromString', ['Contact@cocoders.com']);
$this->toString()->shouldBe('contact@cocoders.com');
}
}
<?php
declare(strict_types=1);
namespace SymfonyLiveWarsaw\Domain;
use SymfonyLiveWarsaw\Domain\User\PasswordHash;
class User
{
//...
/**
* @var Fullname
*/
private $fullname;
//...
public function fullname(): ?Fullname
{
return $this->fullname;
}
public function changeFullname(Fullname $fullname): void
{
$this->fullname = $fullname;
}
}
No dobra, mamy prosty obiekt modelu co teraz?
Musimy pozwolić na utworzenie konta (rejestracje użytkownika)
Pomysł: Czemu by nie zrobić na to serwisu?
No to specujemy!
<?php
namespace spec\SymfonyLiveWarsaw\Application\UseCase;
use SymfonyLiveWarsaw\Application\UseCase\RegisterUser;
use SymfonyLiveWarsaw\Domain\Users;
use SymfonyLiveWarsaw\Domain\User;
use SymfonyLiveWarsaw\Application\UseCase\RegisterUser\UserFactory;
use PhpSpec\ObjectBehavior;
class RegisterUserSpec extends ObjectBehavior
{
function let(Users $users, UserFactory $userFactory)
{
$this->beConstructedWith($users, $userFactory);
}
function it_creates_user_base_on_given_data_and_store_in_persistence_layer(
Users $users,
UserFactory $userFactory,
User $user
) {
$command = new RegisterUser\Command(
'a35c7f52-fdf3-40ed-a69e-2c7f17d174e9',
'leszek.prabucki@gmail.com',
'$argon2id$v=19$m=1024,t=2,p=2$WXl6Wk5zWWpPY3RvWFloMA$emM94iLGzIMjqDC/uk6UFlRIhXQQ72t1f67L+LZEVQA'
);
$command = $command->withFullName('Leszek', 'Prabucki');
$userFactory->create($command)->willReturn($user);
$this->handle($command);
$users->add($user)->shouldHaveBeenCalled();
}
}
<?php
namespace spec\SymfonyLiveWarsaw\Application\UseCase;
use SymfonyLiveWarsaw\Application\UseCase\RegisterUser;
use SymfonyLiveWarsaw\Domain\Users;
use SymfonyLiveWarsaw\Domain\User;
use SymfonyLiveWarsaw\Application\UseCase\RegisterUser\UserFactory;
use SymfonyLiveWarsaw\Application\Exception\UserAlreadyExists;
use PhpSpec\ObjectBehavior;
class RegisterUserSpec extends ObjectBehavior
{
function let(Users $users, UserFactory $userFactory)
{
$this->beConstructedWith($users, $userFactory);
}
//..
function it_cannot_register_user_with_same_email(Users $users)
{
$command = new RegisterUser\Command(
'a35c7f52-fdf3-40ed-a69e-2c7f17d174e9',
'leszek.prabucki@gmail.com',
'$argon2id$v=19$m=1024,t=2,p=2$WXl6Wk5zWWpPY3RvWFloMA$emM94iLGzIMjqDC/uk6UFlRIhXQQ72t1f67L+LZEVQA'
);
$users->has(Email::fromString('leszek.prabucki@gmail.com'))->willReturn(true);
$this->shouldThrow(UserAlreadyExists::class)->duringHandle($command);
}
}
<?php
declare(strict_types=1);
namespace SymfonyLiveWarsaw\Application\UseCase;
use SymfonyLiveWarsaw\Application\Exception\UserAlreadyExists;
use SymfonyLiveWarsaw\Domain\Users;
class RegisterUser
{
/**
* @var Users
*/
private $users;
/**
* @var RegisterUser\UserFactory
*/
private $userFactory;
public function __construct(Users $users, RegisterUser\UserFactory $userFactory)
{
$this->users = $users;
$this->userFactory = $userFactory;
}
public function handle(RegisterUser\Command $command): void
{
if ($this->users->has($command->email())) {
throw UserAlreadyExists::forEmail($command->email());
}
$user = $this->userFactory->create($command);
$this->users->add($user);
}
}
Dzięki TDD udało się napisać kod który realizuje potrzebny biznesowe bez myślenia o szczegółach. TDD i abstrakcja WTF!!
Ok, ale chciałbym jakiś kod wykonać przed i po wykonaniu serwisu (np. tranzakcje, logger)
Command Bus
<?php
namespace SymfonyLiveWarsaw\Application\Infrastructure;
interface Command
{}
interface CommandHandler
{
public function handle(Command $command): void;
}
interface CommandBus
{
public function registerHandler(string $commandName, CommandHandler $handler): void;
public function handle(Command $command): void;
}
<?php
declare(strict_types=1);
namespace SymfonyLiveWarsaw\Application\Infrastructure;
use SymfonyLiveWarsaw\Application\Exception\HandlerNotFound;
final class SynchronousCommandBus implements CommandBus
{
/**
* @var CommandHandler[]
*/
private $handlers = [];
public function registerHandler(string $commandName, CommandHandler $handler): void
{
$this->handlers[$commandName] = $handler;
}
public function handle(Command $command): void
{
if (!isset($this->handlers[get_class($command)])) {
throw new HandlerNotFound(get_class($command));
}
$handler = $this->handlers[get_class($command)];
$handler->handle($command);
}
}
<?php
declare(strict_types=1);
namespace SymfonyLiveWarsaw\Application\UseCase;
use SymfonyLiveWarsaw\Application\Exception\UserAlreadyExists;
use SymfonyLiveWarsaw\Application\Infrastructure\Command;
use SymfonyLiveWarsaw\Application\Infrastructure\CommandHandler;
use SymfonyLiveWarsaw\Domain\Users;
class RegisterUser implements CommandHandler
{
/**
* @var Users
*/
private $users;
/**
* @var RegisterUser\UserFactory
*/
private $userFactory;
public function __construct(Users $users, RegisterUser\UserFactory $userFactory)
{
$this->users = $users;
$this->userFactory = $userFactory;
}
/**
* @param RegisterUser\Command $command
*/
public function handle(Command $command): void
{
if ($this->users->has($command->email())) {
throw UserAlreadyExists::forEmail($command->email());
}
$user = $this->userFactory->create($command);
$this->users->add($user);
}
}
<?php
namespace SymfonyLiveWarsaw\Application\Infrastructure;
use Psr\Log\LoggerInterface;
final class LoggerCommandBus implements CommandBus
{
private $decoratedCommandBus;
private $logger;
public function __construct(CommandBus $decoratedCommandBus, LoggerInterface $logger)
{
$this->decoratedCommandBus = $decoratedCommandBus;
$this->logger = $logger;
}
public function registerHandler(string $commandName, CommandHandler $handler): void
{
$this->decoratedCommandBus->registerHandler($commandName, $handler);
}
public function handle(Command $command): void
{
$this->logger->info(sprintf('Command appear %s', get_class($command)));
try {
$this->decoratedCommandBus->handle($command);
$this->logger->info(sprintf('Command handled %s', get_class($command)));
} catch (\Exception $exception) {
$this->logger->error(
sprintf('Error from command %s', get_class($command)),
[
'exception' => $exception
]
);
throw $exception;
}
}
}
<?php
namespace SymfonyLiveWarsaw\Application\Infrastructure;
use Exception;
final class TransactionalCommandBus implements CommandBus
{
private $decoratedCommandBus;
private $transactionManager;
public function __construct(
CommandBus $decoratedCommandBus,
TransactionManager $transactionManager
) {
$this->decoratedCommandBus = $decoratedCommandBus;
$this->transactionManager = $transactionManager;
}
public function registerHandler(string $commandName, CommandHandler $handler): void
{
$this->decoratedCommandBus->registerHandler($commandName, $handler);
}
public function handle(Command $command): void
{
$this->transactionManager->begin();
try {
$this->decoratedCommandBus->handle($command);
$this->transactionManager->commit();
} catch (Exception $exception) {
$this->transactionManager->rollback();
throw $exception;
}
}
}
Yay! Dodaliśmy funkcje bez modyfikacji istniejącego kodu, a tylko dokładając nowe klasy!
Ok mamy nasz kod co teraz? W jakich miejscach możemy to integrować?
Gdzie możemy się podpiąć?
<?php
// Port 1 - Persistence (możemy implementować interfejs)
interface Users
{
public function add(User $user): void;
public function has(Email $email): bool;
}
// Port 2 - Aplikacja - tworzenie obiektu domenowego (możemy dziedziczyć - brak final)
class UserFactory
{
public function create(Command $command): User {}
}
// Port 3 - Domena (możemy dziedziczyć - brak final)
class User {}
// Port 4 - Infrastruktura (możemy implementować interfejs)
interface CommandBus {}
// Port 5 - Infrastruktura (możemy implementować interfejs)
interface TransactionManager {}
Pora na adaptery, teraz możemy sobie wybrać framework
Instalacja flexa
"extra": {
"src-dir": "src/App"
}
Do composer.json dodajemy
composer require symfony/flex
composer require annotations asset orm-pack twig \
logger mailer form security translation validator
composer require --dev dotenv maker-bundle orm-fixtures profiler
Konfiguracja flexa i doctrine
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
// ... registerBundles
public function getProjectDir(): string
{
return \dirname(__DIR__).'/../';
}
// ... configure container
}
# config/services.yml
parameters:
locale: 'en'
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: '../src/App'
exclude: '../src/App/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
App\Controller\:
resource: '../src/App/Controller'
tags: ['controller.service_arguments']
SymfonyLiveWarsaw\:
resource: '../src/SymfonyLiveWarsaw'
exclude: '../src/SymfonyLiveWarsaw/Domain/{User.php,Email.php}'
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_pgsql'
server_version: '11.3'
url: '%env(resolve:DATABASE_URL)%'
types:
user_id:
class: App\Doctrine\Type\UserIdType
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
mappings:
SymfonyLiveWarsaw:
is_bundle: false
type: xml
dir: '%kernel.project_dir%/src/App/Doctrine/mapping'
prefix: 'SymfonyLiveWarsaw\Domain'
alias: SymfonLiveWarsaw
<?xml version="1.0" encoding="UTF-8"?>
<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
http://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
<entity name="SymfonyLiveWarsaw\Domain\User" table="users">
<indexes>
<index columns="email"/>
</indexes>
<unique-constraints>
<unique-constraint columns="email" name="email_idx" />
</unique-constraints>
<id name="id" type="user_id" column="id" />
<embedded name="email" class="SymfonyLiveWarsaw\Domain\Email"
use-column-prefix="false" />
<embedded name="passwordHash" class="SymfonyLiveWarsaw\Domain\User\PasswordHash"
use-column-prefix="false" />
<embedded name="fullname" class="SymfonyLiveWarsaw\Domain\User\Fullname"
use-column-prefix="false" />
</entity>
</doctrine-mapping>
Konfiguracja Command Bus
<?php
declare(strict_types=1);
namespace App\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use SymfonyLiveWarsaw\Application\Infrastructure\CommandBus;
class CommandBusCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has(CommandBus::class)) {
return;
}
$definition = $container->findDefinition(CommandBus::class);
$taggedServices = $container->findTaggedServiceIds('command_handler');
foreach ($taggedServices as $id => $tags) {
foreach ($tags as $attribues) {
$definition->addMethodCall(
'registerHandler',
[
$id . '\\Command',
new Reference($id)
]
);
}
}
}
}
<?php
namespace App;
use App\DependencyInjection\CommandBusCompilerPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
use SymfonyLiveWarsaw\Application\Infrastructure\CommandHandler;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
// ... rest of code
protected function build(ContainerBuilder $container)
{
$container
->registerForAutoconfiguration(CommandHandler::class)
->addTag('command_handler')
;
$container
->addCompilerPass(new CommandBusCompilerPass())
;
}
}
services:
# other services config
SymfonyLiveWarsaw\Domain\Users: '@App\Doctrine\Users'
SymfonyLiveWarsaw\Application\Infrastructure\TransactionManager:
alias: 'App\Doctrine\TransactionManager'
SymfonyLiveWarsaw\Application\Infrastructure\CommandBus:
class: SymfonyLiveWarsaw\Application\Infrastructure\SynchronousCommandBus
SymfonyLiveWarsaw\Application\Infrastructure\LoggerCommandBus:
decorates: SymfonyLiveWarsaw\Application\Infrastructure\CommandBus
decoration_priority: 1
arguments: ['@SymfonyLiveWarsaw\Application\Infrastructure\LoggerCommandBus.inner', '@logger']
SymfonyLiveWarsaw\Application\Infrastructure\TransactionalCommandBus:
decorates: SymfonyLiveWarsaw\Application\Infrastructure\CommandBus
decoration_priority: 2
arguments:
- '@SymfonyLiveWarsaw\Application\Infrastructure\TransactionalCommandBus.inner',
- '@SymfonyLiveWarsaw\Application\Infrastructure\TransactionManager'
Ok mamy spiętą apkę! Jak będzie wyglądał teraz nasz kontroler?
<?php
namespace App\Controller;
// all uses
class UserController extends AbstractController
{
private $commandBus;
private $encoderFactory;
public function __construct(CommandBus $commandBus, EncoderFactoryInterface $encoderFactory)
{
$this->commandBus = $commandBus;
$this->encoderFactory = $encoderFactory;
}
/**
* @Route("/user", name="user", methods={"POST"})
*/
public function registerUser(Request $request)
{
$form = $this->createForm(RegisterUserType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$formData = $form->getData();
$this->commandBus->handle(new RegisterUser\Command(
(string) User\Id::generateNew(),
$formData['email'],
$this->encoderFactory->getEncoder(User::class)->encodePassword($formData['password'], '')
));
return $this->redirectToRoute('user');
}
return $this->render('user/index.html.twig', ['form' => $form->createView()]);
}
}
Możemy tej samego kodu użyć np w fixtures
<?php
declare(strict_types = 1);
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use SymfonyLiveWarsaw\Application\Infrastructure\CommandBus;
use SymfonyLiveWarsaw\Application\UseCase\RegisterUser;
use SymfonyLiveWarsaw\Domain\User;
class AppFixtures extends Fixture
{
private $commandBus;
private $encoderFactory;
public function __construct(CommandBus $commandBus, EncoderFactoryInterface $encoderFactory)
{
$this->commandBus = $commandBus;
$this->encoderFactory = $encoderFactory;
}
public function load(ObjectManager $manager): void
{
foreach (['leszek.prabucki@gmail.com', 'contact@cocoders.com'] as $email) {
$this->commandBus->handle(new RegisterUser\Command(
User\Id::generateNew()->toString(),
$email,
$this->encoderFactory->getEncoder(User::class)->encodePassword('abc123%asd', '')
));
}
}
}
Co gdyby tak wykonać nasze polecenie asynchronicznie?
Messenger Component
<?php
declare(strict_types=1);
namespace App\Messenger;
use Symfony\Component\Messenger\MessageBusInterface;
use SymfonyLiveWarsaw\Application\Infrastructure\Command;
use SymfonyLiveWarsaw\Application\Infrastructure\CommandBus;
use SymfonyLiveWarsaw\Application\Infrastructure\CommandHandler;
final class MessengerCommandBus implements CommandBus
{
/**
* @var MessageBusInterface
*/
private $messageBus;
public function __construct(MessageBusInterface $messageBus)
{
$this->messageBus = $messageBus;
}
public function registerHandler(string $commandName, CommandHandler $handler): void
{
}
public function handle(Command $command): void
{
$this->messageBus->dispatch($command);
}
}
<?php
declare(strict_types=1);
namespace App\Messenger;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use SymfonyLiveWarsaw\Application\Infrastructure\Command;
use SymfonyLiveWarsaw\Application\Infrastructure\SynchronousCommandBus;
final class CommandBusMessengerHandler implements MessageHandlerInterface
{
/**
* @var SynchronousCommandBus
*/
private $commandBus;
public function __construct(SynchronousCommandBus $commandBus)
{
$this->commandBus = $commandBus;
}
public function __invoke(Command $command)
{
$this->commandBus->handle($command);
}
}
Podsumowanie
Plusy takiej architektury:
-
Modułowość - możliwość dołożenia kodu bez wielkich modyfikacji
-
Jasny podział warstw aplikacji
-
Warstwa testów regresji (tworzymy w TDD)
-
Kod może być łatwo komponowany
-
Nowa wersja zależności to nie problem
Minusy takiej architektury:
-
Więcej kodu - dokładanie dodatkowych danych w domenie powoduje dużo kodu w każdej warstwie
-
Wolniej (trzeba pisać kod nie generować)
-
Dużo więcej kodu infrastruktury (na szczęście to kod pisany zazwyczaj raz)
Krótka historia o tym jak możemy projektować i implementować aplikacje Symfony przy użyciu TDD
By Leszek Prabucki
Krótka historia o tym jak możemy projektować i implementować aplikacje Symfony przy użyciu TDD
- 1,023