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