Repository navigation
Releases: SoloPHP/Repository
Release list
v1.3.0
Solo Repository 1.3 adds pages: ReadQuery::page() reads one page of entities with the number of matching rows on all pages, in an order that keeps rows with equal values on the same pages.
Features
-
ReadQuery::page()takes aPageRequest— the page number from 1, its size and its order — and returns aPagewith the items, the total and the request:use Solo\Repository\PageRequest; $page = $products->read()->with(['category'])->page( new PageRequest(page: 3, perPage: 20, orderBy: ['created_at' => 'DESC']), ['price >=' => '50.00'], ); $page->items; // list<Product>, read by created_at DESC, id DESC $page->total; // every matching product, counted only when the page cannot tell
The primary key ends the order in the direction of its last field, unless the order names it, so rows that tie on every requested field fall on the same pages on every read. A short page tells the total by itself: only a full page, or an empty one past the end, runs
COUNT(*), over the same criteria, visibility and locale. A mapping without a primary key reads the requested order as given and rejects a request without one, and a locking read rejectspage(). On PostgreSQL, an index on(created_at, id)keeps such pages index-ordered; MySQL and MariaDB already keep the key in every secondary index. -
Page::of()applies the same counting rule to rows of custom SQL, andPageRequest::orderEndingWith()the same ordering rule, so a query of its own can page the waypage()does.Page::map()replaces the items, for example with what a response shows, and keeps the total and the request.
Upgrade
composer update solophp/repositoryv1.2.0
Solo Repository 1.2 adds TransactionManager, which gives services transaction boundaries over the application's DBAL connection without handing them SQL access.
Features
-
TransactionManager::run()runs a callback inConnection::transactional()on the connection the repositories already use, and returns its result:use Solo\Repository\TransactionManager; $transactions = new TransactionManager($db); // $products also uses $db $transactions->run(static function () use ($products, $id): void { $products->update($id, ['sku' => 'TABLE-02']); $products->translations()->save($id, 'uk', ['name' => 'Стіл']); $products->links('tags')->sync($id, [2, 5]); });
Construction opens no connection, and the manager never closes the application connection. DBAL keeps commit, rollback and nested savepoints: a successful nested run does not commit the outer transaction, and a caught inner failure rolls back to its savepoint. Callback results keep their type through PHPDoc generics. There are no retries, after-commit callbacks or distributed transactions, and repositories on another connection do not participate.
Upgrade
composer update solophp/repositorySee Transactions and the DBAL escape hatch, the changelog and the full diff since 1.1.0.
v1.1.0
Solo Repository 1.1 makes LIKE patterns escape the same way on every supported database and adds Like, which builds such patterns from plain text.
Fixes
LIKEandNOT LIKEescape with\on SQLite, MySQL, MariaDB and PostgreSQL, so\%,\_and\\match%,_and\themselves. SQLite has no default escape character, and underNO_BACKSLASH_ESCAPESneither has MySQL, nor MariaDB for single-byte character sets; there50\%matched50\followed by anything. A LIKE now ends inESCAPE '\'on SQLite andESCAPE X'5C'on MySQL and MariaDB. Both are literals, so patterns with a fixed prefix keep their index ranges. PostgreSQL, which always escapes with\, gets no clause, and LIKE onbyteafields keeps working.
Features
-
Like::escape(),contains(),startsWith()andendsWith()turn plain text, such as a search term, into a pattern that matches it literally:use Solo\Repository\Like; $products->read()->findBy(['name LIKE' => Like::contains($search)]);
Behavior changes
- On SQLite, and on MySQL and MariaDB under
NO_BACKSLASH_ESCAPES, a\in a pattern escapes the character after it instead of matching a backslash: the patternC:\dirmatchesC:dir. Patterns without\, PostgreSQL and MySQL/MariaDB in their default mode are unchanged. - A pattern that ends in an unpaired
\throwsInvalidArgumentExceptionbefore SQL instead of a result that differs by database.
Upgrade
composer update solophp/repositorySee LIKE patterns, the changelog and the full diff since 1.0.0.
v1.0.0
First stable release of Solo Repository (solophp/repository): typed PHP repositories on Doctrine DBAL. Map a table to your own objects, read through reusable queries, write explicitly, and get translations with one fallback rule and relations loaded in batches.
The code is that of v1.0.0-RC6; two applications run it in production.
What 1.0 covers
- Explicit, typed
MappingandRepository, which an application can extend into a typed repository per table. - Reusable
ReadQuerywith projections, aggregates, bounded iteration by ID,queryBuilder()and ordering by fields ofBelongsTotargets. Filter::all,any,notand relatedEXISTSpredicates, with values checked against their field type before SQL.- Translations with one fallback rule for reading, filtering and sorting, and PATCH upserts through
TranslationStore. - Four relation types with nested batch loading, locale inheritance and per-relation visibility.
- Soft delete, native upserts, atomic
insertMany,LinkSetpivot writes,lockForUpdate()and locking reads withforUpdate(). - Tested on MySQL 8.4, MariaDB 12.3, PostgreSQL 17 and SQLite, with PHP 8.3–8.5 and DBAL 4.3+.
From now on the public API follows semantic versioning; see the API reference for what it covers and the rules for typed repositories.
Install
composer require solophp/repositorySee the documentation, the changelog and the full diff since RC6.
v1.0.0-RC6
Sixth release candidate of Solo Repository (solophp/repository). LinkSet writes no longer fail when another writer stores the same pair first, locking reads on MySQL no longer depend on a deprecated DBAL platform class, and the compatibility rules for typed repositories are documented.
This is a prerelease: the 1.0 API is being validated in real applications before the stable 1.0.0 tag.
Migrating from Base Repository v3: read the migration guide.
Changes since 1.0.0-RC5
- Behavior change:
LinkSet::attach()andsync()skip a pair that another writer stored after the operation read the set, instead of failing on the pivot's unique key. Their INSERT carriesON CONFLICT (pair) DO NOTHINGon PostgreSQL and SQLite andON DUPLICATE KEY UPDATEon MySQL and MariaDB; foreign-key violations still propagate. Concurrent attaches of one pair now both succeed on MySQL and on PostgreSQL in READ COMMITTED, while MariaDB with snapshot isolation (error 1020) and PostgreSQL in REPEATABLE READ or SERIALIZABLE (a serialization failure) still refuse a transaction whose snapshot is older than the other writer's commit. On PostgreSQL and SQLite these writes now need the UNIQUE pair that the guide already required. See LinkSet. - A locking read on MySQL tells
FOR UPDATE OFsupport by the server version, 8.0.1 or later, instead of by DBAL's deprecatedMySQL80Platform, so it keeps locking only the read's table once DBAL drops its MySQL 8 platform classes. MySQL 5.7 and MariaDB keep the plainFOR UPDATE. - The compatibility rules for subclasses of
Repositoryare documented: throughout 1.x it stays a non-finalreadonlyclass with final public methods and private state, a minor release may add public methods, and constructor parameters keep their names and order. See typed repositories.
Install
composer require 'solophp/repository:^1.0@RC'See the changelog for the full list of features and requirements, and the full diff since RC5.
v1.0.0-RC5
Fifth release candidate of Solo Repository (solophp/repository). It adds locking reads, ordering by the fields of related rows and native upserts — what the first application with 140 tables still wrote by hand.
This is a prerelease: the 1.0 API is being validated in real applications before the stable 1.0.0 tag.
Migrating from Base Repository v3: read the migration guide.
Changes since 1.0.0-RC4
ReadQuery::forUpdate()returns the same read as a locking one:find(),findOneBy(),findBy(),rows(),exists()anditerateById()runSELECT ... FOR UPDATEand lock the rows they read until the transaction ends, and throwLogicExceptionoutside a transaction. A locking read never answers from the snapshot of a REPEATABLE READ transaction: MySQL reads the latest committed row, while PostgreSQL and MariaDB reject a row changed since the snapshot as a serialization failure. PostgreSQL and MySQL 8 lock only the rows of the read's table (FOR UPDATE OF). See the read API.orderByaccepts dotted paths throughBelongsTorelations to a field of the target, such ascategory.name, in reads, default relation orders andLoadorders. Each target is joined over the rows a load of the same relation in the read sees, so a list that shows soft-deleted rows throughVisibility::Allalso orders by them.Repository::upsert($key, $data, $onInsert = [])inserts the row of a unique key, or updates the row that already holds it, in one native statement. See Repository writes.- Behavior change:
insertMany()runs a batch that fits one statement without a transaction or savepoint. Inside a caller's transaction on PostgreSQL, a failure of such a batch now aborts that transaction, as a failedinsert()does. Loadand default relation orders accept the same paths, resolved when the read prepares its loads; an invalid order key reports "Order must use field names or relation paths to fields."
Install
composer require 'solophp/repository:^1.0@RC'See the changelog for the full list of features and requirements, and the full diff since RC4.
v1.0.0-RC4
Fourth release candidate of Solo Repository (solophp/repository). Repository can now be extended into a typed repository per table, the shape the first application with 140 tables settled on.
This is a prerelease: the 1.0 API is being validated in real applications before the stable 1.0.0 tag.
Migrating from Base Repository v3: read the migration guide.
Changes since 1.0.0-RC3
Repositoryis no longer final. An application can extend it with areadonlyclass that fixes the mapping in its own constructor, so a container injects each table's repository by type and static analysis reads the model from@extends Repository<Model>. See Typed repositories.- Every public method except the constructor is final: a subclass adds methods of its own and cannot change the inherited reads and writes.
- Code that constructs
Repositorydirectly keeps working unchanged.
Install
composer require 'solophp/repository:^1.0@RC'See the changelog for the full list of features and requirements, and the full diff since RC3.
v1.0.0-RC3
Third release candidate of Solo Repository (solophp/repository). It closes two gaps that moving the first real application to RC2 revealed: long IN lists bind as one DBAL list parameter, and a mapping read only as rows needs no hydrator.
This is a prerelease: the 1.0 API is being validated in real applications before the stable 1.0.0 tag.
Migrating from Base Repository v3: read the migration guide.
Changes since 1.0.0-RC2
INandNOT INcriteria and the key lists of relation loads andlockForUpdate()are bound as one DBAL list parameter (ArrayParameterType) of values converted through the field type, instead of one named parameter per value. DBAL expands the list into the same SQL when it runs the statement. A type without a DBAL list binding, such as boolean, still binds each value, and the parameter budget still counts every value of a list.- Behavior change: a
queryBuilder()with IN criteria holds a list parameter. Run the builder itself, or passgetParameterTypes()along withgetParameters()when its SQL becomes part of another statement. - The
hydratecallback of aMappingis optional, andMapping::$hydrateis nullable. A mapping without one is read throughrows(), aggregates,queryBuilder()and related filters, and written as usual;find(),findOneBy(),findBy(),iterateById()and loading it as a relation target throwLogicExceptionbefore SQL.
Install
composer require 'solophp/repository:^1.0@RC'See the changelog for the full list of features and requirements, and the full diff since RC2.
v1.0.0-RC2
Second release candidate of Solo Repository (solophp/repository). It closes the gaps that migrating the first real application from RC1 revealed, and lowers the time and memory of reads, relation loads and insertMany().
This is a prerelease: the 1.0 API is being validated in real applications before the stable 1.0.0 tag.
Migrating from Base Repository v3: read the migration guide.
Changes since RC1
ReadQuery::queryBuilder($alias, $criteria)returns a DBALQueryBuilderwith the mapped table under the alias in FROM, the criteria and visibility in WHERE, and the translation joins the criteria need. It selects nothing: add the SELECT, joins and grouping, and add your own conditions withandWhere(), becausewhere()replaces the criteria and visibility. It replaces v3'stable()withapplyCriteria().- Mappings without a primary key:
Mapping::$primaryKeymay be NULL for tables with composite keys. Their rows are read, inserted and changed by criteria, and operations by ID throw before SQL. Translations::$defaultLocaleis optional. Without it, a read that names no locale joins no translations and returns NULL translated fields; conditions, sorts and aggregates on translated fields throwLogicException.TranslationStore::all($id)returns the translations of every locale of one owner, keyed by locale.- The
assigncallback of a relation is optional, for relations that are only filtered by or linked; loading such a relation throwsLogicExceptionbefore SQL. - A relation's target factory runs once per owner mapping instead of on every load, filter and link set.
- Reads without relation loads hydrate rows directly, every read releases rows as it goes, relation batches reuse one prepared target read, and
insertMany()no longer copies its rows.
Behavior changes
TranslationStore::save()andseed()insert only the given fields into a new translation row, so omitted fields take their column defaults instead of NULL. A translated column with a non-NULL default therefore keeps that default and does not fall back.Relation::$assignandMapping::$primaryKeyare nullable, andMapping::$generatedIdis false without a primary key.
Install
composer require 'solophp/repository:^1.0@RC'See the changelog for the full list of features and requirements, and the full diff since RC1.
v1.0.0-RC1
First release candidate of Solo Repository (solophp/repository), the successor of solophp/base-repository v3. It replaces inheritance with explicit, typed mappings and composition, moves reads to reusable queries, and loads relations in batches with one translation fallback rule.
This is a prerelease: the 1.0 API is complete and is being validated in a real application before the stable 1.0.0 tag.
Migrating from Base Repository v3: read the migration guide.
Highlights
MappingandRepositoryby composition; constructing them never opens a database connection.ReadQuerywith projections,count()/exists(),sum()/min()/max()/avg()and boundediterateById()exports.- Four relation types with nested batch loading, locale inheritance and per-relation visibility.
Filter::all/any/not/relatedcriteria. Criteria values and written values must have the form of their field type, so drivers cannot silently turn'12x'into 12.- Soft delete,
lockForUpdate()row locks, transactionalinsertMany(), many-to-manyLinkSet, translationsave()/seed(). - Tested on SQLite, MySQL 8.4, MariaDB 12.3 and PostgreSQL 17 with PHP 8.3 and 8.5 and Doctrine DBAL 4.3 or later.
Install
Until the package is on Packagist, add the GitHub repository:
composer config repositories.solophp-repository vcs https://github.com/SoloPHP/Repository.git
composer require 'solophp/repository:^1.0@RC'See the changelog for the full list of features and requirements.