Pular para o conteúdo principal

Mapeamento de Entidades

O Weaver ORM separa os objetos de domínio dos metadados de persistência colocando todas as informações de mapeamento em uma classe mapper dedicada. Esta página cobre todos os aspectos da configuração do mapper.

Por que mappers em vez de atributos?

O Doctrine ORM coloca os metadados de mapeamento diretamente na classe de entidade via atributos do PHP 8:

// Abordagem do Doctrine — a entidade conhece o banco de dados
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
}

O Weaver mantém uma separação estrita:

Classe de entidade  →  objeto PHP simples, sem dependências do ORM
Classe mapper → todo o conhecimento de persistência vive aqui

Benefícios:

  • Zero reflexão em tempo de execução. O mapper é PHP puro retornando arrays e escalares.
  • Sem classes proxy. Nenhuma geração de código em disco é necessária.
  • Seguro para workers. Os mappers não mantêm estado por requisição.
  • Testável em isolamento. Instancie e inspecione um mapper em um teste unitário sem inicializar o Symfony.
  • Totalmente pesquisável. Cada nome de coluna, cada tipo, cada opção aparece em texto simples e é visível no git diff.

Mapper vs entidade: responsabilidades

PreocupaçãoReside em
Lógica de negócio, invariantesClasse de entidade
Propriedades e tipos PHPClasse de entidade
Nome da tabela e schemaMapper
Nomes de colunas, tipos, opçõesMapper
Índices e restriçõesMapper
Hidratação (linha → entidade)Mapper
Extração (entidade → linha)Mapper
RelaçõesMapper

Definição básica de entidade

Uma entidade é qualquer classe PHP. Ela não estende nada, não implementa nada e não importa nada do Weaver\ORM.

<?php
// src/Entity/User.php

declare(strict_types=1);

namespace App\Entity;

use DateTimeImmutable;

final class User
{
public function __construct(
public readonly ?int $id,
public readonly string $name,
public readonly string $email,
public readonly bool $isActive,
public readonly DateTimeImmutable $createdAt,
) {}

public function withEmail(string $email): self
{
return new self(
id: $this->id,
name: $this->name,
email: $email,
isActive: $this->isActive,
createdAt: $this->createdAt,
);
}
}

A entidade pode ser:

  • Imutável (recomendado) — métodos de mutação retornam novas instâncias
  • Mutável — propriedades públicas ou setters são aceitos
  • Abstrata — para hierarquias de herança

AbstractMapper

Cada entidade precisa de exatamente um mapper. Crie uma classe estendendo Weaver\ORM\Mapping\AbstractMapper e implemente os métodos obrigatórios.

<?php
// src/Mapper/UserMapper.php

declare(strict_types=1);

namespace App\Mapper;

use App\Entity\User;
use DateTimeImmutable;
use Weaver\ORM\Mapping\AbstractMapper;
use Weaver\ORM\Mapping\ColumnDefinition;
use Weaver\ORM\Mapping\SchemaDefinition;

final class UserMapper extends AbstractMapper
{
public function table(): string
{
return 'users';
}

public function primaryKey(): string|array
{
return 'id';
}

public function schema(): SchemaDefinition
{
return SchemaDefinition::define(
ColumnDefinition::integer('id')->autoIncrement()->unsigned(),
ColumnDefinition::string('name', 120)->notNull(),
ColumnDefinition::string('email', 254)->unique()->notNull(),
ColumnDefinition::boolean('is_active')->notNull()->default(true),
ColumnDefinition::datetime('created_at')->notNull(),
);
}

public function hydrate(array $row): User
{
return new User(
id: (int) $row['id'],
name: $row['name'],
email: $row['email'],
isActive: (bool) $row['is_active'],
createdAt: new DateTimeImmutable($row['created_at']),
);
}

public function dehydrate(object $entity): array
{
/** @var User $entity */
$data = [
'name' => $entity->name,
'email' => $entity->email,
'is_active' => $entity->isActive,
'created_at' => $entity->createdAt->format('Y-m-d H:i:s'),
];

if ($entity->id !== null) {
$data['id'] = $entity->id;
}

return $data;
}
}

Métodos obrigatórios do mapper

MétodoFinalidade
table(): stringNome da tabela no banco de dados
primaryKey(): string|arrayNome(s) da(s) coluna(s) para a chave primária
schema(): SchemaDefinitionTodas as definições de colunas para DDL e migrações
hydrate(array $row): objectConstrói uma entidade a partir de uma linha bruta do banco de dados
dehydrate(object $entity): arraySerializa uma entidade para um array coluna => valor

