@xf3l1x
f3l1x.io
30.04.2024

 

Command

Bus

Timeline

  • From 0 to 🧙‍♂️

  • Bus, bus, bus

  • Hierarchy / Layers

  • Types of objects

  • Coding

  • Tips & tricks

From zero to hero

class UserPresenter
{

	public function actionDefault()
	{
		$pdo = new PDO('localhost', 'user', 'password');
		$stmt = $pdo->prepare('SELECT * FROM users');
		$stmt->execute();
		$users = $stmt->fetchAll();

		$this->template->users = $users;
	}

}
class DB {

	public static function getUsers()
	{
		$pdo = new PDO('localhost', 'user', 'password');
		$stmt = $pdo->prepare('SELECT * FROM users');
		$stmt->execute();
		return $stmt->fetchAll();
	}

}
class User1Presenter
{

	public function actionDefault()
	{
		$users = DB::getUsers();
	}

}

class User2Presenter
{

	public function actionDefault()
	{
		$users = DB::getUsers();
	}

}
class DB {

	public function __construct($host, $user, $password)
	{
		$this->pdo = new PDO($host, $user, $password);
	}

	public function getUsers()
	{
		$stmt = $this->pdo->prepare('SELECT * FROM users');
		$stmt->execute();
		return $stmt->fetchAll();
	}

}
class UserPresenter
{

	public DB $db;

	public function actionDefault()
	{
		$users = $this->db->getUsers();
	}

}
class DB {

	public function query($sql) {
		// ...
	}

}

class UserModel
{

	public DB $db;

	public function getUsers()
	{
		return $this->db->query('SELECT * FROM users');
	}

}
class UserPresenter
{

	public UserModel $userModel;

	public function actionDefault()
	{
		$users = $this->userModel->getUsers();
	}

}
class UserModel
{

	public Nette\Database\Connection $db;

	public function getUser($id)
	{
		return $this->db->fetch('SELECT * FROM users WHERE id = ?', $id);
	}
    
	public function getUsers($filters = [])
	{
		return $this->db->fetchAll('SELECT * FROM users WHERE', $filters);
	}

}
class UserPresenter
{

	public UserModel $userModel;

	public function actionDefault()
	{
		$users = $this->userModel->getUsers();
	}

	public function actionDetail($id)
	{
		$user = $this->userModel->getUser($id);
	}

}
class UserPresenter
{

	public UserRepository $userRepository;

	public function actionDefault()
	{
		$users = $this->userRepository->findAll(['role' => 'admin']);
	}

	public function actionDetail($id)
	{
		$user = $this->userRepository->findBy(['id' => $id]);
	}

}
class UserRepository
{

	public Connection $db;

	public function findAll($filters, $limit, $offset);
	public function findBy($filters);
	public function fetchBy($filters);
	public function create($data);
	public function update($data, $filters);
	public function delete($filters);

}
class UserPresenter
{

	protected function createComponentForm()
	{
		$form->onSuccess[] = function($form) {
			$this->userRepository->create([
				'name' => $form->values->name,
				'email' => $form->values->email,
			]);
		};
	}

}

class UserApi
{

	public function __invoke(Request $request)
	{
		$this->userRepository->create([
			'name' => $request->values->name,
			'email' => $request->values->email,
		]);
	}

}
class UserPresenter
{

	protected function createComponentForm()
	{
		$form->onSuccess[] = function($form) {
			$this->userService->create($form->values);
		};
	}

}

class UserApi
{

	public function __invoke(Request $request)
	{
		$this->userService->create($request->values);
	}

}

class UserService
{

	public UserRepository $userRepository;

	public function create($data)
	{
		Assert::string($data, 'name');
		Assert::email($data, 'email');

		$this->userRepository->create([
			'name' => $data['name'],
			'email' => $data['email'],
		]);
	}

}
class UserPresenter
{

	protected function createComponentForm()
	{
		$form->onSuccess[] = function($form) {
			$this->userService->create($form->values);
			$this->mailer->send($form->values->email, 'Welcome');
		};
	}

}

class UserApi
{

	public function __invoke(Request $request)
	{
		$this->userService->create($request->values);
		$this->mailer->send($request->values->email, 'Welcome');
	}

}
class UserPresenter
{

