mehedi8gb/api-crudify
Laravel package that generates standardized API CRUD (controller/service/repository/resources/requests/tests) and a query-driven pipeline for relations, filters, sorting, soft deletes, and pagination via URL params. Supports DDD namespaces and auto route/setup.
Install the package:
composer require mehedi8gb/api-crudify --dev
php artisan crudify:install
This copies base classes to app/ and sets up autoloading.
Generate your first CRUD:
php artisan crudify:make Post
This creates a full stack: Controller, Service, Repository, Model, Requests, Resource, Migration, Factory, Seeder, and Test.
Test the API:
php artisan migratephp artisan db:seed/api/posts (or your configured route).Fetch filtered and paginated posts with relations:
curl "http://your-app.test/api/posts?q=title=laravel&with=author,comments&page=1&limit=5"
?q= shorthand filters by title=laravel.?with= loads author and comments relations.Controller Layer:
Controller (auto-generated).Service layer.sendSuccessResponse() for consistent API responses.public function index(): JsonResponse
{
$posts = $this->postService->getPostsCollection();
return $this->successResponse('Posts retrieved', $posts);
}
Service Layer:
handleApiQueryRequest() on the repository to leverage the query pipeline.public function getPostsCollection(): array
{
$data = $this->postRepository->getPostsData(['author']);
return $this->prepareResourceResponse($data, PostResource::class);
}
Repository Layer:
BaseRepository to use the Chain of Responsibility query pipeline.applyFilters() or applySort().public function getPostsData(array $with = []): array
{
return $this->handleApiQueryRequest($this->query(), $with);
}
Query Pipeline Customization:
app/Core/Query/Handlers/ to modify behavior.CacheHandler for query caching.// app/Core/Query/Handlers/Optimization/CacheHandler.php
class CacheHandler extends AbstractQueryHandler
{
public function handle(Builder $builder, Request $request): Builder
{
$cacheKey = generateCacheKey($builder, $request);
return Cache::remember($cacheKey, now()->addHours(1), fn() => $builder);
}
}
Versioned APIs: Use nested namespaces for versioning:
php artisan crudify:make V1/Posts/Post
Routes will auto-generate under /api/v1/posts.
Custom Request Validation:
Extend generated StoreRequest or UpdateRequest to add custom rules:
public function rules(): array
{
return array_merge(parent::rules(), [
'custom_field' => 'required|string|max:255',
]);
}
Resource Customization:
Override toArray() in the generated PostResource to shape responses:
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'custom_field' => $this->whenLoaded('custom_field'),
];
}
Soft Deletes: Enable soft deletes in the model:
use Illuminate\Database\Eloquent\SoftDeletes;
use HasUuids;
class Post extends Model
{
use SoftDeletes, HasUuids;
protected $dates = ['deleted_at'];
}
Use query params to control soft-deleted records:
?trashed=with # Include soft-deleted
?trashed=only # Only soft-deleted
Testing: Use the auto-generated feature test as a baseline:
public function test_it_returns_a_paginated_list_of_posts()
{
$response = $this->getJson('/api/posts?limit=10');
$response->assertStatus(200)
->assertJsonStructure(['meta', 'data']);
}
Query Pipeline Order:
SoftDelete → Relations → Filter → Sort → Pagination.FilterHandler) requires understanding this sequence to avoid breaking expected behavior.Relation Loading Conflicts:
$with in handleApiQueryRequest() overrides model-level $with or Eloquent eager loads.dd($this->query()->toSql()) to verify loaded relations.Soft Delete Quirks:
?trashed=with param includes soft-deleted records but does not restore them.Model::withTrashed()->find($id)->restore().Cache Invalidation:
CacheHandler with tag-based invalidation.Migration Conflicts:
crudify:make on an existing model overwrites the migration.Query Debugging: Log the final query before execution:
$query = $this->query();
\Log::info('Final Query:', ['sql' => $query->toSql(), 'bindings' => $query->getBindings()]);
return $this->handleApiQueryRequest($query, $with);
Handler Debugging:
Temporarily modify AbstractQueryHandler to log handler execution:
public function handle(Builder $builder, Request $request): Builder
{
\Log::info(sprintf('Executing %s', static::class));
return parent::handle($builder, $request);
}
Validation Errors:
Check the FormRequest for custom rules. Use:
php artisan make:request CustomPostRequest
Then extend the generated request to add rules.
Custom Query Handlers:
Add new handlers in app/Core/Query/Handlers/ and register them in BaseRepository:
protected function getQueryHandlers(): array
{
return array_merge(parent::getQueryHandlers(), [
new \App\Core\Query\Handlers\Custom\MyHandler(),
]);
}
Override Base Classes:
Extend BaseRepository, BaseService, or Model in app/ to modify default behavior globally.
Dynamic Route Binding:
Customize route model binding in AppServiceProvider:
Route::bind('post', function ($id) {
return Post::withTrashed()->findOrFail($id);
});
API Schema Export: Re-generate OpenAPI schema after changes:
php artisan crudify:make Post --export-api-schema
Helper Utilities:
Extend app/Helpers/Helpers.php to add custom helper functions:
if (!function_exists('customHelper')) {
function customHelper($input) {
return strtoupper($input);
}
}
Autoloading:
After crudify:install, run composer dump-autoload if helpers are not recognized.
Namespace Conflicts:
Avoid naming conflicts with Laravel’s core classes (e.g., UserController vs. AuthController).
Route Caching: Clear route cache after generating new CRUDs:
php artisan route:clear
Testing:
Use RefreshDatabase trait for tests to ensure a clean state:
use RefreshDatabase;
class PostTest extends TestCase
{
use RefreshDatabase;
// ...
}
How can I help you explore Laravel packages today?