База данных · Репозитории

Репозитории

Репозиторий — класс, привязанный к таблице. Запрос собирается вызовами методов, значения уезжают привязками, а результат приходит объектами вашей сущности. Ниже — как объявить, как собрать и полный разбор каждого метода: что принимает, что возвращает, что делает, когда ничего не нашлось.

Пакет flytachi/winter-ppaСборка RepositoryCoreЧтение RepositoryViewTraitЗапись RepositoryCrudTrait

Что такое репозиторий

Место, где живут запросы к одной таблице. Без него SQL расползается строками по контроллерам: одна и та же выборка пишется заново там, где понадобилась, параметры привязываются руками, а переименованная колонка обнаруживается в рантайме.

php
// без репозитория
$stmt = $pdo->prepare('SELECT * FROM users WHERE status = :s ORDER BY id DESC LIMIT 20');
$stmt->execute(['s' => 'active']);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);   // массивы без типов

// с репозиторием
$users = UserRepository::instance('u')
  ->where(Qb::eq('u.status', 'active'))
  ->orderBy('u.id DESC')
  ->limit(20)
  ->findAll();                              // User[]

Три вещи, которые это даёт помимо краткости: значения всегда уходят привязками, а не конкатенацией; результат гидрируется в сущность, поэтому редактор знает поля; подзапрос, джойн и CTE принимают другой репозиторий, а не строку — и он приносит с собой свои привязки.

Объявление

main/Repositories/UserRepository.php
<?php

namespace Main\Repositories;

use Flytachi\Winter\Ppa\Stereotype\Repository;
use Main\Configurations\MainDbConfig;
use Main\Entities\User;

/** @extends Repository<User> */
class UserRepository extends Repository
{
  public static string $table         = 'users';
  protected string $entityClassName   = User::class;
  protected string $dbConfigClassName = MainDbConfig::class;
}
Свойство Обязательно Что задаёт
$table да имя таблицы; public static, потому что читается и без экземпляра
$dbConfigClassName да к какой базе обращаться
$entityClassName практически во что гидрировать строки
$schema нет схема, если таблица не в схеме по умолчанию

`@extends` — не украшение

Строка /** @extends Repository<User> */ привязывает шаблонный параметр к вашей сущности. С ней findById() возвращает ?User, и редактор знает поля. Без неё — object, и автодополнение молчит; код при этом работает одинаково.

Стереотипы

Класс Что даёт Когда брать
Repository сборка + чтение + запись обычный случай
RepositoryView сборка + чтение представление в базе, отчёт, таблица только на чтение
RepositoryCrud сборка + запись журнал, очередь — пишем, не читаем
CteRepo сборка заготовка, которая существует только как CTE или подзапрос

Выбор — не про стиль, а про то, что нельзя вызвать по ошибке: у RepositoryView нет delete(), и это видно в автодополнении.


Справочник

Сборка запроса

Методы этой группы не обращаются к базе. Они накапливают части будущего запроса в объекте репозитория и возвращают его же, поэтому вызовы составляются в цепочку. Запрос уходит в базу только тогда, когда вызван один из методов чтения или записи.

instance()

Создаёт экземпляр репозитория с чистым состоянием запроса.

Обычный конструктор тоже работает, но instance() короче в цепочке и сразу принимает алиас таблицы — а он нужен всюду, где в запросе появляется вторая таблица.

Синтаксис

php
public static function instance(?string $as = null): static

Параметры

$as — алиас таблицы в запросе. По умолчанию null, то есть таблица участвует под своим именем.

Возвращает

Новый экземпляр репозитория. Состояние запроса пустое.

Пример

php
UserRepository::instance();       // FROM users
UserRepository::instance('u');    // FROM users u

Каждый вызов — отдельный запрос

Условия накапливаются в объекте. Два вызова instance() дают два независимых запроса, а попытка дособрать ранее полученный экземпляр добавит условия к тому, что там уже накоплено.

Поэтому запрос собирают одной цепочкой, а не по частям в разных местах метода.

as()

Задаёт алиас основной таблицы уже созданному репозиторию.

То же, что аргумент instance(), но применимо к экземпляру, который вы получили иначе — например, внедрённому через контейнер.

