Open-Source
I share some of my creations and knowledge gathered over the years through code libraries, primarily aimed at PHP and Symfony.
They'll facilitate development and adherence to good practices within your applications... :)
Symfony Ecosystem
Doctrine Specifications
This layer on top of Doctrine's QueryBuilder will help you reduce code duplication in your Doctrine repositories, making them more reliable, and write expressive queries through fluent method calls:
// 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);
More Less
Your criteria are no longer duplicated across different search methods — your repositories stay well organized and easily testable:
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;
}
}
If needed, hydrate custom classes (DTO) instead of entities to optimize your queries:
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
Improve the reliability and clarity of your Twig templates by explicitly declaring the expected variables, to automatically validate their existence and 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 %}
Declare global resources in your pages to access them from any another template:
{% extends 'Layout.twig' %}
{% register 'has-menu' in 'bodyClasses' %}
{% register 'responsive' in 'bodyClasses' %}
{% register '/css/few-styles.css' in 'styles' %}
{% register once '/css/some-styles.css' in 'styles' %}
{% register once '/js/custom-scripts.js' in 'scripts' %}
<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
Quickly build your APIs using plain Symfony controllers.
/**
* @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);
}
);
}
}
More Less
Responses follow a normalized format and errors are handled automatically:
{
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
A unique identifier similar to a ULID but half the size (64 bits), useful when you need conciseness (at the cost of a little uniqueness).
$uid = SmallUid::random();
$shortString = (string)$uid; // string(11) "LscmjzUyKLR"
$hexString = (string)$uid->getHex(); // string(16) "1234567890abcdef"
Types collections
This library provides strongly typed collections for PHP's primitive types, whose chainable methods offer an advantageous alternative to native PHP array manipulation functions, while extending their capabilities.
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)
Also create your own typed collections for your classes in just a few lines:
/*
* @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
Provides many generic Value Objects to enrich PHP's primitive types:
Name::fromString('Bruce');
Slug::slugify('Héllo world !'); // hello-world
Url::fromString('https://mediagone.fr/');
Color::fromRgb(255, 128, 76); // #ff804c
...
Type your properties and method arguments to strengthen the robustness of your code:
function sendMail(EmailAdress $to, Title $title, Text $content): void {
//...
}
Types enums (for PHP 7.4+)
Provides strongly typed classes to simulate PHP 8.1 enums.
/**
* @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
Replace direct service calls with Commands and Queries and benefit from middleware features: automatic database transactions, logging, security...
Lighter and easier to use than symfony/messenger, it is particularly well-suited for messages internal to an application, eg. in a DDD architecture.
Both systems being complementary: CQRS Bus for internal exchanges (Commands, Queries, Events...) and Messenger for communication with external systems (RabbitMQ, etc.).