Overview
The ManagesOneToMany trait provides complete CRUD functionality for one-to-many (HasMany) relationships with support for:
- Reading related records with filtering, pagination, and sorting
- Creating related records through the relationship
- Updating related records
- Deleting related records
- Bulk operations for all mutation methods
- Excel/PDF export of related data
Namespace: Ronu\RestGenericClass\Core\Traits\ManagesOneToMany
Location: /src/Core/Traits/ManagesOneToMany.php:32
Configuration
Basic Setup
Add the trait to your controller and define $oneToManyConfig:
use Ronu\RestGenericClass\Core\Traits\ManagesOneToMany;
class CountryController extends RestController
{
use ManagesOneToMany;
protected array $oneToManyConfig = [
'states' => [
'relationship' => 'array_states', // HasMany method on parent
'relatedModel' => State::class,
'parentModel' => Country::class,
'foreignKey' => 'country_id', // FK in related table
'localKey' => 'id', // PK in parent table
'mutation' => [
'dataKey' => ['States', 'states'],
'deleteRelated' => true,
],
],
];
}
Configuration Options
Required
relationship (string) - Name of the HasMany method on the parent model
relatedModel (string) - Fully qualified class name of the related model
parentModel (string) - Parent model class name
foreignKey (string) - Foreign key column in the related table
localKey (string) - Primary key column in the parent table
Optional (mutation)
dataKey (array|string) - Request keys to extract data from (default: [])
deleteRelated (bool) - Delete related model on deleteRelation (default: true)
Read Operations
listRelation
List related entities with filtering, pagination, and sorting.
public function listRelation(Request $request, mixed $parentId = null): LengthAwarePaginator|array
Parameters
Request $request - HTTP request with query parameters
mixed $parentId - Parent ID (null = auth user)
Query Parameters
select - Columns to select (default: ['*'])
relations - Relations to eager-load
eq/attr - Equality filters
oper - Complex filters (see Filtering)
orderby - Sort order
pagination - Pagination settings
Example
GET /api/countries/1/states?select=id,name,code&orderby={"name":"asc"}&pagination={"page":1,"pageSize":20}
Response:
{
"current_page": 1,
"data": [
{
"id": 5,
"name": "California",
"code": "CA"
},
{
"id": 12,
"name": "Florida",
"code": "FL"
}
],
"per_page": 20,
"total": 50
}
showRelation
Retrieve a single related entity.
public function showRelation(
Request $request,
mixed $parentIdOrRelatedId,
mixed $relatedId = null
): mixed
Parameters
Request $request - HTTP request
mixed $parentIdOrRelatedId - Parent ID (admin) or related ID (site/mobile)
mixed $relatedId - Related ID (admin only)
Route Shapes
// Site/Mobile: uses auth user as parent
GET /states/5
// Admin: explicit parent ID
GET /countries/1/states/5
Returns
- Related model instance on success
- 404 JSON response if not found
Example Response (Not Found)
{
"success": false,
"error": {
"message": "State with id 99 not found in relation 'states'",
"relation": "states",
"id": 99,
"suggested_fix": "Verify the resource exists via GET on the 'states' endpoint before performing write operations on it."
}
}
Create Operations
createRelation
Create new related entities through the relationship.
public function createRelation(Request $request, mixed $parentId = null): JsonResponse
Single Mode
Route: POST /countries/1/states
Request:
{
"states": {
"name": "California",
"code": "CA",
"population": 39538223
}
}
Response (201):
{
"success": true,
"model": {
"id": 51,
"name": "California",
"code": "CA",
"country_id": 1,
"population": 39538223
}
}
Bulk Mode
Set _scenario to any value containing “bulk”:
Route: POST /countries/1/states?_scenario=bulk_create
Request:
{
"states": [
{"name": "Texas", "code": "TX"},
{"name": "New York", "code": "NY"}
]
}
Response (201):
{
"success": true,
"models": [
{"id": 52, "name": "Texas", "code": "TX", "country_id": 1},
{"id": 53, "name": "New York", "code": "NY", "country_id": 1}
]
}
The foreign key (country_id) is automatically set by Laravel’s relationship.
Update Operations
updateRelation
Update related entities.
public function updateRelation(
Request $request,
mixed $parentIdOrRelatedId = null,
mixed $relatedId = null
): JsonResponse
Single Mode
Route: PUT /countries/1/states/5
Request:
{
"name": "California (Updated)",
"population": 40000000
}
Response:
{
"success": true,
"model": {
"id": 5,
"name": "California (Updated)",
"code": "CA",
"population": 40000000
}
}
Bulk Mode
Route: PUT /countries/1/states?_scenario=bulk_update
Request:
{
"states": [
{"id": 5, "population": 40000000},
{"id": 6, "population": 29000000}
]
}
Response:
{
"success": true,
"models": [
{"id": 5, "name": "California", "population": 40000000},
{"id": 6, "name": "Texas", "population": 29000000}
]
}
Bulk Partial Failure
If some IDs are not found:
{
"success": false,
"models": [
{"id": 5, "name": "California"}
],
"error": {
"message": "2 State record(s) not found in relation 'states': [99, 100]",
"relation": "states",
"not_found_ids": [99, 100],
"suggested_fix": "Fetch the current list via GET on the 'states' endpoint to obtain valid IDs, then retry with only the existing records."
}
}
Delete Operations
deleteRelation
Delete related entities.
public function deleteRelation(
Request $request,
mixed $parentIdOrRelatedId = null,
mixed $relatedId = null
): JsonResponse
Configuration
'mutation' => [
'deleteRelated' => true, // Delete the model (default)
// OR
'deleteRelated' => false, // Do nothing (for soft deletes or custom logic)
]
Single Mode
Route: DELETE /countries/1/states/5
Response:
{
"success": true,
"model": {
"id": 5,
"name": "California"
}
}
Bulk Mode
Route: DELETE /countries/1/states?_scenario=bulk_delete
Request:
Response:
{
"success": true,
"models": [
{"id": 5, "name": "California"},
{"id": 6, "name": "Texas"},
{"id": 7, "name": "Florida"}
]
}
If deleteRelated is true, related records are permanently deleted. Use soft deletes if you need to recover them.
Export Operations
exportRelationExcel
Export related data to Excel.
public function exportRelationExcel(Request $request, mixed $parentId = null): mixed
Parameters
filename (string) - Output filename (default: 'export.xlsx')
columns (array|CSV) - Explicit columns for spreadsheet
select, eq, oper, orderby, relations - Same as listRelation
Example
GET /countries/1/states/export/excel?filename=us-states.xlsx&columns=name,code,population
Requires: maatwebsite/excel package
exportRelationPdf
Export related data to PDF.
public function exportRelationPdf(Request $request, mixed $parentId = null): mixed
Parameters
filename (string) - Output filename (default: 'export.pdf')
template (string) - Blade view name (default: 'pdf')
columns, select, eq, oper, orderby, relations - Same as listRelation
Example
GET /countries/1/states/export/pdf?filename=states.pdf&template=reports.state
Requires: barryvdh/laravel-dompdf package
Internal Methods
resolveRelationConfig (Protected)
Resolves relation configuration from $oneToManyConfig.
protected function resolveRelationConfig(?string $relationName): array
Throws
BadRequestHttpException if relation is not configured
resolveParentEntity (Protected)
Resolves parent entity from ID or auth user.
protected function resolveParentEntity(array $config, mixed $parentId): mixed
Throws
NotFoundHttpException if parent is not found or auth user is missing
Extracts mutation data from request body using dataKey configuration.
protected function extractMutationData(Request $request, array $config): array
executeMutation (Protected)
Wraps mutation operations in a database transaction with error logging.
protected function executeMutation(
Request $request,
callable $operation,
int $status = 200
): JsonResponse
Parameters
Request $request - HTTP request
callable $operation - Closure containing mutation logic
int $status - HTTP status code on success (default: 200, use 201 for create)
Returns
JsonResponse - Operation result or error response
isBulkScenario (Protected)
Checks if the current request is a bulk operation.
protected function isBulkScenario(Request $request): bool
Returns true if _scenario contains “bulk”.
Query Application Methods
applyOneToManyEqFilters (Protected)
Applies equality filters to the query.
protected function applyOneToManyEqFilters(HasMany $query, array $eq): void
applyOneToManyOperFilters (Protected)
Applies complex operator-based filters.
protected function applyOneToManyOperFilters(HasMany $query, array $oper): void
applyOneToManyOrdering (Protected)
Applies ordering to the query.
protected function applyOneToManyOrdering(HasMany $query, array $orderby): void
applyOneToManySingleCondition (Protected)
Parses and applies a single filter condition.
protected function applyOneToManySingleCondition(
$query,
string $condition,
string $boolean
): void
Supports operators: =, !=, <, >, <=, >=, like, not like, ilike, not ilike, in, not in, between, not between, null, not null.
Error Handling
Not Found (Single)
Returns 404 JSON:
{
"success": false,
"error": {
"message": "State with id 99 not found in relation 'states'",
"relation": "states",
"id": 99,
"suggested_fix": "Verify the resource exists via GET on the 'states' endpoint before performing write operations on it."
}
}
Not Found (Bulk)
Partial success with error details:
{
"success": false,
"models": [
{"id": 5, "name": "Found State"}
],
"error": {
"message": "2 State record(s) not found in relation 'states': [99, 100]",
"relation": "states",
"not_found_ids": [99, 100],
"suggested_fix": "Fetch the current list via GET on the 'states' endpoint to obtain valid IDs, then retry with only the existing records."
}
}
// Throws BadRequestHttpException
"One-to-many relation 'invalid' is not configured on CountryController"
Parent Not Found
// Throws NotFoundHttpException
"Country with id 9999 not found"
Transaction Rollback
On any exception during mutation, the transaction is rolled back and the error is logged:
Log::error('O2M mutation failed on states relation', [
'controller' => 'CountryController',
'scenario' => 'bulk_update',
'exception' => $e,
]);
Query Optimization
The trait implements several optimizations:
- Batch loading: Bulk operations load all entities in one query
- Batch deletes: Uses single
whereIn()->delete() for multiple IDs
- Batch refresh: Refreshes all updated entities in one query
- Minimal queries: Typical bulk update = 3 queries (load, N updates, refresh)
- Use bulk operations when updating multiple records
- Eager-load relations to avoid N+1 queries
- Select only needed fields to reduce data transfer
- Add database indexes on foreign keys
- Paginate large result sets to avoid memory issues
Example Controller
Complete example:
use Ronu\RestGenericClass\Core\Controllers\RestController;
use Ronu\RestGenericClass\Core\Traits\ManagesOneToMany;
class CompanyController extends RestController
{
use ManagesOneToMany;
protected array $oneToManyConfig = [
'employees' => [
'relationship' => 'employees',
'relatedModel' => Employee::class,
'parentModel' => Company::class,
'foreignKey' => 'company_id',
'localKey' => 'id',
'mutation' => [
'dataKey' => ['Employees', 'employees'],
'deleteRelated' => true,
],
],
];
}
Routes:
// In routes/api.php
Route::middleware(['auth:api', 'inject:_relation=employees'])->group(function () {
Route::get('companies/{company}/employees', [CompanyController::class, 'listRelation']);
Route::get('companies/{company}/employees/{employee}', [CompanyController::class, 'showRelation']);
Route::post('companies/{company}/employees', [CompanyController::class, 'createRelation']);
Route::put('companies/{company}/employees/{employee}', [CompanyController::class, 'updateRelation']);
Route::delete('companies/{company}/employees/{employee}', [CompanyController::class, 'deleteRelation']);
});