Skip to main content

Overview

Aeros supports model relationships through pivot tables, allowing you to define connections between different models. All relationships use a pivot table approach with automatic table name detection.

Relationship Types

Aeros provides five relationship types:

HasOne

One-to-one relationship through pivot table

HasMany

One-to-many relationship through pivot table

BelongsTo

Inverse relationship through pivot table

BelongsToMany

Many-to-many relationship with pivot table

HasManyThrough

Relationship through intermediate model

HasOne

Define a one-to-one relationship where a model has one related record.

Defining HasOne

namespace App\Models;

use Aeros\Src\Classes\Model;

class User extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }
}

Using HasOne

$user = User::find(1);

// Get related profile
$profile = $user->profile()->first();

// With constraints
$profile = $user->profile()
    ->where('status', 'active')
    ->first();

// Eager loading
$user = User::with('profile')->find(1);
echo $user->profile->bio;

Auto-Detection

The relationship automatically:
  • Detects pivot table name (e.g., profile_user)
  • Detects foreign keys (e.g., user_id, profile_id)
  • Uses alphabetical ordering for table name

HasMany

Define a one-to-many relationship where a model has multiple related records.

Defining HasMany

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

Using HasMany

$user = User::find(1);

// Get all posts
$posts = $user->posts()->get();

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

// With constraints
$publishedPosts = $user->posts()
    ->where('status', 'published')
    ->orderBy('created_at', 'DESC')
    ->get();

// Limit results
$recentPosts = $user->posts()
    ->limit(5)
    ->get();

Method Chaining

// Complex query on relationship
$posts = $user->posts()
    ->where('status', 'published')
    ->where('views', '>', 1000)
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->get();

// Count related records
$count = $user->posts()->count();

// Check existence
$hasPosts = $user->posts()->exists();

BelongsTo

Define an inverse relationship where a model belongs to a parent model.

Defining BelongsTo

class Post extends Model
{
    public function author()
    {
        return $this->belongsTo(User::class);
    }
}

Using BelongsTo

$post = Post::find(1);

// Get the author
$author = $post->author()->first();

echo $author->name;

// Eager loading
$post = Post::with('author')->find(1);
echo $post->author->name;

BelongsToMany

Define a many-to-many relationship where models can be related to multiple records.

Defining BelongsToMany

class User extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

class Role extends Model
{
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

Pivot Table Convention

Pivot tables follow alphabetical naming:
// User + Role → role_user (alphabetical order)
// Post + Tag → post_tag
// Category + Product → category_product

Using BelongsToMany

$user = User::find(1);

// Get all roles
$roles = $user->roles()->get();

foreach ($roles as $role) {
    echo $role->name;
}

// With constraints
$activeRoles = $user->roles()
    ->where('active', true)
    ->get();

Attaching & Detaching

$user = User::find(1);

// Attach roles (create relationships)
$user->roles()->attach([1, 2, 3]);

// Attach with pivot data
$user->roles()->attach([1], ['expires_at' => '2024-12-31']);

// Detach specific roles
$user->roles()->detach([2]);

// Detach all roles
$user->roles()->detach();

// Sync roles (replace all)
$user->roles()->sync([1, 3, 4]);

// Toggle roles
$result = $user->roles()->toggle([1, 2]);
// Returns: ['attached' => [2], 'detached' => [1]]

Pivot Data

Access additional pivot table columns:
$roles = $user->roles()
    ->withPivot(['created_at', 'expires_at'])
    ->get();

foreach ($roles as $role) {
    echo $role->name;
    echo $role->pivot->created_at;
    echo $role->pivot->expires_at;
}

HasManyThrough

Define a relationship through an intermediate model.

Example Schema

countries
  - id
  - name

users
  - id
  - country_id
  - name

posts
  - id
  - user_id
  - title

// Country → Users → Posts

Defining HasManyThrough

class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            Post::class,    // Final model
            User::class     // Intermediate model
        );
    }
}