Синтаксис

php
public function as(string $alias): static

Параметры

$alias — алиас. Дальше на него ссылаются в условиях и списке колонок.

Возвращает

Тот же репозиторий.

Пример

php
$repository->as('u')->select('u.name')->where(Qb::eq('u.active', 1));

Результат

text
SELECT u.name FROM users u WHERE u.active = :iqb0

select()

Задаёт список колонок вместо подставляемого по умолчанию.

Без вызова репозиторий выбирает поля сущности — те, что объявлены в классе из $entityClassName. Это удобно, пока строки гидрируются в сущность; для агрегатов и выборки пары колонок список задают вручную.

Синтаксис

php
public function select(string $option): static

Параметры

$option — список колонок строкой, как он выглядел бы в SQL: 'id, name', 'COUNT(*) AS cnt', 'u.name, o.total'.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()
  ->select('id, name')
  ->where(Qb::like('name', 'Ан%'))
  ->orderBy('name ASC')
  ->limit(20, 40);

Результат

text
SELECT id, name FROM users WHERE name LIKE :iqb1 ORDER BY name ASC LIMIT 20 OFFSET 40

Список колонок — единственное место, где нужен SQL-текст

Значения сюда не подставляют: для них есть привязки. Если в списке появляется пользовательский ввод — это ошибка, и её нужно переписать на условие через Qb.

from()

Меняет таблицу, из которой идёт выборка.

Нужен в двух случаях: когда выбирают из CTE, объявленного через with(), и когда основной таблицей должен стать другой репозиторий.

Синтаксис

php
public function from(RepositoryInterface|string $repository): static

Параметры

$repository — другой репозиторий либо имя: таблицы, представления или ранее объявленного CTE.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()
  ->with('recent', OrderRepository::instance())
  ->select('*')
  ->from('recent');

Результат

text
WITH recent AS (SELECT id, user_id, total FROM orders) SELECT * FROM recent

where(), andWhere(), orWhere(), xorWhere()

Добавляют условие к запросу.

where() задаёт условие, остальные три присоединяют следующее логической связкой: AND, OR, XOR. Условия строятся объектом Qb — он же отвечает за привязку значений, поэтому пользовательский ввод не попадает в текст запроса ни при каком написании.

Синтаксис

php
public function where(?Qb $qb): static
public function andWhere(Qb $qb): static
public function orWhere(Qb $qb): static
public function xorWhere(Qb $qb): static

Параметры

$qb — условие. У where() допускается null — удобно, когда фильтр необязателен и собирается по условию: null просто ничего не добавляет.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()
  ->where(Qb::eq('active', true))
  ->andWhere(Qb::gt('age', 18))
  ->orWhere(Qb::eq('role', 'admin'));

Результат

text
SELECT id, name, email, active FROM users
WHERE active IS TRUE AND age > :iqb2 OR role = :iqb3

Основные конструкторы условий

Вызов SQL
Qb::eq('id', 42) id = :bind
Qb::neq('status', 'draft') status <> :bind
Qb::gt, gte, lt, lte >, >=, <, <=
Qb::in('id', [1, 2, 3]) id IN (:b1, :b2, :b3)
Qb::notIn('id', [4, 5]) id NOT IN (…)
Qb::like('name', 'Ан%') name LIKE :bind
Qb::between('id', 1, 100) id BETWEEN :b1 AND :b2
Qb::isNull('deleted_at') deleted_at IS NULL
Qb::and(...), Qb::or(...) группировка условий скобками
Qb::raw('orders.user_id = users.id') текст как есть — без привязок

`Qb::raw()` не привязывает значения

Этот конструктор вставляет текст в запрос как есть. Он существует для условий, где нет значений вовсе — например, сравнения двух колонок в джойне. Подставлять в него что-либо пришедшее от пользователя нельзя: это прямой путь к внедрению SQL.

join(), joinInner(), joinLeft(), joinRight(), joinCross()

Присоединяют к запросу другую таблицу.

Все пять принимают репозиторий, а не имя таблицы. Это не формальность: присоединённый репозиторий приносит с собой своё имя таблицы, схему и — если у него уже собраны условия — свои привязки.

