Let's look at an example. We have an entity User, which is related to other entities by the relation HasOne and
HasMany:
User {
id: int
name: string
profile: ?Profile (HasOne, nullable, lazy load)
posts: collection (HasMany, lazy load)
}
When we load the entity User using code $user = (new Select($this->orm, User::class))->fetchOne(); (without eager
loading of related entities), then we get the User entity, in which relations to other entities are references
(objects of the ReferenceInterface class).
In Cycle ORM v1, users faced issues when these references had to be resolved. Yes, sometimes it is more expedient
to load the relation of one entity from a large collection than to preload relations for the entire collection.
Our separate package cycle/proxy-factory could help with this issue,
the task of which is to replace Reference with a proxy object.
When accessing such a proxy object, the reference is automatically resolved:
$email = $user->profile->email; // when accessing the profile property, the proxy object automatically
// makes a request to the databaseHowever, in the case of a nullable One to One relation, we cannot use this code:
$userHasProfile = $user->profile === null;Indeed, the proxy $user->profile will not be able to rewrite itself into null if the required profile does not
exist in the DB.
There were also problems with typing: in the User class it is not possible to set the profile property with the
?Profile type, because ORM without eager loading tries to write ReferenceInterface there.
We have changed a few things in Cycle ORM v2. Now all entities are created as proxies by default.
The advantages that we get by doing it:
- The user in the usual use will not encounter the
ReferenceInterface. - Property typing works:
class User { public iterable $posts; private ?Profile $profile; public function getProfile(): Profile { if ($this->profile === null) { $this->profile = new Profile(); } return $this->profile; } }
- We have preserved the usability of references for those who used them:
To get raw entity data, use the mapper:
/** @var \Cycle\ORM\ORMInterface $orm */ // Create a proxy for the User entity $user = $orm->make(User::class, ['name' => 'John']); // We know the group id, but we don't want to load it from DB. // This is enough for us to fill in the User>(belongsTo)>Group relation $user->group = new \Cycle\ORM\Reference\Reference('user_group', ['id' => 1]); (new \Cycle\ORM\Transaction($orm))->persist($user)->run(); $group = $user->group; // if desired, we can load a group from the heap or database using our Reference
$rawData = $mapper()
The rules for creating entities are determined by their mappers. You can set which entities will be created as proxies and which ones will not.
Mappers from the box:
\Cycle\ORM\Mapper\Mapper- generates proxies for entity classes.\Cycle\ORM\Mapper\PromiseMapper- works directly with the entity class. It also writes objects of the\Cycle\ORM\Reference\Promiseclass to unloaded relation properties.\Cycle\ORM\Mapper\StdMapper- for working with classless entities. GeneratesstdClassobjects with\Cycle\ORM\Reference\Promiseobjects on unloaded relation properties.\Cycle\ORM\Mapper\ClasslessMapper- for working with classless entities. Generates proxy entities.
To use proxy entities, you need to follow a few simple rules:
- Entity classes should not be final.
The proxy class extends the entity class, and we would not like to use hacks for this. - Do not use code like this in the application:
get_class($entity) === User::class. Use$entity instanceof User. - Write the code of the entity without taking into account the fact that it can become a proxy object.
Use typing and private fields.
Even if you directly access the$this->profilefield, the relation will be loaded and you will not get aReferenceInterfaceobject.
We've added support for custom collections for the hasMany and ManyToMany relations.
Custom collections can be configured individually for each relation by specifying aliases and interfaces (base classes):
use Cycle\ORM\Relation;
$schema = [
User::class => [
//...
Schema::RELATIONS => [
'posts' => [
Relation::TYPE => Relation::HAS_MANY,
Relation::TARGET => Post::class,
Relation::COLLECTION_TYPE => null, // <= The default collection is used
Relation::SCHEMA => [ /*...*/ ],
],
'comments' => [
Relation::TYPE => Relation::HAS_MANY,
Relation::TARGET => Comment::class,
Relation::COLLECTION_TYPE => 'doctrine', // <= Matching by the alias `doctrine`
Relation::SCHEMA => [ /*...*/ ],
],
'tokens' => [
Relation::TYPE => Relation::HAS_MANY,
Relation::TARGET => Token::class,
Relation::COLLECTION_TYPE => \Doctrine\Common\Collections\Collection::class, // <= Matching by the class
Relation::SCHEMA => [ /*...*/ ],
]
]
],
Post::class => [
//...
Schema::RELATIONS => [
'comments' => [
Relation::TYPE => Relation::HAS_MANY,
Relation::TARGET => Comment::class,
Relation::COLLECTION_TYPE => \App\CommentsCollection::class, // <= Mapping by the class of
// an extendable collection
Relation::SCHEMA => [ /*...*/ ],
]
]
]
];Aliases and interfaces can be configured in the \Cycle\ORM\Factory object,
which is passed to the ORM class constructor.
$arrayFactory = new \Cycle\ORM\Collection\ArrayCollectionFactory();
$doctrineFactory = new \Cycle\ORM\Collection\DoctrineCollectionFactory();
$illuminateFactory = new \Cycle\ORM\Collection\IlluminateCollectionFactory();
$orm = new \Cycle\ORM\ORM(
(new \Cycle\ORM\Factory(
$dbal,
null,
null,
$arrayFactory // <= Default Collection Factory
))
->withCollectionFactory(
'doctrine', // <= An alias that can be used in the DB Schema
$doctrineFactory,
\Doctrine\Common\Collections\Collection::class // <= Interface for collections that the factory can create
)
// For the Illuminate Collections factory to work, you need to install the `illuminate/collections` package
->withCollectionFactory('illuminate', $illuminateFactory, \Illuminate\Support\Collection::class)
);The collection interface is used for those cases when you extend collections to suit your needs.
// Extend the collection
class CommentCollection extends \Doctrine\Common\Collections\ArrayCollection {
public function filterActive(): self { /* ... */ }
public function filterHidden(): self { /* ... */ }
}
// Specify it in the DB Schema
$schema = [
Post::class => [
//...
Schema::RELATIONS => [
'comments' => [
Relation::TYPE => Relation::HAS_MANY,
Relation::TARGET => Comment::class,
Relation::COLLECTION_TYPE => CommentCollection::class, // <=
Relation::SCHEMA => [ /*...*/ ],
]
]
]
];
// Use it
$user = (new Select($this->orm, User::class))->load('comments')->fetchOne();
/** @var CommentCollection $comments */
$comments = $user->comments->filterActive()->filterHidden();An important difference between the 'Many to Many' and 'Has Many' relations is that it involves Pivots — intermediate entities from the cross-table.
The 'Many to Many' relation has been rewritten in such a way that now there is no need to collect pivots in the entity
collection. You can even use arrays.
However, if there is a need to work with pivots, your collection factory will have to produce a collection that
implements the PivotedCollectionInterface interface. An example of such a factory is DoctrineCollectionFactory.