Using HasManyThrough

$country = Country::find(1);

// Get all posts from this country
$posts = $country->posts()->get();

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

// With constraints
$publishedPosts = $country->posts()
    ->where('status', 'published')
    ->get();

Custom Keys

public function posts()
{
    return $this->hasManyThrough(
        Post::class,
        User::class,
        'country_id',      // Foreign key on users table
        'user_id',         // Foreign key on posts table
        'id',              // Local key on countries table
        'id'               // Local key on users table
    );
}

Relationship Methods

All relationships support these chainable methods:
where()
self
Add WHERE constraint
$user->posts()->where('status', 'published')
orWhere()
self
Add OR WHERE constraint
$user->posts()->orWhere('featured', true)
orderBy()
self
Add ORDER BY clause
$user->posts()->orderBy('created_at', 'DESC')
limit()
self
Limit results
$user->posts()->limit(10)
offset()
self
Skip results
$user->posts()->offset(20)
take()
self
Alias for limit()
$user->posts()->take(5)
get()
array|Model|null
Execute query and get results
$posts = $user->posts()->get()
first()
Model|null
Get first result
$post = $user->posts()->first()
count()
int
Count related records
$count = $user->posts()->count()
exists()
bool
Check if any related records exist
$hasPosts = $user->posts()->exists()

Eager Loading

Prevent N+1 query problems by eager loading relationships.

Basic Eager Loading

// ❌ N+1 Problem (1 + N queries)
$users = User::query()->get();
foreach ($users as $user) {
    echo $user->profile->bio;  // Separate query for each user
}

// ✅ Eager Loading (2 queries)
$users = User::with('profile')->query()->get();
foreach ($users as $user) {
    echo $user->profile->bio;  // No additional query
}

Multiple Relationships

$users = User::with(['profile', 'posts', 'roles'])
    ->query()
    ->get();

foreach ($users as $user) {
    echo $user->profile->bio;
    echo count($user->posts);
    echo $user->roles[0]->name;
}

Lazy Loading

Load relationships after fetching the model:
$user = User::find(1);

// Load if not already loaded
$user->load('posts');

// Reload even if loaded
$user->reload('posts');

// Load multiple
$user->loadMany(['posts', 'roles']);

Custom Pivot Tables

Override auto-detected pivot table names:
public function roles()
{
    return $this->belongsToMany(
        Role::class,
        'user_role_assignments',  // Custom pivot table
        'user_id',                // Foreign key
        'role_id'                 // Related key
    );
}

Relationship Examples

Blog System

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
    
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

class Post extends Model
{
    public function author()
    {
        return $this->belongsTo(User::class);
    }
    
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
    
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

E-commerce

class Customer extends Model
{
    public function orders()
    {
        return $this->hasMany(Order::class);
    }
    
    public function address()
    {
        return $this->hasOne(Address::class);
    }
}

class Product extends Model
{
    public function categories()
    {
        return $this->belongsToMany(Category::class);
    }
    
    public function orders()
    {
        return $this->belongsToMany(Order::class)
            ->withPivot(['quantity', 'price']);
    }
}

Best Practices

Use eager loading to prevent N+1 queries
Name relationships descriptively (e.g., author() instead of user())
Add constraints to relationships when needed
Use pivot data for many-to-many relationships with additional info
Create pivot tables with proper indexes on foreign key columns
Clear cached relationships after attach/detach operations

Database Schema

Pivot tables should follow this structure:
CREATE TABLE role_user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    role_id INTEGER NOT NULL,
    user_id INTEGER NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(role_id, user_id)
);

CREATE INDEX idx_role_user_role_id ON role_user(role_id);
CREATE INDEX idx_role_user_user_id ON role_user(user_id);

Next Steps

Models

Learn more about model features

Query Builder

Build complex queries on relationships

Build docs developers (and LLMs) love