Skip to content

Commit

Permalink
array collection
Browse files Browse the repository at this point in the history
  • Loading branch information
priyadi committed Jul 7, 2024
1 parent c5157a5 commit 2eb0708
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 3 deletions.
134 changes: 134 additions & 0 deletions docs/collections/02-implementations/04-array-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
title: ArrayCollection
---

Modification of Doctrine's `ArrayCollection`, so that it does `matching()`
against the private properties directly, not against the return values of their
getters.

## Installation

```bash
composer require rekalogika/collections-domain
```

## Usage

Simply substitute `Doctrine\Common\Collections\ArrayCollection` to
`Rekalogika\Domain\Collections\ArrayCollection`:

```diff
- use Doctrine\Common\Collections\ArrayCollection;
+ use Rekalogika\Domain\Collections\ArrayCollection;
```

It should be safe to do a mass find-and-replace in all of your entities.

## Description

Doctrine ORM does the `matching()` against the entities' private properties
directly. While `ArrayCollection` does it against the return values of the
getters.

Therefore, there will be a 'mismatching' in the collection's behavior between
when the instance is an `ArrayCollection` (when the owning entity is new) and
when it is a `PersistentCollection` (when the owning entity is hydrated from the
database).

The problem happens when, for example:

* The property does not have a getter, or
* The getter returns a different value or different type from the property's
value, or
* The getter contains business logic, and does not return the property's value
as is.

The problem usually happens with new, not-yet-persisted entities, and in unit
tests where the tests don't involve the database.

Our `ArrayCollection` changes the behavior so that it does the `matching()`
against the private properties directly.

## Limitation

The problem will also happen with `fetch` set to `EAGER`, or when the collection
is initialized before the `matching()` is called. Unfortunately, it is
impossible to fix using this solution.

However, if you can afford to fetch the collection eagerly, then you can afford
to use `filter()` instead. Unlike `matching()`, `filter()` is always consistent
in all cases.

## Example

The following classes implement the [null object
pattern](https://en.wikipedia.org/wiki/Null_object_pattern). If the
`nationality` property is `null`, it will return an instance of `Stateless`
instead of `null`:

```php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

class Country {}
class Stateless extends Country {}

class Player
{
public function __construct(
private string $name,
private Team $team,
private ?Country $nationality = null,
) {
}

public function getName(): string
{
return $this->name;
}

public function getTeam(): Team
{
return $this->team;
}

public function getNationality(): Country
{
return $this->country ?? new Stateless();
}
}

class Team
{
/** @var Collection<int,Player> */
private Collection $players;

public function __construct()
{
$this->players = new ArrayCollection();
}

/**
* @return Collection<int,Player>
*/
public function getPlayers(): Collection
{
return $this->players;
}

public function getStatelessPlayers(): Collection
{
return $this->players->matching(
Criteria::create()
->where(Criteria::expr()->isNull('nationality'))
);
}
```

If the `Team` is not yet persisted, the `getStatelessPlayers()` method will
incorrectly return an empty collection every time. But if the `Team` object is
hydrated from the database, it will correctly return the players without a
nationality.

Changing the code to use `Rekalogika\Domain\Collections\ArrayCollection` will
resolve the problem.
7 changes: 4 additions & 3 deletions docs/collections/04-misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ the headaches:

* You still have compact, binary UUIDs in the database.
* You still have time-ordered UUIDs.
* You still have the opportunity to work with object-based UUIDs in your PHP
code.
* You still have the means to work with object-based UUIDs in your PHP code
using the `getUuid()` method.
* You don't need to change how you work with `QueryBuilder`'s `setParameter()`.
* `Collection` key is now usable. You no longer need to choose whether to use
* `Collection` key is now usable. You will be able to reliably call
`$collection->get($id)`. You no longer need to choose whether to use
`toRfc4122()` or `toBinary()` depending on the database driver, or even
depending on whether the `Collection` is lazily loaded or not.
* If you previously use object-based UUIDs, it should not be difficult to
Expand Down

0 comments on commit 2eb0708

Please sign in to comment.