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:

// Auto-increment (MySQL/PostgreSQL)
/**
 * @Orm\Column(name="id", type="integer", primary_key=true)
 * @Orm\PrimaryKeyStrategy(strategy="identity")
 */
private ?int $id = null;

// UUID generation
/**
 * @Orm\Column(name="uuid", type="string", limit=36, primary_key=true)
 * @Orm\PrimaryKeyStrategy(strategy="uuid")
 */
private ?string $uuid = null;

// Database sequence
/**
 * @Orm\Column(name="seq_id", type="integer", primary_key=true)
 * @Orm\PrimaryKeyStrategy(strategy="sequence")
 */
private ?int $sequenceId = null;

Primary key properties must be nullable (?int, ?string) because new entities don't have IDs until after flush().

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"})
 */
class ProductEntity { }

@Orm\Index - Creates regular indexes for frequently queried columns
@Orm\UniqueIndex - Creates unique constraints to prevent duplicate values

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