	protected function createComponentForm()
	{
		$form->onSuccess[] = function($form) {
			$this->userFacade->create($form);
		};
	}

}
class UserFacade
{

	public UserRepository $userRepository;

	public Mailer $mailer;

	public function create($data)
	{
		Assert::data($data);

		$this->userRepository->create([
			'name' => $data['name'],
			'email' => $data['email'],
		]);

		$this->mailer->send($data['email'], 'Welcome');
	}

}
class UserFacade
{

	public function __construct(
		UserRepository $userRepository,
		ProfileRepository $profileRepository,
		Translator $translator,
		AcmeHttpConnector $httpConnector,
		CurrencyConverter $currencyConverter,
		DataValidator $dataValidator,
		Mailer $mailer
	)
	{
	}

	public function createUser(...);
	public function updateUser(...);
	public function delateUser(...);
	public function findUserById(...);
	public function findAllUsers(...);
	public function sendEmailToUsers(...);
	public function deactivateUsers(...);
	public function activateProfiles(...);

}
class CreateUserCommand
{

	public function __construct(
		public string $name,
		public string $email
	)
	{
	}

}

class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$data = [
			'name' => $command->name,
			'email' => $command->email,
		];

		$this->userRepository->create($data);
	}

}
class UserPresenter
{

	protected function createComponentForm()
	{
		$form->onSuccess[] = function($form) {
			$this->commandBus->dispatch(
				new CreateUserCommand(
					$form->values->name,
					$form->values->email
				)
			);
		};
	}

}

class CommandBus
{

	public function dispatch($command) {
		if ($command instanceof CreateUserCommand) {
			$this->createUserHandler->handle($command);
		}
		// ...
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$data = [
			'name' => $command->name,
			'email' => $command->email,
		];

		$this->userRepository->create($data);

		$this->mailer->send($command->email, 'Welcome');

		$this->logger->info('User created', $data);
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$data = [
			'name' => $command->name,
			'email' => $command->email,
		];

		$this->userRepository->create($data);

		$this->mailer->send($command->email, 'Welcome');

		$this->logger->info('User created', $data);
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$data = [
			'name' => $command->name,
			'email' => $command->email,
		];

		$this->userRepository->create($data);

		$this->eventDispatcher->dispatch(
			new UserCreatedEvent($data)
		);
	}

}
class EventDispatcher
{

	public function dispatch($event) {
		foreach ($this->subscribers[$event] as $subscriber) {
			$subscriber->handle($event);
		}
	}

}

class LoggerSubscriber
{

	public static function getSubscribedEvents()
	{
		return [UserCreatedEvent::class => 'handle'];
	}

	public function handle(UserCreatedEvent $event) {
		$this->logger->info('User created', $event->data);
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$data = [
			'name' => $command->name,
			'email' => $command->email,
		];

		$this->userRepository->create($data);

		$this->eventDispatcher->dispatch(
			new UserCreatedEvent($data)
		);
	}

}
class UserPresenter
{

	protected function createComponentForm()
	{
		$form->onSuccess[] = function($form) {
			$user = $this->commandBus->dispatch(
				new CreateUserCommand(
					$form->values->name,
					$form->values->email
				)
			);
            
			$this->flashMessage(
				sprintf('User %s created', $user['name'])
			);
		};
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$data = [
			'name' => $command->name,
			'email' => $command->email,
		];

		$user = $this->userRepository->create($data);

		return $user;
	}

}
class UserPresenter
{

	protected function createComponentForm()
	{
		$form->onSuccess[] = function($form) {
			$userRecord = $this->commandBus->dispatch(
				new CreateUserCommand(
					$form->values->name,
					$form->values->email
				)
			);

			$this->flashMessage(
				sprintf('User %s created', $userRecord->name)
			);

			$userRecord->ref('profile')->nickname;
			$userRecord->update(['email' => 'john@email.tld']);
			$userRecord->delete();
		};
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$data = [
			'name' => $command->name,
			'email' => $command->email,
		];

		$userRecord = $this->userRepository->create($data);

		return $userRecord;
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$user = new User();
		$user->name = $command->name;
		$user->email = $command->email;

		$this->entityManager->persist($user);
		$this->entityManager->flush();
	}

}
class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$user = new User();
		$user->name = $command->name;
		$user->email = $command->email;

		try {
			$this->entityManager->persist($user);
			$this->entityManager->flush();
		} catch (UniqueConstraintViolationException $e) {
			throw EntityExistsException::from($user, $e);
		}
	}

}
class UpdateUserHandler
{

