Doctrine Repository Monadic Helper
Description
This project provides the necessary classes and services to use Doctrine repositories in a more functional way, by using monads.
This project also demonstrate that it's a nice and clean way to work with repositories and non-deterministic data store, in this case, a database.
There is no need to always check for the existence of an entity, so we are able to reduce the amount of conditions and cruft, while focusing on what's important and relevant only.
When using properly typed monads and callbacks, types inconsistencies will be instantly detected by static analysis tools. This provides a safer and better way to design functions and data transformation methods.
The monad in use in this project is the Either monad, provided by the contrib package Lamphpda from Marco Perone.
Installation
composer require loophp/repository-monadic-helper
Usage
To use this package and use monadic repositories, you can choose between 3 different ways:
- Without alteration of existing repositories
- By using an independent service which is creating a monadic repository from an entity class name or an existing repository.
- With alteration of existing repositories
- Upgrade repositories with an interface and a trait and add relevant typing information like:
@implements MonadicServiceEntityRepositoryInterface<EntityClassName>
- Replace
extends ServiceEntityRepository
withextends MonadicServiceEntityRepository
and add relevant typing information like:@extends MonadicServiceEntityRepository<EntityClassName>
- Upgrade repositories with an interface and a trait and add relevant typing information like:
According to me, the best way to use this package is to use the first option.
By using a dedicated service
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\MyCustomEntity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Throwable;
final class MyCustomController
{
public function __invoke(
MonadicStandardRepositoryFactoryInterface $monadicStandardRepositoryFactory
) {
$body = $monadicStandardRepositoryFactory
->fromEntity(MyCustomEntity::class);
->find(123) // This returns a Either monad.
->map(
static fn (MyCustomEntity $entity): string => $entity->getTitle()
)
->eval(
static fn (Throwable $exception): string => $exception->getMessage(),
static fn (string $entity): string => $entity
);
return new Response($body);
}
}
By altering existing Doctrine repositories
Upgrade your Doctrine repositories from:
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\MyCustomEntity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method MyCustomEntity|null find($id, $lockMode = null, $lockVersion = null)
* @method MyCustomEntity|null findOneBy(array $criteria, array $orderBy = null)
* @method MyCustomEntity[] findAll()
* @method MyCustomEntity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class MyCustomEntityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MyCustomEntity::class);
}
}
To:
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\MyCustomEntity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use loophp\RepositoryMonadicHelper\MonadicServiceEntityRepository;
use Throwable;
/**
* @extends MonadicServiceEntityRepository<MyCustomEntity>
*/
class MyCustomEntityRepository extends MonadicServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, MyCustomEntity::class);
}
}
Update the way you're using Repositories from:
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\MyCustomEntity;
use App\Repository\MyCustomEntityRepository;
use Symfony\Component\HttpFoundation\Response;
final class MyCustomController
{
public function __invoke(MyCustomEntityRepository $myCustomEntityRepository): Response
{
$id = /* Whatever value */;
/** @var null|MyCustomEntity $myCustomEntity */
$myCustomEntity = $myCustomEntityRepository->find($id);
if ($myCustomEntity === null) {
return new Response('No entity found with such ID.'),
}
return new Response(sprintf('Entity ID found, title: %s', $myCustomEntity->getTitle()))
}
}
To:
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\MyCustomEntity;
use App\Repository\MyCustomEntityRepository;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
final class MyCustomController
{
public function __invoke(MyCustomEntityRepository $myCustomEntityRepository): Response
{
$id = /* Whatever value */;
$responseBody = $myCustomEntityRepository
->eitherFind($id)
->eval(
fn (Throwable $exception): string => $exception->getMessage(),
fn (MyCustomEntity $myCustomEntity): string =>
sprintf('Entity ID found, title: %s', $myCustomEntity->getTitle())
);
return new Response($responseBody);
}
}
Todo
- Get rid of PHPStan baseline as soon as this PR is released,
- Improve documentation and code examples.
Contributing
Feel free to contribute by sending Github pull requests. I'm quite responsive :-)
If you can't contribute to the code, you can also sponsor me on Github.
Changelog
See CHANGELOG.md for a changelog based on git commits.
For more detailed changelogs, please check the release changelogs.