Entity Management & Annotations

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

@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", inversedBy="products", 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 Property in target entity for reverse mapping
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", indexBy="productId")
     */
    public EntityCollection $products;

    public function __construct() {
        $this->products = new EntityCollection();
    }
}
Parameter Required Description
targetEntity Yes Related entity class name
mappedBy Yes Foreign key property in target entity
indexBy No Property to use as collection index (allows $category->products[123] access)

@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 Property in target entity (owning side)
mappedBy No Foreign key property (inverse side)
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.

Indexes

Define database indexes at the class level 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 { }
Annotation Description
@Orm\Index Creates a regular index for frequently queried columns
@Orm\UniqueIndex Creates a unique constraint to prevent duplicate values
@Orm\FullTextIndex Creates a full-text search index for efficient text search across large content fields

@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).