Download the PHP package kinetis/orm without Composer

On this page you can find all versions of the php package kinetis/orm. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package orm

kinetis/orm
A data mapper and unit of work for MySQL and PostgreSQL

Packagist Version Packagist Downloads PHP Version License CI


Part of Kinetis, a non-blocking PHP framework for API-first applications, developed in the kinetis-dev/kinetis monorepo. Usable standalone: in production it depends only on kinetis/query-builder and kinetis/persistence.

Plain PHP classes marked #[Entity] are mapped into portable metadata, loaded through typed repositories and entity queries, and hydrated without their constructors. Each unit of work holds one object per row, tracks changes to the entities it holds, and writes new, changed and removed entities in one transaction when it is flushed. Updates and deletes of an entity carrying #[Version] are optimistically locked. A transaction session locks entity rows and writes entities and query-builder SQL in one transaction. An entity references another through an explicit #[BelongsTo] relationship, and reaches the entities referencing it through explicit #[HasOne] and #[HasMany] inverse relationships; an entity query loads either side when asked. An inverse relationship marked owned is an aggregate: one persist() writes the whole graph below it in foreign-key order, one remove() deletes it, and a child dropped from a relationship the manager loaded is deleted or moved. A #[ManyToMany] relationship maps a join table, whose rows one side owns and the other only reads.

This README is the package's contract. How a Kinetis application wires it: kinetis.dev/docs/orm.html.

Entities

MappingException refuses, when the metadata is built and before any SQL: a class without #[Entity], a readonly class or property, a hooked or virtual property, an untyped property, a union other than a nullable type, an intersection, any other type (mixed, array without #[HasMany] or #[ManyToMany], an object without #[BelongsTo] or #[HasOne], such as DateTime, DateTimeInterface or a subclass of DateTimeImmutable, a unit enum), a missing or second identifier, a generated identifier not typed ?int, a second #[Version], a version property that is the identifier or is not typed int, an invalid name or connection name, the reserved connection name app, a duplicate column, and each relationship refusal under "Relationships".

Identifiers

Metadata

fromClasses() maps exactly the classes it is given and refuses one that is not an entity, or a relationship whose target is not one of them. toArray() holds only class names, table and column names, type names and flags, ordered by class, so the same classes always produce the same array. Each entity lists its column-mapped properties and, apart from them, its inverse relationships as inverses and its #[ManyToMany] relationships as joins, each carrying the join table and the two columns that end of it reads, and its connection. connections() lists every connection an entity names, each once and in byte order, and connectionFor($class) returns one entity's. fromArray() accepts only what toArray() writes for those classes as they are declared now: a missing or extra field, a wrong type, an unknown class or a mapping the source no longer produces throws MappingException. It reflects the classes it names and nothing else. Nothing is cached outside the instance.

Opening a unit of work

OrmFactory takes a MysqlLink or PostgresLink client, never a transaction (see "Transaction sessions"), and holds no unit-of-work state, so one factory serves the whole process. It maps the entities of one connection, default unless its third argument names another (see "Connections"). open() returns a new EntityManager, which belongs to one unit of work:

A detached entity stays an ordinary PHP object, and nothing tracks its changes. Open a separate manager for each concurrent Fiber; managers never share identities.

Connections

An application whose entities live on more than one database keeps one MetadataRegistry for all of them and one client per connection:

$reportingDb is the reporting database's client, built as $db is above.

Loading

Every entity query selects all mapped columns. Loading a row:

  1. converts every mapped column of every row in the result before any entity is allocated, the identifier first. A missing column, a null identifier or a value its property does not admit throws MappingException, and nothing from that result is allocated or registered. Extra columns are ignored;
  2. returns the object already held for the identifier, untouched;
  3. otherwise allocates the entity with ReflectionClass::newInstanceWithoutConstructor(), writes each column-mapped property but a #[BelongsTo] directly — no constructor, setter, hook or magic method runs — and only then registers it, with a snapshot of the converted values, a #[BelongsTo]'s foreign key included. Every relationship, of either side, stays uninitialized.