	public function handle(UpdateUserHandler $command)
	{
		$user = $this->entityManager
			->getRepository(User::class)
			->findOneBy(['uuid' => $command->uuid]);

		if ($user === null) {
			throw EntityNotFoundException::byUuid($command->uuid);
		}

		if ($command->name !== null) {
			$user->name = $command->name;
		}

		$this->entityManager->persist($user);
		$this->entityManager->flush();
	}

}

Bus, bus, bus

Hierarchy / Layers

class UserPresenter
{

	public function actionList()
	{
		$filter = new UserFilter();
		$filter->setName('name', $this->getParameter('name'));
		$filter->setName('email', $this->getParameter('email'));
	}
	
	public function actionList()
	{
		$filter = new UserFilter();
		$parameters = $this->getParameters();
		if (isset($parameters['name'])) {
			$filter->setName('name', $parameters['name']);
		}
		if (isset($parameters['email'])) {
			$filter->setName('email', $parameters['email']);
		}
	}

}
class UserPresenter
{

	public function actionList()
	{
		$filter = UserFilter::fromRequest($this->getParameters());
	}

}

class UserFilter
{

	public static function fromRequest(array $parameters): self
	{
		$filter = new self();
		if (isset($parameters['name'])) {
			$filter->setName('name', $parameters['name']);
		}
		if (isset($parameters['email'])) {
			$filter->setName('email', $parameters['email']);
		}
		return $filter;
	}

}
class UserFilter
{

	public static function fromRequest(array $parameters): self
	{
		$filter = new self();
		if (isset($parameters['name'])) {
			$filter->setName('name', $parameters['name']);
		}
		if (isset($parameters['email'])) {
			$filter->setName('email', $parameters['email']);
		}
		return $filter;
	}
    
    
	public static function fromConsole(array $args): self
	{
		$filter = new self();
		if (isset($args['--name'])) {
			$filter->setName('name', $args['--name']);
		}
		if (isset($args['--email'])) {
			$filter->setEmail('email', $args['--email']);
		}
		return $filter;
	}

}
class UserPresenter
{

	public function actionList()
	{
		$this->bus->dispatch(
			new GetUsersQuery(
				filter: UserFilter::fromRequest($this->getParameters())
			)
		);
	}

}

class GetUsersQuery
{

	public function __construct(
		public UserFilter $filter
	)
	{
	}

}

Types of objects

#[ORM\Entity]
#[ORM\Table(name: 'users')]
class UserEntity
{

	#[ORM\Column(type: 'uuid', unique: true)]
	#[ORM\Id]
	public UuidInterface|string $uuid;

	#[ORM\Column(type: 'text', nullable: false)]
	public string $name;

}
class EmailValueObject
{

	public function __construct(string $email)
	{
		if (!Validators::isEmail($email)) {
			throw new InvalidArgumentException;
		}
		
		$this->email = $email;
	}

}
class UserDto
{

	public function __construct(
		public string $name,
		public EmailValueObject $email
	)
	{
	}

	public function format(): string
	{
		return sprintf('%s <%s>', $this->name, $this->email->email);
	}

}

Types

Entrypoint

CreateUserPresenter

CreateUserController

-CreateUserRequest

---CreateUserDto (body)

-CreateUserResponse

CreateUserCommand

Domain

CreateUserCommand

CreateUserHandler

CreateUserResult

ListUserCommand

-ListUserFilter

ListUserResult

 

ListDeviceQuery

ListDeviceHandler

Data

UserEntity

UserRepository

 

UserQuery

UserServiceRepository

Coding

https://git.moderntv.eu/playground/ddd-playground

Tips & Tricks

composer require nette/utils

Nette Validators

if (!Validators::isEmail($email)) {
	throw new InvalidArgumentException;
}


if (!Validators::is($val, 'int|string|bool')) {
	// ...
}


$arr = ['foo' => 'Nette'];

Validators::assertField($arr, 'foo', 'string:5');
Validators::isNumeric(23);
Validators::isNone(0);
Validators::isTypeDeclaration('string|null');
Validators::isUrl('https://nette.org:8080/path?query#fragment');
composer require nette/utils

