mehedi8gb/api-crudify
Laravel package that generates standardized API CRUD (controller/service/repository/model/requests/resources/tests) and powers query-driven endpoints via a chainable pipeline for relations, filtering, sorting, soft deletes, and pagination—all controlled by URL query params.
Install the Package
composer require mehedi8gb/api-crudify --dev
php artisan crudify:install
Generate a CRUD Resource
php artisan crudify:make Post
This creates a full stack: Controller, Service, Repository, Model, Requests, Resource, Migration, Factory, Seeder, and Test.
Define API Routes
The package auto-registers routes in routes/api.php. Verify the generated routes:
// routes/api.php
Route::apiResource('posts', \App\Http\Controllers\PostController::class);
Test the API
php artisan serve
Send a request to GET /api/posts with optional query params:
/api/posts?q=title=laravel&limit=5
Use the ?q= shorthand to filter records:
GET /api/posts?q=title=laravel|status=published
This automatically applies:
Define Model & Migration
// app/Models/Post.php
class Post extends Model
{
protected $with = ['author']; // Auto-load relations
protected $fillable = ['title', 'content'];
}
Extend Repository for Custom Logic
// app/Repositories/PostRepository.php
class PostRepository extends BaseRepository
{
public function getPostsData(array $with = []): array
{
return $this->handleApiQueryRequest(
$this->query()->where('published', true),
$with
);
}
}
Customize Service Layer
// app/Services/PostService.php
class PostService extends BaseService
{
public function getPublishedPosts(): array
{
$data = $this->postRepository->getPostsData();
return $this->prepareResourceResponse($data, PostResource::class);
}
}
Override Controller Methods
// app/Http/Controllers/PostController.php
class PostController extends Controller
{
public function index(): JsonResponse
{
$posts = $this->postService->getPublishedPosts();
return $this->successResponse('Posts retrieved', $posts);
}
}
Domain-Specific Namespaces
php artisan crudify:make V1/Blog/Post
Generates routes under /api/v1/blog/posts.
Postman Schema Export
php artisan crudify:make Post --export-api-schema
Auto-generates OpenAPI schema in storage/api-schemas/.
Custom Query Handlers
Extend AbstractQueryHandler for domain-specific logic:
namespace App\Core\Query\Handlers\Custom;
class PublishedOnlyHandler extends AbstractQueryHandler
{
public function handle($builder, $request): void
{
$builder->where('published', true);
}
}
Register in BaseRepository::getQueryHandlers().
Bulk Operations
Use handleApiBulkRequest() in repositories for batch operations:
public function bulkUpdate(array $ids, array $data): array
{
return $this->handleApiBulkRequest($ids, $data, 'update');
}
Caching Strategies
Leverage cacheQuery() helper:
$cachedData = cacheQuery(
fn() => $this->getPostsData(),
'getPosts',
[],
60 * 15 // 15 minutes
);
Query Handler Order Matters
The pipeline executes in this order:
SoftDelete → Relations → Filter → Sort → Pagination
Reordering handlers in BaseRepository::getQueryHandlers() can break expected behavior.
Soft Delete Conflicts
If ?trashed=with is used but the model lacks soft deletes, the query will fail. Ensure:
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model { use SoftDeletes; }
Relation Loading Overrides
Explicit $with in handleApiQueryRequest() overrides:
$with propertygetEagerLoads()
Pass [] to use defaults.Pagination Edge Cases
?limit=all bypasses pagination but still applies other handlers.meta object) is omitted when limit=all.Validation Requests
Generated StoreRequest/UpdateRequest use authorize() but not rules(). Extend them:
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'content' => 'required|string',
];
}
Inspect Query Pipeline
Temporarily modify HandleApiQueryRequest to log each handler’s output:
// app/Core/Query/HandleApiQueryRequest.php
public function handleApiQueryRequest($builder, array $with = []): array
{
dd($this->queryHandlers); // Debug handler chain
// ...
}
Validate Query Parameters
Use tinker to test parameter parsing:
php artisan tinker
>>> $request = new \Illuminate\Http\Request(['q' => 'title=test']);
>>> (new \App\Core\Query\Handlers\Core\FilterHandler())->handle($builder, $request);
Check Handler Registration
Verify handlers are loaded in BaseRepository:
protected function getQueryHandlers(): array
{
return [
new SoftDeleteHandler(),
new RelationHandler(),
new FilterHandler(),
new SortHandler(),
new PaginationHandler(),
];
}
Custom Response Formatting
Override BaseService::prepareResourceResponse():
protected function prepareResourceResponse($data, string $resourceClass): array
{
return [
'data' => $this->resource($data, $resourceClass),
'custom_meta' => ['total_items' => count($data)],
];
}
Dynamic Route Binding
Extend BaseController to add custom route model binding:
protected function getRouteKeyName(): string
{
return 'slug'; // Override default 'id'
}
Query Cache Invalidation
Use cache()->forget() in service methods:
public function updatePost(Post $post, array $data): Post
{
$post->update($data);
cache()->forget('posts_list'); // Invalidate cache
return $post;
}
API Versioning
For multi-version APIs, extend BaseService:
class V2\PostService extends BaseService
{
protected $version = 'v2';
// Custom logic for v2
}
Autoloading
After crudify:install, run:
composer dump-autoload
If helpers aren’t available.
Base Class Restoration
The package auto-restores missing base classes on crudify:make. To bypass:
php artisan crudify:make Post --skip-restore
Route Prefix
Modify config/crudify.php to change the default /api prefix:
'prefix' => 'app',
Pagination Defaults
Override in BaseRepository:
protected function getDefaultPagination(): array
{
return ['limit' => 20, 'page' => 1];
}
Selective Field Loading
Use ->select() in custom queries to reduce payload size:
$builder->select(['id', 'title', 'created_at']);
Disable Pipeline for Simple Queries Bypass handlers with raw queries:
$this->model::where('status', 'active')->get();
Cache Query Handlers Cache frequent queries at the repository level:
public function getFeaturedPosts(): array
{
return cache()->remember('featured_posts', 3600, function() {
return $this->handleApiQueryRequest(
$this->query()->where('featured', true)->limit(10)
);
});
}
Lazy-Load Relations Use `with
How can I help you explore Laravel packages today?