Relationships

This guide presents all relationship types in Hector ORM using named parameters, including filtering options, polymorphic handling, and advanced querying via builders. It also provides contextual explanations to help you understand when and how to use each feature.

Overview of attributes

Hector ORM provides four attributes to declare entity relationships. These attributes are declared as PHP attributes and support named parameters.

Attribute Purpose
HasOne Defines a one-to-one or many-to-one link
HasMany Defines a one-to-many relationship
BelongsToMany Defines a many-to-many link via pivot
BelongsTo Inverse of HasOne, or polymorphic link

Parameters

Parameter Type Required Description
target string (FQCN) Yes The target entity class
name string Yes The public name used to access the relation
columns array No Associative mapping of local ↔ foreign keys
pivotTable string No Name of the pivot table (for BelongsToMany only)
columnsFrom array No Mapping of source → pivot table columns (for BelongsToMany)
columnsTo array No Mapping of pivot → target table columns (for BelongsToMany)

Additional parameters

These optional named parameters can be used to filter or shape the relationship:

Parameter Type Description
where array Static WHERE conditions on the related entity
orderBy string or array ORDER BY clause
groupBy string or array GROUP BY clause
having array HAVING conditions for grouped queries
limit int Maximum number of results returned

One-to-One / Many-to-One

A HasOne relationship indicates that the current entity is linked to one instance of another entity. When used with BelongsTo, it defines the inverse side of the relation.

#[HasOne(
    target: Profile::class,
    name: 'profile'
)]
class User extends MagicEntity {}

#[BelongsTo(
    target: User::class,
    name: 'user'
)]
class Profile extends MagicEntity {}

With filters

Use filtering parameters directly to restrict results statically (e.g. only active profiles).

#[HasOne(
    target: Profile::class,
    name: 'profile',
    where: ['active' => '1']
)]

One-to-Many

A HasMany relationship allows a single entity to reference multiple target entities. This is typically used for collections.

Example: A User is linked to multiple Posts

#[HasMany(
    target: Post::class,
    name: 'posts'
)]
class User extends MagicEntity {}

With sorting and limit

#[HasMany(
    target: Post::class,
    name: 'posts',
    orderBy: ['created_at' => 'DESC'],
    limit: 5
)]

Many-to-Many

A BelongsToMany relationship is used when an entity is related to many others, and vice versa, through a pivot table.

Example: A User can be assigned multiple Roles

#[BelongsToMany(
    target: Role::class,
    name: 'roles'
)]
class User extends MagicEntity {}

Customizing the pivot

#[BelongsToMany(
    target: Role::class,
    name: 'roles',
    pivotTable: 'user_role',
    columnsFrom: ['user_id' => 'id'],
    columnsTo: ['role_id' => 'id'],
    where: ['enabled' => '1'],
    orderBy: ['name' => 'ASC']
)]

Polymorphic Relationships

Polymorphic relations allow one entity to reference several types of targets through a shared interface. These are defined by using multiple #[BelongsTo(...)] attributes with the same name, but filtered by custom logic based on your entity data.

There is no explicit type parameter supported. Instead, Hector ORM resolves which relation to load by comparing declared relation keys (columns) and applying them based on your implementation (e.g. via discriminator column).

Example: A Comment is linked to either an Article or a Video entity

#[BelongsToMany(
    target: Article::class,
    name: 'articles',
    pivotTable: 'comment_link',
    columnsFrom: ['id' => 'comment_id'],
    columnsTo: ['entity_id' => 'article_id'],
    where: ['entity_type' => 'article']
)]
#[BelongsToMany(
    target: Video::class,
    name: 'videos',
    pivotTable: 'comment_link',
    columnsFrom: ['id' => 'comment_id'],
    columnsTo: ['entity_id' => 'video_id'],
    where: ['entity_type' => 'video']
)]
class Comment extends MagicEntity {}

Accessing Relations

After declaration, relationships are directly accessible as properties (if using MagicEntity).

$user->profile; // Profile entity
$comment->target; // Either Article or Video instance

For collections:

foreach ($user->posts as $post) {
    echo $post->title;
}

If you’re not using MagicEntity, you can expose relationships through explicit methods:

class User extends Entity {
    public function getProfile(): ?Profile
    {
        return $this->getRelated()->get('profile');
    }
    
    public function getPosts(): Collection
    {
        return $this->getRelated()->get('posts');
    }
}

Eager Loading

Use with() to preload relationships and avoid the N+1 query problem.

$users = User::builder()->with(['profile', ['posts' => ['user']]])->all();
$comments = Comment::builder()->with(['target'])->all();

Persisting Relations

Related objects are not automatically persisted. Save them explicitly before assigning.

$role = Role::create(['name' => 'Editor']);
// manually assign role to relation and persist pivot manually if needed
$user->save();

Without Foreign Keys

If your database does not enforce foreign keys, always declare column mappings manually.

#[HasOne(
    target: Company::class,
    name: 'company',
    columns: ['company_id' => 'id']
)]
class Employee extends MagicEntity {}

Using Relationship Builders

Relationship builders allow dynamic filtering or querying of related records.

$builder = $user->getRelated()->getBuilder('posts');
$posts = $builder->where('status', 'published')->limit(5)->all();

Utilities & Debugging

The getRelated() object gives access to utilities for managing relationships manually.

$user->getRelated()->get('profile');
$user->getRelated()->set('profile', $profile);
$user->getRelated()->isset('profile');
$user->getRelated()->exists('profile');

Inspect loaded relations:

print_r($user->getRelated()->all());

Best Practices

  • Use named parameters for clarity and readability
  • Define columns explicitly in absence of foreign keys
  • Declare filtering options (where, orderBy, groupBy, having, limit) as named parameters directly in the relationship attribute
  • Use with() to improve performance via eager loading
  • Prefer getBuilder() for dynamic queries
  • For polymorphic relationships, rely on multiple relation declarations with shared name, and control resolution using application logic (e.g., a target_type discriminator)

Last updated: Wed, 17 Sep 2025 12:38