Modern PHP development with Laravel
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