<?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"
]
];
It's an immutable object representing a value in a domain.
It's comparable to other value objects by its own values.
It's a mutable object with an identity, it can be represented in different ways.
It's comparable to other entities by its ID.
It's an abstraction layer to provide access to all the entities and the value objects related to an aggregate.
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.
It's an immutable object used to represent something important that occurred in the domain.
<?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()
{
}
}
<?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);
}
}
}
christian.nastasi@gmail.com damianopetrungaro@gmail.com