Annotations define how entities map to database tables, relationships, indexes, and lifecycle behavior.
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; }
}
Maps an entity class to a database table:
/**
* @Orm\Table(name="products")
*/
class ProductEntity { }
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 |
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().
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 |
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) |
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 |
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.
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
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).