Синтаксис

php
public function join(RepositoryInterface|string $repository, Qb|string $on): static
public function joinInner(RepositoryInterface|string $repository, Qb|string $on): static
public function joinLeft(RepositoryInterface|string $repository, Qb|string $on): static
public function joinRight(RepositoryInterface|string $repository, Qb|string $on): static
public function joinCross(RepositoryInterface|string $repository): static

Параметры

$repository — репозиторий присоединяемой таблицы, либо её имя строкой.

$on — условие соединения. У joinCross() его нет: перекрёстное соединение по определению не имеет условия.

Возвращает

Тот же репозиторий.

Какой из пяти брать

Метод SQL Что делает
join() JOIN как решит база; на практике то же, что INNER
joinInner() INNER JOIN только строки, для которых нашлась пара
joinLeft() LEFT JOIN все строки левой таблицы; без пары — NULL в колонках правой
joinRight() RIGHT JOIN зеркально предыдущему
joinCross() CROSS JOIN все сочетания строк обеих таблиц

Пример

php
UserRepository::instance()
  ->select('users.name, orders.total')
  ->joinLeft(OrderRepository::instance(), Qb::raw('orders.user_id = users.id'));

Результат

text
SELECT users.name, orders.total FROM users
LEFT JOIN orders ON(orders.user_id = users.id)

groupBy()

Группирует строки по значению колонок — для агрегатов вроде COUNT, SUM, AVG.

Синтаксис

php
public function groupBy(string $context): static

Параметры

$context — список колонок строкой: 'email', 'user_id, status'.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()
  ->select('email, COUNT(*) AS cnt')
  ->groupBy('email');

having()

Фильтрует уже сгруппированные строки.

Отличается от where() моментом применения: WHERE отсеивает строки до группировки, HAVING — получившиеся группы. Поэтому агрегат (COUNT(*) > 1) можно проверить только здесь.

Синтаксис

php
public function having(string $context): static

Параметры

$context — условие строкой. Привязок здесь нет, поэтому пользовательскому вводу тут не место.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()
  ->select('email, COUNT(*) AS cnt')
  ->groupBy('email')
  ->having('COUNT(*) > 1');

Результат

text
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1

orderBy()

Задаёт порядок строк в результате.

Синтаксис

php
public function orderBy(string $context): static

Параметры

$context — колонки и направление: 'id DESC', 'name ASC, created_at DESC'.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()->orderBy('created_at DESC, id DESC');

Порядок нужен всюду, где есть постраничность

Без ORDER BY база не обязана возвращать строки в одном и том же порядке между запросами. На практике это проявляется так: одна и та же запись попадает и на первую страницу, и на вторую, а другая не попадает никуда.

limit()

Ограничивает число строк и задаёт смещение.

Синтаксис

php
public function limit(int $limit, int $offset = 0): static

Параметры

$limit — сколько строк вернуть.

$offset — сколько пропустить от начала. По умолчанию 0.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()->orderBy('id')->limit(20, 40);   // третья страница по 20

Результат

text
SELECT id, name, email, active FROM users ORDER BY id LIMIT 20 OFFSET 40

Для страниц есть отдельный инструмент

Считать смещения руками приходится редко: постраничный вывод со счётчиком страниц и курсором описан на странице Пагинация.

forBy()

Добавляет блокировку строк в конце запроса — FOR UPDATE и подобные.

Нужен, когда выбранные строки будут изменены в этой же транзакции и нельзя допустить, чтобы их одновременно изменил кто-то другой.

Синтаксис

php
public function forBy(string $context): static

Параметры

$context — текст блокировки: 'UPDATE', 'SHARE', 'UPDATE NOWAIT', 'UPDATE SKIP LOCKED'. Набор зависит от базы.

Возвращает

Тот же репозиторий.

Пример

php
$order = OrderRepository::instance()
  ->where(Qb::eq('id', $id))
  ->forBy('UPDATE')
  ->find();

union(), unionAll()

Объединяют результат с результатом другого репозитория.

union() убирает дубликаты строк, unionAll() оставляет всё как есть и потому быстрее.

