Revision: Thu, 18 Apr 2024 09:24:13 GMT

Relation collections

Collection factory is responsible for creation and filling *Many relation collections.

Cycle ORM used to use doctrine/collections as a default collection for *Many relations, but since v2.x it doesn't use doctrine/collections out of the box anymore. Now you have an ability to opt which collection type will be used for *Many relations by default and which for specific entities relations.

Collection factories

Collection factory class Description Required package Since ORM
Cycle\ORM\Collection\ArrayCollectionFactory Uses PHP array as collection Always available 2.0
Cycle\ORM\Collection\DoctrineCollectionFactory Doctrine ArrayCollection doctrine/collections 2.0
Cycle\ORM\Collection\IlluminateCollectionFactory Laravel collection illuminate/collection 2.0
Cycle\ORM\Collection\LoophpCollectionFactory Loophp collection loophp/collection 2.2

Note
ArrayCollectionFactory has some limitations when it is using with the Proxy Mapper and lazy loading.

php
$post->tags[] = new Tag(); // If the relation "tags" isn't loaded then the tag won't be added

In this case you should be sure the relation is loaded.

php
$post->tags; // Load relation
$post->tags[] = new Tag();

Note
DoctrineCollectionFactory can make an extended ArrayCollection instance that implements the PivotedCollectionInterface and can store pivote data for Many to Many relations.

Note
LoophpCollectionFactory supports only collections of loophp\collection\Collection class. To use a custom implementation of the interface loophp\collection\Contract\Collection you should make a custom collection factory.

Configuration

By default, Cycle ORM uses Cycle\ORM\Collection\ArrayCollectionFactory.

php
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Relation\HasMany;

#[Entity]
class User
{
    // ...

    #[HasMany(target: Post::class)]
    public array $posts;
}

In order to use alternate collection type by default, you need to pass desired collection factory as a second argument (defaultCollectionFactory) to Cycle\ORM\Factory object:

php
use Cycle\ORM;

$schema = new ORM\Schema(...);

$factory = (new ORM\Factory(
    dbal: $dbal,
    defaultCollectionFactory: new ORM\Collection\ArrayCollectionFactory    // Default collection factory
))
    // requires doctrine/collections package
    ->withCollectionFactory(
        'doctrine',                                         // Alias
         new ORM\Collection\DoctrineCollectionFactory,
         \Doctrine\Common\Collections\Collection::class    // <= Base collection
    )

    // requires illuminate/collections package
    ->withCollectionFactory(
        'illuminate',
        new ORM\Collection\IlluminateCollectionFactory,
        \Illuminate\Support\Collection::class
    );

$orm = new ORM\ORM(
    factory: $factory,
    schema: $schema
);

Method Factory::withCollectionFactory returns a new, immutable Factory object, and you need to rebind factory for Cycle\ORM\ORM object after adding a new collection factory.

php
use Cycle\ORM;

$orm = new ORM\ORM(...);

$container = new Container();
$container->bindSingleton(ORM\ORMInterface::class, $orm);

$factory = $orm->getFactory()
    ->withCollectionFactory(
        'doctrine',
         new ORM\Collection\DoctrineCollectionFactory,
          \Doctrine\Common\Collections\Collection::class
    );

$orm = $orm->with(factory: $factory);

$container->bindSingleton(ORM\ORMInterface::class, $orm);

Relation collection type definition

php
class CommentCollection extends \Doctrine\Common\Collections\ArrayCollection {
    public function filterActive(): self { /* ... */ }
    public function filterHidden(): self { /* ... */ }
}

$schema = [
    User::class => [
        //...
        Schema::RELATIONS   => [
            'posts' => [
                Relation::TYPE => Relation::HAS_MANY,
                Relation::TARGET => Post::class,
                Relation::COLLECTION_TYPE => null, // <= Will be used a default collection factory
                Relation::SCHEMA => [ /*...*/ ],
            ],
            'comments' => [
                Relation::TYPE => Relation::HAS_MANY,
                Relation::TARGET => Comment::class,
                Relation::COLLECTION_TYPE => 'doctrine', // <= Will be used collection factory with alias doctrine
                Relation::SCHEMA => [ /*...*/ ],
            ],
            'tokens' => [
                Relation::TYPE => Relation::HAS_MANY,
                Relation::TARGET => Token::class,
                Relation::COLLECTION_TYPE => \Doctrine\Common\Collections\Collection::class, // <= Will be used collection factory with matching by base class
                Relation::SCHEMA => [ /*...*/ ],
            ]
        ]
    ],
    Post::class => [
        //...
        Schema::RELATIONS   => [
            'comments' => [
                Relation::TYPE => Relation::HAS_MANY,
                Relation::TARGET => Comment::class,
                Relation::COLLECTION_TYPE => CommentsCollection::class, // <= Will be used collection factory with matching by base class
                Relation::SCHEMA => [ /*...*/ ],
            ]
        ]
    ]
];

Accessing Collection

The ORM will automatically instantiate a collection instance for your relations, however, you are still required to initiate empty collections in your constructor to use newly created entities:

php
use Doctrine\Common\Collections\ArrayCollection;
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Relation\HasMany;

#[Entity]
class User
{
    // ...

    #[HasMany(target: Post::class, collection: 'doctrine')]
    public ArrayCollection $posts;

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

The collection property will be set automatically on the selection:

php
$user = $orm->getRepository(User::class)
    ->select()
    ->with('posts')->limit(1)->fetchOne();

print_r($user->posts);

Collection API

You can create your own collection factories by implementing Cycle\ORM\Collection\CollectionFactoryInterface interface

php
use Cycle\ORM\Collection\CollectionFactoryInterface;

class ArrayCollectionFactory implements CollectionFactoryInterface
{
    public function withCollectionClass(string $class): static
    {
        // Do nothing
        return $this;
    }

    public function collect(iterable $data): array
    {
        return match (true) {
            \is_array($data) => $data,
            $data instanceof \Traversable => \iterator_to_array($data),
            default => throw new CollectionFactoryException('Unsupported iterable type.'),
        };
    }
}
Edit this page