kalimeromk / slim-mvc-starter
A production-ready Slim 4 MVC Starter Kit with Eloquent ORM, database migrations, JWT authentication, role-based permissions, and modular architecture
Requires
- php: >=8.3
- ext-fileinfo: *
- eftec/bladeone: ^4.19
- illuminate/database: ^12.0
- illuminate/pagination: ^12.38
- illuminate/support: ^12.26
- illuminate/translation: ^12.7
- illuminate/validation: ^12.7
- monolog/monolog: ^3.0
- php-di/slim-bridge: ^3.4
- phpmailer/phpmailer: ^6.9
- predis/predis: ^3.2
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
- slim/csrf: ^1.5
- slim/http: ^1.3
- slim/psr7: ^1.5
- slim/slim: ^4.0
- symfony/console: ^6.0
- vlucas/phpdotenv: ^5.6
- zircote/swagger-php: ^6.0
Requires (Dev)
- fakerphp/faker: ^1.24
- laravel/pint: ^1.21
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.4
- rector/rector: ^2.0.19
README
A modern, production-ready starter kit for building web applications with Slim Framework 4, featuring Eloquent ORM, Blade templating, comprehensive authentication, and a robust testing suite.
๐ Features
- MVC Architecture - Clean separation of concerns with Slim 4
- Eloquent ORM - Laravel's powerful database toolkit
- Blade Templating - Lightweight BladeOne engine with custom directives (@auth, @guest, @csrf, @method, @can, @role)
- Authentication System - JWT-based API auth and session-based web auth
- Authorization - Role and permission-based access control with middleware and policies
- Form Request Validation - Laravel-style validation with automatic error handling
- Rate Limiting - Built-in protection against brute force attacks
- CORS Support - Configurable CORS middleware for API endpoints
- Error Logging - PSR-3 compatible logging with Monolog
- API Resources - Consistent API response formatting with Resource classes
- Consistent API Responses - Standardized JSON responses with enums and helper methods
- Repository Pattern - Clean data access layer abstraction for better testability and maintainability
- Exception Handling - Custom exception classes with centralized exception handling middleware
- Caching Layer - Multi-driver cache system (File, Redis, Null) with helper functions
- Cookie Helper - Encrypted cookie management with AES-256-CBC
- API Query Builder - Filter, sort, search with operators and pagination
- Testing Suite - Comprehensive test coverage with PHPUnit (286+ tests, 599+ assertions)
- CLI Commands - Artisan-like commands for scaffolding (modules, models, controllers, requests)
- Modular Architecture - Feature-based module organization for better scalability
- Automatic Dependency Registration - Dependencies automatically registered when creating modules
- PHP 8.4 Ready - Modern PHP features and Rector automated refactoring
- Docker Ready - Complete Docker setup for development
- Environment Validation - Fail-fast configuration validation
- Auto-Discovery - Automatic DI registration with caching
- Generic CRUD - Reusable CRUD with 87% less code
๐ Requirements
- PHP >= 8.4
- Composer
- Docker & Docker Compose (for development)
- MySQL/MariaDB (or SQLite for testing)
๐ ๏ธ Installation
-
Clone the repository:
git clone https://github.com/KalimeroMK/Slim4MVC cd Slim4MVC -
Install dependencies:
composer install
-
Configure environment:
cp .env.example .env
Edit
.envand configure:- Database credentials
- JWT_SECRET (generate a strong secret key)
- Mail settings
- CORS origins (if needed)
- Cache driver (file/redis) and Redis settings (if using Redis)
- Cookie settings (encryption, secure flags for production)
-
Start Docker containers:
docker-compose up -d
-
Clear cache (if you encounter namespace errors):
php clear-cache.php docker compose restart
-
Run migrations:
php run_migrations.php
-
Seed database (optional):
php slim seed:database
-
Setup storage permissions:
# Using PHP script (recommended) php setup-storage.php # Or using bash script ./setup-storage.sh # Or manually chmod -R 775 storage chown -R www-data:www-data storage # Adjust user/group as needed
The application will be available at http://localhost:81
๐ Project Structure
The project uses a modular architecture where each feature is organized as an independent module:
โโโ app/
โ โโโ Console/ # CLI Commands
โ โ โโโ Commands/ # Console commands (make:module, make:request, etc.)
โ โโโ Modules/ # Feature modules
โ โ โโโ Core/ # Core module (base classes, middleware, support)
โ โ โ โโโ Application/
โ โ โ โ โโโ Actions/ # Core actions (Auth actions)
โ โ โ โ โโโ DTOs/ # Core DTOs
โ โ โ โ โโโ Enums/ # Enums (HttpStatusCode, ApiResponseStatus)
โ โ โ โ โโโ Interfaces/
โ โ โ โโโ Infrastructure/
โ โ โ โโโ Events/ # Event system
โ โ โ โโโ Exceptions/ # Custom exceptions
โ โ โ โโโ Http/
โ โ โ โ โโโ Controllers/ # Base controllers
โ โ โ โ โโโ Middleware/ # Middleware
โ โ โ โ โโโ Requests/ # Base FormRequest
โ โ โ โ โโโ Resources/ # Base Resource
โ โ โ โโโ Jobs/ # Queue jobs
โ โ โ โโโ Policies/ # Base Policy
โ โ โ โโโ Providers/ # Service providers
โ โ โ โโโ Queue/ # Queue system
โ โ โ โโโ Repositories/ # Base repositories
โ โ โ โโโ Support/ # Helper classes (Auth, Logger, Mailer, Cookie)
โ โ โ โโโ Cache/ # Cache system (File, Redis, Null drivers)
โ โ โ โโโ Query/ # API Query Builder (filter, sort, search)
โ โ โ โโโ View/ # Blade integration
โ โ โโโ Auth/ # Authentication module
โ โ โ โโโ Application/
โ โ โ โ โโโ Actions/Auth/ # Login, Register, PasswordRecovery, etc.
โ โ โ โ โโโ DTOs/Auth/ # Auth DTOs
โ โ โ โ โโโ Interfaces/Auth/
โ โ โ โโโ Infrastructure/
โ โ โ โโโ Http/
โ โ โ โ โโโ Controllers/
โ โ โ โ โ โโโ Api/ # API AuthController (JWT)
โ โ โ โ โ โโโ Web/ # Web AuthController (Session)
โ โ โ โ โโโ Requests/Auth/
โ โ โ โโโ Providers/ # AuthServiceProvider
โ โ โ โโโ Routes/ # API and Web routes
โ โ โโโ User/ # User module
โ โ โโโ Role/ # Role module
โ โ โโโ Permission/ # Permission module
โ โโโ Support/ # Legacy support (backward compatibility)
โโโ bootstrap/ # Application bootstrap files
โ โโโ modules.php # Module loader
โ โโโ modules-register.php # Module registration
โโโ database/
โ โโโ migrations/ # Database migrations
โ โโโ seed/ # Database seeders
โโโ public/ # Web root
โโโ resources/
โ โโโ views/ # Blade templates
โ โโโ lang/ # Translation files
โโโ routes/ # Main route files (web.php, api.php)
โโโ stubs/ # Code generation stubs
โ โโโ Module/ # Module structure stubs
โโโ storage/
โ โโโ cache/ # Application cache
โ โ โโโ data/ # File cache storage
โ โ โโโ view/ # Blade compiled views
โ โโโ logs/ # Application logs
โ โโโ queue/ # Queue storage (file driver)
โ โโโ sessions/ # Session storage (file driver)
โโโ tests/ # PHPUnit tests
โโโ Unit/ # Unit tests
โโโ Feature/ # Feature tests
Module Structure
Each module follows a consistent structure:
app/Modules/Example/
โโโ Application/ # Business logic layer
โ โโโ Actions/ # Business logic actions
โ โโโ DTOs/ # Data Transfer Objects
โ โโโ Services/ # Service classes
โ โโโ Interfaces/ # Service contracts
โโโ Infrastructure/ # Infrastructure layer
โ โโโ Models/ # Eloquent models
โ โโโ Repositories/ # Data access layer
โ โโโ Http/
โ โ โโโ Controllers/ # Request handlers
โ โ โโโ Requests/ # Form request validation
โ โ โโโ Resources/ # API resource transformers
โ โโโ Providers/ # Service providers
โ โโโ Routes/ # Module routes (api.php, web.php)
โโโ Exceptions/ # Module-specific exceptions
โโโ Observers/ # Eloquent observers
โโโ Policies/ # Authorization policies
๐ฏ Usage
Creating Modules
The recommended way to create new features is using the modular architecture:
# Create a new module php slim make:module Product # Create module with custom model name php slim make:module Product --model=Item # Create module with migration php slim make:module Product --migration
This will automatically create:
- Complete module structure (Application, Infrastructure layers)
- Actions (Create, Update, Delete, Get, List)
- DTOs (Create, Update)
- Interfaces (CreateActionInterface, UpdateActionInterface)
- Model and Repository
- Controller with CRUD methods
- Form Requests (Create, Update)
- API Resource
- Policy
- Service Provider
- API Routes
- Automatic dependency registration in
bootstrap/dependencies.php - Automatic module registration in
bootstrap/modules-register.php
Example:
php slim make:module Blog --model=Post --migration
This creates:
app/Modules/Blog/with complete structureCreatePostAction,UpdatePostAction, etc.PostRepositoryautomatically registered in Service ProviderCreatePostActionInterfaceandUpdatePostActionInterfaceautomatically registered inbootstrap/dependencies.php- Module automatically registered in
bootstrap/modules-register.php
Creating Models and Migrations
# Create a model php slim make:model Product # Create a model with migration php slim make:model Product -m
Creating Controllers
php slim make:controller Product
This will create:
- Controller
- Actions (Create, Update, Delete, Get, List)
- DTOs (Create, Update)
- Form Requests (Create, Update)
Creating Form Requests
# Create a basic request php slim make:request User/CreateUserRequest # Create request with auto-generated rules from model php slim make:request User/CreateUserRequest --model=User # Create update request with model php slim make:request User/UpdateUserRequest --model=User --type=update # Short syntax php slim make:request User/CreateUserRequest -m User -t create
The --model option automatically generates validation rules based on the model's $fillable fields and $casts.
Running Migrations
# Run migrations php run_migrations.php # Rollback last migration php run_migrations.php rollback # Refresh all migrations php run_migrations.php refresh
Available Commands
# Module creation php slim make:module <ModuleName> [--model=<ModelName>] [--migration] # Model creation php slim make:model <ModelName> [-m] # Controller creation php slim make:controller <ControllerName> # Request creation php slim make:request <Namespace/RequestName> [--model=<ModelName>] [--type=<create|update>] # Database seeding php slim seed:database # Queue processing php slim queue:work [--stop-when-empty] [--max-jobs=<number>] # Cache management php slim cache:clear [--tag=<tag>] [--driver=<driver>] # List all routes php slim list-routes
Running Tests
# Run all tests ./vendor/bin/phpunit # or composer test # Run with detailed output ./vendor/bin/phpunit --testdox # Run specific test suite ./vendor/bin/phpunit tests/Unit
๐ Authentication
The application includes a dedicated Auth module that handles both API and Web authentication.
Auth Module
The Auth module (app/Modules/Auth/) provides:
- API Authentication - JWT-based authentication for API endpoints
- Web Authentication - Session-based authentication for web routes
- Password Recovery - Token-based password reset functionality
- Event-driven - Dispatches events for user registration and password reset
API Authentication (JWT)
The API uses JWT tokens for authentication. After successful login, include the token in requests:
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:81/api/v1/users
Web Authentication (Session)
Web routes use session-based authentication. The AuthWebMiddleware handles authentication for web routes.
๐ก๏ธ Authorization
Using Middleware
Role-based access:
$app->get('/admin/dashboard', [DashboardController::class, 'index']) ->add(new CheckRoleMiddleware()) ->setArgument('roles', 'admin'); // Multiple roles (user needs one of these) $app->get('/reports', [ReportController::class, 'index']) ->add(new CheckRoleMiddleware()) ->setArgument('roles', ['admin', 'manager']);
Permission-based access:
$app->post('/users', [UserController::class, 'store']) ->add(new CheckPermissionMiddleware()) ->setArgument('permissions', 'create-users'); // Multiple permissions $app->put('/posts/{id}', [PostController::class, 'update']) ->add(new CheckPermissionMiddleware()) ->setArgument('permissions', ['edit-posts', 'publish-posts']);
Using Policies
Create a policy:
// app/Policies/PostPolicy.php class PostPolicy extends Policy { public function update(User $user, Post $post): bool { return $user->id === $post->user_id || $user->hasPermission('edit-posts'); } }
Use in controller:
public function update(Request $request, Response $response, int $id): Response { $post = Post::find($id); if (!$this->authorize('update', $post)) { return $this->respondUnauthorized(); } // Update logic... }
โก Rate Limiting
Rate limiting is automatically applied to authentication endpoints (5 requests per minute). You can apply it to any route:
use App\Modules\Core\Infrastructure\Http\Middleware\RateLimitMiddleware; $rateLimit = new RateLimitMiddleware(10, 60); // 10 requests per 60 seconds $app->post('/api/endpoint', [Controller::class, 'method']) ->add($rateLimit);
๐ CORS Configuration
CORS is configured globally in bootstrap/middleware.php. Configure allowed origins in .env:
CORS_ORIGINS=* # or specific origins CORS_ORIGINS=http://localhost:3000,https://example.com
๐ Logging
The application uses Monolog for logging. Use the Logger helper:
use App\Modules\Core\Infrastructure\Support\Logger; Logger::error('Something went wrong', ['user_id' => 123]); Logger::warning('Suspicious activity detected'); Logger::info('User logged in', ['email' => $email]); Logger::debug('Debug information', $data);
Logs are written to storage/logs/slim.log. Log level is automatically set based on APP_ENV:
production: Warning and abovelocal/development: Debug and above
๐จ Blade Templating
This project uses BladeOne - a lightweight, standalone Blade engine optimized for Slim 4. It provides the same Blade syntax as Laravel but without the heavy dependencies.
Custom Blade Directives
The following Laravel-like directives are available:
Authentication Directives
@guest <a href="/login">Login</a> <a href="/register">Register</a> @endguest @auth <p>Welcome, {{ AuthHelper::user()['name'] }}!</p> <form method="POST" action="/logout"> @csrf <button type="submit">Logout</button> </form> @endauth
CSRF Protection
<form method="POST" action="/profile"> @csrf <!-- or manually: --> <input type="hidden" name="_token" value="{{ AuthHelper::csrfToken() }}"> </form>
HTTP Method Spoofing
<form method="POST" action="/users/1"> @csrf @method('DELETE') <button type="submit">Delete User</button> </form>
Authorization Directives
@can('edit-posts') <a href="/posts/1/edit">Edit Post</a> @endcan @role('admin') <a href="/admin">Admin Panel</a> @endrole
AuthHelper Class
The AuthHelper class provides static methods for authentication:
use App\Modules\Core\Infrastructure\Support\AuthHelper; // Check authentication if (AuthHelper::check()) { // User is logged in } // Check if guest if (AuthHelper::guest()) { // User is not logged in } // Get current user $user = AuthHelper::user(); echo $user['name']; // Get user ID $userId = AuthHelper::id(); // Check permissions and roles if (AuthHelper::can('create-posts')) { // User can create posts } if (AuthHelper::hasRole('admin')) { // User is admin } // Set user data after login AuthHelper::setUser([ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'roles' => ['user', 'editor'], 'permissions' => ['view-posts', 'create-posts'] ]); // Logout user AuthHelper::logout();
Standard Blade Syntax
All standard Blade directives work as expected:
@extends('layouts.app') @section('content') <h1>{{ $title }}</h1> @foreach($users as $user) <p>{{ $user->name }}</p> @endforeach @if($condition) <span>{{ $message }}</span> @elseif($otherCondition) <span>Other message</span> @else <span>Default message</span> @endif @include('partials.footer') @endsection
๐งช Testing
The project includes a comprehensive test suite covering:
Test Suites
| Suite | Tests | Description |
|---|---|---|
| Unit | 404+ | Isolated component tests |
| Integration | 63+ | Database and service integration |
| Feature | 21+ | End-to-end API tests |
| Edge Cases | 18+ | Boundary and unusual scenarios |
New in v2.0 (173 additional tests)
Environment Validation Tests (48 tests)
- Production environment validation
- Local/development environment validation
- Weak secret detection
- Missing configuration detection
- Edge cases (unicode, special chars, boundary values)
JWT Service Tests (51 tests)
- Token generation and validation
- Refresh token rotation
- Fingerprint-based security
- Token theft detection
- Multiple algorithms (HS256, HS384, HS512)
- Edge cases (expired tokens, tampered tokens)
Auto-Discovery Tests (35 tests)
- Module scanning
- Cache warming and clearing
- Production vs development behavior
- Statistics generation
Generic CRUD Tests (39 tests)
- Create, Read, Update, Delete operations
- Pagination
- Action factory
- Integration with real database
Running Tests
# Run all tests composer test # Run specific test suites ./vendor/bin/phpunit --testsuite Unit ./vendor/bin/phpunit --testsuite Integration ./vendor/bin/phpunit --testsuite Feature ./vendor/bin/phpunit --testsuite EdgeCases # Run with coverage ./vendor/bin/phpunit --coverage-html coverage # Run specific test file ./vendor/bin/phpunit tests/Unit/EnvironmentValidatorTest.php
Test Coverage
- โ 475+ tests
- โ 900+ assertions
- โ All new features tested
- โ Edge cases covered
- โ Integration with real database
- โ PHP 8.4 optimized with Rector
Code Quality Tools
Laravel Pint (Code formatting):
# Check code style ./vendor/bin/pint --test # Fix code style ./vendor/bin/pint
Rector (Automated refactoring):
# Check what would be changed ./vendor/bin/rector process --dry-run # Apply refactoring ./vendor/bin/rector process
Combined (CI/CD ready):
./vendor/bin/pint && ./vendor/bin/rector process && ./vendor/bin/phpunit
๐ง Configuration
Environment Variables
Key environment variables in .env:
# Application APP_ENV=local APP_URL=http://localhost:81 # Database DB_CONNECTION=mysql DB_HOST=slim_db DB_PORT=3306 DB_DATABASE=slim DB_USERNAME=slim DB_PASSWORD=secret # JWT JWT_SECRET=your-secret-key-here # Mail MAIL_HOST=smtp.example.com MAIL_PORT=587 MAIL_USERNAME=your_email@example.com MAIL_PASSWORD=your_password MAIL_FROM_ADDRESS=noreply@example.com MAIL_FROM_NAME="Your App Name" MAIL_ENCRYPTION=tls # CORS CORS_ORIGINS=* # Cache CACHE_DRIVER=redis CACHE_PREFIX=slim_cache CACHE_PATH=/var/www/html/storage/cache/data REDIS_CACHE_DATABASE=1 # Queue QUEUE_DRIVER=redis QUEUE_RETRY_AFTER=90 # Session SESSION_DRIVER=redis SESSION_LIFETIME=120 # Cookies COOKIE_TTL=3600 COOKIE_PATH=/ COOKIE_DOMAIN= COOKIE_SECURE=true COOKIE_HTTP_ONLY=true COOKIE_SAME_SITE=Lax COOKIE_ENCRYPT=true
๐ฆ API Resources
The application uses Resource classes to format API responses consistently:
use App\Modules\User\Infrastructure\Http\Resources\UserResource; // Single resource return ApiResponse::success(UserResource::make($user)); // Collection return ApiResponse::success(UserResource::collection($users));
Available Resources:
UserResource- Formats user data (hides password, includes roles)RoleResource- Formats role data (includes permissions)PermissionResource- Formats permission data (includes roles)
๐ API Response Format
All API responses follow a consistent format using the ApiResponse helper:
Success Response:
{
"status": "success",
"data": {...},
"message": "Optional message"
}
Error Response:
{
"status": "error",
"message": "Error message",
"code": "ERROR_CODE",
"errors": {
"field": ["Error message"]
}
}
Usage in Controllers:
use App\Support\ApiResponse; use App\Enums\HttpStatusCode; // Success return ApiResponse::success($data); return ApiResponse::success($user, HttpStatusCode::CREATED); // Errors return ApiResponse::error('Error message'); return ApiResponse::unauthorized(); return ApiResponse::notFound('User not found'); return ApiResponse::validationError(['email' => ['Invalid email']]);
๐ API Endpoints
Authentication
POST /api/v1/register- Register new userPOST /api/v1/login- Login and get JWT tokenPOST /api/v1/password-recovery- Request password resetPOST /api/v1/reset-password- Reset password with token
Users (requires authentication)
GET /api/v1/users- List all usersPOST /api/v1/users- Create userGET /api/v1/users/{id}- Get userPUT /api/v1/users/{id}- Update userDELETE /api/v1/users/{id}- Delete user
Roles (requires authentication)
GET /api/v1/roles- List all rolesPOST /api/v1/roles- Create roleGET /api/v1/roles/{id}- Get rolePUT /api/v1/roles/{id}- Update roleDELETE /api/v1/roles/{id}- Delete role
Permissions (requires authentication)
GET /api/v1/permissions- List all permissionsPOST /api/v1/permissions- Create permissionGET /api/v1/permissions/{id}- Get permissionPUT /api/v1/permissions/{id}- Update permissionDELETE /api/v1/permissions/{id}- Delete permission
๐๏ธ Architecture
The project follows a modular clean architecture pattern:
Modular Architecture
The application is organized into independent modules, each containing:
-
Application Layer - Business logic
- Actions - Business logic operations
- DTOs - Data Transfer Objects for type-safe data handling
- Interfaces - Service contracts for dependency injection
- Services - Complex business logic services
-
Infrastructure Layer - Technical implementation
- Models - Eloquent models for database interaction
- Repositories - Data access layer abstraction (Repository pattern)
- Controllers - Thin controllers that delegate to Actions
- Requests - Form request validation
- Resources - API response transformers
- Providers - Service providers for dependency registration
- Routes - Module-specific routes
-
Cross-cutting Concerns
- Middleware - Request/response processing
- Policies - Authorization logic
- Exceptions - Custom exception classes for better error handling
- Events - Event-driven architecture
- Jobs - Asynchronous task processing
Module Registration
Modules are automatically registered via bootstrap/modules-register.php:
return [ // Core module must be loaded first App\Modules\Core\Infrastructure\Providers\CoreServiceProvider::class, // Auth module App\Modules\Auth\Infrastructure\Providers\AuthServiceProvider::class, // Feature modules App\Modules\User\Infrastructure\Providers\UserServiceProvider::class, App\Modules\Role\Infrastructure\Providers\RoleServiceProvider::class, App\Modules\Permission\Infrastructure\Providers\PermissionServiceProvider::class, ];
Dependency Injection
The application uses PHP-DI with automatic dependency registration:
- Repositories - Automatically registered in Service Providers
- Action Interfaces - Automatically registered in
bootstrap/dependencies.phpwhen usingmake:module - Autowiring - PHP-DI automatically resolves constructor dependencies
How it works:
-
When creating a module with
make:module:- Repository is registered in
ServiceProvider::register() - Action Interfaces are registered in
bootstrap/dependencies.php - Use statements are automatically added
- Repository is registered in
-
PHP-DI Autowiring:
- Automatically resolves concrete classes (no registration needed)
- Resolves constructor dependencies via type hints
- Example:
LoginActionneedsUserRepositoryโ automatically injected
-
Interface-based injection:
- Controllers use interfaces (e.g.,
CreateUserActionInterface) - PHP-DI resolves to implementation via
dependencies.php - Allows for easy mocking in tests
- Controllers use interfaces (e.g.,
Repository Pattern
The application uses the Repository pattern to abstract data access logic:
use App\Modules\User\Infrastructure\Repositories\UserRepository; class UserController extends Controller { public function __construct( private readonly UserRepository $repository ) {} public function show(int $id): Response { $user = $this->repository->findOrFail($id); return ApiResponse::success(UserResource::make($user)); } }
Automatic Registration:
When you create a module with make:module, the Repository is automatically registered in the Service Provider:
// ServiceProvider::register() $container->set(UserRepository::class, \DI\autowire(UserRepository::class));
Available Repositories:
UserRepository- User data access with methods likefindByEmail(),findByPasswordResetToken()RoleRepository- Role data access with methods likefindByName(),paginateWithPermissions()PermissionRepository- Permission data access with methods likefindByName(),paginateWithRoles()
Action Pattern with Interfaces
Actions implement interfaces for better testability and flexibility:
// Interface interface CreateUserActionInterface { public function execute(CreateUserDTO $dto): User; } // Implementation final class CreateUserAction implements CreateUserActionInterface { public function __construct( private readonly UserRepository $repository ) {} public function execute(CreateUserDTO $dto): User { return $this->repository->create([...]); } }
Automatic Registration:
When you create a module, Action Interfaces are automatically registered in bootstrap/dependencies.php:
CreateUserActionInterface::class => \DI\autowire(CreateUserAction::class),
Working with Modules
Creating a new module:
php slim make:module Product --migration
This creates a complete module structure. After creation:
-
Update the Model (
app/Modules/Product/Infrastructure/Models/Product.php):- Add
$fillablefields - Add
$castsfor type casting - Add relationships if needed
- Add
-
Update the DTOs (
app/Modules/Product/Application/DTOs/):- Add properties to
CreateProductDTO - Add optional properties to
UpdateProductDTO
- Add properties to
-
Update the Actions (
app/Modules/Product/Application/Actions/):- Map DTO properties to model attributes in
CreateProductAction - Add business logic as needed
- Map DTO properties to model attributes in
-
Update the Controller (
app/Modules/Product/Infrastructure/Http/Controllers/ProductController.php):- Map request data to DTOs in
store()andupdate()methods
- Map request data to DTOs in
-
Update the Resource (
app/Modules/Product/Infrastructure/Http/Resources/ProductResource.php):- Add fields to the resource output
-
Update the Requests (
app/Modules/Product/Infrastructure/Http/Requests/):- Add validation rules as needed
-
Update the Policy (
app/Modules/Product/Policies/ProductPolicy.php):- Add authorization logic
Module is ready to use! Routes are automatically loaded from app/Modules/Product/Infrastructure/Routes/api.php.
Exception Handling
Custom exception classes provide consistent error handling:
use App\Modules\Core\Infrastructure\Exceptions\NotFoundException; use App\Modules\Core\Infrastructure\Exceptions\InvalidCredentialsException; use App\Modules\Core\Infrastructure\Exceptions\UnauthorizedException; use App\Modules\Core\Infrastructure\Exceptions\ForbiddenException; use App\Modules\Core\Infrastructure\Exceptions\BadRequestException; // In Actions throw new NotFoundException('User not found'); throw new InvalidCredentialsException('Invalid email or password');
The ExceptionHandlerMiddleware automatically converts exceptions to appropriate API responses.
๐ New Features
Environment Validation (Fail-Fast)
Environment validation runs automatically at application startup to catch configuration errors early.
# Validate configuration
php slim discovery --validate
Features:
- Validates
JWT_SECRETlength (minimum 32 characters) - Checks database connectivity
- Validates
APP_ENVsettings - Production-specific checks (Redis, Mail, Cache)
Example Output:
๐ Environment Validation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Environment configuration is valid
------------------- ----------
Setting Value
------------------- ----------
Environment local
JWT Configured Yes
JWT Secret Length 42 chars
DB Connection mysql
------------------- ----------
Auto-Discovery for Dependency Injection
Automatically discovers and registers Interface โ Implementation bindings.
# View discovery statistics php slim discovery --stats # Warm cache for production php slim discovery --warm # Refresh cache php slim discovery --refresh
Before (Manual Registration):
// bootstrap/dependencies.php return [ CreateUserActionInterface::class => autowire(CreateUserAction::class), UpdateUserActionInterface::class => autowire(UpdateUserAction::class), // ... 80+ more lines ];
After (Auto-Discovery):
// bootstrap/dependencies.php $discovery = new OptimizedDiscovery(); return $discovery->buildDefinitions();
JWT Service with Refresh Tokens
Enhanced JWT implementation with security best practices.
use App\Modules\Core\Infrastructure\Support\AdvancedJwtService; $jwtService = new AdvancedJwtService( secret: $_ENV['JWT_SECRET'], algorithm: 'HS256', issuer: 'my-app', audience: 'my-api' ); // Generate access token $token = $jwtService->generateAccessToken( userId: 123, claims: ['role' => 'admin'], ttl: 3600 // 1 hour ); // Generate token pair (access + refresh) $tokenPair = $jwtService->generateRefreshToken(userId: 123); // Returns: access_token, refresh_token, expires_in // Rotate refresh token (security best practice) $newPair = $jwtService->rotateRefreshToken($tokenPair->getRefreshToken()); // Validate token if ($jwtService->verify($token)) { $payload = $jwtService->decode($token); $userId = $payload->sub; } // Get token info $info = $jwtService->getTokenInfo($token); // Returns: algorithm, type, issuer, expires_at, is_expired, etc.
Features:
- Access tokens (short-lived)
- Refresh tokens (long-lived with rotation)
- Fingerprint-based theft detection
- Issuer and audience validation
- Token blacklisting support
Generic CRUD Controller
Build complete CRUD APIs with minimal code.
Before (150 lines):
class UserController extends Controller { public function __construct( private CreateUserAction $createAction, private UpdateUserAction $updateAction, // ... 5 more actions ) {} public function index(Request $request): Response { /* 20 lines */ } public function store(Request $request): Response { /* 15 lines */ } public function show(Request $request, array $args): Response { /* 10 lines */ } public function update(Request $request, array $args): Response { /* 15 lines */ } public function destroy(Request $request, array $args): Response { /* 10 lines */ } }
After (20 lines):
class UserController extends GenericCrudController { protected string $repositoryClass = UserRepository::class; protected ?string $resourceClass = UserResource::class; protected array $defaultRelations = ['roles']; protected array $fillable = ['name', 'email', 'password']; }
Benefits:
- 87% less code
- 67% faster development
- Automatic pagination, relations, and validation
- Backwards compatible with existing controllers
See Migration Guide for detailed migration instructions.
๐ Security Features
- โ Password hashing with bcrypt
- โ JWT token authentication (API)
- โ Session-based authentication (Web)
- โ CSRF protection for web routes
- โ Rate limiting on auth endpoints (5 requests/minute)
- โ Rate limiting on all API endpoints (configurable)
- โ Input validation with FormRequest
- โ SQL injection protection (Eloquent ORM)
- โ XSS protection (Blade templating)
- โ Secure session handling
- โ Cookie encryption (AES-256-CBC) for sensitive data
- โ Centralized exception handling with proper error responses
- โ Modular architecture for better code organization
- โ Automatic dependency registration
- โ Custom Blade directives (@auth, @guest, @csrf, @method, @can, @role)
๐พ Caching
The application includes a powerful caching layer with multiple driver support:
Features
- Multiple Drivers: File, Redis, and Null (for testing)
- Simple API: Laravel-like helper functions
- Repository Integration: Built-in trait for easy model caching
- Tag-based Cache Clearing: Flush cache by tags
- PSR-16 Compatible: Standard caching interface
Configuration
Configure caching in your .env file:
# Cache Driver: file, redis, null CACHE_DRIVER=redis CACHE_PREFIX=slim_cache CACHE_PATH=/var/www/html/storage/cache/data # Redis Cache Database (separate from queue/session) REDIS_CACHE_DATABASE=1
Usage
Helper Functions
// Store a value cache_put('key', 'value', 3600); // TTL in seconds (null = forever) // Retrieve a value $value = cache('key'); $value = cache('key', 'default'); // With default // Remember pattern (cache or compute) $users = cache_remember('users', 300, function () { return User::all(); }); // Check existence if (cache_has('key')) { // ... } // Delete a value cache_forget('key'); // Clear all cache cache_flush(); // Clear by tag cache_flush('users'); cache_flush(['users', 'posts']);
Dependency Injection
use App\Modules\Core\Infrastructure\Cache\CacheInterface; class UserController extends Controller { public function __construct( private readonly CacheInterface $cache ) {} public function index(): Response { $users = $this->cache->remember('users', 300, function () { return User::all(); }); return ApiResponse::success($users); } }
Repository Caching
use App\Modules\Core\Infrastructure\Cache\RepositoryCacheExample; use App\Modules\Core\Infrastructure\Repositories\EloquentRepository; class UserRepository extends EloquentRepository { use RepositoryCacheExample; protected string $cacheTag = 'users'; protected int $repositoryCacheTtl = 600; // 10 minutes protected function model(): string { return User::class; } // Use cached methods public function findCached(int $id): ?User { return $this->findCached($id); } // Custom cached query public function findByEmailCached(string $email): ?User { return $this->remember("email:{$email}", function () use ($email) { return $this->findByEmail($email); }); } }
Cacheable Trait
use App\Modules\Core\Infrastructure\Cache\Cacheable; class ProductService { use Cacheable; protected string $cacheTag = 'products'; protected int $cacheTtl = 3600; public function getFeaturedProducts() { return $this->remember('featured', function () { // Expensive query return Product::featured()->with('category')->get(); }); } public function clearProductCache() { $this->cacheFlush(); } }
Cache Drivers
File Driver
Stores cache in filesystem. Good for single-server setups.
CACHE_DRIVER=file CACHE_PATH=/var/www/html/storage/cache/data
Redis Driver
Recommended for production and multi-server setups.
CACHE_DRIVER=redis REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_CACHE_DATABASE=1
Null Driver
Disables caching. Useful for testing.
CACHE_DRIVER=null
CLI Commands
# Clear application cache php slim cache:clear # Clear cache by tag php slim cache:clear --tag=users
๐ช Cookies
The application includes a powerful Cookie Helper with encryption support:
Features
- Easy API: Simple get/set methods with helper functions
- Encryption: AES-256-CBC encryption for sensitive data
- Flexible: Support for arrays, objects, and primitives
- Secure: Configurable secure, httpOnly, and SameSite options
- Framework Agnostic: Works with any PHP application
Configuration
Configure cookies in your .env file:
# Cookie Settings COOKIE_TTL=3600 COOKIE_PATH=/ COOKIE_DOMAIN= COOKIE_SECURE=true COOKIE_HTTP_ONLY=true COOKIE_SAME_SITE=Lax COOKIE_ENCRYPT=true
Usage
Helper Functions
// Set a cookie cookie_set('user_id', 123, 3600); cookie_set('preferences', ['theme' => 'dark'], 86400); // Get a cookie $userId = cookie_get('user_id'); $userId = cookie_get('user_id', 0); // With default // Check if exists if (cookie_has('user_id')) { // ... } // Delete a cookie cookie_delete('user_id'); // Delete all cookies cookie_flush(); // Forever cookie (10 years) cookie_forever('remember_me', 'yes'); // Remember pattern (get or set) $settings = cookie_remember('app_settings', 3600, function () { return Settings::getDefaults(); });
Using the Class Directly
use App\Modules\Core\Infrastructure\Support\CookieHelper; $cookie = CookieHelper::getInstance(); // Set with custom options $cookie->set('session', $data, 3600, [ 'secure' => true, 'httponly' => true, 'samesite' => 'Strict' ]); // Get value $value = $cookie->get('session'); // Make cookie for Slim Response $cookieData = $cookie->make('name', 'value', 3600); $response = $response->withHeader('Set-Cookie', 'name=' . $cookieData['value'] . '; Path=' . $cookieData['path'] );
In Controllers
class UserController extends Controller { public function login(Request $request, Response $response): Response { // Set user preference cookie cookie_set('last_login', date('Y-m-d H:i:s'), 86400 * 30); // Set encrypted session data cookie_set('user_prefs', [ 'theme' => 'dark', 'lang' => 'en' ], 3600); return $response; } public function logout(Request $request, Response $response): Response { // Clear user cookies cookie_delete('user_prefs'); cookie_delete('last_login'); return $response; } }
Cookie Encryption
Cookies are automatically encrypted using AES-256-CBC when COOKIE_ENCRYPT=true:
// Encryption is transparent - works the same way $helper = new CookieHelper(encryptionEnabled: true); $helper->set('secret', 'sensitive data'); // Automatically decrypted on get $data = $helper->get('secret'); // 'sensitive data'
Security Best Practices
- Always use encryption for sensitive data
- Set httpOnly to prevent XSS attacks
- Use secure flag in production (HTTPS only)
- SameSite=Lax or Strict for CSRF protection
- Reasonable TTL - don't make cookies live forever
๐ API Query Builder
Powerful query parameter parsing for REST API filtering, sorting, and searching.
Features
- Filtering - Filter by any field with operators (
eq,ne,gt,gte,lt,lte,like,in, etc.) - Sorting - Multi-column sort with direction (
asc/desc) - Searching - Full-text search across multiple fields
- Field Selection - Select only needed fields (sparse fieldsets)
- Relationships - Eager load related data (
include) - Ranges - Filter by numeric ranges (price, dates, etc.)
- Pagination - Automatic pagination with configurable limits
Usage
Basic Filtering
// URL: GET /api/users?filter[role]=admin&filter[active]=true $users = User::filter($request)->paginate();
Sorting
// URL: GET /api/users?sort=-created_at,name // Sort by created_at DESC, then name ASC $users = User::filter($request)->paginate();
Searching
// In User model protected array $searchable = ['name', 'email']; // URL: GET /api/users?search=john // Searches in name and email fields $users = User::filter($request)->paginate();
Field Selection
// URL: GET /api/users?fields=id,name,email // Returns only id, name, and email $users = User::filter($request)->paginate();
Eager Loading
// URL: GET /api/users?include=posts,comments $users = User::filter($request)->paginate();
Ranges
// URL: GET /api/products?range[price]=10,100 $products = Product::filter($request)->paginate();
Advanced Filters with Operators
// URL: GET /api/users?filter[age]=gt:18&filter[name]=like:john // Operators: eq, ne, gt, gte, lt, lte, like, starts, ends, in, nin, null
Using in Controllers
use App\Modules\Core\Infrastructure\Query\Filterable; class UserController extends Controller { public function index(Request $request): Response { // Using the Filterable trait $result = User::filterPaginate($request); return ApiResponse::success($result['items'], HttpStatusCode::OK, null, $result['pagination']); } }
Model Configuration
use App\Modules\Core\Infrastructure\Query\Filterable; class User extends Model { use Filterable; // Fields allowed for filtering protected array $filterable = ['name', 'email', 'role', 'active']; // Fields allowed for sorting protected array $sortable = ['id', 'name', 'created_at', 'updated_at']; // Fields allowed for searching protected array $searchable = ['name', 'email']; // Default sort order protected array $defaultSort = ['created_at' => 'desc']; }
Using QueryBuilder Directly
use App\Modules\Core\Infrastructure\Query\QueryBuilder; // Simple usage $users = (new QueryBuilder($request))->paginate(User::class); // With configuration $builder = new QueryBuilder($request, [ 'filterable' => ['name', 'email'], 'sortable' => ['id', 'name'], 'searchable' => ['name', 'email'], ]); $users = $builder->paginate(User::class);
Helper Functions
// Parse query parameters $parser = query_parser($request); $filters = $parser->filters(); $sorts = $parser->sorts(); $search = $parser->search(); // Apply filters to query $query = query_filter(User::class, $request); $users = $query->paginate(); // Get paginated results directly $result = query_paginate(User::class, $request); // Returns: ['items' => [...], 'pagination' => [...]]
URL Parameter Reference
| Parameter | Example | Description |
|---|---|---|
filter[field] |
filter[status]=active |
Filter by field value |
filter[field]=op:value |
filter[age]=gt:18 |
Filter with operator |
sort |
sort=-created_at,name |
Sort by fields |
search |
search=john |
Full-text search |
fields |
fields=id,name,email |
Select specific fields |
include |
include=posts,comments |
Eager load relations |
range[field] |
range[price]=10,100 |
Filter by range |
page |
page=2 |
Page number |
per_page |
per_page=25 |
Items per page (max 100) |
Filter Operators
| Operator | Example | Description |
|---|---|---|
eq |
filter[id]=eq:1 |
Equal (default) |
ne |
filter[status]=ne:draft |
Not equal |
gt |
filter[age]=gt:18 |
Greater than |
gte |
filter[price]=gte:10 |
Greater than or equal |
lt |
filter[age]=lt:65 |
Less than |
lte |
filter[price]=lte:100 |
Less than or equal |
like |
filter[name]=like:john |
Contains substring |
starts |
filter[name]=starts:John |
Starts with |
ends |
filter[name]=ends:Doe |
Ends with |
in |
filter[id]=in:1,2,3 |
In array |
nin |
filter[status]=nin:deleted |
Not in array |
null |
filter[deleted_at]=null:true |
Is null / not null |
๐ฆ Dependencies
Core
slim/slim- Slim Framework 4illuminate/database- Eloquent ORMilluminate/validation- Validationeftec/bladeone- Lightweight Blade templating enginephp-di/slim-bridge- Dependency injection
Authentication & Security
firebase/php-jwt- JWT tokensslim/csrf- CSRF protectiontuupola/cors-middleware- CORS support
Utilities
monolog/monolog- Loggingphpmailer/phpmailer- Email sendingvlucas/phpdotenv- Environment variablesilluminate/pagination- Pagination supportilluminate/support- Laravel support packagepredis/predis- Redis client (cache, queue, sessions)
Development
phpunit/phpunit- Testinglaravel/pint- Code formattingrector/rector- Code refactoring
๐ Deployment
Storage Directory Setup
The application requires write access to the storage/ directory for logs, cache, and queue files.
Setup
For Nginx:
# Set ownership sudo chown -R www-data:www-data storage # Set permissions sudo chmod -R 775 storage
For Apache:
# Set ownership sudo chown -R www-data:www-data storage # Set permissions sudo chmod -R 775 storage
For Docker: The storage directory is automatically mounted and permissions are handled by the container.
Web Server Configuration
Nginx Configuration
The nginx configuration is located in docker/nginx/app.conf. For production, ensure:
- Document root points to
/var/www/html/public - PHP-FPM is configured correctly
- Storage directory is protected (denied access in nginx config)
- Security headers are set
Example production nginx configuration:
server { listen 80; server_name your-domain.com; root /var/www/html/public; # Deny access to storage and cache location ~ ^/(storage|cache)/ { deny all; } location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }
Apache Configuration
The .htaccess file in public/ directory handles URL rewriting. Ensure:
-
mod_rewrite is enabled:
sudo a2enmod rewrite sudo systemctl restart apache2
-
AllowOverride is set to All in Apache config:
<Directory /var/www/html/public> Options -Indexes +FollowSymLinks AllowOverride All Require all granted </Directory>
-
Storage directory is protected (already configured in
.htaccess)
Permissions Reference
| Directory | Permissions | Owner | Purpose |
|---|---|---|---|
storage/ |
775 | www-data | Root storage directory |
storage/logs/ |
775 | www-data | Application logs |
storage/cache/ |
775 | www-data | Blade view cache |
storage/queue/ |
775 | www-data | Queue job files |
Note: Adjust user/group (www-data, nginx, apache) based on your server configuration.
Security Considerations
- Storage directory should never be accessible via web browser
- Environment files (
.env) should be protected - Composer files should not be accessible
- Git files should not be accessible
- File uploads (if implemented) should be stored outside
public/directory
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
๐ License
This project is licensed under the MIT License.
๐ Acknowledgments
- Slim Framework
- Laravel for Eloquent, Blade, and Validation
- All the amazing open-source contributors
Made with โค๏ธ for the PHP community