Синтаксис

php
public function union(RepositoryInterface $repository): static
public function unionAll(RepositoryInterface $repository): static

Параметры

$repository — репозиторий, чей результат присоединяется.

Возвращает

Тот же репозиторий.

Пример

php
ActiveUserRepository::instance()->select('name')
  ->unionAll(ArchivedUserRepository::instance()->select('name'));

Списки колонок должны совпадать

Обе части объединения обязаны возвращать одинаковое число колонок совместимых типов — этого требует SQL, и проверить это за вас репозиторий не может. Практический вывод: задавайте select() явно обеим сторонам, иначе каждая подставит поля своей сущности.

with(), withRecursive()

Объявляют общее табличное выражение — CTE, именованный подзапрос, на который дальше можно ссылаться как на таблицу.

withRecursive() объявляет рекурсивное выражение — то, которое ссылается само на себя. Так обходят деревья: категории, комментарии, структуру подчинения.

Синтаксис

php
public function with(string $name, RepositoryInterface $repository, ?string $modifier = null): static
public function withRecursive(string $name, RepositoryInterface $repository): static

Параметры

$name — имя выражения. По нему на него ссылаются в from() и джойнах.

$repository — репозиторий, чей запрос становится телом выражения.

$modifier — дополнительное указание базе, например MATERIALIZED. Поддержка зависит от базы.

Возвращает

Тот же репозиторий.

Пример

php
UserRepository::instance()
  ->with('recent', OrderRepository::instance())
  ->select('*')
  ->from('recent');

Результат

text
WITH recent AS (SELECT id, user_id, total FROM orders) SELECT * FROM recent

binding()

Добавляет привязки значений вручную.

Нужен в редком случае: когда в запросе есть именованный placeholder, появившийся не через Qb — например, внутри выражения, переданного в select() или having().

Синтаксис

php
public function binding(?array $binds): static

Параметры

$binds — массив привязок. null очищает добавленные ранее.

Возвращает

Тот же репозиторий.


Чтение

Методы этой группы выполняют собранный запрос. После выполнения состояние сбрасывается — тот же экземпляр можно собрать заново, накопленные условия к следующему запросу не прилипнут.

find()

Выполняет запрос и возвращает первую подходящую строку.

К запросу автоматически добавляется ограничение в одну строку, поэтому база не выбирает лишнего, даже если условию соответствуют тысячи записей.

Синтаксис

php
public function find(?string $entityClassName = null): ?object

Параметры

$entityClassName — класс, в который гидрировать строку. По умолчанию берётся $entityClassName репозитория.

Возвращает

Объект сущности либо null, если ничего не найдено.

Ошибки

RepositoryException — при ошибке базы: недоступна, синтаксис запроса неверен, нарушено ограничение.

Пример

php
$user = UserRepository::instance()
  ->where(Qb::eq('email', $email))
  ->find();

if ($user === null) {
  return ResponseEntity::notFound(['message' => 'Пользователь не найден']);
}

findAll()

Выполняет запрос и возвращает все подходящие строки.

Синтаксис

php
public function findAll(?string $entityClassName = null): array

Параметры

$entityClassName — класс для гидрации. По умолчанию — сущность репозитория.

Возвращает

Массив объектов. Пустой массив, если ничего не найдено — не null, поэтому результат можно сразу перебирать без проверки.

Ошибки

RepositoryException — при ошибке базы.

Пример

php
$users = UserRepository::instance()
  ->where(Qb::eq('active', true))
  ->orderBy('name ASC')
  ->findAll();

Результат

text
[{"id":1,"name":"Анна П.","email":"anna@example.com","active":1}]

Ограничение задавайте сами

findAll() без limit() вернёт все строки, подошедшие под условие, и все они окажутся в памяти процесса. На таблице в миллион записей это заканчивается исчерпанием памяти.

findById()

Возвращает строку по первичному ключу.

Короткая запись для where(Qb::eq(<первичный ключ>, $id))->find(). Имя колонки ключа репозиторий определяет сам по разметке сущности.

Синтаксис

php
public function findById(string|int $id, ?string $entityClassName = null): ?object

