Skip to main content

Overview

Rest Generic Class provides powerful relation loading capabilities that allow clients to:
  • Eager-load relationships with the relations parameter
  • Select specific fields from relations
  • Filter and sort related data
  • Nest relations multiple levels deep
  • Load pivot data for many-to-many relationships

Basic Relation Loading

Declaring Relations

First, declare allowed relations in your model:
use Ronu\RestGenericClass\Core\Models\BaseModel;

class Product extends BaseModel
{
    const RELATIONS = ['category', 'reviews', 'tags', 'reviews.user'];
    
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
    
    public function reviews()
    {
        return $this->hasMany(Review::class);
    }
    
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}
Only relations listed in const RELATIONS can be loaded. This is a security feature to prevent unauthorized data access.

Loading Relations

Load relations via the relations query parameter:
GET /api/products?relations=["category","reviews"]
Response includes nested data:
{
  "data": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 999.99,
      "category": {
        "id": 5,
        "name": "Electronics"
      },
      "reviews": [
        {
          "id": 10,
          "rating": 5,
          "comment": "Great product!"
        }
      ]
    }
  ]
}

Field Selection

Select Specific Fields

Limit fields from relations:
GET /api/products?relations=["category:id,name","reviews:rating,comment"]
This loads only id and name from category, and rating and comment from reviews.
The primary key is always included automatically, even if not specified.

Combine with Main Model Selection

GET /api/products?select=id,name,price&relations=["category:name"]
Returns:
{
  "data": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 999.99,
      "category": {
        "id": 5,
        "name": "Electronics"
      }
    }
  ]
}

Nested Relations

Loading Multi-Level Relations

Load relations of relations using dot notation:
GET /api/products?relations=["reviews.user","category.parent"]
All nested relations must be declared in the RELATIONS constant:
const RELATIONS = ['reviews', 'reviews.user', 'category', 'category.parent'];

Deep Nesting Example

class Order extends BaseModel
{
    const RELATIONS = [
        'customer',
        'items',
        'items.product',
        'items.product.category',
        'items.product.reviews',
        'items.product.reviews.user'
    ];
}
Request:
GET /api/orders?relations=["items.product.category","items.product.reviews.user"]

Filter on Relation Fields

Use the _nested parameter to apply oper filters to relations:
GET /api/products?relations=["reviews"]&_nested=true&oper={"and":["reviews.rating|>=|4"]}
This filters products where reviews have a rating >= 4.

WhereHas Conditions

Filter parent records based on related data:
GET /api/products?oper={"and":["category.name|=|Electronics"]}
Returns only products where the category name is “Electronics”.

Complex Nested Filters

{
  "oper": {
    "and": [
      "status|=|active",
      "reviews.rating|>=|4",
      "reviews.verified|=|true"
    ]
  },
  "relations": ["reviews", "category"],
  "_nested": true
}

Relation Types

One-to-Many (HasMany)

public function posts()
{
    return $this->hasMany(Post::class);
}
Usage:
GET /api/users/1?relations=["posts"]
See ManagesOneToMany Trait for CRUD operations on one-to-many relations.

Many-to-One (BelongsTo)

public function author()
{
    return $this->belongsTo(User::class, 'user_id');
}
Usage:
GET /api/posts?relations=["author"]

Many-to-Many (BelongsToMany)

public function tags()
{
    return $this->belongsToMany(Tag::class)
        ->withPivot('order', 'featured')
        ->withTimestamps();
}
Usage:
GET /api/posts?relations=["tags"]
Pivot data is automatically included:
{
  "id": 1,
  "title": "Post Title",
  "tags": [
    {
      "id": 5,
      "name": "Laravel",
      "pivot": {
        "post_id": 1,
        "tag_id": 5,
        "order": 1,
        "featured": true
      }
    }
  ]
}
See ManagesManyToMany Trait for advanced many-to-many operations.

Has-One-Through and Has-Many-Through

public function country()
{
    return $this->hasOneThrough(
        Country::class,
        Address::class,
        'user_id',     // Foreign key on addresses
        'id',          // Foreign key on countries
        'id',          // Local key on users
        'country_id'   // Local key on addresses
    );
}
Usage:
GET /api/users?relations=["country"]

