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.
Example: A User has one related Profile entity
#[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., atarget_type
discriminator)