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 usedoctrine/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 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 extendedArrayCollection
instance that implements thePivotedCollectionInterface
and can store pivote data for Many to Many relations.
Note
LoophpCollectionFactory
supports only collections ofloophp\collection\Collection
class. To use a custom implementation of the interfaceloophp\collection\Contract\Collection
you should make a custom collection factory.
By default, Cycle ORM uses Cycle\ORM\Collection\ArrayCollectionFactory
.
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:
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 forCycle\ORM\ORM
object after adding a new collection factory.
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);
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 => [ /*...*/ ],
]
]
]
];
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:
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:
$user = $orm->getRepository(User::class)
->select()
->with('posts')->limit(1)->fetchOne();
print_r($user->posts);
You can create your own collection factories by implementing Cycle\ORM\Collection\CollectionFactoryInterface
interface
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.'),
};
}
}