ibekzod / microcrud
CRUD package for Laravel - Service-Repository pattern with caching, queues, and dynamic filtering
Installs: 2 473
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/ibekzod/microcrud
Requires
- php: >=7.0
- illuminate/database: ~5.2.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/filesystem: ~5.2.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/support: ~5.2.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0
README
MicroCRUD
MicroCRUD is a comprehensive Laravel package that eliminates boilerplate code and accelerates API development. Build production-ready RESTful APIs in minutes with advanced features like type-aware filtering, intelligent caching, queue support, and automatic validation.
Why MicroCRUD?
Stop writing the same CRUD logic over and over. MicroCRUD provides:
- ⚡ Rapid Development - Create full CRUD APIs with just 3 classes
- 🎯 Type-Aware Filtering - Automatic search filters based on database column types
- 🔍 Advanced Querying - Range filters, dynamic sorting, grouping, soft deletes, pagination
- 📊 Dynamic Grouping - Group by model columns or relations with auto-joins and eager loading
- ✅ Auto Validation - Generate validation rules from database schema
- 💾 Smart Caching - Tag-based cache with automatic invalidation
- 🚀 Queue Support - Background processing for heavy operations
- 🌐 Multi-Database - MySQL, PostgreSQL, SQLite, SQL Server
- 🌍 i18n Ready - Multi-language support out of the box
- 📦 Bulk Operations - Process multiple records efficiently
- 🎨 Highly Extensible - Hooks, events, and customization points
Table of Contents
- Requirements
- Installation
- Quick Start
- Architecture
- Core Concepts
- Features
- API Documentation
- Advanced Usage
- Middleware
- Configuration
- Examples
- Testing
- Contributing
- Changelog
- License
Requirements
| Requirement | Version |
|---|---|
| PHP | ^7.0 | ^8.0 | ^8.1 | ^8.2 | ^8.3 |
| Laravel | 5.2 - 12.x |
Installation
Install the package via Composer:
composer require ibekzod/microcrud
The package will automatically register itself via Laravel's package discovery.
Publish Assets (Optional)
Publish configuration files and translations:
php artisan vendor:publish --provider="Microcrud\MicrocrudServiceProvider"
This will create:
config/microcrud.php- Package configurationconfig/schema.php- Multi-schema database configuration (PostgreSQL)lang/vendor/microcrud/- Translation files (en, ru, uz)
Quick Start
Create a complete CRUD API in 3 steps:
Step 1: Create Your Model
<?php namespace App\Models; use Microcrud\Abstracts\Model; class Product extends Model { protected $fillable = [ 'name', 'description', 'price', 'stock', 'category_id', 'is_active' ]; // Define relationships as usual public function category() { return $this->belongsTo(Category::class); } }
Step 2: Create Your Service
<?php namespace App\Services; use Microcrud\Abstracts\Service; use App\Models\Product; class ProductService extends Service { protected $model = Product::class; // That's it! You now have full CRUD functionality // Optionally enable advanced features: // protected $enableCache = true; // protected $useJob = true; }
Step 3: Create Your Controller
<?php namespace App\Http\Controllers; use Microcrud\Http\CrudController; use App\Services\ProductService; class ProductController extends CrudController { protected $service = ProductService::class; // Optionally override specific methods for custom logic }
Step 4: Register Routes
Option 1: Use Route Macros (Recommended - all POST endpoints):
// routes/api.php // Single resource Route::microcrud('products', ProductController::class); // Multiple resources Route::microcruds([ 'products' => ProductController::class, 'categories' => CategoryController::class, 'orders' => OrderController::class, ]);
This creates 7 POST endpoints:
POST /products/create→ create()POST /products/update→ update()POST /products/show→ show()POST /products/index→ index()POST /products/delete→ delete()POST /products/restore→ restore()POST /products/bulk-action→ bulkAction()
Option 2: RESTful Routes (Standard Laravel):
// routes/api.php Route::apiResource('products', ProductController::class); Route::post('products/{id}/restore', [ProductController::class, 'restore']); Route::post('products/bulk', [ProductController::class, 'bulkAction']);
That's it! You now have a fully functional API with:
- ✅ List with pagination and filtering
- ✅ Create with validation
- ✅ Read single item
- ✅ Update with validation
- ✅ Delete (soft/hard)
- ✅ Restore soft-deleted items
- ✅ Bulk operations
Architecture
MicroCRUD follows the Service-Repository-Controller pattern with a focus on separation of concerns:
┌─────────────────────────────────────────────────────────┐
│ HTTP Request │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Controller Layer │
│ • CrudController (Abstract) │
│ • ApiBaseController (Response Formatting) │
│ • Your Controllers extend CrudController │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Service Layer │
│ • Service (Abstract) - Business Logic │
│ • Validation, Caching, Transactions │
│ • Query Building, Filtering │
│ • Job Dispatching │
│ • Before/After Hooks │
└────────────────────┬────────────────────────────────────┘
│
├──────────────┬──────────────┬───────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│ Model │ │ Cache │ │ Jobs │ │ Events │
│ Layer │ │ Layer │ │ Layer │ │ Layer │
└────┬─────┘ └──────────┘ └────────┘ └────────┘
│
▼
┌──────────┐
│ Database │
└──────────┘
Component Breakdown
| Component | Purpose | File Location |
|---|---|---|
| Model | Eloquent ORM models | Microcrud\Abstracts\Model |
| Service | Business logic & operations | Microcrud\Abstracts\Service |
| Controller | HTTP handling & routing | Microcrud\Http\CrudController |
| Resource | API response transformation | Microcrud\Responses\ItemResource |
| Middleware | Request preprocessing | Microcrud\Middlewares\* |
| Jobs | Background processing | Microcrud\Abstracts\Jobs\* |
| Exceptions | Error handling | Microcrud\Abstracts\Exceptions\* |
Core Concepts
Services
Services contain all business logic. The base Service class provides:
// Core CRUD operations $service->index($data); // List with filters $service->show($id); // Get single item $service->create($data); // Create new item $service->update($id, $data); // Update existing item $service->delete($id); // Delete (soft/hard) $service->restore($id); // Restore soft-deleted // Bulk operations $service->bulkCreate($items); $service->bulkUpdate($items); $service->bulkDelete($ids); $service->bulkRestore($ids); // Query manipulation $service->setQuery($query); $service->getQuery(); $service->applyDynamicFilters($query, $data); // Validation $service->indexRules(); $service->createRules(); $service->updateRules(); // Cache control $service->enableCache(); $service->clearCache();
Controllers
Controllers handle HTTP requests and delegate to services:
class CrudController extends ApiBaseController { // All methods return formatted JSON responses: index() → 200 OK (paginated list) show($id) → 200 OK (single item) create() → 201 Created update($id) → 202 Accepted delete($id) → 202 Accepted restore($id) → 202 Accepted bulkAction() → 202 Accepted }
Resources
Resources transform model data for API responses:
class ItemResource extends JsonResource { // Automatically: // - Formats dates (Y-m-d H:i:s) // - Converts _id fields to nested objects // - Handles relationships public function toArray($request) { return $this->forModel(); } }
Route Macros
MicroCRUD provides convenient Route macros for registering CRUD resources:
// Single resource Route::microcrud('products', ProductController::class); // Multiple resources Route::microcruds([ 'products' => ProductController::class, 'categories' => CategoryController::class, ]);
With Middleware & Prefix:
Route::prefix('v1')->middleware(['auth:api'])->group(function () { Route::microcruds([ 'products' => ProductController::class, 'orders' => OrderController::class, ]); });
Benefits:
- ✅ 75% less code than manual route definitions
- ✅ Consistent pattern across all resources
- ✅ Works with middleware, prefixes, and versioning
- ✅ All POST endpoints (production-tested pattern)
Features
Dynamic Filtering
MicroCRUD automatically detects column types and provides intelligent filtering:
String Columns → LIKE Search
GET /products?search_by_name=laptop # SELECT * FROM products WHERE name LIKE '%laptop%'
Numeric Columns → Exact Match + Range
GET /products?search_by_price=999 # SELECT * FROM products WHERE price = 999 GET /products?search_by_price_min=100&search_by_price_max=500 # SELECT * FROM products WHERE price >= 100 AND price <= 500 GET /products?search_by_stock_min=10 # SELECT * FROM products WHERE stock >= 10
Date Columns → Exact Match + Range
GET /products?search_by_created_at=2025-01-15 # SELECT * FROM products WHERE DATE(created_at) = '2025-01-15' GET /products?search_by_created_at_from=2025-01-01&search_by_created_at_to=2025-01-31 # SELECT * FROM products WHERE DATE(created_at) >= '2025-01-01' # AND DATE(created_at) <= '2025-01-31'
Boolean Columns → Exact Match
GET /products?search_by_is_active=1 # SELECT * FROM products WHERE is_active = 1
Dynamic Sorting
GET /products?order_by_price=asc GET /products?order_by_created_at=desc GET /products?order_by_name=asc&order_by_price=desc
Dynamic Grouping
Group results by model columns or relation columns:
Simple Grouping:
# Group by single column POST /apartments/index { "group_bies": ["object_id"] } # SELECT apartments.* FROM apartments GROUP BY apartments.object_id # Group by multiple columns POST /apartments/index { "group_bies": ["object_id", "block_id"] } # SELECT apartments.* FROM apartments GROUP BY apartments.object_id, apartments.block_id # Group by relation column (automatically joins and eager loads) POST /apartments/index { "group_bies": ["block.manager_id"] } # SELECT apartments.*, blocks.manager_id # FROM apartments # LEFT JOIN blocks ON apartments.block_id = blocks.id # GROUP BY blocks.manager_id
Grouped Pagination (Top N per Group):
# Get first 10 apartments per block (using window functions) POST /apartments/index { "group_bies": { "block_id": { "limit": 10, "order_by": "created_at", "order_direction": "desc" } } } # Uses ROW_NUMBER() OVER (PARTITION BY block_id ORDER BY created_at DESC) # Returns max 10 apartments per block # Get top 5 highest-priced apartments per object POST /apartments/index { "group_bies": { "object_id": { "limit": 5, "order_by": "price", "order_direction": "desc" } } } # Get first 3 apartments per manager (relation-based grouping) POST /apartments/index { "group_bies": { "block.manager_id": { "limit": 3, "search": "John", "order_by": "id" } } }
Mixed Syntax (Simple + Configured):
POST /apartments/index { "group_bies": { "object_id": { "limit": 10, "search": "Building A" }, "status": null, "block_id": null } }
Features:
- ✅ Simple GROUP BY - For aggregations and unique value queries
- ✅ Top N per Group - Get first/last/top N records per group using window functions
- ✅ Hierarchical Responses - Nested parent-child structure with
hierarchical: true - ✅ Relation Support - Group by relation columns (e.g.,
block.manager_id) - ✅ Auto JOIN & Eager Load - Automatically joins and eager loads relations
- ✅ Search within Groups - Filter specific groups with search parameter
- ✅ Nested Relations - Supports multi-level relations (e.g.,
block.manager.department_id) - ✅ Relation Exclusion - Prevent duplicate data with
exclude_relationsparameter - ✅ Validates Everything - Checks columns and relations exist
- ✅ Database Agnostic - Works with MySQL 8+, PostgreSQL, SQL Server
Response Structure:
The response format depends on whether hierarchical is enabled:
1. Flat Grouped Response (hierarchical: false or not set)
Basic (returns first record per group - non-deterministic):
POST /apartments/index { "group_bies": ["object_id"], "page": 1, "limit": 10 }
With Aggregate Selection (deterministic):
Method 1: Top-level parameters
POST /apartments/index { "group_bies": ["object_id"], "group_order_by": "created_at", "group_order_direction": "desc", "page": 1, "limit": 10 }
Method 2: Inline syntax (recommended)
POST /apartments/index { "group_bies": { "object_id": { "order_by_created_at": "desc" } }, "page": 1, "limit": 10 }
Both return newest apartment per object
Or use group_aggregate shortcuts:
{
"group_bies": ["object_id"],
"group_aggregate": "last",
"group_order_by": "id"
}
first: First record (ORDER BY column ASC, get first)last: Last record (ORDER BY column DESC, get first)max: Max value (ORDER BY column DESC, get first)min: Min value (ORDER BY column ASC, get first)
Response (flat list with standard pagination):
{
"pagination": {
"current": 1,
"totalPage": 10,
"totalItem": 100
},
"data": [
{
"id": 45,
"name": "Apartment 1-Z",
"object_id": 1,
"created_at": "2025-01-29 14:20:00",
"object": {
"id": 1,
"name": "Sunrise Apartments"
}
},
{
"id": 89,
"name": "Apartment 2-Y",
"object_id": 2,
"created_at": "2025-01-28 09:15:00",
"object": {
"id": 2,
"name": "Sunset Towers"
}
}
// ... 8 more apartments (newest per unique object_id)
]
}
2. Hierarchical Grouped Response (hierarchical: true)
Basic example:
POST /apartments/index { "group_bies": ["object_id"], "hierarchical": true, "page": 1, "limit": 10 }
With aggregations and inline ordering:
POST /apartments/index { "group_bies": { "block.manager_id": { "aggregations": { "count": true, "sum": ["price"], "avg": ["price"] }, "order_by_sold_at": "desc" } }, "hierarchical": true, "page": 1, "limit": 10 }
Returns nested structure (group → children):
{
"pagination": {
"current": 1,
"totalPage": 5,
"totalItem": 48
},
"data": [
{
"group": {
"id": 1,
"name": "Sunrise Apartments"
},
"data": [
{"id": 1, "name": "Apartment 1-A"},
{"id": 2, "name": "Apartment 1-B"},
{"id": 3, "name": "Apartment 1-C"}
// ALL apartments in this object (object relation auto-excluded)
]
},
{
"group": {
"id": 2,
"name": "Sunset Towers"
},
"data": [
{"id": 15, "name": "Apartment 2-A"}
]
}
// ... 8 more objects (10 groups total)
]
}
Comparison Table:
| Feature | Flat (hierarchical: false) | Hierarchical (hierarchical: true) |
|---|---|---|
| Structure | [{...}, {...}] |
[{group: {...}, data: [...]}, ...] |
| Parent Data | Repeated in each record | Once per group |
| Records per Group | 1 (configurable with group_aggregate) |
ALL |
| Pagination | Paginates individual records | Paginates groups |
| Relations | Included (normal behavior) | Auto-excluded from children |
| Aggregate Selection | ✅ group_aggregate, group_order_by |
N/A |
| Use Case | Standard listing, latest/oldest per group | Parent-child navigation |
| Performance | Good for simple lists | Better for complex hierarchies |
3. With Aggregation - Add selectRaw() in beforeIndex() for COUNT/SUM/AVG/MAX/MIN
4. Top N per Group - Use window functions (see Performance Guide)
5. With Relations - Automatically eager loads relations (no N+1)
// Example: Add aggregation for grouped queries class ApartmentService extends Service { public function beforeIndex() { $data = $this->getData(); $query = $this->getQuery(); if (!empty($data['group_bies'])) { $table = $this->getModelTableName(); $query->selectRaw("COUNT(*) as apartment_count") ->selectRaw("SUM(price) as total_value") ->selectRaw("AVG(price) as avg_price"); } $this->setQuery($query); return parent::beforeIndex(); } }
Example Response (with aggregation):
{
"data": [
{
"object_id": 101,
"apartment_count": 45,
"total_value": 15750000,
"avg_price": 350000
}
]
}
Example Model Setup:
class Apartment extends Model { public function block() { return $this->belongsTo(Block::class); } } class Block extends Model { public function manager() { return $this->belongsTo(User::class, 'manager_id'); } }
Pagination
GET /products?page=2&limit=50 GET /products?is_all=1 # Get all without pagination
Combining Filters
GET /products?search_by_name=laptop &search_by_price_min=500 &search_by_price_max=2000 &search_by_is_active=1 &order_by_price=asc &page=1 &limit=20
Validation System
Validation rules are auto-generated from your database schema:
class ProductService extends Service { protected $model = Product::class; // Override to customize rules public function createRules($rules = [], $replace = false) { return parent::createRules([ 'name' => 'required|string|max:255|unique:products', 'price' => 'required|numeric|min:0', 'stock' => 'required|integer|min:0', 'category_id' => 'required|exists:categories,id', 'is_active' => 'boolean', ], $replace); } public function updateRules($rules = [], $replace = false) { return parent::updateRules([ 'name' => 'sometimes|string|max:255', 'price' => 'sometimes|numeric|min:0', 'stock' => 'sometimes|integer|min:0', ], $replace); } }
Auto-Generated Rules Based on Column Types:
string→sometimes|nullableinteger→sometimes|integernumeric→sometimes|numericdate→sometimes|dateboolean→sometimes|boolean
Plus automatic range validation:
- Integer:
search_by_{column}_min,search_by_{column}_max - Date:
search_by_{column}_from,search_by_{column}_to
Caching
Enable intelligent caching with automatic invalidation:
class ProductService extends Service { protected $model = Product::class; protected $enableCache = true; protected $cacheExpiration = 3600; // seconds // Cache is automatically: // ✓ Created on read operations // ✓ Tagged by model name // ✓ Invalidated on create/update/delete // ✓ Scoped to query parameters }
Manual cache control:
$service->enableCache(); $service->disableCache(); $service->clearCache();
Queue Jobs
Process heavy operations in the background:
class ProductService extends Service { protected $model = Product::class; protected $useJob = true; protected $queueName = 'products'; // Now create/update operations are queued automatically }
Available Jobs:
StoreJob- Background creationUpdateJob- Background updatesDeleteJob- Background deletion
Job Features:
- ✅
ShouldQueue- Async processing - ✅
ShouldBeUnique- Prevent duplicates - ✅ Failed job logging
- ✅ Configurable queue names
Bulk Operations
Process multiple records efficiently:
// Bulk Create POST /products/bulk { "action": "create", "data": [ {"name": "Product 1", "price": 100}, {"name": "Product 2", "price": 200}, {"name": "Product 3", "price": 300} ] } // Bulk Update POST /products/bulk { "action": "update", "data": [ {"id": 1, "price": 150}, {"id": 2, "price": 250} ] } // Bulk Delete POST /products/bulk { "action": "delete", "ids": [1, 2, 3, 4, 5] } // Bulk Restore POST /products/bulk { "action": "restore", "ids": [1, 2, 3] } // Bulk Show POST /products/bulk { "action": "show", "ids": [1, 2, 3] }
Bulk Features:
- Transaction support (all or nothing)
- Queue support for large batches
- Validation for each item
- Progress tracking
Soft Deletes
Full soft delete support with easy restoration:
# Soft delete (default) DELETE /products/1 # Force delete (permanent) DELETE /products/1?force_delete=true # Restore soft-deleted POST /products/1/restore # List with soft-deleted items GET /products?with_trashed=true # List only soft-deleted items GET /products?only_trashed=true
// In your service $service->delete($id); // Soft delete $service->delete($id, true); // Force delete $service->restore($id); // Restore
Hooks & Events
Add custom logic at any point in the lifecycle:
class ProductService extends Service { protected $model = Product::class; // Before hooks (can modify data) public function beforeCreate($data) { $data['sku'] = 'PRD-' . strtoupper(uniqid()); $data['slug'] = Str::slug($data['name']); return $data; } public function beforeUpdate($id, $data) { Log::info("Updating product {$id}", $data); return $data; } // After hooks (receive created/updated item) public function afterCreate($item) { Cache::tags(['products'])->flush(); event(new ProductCreated($item)); return $item; } public function afterUpdate($item) { event(new ProductUpdated($item)); return $item; } public function afterDelete($item) { Storage::deleteDirectory("products/{$item->id}"); return $item; } public function afterRestore($item) { event(new ProductRestored($item)); return $item; } public function afterIndex() { // Called after listing items } }
Available Hooks:
beforeCreate($data)afterCreate($item)beforeUpdate($id, $data)afterUpdate($item)beforeDelete($id)afterDelete($item)beforeRestore($id)afterRestore($item)afterIndex()
API Documentation
Standard Response Format
Success Response (Single Item)
{
"success": true,
"data": {
"id": 1,
"name": "Laptop",
"price": 999.99,
"stock": 15,
"is_active": true,
"created_at": "2025-01-15 10:30:00",
"updated_at": "2025-01-15 10:30:00"
}
}
Success Response (Paginated List)
{
"success": true,
"data": [
{
"id": 1,
"name": "Laptop",
"price": 999.99
},
{
"id": 2,
"name": "Mouse",
"price": 29.99
}
],
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 20,
"to": 20,
"total": 100
}
}
Error Response (Validation)
{
"success": false,
"message": "The given data was invalid.",
"errors": {
"name": ["The name field is required."],
"price": ["The price must be at least 0."]
}
}
Error Response (Not Found)
{
"success": false,
"message": "Resource not found"
}
HTTP Status Codes
| Method | Endpoint | Success Status | Description |
|---|---|---|---|
| GET | /products |
200 OK | List products |
| GET | /products/{id} |
200 OK | Get single product |
| POST | /products |
201 Created | Create product |
| PUT | /products/{id} |
202 Accepted | Update product |
| DELETE | /products/{id} |
202 Accepted | Delete product |
| POST | /products/{id}/restore |
202 Accepted | Restore product |
| POST | /products/bulk |
202 Accepted | Bulk operation |
Error Status Codes
| Status Code | Method | Description |
|---|---|---|
| 400 | errorBadRequest() |
Bad Request |
| 401 | errorUnauthorized() |
Unauthorized |
| 403 | errorForbidden() |
Forbidden |
| 404 | errorNotFound() |
Not Found |
| 422 | errorValidation() |
Validation Error |
| 500 | error() |
Internal Server Error |
Advanced Usage
Custom Resources
Create custom transformations for your API responses:
<?php namespace App\Http\Resources; use Microcrud\Responses\ItemResource; class ProductResource extends ItemResource { public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, 'price' => [ 'amount' => $this->price, 'formatted' => '$' . number_format($this->price, 2), 'currency' => 'USD', ], 'stock' => [ 'quantity' => $this->stock, 'status' => $this->stock > 0 ? 'in_stock' : 'out_of_stock', 'low_stock' => $this->stock < 10, ], 'category' => new CategoryResource($this->whenLoaded('category')), 'images' => ImageResource::collection($this->whenLoaded('images')), 'is_active' => (bool) $this->is_active, 'timestamps' => [ 'created' => $this->created_at->toISOString(), 'updated' => $this->updated_at->toISOString(), ], ]; } }
Use in controller:
class ProductController extends CrudController { protected $service = ProductService::class; protected $resource = ProductResource::class; }
Transaction Management
Transactions are enabled by default. Control them per service:
class ProductService extends Service { protected $model = Product::class; protected $useTransaction = true; // default // Or disable for specific operations public function create($data) { $this->setIsTransactionEnabled(false); return parent::create($data); } }
Custom Query Scopes
Add custom query modifications:
class ProductService extends Service { protected $model = Product::class; public function index($data) { // Apply custom query before processing $query = $this->model::query() ->with(['category', 'images']) ->where('is_active', true) ->whereHas('category', function($q) { $q->where('active', true); }); $this->setQuery($query); return parent::index($data); } }
Preventing N+1 Queries
Eager load relationships to prevent N+1 queries:
class ProductService extends Service { protected $model = Product::class; public function index($data) { // Eager load relationships $query = $this->model::with([ 'category', 'images', 'reviews' => function($q) { $q->where('approved', true); } ]); $this->setQuery($query); return parent::index($data); } public function show($id) { $query = $this->model::with(['category', 'images', 'reviews']); $this->setQuery($query); return parent::show($id); } }
Without Global Scopes
Remove global scopes temporarily:
$service->withoutScopes(['ActiveScope'])->index($data); $service->withoutScopes()->index($data); // Remove all scopes
Parent-Child Relationships
For hierarchical data like categories or organizational structures:
use Microcrud\Traits\ParentChildTrait; class Category extends Model { use ParentChildTrait; protected $fillable = ['name', 'parent_id']; }
Usage:
$category = Category::find(1); // Get direct children $children = $category->children; // Get all descendants (recursive) $allDescendants = $category->allChildren; // Get parent $parent = $category->parent; // Get all descendant IDs $ids = $category->getAllDescendantIds(); // [2, 3, 4, 5, ...] // Get full tree from root $tree = Category::getRootWithChildren();
Auto-cascade delete:
$category->delete(); // Automatically deletes all children
HTTP Client Service
Make external API calls with the built-in HTTP client:
use Microcrud\Services\Curl\Services\CurlService; $curl = new CurlService(); // GET request $curl->setUrl('https://api.example.com/users') ->setHeaders([ 'Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json', ]) ->setParams(['page' => 1, 'limit' => 20]); $response = $curl->get(); // POST request $curl->setUrl('https://api.example.com/users') ->setParams([ 'name' => 'John Doe', 'email' => 'john@example.com', ]); $response = $curl->post(); // Other methods $curl->put(); $curl->patch(); $curl->delete();
Middleware
LocaleMiddleware
Automatically sets application locale from Accept-Language header:
// app/Http/Kernel.php protected $routeMiddleware = [ 'locale' => \Microcrud\Middlewares\LocaleMiddleware::class, ]; // routes/api.php Route::middleware(['locale'])->group(function () { Route::apiResource('products', ProductController::class); });
Request:
GET /api/products Accept-Language: ru
Behavior:
- Checks
Accept-Languageheader - Matches against configured locales (
config('microcrud.locales')) - Falls back to
config('microcrud.locale')
TimezoneMiddleware
Sets timezone from Timezone header:
protected $routeMiddleware = [ 'timezone' => \Microcrud\Middlewares\TimezoneMiddleware::class, ]; Route::middleware(['timezone'])->group(function () { // Your routes });
Request:
GET /api/products Timezone: America/New_York
Default: Uses config('microcrud.timezone', 'UTC')
LogHttpRequest
Logs all HTTP requests with URL, headers, and parameters:
protected $routeMiddleware = [ 'log.http' => \Microcrud\Middlewares\LogHttpRequest::class, ]; Route::middleware(['log.http'])->group(function () { // Your routes });
Logs to Laravel's default log channel.
Configuration
config/microcrud.php
<?php return [ /* |-------------------------------------------------------------------------- | Database Connection |-------------------------------------------------------------------------- | | Specify a custom database connection. Leave empty to use default. | */ 'connection' => env('MICROCRUD_DB_CONNECTION', ''), /* |-------------------------------------------------------------------------- | Authorization |-------------------------------------------------------------------------- | | Enable/disable authorization checks in controllers. | */ 'authorize' => env('MICROCRUD_AUTHORIZE', true), /* |-------------------------------------------------------------------------- | Supported Locales |-------------------------------------------------------------------------- | | List of locales supported by your application. | */ 'locales' => ['en', 'ru', 'uz'], /* |-------------------------------------------------------------------------- | Default Locale |-------------------------------------------------------------------------- | | The default locale for your application. | */ 'locale' => env('MICROCRUD_LOCALE', 'en'), /* |-------------------------------------------------------------------------- | Default Timezone |-------------------------------------------------------------------------- | | The default timezone used by TimezoneMiddleware. | */ 'timezone' => env('MICROCRUD_TIMEZONE', 'UTC'), ];
config/schema.php
For PostgreSQL multi-schema support:
<?php return [ 'notification' => env('DB_NOTIFICATION_SCHEMA', 'public'), 'upload' => env('DB_UPLOAD_SCHEMA', 'public'), 'user' => env('DB_USER_SCHEMA', 'public'), // Add your schemas here ];
Environment Variables
Add to your .env:
# MicroCRUD Configuration MICROCRUD_DB_CONNECTION=mysql MICROCRUD_AUTHORIZE=true MICROCRUD_LOCALE=en MICROCRUD_TIMEZONE=UTC # PostgreSQL Schemas (if needed) DB_NOTIFICATION_SCHEMA=notifications DB_UPLOAD_SCHEMA=uploads DB_USER_SCHEMA=users
Examples
Complete E-commerce Product API
// app/Models/Product.php namespace App\Models; use Microcrud\Abstracts\Model; class Product extends Model { use SoftDeletes; protected $fillable = [ 'name', 'slug', 'description', 'price', 'sale_price', 'sku', 'stock', 'category_id', 'brand_id', 'is_active' ]; protected $casts = [ 'price' => 'decimal:2', 'sale_price' => 'decimal:2', 'is_active' => 'boolean', ]; public function category() { return $this->belongsTo(Category::class); } public function brand() { return $this->belongsTo(Brand::class); } public function images() { return $this->hasMany(ProductImage::class); } public function reviews() { return $this->hasMany(Review::class); } } // app/Services/ProductService.php namespace App\Services; use Microcrud\Abstracts\Service; use App\Models\Product; use Illuminate\Support\Str; class ProductService extends Service { protected $model = Product::class; protected $enableCache = true; protected $cacheExpiration = 3600; protected $useTransaction = true; public function beforeCreate($data) { $data['slug'] = Str::slug($data['name']); $data['sku'] = 'PRD-' . strtoupper(uniqid()); return $data; } public function afterCreate($item) { Cache::tags(['products'])->flush(); event(new ProductCreated($item)); return $item; } public function createRules($rules = [], $replace = false) { return parent::createRules([ 'name' => 'required|string|max:255', 'description' => 'nullable|string', 'price' => 'required|numeric|min:0', 'sale_price' => 'nullable|numeric|min:0|lt:price', 'stock' => 'required|integer|min:0', 'category_id' => 'required|exists:categories,id', 'brand_id' => 'nullable|exists:brands,id', 'is_active' => 'boolean', ], $replace); } public function index($data) { $query = $this->model::with(['category', 'brand', 'images']) ->withCount('reviews') ->withAvg('reviews', 'rating'); $this->setQuery($query); return parent::index($data); } } // app/Http/Controllers/ProductController.php namespace App\Http\Controllers; use Microcrud\Http\CrudController; use App\Services\ProductService; use App\Http\Resources\ProductResource; class ProductController extends CrudController { protected $service = ProductService::class; protected $resource = ProductResource::class; } // app/Http/Resources/ProductResource.php namespace App\Http\Resources; use Microcrud\Responses\ItemResource; class ProductResource extends ItemResource { public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->name, 'slug' => $this->slug, 'description' => $this->description, 'pricing' => [ 'regular' => $this->price, 'sale' => $this->sale_price, 'discount' => $this->sale_price ? round((($this->price - $this->sale_price) / $this->price) * 100) : 0, ], 'inventory' => [ 'sku' => $this->sku, 'stock' => $this->stock, 'in_stock' => $this->stock > 0, ], 'category' => new CategoryResource($this->whenLoaded('category')), 'brand' => new BrandResource($this->whenLoaded('brand')), 'images' => ImageResource::collection($this->whenLoaded('images')), 'rating' => [ 'average' => round($this->reviews_avg_rating ?? 0, 1), 'count' => $this->reviews_count ?? 0, ], 'is_active' => (bool) $this->is_active, 'created_at' => $this->created_at->toISOString(), ]; } }
API Usage Examples
# List products with filters curl -X GET "http://api.example.com/products?search_by_name=laptop&search_by_price_min=500&search_by_price_max=2000&order_by_price=asc&page=1&limit=20" # Get single product curl -X GET "http://api.example.com/products/1" # Create product curl -X POST "http://api.example.com/products" \ -H "Content-Type: application/json" \ -d '{ "name": "Gaming Laptop", "description": "High-performance gaming laptop", "price": 1299.99, "stock": 50, "category_id": 5, "is_active": true }' # Update product curl -X PUT "http://api.example.com/products/1" \ -H "Content-Type: application/json" \ -d '{ "price": 1199.99, "sale_price": 999.99 }' # Delete product (soft) curl -X DELETE "http://api.example.com/products/1" # Force delete curl -X DELETE "http://api.example.com/products/1?force_delete=true" # Restore product curl -X POST "http://api.example.com/products/1/restore" # Bulk create curl -X POST "http://api.example.com/products/bulk" \ -H "Content-Type: application/json" \ -d '{ "action": "create", "data": [ {"name": "Product 1", "price": 99.99, "stock": 10, "category_id": 1}, {"name": "Product 2", "price": 199.99, "stock": 5, "category_id": 1} ] }'
Testing
While the package doesn't include a test suite, here's how to test your implementations:
// tests/Feature/ProductApiTest.php namespace Tests\Feature; use Tests\TestCase; use App\Models\Product; use App\Models\Category; class ProductApiTest extends TestCase { public function test_can_list_products() { Product::factory()->count(5)->create(); $response = $this->getJson('/api/products'); $response->assertStatus(200) ->assertJsonStructure([ 'success', 'data' => [ '*' => ['id', 'name', 'price'] ], 'meta' ]); } public function test_can_create_product() { $category = Category::factory()->create(); $data = [ 'name' => 'Test Product', 'price' => 99.99, 'stock' => 10, 'category_id' => $category->id, ]; $response = $this->postJson('/api/products', $data); $response->assertStatus(201) ->assertJson(['success' => true]); $this->assertDatabaseHas('products', ['name' => 'Test Product']); } public function test_can_filter_products_by_price() { Product::factory()->create(['price' => 50]); Product::factory()->create(['price' => 150]); Product::factory()->create(['price' => 250]); $response = $this->getJson('/api/products?search_by_price_min=100&search_by_price_max=200'); $response->assertStatus(200); $this->assertCount(1, $response->json('data')); } }
Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
git clone https://github.com/ibekzod/microcrud.git
cd microcrud
composer install
Coding Standards
- Follow PSR-12 coding standards
- Write descriptive commit messages
- Add tests for new features
- Update documentation
Changelog
[Latest] - 2025-01-30
Added
- ✨ Dynamic Grouping (group_bies) - Group results by model columns or relations with auto-joins and eager loading
- ✨ Grouped Pagination - "Top N per Group" queries using window functions (ROW_NUMBER OVER PARTITION BY)
- ✨ Hierarchical Grouped Responses - Nested parent-child structure with pagination at each level (
hierarchical: true) - ✨ Relation Exclusion - Prevent duplicate relation data in leaf resources (
exclude_relationsparameter) - ✨ Group Aggregations - Calculate COUNT/SUM/AVG/MAX/MIN per group level
- ✨ Route Macros -
Route::microcrud()andRoute::microcruds()for easy resource registration - ✨ Enhanced Exceptions - Rich error context with toArray()/toJson() methods
- ✨ Improved Middlewares - Better security, logging, and validation
- 📚 Comprehensive Documentation - Enhanced code documentation throughout
Improved
- ⚡ Better Error Handling - ValidationException, CreateException, UpdateException, DeleteException, NotFoundException
- 📝 Controller Documentation - Full PHPDoc for all methods
- 🎨 Code Quality - Better structure, logging, and maintainability
- 🔒 Security - Sensitive data filtering in LogHttpRequest middleware
Previous Releases
- Type-aware dynamic search filters - min/max for numeric, from/to for dates
- DeleteJob - Background deletion operations
- Configurable timezone - Via
config('microcrud.timezone') - DYNAMIC search_by_column & order_by_column - Added dynamic filtering
- Bulk actions - Implemented bulk operations
Security
If you discover any security-related issues, please email erkinovbegzod.45@gmail.com instead of using the issue tracker.
Credits
- Author: iBekzod
- Email: erkinovbegzod.45@gmail.com
- Package: ibekzod/microcrud
License
The MIT License (MIT). Please see License File for more information.
Made with ❤️ by iBekzod