Skip to content
docsv0.2.4

MysqlRepository

MySQL document-store {@see RepositoryInterface}: the production root — the same six-method contract as {@see FileRepository} and {@see SqliteRepository}, over a MySQL server, and still zero migrations. Each entity class gets one InnoDB table of two payload columns — `id VARCHAR(191)` plus a native `doc JSON` column holding the entity's `toArray()` — created lazily with `CREATE TABLE IF NOT EXISTS` on first use. The `toArray()`/`fromArray()` pair IS the schema: adding a field to an entity never touches the database. The table name derives from the entity class by the exact rule {@see SqliteRepository} uses — snake_case of the short class name plus a crc32 suffix of the fully-qualified name (`App\Blog\ Post` becomes `post_2f1f1893`) — so an entity keeps one table name across backends and two classes with the same short name never silently share a table. Tables are utf8mb4 with the binary collation, so id matching is exact — never case-folded or trailing-space-trimmed — and insertion order of {@see self::all()} rides an internal `seq BIGINT UNSIGNED AUTO_INCREMENT` column, exactly as in the SQLite backend. Concurrency is InnoDB row locking. Every fresh-id save runs a locking read (`SELECT … FOR UPDATE`) inside a transaction — the InnoDB equivalent of SqliteRepository's `BEGIN IMMEDIATE` — and re-saves ride the row lock of the single-statement upsert, so concurrent processes can neither mint the same fresh id nor lose each other's rows; {@see self::save()} documents the reasoning. Takes a DSN, not an injected PDO, because the package's construction idiom is a location plus an entity class ({@see FileRepository} a path, {@see SqliteRepository} a database file): the repository owns its connection and opens it lazily on first use, so construction never touches the network — and when the server cannot be reached, the failure is {@see MysqlRepositoryException}, an error that teaches what broke, why it matters, and the ways out, instead of a bare driver error. Needs `ext-pdo_mysql` (suggested, not required, by the package).

MysqlRepository::__construct()

public function __construct(string $dsn, string $entityClass, ?string $user = null, ?string $password = null):

Parameters

Parameters of __construct()
NameTypeDescription
$dsnstringPDO DSN of the server and database (e.g. `mysql:host=127.0.0.1;port=3306;dbname=app`); when it names no `charset`, `utf8mb4` is appended so unicode documents survive byte-for-byte
$entityClassclass-string<T>the entity class rows are rehydrated into via `fromArray()`
$user(string | null)the user to connect as, when not carried by the DSN
$password(string | null)the password to connect with, when not carried by the DSN

MysqlRepository::find()

public function find(string|int $id): ?Milpa\Data\EntityInterface

The entity stored under `$id`, or `null` when no entity is stored under it.

Parameters

Parameters of find()
NameTypeDescription
$idstring|int

MysqlRepository::save()

public function save(Milpa\Data\EntityInterface $entity): string|int

Persists `$entity` as one JSON document row. When `$entity->id()` is `null`, the fresh id is computed from a locking read (`SELECT … FOR UPDATE`) inside the transaction — the InnoDB equivalent of SqliteRepository's `BEGIN IMMEDIATE`. Under InnoDB's default REPEATABLE READ a plain SELECT reads a snapshot and takes no locks, so two concurrent savers would compute the same fresh id and the second upsert would silently swallow the first row; `FOR UPDATE` takes exclusive next-key locks over every id it scans plus the gap above them, so a concurrent minting saver blocks until this transaction commits and then sees the row it must not collide with. Re-saves with a preset id ride the upsert itself: one `INSERT … ON DUPLICATE KEY UPDATE` statement is atomic under the unique `id` index's row lock, updating the row in place so the entity's `seq` — its position in {@see self::all()} — never moves. The stored document always carries the id actually used under the key `'id'`, regardless of what `$entity->toArray()` returned for it.

Parameters

Parameters of save()
NameTypeDescription
$entityT

MysqlRepository::delete()

public function delete(string|int $id): void

Removes the row stored under `$id`. A no-op when no entity is stored under it.

Parameters

Parameters of delete()
NameTypeDescription
$idstring|int

MysqlRepository::all()

public function all(): array

Every stored entity, in insertion order — rows come back ordered by the internal auto-increment `seq` column, which records the order ids were first saved regardless of the ids themselves.

MysqlRepository::nextId()

public function nextId(): int

The id to assign to the next entity saved without one of its own — one past the highest integer id currently stored, or `1` when the store holds no integer id. Ids live in a VARCHAR column, so the maximum is computed in PHP over the fetched ids, exactly like `FileRepository` does over its array keys — a stored `'42'` counts as the integer it names, `'custom-id'` does not.

MysqlRepository::query()

public function query(array $criteria): array

Stored entities matching every `$criteria` pair by strict equality. Deliberately filtered in PHP after fetching every row — not pushed into SQL — so equality semantics stay exactly {@see FileRepository}'s (`===` over the decoded `toArray()` values, JSON types intact), at the cost of reading the whole table. For the collection sizes this backend targets that is the right trade; a SQL-side filter could come later without touching the contract.

Parameters

Parameters of query()
NameTypeDescription
$criteriaarray<string, mixed>