База данных · PPA

Сущности и маппинг

Сущность — это типизированный объект строки таблицы. Колонки описываются атрибутами на свойствах: тип, ключ, индекс, внешний ключ, ограничение. Из этих же атрибутов PPA строит SELECT и — при миграции — DDL таблицы.

Маркер #[Table]Атрибуты Ppa\Mapping\AttributesРоль гидрация + схема

Что такое сущность и зачем

Сущность (entity) — PHP-класс, одно свойство которого соответствует одной колонке таблицы.

Проблема. Если результат запроса — безликий массив ($row['created_at']), нет ни типов, ни автодополнения, ни единого места, где видно устройство таблицы. Колонки, их типы и связи размазаны по SQL-строкам.

Решение. Опишите строку типизированным классом, а колонки — атрибутами. PPA гидрирует результат в такой объект (типы, автодополнение), а из атрибутов умеет сгенерировать схему таблицы. Один класс — источник правды о структуре. Об этом и раздел.

Базовый вид

Сгенерируйте сущность командой make (флаг -e):

bash
php call make -e .User   # → main/User.php

Класс помечается #[Table], свойства — атрибутами колонок:

main/User.php
<?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)
php
#[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: nullDEFAULT NULL, falseDEFAULT FALSE, 'pending'DEFAULT 'pending', число → как есть.

php
#[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.

php
#[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.

php
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;

Полный пример

main/Entities/FileRecord.php
<?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 свойство']. Отсутствующие в карте свойства выбираются по имени:

php
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",
      ];
  }
}

Дальше