Параметры

$id — значение первичного ключа.

$entityClassName — класс для гидрации.

Возвращает

Объект сущности либо null.

Пример

php
$user = UserRepository::instance()->findById(1);

Результат

text
{"id":1,"name":"Анна","email":"anna@example.com","active":1}

findBy()

Возвращает первую строку, подходящую под переданное условие.

Отличается от find() тем, что условие передаётся аргументом, а не собирается цепочкой. Удобно для однострочных выборок.

Синтаксис

php
public function findBy(Qb $qb, ?string $entityClassName = null): ?object

Параметры

$qb — условие.

$entityClassName — класс для гидрации.

Возвращает

Объект сущности либо null.

Пример

php
$user = UserRepository::instance()->findBy(Qb::eq('email', $email));

findAllBy()

Возвращает все строки, подходящие под переданное условие.

Синтаксис

php
public function findAllBy(?Qb $qb = null, ?string $entityClassName = null): array

Параметры

$qb — условие. null (умолчание) означает «без условия» — вернутся все строки таблицы.

$entityClassName — класс для гидрации.

Возвращает

Массив объектов, возможно пустой.

Пример

php
$active = UserRepository::instance()->findAllBy(Qb::eq('active', true));
$all    = UserRepository::instance()->findAllBy();          // вся таблица

findByIdOrThrow()

Возвращает строку по первичному ключу или бросает исключение, если её нет.

Существует ради избавления от повторяющейся проверки «нашли — не нашли — вернуть 404». Исключение всплывает наружу и превращается фреймворком в HTTP-ответ, поэтому промежуточным слоям не нужно ни проверять, ни пробрасывать результат.

Синтаксис

php
public function findByIdOrThrow(
  string|int $id,
  ?string $entityClassName = null,
  string $message = 'Entity not found',
  HttpCode $httpCode = HttpCode::NOT_FOUND,
): object

Параметры

$id — значение первичного ключа.

$entityClassName — класс для гидрации.

$message — сообщение исключения. Дойдёт до клиента, поэтому пишите его так, как готовы показать.

$httpCode — код ответа, которым обернётся исключение. По умолчанию 404.

Возвращает

Объект сущности. null не возвращается никогда.

Ошибки

EntityException — если строка не найдена. Код ответа берётся из $httpCode.

Пример

