Clean Architecture

Clean Architecture

Who we are

Damiano Petrungaro

Christian Nastasi

<?php

$me = [
    'name' => 'Damiano',
    'surname' => 'Petrungaro',
    'job' => 'Software Engineer',
    'company' => [
        'name' => 'HelloFresh',
        'url' => 'https://hellofresh.com/'
    ],
    'maintainers' => ['conventional commit', 'laravel italia'],
    'github' => 'https://github.com/damianopetrungaro',
    'twitter' => '@damiano_dev',
    'buzzwords' => [
        'food',
        'php',
        'golang',
        'perfectionist',
        'DDD',
        'never stop learning',
    ]
];
<?php

$me = [
    "name"    => "Christian",
    "surname" => "Nastasi",
    "company" => null,
    "job"     => [
        "Software Craftman",
        "Technical Coach",
        "Agile Coach"
    ],
    "github" => "https://github.com/cnastasi",
    "twitter" => "@c_nastasi",
    "buzzwords" => [
        "quality is not an optional",
        "php",
        "agile", 
        "ddd", "bdd", "tdd",
        "framework-less",
        "nerd"
    ]
];

The PHP evolution

Composer

PSRs

PHP 7.x

Frameworks

Last 6 years

Community

🔝 Laravel 🔝

What can we build?


Now we can build a Laravel application using modern methodologies
 

  • TDD

  • SOLID

  • Continuous Integration

  • Clean Code

  • Domain Driven Design

  • Hexagonal Architecture

Laravel, it's a tool.

The domain, it's not a tool!

Value objects

It's an immutable object representing a value in a domain.
 

It's comparable to other value objects by its own values.

Entities & Aggregate

It's a mutable object with an identity, it can be represented in different ways.

 

It's comparable to other entities by its ID.

  

Repositories

It's an abstraction layer to provide access to all the entities and the value objects related to an aggregate.

Services

It's an object to use to manipulate entities and value objects when the logic doesn't fit or distort their integrity.

Not to be confused with the application or infrastructure services.

Events

It's an immutable object used to represent something important that occurred in the domain.

Example:
List articles

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

// ...

final class ListArticlesController extends Controller
{
    // ...

    public function __construct(
        ListArticlesHandler $handler,
        SerializeArticle $serializeArticle
    )
    {
        $this->handler = $handler;
        $this->serializeArticle = $serializeArticle;
    }

    public function __invoke(ListArticlesRequest $request)
    {
        $articleCollection = ($this->handler)(new ListArticlesCommand(
            (int) $request->query('skip'),
            (int) $request->query('take')
        ));
        $articles = \array_map(function (Article $article) {
            return ($this->serializeArticle)($article);
        }, $articleCollection->toArray());
        return response()->json($articles);
    }
}
<?php
declare(strict_types=1);

namespace App\Http\Requests;

// ...

final class ListArticlesRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'skip' => ['int'],
            'take' => ['int'],
        ];
    }

    protected function validationData(): array
    {
        return $this->query();
    }

    /**
     * @throws HttpResponseException
     */
    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(response()->json($validator->errors(), 422));
    }
}
<?php

declare(strict_types=1);

namespace Acme\Article\UseCase\ListArticles;

final class ListArticlesCommand
{
    // ...
    public function __construct(?int $skip, ?int $take)
    {
        $this->skip = $skip;
        $this->take = $take;
    }

    public function getSkip(): ?int
    {
        return $this->skip;
    }

    public function getTake(): ?int
    {
        return $this->take;
    }
}
<?php

declare(strict_types=1);

namespace Acme\Article\UseCase\ListArticles;
// ...

final class ListArticlesHandler
{
    // ...

    public function __construct(ArticleRepository $articleRepository)
    {
        $this->articleRepository = $articleRepository;
    }

    public function __invoke(ListArticlesCommand $command): ArticleCollection
    {
        $skip = $command->getSkip();
        if (0 === $skip || null === $skip) {
            $skip = ArticleRepository::DEFAULT_SKIP;
        }

        $take = $command->getTake();
        if (0 === $take || null === $take) {
            $take = ArticleRepository::DEFAULT_TAKE;
        }

        return $this->articleRepository->list($skip, $take);
    }
}
<?php