Polymorphic Relations

public function comments()
{
    return $this->morphMany(Comment::class, 'commentable');
}
Usage:
GET /api/posts?relations=["comments"]
GET /api/videos?relations=["comments"]

Advanced Scenarios

Conditional Relations

Load different relations based on user role:
public function listAll(Request $request): array
{
    $params = $this->processParams($request);
    
    // Admin sees everything
    if (auth()->user()->isAdmin()) {
        $params['relations'][] = 'internalNotes';
    }
    
    return $this->service->list_all($params);
}

Counting Relations

Use withCount() for relation counts:
const RELATIONS = ['posts', 'posts_count'];

public function posts()
{
    return $this->hasMany(Post::class);
}
Request:
GET /api/users?relations=["posts_count"]
Response:
{
  "id": 1,
  "name": "John Doe",
  "posts_count": 42
}

Aggregate Functions

Load computed values:
const RELATIONS = ['orders', 'orders_sum_total'];

public function orders()
{
    return $this->hasMany(Order::class);
}

Relation Management Traits

Rest Generic Class provides traits for managing related records:

ManagesOneToMany

Provides CRUD endpoints for HasMany relationships:
use Ronu\RestGenericClass\Core\Traits\ManagesOneToMany;

class CountryController extends RestController
{
    use ManagesOneToMany;
    
    protected array $oneToManyConfig = [
        'states' => [
            'relationship'  => 'array_states',
            'relatedModel'  => State::class,
            'parentModel'   => Country::class,
            'foreignKey'    => 'country_id',
            'localKey'      => 'id',
        ],
    ];
}
See ManagesOneToMany Trait for details.

ManagesManyToMany

Provides CRUD endpoints for BelongsToMany relationships:
use Ronu\RestGenericClass\Core\Traits\ManagesManyToMany;

class UserController extends RestController
{
    use ManagesManyToMany;
    
    protected array $manyToManyConfig = [
        'roles' => [
            'relationship'  => 'array_roles',
            'relatedModel'  => Role::class,
            'pivotModel'    => UserRole::class,
            'parentModel'   => User::class,
            'parentKey'     => 'user_id',
            'relatedKey'    => 'role_id',
        ],
    ];
}
See ManagesManyToMany Trait for details.

Configuration

Configure relation behavior in config/rest-generic-class.php:
'filtering' => [
    // Enforce relation allowlist (recommended: true)
    'strict_relations' => true,
    
    // Maximum nesting depth for relations
    'max_depth' => 5,
],

Performance Optimization

Eager Loading vs Lazy Loading

Good - Eager load to avoid N+1 queries:
GET /api/products?relations=["category","reviews"]
Bad - Without eager loading, each product triggers separate queries:
GET /api/products  
# Then accessing $product->category in Blade/Vue causes N+1

Select Only Needed Fields

GET /api/products?select=id,name&relations=["category:id,name"]
This reduces data transfer and JSON serialization overhead.

Limit Relation Depth

Avoid deeply nested relations in production:
// Avoid this in high-traffic endpoints
relations=["order.items.product.category.parent.region"]

// Better: load in separate requests or denormalize data
relations=["order.items.product"]

Error Handling

Common relation errors:
// Relation not in RELATIONS constant
relations=["secret_field"]  
// Throws: 500 error if strict_relations is enabled

// Invalid relation name
relations=["nonexistent"]  
// Laravel error: Relationship not found

// Invalid field syntax
relations=["category::id,name"]  
// Should use single colon: "category:id,name"

Security Best Practices

  1. Always declare relations: Never allow arbitrary relation loading
  2. Use field selection: Prevent exposure of sensitive fields
  3. Validate nested filters: Ensure _nested doesn’t expose restricted data
  4. Limit depth: Set reasonable max_depth in config
  5. Check permissions: Use middleware to restrict sensitive relations
Example permission check:
public function listAll(Request $request): array
{
    $params = $this->processParams($request);
    
    // Remove sensitive relations for non-admin users
    if (!auth()->user()->isAdmin()) {
        $params['relations'] = array_diff(
            $params['relations'], 
            ['internalNotes', 'privateData']
        );
    }
    
    return $this->service->list_all($params);
}

Build docs developers (and LLMs) love