Métodos opcionais do mapper

MétodoFinalidade
readOnly(): boolRetorne true para entidades suportadas por views (sem INSERT/UPDATE/DELETE)
discriminatorColumn(): ?stringUsado para Herança de Tabela Única (Single Table Inheritance)
discriminatorMap(): arrayUsado para Herança de Tabela Única
parentMapper(): ?stringUsado para Herança de Tabela por Classe (Class Table Inheritance)

Tipos de coluna

Todas as definições de colunas usam métodos de fábrica estáticos no ColumnDefinition. Cada método retorna uma instância de ColumnDefinition com uma API de configuração fluente.

string

Mapeia para VARCHAR(n). O comprimento padrão é 255.

ColumnDefinition::string('username')                    // VARCHAR(255) NOT NULL
ColumnDefinition::string('slug', 100) // VARCHAR(100) NOT NULL
ColumnDefinition::string('nickname')->nullable() // VARCHAR(255) NULL

integer, bigint, smallint

ColumnDefinition::integer('sort_order')                 // INT NOT NULL
ColumnDefinition::integer('quantity')->default(0) // INT NOT NULL DEFAULT 0
ColumnDefinition::integer('stock')->unsigned() // INT UNSIGNED NOT NULL
ColumnDefinition::bigint('view_count')->default(0) // BIGINT NOT NULL DEFAULT 0
ColumnDefinition::smallint('priority')->unsigned() // SMALLINT UNSIGNED NOT NULL

float e decimal

Use decimal para valores financeiros; float para coordenadas e medidas.

ColumnDefinition::float('latitude')
ColumnDefinition::float('longitude')
ColumnDefinition::decimal('price', 10, 2) // DECIMAL(10,2) NOT NULL
ColumnDefinition::decimal('tax_rate', 5, 4)->default('0.0000')

Hidrate decimal como string para preservar a precisão:

price: $row['price'],  // manter como string, passar para um objeto de valor Money

boolean

Mapeia para TINYINT(1) no MySQL, BOOLEAN no PostgreSQL/SQLite.

ColumnDefinition::boolean('is_active')->default(true)
ColumnDefinition::boolean('email_verified')->default(false)

Sempre faça cast explicitamente no hydrate:

isActive: (bool) $row['is_active'],

datetime, date, time

ColumnDefinition::datetime('published_at')->nullable()   // DATETIME NULL
ColumnDefinition::date('birth_date')->nullable() // DATE NULL
ColumnDefinition::time('opens_at') // TIME NOT NULL

datetime retorna um \DateTime mutável. Prefira datetimeImmutable para código novo:

ColumnDefinition::datetimeImmutable('created_at')        // DATETIME NOT NULL
ColumnDefinition::datetimeImmutable('updated_at')->nullable()

Hidratação:

createdAt: new \DateTimeImmutable($row['created_at']),
updatedAt: isset($row['updated_at']) ? new \DateTimeImmutable($row['updated_at']) : null,

Extração:

'created_at' => $entity->createdAt->format('Y-m-d H:i:s'),
'updated_at' => $entity->updatedAt?->format('Y-m-d H:i:s'),

json

Mapeia para JSON (MySQL 5.7.8+, PostgreSQL, SQLite). Você controla a codificação/decodificação no hydrate / dehydrate.

ColumnDefinition::json('metadata')->nullable()
ColumnDefinition::json('settings')

Hidratação:

metadata: $row['metadata'] !== null
? json_decode($row['metadata'], true, 512, JSON_THROW_ON_ERROR)
: null,

Extração:

'metadata' => $entity->metadata !== null
? json_encode($entity->metadata, JSON_THROW_ON_ERROR)
: null,

text, blob

ColumnDefinition::text('body')                           // TEXT NOT NULL
ColumnDefinition::text('description')->nullable() // TEXT NULL
ColumnDefinition::blob('thumbnail') // BLOB NOT NULL

guid (UUID como CHAR(36))

ColumnDefinition::guid('external_ref')->nullable()       // CHAR(36) NULL

Tipos de chave primária

Inteiro com auto-incremento

ColumnDefinition::integer('id')->autoIncrement()->unsigned()
id  INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)

