Annotations Reference

Annotations define how entities map to database tables, relationships, indexes, and lifecycle behavior.

Table and Column Mapping

The @Orm\Table and @Orm\Column annotations define basic entity structure:

<?php
namespace App\Entity;

use Quellabs\ObjectQuel\Annotations\Orm;

/**
 * @Orm\Table(name="products")
 */
class ProductEntity {

    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     * @Orm\PrimaryKeyStrategy(strategy="identity")
     */
    private ?int $productId = null;

    /**
     * @Orm\Column(name="name", type="string", limit=255)
     */
    private string $name;

    /**
     * @Orm\Column(name="price", type="decimal", limit="10,2")
     */
    private float $price;

    public function getProductId(): ?int { return $this->productId; }
    public function getName(): string { return $this->name; }
    public function setName(string $name): void { $this->name = $name; }
    public function getPrice(): float { return $this->price; }
    public function setPrice(float $price): void { $this->price = $price; }
}

@Orm\Table

Maps an entity class to a database table:

/**
 * @Orm\Table(name="products")
 */
class ProductEntity { }

@Orm\Column

Maps a property to a database column. The name and type parameters are required:

/**
 * @Orm\Column(name="product_name", type="string", limit=255)
 */
private string $name;

/**
 * @Orm\Column(name="price", type="decimal", limit="10,2", unsigned=true)
 */
private float $price;

/**
 * @Orm\Column(name="description", type="text", nullable=true)
 */
private ?string $description = null;
Parameter Required Description Example
name Yes Database column name name="product_id"
type Yes Data type: integer, string, decimal, datetime, boolean, text, json, enum type="string"
limit No Max length (strings) or precision (decimals: "10,2") limit=255
nullable No Allow NULL values (default: false) nullable=true
unsigned No Unsigned numeric types (default: false) unsigned=true
default No Default value when NULL default="Unknown"
primary_key No Mark as primary key (default: false) primary_key=true

JSON Columns

Columns with type="json" store structured data as a JSON string in the database. ObjectQuel automatically decodes the value to a PHP array on hydration and re-encodes it to a JSON string on persist — no manual serialization is needed.

class ProductEntity {

    /**
     * @Orm\Column(name="attributes", type="json", nullable=true)
     */
    private ?array $attributes = null;

    public function getAttributes(): ?array { return $this->attributes; }
    public function setAttributes(?array $attributes): void { $this->attributes = $attributes; }
}

The PHP property must be declared as array or ?array. ObjectQuel handles the JSON encode/decode boundary transparently; the application always works with plain PHP arrays.

Database engine mapping

The ORM type json is engine-independent. make:migrations translates it to the correct DDL type for the connected database:

Engine DDL type Notes
MySQL / MariaDB json Native JSON type with constraint checking
PostgreSQL jsonb Binary JSON; preferred because it supports GIN indexing
SQLite json Stored as text; SQLite validates nothing at the storage level
Schema comparison is engine-aware. When PostgreSQL returns jsonb from its schema catalog, ObjectQuel normalizes it to json before diffing — so a column you declared as type="json" never generates a spurious ALTER COLUMN on every make:migrations run.

@Orm\SourceField

Marks a property as receiving its value from an external source range (such as a JSON source) during hydration. When a query joins a database entity against a json_source() range, ObjectQuel writes the matching field value directly onto the entity property — no manual mapping is needed after the query.

class OrderEntity {

    /**
     * Populated from the JSON source field "name" when a matching record is found.
     * @Orm\SourceField(field="name")
     */
    private ?string $productName = null;

    /**
     * Explicit range — required when the query contains multiple JSON sources.
     * @Orm\SourceField(field="displayName", range="profile")
     */
    private ?string $displayName = null;
}
Parameter Required Description
field Yes The field name as it appears in the source data (e.g. "name", "inStock"). This is the key inside the JSON object, not the query alias.
range No The range alias to read from (e.g. "product"). Required when the query declares more than one external source range; inferred automatically when only one is present.
No-data behaviour: If the named range is not present in the current query, the property is left untouched and retains its default value. No error is raised. The same entity class can therefore be used in queries with and without the JSON source.
Ambiguity error: Omitting range when the query contains multiple JSON source ranges is caught at query compilation time and raises a semantic error naming the conflicting ranges.

@Orm\PrimaryKeyStrategy

Defines how primary key values are generated. When omitted, ObjectQuel defaults to identity.

Strategy Default Column type Description
identity Yes integer Relies on the database's auto-increment mechanism (MySQL AUTO_INCREMENT, PostgreSQL SERIAL). The ID is assigned by the database on insert and read back after flush().
sequence No integer Determines the next ID before insert by querying SELECT MAX(primary_key) + 1 on the table. Useful when you need the ID available before the record is written, but not safe under high concurrency without additional locking.
uuid No string (limit=36) Generates a UUID version 7 value in PHP before insert. UUID7 is time-ordered, making it index-friendly and suitable as a primary key in distributed systems.
Primary key properties must be nullable (?int, ?string) because new entities don't have IDs until after flush(). The exception is uuid, where the value is generated before the insert — but nullable is still recommended for consistency.

Relationship Annotations

@Orm\ManyToOne

Defines a many-to-one relationship where many entities reference one target entity:

class ProductEntity {
    /**
     * @Orm\ManyToOne(targetEntity="CategoryEntity", fetch="EAGER")
     * @Orm\RequiredRelation
     */
    private ?CategoryEntity $category = null;

    /**
     * @Orm\Column(name="category_id", type="integer")
     */
    private int $categoryId;
}
Parameter Required Description
targetEntity Yes Related entity class name
inversedBy No FK property on the target entity to join to. Defaults to the target's primary key
relationColumn No FK property on this entity holding the join column. Defaults to {property}Id (e.g. categoryId)
fetch No Loading strategy: EAGER (load immediately) or LAZY (load on access). Default: EAGER

@Orm\OneToMany

Defines a one-to-many relationship where one entity has many related entities:

use Quellabs\ObjectQuel\Collections\EntityCollection;

class CategoryEntity {
    /**
     * @Orm\OneToMany(targetEntity="ProductEntity", mappedBy="categoryId", orderBy="productId ASC")
     */
    public EntityCollection $products;

    public function __construct() {
        $this->products = new EntityCollection();
    }
}
Parameter Required Description
targetEntity Yes Related entity class name
mappedBy Yes FK property on the target entity that references this entity
relationColumn No FK property on the target entity holding the join column. Defaults to {property}Id (e.g. categoryId)
orderBy No Sort order for the collection (e.g. orderBy="name ASC, price DESC")
fetch No Loading strategy: EAGER (load immediately) or LAZY (load on access). Default: LAZY

@Orm\OneToOne

Defines a one-to-one relationship between two entities:

// Owning side (has the foreign key)
class UserEntity {
    /**
     * @Orm\OneToOne(targetEntity="ProfileEntity", inversedBy="user", relationColumn="profile_id", fetch="EAGER")
     */
    private ?ProfileEntity $profile = null;
}

// Inverse side (referenced by foreign key)
class ProfileEntity {
    /**
     * @Orm\OneToOne(targetEntity="UserEntity", mappedBy="profileId", relationColumn="profile_id")
     */
    private ?UserEntity $user = null;
}
Parameter Required Description
targetEntity Yes Related entity class name
inversedBy No FK property on the inverse-side entity
mappedBy No FK property on the owning-side entity
relationColumn No Database column name for the foreign key
fetch No Loading strategy: EAGER or LAZY. Default: EAGER

@Orm\EntityBridge

ObjectQuel handles many-to-many relationships using explicit entity bridges rather than hidden join tables. This gives you full control over the relationship, including the ability to add metadata:

/**
 * @Orm\Table(name="product_categories")
 * @Orm\EntityBridge
 */
class ProductCategoryEntity {
    /**
     * @Orm\ManyToOne(targetEntity="ProductEntity")
     */
    private ProductEntity $product;

    /**
     * @Orm\ManyToOne(targetEntity="CategoryEntity")
     */
    private CategoryEntity $category;

    /**
     * @Orm\Column(name="assigned_at", type="datetime")
     */
    private \DateTime $assignedAt;

    // You can add any additional metadata to the relationship
    /**
     * @Orm\Column(name="assigned_by", type="integer")
     */
    private int $assignedBy;
}

Entity bridges are regular entities - you can query them, add business logic, and include relationship-specific data.

@Orm\RequiredRelation

Marks a relationship as required, using INNER JOIN instead of LEFT JOIN for better performance:

class ProductEntity {
    /**
     * @Orm\ManyToOne(targetEntity="CategoryEntity", fetch="EAGER")
     * @Orm\RequiredRelation
     */
    private CategoryEntity $category;
}

Use this when the relationship must always exist (e.g., every product must have a category).

Index Annotations

Index annotations are placed at the class level and define database indexes to improve query performance:

/**
 * @Orm\Table(name="products")
 * @Orm\Index(name="idx_product_search", columns={"name", "description"})
 * @Orm\Index(name="idx_price", columns={"price"})
 * @Orm\UniqueIndex(name="idx_unique_sku", columns={"sku"})
 * @Orm\FullTextIndex(name="idx_contents", columns={"contents"})
 */
class ProductEntity { }

@Orm\Index

Creates a regular index on one or more columns to speed up frequently queried fields:

/**
 * @Orm\Table(name="products")
 * @Orm\Index(name="idx_price", columns={"price"})
 */
class ProductEntity { }
Parameter Required Description
name Yes Index name as it will appear in the database schema
columns Yes Array of column names to include in the index (e.g. {"name", "description"})

@Orm\UniqueIndex

Creates a unique index that prevents duplicate values across the indexed columns:

/**
 * @Orm\Table(name="products")
 * @Orm\UniqueIndex(name="idx_unique_sku", columns={"sku"})
 */
class ProductEntity { }
Parameter Required Description
name Yes Index name as it will appear in the database schema
columns Yes Array of column names that must be unique in combination (e.g. {"sku"})

@Orm\FullTextIndex

Creates a full-text search index for efficient text search across large content fields:

/**
 * @Orm\Table(name="articles")
 * @Orm\FullTextIndex(name="idx_contents", columns={"contents"})
 */
class ArticleEntity { }
Parameter Required Description
name Yes Index name as it will appear in the database schema
columns Yes Array of text column names to include in the full-text index (e.g. {"title", "body"})