Open-Source
Je partage certaines de mes créations et connaissances accumulées au fil des années sous la forme de bibliothèques de code, principalement destinées à PHP et Symfony.
Elles faciliteront le développement et le respect des bonnes pratiques au sein de vos applications... :)
Écosystème Symfony
Doctrine Specifications
Cette surcouche du QueryBuilder de Doctrine vous permettra de réduire les duplications de code dans vos repositories Doctrine, les rendant plus fiables, et d'écrire des requêtes expressives grâce à des appels fluides :
// We want all published articles in a given category
$publishedCategoryArticles = ManyArticles::asEntity()
->postedByUser($userId)
->byCategory($category)
->orderedByDateDesc();
// Conditionnal pagination
if ($pageNumber) {
$publishedCategoryArticles->paginate($pageNumber, 10);
}
// Retrieve articles from the db
$articles = $repository->find($publishedCategoryArticles);
En lire plus Moins
Vos conditions ne sont plus dupliquées entre différentes méthodes de recherche — vos repositories restent ainsi bien organisés et facilement testables :
final class ManyArticles extends SpecificationCompound
{
public static function asEntity() : self
{
return new self(
SpecificationRepositoryResult::MANY_OBJECTS,
SelectEntity::specification(Article::class, 'article'),
);
}
public function postedByUser(UserId $userId) : self
{
$this->whereFieldEqual('article.author', 'userId', $userId);
return $this;
}
public function byCategory(Category $category) : self
{
$this->whereFieldEqual('article.categoryId', 'categoryId', $category->getId());
return $this;
}
public function orderedByDateDesc() : self
{
$this->orderResultsByDesc('article.publishedAt');
// Equivalent to:
// $doctrineQuerybuilder->addOrderBy('article.publishedAt', 'DESC');
return $this;
}
public function paginate(int $page, int $itemPerPage) : self
{
$this->limitResultsPaginate($page, $itemPerPage);
// Equivalent to:
// $query->setMaxResults($itemPerPage);
// $query->setFirstResult(($page - 1) * $itemsPerPage);
return $this;
}
}
Si besoin, hydratez très facilement des classes personnalisées (DTO) à la place des entités pour optimiser vos requêtes :
final class ArticleModel implements SpecificationReadModel
{
public function __construct(
public readonly int $id,
public readonly string $title,
public readonly string $content,
public readonly int $categoryId,
public readonly string $categoryName,
) {}
public static function getDqlConstructorArguments(): array
{
return [
'article.id',
'article.title',
'article.content',
'category.id',
'category.name',
];
}
}
final class ManyArticles extends SpecificationCompound
{
public static function asModel() : self
{
return new self(
SpecificationRepositoryResult::MANY_OBJECTS,
SelectReadModel::specification(Article::class, 'article', ArticleModel::class),
JoinLeft::specification('article.category', 'category') // Join declaration
);
}
// ...
}
Twig Powerpack
Améliorez la fiabilité et la clarté de vos templates Twig en déclarant explicitement les variables attendues, pour valider automatiquement leur existence et leur type :
{% extends 'Layout.twig' %}
{% expect 'string' as TITLE %}
{% expect 'bool' as ENABLED %}
{% expect 'float' as AMOUNT %}
{% expect 'int' as COUNT %}
{% expect array of 'int' as IDS %}
{% expect 'App\\Classes\\Foo' as FOO %}
{% expect nullable 'App\\Classes\\Foo' as FOO_OR_NULL %}
Déclarez des ressources globales dans vos pages pour y accéder à partir de n'importe quel autre template :
{% extends 'Layout.twig' %}
{% register 'has-menu' in 'bodyClasses' %}
{% register 'responsive' in 'bodyClasses' %}
{% register '/js/custom-scripts.js' in 'scripts' %}
{% register '/css/few-styles.css' in 'styles' %}
{% register once '/css/some-styles.css' in 'styles' %}
{% register once '/css/some-styles.css' in 'styles' %} <!-- will not be included twice -->
<html>
<head>
...
{% for css in registry('styles') %}
<link rel="stylesheet" href="{{ css }}" />
{% endfor %}
<!-- <link rel="stylesheet" href="/css/few-styles.css" /> -->
<!-- <link rel="stylesheet" href="/css/some-styles.css" /> -->
</head>
<body class="{{ registry('bodyClasses')|join(' ') }}">
<!-- <body class="has-menu responsive"> -->
...
{% for js in registry('scripts') %}
<script src="{{ js }}"></script>
{% endfor %}
<!-- <script src="/js/custom-scripts.js"></script> -->
</body>
</html>
Symfony Easy API
Construisez vos API rapidement à partir de simples controlleurs Symfony.
/**
* @Route("/api/things", name="api_things", methods={"GET"})
*/
final class ApiEndpointController
{
public function __invoke(EasyApi $easyApi, ThingRepository $thingRepository) : Response
{
return $easyApi->response(
function() use ($thingRepository)
{
$things = $thingRepository->findAll();
return ApiPayload200Success::createWithArrayResult($things);
}
);
}
}
En lire plus Moins
Les réponses suivent un format normalisé et les erreurs sont gérées automatiquement :
{
success: true,
status: "ok",
statusCode: 200,
payload: {
results: [
{ id: 1, name: "First thing" },
{ id: 2, name: "Second thing" },
{ id: 3, name: "Third thing" }
],
resultsCount: 3,
resultsCountTotal: 3,
page: 1,
pageCount: 1
}
}
{
success: false,
status: "not_found",
statusCode: 404,
error: "not_found",
errorDescription: "Thing not found (id: 1)",
errorCode: 0
}
{
success: false,
status: "server_error",
statusCode: 500,
error: "server_error",
errorDescription: "Unexpected server error: Invalid `$thingId` value (-1)",
errorCode: 0
}
Types & Value Objects
Small UID
Un identifiant unique similaire à un ULID mais moitié moins grand (64 bits), utile lorsqu'on a besoin de concision (au coût d'un peu d'unicité).
$uid = SmallUid::random();
$shortString = (string)$uid; // string(11) "LscmjzUyKLR"
$hexString = (string)$uid->getHex(); // string(16) "1234567890abcdef"
Types collections
Cette bibliothèque propose des collections fortement typées pour les types primitifs de PHP.
Leurs méthodes chaînables offrent une alternative aux fonctions PHP natives de manipulation des tableaux, tout en étendant leurs fonctionnalités.
IntCollection::fromArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
->where(fn($n) => $n > 4)
->append(10)
->select(static fn($n) => $n * 10)
->forEach(static fn(int $n) => var_dump($n));
// Outputs:
// int(50)
// int(60)
// int(70)
// int(80)
// int(90)
// int(100)
Créez vos propres collections typées pour vos classes en quelques lignes :
/*
* @extends ClassCollection<Foo>
*/
class FooCollection extends ClassCollection
{
protected static function classFqcn() : string
{
return Foo::class
}
}
// Examples of usage
FooCollection::new();
FooCollection::fromArray([new Foo(), new Foo()]);
FooCollection::fromArray([new Foo(), new Bar()]); // throws InvalidCollectionItemException
Types common
Fournit de nombreux Value-objects génériques pour enrichir les types primitifs de PHP :
Name::fromString('Bruce');
Slug::slugify('Héllo world !'); // hello-world
Url::fromString('https://mediagone.fr/open-source');
Color::fromRgb(255, 128, 76); // #ff804c
...
Typez vos propriétés et les arguments de vos méthodes pour renforcer la robustesse de votre code :
function sendMail(EmailAdress $to, Title $title, Text $content): void {
//...
}
Types enums (pour PHP 7.4+)
Fournit des classes fortement typées permettant de simuler les enums de PHP 8.1.
/**
* @method static ArticleStatus DRAFT
* @method static ArticleStatus PUBLISHED
*/
final class ArticleStatus extends EnumInt
{
private const DRAFT = 0
private const PUBLISHED = 1
}
// Enum values are accessible using static methods which return an instance of ArticleStatus
ArticleStatus::DRAFT()
ArticleStatus::PUBLISHED()
// Returned enum instances are singletons — strict identity comparison works
ArticleStatus::DRAFT() === ArticleStatus::DRAFT() // true
ArticleStatus::PUBLISHED() === ArticleStatus::DRAFT() // false
CQRS Bus
Remplacez les appels directs à vos services par des Commands et des Queries et bénéficiez des fonctionnalités de middleware : transactions de base de données automatiques, journalisation, sécurité...
Plus léger et simple d'utilisation que symfony/messenger, il est particulièrement adapté aux messages internes à une application, par exemple dans une architecture DDD.
Les deux systèmes étant complémentaires : CQRS Bus pour les échanges internes (Commands, Queries, Events...) et Messenger pour la communication avec des systèmes externes (RabbitMQ, etc.).