innmind / doctrine
Declarative approach to eliminate state where not necessary
Requires
- php: ~8.2
- doctrine/orm: ^2.7
- innmind/immutable: ~4.0|~5.0
- innmind/specification: ~4.0
- ramsey/uuid: ^4.0
Requires (Dev)
- innmind/black-box: ^5.5.2
- innmind/coding-standard: ~2.0
- phpunit/phpunit: ~10.2
- scienta/doctrine-json-functions: ^4.3
- symfony/cache: ^5.0
- symfony/console: <7.0
- vimeo/psalm: ~5.6
Suggests
- scienta/doctrine-json-functions: To be able to search within a json field
README
Important
This project has been a stepping stone for the Formal ORM. It's no longer actively maintained and will be archived at some point.
This library is an abstraction on top of Doctrine with the intention to remove all implicit states.
Managing the state in an application can become hard when the codebase grows and states (especially implicit ones) is one of the source of bugs in applications.
Doctrine is usually the default choice (not an actual fact) when interacting with databases as it comes bundled with Symfony. Its interface cover many use cases but expose many implicit states such as transactions or cursors in collections.
This is solved here by using principles from the functional programming paradigm.
Installation
composer require innmind/doctrine
Design choices
Click to expand
Sequence
vs Set
Set
has been discarded for this library as the unicity of entities cannot be guaranted from the returned collections. It also would prevent the use of the map
function as many entities may be mapped to a single new value, this may lead to unexpected behaviour for newcomers to such paradigm. This is mainly why the choice has been toward Sequence
.
In case you really want to use sets, you may use innmind/immutable
.
Enforcing the use of an Id
Doctrine allows you to generate an id for you when your entities are persisted. This is an implicit state change.
In order to avoid this implicit you need to specify the id before persisting your entities. This prevents you from relying on the auto generated id from your database as you can't avoid collisions.
The unique solution (that I'm aware of) is to use UUID
s. The Id
provided by this library use them so you don't have to think of it anymore.
A single Id
class for all entities
This is no longer a problem as it is provided with a template understood by vimeo/psalm
.
No flush
method on the Manager
Being free to call the persist
and flush
methods when you wish it opens the door to implicit states in your codebase. You may end up either flushing unwanted persisted entities (persist
calls before an error occured) or forgetting to flush
persisted entities (resulting in lost state change).
Here this is avoided by forcing to execute all mutations in a given context (via Manager::mutate()
and Manager::transaction()
). So it's always all or nothing.
Usage
All the use cases below use the code declared in the example
folder.
Pre-requisite for all use cases:
use Innmind\Doctrine\{ Manager, Sort, }; use Example\Innmind\Doctrine\User; $manager = Manager::of($entityManager);
Fetching all entities from the database
$manager ->repository(User::class) ->all() ->sort('username', Sort::asc) ->fetch() ->foreach(function(User $user): void { echo $user->username()."\n"; });
Note: The queries are delayed to the last moment possible to leverage the database as most as possible.
Pagination
$numberOfElementPerPage = 10; $manager ->repository(User::class) ->all() ->sort('username', Sort::asc) ->drop($page * $numberOfElementPerPage) ->take($numberOfElementPerPage) ->fetch() ->foreach(function(User $user): void { echo $user->username()."\n"; });
Filtering
It uses the Specification
pattern (normalized in the library innmind/specification
).
use Example\Innmind\Doctrine\Username; $manager ->repository(User::class) ->matching( Username::of('alice')->or( Username::of('jane'), ), ) ->sort('username', Sort::asc) ->drop(20) ->take(10) ->fetch() ->foreach(function(User $user): void { echo $user->username()."\n"; });
This example is the equivalent of SELECT * FROM user WHERE username = 'alice' OR username = 'jane' ORDER BY username OFFSET 20 LIMIT 10
.
Note: This chain of method calls result once again in a single database call.
Adding new entities
use Innmind\Doctrine\Id; use Innmind\Immutable\Either; $user = $manager->mutate(function($manager): Either { $user = new User( Id::new(), 'someone', ); $manager ->repository(User::class) ->add($user); return Either::right($user); });
If you try to call Repository::add()
or Repository::remove()
outside the function it will raise an exception.
Note: If the function throws an exception or an Either::left
is returned then nothing will be flushed to the database.
Transactions
$manager->transaction(function($manager, $flush): Either { $progress = 0; $repository = $manager->repository(User::class); foreach ($someSource as $args) { $repository->add(new User(...$args)); ++$progress; if ($progress % 20 === 0) { // flush entities to the database every 20 additions $flush(); } } return Either::right(null); });
Note: Call the $flush
function only when in a context of imports as it will detach all the entities from entity manager, meaning if you kept references to entities they will no longer be understood by doctrine
Accessing the values inside a sequence
Sometimes you may want to manipulate an array so it can be used with php functions such as json_encode
.
use Symfony\Component\HttpFoundation\JsonResponse; /** @var list<array{username: string, registerIndex: int}> */ $data = $manager ->repository(User::class) ->all() ->sort('registerIndex', Sort::asc) ->fetch() ->map(static fn(User $user): array => [ 'username' => $user->username(), 'registerIndex' => $user->registerIndex(), ]) ->toList(); new JsonResponse($data);
Filtering on relations
You can specify the property of an entity relationship in a specification property field.
use Example\Innmind\Doctrine\Child; $users = $manager ->repository(User::class) ->matching( Child::of('alice')->or( Child::of('jane'), ), );
The Child
specification use the property children.username
thus specifying the username of a user's children.
Note: for now only one level relationship is allowed in a specification property.
Lazy loading collections
In certain cases the amount of entities you will fetch won't fit in memory. To still work with this king of scenario you need to use a lazy Sequence
and perform periodic clears.
$_ = $manager ->repository(User::class) ->all() ->lazy() // instruct to load one entity at a time ->fetch() ->foreach(function($user) use ($manager) { doStuff($user); // this clear is important to make doctrine forget about the loaded // entities and will consequently free memory $manager->clear(); });
Note: this feature won't work however if you use bi-directional relationships in your entities, for some reason doctrine won't free memory.
Note 2: you should only use this feature when reading data. Using this in a write context may have unexpected side effects!