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