Property type Admitted driver value
string a string
int an int, or its canonical decimal string (no sign but -, no leading zero, whitespace, fraction or exponent, within PHP's range)
float a finite int, float or numeric string
bool a bool, 0, 1, "0" or "1"
backed enum a case, or a backing value admitted under its backing type's rule
DateTimeImmutable a DateTimeImmutable, or a UTC string YYYY-MM-DD HH:MM:SS with an optional . and one to six fraction digits, loaded in UTC (see "Timestamps")
Date a Date, or a string YYYY-MM-DD naming a day that exists, loaded as a Date (see "Dates")
?T also null

A mapped-value message names the class, property, table and column, and describes the value by its type only — never by its potentially secret contents.

Repositories and queries

find() converts its argument like the identifier property and returns a held identity under exactly that key without SQL. Otherwise it queries the identifier column, and the row that comes back resolves through the identity map by the identifier the database returned. A UUID looked up in different letter case misses the held key and queries; it returns the object already held only when the column's type or collation matches that spelling and the database returns the stored identifier.

findBy() joins equality predicates on properties. query() returns an EntityQuery with:

A property name resolves to its column and a value converts through the property's type before either reaches the query builder, so an unknown property or an inadmissible value throws MappingException before SQL. A backed enum binds as its backing value. An inverse relationship maps no column, so naming one there throws MappingException before SQL too (see "Relationship predicates").

get() and findBy() buffer every matching row, however many there are. Page through a result that can grow with cursorPaginate().

EntityRepository is final. An application repository wraps it:

Relationships

Organization is an application entity with a name property, and $cursor the cursor of the page the client asked for.

Loading relationships

with(string ...$relations) names relationship paths: each segment is a #[BelongsTo], #[HasOne], #[HasMany] or #[ManyToMany] property of the entity the segment before it loads, as in author.organization, comments.author or author.posts. Repeated calls add to one set. An empty segment, an unknown property or a property that is not a relationship throws MappingException before SQL.

get(), first(), paginate() and cursorPaginate() load the paths after their own statements. find(), findBy(), count() and exists() load nothing and send only their usual statements, so with(...)->count() counts what with(...)->get() returns. Each relationship loads one level at a time. A #[BelongsTo]:

  1. The distinct non-null foreign keys of the level's entities whose relationship is uninitialized are read from their snapshots.
  2. One SELECT of the target's mapped columns where its identifier column is IN those keys, for at most 1,000 keys per statement, on the manager's link. There is no join. Every key is selected, the key of a target the manager already holds included, and a level without keys sends nothing.
  3. Every row loads under "Loading", so a held target is returned as it is. A null key sets a nullable relationship to null; a key no row matches throws MappingException, naming the class, property and column.
  4. The targets are the next level's entities.

An inverse relationship:

  1. The distinct identifiers of the level's entities whose relationship is uninitialized are read from their snapshots.
  2. One SELECT of the target's mapped columns where the column of its mappedBy property is IN those identifiers, for at most 1,000 identifiers per statement, ordered by the target's identifier column ascending, on the manager's link. There is no join, and a level without identifiers sends nothing.
  3. Every row loads under "Loading", so a held target is returned as it is, and joins the entity its foreign-key value in that row names — not the one a held target's snapshot or its unflushed #[BelongsTo] property names. A target scheduled for removal still loads until its DELETE commits.
  4. A #[HasMany] is set to the list of its rows' targets, in that order, and to an empty list without rows. A #[HasOne] is set to the target of its one row. Without a row it is set to null when nullable and otherwise throws MappingException; more than one row throws MappingException. Both messages name the class, property, target and column.
  5. The targets are the next level's entities.

A #[ManyToMany] relationship reads its join table first, as "Many-to-many relationships" states.

A relationship the application already initialized, on either side, is never overwritten by a load, and no load compares it with the database. A flush reconciles an owned relationship, and only one this manager loaded itself (see "Aggregates"). What an initialized relationship holds joins the next level and must be an entity of its target class that this manager manages — for a #[HasMany], every value of the array, whatever its keys — or null where the type allows it. Anything else throws InvalidEntityStateException before that level's statements, on the last segment of a path too.

Every statement of the load runs inside its terminal, and the manager is checked after each one before anything is assigned, so a close() meanwhile throws ClosedEntityManagerException. A failure keeps what earlier statements assigned. In a transaction session every statement runs on the session's transaction, and a failure fails the session as any terminal failure does. lockForUpdate() and lockForShare() lock the root statement only: a relationship's statements carry no lock clause.

Cardinality and buffering

The database is the authority on cardinality. Give the foreign-key column a #[HasOne] reads a unique constraint: the ORM refuses duplicate rows rather than choosing one, and never inspects the schema.

A #[HasMany] loads every matching row into one array, buffered in full like get() and findBy(). A root cursorPaginate() bounds how many entities a page holds, not how many targets each of them loads. Do not eager-load a collection that can grow without bound; page through the target's repository by its #[BelongsTo] property instead:

Relationship predicates

A #[BelongsTo] property is also its foreign key: where(), whereIn(), orderBy(), findBy() and a cursorPaginate() property compile to its column, and a value converts like the target's identifier, so '7' matches an int identifier and an entity object throws MappingException. No predicate reaches a target's own properties; a join belongs to builder().

An inverse or #[ManyToMany] relationship maps no column of its own table: where(), whereIn(), orderBy(), findBy() and a cursorPaginate() property naming one throw MappingException before SQL. Filter, order and page the targets of an inverse relationship through their own repository by their #[BelongsTo] property, as above, and those of a #[ManyToMany] through its join table with builder().

Writing relationships

Aggregates

An owned relationship makes one entity and the rows below it a single unit: one persist() writes the whole graph, in an order its foreign keys can take, inside one transaction.

Author::$profile is #[HasOne(mappedBy: 'author', owned: true)], so flush() finds the profile through it and writes the author's row first, because the profile's author_id names it. The same holds at any depth and for a #[HasMany]: a post's owned comments are written after the post, and their own owned relationships after them. Where the parent's identifier is generated, the key its INSERT produced is what the child's foreign key is written with, inside the same transaction, and it reaches the parent's own property only once COMMIT returns.

Entities persisted separately need no ordering either. Each persist() schedules one entity, and flush() orders every row it holds at once:

What a flush discovers

One pass runs at the start of flush(), before any statement, from every entity awaiting insert in persist() order and then every managed entity, across their initialized owned relationships, depth first in mapped declaration order and array order. It is iterative and cycle-safe, and it reads nothing from the database.

Loaded relationships, and what a flush may change through them

The ORM never guesses what the database holds through a relationship. An owned relationship has a membership to reconcile against only when this manager loaded it with with(), or when its owner was inserted by a flush whose COMMIT returned.

For a relationship the manager loaded, flush() compares the membership it loaded with the one the property holds now. Every decision reads the child's foreign key — its #[BelongsTo] property, or, where that property was never loaded, the value in its snapshot:

The child Its final foreign key What the flush does
is in the relationship names this owner nothing; its own changes write as usual
is in the relationship names another owner InvalidEntityStateException before SQL
was dropped from it still names this owner, or is null DELETE, with the aggregate below it
was dropped from it names another owner the ordinary UPDATE moves the row

Dropping an eagerly loaded child from a collection is a complete instruction on its own: its #[BelongsTo] property may stay uninitialized, and a non-nullable one never has to be given an impossible null. Moving a child to another owner means assigning that owner to the child's #[BelongsTo] property; if the new owner's owned relationship is loaded too, it must hold the child, since the ORM does not repair the object graph.

For an owned relationship the manager did not load, flush() still discovers and inserts new children through it, and changes nothing else: what the database holds behind it is unknown, so no removal, replacement or orphan is inferred. Load it with with(), or operate on the child entity itself.

Adding or removing children writes no owner column and does not advance the owner's version. A relationship that is merely reordered sends no SQL at all.

Removing an aggregate

remove($owner) schedules the owner and everything below it through its owned relationships, and detaches every entity awaiting insert it reaches without SQL. persist() on a removed owner cancels the removal for the same subgraph.

A managed owner whose owned relationship is uninitialized, or was never loaded by this manager, is refused with InvalidEntityStateException: skipping it would leave rows the mapping promises to remove, and loading it would be I/O no property access performs. Load the relationship, or delete the rows with an explicit operation or a database ON DELETE CASCADE outside this contract.

Statement order

flush() builds one ordered plan before it sends anything:

Where no dependency decides, the order is join-row deletes, entity deletes, entity inserts, join-row inserts, then updates; entity statements by class and by identifier where it is known, join-row statements by join table and by their pair, and both last by the order they were scheduled in. Deleting first frees the unique foreign-key slot an owned #[HasOne] replacement needs.

Loops

Rows that reference each other in a loop have no such order. A flush breaks one by deferring a nullable foreign key: the INSERT sends NULL and an UPDATE writes the key once both rows exist, inside the same transaction. That UPDATE must affect exactly one row; it matches no version and advances none, because it completes one logical insert before the row is externally visible, and the committed snapshot holds the final key. A loop among rows that are all being deleted is nullified the same way before the DELETEs.

Only a nullable foreign key breaks a loop. A loop of NOT NULL columns throws InvalidEntityStateException before a transaction starts, naming the relationships it runs through: Kinetis does not depend on a backend's deferred-constraint configuration.

Many-to-many relationships

#[ManyToMany] maps a join table. One side owns it, naming the table and both of its columns; the other side is optional and only reads it.

Loading a join collection

with('students') and with('courses') load either side, after the root statement and on the manager's link:

  1. One SELECT of both join columns where this end's column is IN the distinct identifiers of the level's entities whose property is uninitialized, for at most 1,000 identifiers per statement, ordered by both columns. A level without identifiers sends nothing, and a join column holding no identifier throws MappingException.
  2. One SELECT of the target's mapped columns where its identifier column is IN the distinct identifiers those rows name, again at most 1,000 per statement. There is no join. Every row loads under "Loading", so a held target is returned as it is, and an identifier no row matches throws MappingException.
  3. Each property is set to the list of its own rows' targets, in the order step 1 read them, and to an empty list without rows. An initialized property is never overwritten; what it holds joins the next level and must be an entity of the target class this manager manages.

A join collection is a set of entity identities, so its array order carries no meaning and a duplicate is refused before SQL: a collection holding one target object twice, and two objects naming one row, which never reach one collection together because a manager holds one object per identity and refuses a second for one it already holds.

Writing a join collection

Only the owning side writes, and it writes join rows alone: a link is never a target's insert, update or deletion.

The owning collection What flush() writes
of an entity awaiting insert one INSERT per link it holds
loaded with with() one DELETE per pair it lost, one INSERT per pair it gained
loaded with with(), on an owner carrying #[Version] the same, and one UPDATE advancing the owner's version
loaded and only reordered nothing
of an owner scheduled for deletion one DELETE by the join column, before the owner's own
uninitialized nothing
initialized by the application on a managed owner InvalidEntityStateException before SQL

$dropped and $enrolled are students this manager holds.

A link that carries data of its own — a grade, a position, a deleted_at — is not a join row but an entity: map an Enrollment class with a surrogate identifier and two #[BelongsTo] properties, and it persists, orders and removes under "Aggregates" like any other row.

Writing

For one manager, an object is in one of these states:

State contains() What flush() writes
Not held: new, or detached false nothing, unless an owned relationship reaches it (see "Aggregates")
Awaiting insert true an INSERT
Managed true an UPDATE of its changed columns, and its next version when a versioned entity's columns or owning join membership changed
Scheduled for deletion true a DELETE

A refusal changes nothing and throws InvalidEntityStateException, or MappingException for a property value the mapping does not admit.

Flushing

flush() writes everything the manager has pending in one transaction. A manager from open() begins it on the factory's client; inside a transaction session, flush() writes on the session's transaction and leaves COMMIT to the factory (see "Transaction sessions"):

  1. Before the transaction, it walks every initialized owned relationship once and schedules the new entities it reaches (see "Aggregates"), reads and validates every entity awaiting insert as persist() does and every managed entity, refuses an identifier or version that changed and an UPDATE whose version cannot advance, reconciles every owned relationship it loaded, diffs every owning join collection it may write, and orders every statement. With nothing to write, it returns without a transaction or any I/O.
  2. One INSERT per entity awaiting insert, of every mapped column but a generated identifier.
  3. One UPDATE of the changed columns, or one DELETE, per entity, by the identifier column and, for a versioned entity, the version column (see "Optimistic locking"). A versioned owner whose owning #[ManyToMany] membership changed takes that UPDATE too, of the version column alone where no other column changed.
  4. An UPDATE per deferred foreign key, where a loop of references needed one (see "Loops").
  5. One DELETE per join row an owning #[ManyToMany] collection dropped, one per join table of an entity being deleted, and one INSERT per join row such a collection gained (see "Many-to-many relationships").
  6. COMMIT.

Statements 2 to 5 run in the one order "Statement order" states: every row after the rows its foreign keys name and a versioned owner's UPDATE between the join rows it changed, and otherwise join-row deletes, entity deletes, entity inserts, join-row inserts then updates, by class, identifier and join pair, so concurrent flushes take row locks in one order where no dependency decides. Every statement runs on that transaction, and nothing is batched. A statement that needs a key an earlier INSERT generated takes it from that INSERT; no entity property holds it before COMMIT.

An entity's DELETE and a deferred foreign key's UPDATE must affect exactly one row, and an entity's UPDATE at most one; a join row's DELETE may affect any number. An unversioned entity UPDATE affecting none is followed by an existence check on the same transaction: the MySQL family counts changed rows rather than matched ones, so an UPDATE writing the values its row already holds reports zero. A versioned UPDATE or DELETE affecting none throws OptimisticLockException without that check. A missing row, more than one affected row, or a generated key that is null or not an int within PHP's range throws InvalidEntityStateException before COMMIT.

Only a COMMIT that returns changes the manager or its entities. Each inserted or updated entity is then snapshotted with the values the flush sent, not whatever its properties hold by then; a generated key is written into its property and registered as the entity's identity; a versioned entity's version, as inserted or advanced by its update, is written into its property; every reconciled owned relationship and every written join collection takes the membership the plan was built from; and a deleted entity is detached.

While flush() runs, the manager refuses every call but close() and isClosed() with InvalidEntityStateException, including a call made on the flushing Fiber by code the flush reaches, such as SQL instrumentation.

When a flush fails

This table covers a manager from open(). A flush inside a transaction session fails the session instead: see "When a session fails".

Failure Afterwards Pending work Throws
Validation, or beginTransaction() open kept that exception
Anything before COMMIT, with the rollback returning open, unless close() ran kept that exception, unwrapped: OptimisticLockException, or a driver SqlException as the driver threw it — a QueryException, a ConnectionException, or a TransactionException from a transaction the server already ended
Anything before COMMIT, with the rollback throwing closed abandoned RollbackFailedException: getPrevious() is the first failure, $rollbackFailure the rollback's
COMMIT closed abandoned UnknownFlushOutcomeException: getPrevious() is the COMMIT failure

A failure before COMMIT sent no COMMIT, so the database kept nothing of the flush, and the manager and its entities are as they were: every insert, change and deletion is still pending against its original snapshot, and no generated key was assigned. Calling flush() again after correcting the cause, such as a unique key conflict, sends the whole flush again in a new transaction. flush() never retries by itself. After RollbackFailedException the manager is closed; the work can be redone with a new one.

flush()'s @throws names Kinetis\Persistence\Exception\SqlException for that whole unwrapped family, so a caller may catch any member of it. To recover from a unique key conflict, catch QueryException and ask isUniqueViolation(), which kinetis/persistence's README defines for every driver.

UnknownFlushOutcomeException means COMMIT was sent and the call failed: the database may or may not have applied the flush, and no entity was changed. Establish what the database holds before doing the work again; replaying it as if it had failed can apply it twice.

close() is accepted while flush() runs, from any Fiber. It detaches everything, then closes the flush's transaction — from another Fiber by discarding its connection rather than sending ROLLBACK — and the flush fails by the table above: before COMMIT with the driver's failure or ClosedEntityManagerException, once COMMIT was sent with UnknownFlushOutcomeException. A COMMIT that still returns changes nothing in the closed manager.

Optimistic locking

A conflict fails the flush before COMMIT, as "When a flush fails" states: the transaction is rolled back, the exception is rethrown unwrapped, and the manager stays open with every version, snapshot and pending change as it was, the rest of the flush included. Calling flush() again sends the same stale statement and conflicts again: the manager never reloads, merges or retries. To recover, clear() the manager, which abandons all of its pending work, or close() it and open another, then load the entity again, reapply the operation and flush. A rollback that fails throws RollbackFailedException; a COMMIT that fails throws UnknownFlushOutcomeException, leaves the version in memory unchanged and must not be replayed as if it failed.

An entity is a plain object that code can change while the flush waits on the database. Once COMMIT returns, its version property is overwritten with the version the database acknowledged, while a change to any other property stays pending against the snapshot of the values sent, and the next flush() writes it.

Timestamps

Dates

The query builder underneath

EntityQuery::builder() returns a copy of the underlying Kinetis\QueryBuilder\Query, carrying the table, every mapped column and the predicates added so far, for SQL the entity query does not express: joins, projections, raw fragments, aggregates. Changing the copy leaves the entity query unchanged, and its terminals return arrays or DTOs that no EntityManager manages.

EntityManager::builder() returns a fresh Query on the manager's link: the factory's client for a manager from open(), the session's transaction inside OrmFactory::transaction(). Its results are unmanaged too.

Transaction sessions

Order is the entity under "Optimistic locking", and order_events an application table. OrmFactory::transaction(callable $callback): mixed begins one transaction on the factory's client, passes the callback an EntityManager bound to it, commits once the callback returns, and returns the callback's result.

When a session fails

Way out COMMIT Entity state Throws
beginTransaction() fails not sent untouched; no manager exists that exception
A nested call not sent the running session is unaffected InvalidEntityStateException
An ORM failure was recorded, whether the callback then returned or threw, and the rollback returns not sent not applied that first ORM failure
The callback throws with no ORM failure recorded, and the rollback returns not sent not applied the callback's exception
Either of the two above, with the rollback throwing not sent not applied RollbackFailedException: getPrevious() is the primary failure, $rollbackFailure the rollback's
The callback returns with the manager closed and no ORM failure recorded not sent not applied ClosedEntityManagerException
COMMIT throws unacknowledged not applied CommitNotAcknowledgedException: getPrevious() is the driver failure
COMMIT returns acknowledged applied nothing: the callback's result is returned

The primary failure is the first failure an entity query terminal or flush() threw, once one is recorded, even when the callback caught it and then threw an exception of its own; only a callback that throws with no ORM failure recorded makes its own exception primary. A rollback that returns rethrows the primary failure, and a rollback that throws makes it RollbackFailedException::getPrevious().

In every row but the first two the manager is closed and its entities detached. CommitNotAcknowledgedException means the ORM received no acknowledged COMMIT. The database may already have rolled the transaction back before COMMIT — PostgreSQL does for a transaction a failed statement aborted — or a COMMIT that was sent may have an unknown outcome. Do not assume the work failed and do not replay it; establish what the database holds first. A flush() on a manager from open() keeps its own contract and UnknownFlushOutcomeException.

A Fiber destroyed while suspended inside the callback sends neither COMMIT nor ROLLBACK: the transaction is dropped, its connection discarded so the server rolls the work back, and kinetis/persistence records the outcome as unknown.

A manager from open() never joins a transaction. Its flush() is not supported while the calling Fiber holds a transaction on the same client, as inside a TransactionGuard::transaction() or OrmFactory::transaction() callback: the flush's transaction needs a second connection, and waiting for a row lock the first transaction holds blocks the Fiber on itself. Its reads are refused there with Kinetis\Persistence\Exception\TransactionException rather than run outside the transaction — by a native client for the Fiber holding the transaction, by a PDO client for every Fiber. Use the session's manager instead.

Not in scope

Join-table payload, ordering or soft deletion, cascading a target's persistence or removal through a join table, foreign-key writes through an inverse relationship or fixup of either side, cascades a relationship does not own, cascade rules per operation, orphan removal through a relationship this manager never loaded, collection objects or mutation APIs, lazy loading or proxies, joined eager loading, streamed, capped or partial collections, predicates on a target's properties, timestamp, string or database-generated versions, refreshing or merging an entity, conflict resolution, relationships, flushes or transactions across connections, routing an entity to another connection at run time, replicas or sharding, joining a transaction the application began, nested sessions or savepoints, more than one writing flush per session, provisional identifiers or versions, batched or bulk writes, automatic retries, flushing on close() or destruction, automatic timestamps, DateTime properties, timestamptz or MySQL TIMESTAMP columns, time-only values, date arithmetic, time zone or precision options, custom value converters, UUID generation, composite identifiers, inheritance, partial entities, transient properties, schema validation, CLI commands, streaming, or static model methods.

Static analysis

The ORM accesses every mapped property through reflection: it reads a column's and a relationship owner's value while it plans and flushes, and it writes a hydrated column, a generated identifier once the insert commits, and a loaded relationship. PHPStan sees none of that hidden use and reports a property the application only writes as property.onlyWritten. isAlwaysRead() is the only extension answer that clears that false positive, so registering this package's extension tells PHPStan that every non-static property of an #[Entity] class is read:

There is no extension installer to do it: add the includes: entry by hand.

That exempts those properties from property.unusedType as well, a generated identifier's ?int included: PHPStan stops checking a property's type as soon as an extension calls it always read, and offers no narrower answer. The exemption reaches nothing but #[Entity] classes, and a static property on one stays reported.

Installation

Requires PHP 8.4+ and the extension for the driver you use (see kinetis/persistence). In a Kinetis application, kinetis/database-bridge compiles the entity metadata, binds an OrmFactoryRegistry for the worker and an EntityManagerRegistry for each request over every connection an entity names, and binds OrmFactory and the request's EntityManager as the default connection's entries of the two. Full documentation: kinetis.dev/docs/orm.html.

License

MIT — see LICENSE.


All versions of orm with dependencies

PHP Build Version
Package Version
Requires php Version ^8.4
kinetis/persistence Version ^1.4.5
kinetis/query-builder Version ^1.8.2
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package kinetis/orm contains the following files

Loading the files please wait ...