declare(strict_types=1);

namespace App\Integration\Article\Repository;

final class ArticleQueryBuilderRepository implements ArticleRepository
{
    // ...
    public function __construct(
        DatabaseManager $database,
        SerializeArticle $serializeArticle,
        HydrateArticle $hydrateArticle,
        LoggerInterface $logger
    ) {
        $this->database = $database;
        $this->logger = $logger;
        $this->serializeArticle = $serializeArticle;
        $this->hydrateArticle = $hydrateArticle;
    }
    // ...
    public function list(
        int $skip = self::DEFAULT_SKIP,
        int $take = self::DEFAULT_TAKE
    ): ArticleCollection
    {
        if ($take > ArticleRepository::MAX_SIZE) {
            $take = ArticleRepository::MAX_SIZE;
        }

        try {
            $rawArticles = $this->database
                ->table(self::TABLE_NAME)
                ->select()
                ->skip($skip)
                ->take($take)
                ->get();
        } catch (QueryException $e) {
            $this->logger->warning('database failure', ['exception' => $e]);
            throw new ImpossibleToRetrieveArticles($e);
        }

        $articles = \array_map(function (stdClass $rawArticle) {
            return ($this->fromArrayMapper)((array) $rawArticle);
        }, $rawArticles->toArray());

        return new ArticleCollection(...$articles);
    }
}
<?php

declare(strict_types=1);

namespace Acme\Article;
// ...
final class Article
{
    // ...
    public function __construct(
        ArticleID $articleID,
        Title $title,
        Body $body,
        AcademicRegistrationNumber $academicID,
        ?ReviewerID $reviewerID,
        ?DateTimeImmutable $publishDate,
        DateTimeImmutable $creationDate,
        ?DateTimeImmutable $lastUpdateDate
    ) {
        $this->articleID = $articleID;
        $this->title = $title;
        $this->body = $body;
        $this->academicRegistrationNumber = $academicID;
        $this->reviewerID = $reviewerID;
        $this->publishDate = $publishDate;
        $this->creationDate = $creationDate;
        $this->lastUpdateDate = $lastUpdateDate;
    }
}
<?php declare(strict_types=1);

namespace Acme\Article\ValueObject;

// ...

final class Title
{
    public const MIN_LENGTH = 5;
    public const MAX_LENGTH = 100;

    public function __construct(string $value)
    {
        $value = \trim($value);
        $length = \mb_strlen($value);
        if ($length < self::MIN_LENGTH || $length >= self::MAX_LENGTH) {
            throw InvalidTitle::fromInvalidLength($value);
        }

        $this->value = $value;
    }

    public function __toString(): string
    {
        return $this->value;
    }

    public function isEquals(self $title): bool
    {
        return $this->value === (string) $title;
    }

    private function __clone()
    {
    }
}

Example:
Write Article

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

// ...

final class WriteArticleController extends Controller
{
    // ...
    public function __construct(
        WriteArticleHandler $handler,
        SerializeAcademic $serializeAcademic
    )
    {
        $this->handler = $handler;
        $this->serializeAcademic = $serializeAcademic;
    }

    public function __invoke(WriteArticleRequest $request)
    {
        $command = new WriteArticleCommand(
            new Title($request->get('title')),
            new Body($request->get('body')),
            AcademicRegistrationNumber::fromString($request->route()->parameter('id'))
        );
        $academic = ($this->handler)($command);

        return response()->json($this->serializeAcademic->withoutPassword($academic), 201);
    }
}
<?php

declare(strict_types=1);

namespace App\Http\Requests;
// ...

final class WriteArticleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', new ArticleTitleRule()],
            'body' => ['required', 'string', new ArticleBodyRule()],
            'id' => ['required', 'string', new AcademicRegistrationNumberRule()],
        ];
    }

    protected function validationData(): array
    {
        return \array_merge($this->route()->parameters(), $this->all());
    }

    /**
     * @throws HttpResponseException
     */
    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(response()->json($validator->errors(), 422));
    }
}
<?php

