Сущности и маппинг
Сущность — это типизированный объект строки таблицы. Колонки описываются
атрибутами на свойствах: тип, ключ, индекс, внешний ключ, ограничение. Из
этих же атрибутов PPA строит SELECT и — при миграции — DDL таблицы.
Что такое сущность и зачем
Сущность (entity) — PHP-класс, одно свойство которого соответствует одной колонке таблицы.
Проблема. Если результат запроса — безликий массив ($row['created_at']), нет
ни типов, ни автодополнения, ни единого места, где видно устройство таблицы.
Колонки, их типы и связи размазаны по SQL-строкам.
Решение. Опишите строку типизированным классом, а колонки — атрибутами. PPA гидрирует результат в такой объект (типы, автодополнение), а из атрибутов умеет сгенерировать схему таблицы. Один класс — источник правды о структуре. Об этом и раздел.
Базовый вид
Сгенерируйте сущность командой make (флаг -e):
php call make -e .User # → main/User.phpКласс помечается #[Table], свойства — атрибутами колонок:
<?php
namespace Main;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Entity\Table;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Hybrid\Id;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Primal\Varchar;
#[Table]
class User
{
#[Id]
public ?int $id = null;
#[Varchar(200)]
public string $username;
}#[Table] обязателен для миграций
Без #[Table] сущность не попадёт в скан миграций — таблица не сгенерируется. Для
гидрации результатов запроса #[Table] не требуется, но с ним всё в одном месте.
Первичные ключи
Атрибуты-«гибриды» разворачиваются в набор других — это готовые шаблоны для первичных ключей:
| Атрибут | Разворачивается в |
|---|---|
#[Id] |
#[Primary] + автоинкремент + INTEGER, not null |
#[BigId] |
то же с BIGINT |
#[SmallId] |
то же с SMALLINT |
#[UuidPk] |
#[Primary] + UUID, not null + default gen_random_uuid() (pgsql) |
#[Id] public ?int $id = null; // авто-инкремент int PK
#[UuidPk] public ?string $id = null; // UUID PK со значением по умолчаниюТипы колонок
Ровно один тип на свойство (Ppa\Mapping\Attributes\Primal). Тип свойства PHP
проверяется на совместимость на этапе скана:
| Атрибут | SQL (pgsql / mysql) |
|---|---|
#[Integer] |
INTEGER / INT |
#[BigInteger] |
BIGINT |
#[SmallInteger] |
SMALLINT |
#[Boolean] |
BOOLEAN |
#[FloatType] / #[Double] |
REAL / DOUBLE PRECISION |
#[Decimal(precision = 12, scale = 2)] |
NUMERIC(p,s) / DECIMAL(p,s) |
#[Varchar(length = 255)] |
VARCHAR(N) |
#[Char(length)] |
CHAR(N) |
#[Text] |
TEXT |
#[Json] |
JSONB / JSON |
#[TextArray] |
TEXT[] / JSON |
#[Date] / #[Time] |
DATE / TIME |
#[DateTime] |
TIMESTAMP (без TZ) |
#[Timestamp(withTimeZone = true)] |
TIMESTAMP WITH TIME ZONE |
#[Uuid(asBinary = false)] |
UUID / CHAR(36) |
#[Binary(length = 255)] / #[Blob(size)] |
BYTEA / VARBINARY · BLOB |
#[Type('...')] |
Произвольное определение как есть |
Null и значения по умолчанию
| Атрибут | Эффект |
|---|---|
#[NullableIs(isNullable = true)] |
Переопределяет nullability, выведенную из типа PHP |
#[DefaultVal('...')] |
Сырой SQL-дефолт: 'NOW()', 'gen_random_uuid()', "'pending'" |
Без #[DefaultVal] дефолт выводится из значения свойства PHP: null → DEFAULT NULL, false → DEFAULT FALSE, 'pending' → DEFAULT 'pending', число → как есть.
#[Boolean]
public bool $is_deleted = false; // → DEFAULT FALSE
#[Timestamp]
#[DefaultVal('NOW()')]
public string $created_at;Индексы
Атрибуты Ppa\Mapping\Attributes\Idx. Повторяемы — можно навесить несколько на
одно свойство:
| Атрибут | Создаёт |
|---|---|
#[Primary] |
PRIMARY KEY (несколько → составной ключ) |
#[Unique(columns: [], name: ?, where: ?, ...)] |
CREATE UNIQUE INDEX |
#[Index(columns: [], name: ?, where: ?, ...)] |
CREATE INDEX |
columns: ['other'] добавляет в индекс дополнительные колонки (свойство-носитель
подставляется первым). Опции where (частичный индекс) и opClass —
только PostgreSQL.
#[Varchar(200)]
#[NullableIs(false)]
#[Unique] // UNIQUE INDEX по username
public string $username;Внешние ключи и ограничения
Атрибуты Ppa\Mapping\Attributes\Constraint:
| Атрибут | Назначение |
|---|---|
#[ForeignRepo(repoClass, onUpdate, onDelete, name?)] |
FK по репозиторию — таблица и PK берутся из него |
#[ForeignKey(table, column, onUpdate, onDelete, name?)] |
FK по явным таблице и колонке |
#[Check('expr', name?)] |
Одно ограничение CHECK |
#[CheckEnum(BackedEnum::class, name?)] |
col IN (...) из кейсов backed-enum |
Действия FK — enum FKAction (Mapping\Constants\FKAction): RESTRICT,
NO_ACTION, SET_DEFAULT, SET_NULL, CASCADE.
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Constraint\ForeignRepo;
use Flytachi\Winter\K2\Ppa\Mapping\Constants\FKAction;
#[Uuid]
#[ForeignRepo(InstanceRepository::class, FKAction::CASCADE, FKAction::CASCADE)]
public string $instance_id;
#[SmallInteger]
#[CheckEnum(InstanceStatus::class)] // col IN (0, 1, 2)
public int $status;Полный пример
<?php
namespace Main\Entities;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Entity\Table;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Hybrid\UuidPk;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Primal\{Uuid, Varchar, BigInteger, Boolean, Timestamp};
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Idx\Unique;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Constraint\ForeignRepo;
use Flytachi\Winter\K2\Ppa\Mapping\Attributes\Additive\DefaultVal;
use Flytachi\Winter\K2\Ppa\Mapping\Constants\FKAction;
#[Table]
class FileRecord
{
#[UuidPk]
public ?string $id = null;
#[Uuid]
#[ForeignRepo(InstanceRepository::class, FKAction::CASCADE, FKAction::CASCADE)]
#[Unique(['name'])]
public string $instance_id;
#[Varchar(255)]
public string $name;
#[BigInteger]
public int $size;
#[Boolean]
public bool $is_public = false;
#[Timestamp]
#[DefaultVal('NOW()')]
public string $created_at;
}Как сущность влияет на SELECT
Свойство $entityClassName репозитория задаёт, во что гидрируются строки, и как
строится список колонок:
- Обычный класс (не наследует
stdClass) → PPA читает публичные свойства и строит явныйSELECT id, name, email FROM .... - Класс, наследующий
\stdClass(или дефолт\stdClass) → выборка через*.
Своя формула колонки — EntityInterface
Чтобы задать SQL-выражение для свойства, реализуйте EntityInterface::selection()
— карта [свойство => 'SQL AS свойство']. Отсутствующие в карте свойства
выбираются по имени:
use Flytachi\Winter\K2\Ppa\Entity\EntityInterface;
class UserCard implements EntityInterface
{
public int $id;
public string $fullName;
public static function selection(): array
{
return [
'fullName' => "CONCAT(u.first_name, ' ', u.last_name) AS fullName",
];
}
}Дальше
- Конструктор запросов — как выбирать эти сущности
- CRUD — вставка и обновление сущностей
- Миграции — генерация схемы из этих атрибутов