🚀 Getting Started
Welcome to the Laravel 12 Base Project documentation! This guide will help you get up and running quickly.
Quick Setup
1. Install Dependencies
# Install PHP dependencies
composer install
# Install Node.js dependencies (for streaming service)
cd streaming-service
npm install
cd ..
2. Configure Environment
# Copy environment file
cp .env.example .env
# Generate application key
php artisan key:generate
# Configure database in .env file
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_password
3. Run Migrations
# Run database migrations
php artisan migrate
# Seed database (optional)
php artisan db:seed
4. Start Services
# Start Laravel development server
php artisan serve
# Start streaming service (in another terminal)
cd streaming-service
npm start
Next Steps
- 📋 Read the Project Overview for architecture details
- 📦 Explore Modules to understand the modular structure
- ⚙️ Learn about Services for business logic
- 🛣️ Check Routes & API for endpoint documentation
- ⚡ Use Artisan Commands to generate CRUD operations
📋 Project Overview
This is a Laravel 12 base project with a modular architecture, featuring a Node.js streaming service for media handling. The project follows Laravel 11+ conventions and best practices.
Laravel 12 PHP 8.2+ Express.js MySQL JWT Auth Tailwind CSS Vite
Project Structure
Features
- ✅ Modular Architecture - Custom modules for Media, Notifications, Verification
- ✅ Multi-user System - Admin, Supervisor, Client, Guest user types
- ✅ Media Streaming - Express.js service for video/audio streaming
- ✅ JWT Authentication - Token-based API authentication
- ✅ Multi-language Support - Translatable models with astrotomic/laravel-translatable
- ✅ CRUD Generators - Artisan commands for rapid development
- ✅ API Documentation - Postman collection generator
- ✅ Service Layer Pattern - Organized business logic
- ✅ Pipeline Pattern - Request processing pipelines
- ✅ Observer Pattern - Model event observers
🏗️ Architecture
Laravel 12 Structure
This project uses Laravel 12 with the new streamlined structure:
app/Http/Kernel.php and
app/Console/Kernel.php.
Configuration is now in bootstrap/app.php.
Bootstrap Configuration
Application configuration is centralized in bootstrap/app.php:
// Routes configuration
->withRouting(
using: function () {
Route::group([
'prefix' => 'api',
'middleware' => ['api', 'set_locale', 'cors'],
], function () {
Route::prefix('guest')->group(base_path('routes/Api/guest.php'));
Route::prefix('admin')->group(base_path('routes/Api/admin.php'));
Route::prefix('client')->group(base_path('routes/Api/client.php'));
});
},
commands: __DIR__.'/../routes/console.php',
health: '/up'
)
// Middleware registration
->withMiddleware(function (Middleware $middleware) {
$middleware->append([
DetectUserType::class,
SetLocale::class,
HandleCors::class,
GzipMiddleware::class
]);
$middleware->alias([
'user_type' => CheckUserType::class,
'active' => ActiveMiddleware::class,
'permission' => PermissionMiddleware::class,
]);
})
Service Providers
Service providers are registered in bootstrap/providers.php (not
config/app.php):
return [
App\Providers\AppServiceProvider::class,
App\Providers\NotificationServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TelescopeServiceProvider::class,
Modules\MediaExpress\MediaExpressServiceProvider::class,
];
Console Scheduling
Scheduled commands are defined in bootstrap/app.php using withSchedule():
->withSchedule(function (Schedule $schedule) {
$schedule->command('media:clean-orphaned')
->everyTwoMinutes()
->withoutOverlapping();
})
Layered Architecture
- Route → Defines endpoint and middleware
- Middleware → Authentication, authorization, validation
- Controller → Handles HTTP request/response
- Request → Form request validation
- Service → Business logic layer
- Model → Database interaction
- Resource → API response transformation
📦 Modules
The project uses a modular architecture with custom Laravel packages located in the modules/ directory. Each module is a self-contained package that can
be easily maintained and extended.
Module Structure
Each module follows this structure:
Available Modules
1. Media Module
Location: modules/Media/
Purpose: Comprehensive media file management system with upload, storage, retrieval, and automatic cleanup.
Features:
- ✅ File upload handling (single & multiple)
- ✅ Media model with polymorphic relationships
- ✅
HasMediatrait for easy model integration - ✅ Automatic file cleanup on model deletion
- ✅ Orphaned media cleanup job
- ✅ Video thumbnail extraction job
- ✅ Hash-based media identification
- ✅ Collection-based media organization
- ✅ Single and multiple media support per collection
How to Use:
Step 1: Add HasMedia Trait to Your Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Modules\Media\Traits\HasMedia;
class Product extends Model
{
use HasMedia {
HasMedia::setAttribute as setMediaAttribute;
HasMedia::getAttribute as getMediaAttribute;
}
// Define media collections
// Key = collection name, Value = isMultiple (true/false)
public array $media_keys = [
'image' => false, // Single image
'gallery' => true, // Multiple images
'video' => false, // Single video
'documents' => true, // Multiple documents
];
protected $guarded = [];
public function setAttribute($key, $value)
{
if (method_exists($this, 'isMediaKey') && $this->isMediaKey($key)) {
return $this->setMediaAttribute($key, $value);
}
return parent::setAttribute($key, $value);
}
public function getAttribute($key)
{
if (method_exists($this, 'isMediaKey') && $this->isMediaKey($key)) {
return $this->getMediaAttribute($key);
}
return parent::getAttribute($key);
}
}
Step 2: Upload Media via API
// POST /api/media/upload
// Form Data:
// - file: (file) - Single or multiple files
// - collection: (string) - Collection name (optional, defaults to 'default')
// Example: Upload single image
POST /api/media/upload
Content-Type: multipart/form-data
file: [image.jpg]
collection: image
// Response:
{
"status": "success",
"data": {
"id": 1,
"hash": "abc123def456..."
},
"message": "Media uploaded successfully"
}
Step 3: Attach Media to Model
use App\Models\Product;
// Create product and attach media by hash
$product = Product::create([
'name' => 'Laptop',
'price' => 999.99,
// Attach media using hash from upload response
'image' => 'abc123def456...', // Single media (hash string)
'gallery' => [ // Multiple media (array of hashes)
'hash1...',
'hash2...',
'hash3...'
]
]);
// Or attach after creation
$product->image = 'abc123def456...';
$product->save();
// Or use attachMediaByHash method
$product->attachMediaByHash('abc123def456...', 'image');
$product->attachMediaByHash(['hash1', 'hash2'], 'gallery');
Step 4: Access Media URLs
// Get media URL (single collection)
$imageUrl = $product->image;
// Returns: "https://example.com/storage/uploads/image/abc123.jpg"
// Get media URLs (multiple collection)
$galleryUrls = $product->gallery;
// Returns: ["https://...", "https://...", "https://..."]
// Get all media URLs
$allMedia = $product->media_urls;
// Returns: [
// 'image' => 'https://...',
// 'gallery' => ['https://...', 'https://...'],
// 'video' => 'https://...',
// 'documents' => ['https://...', 'https://...']
// ]
// Get media with IDs
$imageWithId = $product->getAttributeWithId('image');
// Returns: ['id' => 1, 'url' => 'https://...']
// Access media relationship directly
$mediaItems = $product->media;
foreach ($mediaItems as $media) {
echo $media->url(); // Get full URL
echo $media->path; // Get storage path
echo $media->mime_type;
echo $media->size;
}
Step 5: Update Media
// Update single media (replaces existing)
$product->image = 'new_hash_abc123...';
$product->save();
// Update multiple media (replaces all)
$product->gallery = ['new_hash1', 'new_hash2', 'new_hash3'];
$product->save();
// Add to existing multiple media
$currentGallery = $product->gallery ?? [];
$product->gallery = array_merge($currentGallery, ['new_hash4']);
$product->save();
Step 6: Delete Media
// Delete specific media by ID
DELETE /api/media/{media_id}
// Clear media from collection (set to null)
$product->image = null;
$product->save();
// Media is automatically deleted when model is deleted
$product->delete(); // All associated media files are deleted
API Endpoints:
GET /api/media - List all media
POST /api/media/upload - Upload single/multiple files
POST /api/media/upload-bulk - Bulk upload (optimized)
DELETE /api/media/{id} - Delete media
POST /api/media/clear-orphaned - Clear orphaned media (admin)
Example: Complete Product with Media
// 1. Upload image
$uploadResponse = Http::post('/api/media/upload', [
'file' => $imageFile,
'collection' => 'image'
]);
$imageHash = $uploadResponse->json()['data']['hash'];
// 2. Upload gallery images
$galleryHashes = [];
foreach ($galleryFiles as $file) {
$response = Http::post('/api/media/upload', [
'file' => $file,
'collection' => 'gallery'
]);
$galleryHashes[] = $response->json()['data']['hash'];
}
// 3. Create product with media
$product = Product::create([
'name' => 'Product Name',
'price' => 99.99,
'image' => $imageHash,
'gallery' => $galleryHashes
]);
// 4. Access media in API response
return response()->json([
'id' => $product->id,
'name' => $product->name,
'image' => $product->image, // Single URL
'gallery' => $product->gallery, // Array of URLs
'all_media' => $product->media_urls // All collections
]);
2. MediaExpress Module
Location: modules/MediaExpress/
Purpose: Integration with Express.js streaming service for advanced media handling, streaming, and large file uploads.
Features:
- ✅ Express.js service integration
- ✅ Large file upload support
- ✅ Media streaming capabilities
- ✅ Hash-based file identification
- ✅ Bulk upload optimization
- ✅ Metadata management
How to Use:
Step 1: Configure Express Service
// config/media-express.php
return [
'express' => [
'enabled' => true,
'url' => env('EXPRESS_URL', 'http://localhost:3000'),
'timeout' => 30,
'api_key' => env('EXPRESS_API_KEY'),
],
'use_media_model' => true, // Use Media module's model
'endpoints' => [
'upload' => '/api/media/upload',
'delete' => '/api/media/delete',
'info' => '/api/media/info',
'list' => '/api/media/list',
],
];
Step 2: Upload via Express Service
use Modules\MediaExpress\Services\ExpressFileService;
$expressService = app(ExpressFileService::class);
// Upload single file
$media = $expressService->upload(
model: $product, // Optional: attach to model
file: $uploadedFile, // UploadedFile instance
collection: 'images' // Collection name
);
// Upload multiple files (bulk)
$mediaCollection = $expressService->uploadBulk(
model: $product,
files: [$file1, $file2, $file3],
collection: 'gallery'
);
// Get media info by hash
$mediaInfo = $expressService->getMediaInfo('abc123def456...');
// Returns: ['id', 'hash', 'path', 'disk', 'mime_type', 'size', 'original_name']
Step 3: Use in Controller
namespace App\Http\Controllers;
use Modules\MediaExpress\Services\ExpressFileService;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function store(Request $request, ExpressFileService $expressService)
{
$product = Product::create($request->only(['name', 'price']));
// Upload image via Express service
if ($request->hasFile('image')) {
$media = $expressService->upload(
$product,
$request->file('image'),
'image'
);
// Attach to product
$product->image = $media->hash;
$product->save();
}
return response()->json($product);
}
}
API Endpoints:
POST /api/media-express/upload - Upload file via Express
GET /api/media-express/info/{hash} - Get media info
DELETE /api/media-express/{id} - Delete media via Express
3. Notification Module
Location: modules/Notification/
Purpose: Comprehensive notification system for users and admins with database broadcasting and real-time updates.
Features:
- ✅ User notifications (client, supervisor)
- ✅ Admin notifications (super admin)
- ✅ Database broadcasting channel
- ✅ Notification service with pagination
- ✅ Mark as read functionality
- ✅ Delete notifications
- ✅ User type-based notification filtering
How to Use:
Step 1: Send Notification to User
use App\Models\User;
use Modules\Notification\Models\Notification;
use Illuminate\Notifications\Notification as LaravelNotification;
// Create custom notification
class OrderShippedNotification extends LaravelNotification
{
public function toDatabase($notifiable)
{
return [
'title' => 'Order Shipped',
'message' => 'Your order #12345 has been shipped!',
'type' => 'order',
'data' => [
'order_id' => 12345,
'tracking_number' => 'TRACK123'
]
];
}
}
// Send notification
$user = User::find(1);
$user->notify(new OrderShippedNotification());
Step 2: Send Admin Notification
use Modules\Notification\Models\AdminNotification;
// Create admin notification
class NewOrderNotification extends LaravelNotification
{
public function toDatabase($notifiable)
{
return [
'title' => 'New Order Received',
'message' => 'A new order has been placed',
'type' => 'order',
'data' => ['order_id' => 12345]
];
}
}
// Send to admin
$admin = User::where('user_type', 'super_admin')->first();
$admin->notify(new NewOrderNotification());
Step 3: Get User Notifications via API
// GET /api/notifications
// Headers: Authorization: Bearer {token}
// Response (for client):
{
"status": "success",
"data": {
"client_notifications": {
"data": [
{
"id": 1,
"title": "Order Shipped",
"message": "Your order has been shipped!",
"type": "order",
"read_at": null,
"created_at": "2024-01-15T10:30:00.000000Z"
}
],
"current_page": 1,
"per_page": 10
}
},
"message": "Notifications retrieved successfully"
}
// Response (for admin):
{
"status": "success",
"data": {
"super_admin_notifications": {
"data": [...]
}
}
}
Step 4: Mark Notification as Read
// POST /api/notifications/{id}/read
// Headers: Authorization: Bearer {token}
// Response:
{
"status": "success",
"data": null,
"message": "Notification read successfully"
}
Step 5: Delete Notification
// DELETE /api/notifications/{id}
// Headers: Authorization: Bearer {token}
// Response:
{
"status": "success",
"data": null,
"message": "Notification deleted successfully"
}
Step 6: Use Notification Service
use Modules\Notification\Services\NotificationService;
$notificationService = app(NotificationService::class);
// Get notifications (returns JsonResponse)
$response = $notificationService->getNotifications();
// Mark as read
$response = $notificationService->readNotification($notificationId);
// Delete notification
$response = $notificationService->deleteNotification($notificationId);
Step 7: Broadcast Notifications (Real-time)
// In your notification class
use Modules\Notification\Broadcasting\DatabaseChannel;
class OrderNotification extends LaravelNotification
{
public function via($notifiable)
{
return [DatabaseChannel::class];
}
public function toDatabase($notifiable)
{
return [
'title' => 'New Order',
'message' => 'You have a new order',
'type' => 'order',
'data' => ['order_id' => 123]
];
}
}
// Frontend: Listen to broadcast
// Using Laravel Echo or WebSockets
Echo.private(`user.${userId}`)
.notification((notification) => {
console.log('New notification:', notification);
// Update UI
});
API Endpoints:
GET /api/notifications - Get user notifications (paginated)
POST /api/notifications/{id}/read - Mark notification as read
DELETE /api/notifications/{id} - Delete notification
Example: Complete Notification Flow
// 1. Create notification class
class PaymentReceivedNotification extends LaravelNotification
{
protected $payment;
public function __construct($payment)
{
$this->payment = $payment;
}
public function toDatabase($notifiable)
{
return [
'title' => 'Payment Received',
'message' => "Payment of {$this->payment->amount} received",
'type' => 'payment',
'data' => [
'payment_id' => $this->payment->id,
'amount' => $this->payment->amount
]
];
}
}
// 2. Send notification
$user = User::find($payment->user_id);
$user->notify(new PaymentReceivedNotification($payment));
// 3. User retrieves notifications via API
// GET /api/notifications
// 4. User marks as read
// POST /api/notifications/1/read
// 5. User deletes notification
// DELETE /api/notifications/1
4. Verification Module
Location: modules/Verification/
Purpose: Email and SMS verification system with token-based validation, expiration, and attempt limiting.
Features:
- ✅ Email verification
- ✅ SMS/Phone verification
- ✅ Token-based verification
- ✅ Code expiration (configurable)
- ✅ Attempt limiting (max attempts)
- ✅ Resend functionality
- ✅ Verification events and listeners
- ✅ Notification resolver pattern
- ✅ Rate limiting/throttling
How to Use:
Step 1: Configure Verification
// config/verification.php
return [
'supported_types' => ['email', 'phone'],
'code_length' => 6,
'expires_in_minutes' => 10,
'max_attempts' => 5,
];
Step 2: Send Verification Code
use Modules\Verification\Services\VerificationService;
$verificationService = app(VerificationService::class);
// Generate unique token
$token = Str::random(32);
// Send email verification
$verification = $verificationService->send(
type: 'email',
target: 'user@example.com',
token: $token
);
// Send phone verification
$verification = $verificationService->send(
type: 'phone',
target: '+1234567890',
token: $token,
phone_code: '+1' // Optional phone country code
);
// Response: Verification model with code, expires_at, etc.
Step 3: Send via API
// POST /api/verification/send
// Body:
{
"type": "email", // 'email' or 'phone'
"target": "user@example.com",
"token": "unique_token_here",
"phone_code": "+1" // Optional, for phone verification
}
// Response:
{
"status": "success",
"data": null,
"message": "Verification code sent successfully."
}
Step 4: Verify Code
use Modules\Verification\Services\VerificationService;
$verificationService = app(VerificationService::class);
try {
$result = $verificationService->verify(
type: 'email',
code: '123456', // Code sent to user
token: $token // Same token from send()
);
// Returns:
// [
// 'is_verified' => true,
// 'token' => '...',
// 'target' => 'user@example.com',
// 'phone_code' => null // If phone verification
// ]
} catch (ValidationException $e) {
// Handle validation errors:
// - Code expired
// - Invalid code
// - Max attempts exceeded
// - Already verified
}
Step 5: Verify via API
// POST /api/verification/verify
// Body:
{
"type": "email",
"code": "123456",
"token": "unique_token_here"
}
// Success Response:
{
"status": "success",
"data": {
"is_verified": true,
"token": "unique_token_here",
"target": "user@example.com"
}
}
// Error Response (invalid code):
{
"status": "fail",
"message": "Invalid verification code."
}
// Error Response (expired):
{
"status": "fail",
"message": "Verification code has expired."
}
// Error Response (max attempts):
{
"status": "fail",
"message": "Maximum verification attempts exceeded, try to resend the code."
}
Step 6: Resend Verification Code
use Modules\Verification\Services\VerificationService;
$verificationService = app(VerificationService::class);
// Resend using same token
$verification = $verificationService->resend($token);
// If expired, new code is generated automatically
// If not expired, same code is resent
Step 7: Resend via API
// POST /api/verification/resend
// Body:
{
"type": "email",
"target": "user@example.com",
"token": "unique_token_here"
}
// Response:
{
"status": "success",
"data": null,
"message": "Verification code sent successfully."
}
Step 8: Get User by Token
use Modules\Verification\Services\VerificationService;
$verificationService = app(VerificationService::class);
// Get user associated with verification token
$user = $verificationService->getUserByToken($token);
// Use in registration/verification flow
if ($user && $user->email_verified_at === null) {
$user->email_verified_at = now();
$user->save();
}
Step 9: Listen to Verification Events
use Modules\Verification\Events\VerificationCompleted;
use Illuminate\Support\Facades\Event;
// Listen to verification completed event
Event::listen(VerificationCompleted::class, function ($event) {
$verification = $event->verification;
// Update user verification status
if ($verification->type === 'email') {
$user = User::where('email', $verification->target)->first();
if ($user) {
$user->email_verified_at = now();
$user->save();
}
}
// Or use the listener class
// Modules\Verification\Listeners\VerificationCompletedListener
});
Step 10: Complete Registration Flow Example
// 1. User registers
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => Hash::make('password'),
'email_verified_at' => null // Not verified yet
]);
// 2. Generate verification token
$token = Str::random(32);
// 3. Send verification code
$verificationService = app(VerificationService::class);
$verificationService->send(
type: 'email',
target: $user->email,
token: $token
);
// 4. Store token in session or return to frontend
session(['verification_token' => $token]);
// 5. User enters code, verify it
try {
$result = $verificationService->verify(
type: 'email',
code: $request->input('code'),
token: session('verification_token')
);
// 6. Mark user as verified
$user->email_verified_at = now();
$user->save();
// 7. Clear token
session()->forget('verification_token');
return response()->json(['message' => 'Email verified successfully']);
} catch (ValidationException $e) {
return response()->json([
'message' => $e->getMessage()
], 422);
}
API Endpoints:
POST /api/verification/send - Send verification code
POST /api/verification/verify - Verify code
POST /api/verification/resend - Resend verification code
Using Facade (Alternative):
use Modules\Verification\Facades\VerificationFacade;
// Send verification
VerificationFacade::send('email', 'user@example.com', $token);
// Verify code
$result = VerificationFacade::verify('email', '123456', $token);
// Resend
VerificationFacade::resend($token);
Module Registration
Modules are registered in composer.json as path repositories and
auto-loaded via PSR-4:
"repositories": [
{
"type": "path",
"url": "modules/Media",
"options": { "symlink": true }
},
{
"type": "path",
"url": "modules/MediaExpress",
"options": { "symlink": true }
},
{
"type": "path",
"url": "modules/Notification",
"options": { "symlink": true }
},
{
"type": "path",
"url": "modules/Verification",
"options": { "symlink": true }
}
],
"autoload": {
"psr-4": {
"Modules\\": "modules/"
}
}
Each module's service provider is registered in bootstrap/providers.php:
return [
// ... other providers
\Modules\Media\MediaServiceProvider::class,
\Modules\MediaExpress\MediaExpressServiceProvider::class,
\Modules\Notification\NotificationServiceProvider::class,
\Modules\Verification\VerificationServiceProvider::class,
];
⚙️ Services
Services contain business logic and are organized by context (App, Dashboard, Shared, Utilities).
Service Structure
Base Service Classes
ClientCRUDService (Read-Only)
Location: app/Services/App/Client/ClientCRUDService.php
Purpose: Base service for read-only client-facing operations. Designed for authenticated client users who can only view data, not modify it.
index() and show() methods.
Clients cannot create, update, or delete resources.
Available Methods:
index()- List resources with filtering, searching, paginationshow()- Show single resource details
How index() Works:
The index() method provides a powerful listing endpoint with
multiple features:
public function index(Request $request, $with, $resource): JsonResponse
{
// 1. Reset query with scoped query (applies global scopes)
$this->query = $this->scopedQuery();
// 2. Eager load relationships (if provided)
if (!empty($with)) {
$this->query->with($with);
}
// 3. Apply custom scopes from request
$this->applyScopes($request);
// 4. Apply filters from request
$this->applyFilters($request);
// 5. Apply search across searchable fields
$this->applySearch($request);
// 6. Apply sorting
$this->applySorting($request);
// 7. Select specific columns (if provided)
$columns = $request->get('columns', ['*']);
$this->query->select($columns);
// 8. Apply pagination
$result = $this->applyPagination($request);
// 9. Return formatted response
if ($request->get('paginate', false)) {
$resource::wrap($this->model->getTable());
return json($resource::collection($result)->response()->getData(), ...);
}
return json($resource::collection($result), ...);
}
How show() Works:
public function show($id, $load, $resource): JsonResponse
{
// 1. Resolve model using scoped query (respects global scopes)
$model = $this->resolveModelOrFail($id);
// 2. Eager load relationships (if provided)
if (!empty($load)) {
$model->load($load);
}
// 3. Return formatted response
return json($resource::make($model), __('Data retrieved successfully'));
}
Request Parameters for index():
| Parameter | Type | Description | Example |
|---|---|---|---|
filters |
object | Filter by field values (must be in searchableFields) | {"is_active": true, "category_id": 5} |
search |
string | Search across all searchable fields | "laptop" |
sort |
object | Sort by field(s) (asc/desc) | {"name": "asc", "price": "desc"} |
per_page |
integer | Items per page (default: 15) | 20 |
paginate |
boolean | Return pagination metadata (default: false) | true |
columns |
array | Select specific columns (default: ['*']) | ["id", "name", "price"] |
Example API Request:
GET /api/client/products?filters[is_active]=true&filters[category_id]=5&search=laptop&sort[name]=asc&per_page=20&paginate=true
// Response:
{
"status": "success",
"data": {
"products": [
{
"id": 1,
"name": "Laptop Pro",
"price": 999.99,
"is_active": true
}
],
"current_page": 1,
"per_page": 20,
"total": 1
},
"message": "Index response with pagination"
}
How to Implement ClientCRUDService:
namespace App\Services\App\Client\Products;
use App\Services\App\Client\ClientCRUDService;
use App\Models\Product;
class ProductService extends ClientCRUDService
{
public function __construct()
{
parent::__construct(new Product());
}
/**
* Override scopedQuery to apply default filters
* This ensures clients only see active products
*/
protected function scopedQuery()
{
return $this->model->query()
->where('is_active', true) // Only active products
->where('is_published', true); // Only published products
}
/**
* Override applyGlobalScopes to add global scopes
*/
protected function applyGlobalScopes(Builder $query): void
{
// Apply any global scopes here
// Example: $query->where('tenant_id', auth()->user()->tenant_id);
}
/**
* Override applyScopes to handle custom scope parameters
*/
protected function applyScopes(Request $request): void
{
// Example: Apply custom scopes from request
if ($request->has('featured')) {
$this->query->featured();
}
if ($request->has('in_stock')) {
$this->query->inStock();
}
}
}
Guest Services (Read-Only)
Purpose: Guest services work exactly like ClientCRUDService but for
unauthenticated users. They also only provide index() and show() methods.
Example Guest Service:
namespace App\Services\App\Guest\Products;
use App\Services\App\Client\ClientCRUDService; // Same base class
use App\Models\Product;
class ProductService extends ClientCRUDService
{
public function __construct()
{
parent::__construct(new Product());
}
protected function scopedQuery()
{
// Guests can only see published, active products
return $this->model->query()
->where('is_active', true)
->where('is_published', true)
->where('is_public', true); // Additional public check
}
}
DashboardCRUDService (Full CRUD)
Location: app/Services/Dashboard/DashboardCRUDService.php
Purpose: Base service for admin dashboard operations with full CRUD capabilities. Designed for admin users who can create, read, update, and delete resources.
Available Methods:
index()- List resources with filtering, searching, paginationshow()- Show single resource detailssave()- Create or update resource (used by both store and update)destroy()- Delete resource
How index() Works:
Same as ClientCRUDService, but admins can see all records (including inactive/unpublished).
How show() Works:
Same as ClientCRUDService, but admins can see all records.
How save() Works (Create/Update):
The save() method is a unified method that handles both creating
new records and updating existing ones. It uses Laravel's Pipeline pattern for processing.
public function save($request, $id = null, ?Closure $customSaveLogic = null, $resource = null, $load = []): JsonResponse
{
DB::beginTransaction();
try {
// 1. Resolve model (create new or find existing)
if ($id instanceof Model) {
$this->model = $id; // Model instance provided
} else {
$this->model = $id
? $this->resolveModelOrFail($id) // Update existing
: new $this->model; // Create new
}
// 2. Execute save logic
if ($customSaveLogic !== null) {
// Use custom save logic if provided
$customSaveLogic($request, $this->model);
} else {
// Use default pipeline-based save
$this->runSavePipeline($request);
}
// 3. Commit transaction
DB::commit();
// 4. Eager load relationships (if provided)
if (!empty($load)) {
$this->model->load($load);
}
// 5. Return formatted response
return json(
$resource ? $resource::make($this->model) : null,
__('Data saved successfully')
);
} catch (\Exception $e) {
DB::rollBack(); // Rollback on error
return $this->handleException($e);
}
}
Save Pipeline Stages:
The save() method uses a pipeline with these stages:
- BeforeSave - Pre-save hooks and validations
- FillModel - Fill model attributes from request
- SaveModel - Persist model to database
- SyncRelations - Sync relationships (many-to-many, etc.)
- AfterSave - Post-save hooks and side effects
protected function getSavePipes(): array
{
return [
\App\Pipelines\Base\Save\BeforeSave::class, // 1. Before save hooks
\App\Pipelines\Base\Save\FillModel::class, // 2. Fill attributes
\App\Pipelines\Base\Save\SaveModel::class, // 3. Save to DB
\App\Pipelines\Base\Save\SyncRelations::class, // 4. Sync relations
\App\Pipelines\Base\Save\AfterSave::class, // 5. After save hooks
];
}
How destroy() Works:
public function destroy($id): JsonResponse
{
try {
// 1. Resolve model
$model = $this->resolveModelOrFail($id);
// 2. Execute before destroy hooks
$this->beforeDestroy($model);
// 3. Delete related records (if needed)
$this->deleteRelations($model);
// 4. Delete the model
$model->delete();
// 5. Execute after destroy hooks
$this->afterDestroy($model);
// 6. Return success response
return json(__('Data removed successfully'));
} catch (\Exception $e) {
return $this->handleException($e);
}
}
Custom Hooks (Override in Your Service):
class ProductService extends DashboardCRUDService
{
/**
* Called before saving (create or update)
*/
public function beforeSave($request, $model)
{
// Example: Set default values
if (!$model->exists) {
$model->created_by = auth()->id();
}
$model->updated_by = auth()->id();
}
/**
* Called after saving
*/
public function afterSave($request, $model)
{
// Example: Clear cache, send notifications, etc.
Cache::forget("product_{$model->id}");
event(new ProductSaved($model));
}
/**
* Called before destroying
*/
public function beforeDestroy($model)
{
// Example: Check if can be deleted
if ($model->orders()->count() > 0) {
throw new \Exception('Cannot delete product with orders');
}
}
/**
* Called after destroying
*/
public function afterDestroy($model)
{
// Example: Cleanup
$model->media()->delete(); // Delete associated media
}
/**
* Sync relationships (many-to-many, etc.)
*/
public function syncRelations($request, $model)
{
// Example: Sync categories
if ($request->has('category_ids')) {
$model->categories()->sync($request->category_ids);
}
}
/**
* Delete related records before deleting model
*/
public function deleteRelations($model)
{
// Example: Delete related records
$model->variants()->delete();
$model->reviews()->delete();
}
}
How to Implement DashboardCRUDService:
namespace App\Services\Dashboard\Admin\Products;
use App\Services\Dashboard\DashboardCRUDService;
use App\Models\Product;
class ProductService extends DashboardCRUDService
{
public function __construct()
{
parent::__construct(new Product());
}
/**
* Override scopedQuery for admin (can see all products)
*/
protected function scopedQuery()
{
// Admins can see all products (no filtering)
return $this->model->query();
}
/**
* Custom save logic (optional - uses pipeline by default)
*/
public function save($request, $id = null, ?Closure $customSaveLogic = null, $resource = null, $load = [])
{
// Option 1: Use default pipeline (recommended)
return parent::save($request, $id, null, $resource, $load);
// Option 2: Use custom logic
return parent::save($request, $id, function($request, $model) {
// Custom save logic here
$model->fill($request->validated());
$model->save();
}, $resource, $load);
}
}
Example: Complete Product Service with All Features:
namespace App\Services\Dashboard\Admin\Products;
use App\Services\Dashboard\DashboardCRUDService;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
class ProductService extends DashboardCRUDService
{
public function __construct()
{
parent::__construct(new Product());
}
/**
* Admin can see all products
*/
protected function scopedQuery(): Builder
{
return $this->model->query();
}
/**
* Apply custom scopes from request
*/
protected function applyScopes(Request $request): void
{
if ($request->has('featured')) {
$this->query->featured();
}
if ($request->has('low_stock')) {
$this->query->where('stock', '<', 10);
}
}
/**
* Before save hook
*/
public function beforeSave($request, $model)
{
// Set timestamps
if (!$model->exists) {
$model->created_by = auth()->id();
}
$model->updated_by = auth()->id();
// Generate slug if not provided
if (!$model->slug && $model->name) {
$model->slug = \Str::slug($model->name);
}
}
/**
* After save hook
*/
public function afterSave($request, $model)
{
// Clear cache
\Cache::forget("product_{$model->id}");
// Sync media if provided
if ($request->has('image')) {
$model->image = $request->input('image');
$model->save();
}
if ($request->has('gallery')) {
$model->gallery = $request->input('gallery');
$model->save();
}
}
/**
* Sync relationships
*/
public function syncRelations($request, $model)
{
// Sync categories (many-to-many)
if ($request->has('category_ids')) {
$model->categories()->sync($request->category_ids);
}
// Sync tags (many-to-many)
if ($request->has('tag_ids')) {
$model->tags()->sync($request->tag_ids);
}
}
/**
* Before destroy hook
*/
public function beforeDestroy($model)
{
// Check if product can be deleted
if ($model->orders()->count() > 0) {
throw new \Exception('Cannot delete product with existing orders');
}
}
/**
* Delete related records
*/
public function deleteRelations($model)
{
// Delete variants
$model->variants()->delete();
// Delete reviews
$model->reviews()->delete();
}
}
Creating a Service
Use the Artisan command to generate a service:
# Generate client service (read-only)
php artisan make:service ProductService --path=app/Services/App/Client/Products/
# Generate dashboard service (full CRUD)
php artisan make:service ProductService --path=app/Services/Dashboard/Admin/Products/
Service Comparison
| Feature | ClientCRUDService | Guest Services | DashboardCRUDService |
|---|---|---|---|
| index() | ✅ Yes | ✅ Yes | ✅ Yes |
| show() | ✅ Yes | ✅ Yes | ✅ Yes |
| store() | ❌ No | ❌ No | ✅ Yes (via save()) |
| update() | ❌ No | ❌ No | ✅ Yes (via save()) |
| destroy() | ❌ No | ❌ No | ✅ Yes |
| Pipeline Support | ❌ No | ❌ No | ✅ Yes |
| Custom Hooks | ❌ No | ❌ No | ✅ Yes |
| Transaction Support | ❌ No | ❌ No | ✅ Yes |
| Use Case | Authenticated clients | Public/unauthenticated | Admin dashboard |
🔍 Search, Filter, Sort, Pagination & Scopes
This section explains how to implement and use powerful query features in your API endpoints. All CRUD services (Client, Guest, Dashboard) support these features automatically.
ClientCRUDService, GuestCRUDService,
or DashboardCRUDService. You just need to configure your model
correctly.
📋 Table of Contents
- 1. Setup Your Model
- 2. Search Functionality
- 3. Filter Functionality
- 4. Sort Functionality
- 5. Pagination
- 6. Custom Scopes
- 7. Column Selection
- 8. Complete Examples
1. Setup Your Model
To enable search, filter, and sort functionality, you need to configure your model with the SearchAndFilterAbilities trait and define searchable fields.
Step 1: Add the Trait
namespace App\Models;
use App\Traits\SearchAndFilterAbilities;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use SearchAndFilterAbilities;
protected $guarded = [];
}
Step 2: Define Searchable Fields
Add a $searchableFields array to your model. These are the
fields that can be searched, filtered, and sorted.
class Product extends Model
{
use SearchAndFilterAbilities;
/**
* Fields that can be searched, filtered, and sorted
* Only fields in this array will be processed
*/
public array $searchableFields = [
'name', // Regular field - searchable, filterable, sortable
'description', // Translatable field - searchable, filterable, sortable
'sku', // Regular field
'price', // Regular field
'category_id', // Foreign key - filterable, sortable
'is_active', // Boolean - filterable, sortable
'is_featured', // Boolean - filterable, sortable
'created_at', // Date - filterable, sortable
'updated_at', // Date - filterable, sortable
];
/**
* If using Translatable trait, define translated attributes
* These will be searched in all locales
*/
public array $translatedAttributes = [
'name',
'description',
];
}
$searchableFields can be used for search, filter, and sort. This
is a security feature to prevent unauthorized field access.
2. Search Functionality
Search allows users to search across multiple fields simultaneously using a single search term.
How It Works
- User provides a
searchparameter in the request - System searches across ALL fields in
$searchableFields - For each field:
- If field is translatable → searches in all locales using
orWhereTranslationLike() - If field is regular → uses
orWhere($field, 'LIKE', "%{$search}%")
- If field is translatable → searches in all locales using
- All conditions are combined with
OR(matches if found in ANY field)
API Usage
# Basic search
GET /api/client/products?search=laptop
# Search with other parameters
GET /api/client/products?search=laptop&filters[category_id]=5&sort[name]=asc
# Search in translatable fields (searches all locales)
GET /api/client/products?search=computer
Example Request & Response
# Request
GET /api/admin/products?search=macbook
# Response
{
"status": "success",
"data": [
{
"id": 1,
"name": "MacBook Pro",
"description": "Apple MacBook Pro laptop",
"sku": "MBP-001",
"price": 1999.99
},
{
"id": 2,
"name": "MacBook Air",
"description": "Apple MacBook Air laptop",
"sku": "MBA-001",
"price": 1299.99
}
],
"message": "Data retrieved successfully"
}
What Gets Searched
When you search for "laptop", the system will check:
- Product name contains "laptop"
- Product description contains "laptop"
- SKU contains "laptop"
- Price contains "laptop" (if price is numeric, this won't match, but it's checked)
- Any other field in
$searchableFields
LIKE with wildcards, so "lap" will match "laptop", "Laptop",
"LAPTOP", etc.
3. Filter Functionality
Filter allows users to filter results by specific field values. You can filter by multiple fields simultaneously.
How It Works
- User provides a
filtersobject in the request - For each filter field:
- If field is a date → uses
whereDate($field, $value) - If model has a scope method (e.g.,
scopeActive()) → uses the scope - Otherwise → uses
where($field, $value)(exact match)
- If field is a date → uses
- All filters are combined with
AND(all conditions must match)
API Usage
# Filter by single field
GET /api/client/products?filters[category_id]=5
# Filter by multiple fields
GET /api/client/products?filters[category_id]=5&filters[is_active]=true
# Filter by date
GET /api/client/products?filters[created_at]=2024-01-15
# Combine with search
GET /api/client/products?search=laptop&filters[category_id]=5&filters[is_active]=true
Example Request & Response
# Request
GET /api/admin/products?filters[category_id]=5&filters[is_active]=true&filters[is_featured]=1
# Response
{
"status": "success",
"data": [
{
"id": 1,
"name": "Laptop Pro",
"category_id": 5,
"is_active": true,
"is_featured": true
}
],
"message": "Data retrieved successfully"
}
Filter Types
| Field Type | Filter Behavior | Example |
|---|---|---|
| Date Fields | Uses whereDate() for date comparison |
filters[created_at]=2024-01-15 |
| Scope Methods | Calls model scope method if exists (e.g., scopeActive()) |
filters[active]=true → calls scopeActive($query, true) |
| Regular Fields | Uses exact match where($field, $value) |
filters[category_id]=5 → where('category_id', 5) |
| Boolean Fields | Uses exact match (true/false or 1/0) | filters[is_active]=true or filters[is_active]=1 |
Custom Scope Filters
You can create custom scope methods that will be automatically used when filtering:
class Product extends Model
{
/**
* This scope will be called when filtering by 'active'
* Usage: filters[active]=true
*/
public function scopeActive($query, $value)
{
return $query->where('is_active', $value);
}
/**
* This scope will be called when filtering by 'featured'
* Usage: filters[featured]=1
*/
public function scopeFeatured($query, $value)
{
return $query->where('is_featured', $value);
}
/**
* This scope will be called when filtering by 'in_stock'
* Usage: filters[in_stock]=true
*/
public function scopeInStock($query, $value)
{
if ($value) {
return $query->where('stock', '>', 0);
}
return $query->where('stock', '<=', 0);
}
/**
* This scope will be called when filtering by 'price_range'
* Usage: filters[price_range]=100-500
*/
public function scopePriceRange($query, $value)
{
if (str_contains($value, '-')) {
[$min, $max] = explode('-', $value);
return $query->whereBetween('price', [$min, $max]);
}
return $query;
}
}
// Usage in API:
// GET /api/products?filters[active]=true&filters[featured]=1&filters[in_stock]=true&filters[price_range]=100-500
4. Sort Functionality
Sort allows users to order results by one or more fields in ascending or descending order.
How It Works
- User provides a
sortobject in the request - For each sort field:
- If field is translatable → uses a subquery to sort by translated value
- If field is regular → uses
orderBy($field, $direction)
- Multiple sorts are applied in order (first sort is primary, second is secondary, etc.)
API Usage
# Sort by single field (ascending)
GET /api/client/products?sort[name]=asc
# Sort by single field (descending)
GET /api/client/products?sort[name]=desc
# Sort by multiple fields
GET /api/client/products?sort[price]=desc&sort[name]=asc
# Sort by translatable field (automatically uses current locale)
GET /api/client/products?sort[name]=asc
# Combine with search and filter
GET /api/client/products?search=laptop&filters[category_id]=5&sort[price]=desc&sort[name]=asc
Example Request & Response
# Request: Sort by price (descending), then by name (ascending)
GET /api/admin/products?sort[price]=desc&sort[name]=asc
# Response (sorted by price high to low, then alphabetically by name)
{
"status": "success",
"data": [
{
"id": 3,
"name": "Laptop Pro",
"price": 1999.99
},
{
"id": 1,
"name": "Laptop Air",
"price": 1999.99
},
{
"id": 2,
"name": "Laptop Basic",
"price": 999.99
}
],
"message": "Data retrieved successfully"
}
Sort Directions
| Direction | Description | Example |
|---|---|---|
asc or ASC |
Ascending order (A-Z, 0-9, oldest to newest) | sort[name]=asc |
desc or DESC |
Descending order (Z-A, 9-0, newest to oldest) | sort[price]=desc |
Sorting Translatable Fields
When sorting by translatable fields, the system automatically uses the current locale:
# If your app locale is 'en', this will sort by English name
GET /api/products?sort[name]=asc
# The system generates a subquery like:
# ORDER BY (SELECT name FROM product_translations
# WHERE locale = 'en' AND product_id = products.id LIMIT 1) ASC
5. Pagination
Pagination allows you to control how many results are returned per page and navigate through large result sets.
How It Works
- User provides
per_pageparameter (default: 15) - User can optionally request pagination metadata with
paginate=true - System returns paginated results with metadata (current page, total pages, etc.)
API Usage
# Default pagination (15 items per page, no metadata)
GET /api/client/products
# Custom items per page
GET /api/client/products?per_page=20
# With pagination metadata
GET /api/client/products?per_page=20&paginate=true
# Combine with other features
GET /api/client/products?search=laptop&filters[category_id]=5&sort[price]=desc&per_page=20&paginate=true
Example Request & Response (Without Metadata)
# Request
GET /api/admin/products?per_page=10
# Response (simple array)
{
"status": "success",
"data": [
{"id": 1, "name": "Product 1"},
{"id": 2, "name": "Product 2"},
// ... 8 more items
],
"message": "Data retrieved successfully"
}
Example Request & Response (With Metadata)
# Request
GET /api/admin/products?per_page=10&paginate=true
# Response (with pagination metadata)
{
"status": "success",
"data": {
"products": [
{"id": 1, "name": "Product 1"},
{"id": 2, "name": "Product 2"},
// ... 8 more items
],
"current_page": 1,
"per_page": 10,
"total": 45,
"last_page": 5,
"from": 1,
"to": 10,
"path": "http://example.com/api/admin/products",
"first_page_url": "http://example.com/api/admin/products?page=1",
"last_page_url": "http://example.com/api/admin/products?page=5",
"next_page_url": "http://example.com/api/admin/products?page=2",
"prev_page_url": null
},
"message": "Index response with pagination"
}
Pagination Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
per_page |
integer | 15 | Number of items per page |
page |
integer | 1 | Page number (automatically handled by Laravel) |
paginate |
boolean | false | Include pagination metadata in response |
Navigating Pages
# Page 1 (default)
GET /api/products?per_page=10
# Page 2
GET /api/products?per_page=10&page=2
# Page 3
GET /api/products?per_page=10&page=3
# Or use the URLs from pagination metadata
GET /api/products?page=2 # Uses next_page_url from previous response
6. Custom Scopes
Custom scopes allow you to apply predefined query filters that can be triggered via request parameters or used in your service.
How to Create Custom Scopes
Create scope methods in your model that can be called via the applyScopes() method in your service:
class Product extends Model
{
/**
* Scope: Get only active products
* Usage: Product::active()->get()
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope: Get only featured products
* Usage: Product::featured()->get()
*/
public function scopeFeatured($query)
{
return $query->where('is_featured', true);
}
/**
* Scope: Get products in a specific category
* Usage: Product::inCategory(5)->get()
*/
public function scopeInCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
/**
* Scope: Get products with low stock
* Usage: Product::lowStock()->get()
*/
public function scopeLowStock($query)
{
return $query->where('stock', '<', 10);
}
}
Using Scopes in Service
Override applyScopes() in your service to use custom scopes
based on request parameters:
class ProductService extends DashboardCRUDService
{
/**
* Apply custom scopes based on request parameters
*/
protected function applyScopes(Request $request): void
{
// Apply 'active' scope if requested
if ($request->has('active')) {
$this->query->active();
}
// Apply 'featured' scope if requested
if ($request->has('featured')) {
$this->query->featured();
}
// Apply 'low_stock' scope if requested
if ($request->has('low_stock')) {
$this->query->lowStock();
}
// Apply 'category' scope if requested
if ($request->has('category')) {
$this->query->inCategory($request->get('category'));
}
}
}
API Usage
# Use active scope
GET /api/admin/products?active=1
# Use featured scope
GET /api/admin/products?featured=1
# Use multiple scopes
GET /api/admin/products?active=1&featured=1&low_stock=1
# Combine with other features
GET /api/admin/products?active=1&search=laptop&sort[price]=desc
7. Column Selection
Column selection allows you to specify which columns to retrieve, reducing data transfer and improving performance.
API Usage
# Select specific columns
GET /api/admin/products?columns[]=id&columns[]=name&columns[]=price
# Or as comma-separated (if your API supports it)
GET /api/admin/products?columns=id,name,price
# Default: all columns (*)
Example Request & Response
# Request: Only get id, name, and price
GET /api/admin/products?columns[]=id&columns[]=name&columns[]=price
# Response (only selected columns)
{
"status": "success",
"data": [
{
"id": 1,
"name": "Laptop Pro",
"price": 1999.99
},
{
"id": 2,
"name": "Laptop Air",
"price": 1299.99
}
],
"message": "Data retrieved successfully"
}
8. Complete Examples
Here are complete examples showing how to use all features together.
Example 1: Complete Model Setup
namespace App\Models;
use App\Traits\SearchAndFilterAbilities;
use Astrotomic\Translatable\Translatable;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use SearchAndFilterAbilities;
use Translatable;
protected $guarded = [];
// Searchable, filterable, and sortable fields
public array $searchableFields = [
'name', // Translatable
'description', // Translatable
'sku',
'price',
'category_id',
'is_active',
'is_featured',
'stock',
'created_at',
'updated_at',
];
// Translatable fields
public array $translatedAttributes = [
'name',
'description',
];
// Custom scopes
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeFeatured($query)
{
return $query->where('is_featured', true);
}
public function scopeLowStock($query)
{
return $query->where('stock', '<', 10);
}
public function scopeInCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
}
Example 2: Complete Service Setup
namespace App\Services\Dashboard\Admin\Products;
use App\Services\Dashboard\DashboardCRUDService;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductService extends DashboardCRUDService
{
public function __construct()
{
parent::__construct(new Product());
}
/**
* Apply custom scopes based on request
*/
protected function applyScopes(Request $request): void
{
if ($request->has('active')) {
$this->query->active();
}
if ($request->has('featured')) {
$this->query->featured();
}
if ($request->has('low_stock')) {
$this->query->lowStock();
}
if ($request->has('category')) {
$this->query->inCategory($request->get('category'));
}
}
}
Example 3: Complex API Request
# Complex request with all features
GET /api/admin/products?
search=laptop&
filters[category_id]=5&
filters[is_active]=true&
filters[created_at]=2024-01-15&
sort[price]=desc&
sort[name]=asc&
active=1&
featured=1&
per_page=20&
paginate=true&
columns[]=id&
columns[]=name&
columns[]=price&
columns[]=category_id
# This request will:
# 1. Search for "laptop" in name, description, sku, etc.
# 2. Filter by category_id = 5
# 3. Filter by is_active = true
# 4. Filter by created_at = 2024-01-15
# 5. Apply 'active' scope
# 6. Apply 'featured' scope
# 7. Sort by price (descending), then by name (ascending)
# 8. Return 20 items per page with pagination metadata
# 9. Only return id, name, price, and category_id columns
Example 4: Frontend Integration (JavaScript)
// Example: Building query parameters in JavaScript
function buildProductQuery(filters) {
const params = new URLSearchParams();
// Search
if (filters.search) {
params.append('search', filters.search);
}
// Filters
if (filters.categoryId) {
params.append('filters[category_id]', filters.categoryId);
}
if (filters.isActive !== undefined) {
params.append('filters[is_active]', filters.isActive);
}
// Sort
if (filters.sortBy) {
params.append(`sort[${filters.sortBy}]`, filters.sortOrder || 'asc');
}
// Pagination
if (filters.perPage) {
params.append('per_page', filters.perPage);
}
if (filters.page) {
params.append('page', filters.page);
}
if (filters.includePagination) {
params.append('paginate', 'true');
}
// Scopes
if (filters.active) {
params.append('active', '1');
}
if (filters.featured) {
params.append('featured', '1');
}
return params.toString();
}
// Usage
const query = buildProductQuery({
search: 'laptop',
categoryId: 5,
isActive: true,
sortBy: 'price',
sortOrder: 'desc',
perPage: 20,
page: 1,
includePagination: true,
active: true,
featured: true
});
fetch(`/api/admin/products?${query}`)
.then(response => response.json())
.then(data => {
console.log(data);
});
Example 5: Quick Reference Table
| Feature | Parameter | Example | Description |
|---|---|---|---|
| Search | search |
?search=laptop |
Search across all searchable fields |
| Filter | filters[field] |
?filters[category_id]=5 |
Filter by specific field value |
| Sort | sort[field] |
?sort[price]=desc |
Sort by field (asc/desc) |
| Pagination | per_page |
?per_page=20 |
Items per page (default: 15) |
| Pagination Metadata | paginate |
?paginate=true |
Include pagination info in response |
| Columns | columns[] |
?columns[]=id&columns[]=name |
Select specific columns |
| Scopes | scope_name |
?active=1 |
Apply custom scope (if implemented) |
- Add
SearchAndFilterAbilitiestrait to your model - Define
$searchableFieldsarray - Extend
ClientCRUDService,GuestCRUDService, orDashboardCRUDService
🛣️ Routes & API
Route Structure
Routes are organized by user type in routes/Api/:
Route Prefixes
| Route File | URL Prefix | Middleware | Description |
|---|---|---|---|
guest.php |
/api/guest |
Public | No authentication required |
client.php |
/api/client |
auth:api |
JWT authenticated clients |
admin.php |
/api/admin |
auth:api, user_type:admin |
Admin-only routes |
Common Route Patterns
// Guest routes (public)
Route::get('/countries', [GuestController::class, 'countries']);
// Client routes (authenticated)
Route::middleware('auth:api')->group(function () {
Route::get('/profile', [ProfileController::class, 'show']);
Route::put('/profile', [ProfileController::class, 'update']);
});
// Admin routes (admin only)
Route::middleware(['auth:api', 'user_type:admin'])->group(function () {
Route::apiResource('products', ProductController::class);
});
Module Routes
Modules can define their own routes in modules/{Module}/routes/api.php:
// In module service provider
public function boot()
{
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
}
API Response Format
All API responses follow a consistent format using the json()
helper:
// Success response
return json($data, 'Operation successful', 'success', 200);
// Error response
return json(null, 'Error message', 'fail', 400);
// Response structure:
{
"status": "success|fail",
"message": "Response message",
"data": { ... }
}
🎬 Streaming Service
The project includes a Node.js Express service for handling media streaming and uploads.
Service Overview
streaming-service/
- Express.js - Web framework
- Multer - File upload handling
- MySQL2 - Database connection
- Sharp - Image processing
- JWT - Token validation
Installation & Setup
cd streaming-service
npm install
cp .env.example .env
# Edit .env with your configuration
Configuration
Edit streaming-service/.env:
PORT=3000
LARAVEL_API_URL=http://localhost:8000
STORAGE_PATH=../laravel12-base/storage/app/public
CORS_ORIGIN=http://localhost:8000,http://localhost:3000
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_NAME=your_database
Running the Service
npm run dev
npm start
npm run pm2:start
npm run pm2:stop
npm run pm2:restart
docker build -t media-streaming-service .
docker run -p 3000:3000 --env-file .env media-streaming-service
API Endpoints
1. Upload Media
Endpoint: POST /upload
Headers:
Authorization: Bearer <your-laravel-token>
Content-Type: multipart/form-data
Body:
file: File or File[] (required)
collection: string (optional, default: 'default')
Response:
{
"status": "success",
"message": "Media uploaded successfully",
"data": {
"id": 123,
"hash": "abc123def456..."
}
}
2. Stream Media
Endpoint: GET /stream/:hash
Headers:
Authorization: Bearer <token>
Range: bytes=0- (optional, for seeking)
Usage Example:
// JavaScript
const streamUrl = `http://localhost:3000/stream/${mediaHash}`;
const video = document.createElement('video');
video.src = streamUrl;
video.setAttribute('crossorigin', 'anonymous');
3. Delete Media
Endpoint: DELETE /media/:hash
Headers:
Authorization: Bearer <token>
4. Health Check
Endpoint: GET /health
Integration with Laravel
The streaming service integrates with Laravel through:
- Token Validation: Validates JWT tokens via Laravel API
- Media Info: Fetches media information from Laravel database
- File Storage: Uses Laravel's storage path for file access
Service Structure
⚡ Artisan Commands
The project includes powerful custom Artisan commands for rapid development. These commands follow Laravel's conventions and generate boilerplate code with best practices built-in.
Available Commands
1. CRUD Generator
Command: php artisan make:crud
The most powerful command in the project. Generates a complete CRUD (Create, Read, Update, Delete) setup with all necessary files in one go.
What It Generates
Based on the type you select, it creates:
- Controller - Handles HTTP requests (Admin/Client/Guest variants)
- Service - Business logic layer (extends base CRUD services)
- Form Request - Validation rules (Admin only, for store/update)
- Resource - API response transformation
- DetailsResource - Detailed resource for show endpoints (when needed)
- Model - Eloquent model (optional, with migration)
- Translation Model - For translatable models (optional)
- Routes - API resource routes (optional, auto-registered)
Interactive Prompts
When you run the command, you'll be prompted for:
- Entity Name - Singular form (e.g., "Product", "Category")
- CRUD Type - Choose from:
admin- Full CRUD (index, show, store, update, destroy)client- Read-only (index, show only)guest- Read-only (index, show only)
- Path - Where to place the files (defaults based on type)
- Create Model? - Whether to generate a model with migration
- Model Name - If creating model, specify the name
- Needs Translation? - Whether the model should be translatable
- Add Routes? - Whether to register API resource routes
- Route File - Which route file to use (defaults based on type)
- Place in Group? - Whether to place route inside middleware group
Example Usage
# Run the command
php artisan make:crud
# Interactive session example:
# 1. Enter the name: Product
# 2. Select type: admin
# 3. Path (default: Api/Dashboard/Admin/): [Enter]
# 4. Create model? Yes
# 5. Model name: Product
# 6. Needs translation? Yes
# 7. Add routes? Yes
# 8. Route file (default: Api/admin.php): [Enter]
# 9. Place in group? Yes
# Generated files:
# - app/Http/Controllers/Api/Dashboard/Admin/Products/ProductController.php
# - app/Http/Requests/Api/Dashboard/Admin/Products/ProductRequest.php
# - app/Http/Resources/Api/Dashboard/Admin/Products/ProductResource.php
# - app/Http/Resources/Api/Dashboard/Admin/Products/ProductDetailsResource.php
# - app/Services/Dashboard/Admin/Products/ProductService.php
# - app/Models/Product.php
# - app/Models/ProductTranslation.php
# - database/migrations/XXXX_create_products_table.php
# - routes/Api/admin.php (route added)
CRUD Types Explained
- Full CRUD operations: index, show, store, update, destroy
- Includes Form Request for validation
- Extends
DashboardCRUDService - Uses
custom_controller.stub - Path:
Api/Dashboard/Admin/
- Read-only: index, show only
- No Form Request (no write operations)
- Extends
ClientCRUDService - Uses
client_controller.stub - Path:
Api/App/Client/ - Always includes DetailsResource for show endpoint
- Read-only: index, show only
- Public access (no authentication required)
- Extends
ClientCRUDService - Uses
guest_controller.stub - Path:
Api/App/Guest/
Translation Support
If you enable translations, the command will:
- Add
Translatabletrait to the model - Implement
TranslatableContractinterface - Create a translation model (e.g.,
ProductTranslation) - Add translation table to migration
- Generate DetailsResource for showing translated fields
// Generated translatable model structure
class Product extends Model implements TranslatableContract
{
use Translatable;
public array $translatedAttributes = [];
// Usage:
$product->translate('en')->name;
$product->name; // Current locale
}
Route Registration
When you choose to add routes, the command will:
- Automatically detect the correct route file based on type
- Add the
usestatement for the controller - Register
Route::apiResource()with appropriate methods - Optionally place it inside the last middleware group
// Admin routes (full CRUD)
Route::apiResource('products', ProductController::class)
->only(['index', 'show', 'store', 'update', 'destroy']);
// Client/Guest routes (read-only)
Route::apiResource('products', ProductController::class)
->only(['index', 'show']);
2. Service Generator
Command: php artisan make:service {name?} {--path=} {--force}
Generates a service class following the service layer pattern. Services contain business logic and are organized by context.
Command Options
| Option | Description | Example |
|---|---|---|
name |
Service class name (singular, optional - will prompt if not provided) | ProductService |
--path= |
Destination directory (relative to project root) | app/Services/App/Client/Products/ |
--force |
Overwrite if file already exists | Flag (no value) |
Usage Examples
# Interactive mode (will prompt for name and path)
php artisan make:service
# With name only (will prompt for path)
php artisan make:service ProductService
# With name and path
php artisan make:service ProductService --path=app/Services/App/Client/Products/
# Force overwrite existing file
php artisan make:service ProductService --path=app/Services/App/Client/Products/ --force
Generated Service Structure
namespace App\Services\App\Client\Products;
use App\Services\App\Client\ClientCRUDService;
use App\Models\Product;
class ProductService extends ClientCRUDService
{
public function __construct()
{
parent::__construct(new Product());
}
protected function scopedQuery()
{
return $this->model->query();
}
}
Service Organization
Services are organized by context:
app/Services/App/Client/- Client-facing servicesapp/Services/Dashboard/- Admin dashboard servicesapp/Services/Shared/- Shared servicesapp/Services/Utilities/- Utility services
3. Enum Generator
Command: php artisan make:enum
Generates a PHP enum class with typed cases. Supports both string and integer-backed enums.
Interactive Prompts
- Enum Name - The class name (e.g., "UserType", "TransactionStatus")
- Data Type - Choose "string" or "int"
- States/Cases - Add multiple enum cases:
- State name (uppercase, underscores allowed)
- State value (must match data type)
Example Usage
# Run the command
php artisan make:enum
# Interactive session:
# 1. Enum name: TransactionStatus
# 2. Data type: string
# 3. State name: PENDING
# State value: pending
# 4. Add another? Yes
# 5. State name: COMPLETED
# State value: completed
# 6. Add another? Yes
# 7. State name: FAILED
# State value: failed
# 8. Add another? No
# Generated file: app/Enums/TransactionStatus.php
Generated Enum Example
namespace App\Enums;
enum TransactionStatus: string
{
case PENDING = 'pending';
case COMPLETED = 'completed';
case FAILED = 'failed';
}
// Usage:
$status = TransactionStatus::PENDING;
$value = $status->value; // 'pending'
$name = $status->name; // 'PENDING'
Integer-Backed Enum Example
# For integer enums:
enum Priority: int
{
case LOW = 1;
case MEDIUM = 2;
case HIGH = 3;
case URGENT = 4;
}
Validation Rules
- State names must be uppercase letters and underscores only
- Integer values must be valid integers
- String values can be any string
- Enum name will be automatically capitalized
4. Export Postman Collection
Command: php artisan export:postman
Exports all API routes to a Postman collection format (JSON). Perfect for API testing and documentation.
What It Does
- Scans all registered API routes
- Extracts route information (method, URI, middleware, etc.)
- Generates a Postman Collection v2.1 format JSON file
- Organizes routes by their prefixes (guest, client, admin)
- Includes route parameters and query strings
Usage
# Export to Postman collection
php artisan export:postman
# Output: postman/collection.json
Collection Structure
The generated collection includes:
- Collection metadata (name, description, schema version)
- Folders organized by route prefixes
- Request items with:
- HTTP method (GET, POST, PUT, DELETE, etc.)
- Full URL with parameters
- Headers (including Authorization placeholder)
- Request body examples (for POST/PUT)
5. Export API Documentation
Command: php artisan export:api-docs
Generates comprehensive API documentation from your routes. Creates markdown or HTML documentation.
Features
- Documents all API endpoints
- Includes request/response examples
- Lists required parameters
- Shows authentication requirements
- Groups endpoints by functionality
Usage
# Generate API documentation
php artisan export:api-docs
# Output: postman/docs/api-documentation.md
6. Smart Factory Generator
Command: php artisan make:factory {model}
Generates a factory with intelligent, realistic fake data based on the model's structure and field names.
Smart Features
- Field Name Detection - Analyzes column names to generate appropriate fake data
- Realistic Data - Uses context-aware fakers (emails for email fields, names for name fields, etc.)
- Relationship Support - Handles foreign keys and relationships
- Localization - Supports multiple locales for translatable fields
- Type Detection - Automatically detects field types (string, integer, boolean, etc.)
Usage
# Generate factory for a model
php artisan make:factory Product
# Generated: database/factories/ProductFactory.php
Example Generated Factory
use Illuminate\Database\Eloquent\Factories\Factory;
class ProductFactory extends Factory
{
protected $model = Product::class;
public function definition(): array
{
return [
'name' => $this->faker->words(3, true),
'description' => $this->faker->paragraph(),
'price' => $this->faker->randomFloat(2, 10, 1000),
'stock' => $this->faker->numberBetween(0, 100),
'is_active' => $this->faker->boolean(80),
'category_id' => Category::factory(),
'created_at' => now(),
'updated_at' => now(),
];
}
}
Field Name Patterns
The factory recognizes common patterns:
| Field Pattern | Generated Data |
|---|---|
*_id |
Foreign key (creates related model) |
email |
Valid email address |
phone |
Phone number |
name, title |
Words or sentence |
description, content |
Paragraph text |
price, amount |
Decimal number |
is_*, has_* |
Boolean value |
created_at, updated_at |
Current timestamp |
7. Fill Request Command
Command: php artisan fill:request {path} {--model=} {--with-translation} {--force}
Automatically fills existing FormRequest classes with validation rules extracted from the model's database schema. Saves time by generating validation rules based on column types and constraints.
Command Options
| Option | Description | Example |
|---|---|---|
path |
Request file path (required) | Api/Dashboard/Admin/Cities/CityRequest |
--model= |
Model class name (auto-detected if not provided) | City |
--with-translation |
Include translatable fields even if not detected | Flag (no value) |
--force |
Overwrite existing rules and attributes | Flag (no value) |
What It Does
- Analyzes the model's database table structure
- Generates validation rules based on column types (string, integer, boolean, etc.)
- Handles foreign keys with
existsrules - Detects nullable columns and applies
sometimesrule - Generates human-readable attribute labels
- Supports translatable models with locale-based rules
- Uses
$rulevariable for create/update logic (required on create, sometimes on update)
Usage Examples
# Fill request with auto-detected model
php artisan fill:request Api/Dashboard/Admin/Cities/CityRequest
# Specify model explicitly
php artisan fill:request Api/Dashboard/Admin/Cities/CityRequest --model=City
# Include translation fields
php artisan fill:request Api/Dashboard/Admin/Cities/CityRequest --with-translation
# Force overwrite existing rules
php artisan fill:request Api/Dashboard/Admin/Cities/CityRequest --force
Generated Rules Example
public function rules(): array
{
$rule = getModel($this->route('city'), City::class) ? 'sometimes' : 'required';
$rules = [
'country_id' => [$rule, 'exists:countries,id'],
'name' => ['sometimes', 'string', 'max:255'],
'is_active' => ['sometimes', 'boolean'],
'lat' => ['sometimes', 'numeric', 'between:-90,90'],
'lng' => ['sometimes', 'numeric', 'between:-180,180'],
];
// Translatable fields
foreach (config('translatable.locales') as $locale) {
$rules["{$locale}.name"] = [$rule, 'string', 'max:255'];
}
return $rules;
}
Rule Generation Logic
| Column Type/Pattern | Generated Rules |
|---|---|
*_id (foreign key) |
[$rule, 'exists:table,id'] |
string |
['sometimes', 'string', 'max:255'] |
integer |
['sometimes', 'numeric'] |
boolean or is_*
|
['sometimes', 'boolean'] |
decimal/float |
['sometimes', 'numeric'] |
date/datetime |
['sometimes', 'date'] |
json |
['sometimes', 'array'] |
lat |
['sometimes', 'numeric', 'between:-90,90'] |
lng |
['sometimes', 'numeric', 'between:-180,180'] |
8. Fill Resource Command
Command: php artisan fill:resource {path} {--model=} {--force}
Automatically fills existing Resource classes with model attribute mapping. Generates the toArray() method based on the model's database columns and
relationships.
Command Options
| Option | Description | Example |
|---|---|---|
path |
Resource file path (required) | Api/Dashboard/Admin/Cities/CityResource |
--model= |
Model class name (auto-detected if not provided) | City |
--force |
Overwrite existing toArray method | Flag (no value) |
What It Does
- Scans the model's database table for all columns
- Generates field mappings in
toArray()method - Detects and includes relationships (BelongsTo, HasMany, HasOne)
- Handles translatable models (uses
HasTranslatedAttributestrait for DetailsResource) - Groups location fields (lat, lng) into a location object
- Automatically finds both Resource and DetailsResource files if they exist
- Uses null-safe operators (
?->) for safe property access
Usage Examples
# Fill resource with auto-detected model
php artisan fill:resource Api/Dashboard/Admin/Cities/CityResource
# Specify model explicitly
php artisan fill:resource Api/Dashboard/Admin/Cities/CityResource --model=City
# Force overwrite existing toArray method
php artisan fill:resource Api/Dashboard/Admin/Cities/CityResource --force
Generated Resource Example
public function toArray($request): array
{
return [
'id' => @$this?->id,
'country_id' => @$this?->country_id,
'is_active' => @$this?->is_active,
'location' => [
'lat' => @$this?->lat,
'lng' => @$this?->lng,
],
'country' => [
'id' => @$this?->country?->id,
'name' => @$this?->country?->name,
'short_name' => @$this?->country?->short_name,
],
'created_at' => @$this?->created_at,
];
}
DetailsResource with Translations
use App\Traits\HasTranslatedAttributes;
class CityDetailsResource extends JsonResource
{
use HasTranslatedAttributes;
public function toArray(Request $request): array
{
$data = [
'id' => @$this?->id,
'country_id' => @$this?->country_id,
'is_active' => @$this?->is_active,
'created_at' => @$this?->created_at,
];
// Translated fields are merged automatically
return array_merge($data, $this->getTranslatedData());
}
}
Features
- Auto-detection: Finds both Resource and DetailsResource files
- Relationship Support: Automatically includes BelongsTo relationships
- Location Grouping: Combines lat/lng into location object
- Translation Support: Handles translatable models correctly
- Safe Access: Uses null-safe operators to prevent errors
- Trait Integration: Automatically adds
HasTranslatedAttributestrait when needed
4. Export Postman Collection (Enhanced)
Command: php artisan export:postman {--bearer=} {--basic=} {--with-docs} {--only-uri=} {--except-uri=}
Exports all API routes to a Postman collection format (JSON) with advanced filtering and authentication options.
Command Options
| Option | Description | Example |
|---|---|---|
--bearer= |
Bearer token to use on authenticated endpoints | your-jwt-token |
--basic= |
Basic auth credentials | username:password |
--with-docs |
Also generate markdown API docs alongside collection | Flag (no value) |
--only-uri= |
Comma-separated URI substrings to include | api/admin,countries |
--except-uri= |
Comma-separated URI substrings to exclude | api/guest,test |
Usage Examples
# Basic export
php artisan export:postman
# With bearer token
php artisan export:postman --bearer=eyJ0eXAiOiJKV1QiLCJhbGc...
# With docs generation
php artisan export:postman --with-docs
# Only admin routes
php artisan export:postman --only-uri=api/admin
# Exclude test routes
php artisan export:postman --except-uri=test,debug
# Combined options
php artisan export:postman --bearer=token123 --with-docs --only-uri=api/admin,api/client
Configuration
Configure export behavior in config/api-postman.php:
filename- Output filename pattern (supports {timestamp}, {app})output_path- Directory for collection filebase_url- API base URLenable_formdata- Extract validation rules from FormRequestsstructured- Organize routes into folderscrud_folders- Create folders for CRUD actions
make:crud to generate the structure, use fill:request and fill:resource to
auto-fill validation and response mapping, then use make:factory to
create test data, and finally export:postman to test your API
endpoints!
📦 Complete Project Inventory
This section provides a comprehensive overview of all components in the project.
Models (26 Total)
User- User authentication and profileAddress- User addressesDevice- User devices for notificationsWallet- User wallet systemTransaction- Wallet transactionsWithdrawRequest- Withdrawal requests
Product- Products catalogProductTranslation- Product translationsProductVariation- Product variationsProductAttribute- Product attributesVariationAttribute- Variation attributesCategory- Product categoriesCategoryTranslation- Category translationsAttribute- Product attributesAttributeTranslation- Attribute translationsValue- Attribute valuesValueTranslation- Value translations
Country- CountriesCountryTranslation- Country translationsCity- CitiesCityTranslation- City translations
FAQ- Frequently asked questionsFAQTranslation- FAQ translationsStaticPage- Static pages (About, Terms, etc.)StaticPageTranslation- Static page translationsSlider- Homepage slidersShowRoom- Showroom locationsShowRoomTranslation- Showroom translations
Role- User rolesRoleTranslation- Role translationsPermission- PermissionsPermissionTranslation- Permission translations
Enums (11 Total)
UserType- SUPER_ADMIN, SUPERVISOR, CLIENT, GUESTTransactionStatus- Transaction status valuesTransactionType- Transaction type valuesWithdrawStatus- Withdrawal status valuesPaymentMethod- Payment method typesVerificationToken- Verification token typesNotificationScenario- Notification scenariosMediaType- Media file typesFAQType- FAQ categoriesStaticPageType- Static page typesContinent- Continent enumeration
Services Structure
AddressService- Address managementAuthenticationService- Client authenticationFAQService- FAQ retrievalShowRoomService- Showroom listingStaticPageService- Static page retrievalWalletService- Wallet operations
AttributeService- Attribute managementCategoryService- Category managementCityService- City managementCountryService- Country managementFAQService- FAQ managementProductService- Product managementProductVariationService- Variation managementRoleService- Role managementShowRoomService- Showroom managementSliderService- Slider managementStaticPageService- Static page managementSupervisorService- Supervisor managementValueService- Value management
AuthenticationService- Shared auth logicProfileService- Profile management
FCMNotificationChannel- Firebase Cloud MessagingRedisNotificationChannel- Redis notificationsNotifyOrchestrator- Notification orchestrationStateTransitionService- State machine transitionsServiceGenerator- Service class generator
Controllers Structure
AuthenticationController- Admin login/logoutProfileController- Admin profileAttributeController- Attributes CRUDCategoryController- Categories CRUDCityController- Cities CRUDCountryController- Countries CRUDFAQController- FAQs CRUDProductController- Products CRUDProductVariationController- Variations CRUDRoleController- Roles CRUDShowRoomController- Showrooms CRUDSliderController- Sliders CRUDStaticPageController- Static pages CRUDSupervisorController- Supervisors CRUDValueController- Values CRUD
AuthenticationController- Client register/login/verifyProfileController- Client profile managementAddressController- Addresses CRUDWalletController- Wallet operationsFAQController- FAQ listingShowRoomController- Showroom listingStaticPageController- Static page retrieval
GuestController- Public endpoints (countries, cities)
Middleware (6 Total)
DetectUserType- Detects user type from request (global)SetLocale- Sets application locale (global)GzipMiddleware- Response compression (global)CheckUserType- Validates user type (route middleware)ActiveMiddleware- Ensures user is active (route middleware)PermissionMiddleware- Checks user permissions (route middleware)
Routes Structure
/api/admin)- Authentication: login, logout
- Profile: show, update, change-password, settings
- Resources: roles, supervisors, countries, cities, static-pages, faqs, show-rooms, products, product-variations, attributes, values, sliders
/api/client)- Authentication: register, verify, resend-code, login, logout
- Profile: show, update, change-phone, confirm-phone, change-email, confirm-email, resend-code, settings, delete-account
- Wallet: show, charge, withdraw, transactions
- Resources: addresses (full CRUD)
/api/guest)- Public: countries, cities
- Content: faqs (index), static-pages (index), show-rooms (index)
Modules (4 Total)
Media- Media file managementMediaExpress- Express.js integrationNotification- Notification systemVerification- Email/SMS verification
Artisan Commands (8 Total)
make:crud- Generate complete CRUDmake:service- Generate service classmake:enum- Generate enum classexport:postman- Export Postman collectionexport:api-docs- Generate API documentationmake:smart-factory- Generate smart factoryfill:request- Fill FormRequest with rulesfill:resource- Fill Resource with mappings
🏛️ Architectural Patterns & Concepts
This section explains the core architectural patterns, traits, and concepts used throughout the project.
Dashboard Controller Pattern
The DashboardBaseController is an abstract base controller
for all admin/dashboard CRUD operations. It provides a standardized way to handle full CRUD
operations with minimal code.
Key Features
- Automatic Model Resolution - Automatically resolves models from route parameters
- Permission-Based Middleware - Automatically registers permission middleware per action
- Create or Update Mode - Supports
create_or_updatemode with key columns - Request Injection - Automatically injects FormRequest classes
- Resource Transformation - Uses separate resources for index and show
Controller Structure
class ProductController extends DashboardBaseController
{
public function __construct(ProductService $service)
{
$requirements = [
'model' => Product::class,
'with' => ['category', 'media'], // Eager load for index
'load' => ['translations', 'media'], // Eager load for show
'service' => $service,
'storeRequest' => ProductRequest::class,
'updateRequest' => ProductRequest::class,
'indexResource' => ProductResource::class,
'showResource' => ProductDetailsResource::class,
'permissions' => [
'index' => 'index-products',
'show' => 'show-products',
'store' => 'store-products',
'update' => 'update-products',
'destroy' => 'destroy-products',
],
// Optional: Create or Update mode
'storeMode' => 'create_or_update', // or 'create'
'updateOrCreateKeyColumn' => 'slug', // or ['email', 'slug']
];
parent::__construct($requirements);
}
}
🧪 Interactive API Tester
Available Methods
| Method | Purpose | Route |
|---|---|---|
index() |
List all resources with filtering, searching, pagination | GET /api/admin/products |
show($model) |
Show single resource with eager loaded relations | GET /api/admin/products/{id} |
Create or Update Mode
The controller supports an intelligent create_or_update mode
that automatically updates existing records if they match certain criteria, otherwise
creates new ones.
// In controller constructor
'storeMode' => 'create_or_update',
'updateOrCreateKeyColumn' => 'slug', // Single column
// Or multiple columns
'updateOrCreateKeyColumn' => ['email', 'phone'],
// Or translatable fields
'updateOrCreateKeyColumn' => ['en.name', 'ar.name'],
// The controller will:
// 1. Check if a record exists matching the key column(s)
// 2. If exists, update it
// 3. If not, create a new one
How It Works
- When
store()is called, it checksstoreMode - If
create_or_update, it extracts values from request for key columns - For translatable fields, it checks all locales (e.g.,
en.name,ar.name) - Queries the database using
resolveModelByConditions() - If found, passes the model to
service->save()for update - If not found, creates a new model
Dashboard Service Pattern
The DashboardCRUDService is the base service class for all
admin CRUD operations. It provides advanced features like pipelines, filtering, searching,
and sorting.
Key Features
- Pipeline-Based Saving - Uses Laravel Pipeline for save operations
- Advanced Filtering - Supports filtering by any searchable field
- Full-Text Search - Searches across multiple fields including translations
- Smart Sorting - Handles sorting for both regular and translatable fields
- Pagination - Built-in pagination support
- Transaction Support - All save operations wrapped in transactions
- Custom Hooks - beforeSave, afterSave, beforeDestroy, afterDestroy
Service Structure
class ProductService extends DashboardCRUDService
{
public function __construct()
{
parent::__construct(new Product());
}
// Override scoped query for default filtering
protected function scopedQuery(): Builder
{
return $this->model->query()
->where('is_active', true);
}
// Override to add custom scopes
protected function applyScopes(Request $request): void
{
if ($request->has('featured')) {
$this->query->where('is_featured', true);
}
}
// Custom save logic (optional)
public function beforeSave($request, $model)
{
// Modify request data before saving
return [
'custom_field' => 'custom_value',
// ... other fields
];
}
public function afterSave($request, $model)
{
// Post-save operations (e.g., clear cache, send notifications)
}
public function syncRelations($request, $model)
{
// Sync many-to-many relationships
if ($request->has('tags')) {
$model->tags()->sync($request->tags);
}
}
}
Index Method Features
The index() method supports powerful query parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
search |
string | Search across all searchable fields | ?search=laptop |
filters[field] |
mixed | Filter by specific field | ?filters[category_id]=5 |
sort[field] |
asc|desc | Sort by field | ?sort[name]=asc |
per_page |
integer | Items per page | ?per_page=20 |
paginate |
boolean | Return paginated response | ?paginate=true |
columns |
array | Select specific columns | ?columns[]=id&columns[]=name |
Example API Requests
// Search products
GET /api/admin/products?search=laptop
// Filter by category and active status
GET /api/admin/products?filters[category_id]=5&filters[is_active]=1
// Sort by name ascending
GET /api/admin/products?sort[name]=asc
// Combined: search, filter, sort, paginate
GET /api/admin/products?search=laptop&filters[category_id]=5&sort[price]=desc&per_page=20&paginate=true
// Select specific columns
GET /api/admin/products?columns[]=id&columns[]=name&columns[]=price
Client Controller Pattern
The ClientBaseController is similar to
DashboardBaseController but designed for read-only operations (index and show only).
Key Differences
- Read-Only - Only
index()andshow()methods - No Form Requests - No store/update requests needed
- Simpler Structure - Fewer requirements in constructor
- Client-Facing - Designed for authenticated client users
Controller Structure
class FAQController extends ClientBaseController
{
public function __construct(FAQService $service)
{
$requirements = [
'model' => FAQ::class,
'with' => ['category'], // Eager load for index
'load' => ['category'], // Eager load for show
'service' => $service,
'indexResource' => FAQResource::class,
'showResource' => FAQDetailsResource::class,
'permissions' => [
'index' => 'view-faqs',
'show' => 'view-faqs',
],
];
parent::__construct($requirements);
}
}
Pipeline Pattern
The project uses Laravel's Pipeline pattern for processing save operations. This allows for clean, testable, and extensible code.
What is a Pipeline?
A pipeline is a series of processing steps (pipes) that data passes through. Each pipe can modify the data or perform side effects before passing it to the next pipe.
Save Pipeline Flow
When service->save() is called, the following pipeline
executes:
- BeforeSave - Calls
service->beforeSave()hook - FillModel - Fills model with request data
- SaveModel - Saves the model to database
- SyncRelations - Calls
service->syncRelations()hook - AfterSave - Calls
service->afterSave()hook
Pipeline Classes
// app/Pipelines/Base/Save/BeforeSave.php
class BeforeSave
{
public function __invoke(array $payload, \Closure $next)
{
[$request, $model, $service] = $payload;
// Call beforeSave hook if exists
if (method_exists($service, 'beforeSave')) {
$overrides = $service->beforeSave($request, $model);
// Can return modified data to override fillable data
if (is_array($overrides)) {
$request->attributes->set('__fillableData', $overrides);
}
}
return $next([$request, $model, $service]);
}
}
// app/Pipelines/Base/Save/FillModel.php
class FillModel
{
public function __invoke(array $payload, \Closure $next)
{
[$request, $model, $service] = $payload;
// Get fillable data (from beforeSave or validated request)
$fillable = $request->attributes->get('__fillableData', $request->validated());
// Fill the model
$model->fill($fillable);
return $next([$request, $model, $service]);
}
}
// app/Pipelines/Base/Save/SaveModel.php
class SaveModel
{
public function __invoke(array $payload, \Closure $next)
{
[$request, $model, $service] = $payload;
// Save to database
$model->save();
return $next([$request, $model, $service]);
}
}
// app/Pipelines/Base/Save/SyncRelations.php
class SyncRelations
{
public function __invoke(array $payload, \Closure $next)
{
[$request, $model, $service] = $payload;
// Call syncRelations hook if exists
if (method_exists($service, 'syncRelations')) {
$service->syncRelations($request, $model);
}
return $next([$request, $model, $service]);
}
}
// app/Pipelines/Base/Save/AfterSave.php
class AfterSave
{
public function __invoke(array $payload, \Closure $next)
{
[$request, $model, $service] = $payload;
// Call afterSave hook if exists
if (method_exists($service, 'afterSave')) {
$service->afterSave($request, $model);
}
return $next([$request, $model, $service]);
}
}
Customizing the Pipeline
You can override the pipeline in your service:
class ProductService extends DashboardCRUDService
{
protected function getSavePipes(): array
{
return [
\App\Pipelines\Base\Save\BeforeSave::class,
\App\Pipelines\Base\Save\FillModel::class,
\App\Pipelines\Base\Save\SaveModel::class,
\App\Pipelines\Base\Save\SyncRelations::class,
\App\Pipelines\Base\Save\AfterSave::class,
// Add custom pipes here
\App\Pipelines\Products\ProcessImages::class,
\App\Pipelines\Products\UpdateInventory::class,
];
}
}
Creating Custom Pipes
namespace App\Pipelines\Products;
class ProcessImages
{
public function __invoke(array $payload, \Closure $next)
{
[$request, $model, $service] = $payload;
// Process images if provided
if ($request->has('images')) {
// Your image processing logic
$model->processImages($request->images);
}
return $next([$payload]);
}
}
SearchAndFilterAbilities Trait
The SearchAndFilterAbilities trait provides powerful search
and filter capabilities to models.
How to Use
use App\Traits\SearchAndFilterAbilities;
class Product extends Model
{
use SearchAndFilterAbilities;
// Define which fields are searchable
public array $searchableFields = [
'name', // Regular field
'description', // Translatable field
'sku',
'price',
'category_id',
'is_active',
'created_at',
];
// Define translatable fields (if using Translatable trait)
public array $translatedAttributes = [
'name',
'description',
];
}
Trait Methods
| Method | Purpose | Usage |
|---|---|---|
scopeSearch($query, $search) |
Search across all searchable fields | Product::search('laptop')->get() |
scopeFilter($query, $filters) |
Filter by specific fields | Product::filter(['category_id' => 5])->get()
|
How Search Works
- Checks if
$searchableFieldsarray exists on model - For each searchable field:
- If field is translatable → uses
orWhereTranslationLike() - If field is regular → uses
orWhere($field, 'LIKE', "%{$search}%")
- If field is translatable → uses
- Combines all conditions with
OR(searches across all fields)
How Filter Works
- Accepts an array of field => value pairs
- For each filter:
- If field is a date → uses
whereDate() - If model has a scope method (e.g.,
scopeActive()) → uses the scope - Otherwise → uses
where($field, $value)
- If field is a date → uses
- Only filters fields that are in
$searchableFields
Example Usage
// Search across all searchable fields
$products = Product::search('laptop')->get();
// Filter by specific fields
$products = Product::filter([
'category_id' => 5,
'is_active' => true,
])->get();
// Combined search and filter
$products = Product::search('laptop')
->filter(['category_id' => 5])
->get();
// In service (automatic via DashboardCRUDService)
// GET /api/admin/products?search=laptop&filters[category_id]=5
Custom Scopes
You can create custom scope methods that will be automatically used by the filter:
class Product extends Model
{
// This scope will be called when filtering by 'active'
public function scopeActive($query)
{
return $query->where('is_active', true);
}
// This scope will be called when filtering by 'featured'
public function scopeFeatured($query, $value)
{
return $query->where('is_featured', $value);
}
}
// Usage:
Product::filter(['active' => true, 'featured' => 1])->get();
// Automatically uses scopeActive() and scopeFeatured()
HasMedia Trait
The HasMedia trait from the Media module provides seamless
media file management for models.
Key Features
- Polymorphic Relationship - Models can have multiple media files
- Collections - Organize media into collections (e.g., 'image', 'gallery', 'documents')
- Single/Multiple Files - Support for both single files and multiple files per collection
- Hash-Based - Uses media hashes for referencing (from streaming service)
- Automatic Cleanup - Deletes files when model is deleted
- URL Generation - Automatic URL generation for media files
How to Use
use Modules\Media\Traits\HasMedia;
class Product extends Model
{
use HasMedia {
HasMedia::setAttribute as setMediaAttribute;
HasMedia::getAttribute as getMediaAttribute;
}
// Define media collections
public array $media_keys = [
'image' => false, // Single file (false)
'gallery' => true, // Multiple files (true)
'video' => false, // Single video file
];
}
Setting Media
Media is set using hashes (from streaming service upload):
// Single file (image collection)
$product->image = 'abc123def456'; // Media hash
// Multiple files (gallery collection)
$product->gallery = ['hash1', 'hash2', 'hash3'];
// Or comma-separated string
$product->gallery = 'hash1,hash2,hash3';
// Or array of objects
$product->gallery = [
['hash' => 'hash1'],
['hash' => 'hash2'],
];
$product->save(); // Media is attached after save
Getting Media
// Get single file URL
$imageUrl = $product->image; // Returns: "http://domain.com/storage/path/to/file.jpg"
// Or null if no media
// Get multiple file URLs
$galleryUrls = $product->gallery; // Returns: ["url1", "url2", "url3"]
// Or null if no media
// Get media with IDs
$imageWithId = $product->getAttributeWithId('image');
// Returns: ['id' => 123, 'url' => 'http://...']
$galleryWithIds = $product->getAttributeWithId('gallery');
// Returns: [
// ['id' => 123, 'url' => 'http://...'],
// ['id' => 124, 'url' => 'http://...'],
// ]
// Get all media URLs
$allMedia = $product->media_urls;
// Returns: [
// 'image' => 'http://...',
// 'gallery' => ['http://...', 'http://...'],
// 'video' => null,
// ]
// Access relationship directly
$media = $product->media; // Collection of Media models
$imageMedia = $product->media()->where('collection', 'image')->get();
How It Works
- Setting Media:
- When you set
$product->image = 'hash', it's stored in$pendingMediaHashes - On
save(), thesavedevent fires - The trait's
bootHasMedia()method handles the event - Media records are attached via
attachMediaByHash()
- When you set
- Getting Media:
- When you access
$product->image, it checks if it's a media key - Queries the
mediarelationship - Returns URL(s) for the collection
- When you access
- Deleting Media:
- When model is deleted,
deletingevent fires - All associated media files are deleted from storage
- Media records are force deleted from database
- When model is deleted,
Integration with Translatable
When using both HasMedia and Translatable traits, you need to handle attribute resolution:
class Product extends Model
{
use HasMedia {
HasMedia::setAttribute as setMediaAttribute;
HasMedia::getAttribute as getMediaAttribute;
}
use Translatable {
Translatable::setAttribute as setTranslatableAttribute;
Translatable::getAttribute as getTranslatableAttribute;
}
public function setAttribute($key, $value)
{
// Check media first
if ($this->isMediaKey($key)) {
return $this->setMediaAttribute($key, $value);
}
// Then check translatable
return $this->setTranslatableAttribute($key, $value);
}
public function getAttribute($key)
{
// Check media first
if ($this->isMediaKey($key)) {
return $this->getMediaAttribute($key);
}
// Then check translatable
return $this->getTranslatableAttribute($key);
}
}
Database Transactions
All save operations in DashboardCRUDService are wrapped in
database transactions to ensure data integrity.
How It Works
public function save($request, $id = null, ?Closure $customSaveLogic = null, $resource = null, $load = []): JsonResponse
{
DB::beginTransaction();
try {
// Resolve or create model
if ($id instanceof Model) {
$this->model = $id;
} else {
$this->model = $id ? $this->resolveModelOrFail($id) : new $this->model;
}
// Run save pipeline or custom logic
if ($customSaveLogic !== null) {
$customSaveLogic($request, $this->model);
} else {
$this->runSavePipeline($request);
}
// Commit transaction
DB::commit();
// Load relations if needed
if (!empty($load)) {
$this->model->load($load);
}
return json($resource ? $resource::make($this->model) : null, __('Data saved successfully'));
} catch (\Exception $e) {
// Rollback on error
DB::rollBack();
return $this->handleException($e);
}
}
Benefits
- Atomicity - All operations succeed or fail together
- Data Integrity - No partial saves
- Error Recovery - Automatic rollback on exceptions
- Consistency - Database remains in consistent state
Manual Transactions
You can also use transactions manually in your services:
use Illuminate\Support\Facades\DB;
class ProductService extends DashboardCRUDService
{
public function complexOperation($data)
{
DB::beginTransaction();
try {
// Create product
$product = Product::create($data);
// Create variations
foreach ($data['variations'] as $variation) {
$product->variations()->create($variation);
}
// Update inventory
$this->updateInventory($product);
// Send notifications
$this->notifyAdmins($product);
DB::commit();
return $product;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}
Fillable vs Guarded
Laravel uses $fillable or $guarded to control mass assignment.
Guarded Approach (Used in This Project)
This project uses protected $guarded = []; which allows mass
assignment of all fields except those explicitly guarded.
$guarded = []
means ALL fields are mass assignable. Always validate input using FormRequest classes!
class Product extends Model
{
// Allow mass assignment of all fields
protected $guarded = [];
// This is safe because:
// 1. FormRequest validation ensures only valid fields are submitted
// 2. Only validated data reaches the model
// 3. Sensitive fields (like 'is_admin') are not in the request
}
Fillable Approach (Alternative)
class Product extends Model
{
// Only these fields can be mass assigned
protected $fillable = [
'name',
'description',
'price',
'stock',
'category_id',
// ... other safe fields
];
// Fields NOT in fillable cannot be mass assigned
// Even if they're in the request, they'll be ignored
}
Best Practice
This project uses $guarded = [] because:
- FormRequest validation provides the security layer
- More flexible - don't need to update fillable array for every new field
- Validation rules in FormRequest define what's allowed
- Easier to maintain with many models
Translatable Attributes
The project uses astrotomic/laravel-translatable for
multi-language support.
How to Use
use Astrotomic\Translatable\Translatable;
use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract;
class Product extends Model implements TranslatableContract
{
use Translatable {
Translatable::setAttribute as setTranslatableAttribute;
Translatable::getAttribute as getTranslatableAttribute;
}
// Define which fields are translatable
public array $translatedAttributes = [
'name',
'description',
'slug',
];
}
Database Structure
Translatable models require a translation table:
// Main table: products
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->decimal('price');
$table->integer('stock');
$table->timestamps();
});
// Translation table: product_translations
Schema::create('product_translations', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')->constrained()->onDelete('cascade');
$table->string('locale')->index();
$table->string('name');
$table->text('description');
$table->string('slug');
$table->unique(['product_id', 'locale']);
});
Setting Translations
// Set current locale translation
$product->name = 'Laptop';
$product->description = 'A powerful laptop';
// Set specific locale
$product->translate('en')->name = 'Laptop';
$product->translate('ar')->name = 'لابتوب';
// Set multiple locales at once
$product->translate('en')->name = 'Laptop';
$product->translate('en')->description = 'A powerful laptop';
$product->translate('ar')->name = 'لابتوب';
$product->translate('ar')->description = 'لابتوب قوي';
$product->save();
Getting Translations
// Get current locale (based on app()->getLocale())
$name = $product->name; // Returns name in current locale
// Get specific locale
$nameEn = $product->translate('en')->name;
$nameAr = $product->translate('ar')->name;
// Check if translation exists
if ($product->hasTranslation('en')) {
$name = $product->translate('en')->name;
}
// Get all translations
$translations = $product->translations; // Collection of all translations
Querying Translations
// Search in translations
Product::whereTranslation('name', 'Laptop')->get();
Product::whereTranslationLike('name', '%laptop%')->get();
// Order by translated field
Product::orderByTranslation('name', 'asc')->get();
// Get products with specific locale translation
Product::withTranslation('en')->get();
// Get products with all translations
Product::with('translations')->get();
Configuration
Configure locales in config/translatable.php:
return [
'locales' => [
'en',
'ar',
'fr',
// ... other locales
],
'locale_separator' => '-',
'fallback_locale' => 'en',
'fallback_any_locale' => false,
];
🛠️ Helpers & Utilities
Helper functions are defined in app/Helpers/helpers.php.
Available Helpers
json()
Standardized JSON response helper.
json($data, $message, $status, $headerStatus)
// Examples:
json($user, 'User retrieved', 'success', 200);
json(null, 'Error occurred', 'fail', 400);
log_activity()
Structured logging helper.
log_activity('class_name', $data, 'info|error|warning');
// Example:
log_activity('UserService', ['action' => 'create', 'user_id' => 1], 'info');
throttle()
Rate limiting helper.
throttle($key, $maxAttempts, $decaySeconds, $field);
// Example:
throttle('login:' . $request->ip(), 5, 60);
generate_unique_code()
Generate unique codes for models.
generate_unique_code($length, $model, $column, $type, $letter_type);
// Example:
$code = generate_unique_code(8, Product::class, 'code', 'numbers');
detectLoginField()
Detect if login input is email or phone.
$field = detectLoginField($login); // Returns 'email' or 'phone'
getUserType()
Get user types array from request.
$types = getUserType($request); // Returns array of user types
resolveCoords()
Resolve coordinates from various sources (request, user, IP).
[$lat, $lng] = resolveCoords();
isApi()
Check if request is an API request.
if (isApi($request)) {
// API response
}
uniqueTranslationRule()
Generate unique validation rule for translatable fields.
Rule::unique('name')->where(function ($query) {
return $query->where('locale', $locale);
});
🛡️ Middleware
Middleware is registered in bootstrap/app.php.
Global Middleware
Applied to all requests:
DetectUserType- Detects user type from requestSetLocale- Sets application localeHandleCors- CORS handlingGzipMiddleware- Response compression
Route Middleware
Available as aliases:
| Alias | Class | Purpose |
|---|---|---|
user_type |
CheckUserType |
Check if user has required type |
active |
ActiveMiddleware |
Ensure user is active |
permission |
PermissionMiddleware |
Check user permissions |
set_locale |
SetLocale |
Set application locale |
cors |
HandleCors |
CORS handling |
Usage Examples
// Check user type
Route::middleware(['auth:api', 'user_type:admin'])->group(function () {
// Admin routes
});
// Check if user is active
Route::middleware(['auth:api', 'active'])->group(function () {
// Active user routes
});
// Check permissions
Route::middleware(['auth:api', 'permission:manage-products'])->group(function () {
// Permission-protected routes
});
💻 Development Workflow
Initial Setup
# Install PHP dependencies
composer install
# Install Node dependencies
npm install
# Copy environment file
cp .env.example .env
# Generate application key
php artisan key:generate
# Run migrations
php artisan migrate
# Seed database (if seeders exist)
php artisan db:seed
# Install streaming service dependencies
cd streaming-service
npm install
cp .env.example .env
cd ..
Development Commands
Start Development Server
# Start all services (Laravel, Queue, Logs, Vite)
composer dev
# Or individually:
php artisan serve # Laravel server
php artisan queue:listen # Queue worker
php artisan pail # Log viewer
npm run dev # Vite dev server
Code Quality
# Format code with Pint
./vendor/bin/pint
# Run tests
php artisan test
Creating New Features
- Create/update migration
- Create model (with translations if needed)
- Generate CRUD using
php artisan make:crud - Customize service logic
- Add routes
- Test endpoints
Database Migrations
# Create migration
php artisan make:migration create_products_table
# Run migrations
php artisan migrate
# Rollback last migration
php artisan migrate:rollback
# Refresh migrations
php artisan migrate:refresh
Model Translations
For translatable models, use the astrotomic/laravel-translatable
package:
use Astrotomic\Translatable\Translatable;
use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract;
class Product extends Model implements TranslatableContract
{
use Translatable;
public $translatedAttributes = ['name', 'description'];
// Access translations
$product->translate('en')->name;
$product->name; // Current locale
}
✨ Best Practices
Code Organization
- ✅ Keep business logic in Services, not Controllers
- ✅ Use Form Requests for validation
- ✅ Use Resources for API response formatting
- ✅ Follow PSR-12 coding standards
- ✅ Use Type Hints and Return Types
Laravel 12 Conventions
- ❌ Don't create
app/Http/Kernel.php- usebootstrap/app.php - ❌ Don't create
app/Console/Kernel.php- useroutes/console.php - ✅ Register service providers in
bootstrap/providers.php - ✅ Define scheduled tasks in
bootstrap/app.php - ✅ Use Tailwind CSS for new Blade pages, not Bootstrap
Service Layer Pattern
// ✅ Good: Business logic in service
class ProductService {
public function create(array $data): Product {
// Validation, business rules, etc.
return Product::create($data);
}
}
// ❌ Bad: Business logic in controller
class ProductController {
public function store(Request $request) {
// Don't put business logic here
Product::create($request->all());
}
}
Error Handling
Error handling is centralized in bootstrap/app.php:
- Validation errors return 422 with formatted messages
- Authentication errors return 401
- Authorization errors return 403
- Model not found returns 404
- All errors are logged using
log_activity()
API Response Format
Always use the json() helper for consistent responses:
// Success
return json($data, 'Operation successful');
// Error
return json(null, 'Error message', 'fail', 400);
Database Transactions
DB::beginTransaction();
try {
// Multiple operations
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
Testing
- Write feature tests for API endpoints
- Test service layer logic
- Use factories for test data
- Mock external services
Security
- ✅ Always validate input using Form Requests
- ✅ Use middleware for authentication/authorization
- ✅ Sanitize user input
- ✅ Use parameterized queries (Eloquent does this automatically)
- ✅ Implement rate limiting for sensitive endpoints
- ✅ Use HTTPS in production
📚 Quick Reference
A handy cheat sheet for common commands, patterns, and snippets.
⚡ Artisan Commands
php artisan make:crud Product
Generates: Controller, Service, Request, Resource, Routes, Migration, Factory
php artisan make:service ProductService
php artisan make:enum ProductStatus
php artisan export:postman
php artisan export:api-docs
php artisan fill:request ProductRequest
php artisan fill:resource ProductResource
🔧 Troubleshooting
Common issues and their solutions.
Solution:
composer dump-autoload
php artisan optimize:clear
Solution:
- Check
storage/app/publicpermissions - Run
php artisan storage:link - Verify streaming service is running
- Check CORS configuration
Solution:
- Verify
JWT_SECRETin.env - Check token expiration time
- Ensure
auth:apimiddleware is applied - Verify user has required role/permissions
Solution:
php artisan route:clear
php artisan route:cache
php artisan route:list // Check if route exists
Solution:
php artisan migrate:fresh
php artisan migrate:refresh
php artisan db:seed
Solution:
- Register in
bootstrap/providers.php(Laravel 11+) - Run
php artisan config:clear - Check service provider namespace
Solution:
- Check
config/cors.phpconfiguration - Verify allowed origins in
.env - Ensure CORS middleware is enabled in
bootstrap/app.php
Solution:
- Ensure model uses
Translatabletrait - Check
$translatedAttributesarray - Verify locale is set:
app()->setLocale('en') - Check translation tables exist
💾 Code Snippets Library
Reusable code examples for common tasks. Click to copy!
class ProductController extends DashboardBaseController {
public function __construct(ProductService $service) {
$requirements = [
'model' => Product::class,
'with' => ['category', 'media'],
'load' => ['translations', 'media'],
'service' => $service,
'storeRequest' => ProductRequest::class,
'updateRequest' => ProductRequest::class,
'indexResource' => ProductResource::class,
'showResource' => ProductDetailsResource::class,
'permissions' => [
'index' => 'index-products',
'show' => 'show-products',
'store' => 'store-products',
'update' => 'update-products',
'destroy' => 'destroy-products',
],
];
parent::__construct($requirements);
}
}
class ProductService extends DashboardCRUDService {
protected $model = Product::class;
protected function beforeSave($data) {
// Custom logic before saving
if (isset($data['slug'])) {
$data['slug'] = Str::slug($data['slug']);
}
return $data;
}
protected function afterSave($model, $data) {
// Custom logic after saving
if (isset($data['media'])) {
$model->syncMedia($data['media']);
}
}
}
use Modules\Media\Traits\HasMedia;
use App\Traits\SearchAndFilterAbilities;
use Astrotomic\Translatable\Translatable;
class Product extends Model {
use HasMedia, SearchAndFilterAbilities, Translatable;
protected $searchableFields = ['name', 'description'];
public $translatedAttributes = ['name', 'description'];
protected $fillable = ['slug', 'price', 'stock'];
}
class ProductRequest extends FormRequest {
public function rules(): array {
return [
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
'category_id' => 'required|exists:categories,id',
'media' => 'nullable|array',
'media.*' => 'file|mimes:jpeg,png,jpg|max:2048',
];
}
}
class ProductResource extends JsonResource {
public function toArray($request): array {
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'stock' => $this->stock,
'category' => new CategoryResource($this->whenLoaded('category')),
'media' => MediaResource::collection($this->whenLoaded('media')),
'created_at' => $this->created_at,
];
}
}
// Admin routes
Route::prefix('admin')
->middleware(['auth:api', 'role:admin'])
->group(function () {
Route::apiResource('products', ProductController::class);
});
// Client routes (read-only)
Route::prefix('client')
->middleware(['auth:api', 'role:client'])
->group(function () {
Route::get('products', [ProductController::class, 'index']);
Route::get('products/{id}', [ProductController::class, 'show']);
});
// Search and filter
$products = Product::query()
->search($request->input('search'))
->filter($request->input('filters'))
->sort($request->input('sort'))
->with(['category', 'media'])
->paginate($request->input('per_page', 15));
// With translations
$products = Product::with('translations')
->whereTranslation('name', 'like', '%laptop%')
->get();
// Upload single file
$media = uploadMedia($request->file('image'), 'products');
// Upload to collection
$media = uploadMedia($request->file('image'), 'products', 'gallery');
// Attach to model
$product->attachMedia($media);
// Get media
$product->getMedia('gallery');
$product->getFirstMedia('gallery');
🌐 API Explorer
Interactive explorer for all API endpoints. Filter by method, prefix, or search by name. Click "Test" to send requests!
🚀 Deployment Guide
Step-by-step guide for deploying your Laravel 12 application.
Prerequisites
- PHP 8.2 or higher
- Composer
- Node.js and NPM (for frontend assets)
- MySQL/PostgreSQL database
- Web server (Nginx/Apache)
- SSL certificate (for HTTPS)
Step 1: Server Setup
# Update system
sudo apt update && sudo apt upgrade -y
# Install PHP and extensions
sudo apt install php8.2 php8.2-fpm php8.2-mysql php8.2-xml php8.2-mbstring php8.2-curl php8.2-zip
# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
Step 2: Application Deployment
# Clone repository
git clone your-repo-url /var/www/your-app
cd /var/www/your-app
# Install dependencies
composer install --optimize-autoloader --no-dev
npm install
npm run build
# Set permissions
sudo chown -R www-data:www-data /var/www/your-app
sudo chmod -R 755 /var/www/your-app
sudo chmod -R 775 storage bootstrap/cache
Step 3: Environment Configuration
# Copy environment file
cp .env.example .env
# Generate application key
php artisan key:generate
# Set environment variables
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
# Database configuration
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_password
# JWT configuration
JWT_SECRET=your-jwt-secret-key
# Run migrations
php artisan migrate --force
# Cache configuration
php artisan config:cache
php artisan route:cache
php artisan view:cache
Step 4: Nginx Configuration
server {
listen 80;
server_name yourdomain.com;
root /var/www/your-app/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Step 5: Queue & Scheduler Setup
# Install Supervisor for queue workers
sudo apt install supervisor
# Create supervisor config
sudo nano /etc/supervisor/conf.d/laravel-worker.conf
# Add to config:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/worker.log
# Start supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
# Setup cron for scheduler
* * * * * cd /var/www/your-app && php artisan schedule:run >> /dev/null 2>&1
Step 6: Streaming Service Deployment
# Navigate to streaming service
cd streaming-service
# Install dependencies
npm install --production
# Setup PM2 for process management
npm install -g pm2
# Start service
pm2 start src/index.js --name media-streaming
# Save PM2 configuration
pm2 save
pm2 startup
Post-Deployment Checklist
- ✅ Verify all environment variables are set
- ✅ Test API endpoints
- ✅ Verify media uploads work
- ✅ Check queue workers are running
- ✅ Verify scheduled tasks execute
- ✅ Test authentication flow
- ✅ Enable HTTPS/SSL
- ✅ Setup monitoring and logging
- ✅ Configure backups
⚡ Performance Optimization
Best practices and tips for optimizing your Laravel application performance.
Database Optimization
// ❌ Bad: N+1 query problem
$products = Product::all();
foreach ($products as $product) {
echo $product->category->name; // Query for each product
}
// ✅ Good: Eager loading
$products = Product::with('category')->get();
foreach ($products as $product) {
echo $product->category->name; // No additional queries
}
// Add indexes for frequently queried columns
Schema::table('products', function (Blueprint $table) {
$table->index('category_id');
$table->index('slug');
$table->index(['status', 'created_at']);
});
// Cache expensive queries
$products = Cache::remember('products_list', 3600, function () {
return Product::with('category')->get();
});
// Cache with tags (Redis)
Cache::tags(['products'])->remember('products_list', 3600, function () {
return Product::all();
});
Application Optimization
Enable OPcache in php.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 # Set to 1 in development
// Cache API responses
Route::middleware('cache.headers:public;max_age=3600')->group(function () {
Route::get('products', [ProductController::class, 'index']);
});
// Or in controller
return Cache::remember('products_index_' . $page, 3600, function () {
return ProductResource::collection(Product::paginate(15));
});
# Generate optimized autoloader
composer install --optimize-autoloader --no-dev
# Cache configuration
php artisan config:cache
php artisan route:cache
php artisan view:cache
Frontend Optimization
# Build optimized assets
npm run build
# Or for production
npm run build -- --mode production
Already enabled via GzipMiddleware in this base project.
Queue & Background Jobs
// Dispatch heavy operations to queue
ProcessMediaJob::dispatch($media);
// Use database queue driver for small apps
QUEUE_CONNECTION=database
// Use Redis for better performance
QUEUE_CONNECTION=redis
Monitoring & Profiling
- Use Laravel Telescope for debugging (development only)
- Monitor query performance with
DB::enableQueryLog() - Use Laravel Debugbar for development
- Setup application monitoring (New Relic, Sentry, etc.)
- Monitor server resources (CPU, memory, disk)
🔒 Security Best Practices
Comprehensive security checklist for your Laravel application.
Authentication & Authorization
- ✅ Use strong JWT secrets (minimum 32 characters)
- ✅ Implement token expiration and refresh tokens
- ✅ Use role-based access control (RBAC)
- ✅ Implement permission checks on all protected routes
- ✅ Validate user status (active/inactive) before authentication
- ✅ Implement rate limiting on authentication endpoints
- ✅ Use secure password hashing (bcrypt/argon2)
- ✅ Implement account lockout after failed attempts
Input Validation
- ✅ Always validate input using Form Requests
- ✅ Sanitize user input before processing
- ✅ Use parameterized queries (Eloquent does this automatically)
- ✅ Validate file uploads (type, size, MIME type)
- ✅ Implement CSRF protection for web routes
- ✅ Validate and sanitize file names
- ✅ Use whitelist validation instead of blacklist
API Security
- ✅ Enable CORS with specific allowed origins
- ✅ Implement API rate limiting
- ✅ Use HTTPS for all API communications
- ✅ Validate and sanitize all API inputs
- ✅ Implement proper error handling (don't expose sensitive info)
- ✅ Use API versioning
- ✅ Log security events (failed logins, unauthorized access)
Environment & Configuration
- ✅ Never commit
.envfile - ✅ Use strong, unique secrets for each environment
- ✅ Set
APP_DEBUG=falsein production - ✅ Use environment-specific configuration
- ✅ Rotate secrets regularly
- ✅ Use secure session configuration
- ✅ Enable secure cookies
Database Security
- ✅ Use parameterized queries (Eloquent default)
- ✅ Limit database user permissions
- ✅ Use database encryption for sensitive data
- ✅ Implement database backups
- ✅ Use connection encryption (SSL/TLS)
- ✅ Regularly update database software
File & Media Security
- ✅ Validate file types and sizes
- ✅ Store files outside web root when possible
- ✅ Scan uploaded files for malware
- ✅ Use secure file names (avoid user-provided names)
- ✅ Implement file access controls
- ✅ Set proper file permissions (644 for files, 755 for directories)
Server Security
- ✅ Keep server software updated
- ✅ Use firewall to restrict access
- ✅ Implement fail2ban for brute force protection
- ✅ Use SSH keys instead of passwords
- ✅ Disable unnecessary services
- ✅ Regular security audits
- ✅ Monitor server logs
Code Security
- ✅ Keep dependencies updated (
composer update) - ✅ Review and audit third-party packages
- ✅ Use dependency scanning tools
- ✅ Implement code reviews
- ✅ Follow secure coding practices
- ✅ Regular security testing