A modern, modular PHP framework built with Domain-Driven Design principles, featuring SQLite persistence, optional modules for authentication, email, and background jobs.
- Starting Your Own Project - Beginner-friendly guide with everything you need to build your first application
- Getting Started - Quick setup guide for developers
- Project Instructions - Technical documentation for contributors
- Modern PHP 8.4 with strict types and readonly classes
- Modular architecture - enable only the features you need
- Domain-Driven Design with clean layer separation enforced by deptrac
- SQLite-first with fluent query builder and SQL injection protection
- PSR-11 container with self-registering module providers
- Secure authentication - Session fixation protection, cookie hardening (httponly, secure, samesite), CSRF on all POST requests
- Flash messages across redirects with auto-dismiss
- Per-field validation errors with visual feedback
- Named route URL generation via
path()Twig function andUrlGenerator - Event dispatcher for decoupled cross-module communication
- Email service with AWS SES support and log fallback
- Background job queue with database-backed persistence and safe deserialization
- Reflection-based controller dispatch with automatic parameter injection
- Comprehensive test coverage - 233 tests with unit, integration, and feature tests
- Strict quality checks - PHPStan level 10, PHP-CS-Fixer, Rector, 0 violations
- Responsive design - Mobile-friendly with collapsible sidebar and hamburger menu
- Modern frontend with Tailwind CSS 4 and Alpine.js
- PHP 8.4+ with extensions:
json,pdo,pdo_sqlite - Composer for dependency management
- Node.js & npm for frontend assets
- Laravel Herd (recommended) or any PHP development environment
git clone https://github.com/adamjohnlea/Caro-Framework.git your-project-name
cd your-project-name
composer install
npm installcomposer hooks:installThis installs the pre-commit hook that enforces code quality checks.
touch storage/database.sqlite
chmod -R 775 storageThe database file is git-ignored, so you need to create it after cloning.
cp .env.example .envEdit .env to configure your application:
APP_ENV=local
APP_DEBUG=true
APP_NAME="My App"
DB_DRIVER=sqlite
# DB_PATH is optional - defaults to storage/database.sqlite
# Enable modules as needed
MODULE_AUTH=true
MODULE_EMAIL=false
MODULE_QUEUE=falsenpm run build # One-time build
npm run dev # Watch mode for development
# Copy Alpine.js (required for interactivity)
cp node_modules/alpinejs/dist/cdn.min.js public/js/alpine.min.jsMigrations run automatically on the first web request.
php cli/create-admin.php [email protected] --password=secret123Caro Framework uses a modular, domain-driven architecture with strict layer boundaries:
src/
├── Database/ # Database wrapper, migrations, query grammar
├── Http/
│ ├── Controllers/ # Core HTTP controllers (Home, Health)
│ ├── Middleware/ # Request/response middleware
│ ├── ControllerDispatcher.php # Reflection-based dispatch
│ ├── UrlGenerator.php # Named route URL generation
│ ├── RouteProviderInterface.php # Module route self-registration
│ ├── MiddlewareProviderInterface.php # Module middleware self-registration
│ └── RouteAccessRegistry.php # Public/admin route registry
├── Modules/
│ ├── Auth/ # Authentication module
│ │ ├── AuthServiceProvider.php # Services, routes, middleware
│ │ └── Http/Controllers/ # Auth and User controllers
│ ├── Email/ # Email service module
│ │ └── EmailServiceProvider.php
│ ├── Queue/ # Background job queue module
│ │ └── QueueServiceProvider.php
│ └── {YourModule}/ # Custom modules
│ ├── Application/ # Use cases and services
│ ├── Domain/ # Entities, value objects, interfaces
│ ├── Infrastructure/# Repository implementations
│ ├── Http/Controllers/ # Module controllers
│ └── {Module}ServiceProvider.php # Services, routes, middleware
├── Shared/
│ ├── Cli/ # CliBootstrap for CLI container setup
│ ├── Container/ # ContainerInterface (PSR-11) and Container
│ ├── Database/ # Fluent query builder
│ ├── Events/ # EventDispatcher for cross-module communication
│ ├── Exceptions/ # Shared exception types
│ ├── Providers/ # ServiceProvider base class
│ ├── Session/ # FlashMessageService
│ └── Twig/ # Twig extensions (AppExtension)
└── Views/ # Twig templates
The architecture enforces clean dependencies using deptrac:
- Domain Layer: No dependencies on other layers (only Shared)
- Application Layer: Depends on Domain and Shared only
- Infrastructure Layer: Can depend on Domain, Application, Shared, Database
- Http Layer: Depends on Application, Domain, Shared, Database
- Shared Layer: Can depend on Database (QueryBuilder wraps Database)
All modules are opt-in via .env configuration flags.
Enable: MODULE_AUTH=true
Provides secure session-based authentication with role-based access control:
- Security hardened: Session fixation protection, cookie hardening (httponly, secure, samesite=Strict), 30-minute timeout
- Login/logout functionality (POST-based logout with CSRF)
- User CRUD operations (admin-only) with per-field validation errors
- CSRF protection on all POST requests
- Middleware:
AuthenticationMiddleware,CsrfMiddleware,AuthorizationMiddleware(insrc/Modules/Auth/Http/Middleware/) - Self-registers routes, middleware, and route access via provider interfaces
- Comprehensive tests: 55 tests including feature tests for complete auth flows
Create an admin user:
php cli/create-admin.php [email protected] --password=secret123Enable: MODULE_EMAIL=true
Flexible email service with multiple backends:
SesEmailService- Production email via AWS SES v2LogEmailService- Development fallback that logs emails instead of sending
Configuration for SES:
MODULE_EMAIL=true
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY=your-key
AWS_SES_SECRET_KEY=your-secret
AWS_SES_FROM_ADDRESS[email protected]Usage:
$emailService = $container->get(EmailServiceInterface::class);
$emailService->send(
to: '[email protected]',
subject: 'Welcome!',
body: 'Thanks for signing up.'
);
$emailService->sendWithAttachment(
to: '[email protected]',
subject: 'Your invoice',
body: 'See attached.',
attachmentPath: '/path/to/invoice.pdf',
attachmentName: 'invoice.pdf',
mimeType: 'application/pdf'
);Enable: MODULE_QUEUE=true
Database-backed job queue with worker process:
- Transaction-locked job claiming prevents double-processing
- Automatic retry with exponential backoff
- Manual retry for failed jobs
- Graceful shutdown on SIGTERM/SIGINT
Create a job:
use App\Modules\Queue\Domain\JobInterface;
use App\Shared\Container\ContainerInterface;
readonly class SendWelcomeEmail implements JobInterface
{
public function __construct(
private string $email,
) {}
public function handle(ContainerInterface $container): void
{
// Jobs receive the container interface and can access services
$emailService = $container->get(EmailServiceInterface::class);
$emailService->send($this->email, 'Welcome!', 'Thanks for joining.');
}
public function getQueue(): string
{
return 'default';
}
public function getMaxAttempts(): int
{
return 3;
}
}Dispatch a job:
$queueService = $container->get(QueueService::class);
$queueService->dispatch(new SendWelcomeEmail('[email protected]'));Run the worker:
php cli/worker.php --queue=default --sleep=3The worker will process jobs from the queue continuously. Use Ctrl+C for graceful shutdown.
Caro includes a fluent, immutable query builder for SQLite:
use App\Shared\Database\QueryBuilder;
$users = QueryBuilder::table('users')
->select(['id', 'name', 'email'])
->where('role', '=', 'admin')
->orderBy('created_at', 'DESC')
->limit(10)
->get();
$affectedRows = QueryBuilder::table('users')
->where('id', '=', 123)
->update(['name' => 'New Name']);
QueryBuilder::table('users')->insert([
'name' => 'John Doe',
'email' => '[email protected]',
]);The query builder is dialect-aware and returns raw associative arrays. Domain repositories handle hydration to domain models.
TDD is mandatory. Write tests first, then implement. Every feature, bug fix, or refactoring must have test coverage.
composer test # Run all tests
composer test:coverage # Generate coverage reportTest structure:
tests/Unit/- Fast, isolated tests (no I/O)tests/Integration/- Tests with database (in-memory SQLite)tests/Feature/- End-to-end HTTP request tests
Run quality checks before every commit:
composer quality # Run all checks: cs-check, phpstan, deptrac, test
composer cs-fix # Auto-fix code style
composer rector:fix # Apply automated refactoringsThe pre-commit hook enforces quality checks automatically.
Quality tools:
- PHPStan - Level 10 + strict-rules for maximum type safety
- PHP-CS-Fixer - PSR-12 code style with custom rules
- deptrac - Enforce architectural layer boundaries
- Rector - Automated refactoring and modernization
- PHPUnit 11.x - Testing framework
# Create an admin user
php cli/create-admin.php [email protected] --password=secret123
# Run queue worker
php cli/worker.php --queue=default --sleep=3
# Health check
php cli/doctor.phpTailwind CSS 4 with watch mode:
npm run dev # Watch mode - rebuilds on changes
npm run build # Production buildTemplates use Twig with asset versioning and named route URLs:
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<a href="{{ path('users.index') }}">Users</a>
<a href="{{ path('users.edit', {id: user.id}) }}">Edit</a>Follow these steps to add a new module:
-
Create directory structure:
src/Modules/{ModuleName}/ ├── Application/ │ └── Services/ ├── Domain/ │ ├── Models/ │ ├── ValueObjects/ │ └── Repositories/ ├── Infrastructure/ │ └── Repositories/ ├── Database/ │ └── Migrations/ ├── Http/ │ └── Controllers/ └── {ModuleName}ServiceProvider.php -
Start with Domain Layer - Define value objects, entities, and repository interfaces
-
Add Application Layer - Create services that orchestrate domain logic
-
Add Infrastructure Layer - Implement repository interfaces with SQLite (prefer QueryBuilder for simple CRUD)
-
Create ServiceProvider - Register services, routes, middleware, and route access. Implement provider interfaces as needed:
<?php namespace App\Modules\YourModule; use App\Http\RouteProviderInterface; use App\Http\Router; use App\Shared\Providers\ServiceProvider; final class YourModuleServiceProvider extends ServiceProvider implements RouteProviderInterface { public function register(): void { $this->container->set(YourServiceInterface::class, function () { // Register services }); } public function routes(Router $router): void { $router->get('/your-route', YourController::class, 'index', 'your.index'); $router->post('/your-route', YourController::class, 'store', 'your.store'); } public function boot(): void { // Optional: Post-registration setup } }
-
Place controllers in
src/Modules/{ModuleName}/Http/Controllers/with namespaceApp\Modules\{ModuleName}\Http\Controllers -
Add SQL migration in
src/Modules/{ModuleName}/Database/Migrations/using timestamp naming:YYYY_MM_DD_HHMMSS_description.sql -
Add module toggle:
.env.example:MODULE_{NAME}=falseconfig/config.php: Add to config arrayMigrationRunner: Add to module map
-
Register provider in index.php:
if ($config['modules']['yourmodule']) { $container->registerProvider(new YourModuleServiceProvider($container, $config)); }
Routes, middleware, and route access are automatically collected from the provider interfaces.
-
Add Twig templates in
src/Views/{module}/— use{{ path('route.name') }}for all URLs -
Verify deptrac passes - No layer violations allowed
- Every file starts with
declare(strict_types=1) - Use
readonly classfor immutable objects - Use
#[Override]attribute on interface implementations - All class references use
useimports, never inline\Namespace\Class - Value objects validate in constructor and remain immutable
- Repository interfaces in Domain, implementations in Infrastructure
- Controllers stay thin - delegate to Application Services
- Test file mirrors source structure
- Unit tests mock external dependencies
- Integration tests use in-memory SQLite
- Feature tests exercise full request lifecycle
- Tailwind CSS 4 uses
@import "tailwindcss"syntax - Alpine.js for interactivity
- Asset URLs use
{{ asset('path') }}for cache busting - Avoid inline JavaScript
Caro Framework is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
See the LICENSE file for the complete license text.
- Fork the repository
- Create a feature branch
- Write tests first (TDD)
- Implement your feature
- Run
composer quality- all checks must pass - Submit a pull request
[Add support information here]