Репозитории
Репозиторий — класс, привязанный к таблице. Запрос собирается вызовами методов, значения уезжают привязками, а результат приходит объектами вашей сущности. Ниже — как объявить, как собрать и полный разбор каждого метода: что принимает, что возвращает, что делает, когда ничего не нашлось.
Что такое репозиторий
Место, где живут запросы к одной таблице. Без него SQL расползается строками по контроллерам: одна и та же выборка пишется заново там, где понадобилась, параметры привязываются руками, а переименованная колонка обнаруживается в рантайме.
// без репозитория
$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 принимают другой репозиторий, а не строку — и он приносит с собой свои привязки.
Объявление
<?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() короче в цепочке и сразу принимает
алиас таблицы — а он нужен всюду, где в запросе появляется вторая таблица.
Синтаксис
public static function instance(?string $as = null): staticПараметры
$as — алиас таблицы в запросе. По умолчанию null, то есть таблица участвует под своим
именем.
Возвращает
Новый экземпляр репозитория. Состояние запроса пустое.
Пример
UserRepository::instance(); // FROM users
UserRepository::instance('u'); // FROM users uКаждый вызов — отдельный запрос
Условия накапливаются в объекте. Два вызова instance() дают два независимых запроса,
а попытка дособрать ранее полученный экземпляр добавит условия к тому, что там уже
накоплено.
Поэтому запрос собирают одной цепочкой, а не по частям в разных местах метода.
as()
Задаёт алиас основной таблицы уже созданному репозиторию.
То же, что аргумент instance(), но применимо к экземпляру, который вы
получили иначе — например, внедрённому через контейнер.
Синтаксис
public function as(string $alias): staticПараметры
$alias — алиас. Дальше на него ссылаются в условиях и списке колонок.
Возвращает
Тот же репозиторий.
Пример
$repository->as('u')->select('u.name')->where(Qb::eq('u.active', 1));Результат
SELECT u.name FROM users u WHERE u.active = :iqb0select()
Задаёт список колонок вместо подставляемого по умолчанию.
Без вызова репозиторий выбирает поля сущности — те, что объявлены в классе из
$entityClassName. Это удобно, пока строки гидрируются в сущность; для агрегатов и
выборки пары колонок список задают вручную.
Синтаксис
public function select(string $option): staticПараметры
$option — список колонок строкой, как он выглядел бы в SQL: 'id, name',
'COUNT(*) AS cnt', 'u.name, o.total'.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()
->select('id, name')
->where(Qb::like('name', 'Ан%'))
->orderBy('name ASC')
->limit(20, 40);Результат
SELECT id, name FROM users WHERE name LIKE :iqb1 ORDER BY name ASC LIMIT 20 OFFSET 40Список колонок — единственное место, где нужен SQL-текст
Значения сюда не подставляют: для них есть привязки. Если в списке появляется
пользовательский ввод — это ошибка, и её нужно переписать на условие через Qb.
from()
Меняет таблицу, из которой идёт выборка.
Нужен в двух случаях: когда выбирают из CTE, объявленного через
with(), и когда основной таблицей должен стать другой
репозиторий.
Синтаксис
public function from(RepositoryInterface|string $repository): staticПараметры
$repository — другой репозиторий либо имя: таблицы, представления или ранее объявленного
CTE.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()
->with('recent', OrderRepository::instance())
->select('*')
->from('recent');Результат
WITH recent AS (SELECT id, user_id, total FROM orders) SELECT * FROM recentwhere(), andWhere(), orWhere(), xorWhere()
Добавляют условие к запросу.
where() задаёт условие, остальные три присоединяют следующее логической связкой:
AND, OR, XOR. Условия строятся объектом Qb — он же отвечает за привязку значений,
поэтому пользовательский ввод не попадает в текст запроса ни при каком написании.
Синтаксис
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 просто ничего не добавляет.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()
->where(Qb::eq('active', true))
->andWhere(Qb::gt('age', 18))
->orWhere(Qb::eq('role', 'admin'));Результат
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()
Присоединяют к запросу другую таблицу.
Все пять принимают репозиторий, а не имя таблицы. Это не формальность: присоединённый репозиторий приносит с собой своё имя таблицы, схему и — если у него уже собраны условия — свои привязки.
Синтаксис
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 |
все сочетания строк обеих таблиц |
Пример
UserRepository::instance()
->select('users.name, orders.total')
->joinLeft(OrderRepository::instance(), Qb::raw('orders.user_id = users.id'));Результат
SELECT users.name, orders.total FROM users
LEFT JOIN orders ON(orders.user_id = users.id)groupBy()
Группирует строки по значению колонок — для агрегатов вроде COUNT, SUM, AVG.
Синтаксис
public function groupBy(string $context): staticПараметры
$context — список колонок строкой: 'email', 'user_id, status'.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()
->select('email, COUNT(*) AS cnt')
->groupBy('email');having()
Фильтрует уже сгруппированные строки.
Отличается от where() моментом применения: WHERE
отсеивает строки до группировки, HAVING — получившиеся группы. Поэтому агрегат
(COUNT(*) > 1) можно проверить только здесь.
Синтаксис
public function having(string $context): staticПараметры
$context — условие строкой. Привязок здесь нет, поэтому пользовательскому вводу тут не
место.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()
->select('email, COUNT(*) AS cnt')
->groupBy('email')
->having('COUNT(*) > 1');Результат
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1orderBy()
Задаёт порядок строк в результате.
Синтаксис
public function orderBy(string $context): staticПараметры
$context — колонки и направление: 'id DESC', 'name ASC, created_at DESC'.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()->orderBy('created_at DESC, id DESC');Порядок нужен всюду, где есть постраничность
Без ORDER BY база не обязана возвращать строки в одном и том же порядке между запросами.
На практике это проявляется так: одна и та же запись попадает и на первую страницу, и на
вторую, а другая не попадает никуда.
limit()
Ограничивает число строк и задаёт смещение.
Синтаксис
public function limit(int $limit, int $offset = 0): staticПараметры
$limit — сколько строк вернуть.
$offset — сколько пропустить от начала. По умолчанию 0.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()->orderBy('id')->limit(20, 40); // третья страница по 20Результат
SELECT id, name, email, active FROM users ORDER BY id LIMIT 20 OFFSET 40Для страниц есть отдельный инструмент
Считать смещения руками приходится редко: постраничный вывод со счётчиком страниц и курсором описан на странице Пагинация.
forBy()
Добавляет блокировку строк в конце запроса — FOR UPDATE и подобные.
Нужен, когда выбранные строки будут изменены в этой же транзакции и нельзя допустить, чтобы их одновременно изменил кто-то другой.
Синтаксис
public function forBy(string $context): staticПараметры
$context — текст блокировки: 'UPDATE', 'SHARE', 'UPDATE NOWAIT',
'UPDATE SKIP LOCKED'. Набор зависит от базы.
Возвращает
Тот же репозиторий.
Пример
$order = OrderRepository::instance()
->where(Qb::eq('id', $id))
->forBy('UPDATE')
->find();union(), unionAll()
Объединяют результат с результатом другого репозитория.
union() убирает дубликаты строк, unionAll() оставляет всё как есть и потому быстрее.
Синтаксис
public function union(RepositoryInterface $repository): static
public function unionAll(RepositoryInterface $repository): staticПараметры
$repository — репозиторий, чей результат присоединяется.
Возвращает
Тот же репозиторий.
Пример
ActiveUserRepository::instance()->select('name')
->unionAll(ArchivedUserRepository::instance()->select('name'));Списки колонок должны совпадать
Обе части объединения обязаны возвращать одинаковое число колонок совместимых типов —
этого требует SQL, и проверить это за вас репозиторий не может. Практический вывод:
задавайте select() явно обеим сторонам, иначе каждая подставит поля своей
сущности.
with(), withRecursive()
Объявляют общее табличное выражение — CTE, именованный подзапрос, на который дальше можно ссылаться как на таблицу.
withRecursive() объявляет рекурсивное выражение — то, которое ссылается само на себя.
Так обходят деревья: категории, комментарии, структуру подчинения.
Синтаксис
public function with(string $name, RepositoryInterface $repository, ?string $modifier = null): static
public function withRecursive(string $name, RepositoryInterface $repository): staticПараметры
$name — имя выражения. По нему на него ссылаются в from() и джойнах.
$repository — репозиторий, чей запрос становится телом выражения.
$modifier — дополнительное указание базе, например MATERIALIZED. Поддержка зависит от
базы.
Возвращает
Тот же репозиторий.
Пример
UserRepository::instance()
->with('recent', OrderRepository::instance())
->select('*')
->from('recent');Результат
WITH recent AS (SELECT id, user_id, total FROM orders) SELECT * FROM recentbinding()
Добавляет привязки значений вручную.
Нужен в редком случае: когда в запросе есть именованный placeholder, появившийся не через
Qb — например, внутри выражения, переданного в select() или
having().
Синтаксис
public function binding(?array $binds): staticПараметры
$binds — массив привязок. null очищает добавленные ранее.
Возвращает
Тот же репозиторий.
Чтение
Методы этой группы выполняют собранный запрос. После выполнения состояние сбрасывается — тот же экземпляр можно собрать заново, накопленные условия к следующему запросу не прилипнут.
find()
Выполняет запрос и возвращает первую подходящую строку.
К запросу автоматически добавляется ограничение в одну строку, поэтому база не выбирает лишнего, даже если условию соответствуют тысячи записей.
Синтаксис
public function find(?string $entityClassName = null): ?objectПараметры
$entityClassName — класс, в который гидрировать строку. По умолчанию берётся
$entityClassName репозитория.
Возвращает
Объект сущности либо null, если ничего не найдено.
Ошибки
RepositoryException — при ошибке базы: недоступна, синтаксис запроса неверен, нарушено
ограничение.
Пример
$user = UserRepository::instance()
->where(Qb::eq('email', $email))
->find();
if ($user === null) {
return ResponseEntity::notFound(['message' => 'Пользователь не найден']);
}findAll()
Выполняет запрос и возвращает все подходящие строки.
Синтаксис
public function findAll(?string $entityClassName = null): arrayПараметры
$entityClassName — класс для гидрации. По умолчанию — сущность репозитория.
Возвращает
Массив объектов. Пустой массив, если ничего не найдено — не null, поэтому результат
можно сразу перебирать без проверки.
Ошибки
RepositoryException — при ошибке базы.
Пример
$users = UserRepository::instance()
->where(Qb::eq('active', true))
->orderBy('name ASC')
->findAll();Результат
[{"id":1,"name":"Анна П.","email":"anna@example.com","active":1}]Ограничение задавайте сами
findAll() без limit() вернёт все строки, подошедшие под условие, и все
они окажутся в памяти процесса. На таблице в миллион записей это заканчивается исчерпанием
памяти.
findById()
Возвращает строку по первичному ключу.
Короткая запись для where(Qb::eq(<первичный ключ>, $id))->find(). Имя колонки ключа
репозиторий определяет сам по разметке сущности.
Синтаксис
public function findById(string|int $id, ?string $entityClassName = null): ?objectПараметры
$id — значение первичного ключа.
$entityClassName — класс для гидрации.
Возвращает
Объект сущности либо null.
Пример
$user = UserRepository::instance()->findById(1);Результат
{"id":1,"name":"Анна","email":"anna@example.com","active":1}findBy()
Возвращает первую строку, подходящую под переданное условие.
Отличается от find() тем, что условие передаётся аргументом, а не собирается
цепочкой. Удобно для однострочных выборок.
Синтаксис
public function findBy(Qb $qb, ?string $entityClassName = null): ?objectПараметры
$qb — условие.
$entityClassName — класс для гидрации.
Возвращает
Объект сущности либо null.
Пример
$user = UserRepository::instance()->findBy(Qb::eq('email', $email));findAllBy()
Возвращает все строки, подходящие под переданное условие.
Синтаксис
public function findAllBy(?Qb $qb = null, ?string $entityClassName = null): arrayПараметры
$qb — условие. null (умолчание) означает «без условия» — вернутся все строки таблицы.
$entityClassName — класс для гидрации.
Возвращает
Массив объектов, возможно пустой.
Пример
$active = UserRepository::instance()->findAllBy(Qb::eq('active', true));
$all = UserRepository::instance()->findAllBy(); // вся таблицаfindByIdOrThrow()
Возвращает строку по первичному ключу или бросает исключение, если её нет.
Существует ради избавления от повторяющейся проверки «нашли — не нашли — вернуть 404». Исключение всплывает наружу и превращается фреймворком в HTTP-ответ, поэтому промежуточным слоям не нужно ни проверять, ни пробрасывать результат.
Синтаксис
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.
Пример
#[GetMapping('users/{id}')]
public function show(#[PathVariable] int $id): ResponseEntity
{
$user = UserRepository::instance()->findByIdOrThrow($id, message: 'Пользователь не найден');
return ResponseEntity::ok($user); // сюда попадаем, только если пользователь есть
}findByOrThrow()
То же, что findByIdOrThrow(), но по произвольному условию.
Синтаксис
public function findByOrThrow(
Qb $qb,
?string $entityClassName = null,
string $message = 'Entity not found',
HttpCode $httpCode = HttpCode::NOT_FOUND,
): objectПараметры
$qb — условие поиска.
Остальные совпадают с findByIdOrThrow().
Возвращает
Объект сущности.
Ошибки
EntityException — если строка не найдена.
Пример
$order = OrderRepository::instance()->findByOrThrow(
Qb::and(Qb::eq('number', $number), Qb::eq('user_id', $userId)),
message: 'Заказ не найден',
);findColumn()
Возвращает значение одной колонки первой строки.
Нужен там, где объект не нужен: одно имя, одна сумма, один идентификатор.
Синтаксис
public function findColumn(int $column = 0): mixedПараметры
$column — порядковый номер колонки в списке выборки, считая с нуля. По умолчанию первая.
Возвращает
Значение колонки как его отдала база, либо false, если строк нет.
Пример
$name = UserRepository::instance()
->select('name')
->where(Qb::eq('id', 1))
->findColumn();Результат
'Анна'count()
Возвращает число строк, подходящих под собранный запрос.
Считает база — строки в память не поднимаются. Условия, джойны и группировки, собранные до вызова, учитываются.
Синтаксис
public function count(): intВозвращает
Число строк.
Пример
$total = UserRepository::instance()->where(Qb::eq('active', true))->count();exists()
Сообщает, есть ли хотя бы одна строка, подходящая под собранный запрос.
Отличается от count() > 0 тем, что база останавливается на первом совпадении и не
пересчитывает остальные. На большой таблице разница между «есть ли хоть один» и «сколько
их всего» существенная.
Синтаксис
public function exists(): boolВозвращает
true, если найдена хотя бы одна строка.
Пример
if (UserRepository::instance()->where(Qb::eq('email', $email))->exists()) {
throw new ResponseException('Такой email уже занят', HttpCode::CONFLICT);
}rawFetch()
Выполняет произвольный SQL и возвращает гидрированные объекты.
Запасной выход для запросов, которые не выражаются сборкой: оконные функции, специфичные для базы конструкции, тяжёлые отчёты. Собранное цепочкой состояние при этом не используется — выполняется ровно тот SQL, который вы передали.
Синтаксис
public function rawFetch(string $sql, array $binds = [], ?string $entityClassName = null): arrayПараметры
$sql — текст запроса с именованными placeholder’ами.
$binds — массив объектов привязки CDOBind, по одному на placeholder.
$entityClassName — класс, в который гидрировать строки. По умолчанию — сущность
репозитория.
Возвращает
Массив объектов.
Ошибки
RepositoryException — при ошибке базы.
Пример
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,
);Результат
[{"email":"a@example.com","cnt":2}]Класс гидрации задавайте явно
Без третьего аргумента строки гидрируются в сущность репозитория. Для запроса, чьи колонки с ней не совпадают, это даёт объект, наполовину заполненный умолчаниями, а лишние колонки превращаются в динамические свойства — в PHP 8.2 это уже предупреждение.
Передавайте свой класс под форму результата, как в примере, либо stdClass::class, если
структура заранее не известна.
Запись
Как и методы чтения, все они выполняют запрос немедленно и сбрасывают состояние репозитория после выполнения.
insert()
Добавляет одну строку в таблицу.
Синтаксис
public function insert(object|array $entity): mixedПараметры
$entity — объект сущности либо ассоциативный массив «колонка → значение». Массив удобен,
когда заполняется часть полей и создавать объект ради этого не хочется.
Возвращает
Идентификатор вставленной строки в том виде, в каком его отдала база — для колонки
с автоинкрементом это строка, а не число. Приводите к int сами, если нужен int.
Ошибки
RepositoryException — при ошибке базы: нарушено ограничение уникальности, отсутствует
обязательная колонка, недоступно соединение.
Пример
$user = new User();
$user->name = 'Анна';
$user->email = 'anna@example.com';
$id = UserRepository::instance()->insert($user);
// либо массивом, без создания объекта
$id = UserRepository::instance()->insert(['name' => 'Борис', 'email' => 'boris@example.com']);Результат
'1' // строка, не числоinsertBatch()
Добавляет много строк за один проход.
Отличается от цикла с insert() тем, что строки уходят в базу пачками, а не по одной:
на тысяче записей это разница между тысячей обращений и несколькими. Принимает iterable,
поэтому генератор тоже подойдёт — и тогда все строки не окажутся в памяти одновременно.
Синтаксис
public function insertBatch(Traversable|object|array $entities): voidПараметры
$entities — массив, объект-обход или генератор объектов либо массивов.
Возвращает
Ничего. Идентификаторы вставленных строк не возвращаются — если они нужны, вставляйте по
одной через insert().
Ошибки
RepositoryException — при ошибке базы.
Пример
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()
Изменяет строки, подходящие под условие.
Синтаксис
public function update(object|array $entity, Qb $qb): string|intПараметры
$entity — новые значения: объект сущности либо массив «колонка → значение». Массивом
обновляют часть колонок, не трогая остальные.
$qb — условие отбора строк. Аргумент обязателен: обновление всей таблицы должно быть
написано явно, а не получиться случайно.
Возвращает
Число изменённых строк.
Ошибки
RepositoryException — при ошибке базы.
Пример
$affected = UserRepository::instance()->update(
['name' => 'Анна П.'],
Qb::eq('id', 1),
);Результат
1delete()
Удаляет строки, подходящие под условие.
Синтаксис
public function delete(Qb $qb): string|intПараметры
$qb — условие отбора. Обязателен по той же причине, что и у update().
Возвращает
Число удалённых строк.
Ошибки
RepositoryException — при ошибке базы, включая нарушение внешнего ключа, если на строку
кто-то ссылается.
Пример
$deleted = UserRepository::instance()->delete(Qb::eq('id', 1));upsert()
Вставляет строку, а если такая уже есть — обновляет её.
«Такая» определяется набором колонок, по которым база проверяет конфликт: обычно это
первичный ключ или уникальный индекс. Операция выполняется одним запросом, поэтому между
проверкой и вставкой не может вклиниться другой процесс — в отличие от связки
«проверить exists(), потом вставить».
Синтаксис
public function upsert(object|array $entity, array $conflictColumns, ?array $updateColumns = null): mixedПараметры
$entity — данные строки: объект сущности либо массив.
$conflictColumns — колонки, по которым определяется конфликт. На них должен быть
уникальный индекс, иначе база не поймёт запрос.
$updateColumns — какие колонки обновлять при конфликте. null (умолчание) означает
ничего не обновлять: строка останется прежней, а вставка просто не произойдёт.
Возвращает
Идентификатор строки, как у insert().
Ошибки
RepositoryException — при ошибке базы.
Пример
// Обновить имя, если пользователь с таким 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 и отправляет строки пачками.
Синтаксис
public function upsertBatch(iterable $entities, array $conflictColumns, ?array $updateColumns = null): voidПараметры
$entities — массив, обход или генератор строк.
$conflictColumns — колонки конфликта, общие для всех строк.
$updateColumns — колонки для обновления; null означает «не обновлять».
Возвращает
Ничего.
Пример
UserRepository::instance()->upsertBatch(
readRows('/tmp/import.csv'),
conflictColumns: ['email'],
updateColumns: ['name'],
);Транзакции
Транзакция открывается на соединении, а соединение у единицы работы одно — поэтому все репозитории внутри одного запроса попадают в одну транзакцию автоматически.
$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()
Собирает текст запроса, не выполняя его.
Первое, к чему стоит обращаться, когда результат не тот: видно, во что превратилась цепочка вызовов.
Синтаксис
public function buildSql(array $ignoreParts = []): stringПараметры
$ignoreParts — части, которые нужно исключить из сборки, например ['limit']. По
умолчанию собираются все.
Возвращает
Текст запроса с именами привязок вместо значений. Сами значения смотрят через
getSql().
Пример
$sql = UserRepository::instance()
->where(Qb::eq('id', 42))
->buildSql();Результат
SELECT id, name, email FROM users WHERE id = :iqb0getSql()
Возвращает накопленное состояние запроса — части и привязки.
Нужен, когда мало увидеть текст: например, чтобы понять, какое значение ушло в конкретную привязку.
Синтаксис
public function getSql(?string $param = null): mixedПараметры
$param — имя интересующей части. null (умолчание) возвращает всё состояние целиком.
Возвращает
Массив состояния либо значение запрошенной части.
Пример
$repository = UserRepository::instance()->where(Qb::eq('id', 42));
$state = $repository->getSql(); // всё состояние
$binds = $repository->getSql('binds'); // только привязкиsqlPartsCount()
Возвращает число накопленных частей запроса.
Практическое применение одно: проверить, что состояние пустое. У свежего экземпляра
результат — 0, и если он больше нуля там, где вы ожидали чистый репозиторий, значит на
объекте уже что-то собрано.
Синтаксис
public function sqlPartsCount(): intВозвращает
Число частей; 0 у нетронутого репозитория.
Пример
$repository = UserRepository::instance();
$repository->sqlPartsCount(); // 0
$repository->where(Qb::eq('id', 1))->sqlPartsCount(); // больше нуляcleanCache()
Сбрасывает накопленное состояние запроса.
Методы чтения и записи вызывают его сами после выполнения, поэтому вручную он нужен в одном случае: запрос начали собирать, а выполнять передумали, и тот же экземпляр хочется переиспользовать.
Синтаксис
public function cleanCache(?string $param = null): voidПараметры
$param — имя части, которую нужно сбросить. null (умолчание) сбрасывает всё.
Пример
$repository = UserRepository::instance()->where(Qb::eq('active', true));
if ($skipFilter) {
$repository->cleanCache(); // условие больше не нужно
}db()
Возвращает соединение с базой, на котором работает репозиторий.
Через него открывают транзакции и, при необходимости, обращаются к базе напрямую. Это то же соединение, что у любого другого репозитория той же конфигурации в пределах одного запроса.
Синтаксис
public function db(): CDOВозвращает
Объект соединения.
Пример
$db = UserRepository::instance()->db();
$db->beginTransaction();Сведения о репозитории
Четыре метода, возвращающие то, что задано свойствами класса. Нужны редко — в основном инструментам вроде миграций и генераторов.
public function getDbConfigClassName(): string // класс конфигурации базы
public function getEntityClassName(): string // класс сущности
public function getSchema(): ?string // схема или null
public function originTable(): string // имя таблицы со схемой, если она заданаОшибки
| Исключение | Когда | Что с ним делает фреймворк |
|---|---|---|
RepositoryException |
ошибка базы: недоступна, неверный SQL, нарушено ограничение | превращается в ответ 500 |
EntityException |
строка не найдена в findByIdOrThrow() / findByOrThrow() |
превращается в ответ с переданным кодом, по умолчанию 404 |
Оба всплывают наружу, поэтому ловить их в контроллере обычно не нужно: ответ соберёт фреймворк. Ловят там, где на ошибку есть осмысленная реакция — повтор, запасной источник, пометка задачи как неудачной.
try {
$user = UserRepository::instance()->findByIdOrThrow($id);
} catch (EntityException) {
$user = UserRepository::instance()->insert(['id' => $id, 'name' => 'Гость']);
}Дальше
- Сущности — во что гидрируются строки и как размечаются колонки
- Пагинация — постраничный вывод вместо
limit()вручную - Конфигурация БД — к какой базе обращается репозиторий
- Пул соединений — откуда берётся соединение
db()