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,
],
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
- Always declare relations: Never allow arbitrary relation loading
- Use field selection: Prevent exposure of sensitive fields
- Validate nested filters: Ensure
_nested doesn’t expose restricted data
- Limit depth: Set reasonable
max_depth in config
- 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);
}