php
#[GetMapping('users/{id}')]
public function show(#[PathVariable] int $id): ResponseEntity
{
  $user = UserRepository::instance()->findByIdOrThrow($id, message: 'Пользователь не найден');

  return ResponseEntity::ok($user);   // сюда попадаем, только если пользователь есть
}

findByOrThrow()

То же, что findByIdOrThrow(), но по произвольному условию.

Синтаксис

php
public function findByOrThrow(
  Qb $qb,
  ?string $entityClassName = null,
  string $message = 'Entity not found',
  HttpCode $httpCode = HttpCode::NOT_FOUND,
): object

Параметры

$qb — условие поиска.

Остальные совпадают с findByIdOrThrow().

Возвращает

Объект сущности.

Ошибки

EntityException — если строка не найдена.

Пример

php
$order = OrderRepository::instance()->findByOrThrow(
  Qb::and(Qb::eq('number', $number), Qb::eq('user_id', $userId)),
  message: 'Заказ не найден',
);

findColumn()

Возвращает значение одной колонки первой строки.

Нужен там, где объект не нужен: одно имя, одна сумма, один идентификатор.

Синтаксис

php
public function findColumn(int $column = 0): mixed

Параметры

$column — порядковый номер колонки в списке выборки, считая с нуля. По умолчанию первая.

Возвращает

Значение колонки как его отдала база, либо false, если строк нет.

Пример

php
$name = UserRepository::instance()
  ->select('name')
  ->where(Qb::eq('id', 1))
  ->findColumn();

Результат

text
'Анна'

count()

Возвращает число строк, подходящих под собранный запрос.

Считает база — строки в память не поднимаются. Условия, джойны и группировки, собранные до вызова, учитываются.

Синтаксис

php
public function count(): int

Возвращает

Число строк.

Пример

php
$total = UserRepository::instance()->where(Qb::eq('active', true))->count();

exists()

Сообщает, есть ли хотя бы одна строка, подходящая под собранный запрос.

Отличается от count() > 0 тем, что база останавливается на первом совпадении и не пересчитывает остальные. На большой таблице разница между «есть ли хоть один» и «сколько их всего» существенная.

Синтаксис

php
public function exists(): bool

Возвращает

true, если найдена хотя бы одна строка.

Пример

php
if (UserRepository::instance()->where(Qb::eq('email', $email))->exists()) {
  throw new ResponseException('Такой email уже занят', HttpCode::CONFLICT);
}

rawFetch()

Выполняет произвольный SQL и возвращает гидрированные объекты.

Запасной выход для запросов, которые не выражаются сборкой: оконные функции, специфичные для базы конструкции, тяжёлые отчёты. Собранное цепочкой состояние при этом не используется — выполняется ровно тот SQL, который вы передали.

Синтаксис

php
public function rawFetch(string $sql, array $binds = [], ?string $entityClassName = null): array

Параметры

$sql — текст запроса с именованными placeholder’ами.

$binds — массив объектов привязки CDOBind, по одному на placeholder.

$entityClassName — класс, в который гидрировать строки. По умолчанию — сущность репозитория.

Возвращает

Массив объектов.

Ошибки

RepositoryException — при ошибке базы.

Пример

php
final class EmailStat
{
  public string $email = '';
  public int $cnt = 0;
}

$stats = UserRepository::instance()->rawFetch(
  'SELECT email, COUNT(*) AS cnt FROM users GROUP BY email',
  entityClassName: EmailStat::class,
);

Результат

text
[{"email":"a@example.com","cnt":2}]

Класс гидрации задавайте явно

Без третьего аргумента строки гидрируются в сущность репозитория. Для запроса, чьи колонки с ней не совпадают, это даёт объект, наполовину заполненный умолчаниями, а лишние колонки превращаются в динамические свойства — в PHP 8.2 это уже предупреждение.

Передавайте свой класс под форму результата, как в примере, либо stdClass::class, если структура заранее не известна.


Запись

Как и методы чтения, все они выполняют запрос немедленно и сбрасывают состояние репозитория после выполнения.

insert()

Добавляет одну строку в таблицу.

Синтаксис

php
public function insert(object|array $entity): mixed

Параметры

$entity — объект сущности либо ассоциативный массив «колонка → значение». Массив удобен, когда заполняется часть полей и создавать объект ради этого не хочется.

Возвращает

Идентификатор вставленной строки в том виде, в каком его отдала база — для колонки с автоинкрементом это строка, а не число. Приводите к int сами, если нужен int.

Ошибки

RepositoryException — при ошибке базы: нарушено ограничение уникальности, отсутствует обязательная колонка, недоступно соединение.

Пример

php
$user = new User();
$user->name  = 'Анна';
$user->email = 'anna@example.com';

$id = UserRepository::instance()->insert($user);

// либо массивом, без создания объекта
$id = UserRepository::instance()->insert(['name' => 'Борис', 'email' => 'boris@example.com']);

Результат

text
'1'   // строка, не число

insertBatch()

Добавляет много строк за один проход.

Отличается от цикла с insert() тем, что строки уходят в базу пачками, а не по одной: на тысяче записей это разница между тысячей обращений и несколькими. Принимает iterable, поэтому генератор тоже подойдёт — и тогда все строки не окажутся в памяти одновременно.

Синтаксис

php
public function insertBatch(Traversable|object|array $entities): void

Параметры

$entities — массив, объект-обход или генератор объектов либо массивов.

Возвращает

Ничего. Идентификаторы вставленных строк не возвращаются — если они нужны, вставляйте по одной через insert().

Ошибки

RepositoryException — при ошибке базы.

Пример

php
function readRows(string $path): Generator
{
  $handle = fopen($path, 'rb');
  while (($row = fgetcsv($handle)) !== false) {
      yield ['name' => $row[0], 'email' => $row[1]];
  }
  fclose($handle);
}

UserRepository::instance()->insertBatch(readRows('/tmp/import.csv'));

update()

Изменяет строки, подходящие под условие.

Синтаксис

php
public function update(object|array $entity, Qb $qb): string|int

Параметры

$entity — новые значения: объект сущности либо массив «колонка → значение». Массивом обновляют часть колонок, не трогая остальные.

$qb — условие отбора строк. Аргумент обязателен: обновление всей таблицы должно быть написано явно, а не получиться случайно.

Возвращает

Число изменённых строк.

Ошибки

RepositoryException — при ошибке базы.

Пример

php
$affected = UserRepository::instance()->update(
  ['name' => 'Анна П.'],
  Qb::eq('id', 1),
);

Результат

text
1

delete()

Удаляет строки, подходящие под условие.

Синтаксис

php
public function delete(Qb $qb): string|int

Параметры

$qb — условие отбора. Обязателен по той же причине, что и у update().

Возвращает

Число удалённых строк.

Ошибки

RepositoryException — при ошибке базы, включая нарушение внешнего ключа, если на строку кто-то ссылается.

Пример

php
$deleted = UserRepository::instance()->delete(Qb::eq('id', 1));

upsert()

Вставляет строку, а если такая уже есть — обновляет её.

«Такая» определяется набором колонок, по которым база проверяет конфликт: обычно это первичный ключ или уникальный индекс. Операция выполняется одним запросом, поэтому между проверкой и вставкой не может вклиниться другой процесс — в отличие от связки «проверить exists(), потом вставить».

Синтаксис

php
public function upsert(object|array $entity, array $conflictColumns, ?array $updateColumns = null): mixed

Параметры

$entity — данные строки: объект сущности либо массив.

$conflictColumns — колонки, по которым определяется конфликт. На них должен быть уникальный индекс, иначе база не поймёт запрос.

$updateColumns — какие колонки обновлять при конфликте. null (умолчание) означает ничего не обновлять: строка останется прежней, а вставка просто не произойдёт.

Возвращает

Идентификатор строки, как у insert().

Ошибки

RepositoryException — при ошибке базы.

Пример

php
// Обновить имя, если пользователь с таким email уже есть
UserRepository::instance()->upsert(
  ['email' => 'anna@example.com', 'name' => 'Анна П.'],
  conflictColumns: ['email'],
  updateColumns:   ['name'],
);

// Вставить, если нет; если есть — оставить как было
UserRepository::instance()->upsert(
  ['email' => 'anna@example.com', 'name' => 'Анна'],
  conflictColumns: ['email'],
);

`null` в третьем аргументе — это «не обновлять»

Умолчание легко прочитать как «обновить все колонки» — а оно означает обратное. Если при конфликте строка должна измениться, перечислите колонки явно.

upsertBatch()

То же, что upsert(), но для многих строк за один проход.

Как и insertBatch(), принимает iterable и отправляет строки пачками.

Синтаксис

php
public function upsertBatch(iterable $entities, array $conflictColumns, ?array $updateColumns = null): void

Параметры

$entities — массив, обход или генератор строк.

$conflictColumns — колонки конфликта, общие для всех строк.

$updateColumns — колонки для обновления; null означает «не обновлять».

Возвращает

Ничего.

Пример

php
UserRepository::instance()->upsertBatch(
  readRows('/tmp/import.csv'),
  conflictColumns: ['email'],
  updateColumns:   ['name'],
);

Транзакции

Транзакция открывается на соединении, а соединение у единицы работы одно — поэтому все репозитории внутри одного запроса попадают в одну транзакцию автоматически.

php
$db = UserRepository::instance()->db();

$db->beginTransaction();
try {
  $id = UserRepository::instance()->insert(['email' => $email]);
  OrderRepository::instance()->insert(['user_id' => $id, 'total' => 0]);
  $db->commit();
} catch (Throwable $e) {
  $db->rollBack();
  throw $e;
}

Два разных репозитория здесь пишут в одной транзакции, хотя ни один из них о ней не знает: db() обоих отдаёт одно и то же соединение текущей единицы работы.

Транзакция живёт на соединении, а не на объекте

Отсюда два следствия. Не начинайте транзакцию в одной корутине, рассчитывая закрыть её в другой — там будет другое соединение. И не оставляйте её открытой: соединение вернётся в пул с незакрытой транзакцией и уедет к следующему запросу.


Отладка и служебное

buildSql()

Собирает текст запроса, не выполняя его.

Первое, к чему стоит обращаться, когда результат не тот: видно, во что превратилась цепочка вызовов.

Синтаксис

php
public function buildSql(array $ignoreParts = []): string

Параметры

$ignoreParts — части, которые нужно исключить из сборки, например ['limit']. По умолчанию собираются все.

Возвращает

Текст запроса с именами привязок вместо значений. Сами значения смотрят через getSql().

Пример

php
$sql = UserRepository::instance()
  ->where(Qb::eq('id', 42))
  ->buildSql();

Результат

text
SELECT id, name, email FROM users WHERE id = :iqb0

getSql()

Возвращает накопленное состояние запроса — части и привязки.

Нужен, когда мало увидеть текст: например, чтобы понять, какое значение ушло в конкретную привязку.

Синтаксис

php
public function getSql(?string $param = null): mixed

Параметры

$param — имя интересующей части. null (умолчание) возвращает всё состояние целиком.

Возвращает

Массив состояния либо значение запрошенной части.

Пример

php
$repository = UserRepository::instance()->where(Qb::eq('id', 42));

$state = $repository->getSql();          // всё состояние
$binds = $repository->getSql('binds');   // только привязки

sqlPartsCount()

Возвращает число накопленных частей запроса.

Практическое применение одно: проверить, что состояние пустое. У свежего экземпляра результат — 0, и если он больше нуля там, где вы ожидали чистый репозиторий, значит на объекте уже что-то собрано.

Синтаксис

php
public function sqlPartsCount(): int

Возвращает

Число частей; 0 у нетронутого репозитория.

Пример

php
$repository = UserRepository::instance();

$repository->sqlPartsCount();                       // 0
$repository->where(Qb::eq('id', 1))->sqlPartsCount(); // больше нуля

cleanCache()

Сбрасывает накопленное состояние запроса.

Методы чтения и записи вызывают его сами после выполнения, поэтому вручную он нужен в одном случае: запрос начали собирать, а выполнять передумали, и тот же экземпляр хочется переиспользовать.

Синтаксис

php
public function cleanCache(?string $param = null): void

Параметры

$param — имя части, которую нужно сбросить. null (умолчание) сбрасывает всё.

Пример

php
$repository = UserRepository::instance()->where(Qb::eq('active', true));

if ($skipFilter) {
  $repository->cleanCache();     // условие больше не нужно
}

db()

Возвращает соединение с базой, на котором работает репозиторий.

Через него открывают транзакции и, при необходимости, обращаются к базе напрямую. Это то же соединение, что у любого другого репозитория той же конфигурации в пределах одного запроса.

Синтаксис

php
public function db(): CDO

Возвращает

Объект соединения.

Пример

php
$db = UserRepository::instance()->db();
$db->beginTransaction();

Сведения о репозитории

Четыре метода, возвращающие то, что задано свойствами класса. Нужны редко — в основном инструментам вроде миграций и генераторов.

php
public function getDbConfigClassName(): string   // класс конфигурации базы
public function getEntityClassName(): string     // класс сущности
public function getSchema(): ?string             // схема или null
public function originTable(): string            // имя таблицы со схемой, если она задана

Ошибки

Исключение Когда Что с ним делает фреймворк
RepositoryException ошибка базы: недоступна, неверный SQL, нарушено ограничение превращается в ответ 500
EntityException строка не найдена в findByIdOrThrow() / findByOrThrow() превращается в ответ с переданным кодом, по умолчанию 404

Оба всплывают наружу, поэтому ловить их в контроллере обычно не нужно: ответ соберёт фреймворк. Ловят там, где на ошибку есть осмысленная реакция — повтор, запасной источник, пометка задачи как неудачной.

php
try {
  $user = UserRepository::instance()->findByIdOrThrow($id);
} catch (EntityException) {
  $user = UserRepository::instance()->insert(['id' => $id, 'name' => 'Гость']);
}

Дальше