CRUD и выборка
После сборки запроса его выполняет fetch-метод (find, findAll, count), а
запись — методы insert, update, delete, upsert. Есть и статические
шорткаты (findById, findByIdOrThrow) на частые случаи. Все методы работают с
таблицей репозитория и защищены от инъекций.
Что такое CRUD и зачем
CRUD — Create, Read, Update, Delete: четыре базовые операции над данными.
Проблема. Писать эти операции сырым SQL — однообразно и опасно: INSERT с
интерполяцией значений, UPDATE ... WHERE руками, отдельная забота про типы и
инъекции. Один и тот же код повторяется в каждом репозитории.
Решение. PPA даёт готовые типизированные методы: fetch выполняет собранный
запрос, а insert/update/delete/upsert инкапсулируют запись с параметрами.
Об этом и раздел.
Выполнение выборки
Fetch-методы завершают цепочку конструктора запросов:
final public function find(?string $entityClassName = null): ?object // первая строка или null
final public function findAll(?string $entityClassName = null): array // все строки
final public function findColumn(int $column = 0): mixed // одна колонка
final public function count(): int // COUNT(*)
final public function exists(): bool // EXISTS
final public function rawFetch(string $sql, array $binds = [], ?string $entityClassName = null): arrayuse Flytachi\Winter\K2\Ppa\Repository\Qb;
$user = UserRepository::instance()->where(Qb::eq('email', $email))->find();
$users = UserRepository::instance('u')->where(Qb::eq('u.status', 'active'))->findAll();
$total = UserRepository::instance()->where(Qb::eq('status', 'active'))->count();
$has = UserRepository::instance()->where(Qb::eq('email', $email))->exists();Статические finders
Шорткаты, которые сами создают инстанс, применяют условие и выполняют выборку — для частых случаев без цепочки:
| Метод | Условие | Результат | Бросает при отсутствии |
|---|---|---|---|
findById($id) |
PK = $id | ?object |
нет |
findBy($qb) |
свой Qb |
?object |
нет |
findAllBy($qb = null) |
свой Qb / все |
array |
нет |
findByIdOrThrow($id) |
PK = $id | object |
да — EntityException |
findByOrThrow($qb) |
свой Qb |
object |
да — EntityException |
$user = UserRepository::findById(42); // ?object
$user = UserRepository::findBy(Qb::eq('email', $email)); // ?object
$users = UserRepository::findAllBy(Qb::eq('status', 'active')); // array
$all = UserRepository::findAllBy(); // все строкиВарианты *OrThrow бросают EntityException с HTTP-кодом (по умолчанию 404) —
удобно прямо в контроллере/сервисе, ошибка сама станет корректным ответом:
use Flytachi\Winter\Base\HttpCode;
$user = UserRepository::findByIdOrThrow($id); // → 404, если нет
$user = UserRepository::findByOrThrow(
Qb::eq('token', $resetToken),
message: 'Invalid or expired reset token',
httpCode: HttpCode::UNPROCESSABLE_ENTITY,
);EntityException — это HTTP-ошибка
EntityException несёт HTTP-код и логируется на уровне warning. Брошенная из
сервиса, она превращается в ответ автоматически (см.
Обработка ошибок).
Вставка
public function insert(object|array $entity): mixed // → сгенерированный PK
public function insertGroup(array|object ...$entities): void // пакетная вставкаinsert() возвращает сгенерированный первичный ключ. null-значения в вставку не
попадают — БД подставит дефолты:
$repo = new UserRepository();
$id = $repo->insert([
'id' => null, // исключается → авто-инкремент/дефолт
'name' => 'Alice',
'email' => 'alice@example.com',
'status' => 'active',
]);
// Пакетно (строки чанкуются под лимиты плейсхолдеров):
$repo->insertGroup(
['name' => 'Bob', 'email' => 'b@example.com'],
['name' => 'Carol', 'email' => 'c@example.com'],
);Обновление и удаление
public function update(object|array $entity, Qb $qb): int|string // → число затронутых строк
public function delete(Qb $qb): int|string // → число удалённыхУсловие обязательно
У update() и delete() всегда требуется Qb-условие — варианта «обновить/
удалить всё» нет. Это защита от случайного изменения всей таблицы.
$repo = new UserRepository();
$affected = $repo->update(['status' => 'inactive'], Qb::eq('id', 42));
$affected = $repo->update(
['status' => 'inactive', 'updated_at' => date('Y-m-d H:i:s')],
Qb::and(
Qb::lt('last_login', '2024-01-01'),
Qb::eq('status', 'active'),
),
);
$deleted = $repo->delete(Qb::eq('id', 42));Upsert
public function upsert(object|array $entity, array $conflictColumns, ?array $updateColumns = null): mixed
public function upsertGroup(array $entities, array $conflictColumns, ?array $updateColumns = null): voidВставляет запись; при конфликте по $conflictColumns — обновляет.
$updateColumns = null означает «ничего не делать» (ignore). В выражениях
обновления доступны токены :new и :current:
$repo = new ProductRepository();
// Вставить или обновить при конфликте по sku:
$repo->upsert(
['sku' => 'A-100', 'price' => 250, 'stock' => 5],
['sku'],
['price' => ':new', 'stock' => ':current + :new'],
);
// pgsql: ON CONFLICT (sku) DO UPDATE SET price = EXCLUDED.price, ...
// Игнорировать при конфликте:
$repo->upsert(['sku' => 'A-100', 'price' => 250], ['sku']); // updateColumns = nullОбработка ошибок
Методы записи ловят CDOException и перебрасывают как RepositoryException
(лог alert). Оригинал доступен через getPrevious():
use Flytachi\Winter\K2\Ppa\Repository\RepositoryException;
try {
$repo->insert($user);
} catch (RepositoryException $e) {
$e->getMessage(); // человекочитаемое сообщение
$e->getPrevious(); // исходное CDOException
}Дальше
- Пагинация — постраничная выдача
findAll - Конструктор запросов — сборка условий для этих методов
- Сущности — во что гидрируется результат