O Weaver omite id do INSERT quando o valor é null e lê o valor gerado automaticamente.

UUID v4 (aleatório)

ColumnDefinition::guid('id')->primaryKey()

Gere o UUID no método de fábrica da entidade antes de persistir:

use Symfony\Component\Uid\Uuid;

public static function create(string $name): self
{
return new self(id: (string) Uuid::v4(), name: $name);
}

UUID v7 (ordenado por tempo, recomendado)

O UUID v7 inclui um prefixo de timestamp em milissegundos, tornando as chaves monotonicamente crescentes e reduzindo drasticamente as divisões de página B-tree em comparação com UUIDs aleatórios.

ColumnDefinition::guid('id')->primaryKey()
use Symfony\Component\Uid\Uuid;

public static function create(string $name): self
{
return new self(id: (string) Uuid::v7(), name: $name);
}

Chave de string natural

Quando a chave de negócio é naturalmente única (código de país, código de moeda, slug):

ColumnDefinition::string('code', 3)->primaryKey()

Chave primária composta

ColumnDefinition::integer('user_id')->primaryKey(),
ColumnDefinition::integer('role_id')->primaryKey(),
ColumnDefinition::datetimeImmutable('assigned_at'),
PRIMARY KEY (user_id, role_id)

Opções de coluna

Todas as opções estão disponíveis como métodos fluentes em ColumnDefinition:

MétodoEfeito
->nullable()Coluna aceita valores NULL
->default($value)Define uma cláusula DEFAULT no DDL
->unsigned()Aplica UNSIGNED (apenas tipos inteiro)
->unique()Adiciona uma restrição UNIQUE
->primaryKey()Marca a coluna como parte da chave primária
->autoIncrement()Adiciona AUTO_INCREMENT (apenas PKs inteiras)
->generated()Coluna é computada pelo banco; excluída de INSERT/UPDATE
->comment(string)Adiciona um comentário DDL no nível da coluna

Mapeamento de enums do PHP 8.1

Enums PHP com backing type (string ou int) mapeiam naturalmente para colunas do banco de dados.

Enum com backing string

enum OrderStatus: string
{
case Pending = 'pending';
case Confirmed = 'confirmed';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}

Mapper:

ColumnDefinition::string('status', 20)
->comment('pending|confirmed|shipped|delivered|cancelled')

Hidratação:

status: OrderStatus::from($row['status']),

Extração:

'status' => $entity->status->value,

Enum com backing int

enum Priority: int
{
case Low = 1;
case Normal = 2;
case High = 3;
case Urgent = 4;
}

Mapper:

ColumnDefinition::smallint('priority')->unsigned()

Hidratação:

priority: Priority::from((int) $row['priority']),

Enum nullable

ColumnDefinition::string('resolution', 20)->nullable()

Hidratação:

resolution: $row['resolution'] !== null
? Resolution::from($row['resolution'])
: null,
dica

Sempre armazene ->value (por exemplo, 'pending'), nunca ->name (por exemplo, 'Pending'). Os rótulos podem ser renomeados livremente em PHP; os valores não podem ser alterados sem uma migração.

Colunas geradas / computadas

Colunas preenchidas pelo motor do banco de dados (por exemplo, GENERATED ALWAYS AS) devem ser excluídas das declarações INSERT e UPDATE.

ColumnDefinition::string('full_name', 162)->generated(),
ColumnDefinition::decimal('total', 10, 2)->generated(),

O Weaver remove automaticamente as colunas generated das cargas de escrita. Elas ainda aparecem no hydrate.

Aliases de coluna

Use um alias quando o nome da propriedade PHP for diferente do nome da coluna no banco de dados:

// Propriedade PHP 'email' mapeia para a coluna DB 'usr_email'
ColumnDefinition::string('email')->alias('usr_email')

No hydrate, use o nome da coluna (o alias) como chave do array:

email: $row['usr_email'],

No dehydrate, retorne o nome da coluna como chave:

'usr_email' => $entity->email,

Registrando mappers no Symfony

Se autoconfigure: true estiver definido em config/services.yaml (padrão do Symfony), qualquer classe que estenda AbstractMapper nos mapper_paths configurados é automaticamente marcada e registrada — sem necessidade de definição manual de serviço.

Para registro explícito ou para substituir padrões:

# config/services.yaml
services:
App\Mapper\UserMapper:
tags:
- { name: weaver.mapper }