Entity Bridge

Entity Bridges are ObjectQuel's approach to many-to-many relationships. Instead of hiding join tables, ObjectQuel makes them explicit, first-class entities that you can query, extend, and control.

What are Entity Bridges?

An Entity Bridge is a regular entity that connects two other entities in a many-to-many relationship. While some ORMs hide join tables, ObjectQuel makes them visible and manageable:

/**
 * @Orm\Table(name="product_tags")
 * @Orm\EntityBridge
 */
class ProductTagEntity {
    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     */
    private int $productId;

    /**
     * @Orm\Column(name="tag_id", type="integer", primary_key=true)
     */
    private int $tagId;

    /**
     * @Orm\ManyToOne(targetEntity="ProductEntity")
     */
    private ProductEntity $product;

    /**
     * @Orm\ManyToOne(targetEntity="TagEntity")
     */
    private TagEntity $tag;
}

The @Orm\EntityBridge annotation marks the entity as a bridge, helping ObjectQuel optimize queries.

Why Entity Bridges?

Making bridge entities explicit provides several advantages:

  • Store metadata - Add fields like timestamps, user IDs, or status flags directly on the relationship
  • Add business logic - Include methods and validation on the relationship itself
  • Query the relationship - Write queries against the bridge entity to find specific connections
  • Full control - Use all ObjectQuel features (lifecycle events, validation, etc.) on relationships
  • No magic - The bridge table is visible in your codebase, making the schema clear

Adding Metadata to Relationships

Bridge entities can store relationship-specific data:

/**
 * @Orm\Table(name="product_tags")
 * @Orm\EntityBridge
 */
class ProductTagEntity {
    /**
     * @Orm\Column(name="product_id", type="integer", primary_key=true)
     */
    private int $productId;

    /**
     * @Orm\Column(name="tag_id", type="integer", primary_key=true)
     */
    private int $tagId;

    /**
     * @Orm\ManyToOne(targetEntity="ProductEntity")
     */
    private ProductEntity $product;

    /**
     * @Orm\ManyToOne(targetEntity="TagEntity")
     */
    private TagEntity $tag;

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

    /**
     * @Orm\Column(name="assigned_by", type="integer")
     */
    private int $assignedBy;

    /**
     * @Orm\Column(name="priority", type="integer")
     */
    private int $priority = 0;

    // Business logic on the relationship
    public function isRecentlyAssigned(): bool {
        return $this->assignedAt > new \DateTime('-7 days');
    }

    public function wasAssignedBy(int $userId): bool {
        return $this->assignedBy === $userId;
    }
}

Working with Bridge Entities

Bridge entities are used like any other entity:

// Creating a relationship
$bridge = new ProductTagEntity();
$bridge->setProduct($product);
$bridge->setTag($tag);
$bridge->setAssignedAt(new \DateTime());
$bridge->setAssignedBy($userId);
$bridge->setPriority(5);

$entityManager->persist($bridge);
$entityManager->flush();

// Removing a relationship
$entityManager->remove($bridge);
$entityManager->flush();

// Finding relationships
$assignments = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    range of p is App\\Entity\\ProductEntity via pt.product
    range of t is App\\Entity\\TagEntity via pt.tag
    retrieve (pt, p.name, t.name)
    where pt.assignedBy = :userId
    sort by pt.assignedAt desc
", ['userId' => 123]);

Querying Bridge Entities

You can query bridge entities directly to analyze relationships:

// Find all products tagged with a specific tag
$results = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    range of p is App\\Entity\\ProductEntity via pt.product
    retrieve (p)
    where pt.tagId = :tagId
", ['tagId' => 5]);

// Find recent tag assignments
$results = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    retrieve (pt)
    where pt.assignedAt > :date
    sort by pt.assignedAt desc
", ['date' => '2024-01-01']);

// Find all tags for a product
$results = $entityManager->executeQuery("
    range of pt is App\\Entity\\ProductTagEntity
    range of t is App\\Entity\\TagEntity via pt.tag
    retrieve (t)
    where pt.productId = :productId
", ['productId' => 123]);

Bridge Entity Best Practices

  • Use composite primary keys (both foreign keys) for the bridge table
  • Initialize collections in both related entities if you want bidirectional navigation
  • Add indexes on foreign key columns for better query performance
  • Consider adding a unique constraint if the relationship should only exist once
  • Use lifecycle events (@PrePersist, @PostDelete) for relationship-specific logic