Reusable CRUD Service Layer
AbstractCrudService is a powerful base class for CRUD services that bundles query building, resource transformation, lifecycle hooks, domain events, and a unified response contract.
Features
- Querying with filter/sort support
- Resource-based output transformation
- Transaction wrapping for mutating actions
- Lifecycle hooks (
beforeStore,afterUpdate, etc.) - Optional soft-delete restore/force-delete flow
- Standard response contract via
JobMetric\PackageCore\Output\Response
Typical Service Example
class PostService extends \JobMetric\PackageCore\Services\AbstractCrudService
{
protected string $entityName = 'Post';
protected bool $softDelete = true;
protected bool $hasRestore = true;
protected bool $hasForceDelete = true;
protected static string $modelClass = \App\Models\Post::class;
protected static string $resourceClass = \App\Http\Resources\PostResource::class;
protected static array $fields = ['id', 'title', 'status', 'created_at'];
protected static array $defaultSort = ['-id'];
}
Main Methods
query
Builds the base query with filtering/sorting support and optional relations.
Use this when you need to compose custom flows on top of the standard query pipeline.
$query = $service->query(['status' => 'published'], ['author']);
paginate
Returns paginated, transformed results using the configured resource class.
Best for index endpoints that need stable API pagination behavior.
$response = $service->paginate(20, ['status' => 'published'], ['author']);
all
Returns all matched records without pagination while still applying transformation rules.
Use carefully for bounded result sets.
$response = $service->all(['status' => 'draft']);
show
Retrieves a single record by identifier and applies resource transformation.
Use this for detail endpoints and internal service reads.
$response = $service->show(1, ['author', 'comments']);
store
Creates a new record within the service lifecycle (hooks/events/response).
Input can be normalized through changeFieldStore before persistence.
$response = $service->store(['title' => 'New Post', 'status' => 'published']);
update
Updates an existing record through the same lifecycle pipeline as create operations.
Useful for centralizing validation-adjacent mutation logic.
$response = $service->update(1, ['title' => 'Updated Title']);
destroy
Deletes a record (soft delete when enabled).
Use this for standard removal operations with event support.
$response = $service->destroy(1);
restore
Restores a soft-deleted record when restore support is enabled.
Useful for reversible delete workflows.
$response = $service->restore(1);
forceDelete
Permanently removes a soft-deleted record when force-delete is enabled.
Use for irreversible cleanup operations only.
$response = $service->forceDelete(1);
Quick usage example:
$service = app(PostService::class);
$list = $service->paginate(20, ['status' => 'published'], ['author']);
$single = $service->show(1, ['author', 'comments']);
Hook Points
Override these methods when needed:
changeFieldStore,beforeStore,afterStorechangeFieldUpdate,beforeUpdate,afterUpdatebeforeDestroy,afterDestroybeforeRestore,afterRestorebeforeForceDelete,afterForceDelete
Hook example:
protected function beforeStore(Model $model, array &$data): void
{
$data['created_by'] = auth()->id();
}
protected function afterUpdate(Model $model, array &$data): void
{
activity()->performedOn($model)->log('Entity updated');
}
Event Properties
Use these properties to dispatch domain events after operations:
$storeEventClass$updateEventClass$deleteEventClass$restoreEventClass$forceDeleteEventClass
Example:
protected static ?string $storeEventClass = \App\Events\PostStored::class;
Response Contract
Service methods can return a standardized Response::make(...) payload:
Response::make(
true,
'Post created successfully',
PostResource::make($post),
201
);
Best Practices
- Keep one dedicated service per entity/domain aggregate
- Centralize mutation logic in hooks instead of controllers
- Keep event mappings explicit and predictable
- Pass required relations intentionally in
show/paginate - Use DTO/FormRequest normalization before
store/update
Common Pitfalls
- Moving too much business logic into controllers
- Inconsistent response shapes between endpoints
- Skipping transactions for critical operations
- Forgetting soft-delete feature flags (
hasRestore,hasForceDelete)