Nette Validators

Validators::assert*; // throws exception

Validators::is*; // return bool
composer require nette/schema

Nette Schema

$schema = Expect::structure([
	'uuid' => Expect::string(),
	'name' => Expect::string()->required(),
	'description' => Expect::string()->required(),
	'created' => Expect::string()
    	->assert(static fn ($date) => Validators::isDateTime($date), 'datetime'),
	'owner' => Expect::string()->required(),
])->castTo(CreateProjectDto::class);


try {
	$normalized = (new Process())->process($schema, $data);
} catch (ValidationException $e) {
	echo 'Data is invalid: ' . $e->getMessage();
}

Bus + Transaction

class CreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$user = new User();
		$user->name = $command->name;
		$user->email = $command->email;

		$this->entityManager->beginTransaction();
            
		try {
			$this->entityManager->persist($user);
			$this->entityManager->flush();
        	$this->entityManager->commit();
		} catch (UniqueConstraintViolationException $e) {
        	$this->entityManager->rollback();
			throw EntityExistsException::from($user, $e);
		}
	}

}

Complex Bus

class ComplexCreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$this->bus->dispatch(new CreateUserCommand());
		$this->bus->dispatch(new SetupUserCommand());
		$this->bus->dispatch(new MakeUserFreeCommand());
	}

}

class ComplexCreateUserHandler
{

	public function handle(CreateUserCommand $command) {
		$this->bus->dispatch(new CreateUserCommand());
        
        $this->eventDispatcher->dispatch(UserCreated::create());
	}

}

Nette DI

services:
	fooFactory: FooFactory
	barFactory: BarFactory

	eventFactory:
		class: EventFactory
		setup:
			- addFactory('foo', @fooFactory)
			- addFactory('bar', @barFactory)
services:
	fooFactory: FooFactory
	barFactory: BarFactory

	eventFactory:
		class: EventFactory
		setup:
			- addFactory('foo', @fooFactory)
			- addFactory('bar', @barFactory)
public function createServiceBarFactory(): BarFactory
{
	return new BarFactory;
}

public function createServiceFooFactory(): FooFactory
{
	return new FooFactory;
}

public function createServiceEventFactory(): EventFactory
{
	$service = new EventFactory;
	$service->addFactory('foo', $this->getService('fooFactory'));
	$service->addFactory('bar', $this->getService('barFactory'));
	return $service;
}
class EventService
{

	public function __construct(EventFactory $eventFactory)
	{
	}
	
}

public function createServiceEventService(): EventService
{
	$service = new EventService($this->getService('eventFactory'));
	return $service;
}
class Container
{

	public function getService($service)
	{
		if (!isset($this->services[$service])) {
			$this->services[$service] = $this->{'createService' . ucfirst($service)}();
		}
		return $this->services[$service];
	}

}
class EventService
{

	public function __construct(EventFactory $eventFactory)
	{
	}
	
}

public function createServiceEventService(): EventService
{
	$service = new EventService($this->getService('eventFactory'));
	return $service;
}
class Container
{

	public function getService($service)
	{
		if (!isset($this->services[$service])) {
			$this->services[$service] = $this->{'createService' . ucfirst($service)}();
		}
		return $this->services[$service];
	}

}
class ContainerEventFactory
{

	private array $factories = [];
	private array $services = [];

	public function __construct(
		private Container $container
	)
	{
	}

	public function add(string $factory, string $event): void
	{
		$this->factories[$event] = $factory;
	}

	public function create(string $event): Event
	{
		if (!isset($this->services[$event])) {
			$this->services[$event] = $this->container->getService($this->factories[$event]);
		}
		
		return $this->factories[$event]->create($event);
	}

}
services:
	fooFactory: FooFactory
	barFactory: BarFactory

	eventFactory:
		class: ContainerEventFactory
		setup:
			- addFactory('foo', fooFactory)
			- addFactory('bar', barFactory)

Recap

Recap

  • Bus (3x)
  • Mapping (from <=> to)
  • Data objects (entity, dto, VO)
  • Data validations

Thank you!

@xf3l1x
f3l1x.io

2024-04-30-command-bus

By Milan Felix Šulc

2024-04-30-command-bus

  • 168