π 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 |
1οΈβ£ 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. These are defined by using multiple relationship attributes with different names, filtered by a discriminator column.
Example
A Comment can be linked to either Articles or Videos:
#[HasMany(
target: Comment::class,
name: 'comments',
columns: ['id' => 'commentable_id'],
where: ['commentable_type' => 'article']
)]
class Article extends MagicEntity {}
#[HasMany(
target: Comment::class,
name: 'comments',
columns: ['id' => 'commentable_id'],
where: ['commentable_type' => 'video']
)]
class Video extends MagicEntity {}
#[BelongsTo(
target: Article::class,
name: 'article',
columns: ['commentable_id' => 'id'],
where: ['commentable_type' => 'article']
)]
#[BelongsTo(
target: Video::class,
name: 'video',
columns: ['commentable_id' => 'id'],
where: ['commentable_type' => 'video']
)]
class Comment extends MagicEntity {
public int $commentable_id;
public string $commentable_type; // 'article' or 'video'
}
Accessing the related entity:
$comment = Comment::find(1);
// Access based on type
if ($comment->commentable_type === 'article') {
$target = $comment->article;
} else {
$target = $comment->video;
}
π 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::query()->with(['profile', 'posts'])->all();
// Nested eager loading
$users = User::query()->with(['posts' => ['comments', 'author']])->all();
πΎ Persisting Relations
Assigning Relations
With MagicEntity, you can assign relations directly as properties:
$user = User::find(1);
// One-to-One / Many-to-One
$profile = new Profile();
$profile->bio = 'Hello world';
$user->profile = $profile;
// One-to-Many / Many-to-Many (assign collection)
$user->roles = new Collection([$role1, $role2]);
With classic Entity, use getRelated()->set():
$user->getRelated()->set('profile', $profile);
Saving New Relations
When assigning new (not yet persisted) entities, the ORM automatically detects they need to be saved:
$user = User::find(1);
$profile = new Profile();
$profile->bio = 'New profile';
$user->profile = $profile;
$user->save(); // Profile is automatically saved (new entity detected)
Saving Modified Relations
For existing (already persisted) relations that have been modified, use save(cascade: true):
$user = User::find(1);
$user->profile->bio = 'Updated bio'; // Modify existing relation
$user->save(cascade: true); // Required to persist changes on existing relations
Tip: save(cascade: true) is only necessary when modifying already-persisted related entities. For new relations, a simple save() is sufficient.
Working with Collections
$user = User::find(1);
// Append to existing collection
$user->posts[] = $newPost;
$user->save(); // New post is saved automatically
// Modify existing post in collection
$user->posts[0]->title = 'Updated title';
$user->save(cascade: true); // Required for existing entity
Removing Relations
// Clear a One-to-One / Many-to-One relation
$user->profile = null;
$user->save();
// Remove from collection
$roles = $user->roles;
unset($roles[0]);
$user->save();
// Clear relation cache to force reload from DB
$user->getRelated()->unset('roles');
π« 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');
$recentPosts = $builder
->where('status', 'published')
->where('created_at', '>=', '2025-01-01')
->orderBy('created_at', 'DESC')
->limit(5)
->all();
Tip: The builder is independent of the cached relation. It always queries the database.
π οΈ Utilities & Debugging
The getRelated() object gives access to utilities for managing relationships.
API Reference
| Method | Description |
|---|---|
get(string $name) |
Get related entity or collection (lazy-loads) |
set(string $name, $val) |
Assign a related entity or collection |
isset(string $name) |
Check if relation is already loaded |
unset(string $name) |
Clear cached relation (will reload on next access) |
exists(string $name) |
Check if relation is declared on entity |
getBuilder(string $name) |
Get a query builder for the relation |
save(bool $cascade) |
Save all loaded relations |
Examples
// Get relation (lazy-loads if not cached)
$profile = $user->getRelated()->get('profile');
// Assign relation
$user->getRelated()->set('profile', $newProfile);
// Check if already loaded (no DB query)
if ($user->getRelated()->isset('posts')) {
// ...
}
// Check if relation exists on entity
if ($user->getRelated()->exists('comments')) {
// ...
}
// Clear cache to force reload
$user->getRelated()->unset('posts');
$freshPosts = $user->posts; // Reloads from DB
β Best Practices
- Use named parameters for clarity and readability
- Define
columnsexplicitly 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_typediscriminator)