Hexagonal Architecture w PHP
O Mnie
Domena
Users (Repository)
Doctrine
PDO
RegisterUserResponder
Aplikacja
Symfony
https://github.com/cocoders/invoice-design-patterns
Let's code
Nasz piękny legacy wczesny 2004 ;)
Docker & Docker Compose
https://github.com/cocoders/invoice-design-patterns/blob/master/docker-compose.yml
version: '3.4'
services:
web:
image: nginx:1.13.7
depends_on:
- php
volumes:
- ./docker/nginx/invoice.conf:/etc/nginx/conf.d/default.conf
- ./public:/var/www/invoice/public
ports:
- "8080:80"
php:
image: cocoders/php-fpm:7.2.0
volumes:
- .:/var/www/invoice
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
environment:
POSTGRES_DSN: 'pgsql:host=postgres;port=5432;dbname=invoice;'
POSTGRES_USER: 'invoice'
POSTGRES_DB: 'invoice'
POSTGRES_PASSWORD: '!E4\mP-C;Q!@2vV3'
restart: always
working_dir: /var/www/invoice
postgres:
image: postgres:10.1
volumes:
- pg_data:/var/lib/postgresql/data
- ./docker/postgres/initsql:/docker-entrypoint-initdb.d/
environment:
POSTGRES_PASSWORD: '!E4\mP-C;Q!@2vV3'
POSTGRES_USER: 'invoice'
restart: always
ports:
- "5432:5432"
volumes:
pg_data:
docker-compose up -d
docker-compose exec php bash
PHPSpec i PHPUnit
Let's code
composer require --dev "phpspec/phpspec:4.2.5"
PHPSpec
composer require --dev "phpunit/phpunit:^6.5"
PHPUnit
git checkout 01-phpspec-and-phpunit
PHPSpec i PHPUnit
Tworzymy opis domena użytkownka w phpspecu
Domena User
- Użytkownik musi podać email
- Użytkownik musi podać hasło
- Email powinnen mieć prawidłową strukture, musimy na niego wysyłać faktury
- Użytkownik może podać dane profilowe (nie obowiązkowe przy rejestracji)
Domena User -Rejestracja
Przygotujmy model
Domena User - Rejestracja
bin/phpspec desc "Invoice\Domain\User"
Domena User - Rejestracja
bin/phpspec run
Domena User - Rejestracja
Value Object dla Email?
Domena User - Rejestracja
bin/phpspec desc "Invoice\Domain\Email"
Domena User - Rejestracja
Email - protecting invariants
Domena User - Rejestracja
git checkout 02-user-model
Domena User - Przykład
Tworzymy opis przypadku użycia naszej aplikacji w PHPSpecu, dla przypadku rejestracji użytkownika
Aplikacja - RegisterUser
Aplikacja - RegisterUser
Domena
Users
Adapter
Aplikacja
TransactionManager
UserFactory
Jako domena i aplikacja wystawiamy nasze porty (Interfejsy)
Jako domena i aplikacja udostępniamy kontrakt
Jak zaplanować porty?
Aplikacja - RegisterUser
bin/phpspec desc "Invoice\Application\UseCase\RegisterUser"
Aplikacja - RegisterUser
Aplikacja - RegisterUser
<?php
namespace spec\Invoice\Application\UseCase;
use Invoice\Application\TransactionManager;
use Invoice\Domain\Users;
use Invoice\Domain\UserFactory;
use Invoice\Domain\User;
use PhpSpec\ObjectBehavior;
class RegisterUserSpec extends ObjectBehavior
{
function let(
TransactionManager $transactionManager,
Users $users,
UserFactory $userFactory
) {
$this->beConstructedWith($transactionManager, $users, $userFactory);
}
git checkout 03-register-user-spec
Aplikacja - RegisterUser
Implementacja UseCase RegisterUser
Aplikacja - RegisterUser
git checkout 04-register-user-implementation
Aplikacja - RegisterUser przykład
Trzeba zabezpieczeczyć aplikacje w taki sposób aby nie tworzyć drugi raz usera który jest już w bazie
Aplikacja - RegisterUser
git checkout 05-register-user-is-in-storage-validation
Aplikacja - RegisterUser
Implementacja adapterów
RegisterUser Adapters
RegisterUser Adapters
Domena
Users
PDO Adapter
Aplikacja
TransactionManager
UserFactory
Cocoders\Invoice\Adapter\Pdo\Application\TransactionManager
Cocoders\Invoice\Adapter\Pdo\Domain\Users
Cocoders\Invoice\Adapter\Pdo\Domain\User
Cocoders\Invoice\Adapter\Pdo\Domain\UserFactory
extends User
RegisterUser Adapters
Unit Of Work i Transaction Manager na co nam to?
Unit Of Work
"Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems." - Martin Fowler
Unit Of Work
Jak zaimplementować zarządzanie tranzakcjami żeby było "Framework Agnostic"?
Już to zrobiliśmy
<?php
declare(strict_types=1);
namespace Invoice\Application;
interface TransactionManager
{
public function begin(): void;
public function commit(): void;
public function rollback(): void;
}
Pora na implementacje w PDO
RegisterUser PDO Adapters
git checkout 06-pdo-adapter-transaction-manager
RegisterUser PDO Adapters
Zadanie dla was implementacja reszty adatpera PDO
RegisterUser PDO Adapters
git checkout 07-pdo-adapter
RegisterUser PDO Adapters
Integracja z legacy
Co z tym?
function register()
{
global $connection, $registerErrors;
if (empty($_POST['email'])) {
$registerErrors['email'] = "Email field was empty.";
} elseif (empty($_POST['password'])) {
$registerErrors['password'] = "Password field was empty.";
} elseif (!empty($_POST['email']) && !empty($_POST['password'])) {
$stmt = $connection->prepare('SELECT id, email FROM users WHERE email = :email');
$stmt->execute(['email' => $_POST['email']]);
$users = $stmt->fetchAll();
if (count($users) > 0) {
$registerErrors['email'] = "User with given email exists already.";
return;
}
$stmt = $connection->prepare('INSERT INTO users
(email, password_hash) VALUES (:email, :password)');
$stmt->execute(['email' => $_POST['email'], 'password' => password_hash($_POST['password'], PASSWORD_BCRYPT)]);
header('Location: /login.php?successRegister=1');
exit;
}
}
Co z tym?
Nowy port dla powiadomień idących z przypadku użycia RegisterUser (tzw. Responder tnx Uncle Bob)
Nowy port - responder
function register()
{
global $registerErrors, $registerUser;
if (empty($_POST['password'])) {
$registerErrors->addError("password", "Password field was empty.");
return;
}
$registerUser->registerResponder(new RegisterUserResponder($registerErrors));
$registerUser->execute(new RegisterUser\Command(
$_POST['email'],
password_hash($_POST['password'], PASSWORD_BCRYPT)
));
if ($registerErrors->errors()) {
return;
}
header('Location: /login.php?successRegister=1');
exit;
}
Nowy port - responder
git checkout 08-legacy-integration-and-adapter
Aplikacja EditProfile UseCase
Zadanie dla was: Implementacja nowego przypadku użycia EditProfile (Test first)
Aplikacja EditProfile UseCase
function editProfile()
{
global $connection;
$stmt = $connection->prepare('UPDATE users SET name = :name, vat = :vat,
address = :address WHERE id = :user_id');
$stmt->execute([
'vat' => $_POST['vat'],
'address' => $_POST['address'],
'name' => $_POST['name'],
'user_id' => $_SESSION['loggedInUser']['id']
]);
header('Location: /index.php?page=user-profile&successMessage="Profile data updated successfully"');
exit;
}
git checkout 09-edit-profile-use-case
Nowy adapter
Doctrine jako adapter
Nowy adapter
Dodanie mapingu dla Usera
Nowy adapter
Impelementacja Users, User, UserFactory
Nowy adapter
git checkout 10-doctrine-adapter
Nowy adapter
To może symfony?
Nowy adapter
Proszeeee
Nowy adapter
git checkout 11-symfony-integration
CQRS
Command Query Responsibility Segregation
CQRS
Rodzielmy klasy/architekture z podziałem na odczyt i zapis
CQRS
Rodzielmy klasy/architekture z podziałem na odczyt i zapis
CQRS
Odczyt dla danych do formularza. UsersListQuery dla listy użytkowników
CQRS
Odczyt w DBAL
CQRS
Przykłady
Szkolenie Hegaxon 23.01.2018
By Leszek Prabucki
Szkolenie Hegaxon 23.01.2018
- 710