declare(strict_types=1);

namespace App\Rules;

// ...

final class AcademicRegistrationNumberRule implements Rule
{
    /**
     * @var string
     */
    private $message;

    public function passes($attribute, $value): bool
    {
        try {
            AcademicRegistrationNumber::fromString($value);

            return true;
        } catch (InvalidAcademicRegistrationNumber $e) {
            $this->message = $e->getMessage();

            return false;
        }
    }

    public function message(): ?string
    {
        return $this->message;
    }
}
<?php

declare(strict_types=1);

namespace Acme\Academic\UseCase\WriteArticle;

// ...

final class WriteArticleCommand
{
    // ...
    public function __construct(
        Title $title,
        Body $body,
        AcademicRegistrationNumber $academicRegistrationNumber
    )
    {
        $this->title = $title;
        $this->body = $body;
        $this->academicRegistrationNumber = $academicRegistrationNumber;
    }

    public function getTitle(): Title
    {
        return $this->title;
    }

    public function getBody(): Body
    {
        return $this->body;
    }

    public function getAcademicRegistrationNumber(): AcademicRegistrationNumber
    {
        return $this->academicRegistrationNumber;
    }
}
<?php

declare(strict_types=1);

namespace Acme\Academic\UseCase\WriteArticle;

// ...

final class WriteArticleHandler
{
    // ...
    public function __construct(AcademicRepository $academicRepository)
    {
        $this->academicRepository = $academicRepository;
    }

    public function __invoke(WriteArticleCommand $command): Academic
    {
        $academic = $this->academicRepository->getById(
            $command->getAcademicRegistrationNumber()
        );

        $article = Article::create(
            $this->academicRepository->nextArticleID(),
            $command->getTitle(),
            $command->getBody(),
            $command->getAcademicRegistrationNumber(),
            new \DateTimeImmutable()
        );

        $academic->write($article);
        $this->academicRepository->update($academic);

        return $academic;
    }
}
<?php

declare(strict_types=1);

namespace App\Integration\Academic\Repository;
 // ...

final class AcademicQueryBuilderRepository implements AcademicRepository
{
   // ...
    public function __construct(
        DatabaseManager $databaseManager,
        SerializeAcademic $serializeAcademic,
        HydrateAcademic $hydrateAcademic,
        LoggerInterface $logger
    ) {
        $this->logger = $logger;
        $this->databaseManager = $databaseManager;
        $this->serializeAcademic = $serializeAcademic;
        $this->hydrateAcademic = $fromArrayMapper;
    }
// ...

    public function update(Academic $academic): void
    {
        $rawAcademic = $this->serializeAcademic->withPassword($academic);
        $rawArticles = $rawAcademic['articles'];
        $rawAcademicWithoutArticles = $rawAcademic;
        unset($rawAcademicWithoutArticles['articles']);

        $this->databaseManager->beginTransaction();
        try {
            foreach ($rawArticles as $rawArticle) {
                $this->databaseManager
                    ->table(ArticleQueryBuilderRepository::TABLE_NAME)
                    ->updateOrInsert(['id' => $rawArticle['id']], $rawArticle);
            }
            $this->databaseManager->table(self::TABLE_NAME)
                 ->where('id', '=', (string) $academic->registrationNumber())
                 ->update($rawAcademicWithoutArticles);
            $this->databaseManager->commit();
        } catch (QueryException $e) {
            $this->databaseManager->rollback();
            $this->logger->error(
                'database failure', 
                ['exception' => $e, 'academic' => $rawAcademic]
            );
            throw new ImpossibleToSaveAcademic($e);
        }
    }
}

Example:
Review Article

Let's review the questions

That's all folks

christian.nastasi@gmail.com   damianopetrungaro@gmail.com

Feel free to contact us

Letture:
https://bit.ly/2KJgeuq

Joind:
https://goo.gl/kx6kwT