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 database
However, 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\Promise
class to unloaded relation properties.\Cycle\ORM\Mapper\StdMapper
- for working with classless entities. GeneratesstdClass
objects with\Cycle\ORM\Reference\Promise
objects 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->profile
field, the relation will be loaded and you will not get aReferenceInterface
object.
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
.