You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Gist is dedicated to general changes in Cycle ORM 2.0 in relation to the first version.
It is being developed in the 2.0.x-dev branch and will be updated as updates are made.
Installation:
In the composer.json set the directive minimum-stability: "dev",
then run composer require cycle/orm "2.0.x-dev" (or add by hand).
When migrating from Cycle ORM v1 to v2, first of all upgrade ORM v1 to the latest version,
and then remove all obsolete (marked @deprecated) constants, methods, and classes.
Database package
The package spiral/database moves to
cycle/database.
Its development will continue as part of Cycle.
Cycle ORM v2.0 uses cycle/database.
All Spiral\Database\* classes must be replaced by Cycle\Database\* during migration.
The doctrine/collections has been removed from the require section in composer.json.
If you use or intend to use these collections, install them separately and add DoctrineCollectionFactory to the Factory configuration.
You can install the annotations and attributes package for Cycle ORM v2 with the composer command:
composer require cycle/annotated "2.0.x-dev"
Composite keys.
Composite keys can now be specified in attributes.
The primary key of an entity can be specified in several ways:
The primary option of the Table attribute:
useCycle\Annotated\Annotation\Entity;
useCycle\Annotated\Annotation\Table;
#[Entity()]
#[Table(primary: ['id1', 'id2'])
class User {}
By a separate attribute PrimaryKey:
useCycle\Annotated\Annotation\Entity;
useCycle\Annotated\Annotation\Table\PrimaryKey;
#[Entity()]
#[PrimaryKey(['id1', 'id2'])]
class User {}
The primary option of the Column attribute
useCycle\Annotated\Annotation\Column;
useCycle\Annotated\Annotation\Entity;
#[Entity()]
class Pivot {
#[Column(type: 'bigInteger', primary: true)]
public ?int$post_id = null;
#[Column(type: 'bigInteger', primary: true)]
public ?int$comment_id = null;
}
Use arrays to set up composite keys in relations:
useCycle\Annotated\Annotation\Column;
useCycle\Annotated\Annotation\Entity;
useCycle\Annotated\Annotation\Relation\HasMany;
#[Entity()]
class User {
#[Column(type: 'bigInteger', primary: true)]
public ?int$field1 = null;
#[Column(type: 'bigInteger', primary: true)]
public ?int$field2 = null;
#[HasMany(target: 'comment', innerKey: ['field1', 'field2'], outerKey: ['field1', 'field2'])
public array $comments = [];
}
Parameter renames
In the Entity attribute, the constrain parameter is renamed to scope.
In the attribute ManyToMany a typo is corrected: though is renamed to through.
'Many To Many' relation
The two counter links Many To Many with parameter createIndex = true no longer create two unique indexes
in the cross table. Instead, one unique index and one non-unique index are created.
Given that ORM v2 supports complex keys, there is no longer a need for a separate id field at the Pivot entity:
as a primary key, you can use fields that refer to the identifiers of linked entities.
This entailed breaking backward compatibility at the internal API level and so on.
Mapper.
The internals of the base mapper have changed a bit. The scalar properties primaryKey and primaryColumn have been removed
In favor of the array properties primaryKeys and primaryColumns respectively.
The nextPrimaryKey() method should return an associative key array.
In mappers, key plurality should now be considered.
Database Command (Insert / Update / Delete), State and Node
The properties and parameters of many methods that store and pass keys (both PKs and links) have been moved to arrays.
These classes may be found in the mapper.
Select
wherePK(): for composite keys, PK is passed by array. The parameter is now variadic in case you want to pass multiple PKs
(Previously, you could do this with a Parameter class).
# Composite keys:$select->wherePK([1, 1], [1, 2], [1, 3]);
# Ordinary keys:$select->wherePK(1, 2, 3);
# or the old way:$select->wherePK(newParameter([1, 2, 3]))
As long as associative arrays are not supported (example: $select->wherePK(['key1' => 1, 'key2' => 1]);), so you should keep
the order of the passed values (the order must be the same as in the schema).
Thanks to PHP 8's ability to pass named arguments, the IDE will suggest possible parameters and their types.
Depending on the desired way of configuring the database connection, an appropriate config class can be selected.
For example, the same parameters for Postgres can be passed as a DSN string or separate parameters:
Task:
Save an arbitrary set of entities of these classes in relational database,
following the principles of the relational model.
Proposition:
Relational databases doesn't support inheritance.
Possible solutions:
One way is to use one table for all entity classes, selecting one column for associating
with the entity class.
But the set of attributes of an entity can be very different between two subclasses of the same hierarchy.
The problem of this approach is: MANY unused columns per record. As many as the number of unique
fields the classes will have.
Other solution is to allocate additional tables for the unique fields of each class.
To get Cat entity, for example, we should JOIN tables cat, animal and pet.
Disadvantage of this approach is the more parents with tables entity class has, the more uncomfortable saving of entity
become: need to update and insert values into all tables.
As can be seen, there is no easy way to map a class hierarchy to relational database tables.
Can ORM take over this task by providing the user with a familiar interface? Yes, it can.
The solutions above, does not describe any kind of innovative approach and is a loose interpretation of
known forms of inheritance: JTI (Joined Table Inheritance) and STI (Single Table Inheritance).
The ability to implement STI was initially available in Cycle ORM v1 in some way extent.
STI Support has been improved In Cycle ORM v2, and JTI support has been added.
Joined Table Inheritance
in JTI each entity in class hierarchy map to individual table.
Each table contains only columns of the class associated with it and identifier column, need for joining tables. Note: JTI in Doctrine ORM called Class Table Inheritance strategy.
// Base Classclass Animal { // Matches table "animal" with `id` and` age` columnspublic ?int$id;
publicint$age;
}
// Subclassesclass Pet extends Animal { // Matches table "pet" with `id` and `name` columnspublicstring$name;
}
class Cat extends Pet { // Matches table "cat" with `id` и `frags` columnspublicint$frags;
}
class Dog extends Pet { // Matches table "dog" with `id` и `trainingLevel` columnspublicint$trainingLevel;
}
ORM Schema
Schema::PARENT option is used to configure inheritance in the schema,
the value can be parent class or its role.
Loading a specific subclass in the hierarchy will result in an SQL query,
containing an INNER JOIN to all tables in its inheritance path.
For example, loading data for the entity Cat, ORM will execute a query like this:
SELECT ... FROM cat INNER JOIN pet USING (id) INNER JOIN animal USING (id) ...
If the entity being loaded is a base class,
then all tables corresponding to subclasses will be loaded by default.
Thus, loading the class Pet, ORM will execute a query like this:
SELECT ...
FROM pet
INNER JOIN animal USING (id)
LEFT JOIN cat ONpet.id=cat.idLEFT JOIN dog ONpet.id=dog.idLEFT JOIN hamster ONpet.id=hamster.id
...
To disable automatic subclasses tables joining, use the loadSubclasses(false) method:
/** @var Pet[] */$cat = (new \Cycle\ORM\Select($this->orm, Pet::class))
->loadSubclasses(false)->fetchAll();
Note, that when implementing JTI method, the Primary Key of the base class table must be used
as unique index in all tables of child entities,
but it can be auto-incremental only in the table of the base class.
You can define different columns to be a Primary Key for each entity in class hierarchy, however the value of this
columns will be the same.
The default behaviour, where the Primary Key of the subclass is matched with the
Primary Key of the parent, can be changed with option SchemaInterface::PARENT_KEY.
Deleting the entity of certain subclass will result in the execution of a query to delete a record
from the corresponding table only.
use \Cycle\ORM\Select;
$cat = (newSelect($this->orm, Cat::class))->wherePK(42)->fetchOne();
(new \Cycle\ORM\Transaction())->delete($cat)->run();
// Deletes record from cat table only. Record still exists in parent tables:$cat = (newSelect($this->orm, Cat::class))->wherePK(42)->fetchOne(); // Null$pet = (newSelect($this->orm, Pet::class))->wherePK(42)->loadSubclasses(false)->fetchOne(); // Pet (id:42)$base = (newSelect($this->orm, Aimal::class))->wherePK(42)->loadSubclasses(false)->fetchOne(); // Animal (id:42)
ORM relies on foreign keys, which will result in data deletion from tables below in the inheritance hierarchy.
If no foreign keys is set, the commands for deletion should be set in the mapper of deleting entity.
Relations in JTI
You can use any relations in ALL classes of hierarchy.
Eager relations of subclasses and parent classes ALWAYS loads automatically.
Lazy parent class relations can be loaded by hands the same way, as if these relations were originally
have been configured for the requested role:
$cat = (new \Cycle\ORM\Select($this->orm, Cat::class))
->load('thread_balls') // Cat class relation
->load('current_owner') // Pet class relation
->load('parents') // Animal class relation
->wherePK(42)->fetchOne();
Attention! Do not load relations to the JTI hierarchy in one query. The resulting combination of LEFT and INNER
joins, will most likely result in as incorrect resulting query.
$cat = (new \Cycle\ORM\Select($this->orm, Owner::class))
->load('pet', ['method' => Select::SINGLE_QUERY]) // <= pet is subclass of inheritance hierarchy
->wherePK(42)->fetchOne();
Single Table Inheritance
STI implies the use of one table for several subclasses of the hierarchy. This means, all attributes of specified classes in hierarchy are located in one common table.
Special discriminator column used to determine which data belongs to which class.
If some subclass has an attribute, that NOT COMMON to ALL other classes of that table, then
it should be saved in a column with default value set.
Otherwise, saving neighboring classes will cause an error.
Example
// Base Classclass Pet extends Animal {
public ?int$id;
publicstring$name; // Common Field for all classes
}
class Cat extends Pet {
publicint$frags; // Unique Field of Cat class
}
class Dog extends Pet {
publicint$trainingLevel; // Unique Field of Dog Class
}
/* Таблица: id: int, primary _type: string // Discriminator column name: string frags: int, nullable, default=null trainingLevel: int, nullable, default=null */
ORM Scheme
Schema::CHILDREN option is used to enum classes of one table.
The value of this key is array of the form Discriminator Value => Role or entity Class.
Use Schema::DISCRIMINATOR option to set the name of discriminator field.
useCycle\ORM\SchemaInterfaceasSchema;
$schema = new \Cycle\ORM\Schema([
Pet::class => [
Schema::ROLE => 'role_pet',
Schema::MAPPER => Mapper::class,
Schema::DATABASE => 'default',
Schema::TABLE => 'pet',
Schema::CHILDREN => [ // list of the subclasses'cat' => Cat::class,
'dog' => 'role_dog', // Can use role name instead of class name
],
Schema::DISCRIMINATOR => 'pet_type', // Discriminator field name
Schema::PRIMARY_KEY => 'id',
//In Base Class Schema should be Listed all Child Classes Fields along with Discriminator Field
Schema::COLUMNS => [
'id',
'pet_type' => 'type_column', // Configuring Discriminator field in table'name',
'frags', // Field from class Cat'trainingLevel'// Field from class Dog
],
Schema::TYPECAST => ['id' => 'int', 'frags' => 'int', 'trainingLevel' => 'int'],
],
Cat::class => [
Schema::ROLE => 'role_cat',
],
Dog::class => [
Schema::ROLE => 'role_dog',
],
]);
Describing this Schema:
Table pet used for storing entities of Base Class Pet and his child Classes: Cat and Dog.
Discriminator value will be stored in column pet_type.
During selecting entity from a table, depending on the value of per_type column (cat or dog), ORM will instantiate entity of class Cat or Dog respectively.
If value of discriminator will differ from cat or dog, the entity of Base Class will be instantiated.
Specifics of STI:
Can't use classless entities: entity must have class.
Therefore stdMapper and ClasslessMapper mappers are not STI-compatible.
Base Class can be Abstract, but you should guarantee, that all possible values of Discriminator column correspond to
their subclasses.
All Unique Columns of all Subclasses should have Default Value.
ORM will set the desired value of the Discriminator field when saving record to the database, no need to pre-fill
Discriminator field in the entity.
Request for an entity of a certain class from the general table is not accompanied by a filtering condition by value
discriminator. This will be fixed in the future, but now, if necessary, you should add an expression like:
->where('_type', '=', 'cat').
Relations in STI
You can use any relations in Base Class. They will automatically be applied to subclasses.
By using different forms of inheritance you can implement different strategies.
Combine STI and JTI within the one hierarchy for the reasons of Expediency, and Cycle ORM will take care of the rest.
The minimum version of PHP is now 8.0.
Class properties and method signatures are more strictly typed.
Some external API methods have clarified their return types.
This is a BC break change, so make sure to specify the same or different types according to the LSP principle in the implementations of the interfaces.
For example, \Cycle\ORM\RepositoryInterface methods findByPK() and findOne() have the return type ? object specified. If your code overrides one of these methods, the return type should be ?object or a more precise one (e.g. ?User).
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 {
publiciterable$posts;
private ?Profile$profile;
publicfunctiongetProfile(): Profile
{
if ($this->profile === null) {
$this->profile = newProfile();
}
return$this->profile;
}
}
We have preserved the usability of references for those who used them:
/** @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
To get raw entity data, use the mapper: $rawData = $mapper()
Usage
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. Generates stdClass 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
a ReferenceInterface object.
Custom Collections
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):
useCycle\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.
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.
Rewritten algorithm for entity persistence. Recursion is deployed to the queue.
DB commands are lightened and do not participate in the mechanism of signing on changes of fields (forward).
Connections, their interfaces and the connection map have been redesigned for the new logic.
It has not given a speed boost yet, but large graphs are saved with significantly less memory consumption.
Now some cases of speculative entity persistence don't work.
Parent relationships are checked for each entity, even if they are not explicitly specified by BelongsTo or RefersTo relationships.
Mapper and commands.
Removed interfaces ContextCarrierInterface and ProducerInterface, the mapper methods queueCreate and queueUpdate return the more general CommandInterface.
Removed commands Nil, Condition, ContextSequence, Split.
If you implement your own command that supports rollback or complete, you have to add an interface corresponding to the method.
Otherwise, the command will not stay in the transaction after execution.
Besides, typing on primary key didn't work in case of auto-generated values on database side:
lastInsertID simply wasn't converted to the right type after inserting record.
This could lead to problems in typed code.
In general, it's not even very clear how to substitute "typecaster" and how to affect the typecasting process at all.
Now the ORM, to convert raw data into prepared data ( casted to its types),
first uses the cast() method in the entity mapper.
The entity mapper uses an entity-specific TypecastInterface object to type the entity fields.
Relation data is typed by the relations themselves through the associated entity mappers.
In the custom mapper you are free to override the cast() method to change the typing process.
ORM Schema and TypecastInterface
If you use annotations to configure a typecast, the settings are written to the entity schema
in the SchemaInterface::TYPECAST parameter as an associative array field => type,
where type can be one of the base types (int, bool, float, datetime) or a callable.
To perform the configuration defined in SchemaInterface::TYPECAST,
the instance of \Cycle\ORM\\Parser\Typecast will be used by default.
However, you can replace the implementation by specifying a class or alias
in the SchemaInterface::TYPECAST_HANDLER parameter.
In this case, the ORM will get the custom implementation from the container,
implying that the result is an instance of class \Cycle\ORM\Parser\TypecastInterface.
The TypecastInterface objects are created once per role and cached in the mapper and EntityRegistry.
As an example, a draft for a custom typecast class might look like this:
In SchemaInterface::TYPECAST_HANDLER you can specify a list of tipecast definitions.
In this case, each item in the list will in turn get a portion of the custom
rules filtered out by the previous typecast implementation.
Typecast in Select::fetchData() and ORM::make()
As a result of the redesign, the typecasting process has been moved out of the raw database parsing
to a later step (in ORM::make()).
This means the Select::fetchData() method return raw data if you pass the typecast: false argument.
The ORM::make() method signature also has the typecast parameter set to false by default.
If you pass raw data to the ORM::make(), you should pass the typecast: false argument.