julienlinard / core-php
Framework MVC complet pour applications PHP avec Container DI, Controllers, Views, Forms, Session et plus
Installs: 198
Dependents: 3
Suggesters: 0
Security: 0
Stars: 13
Watchers: 0
Forks: 0
pkg:composer/julienlinard/core-php
Requires
- php: >=8.0
- julienlinard/php-cache: ^1.0
- julienlinard/php-dotenv: ^1.0
- julienlinard/php-router: ^1.2.1
- julienlinard/php-validator: ^1.0
Requires (Dev)
- phpunit/phpunit: ^12.0
Suggests
- julienlinard/php-vision: ^1.0 pour utiliser le moteur de template Vision avec syntaxe {{ variable }}
README
🇫🇷 Read in French | 🇬🇧 Read in English
💝 Support the project
If this package is useful to you, consider becoming a sponsor to support the development and maintenance of this open source project.
A modern and complete MVC framework for PHP 8+ with Container DI, Controllers, Views, Forms, Session, Cache and more.
🚀 Installation
composer require julienlinard/core-php
Requirements: PHP 8.0 or higher
⚡ Quick Start
<?php require_once __DIR__ . '/vendor/autoload.php'; use JulienLinard\Core\Application; use JulienLinard\Core\Controller\Controller; use JulienLinard\Core\View\View; // Bootstrap the application $app = Application::create(__DIR__); $app->start();
📋 Features
- ✅ Application - Main framework class
- ✅ Container DI - Dependency injection with auto-wiring
- ✅ Controllers - Base class with utility methods
- ✅ Views - Template engine with layouts + file-based view cache
- ✅ Models - Base Model class with hydration
- ✅ Forms - Form validation and error handling (powered by php-validator)
- ✅ Session - Session management with flash messages
- ✅ Cache - Integrated caching system (php-cache)
- ✅ Middleware - Integrated middleware system
- ✅ Security - CSRF + Rate Limiting + Security Headers middleware
- ✅ Performance - Response Compression (gzip) middleware
- ✅ Logging - Automatic log rotation with compression
- ✅ Config - Configuration management
- ✅ Exceptions - Centralized error handling
📖 Documentation
Application
use JulienLinard\Core\Application; // Create an application instance $app = Application::create(__DIR__); // Get existing instance (may return null) $app = Application::getInstance(); // Get instance or create it if it doesn't exist (useful for error handlers) $app = Application::getInstanceOrCreate(__DIR__); // Get instance or throw exception if it doesn't exist $app = Application::getInstanceOrFail(); // Configure view paths $app->setViewsPath(__DIR__ . '/views'); $app->setPartialsPath(__DIR__ . '/views/_templates'); // Start the application $app->start();
Controllers
use JulienLinard\Core\Controller\Controller; class HomeController extends Controller { public function index() { return $this->view('home/index', [ 'title' => 'Home', 'data' => [] ]); } public function redirect() { return $this->redirect('/login'); } public function json() { return $this->json(['message' => 'Hello']); } }
Views
use JulienLinard\Core\View\View; // Full view with layout $view = new View('home/index'); $view->render(['title' => 'Home']); // Partial view (without layout) $view = new View('partials/header', false); $view->render(); // Enable view cache (directory created automatically) View::configureCache(__DIR__.'/storage/view-cache', 300); // 5 min TTL View::setCacheEnabled(true); // Later clear expired cache files (older than 1 hour) $deleted = View::clearCache(3600);
View Caching Explained
- Cache key includes: view name, full/partial flag, data hash, mtimes of view + partials.
- Automatic invalidation if: TTL exceeded OR source view/partials modified.
- Disable by calling:
View::setCacheEnabled(false)orView::configureCache(null). - Safe writes using file locking (avoids race conditions).
- Useful for expensive templates (loops, heavy formatting, large partials).
### Models
```php
use JulienLinard\Core\Model\Model;
class User extends Model
{
public ?int $id = null;
public string $email;
public string $name;
public function toArray(): array
{
return [
'id' => $this->id,
'email' => $this->email,
'name' => $this->name,
];
}
}
// Automatic hydration
$user = new User(['id' => 1, 'email' => 'test@example.com', 'name' => 'John']);
Forms & Validation
core-php includes php-validator for advanced form validation with custom rules, multilingual messages, and sanitization.
Using the validate() method (Recommended)
use JulienLinard\Core\Form\Validator; $validator = new Validator(); $result = $validator->validate($data, [ 'email' => 'required|email', 'password' => 'required|min:8|max:255', 'age' => 'required|numeric|min:18' ]); if ($result->hasErrors()) { // Get all errors foreach ($result->getErrors() as $error) { echo $error->getMessage() . "\n"; } // Get errors for a specific field $emailErrors = $result->getErrorsForField('email'); } else { // Validation successful }
Advanced features
use JulienLinard\Core\Form\Validator; $validator = new Validator(); // Custom error messages $validator->setCustomMessages([ 'email.email' => 'Please enter a valid email address', 'password.min' => 'Password must be at least 8 characters' ]); // Set locale for multilingual messages $validator->setLocale('en'); // Enable/disable automatic sanitization $validator->setSanitize(true); // Register custom validation rules $validator->registerRule(new CustomRule()); // Validate $result = $validator->validate($data, $rules);
Manual validation (Legacy method)
use JulienLinard\Core\Form\FormResult; use JulienLinard\Core\Form\FormError; use JulienLinard\Core\Form\FormSuccess; use JulienLinard\Core\Form\Validator; $formResult = new FormResult(); $validator = new Validator(); // Manual validation if (!$validator->required($data['email'])) { $formResult->addError(new FormError('Email required')); } if (!$validator->email($data['email'])) { $formResult->addError(new FormError('Invalid email')); } if ($formResult->hasErrors()) { // Handle errors } else { $formResult->addSuccess(new FormSuccess('Form validated')); }
Session
use JulienLinard\Core\Session\Session; // Set a value Session::set('user_id', 123); // Get a value $userId = Session::get('user_id'); // Flash message Session::flash('success', 'Operation successful'); // Remove Session::remove('user_id');
DI Container
use JulienLinard\Core\Container\Container; $container = new Container(); // Simple binding $container->bind('database', function() { return new Database(); }); // Singleton $container->singleton('logger', function() { return new Logger(); }); // Automatic resolution $service = $container->make(MyService::class);
🔗 Integration with other packages
Integration with php-router
core-php automatically includes php-router. The router is accessible via getRouter().
use JulienLinard\Core\Application; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Response; $app = Application::create(__DIR__); $router = $app->getRouter(); // Define routes in your controllers class HomeController extends \JulienLinard\Core\Controller\Controller { #[Route(path: '/', methods: ['GET'], name: 'home')] public function index(): Response { return $this->view('home/index', ['title' => 'Home']); } } $router->registerRoutes(HomeController::class); $app->start();
Integration with php-dotenv
core-php automatically includes php-dotenv. Use loadEnv() to load environment variables.
use JulienLinard\Core\Application; $app = Application::create(__DIR__); // Load .env file $app->loadEnv(); // Variables are now available in $_ENV echo $_ENV['DB_HOST'];
Integration with php-validator
core-php automatically includes php-validator. The Core\Form\Validator class uses php-validator internally, providing advanced validation features while maintaining backward compatibility.
use JulienLinard\Core\Form\Validator; $validator = new Validator(); // Use advanced features $validator->setCustomMessages(['email.email' => 'Invalid email']); $validator->setLocale('en'); $validator->setSanitize(true); // Validate with rules $result = $validator->validate($data, [ 'email' => 'required|email', 'password' => 'required|min:8' ]); // Access the underlying php-validator instance for advanced features $phpValidator = $validator->getPhpValidator(); $phpValidator->registerRule(new CustomRule());
Integration with php-cache
core-php automatically includes php-cache. The caching system is available via the Cache class.
use JulienLinard\Core\Application; use JulienLinard\Cache\Cache; $app = Application::create(__DIR__); // Initialize cache (optional, can be done in configuration) Cache::init([ 'default' => 'file', 'drivers' => [ 'file' => [ 'path' => __DIR__ . '/cache', 'prefix' => 'app', 'ttl' => 3600, ], ], ]); // Use cache in your controllers class ProductController extends \JulienLinard\Core\Controller\Controller { #[Route(path: '/products/{id}', methods: ['GET'], name: 'product.show')] public function show(int $id): Response { // Get from cache $product = Cache::get("product_{$id}"); if (!$product) { // Load from database $product = $this->loadProductFromDatabase($id); // Cache with tags Cache::tags(['products', "product_{$id}"])->set("product_{$id}", $product, 3600); } return $this->view('product/show', ['product' => $product]); } #[Route(path: '/products/{id}', methods: ['DELETE'], name: 'product.delete')] public function delete(int $id): Response { // Delete product $this->deleteProductFromDatabase($id); // Invalidate cache Cache::tags(["product_{$id}"])->flush(); return $this->json(['success' => true]); } }
View-Level Caching (Built-in)
The template engine has its own lightweight file cache focused on pure rendered HTML. Use it for fragment/page caching even if you already use php-cache for data.
use JulienLinard\Core\View\View; // Configure (enables automatically if directory provided) View::configureCache(__DIR__.'/storage/view-cache', 600); // 10 min // Optionally toggle View::setCacheEnabled(true); // Render (cached transparently) echo (new View('home/index'))->render(['title' => 'Hello']); // Clear expired entries (max age 0 = all) View::clearCache(0);
When to use which:
- Use
php-cachefor data (arrays, objects, API results). - Use view cache for fully rendered HTML sections/pages.
Security Middleware
Rate Limiting Middleware
Protect endpoints against brute force or abusive traffic.
use JulienLinard\Core\Middleware\RateLimitMiddleware; use JulienLinard\Core\Application; $app = Application::create(__DIR__); $router = $app->getRouter(); // 100 requests / 60s per IP (file storage) $router->addMiddleware(new RateLimitMiddleware(100, 60, __DIR__.'/storage/ratelimit'));
Behavior:
- Tracks counts per IP + route path.
- Returns HTTP 429 with simple body when exceeded.
- Window resets automatically after configured seconds.
- Storage strategy: flat file (extendable to memory/Redis in future versions).
Recommended values:
- Login: 10 / 60s
- Generic API: 100 / 60s
- Expensive endpoints: 20 / 300s
Security Headers Middleware
Add security HTTP headers to protect against common attacks (XSS, clickjacking, MIME sniffing, etc.).
use JulienLinard\Core\Middleware\SecurityHeadersMiddleware; use JulienLinard\Core\Application; $app = Application::create(__DIR__); $router = $app->getRouter(); // Default configuration (good security defaults) $router->addMiddleware(new SecurityHeadersMiddleware()); // Custom configuration $router->addMiddleware(new SecurityHeadersMiddleware([ 'csp' => "default-src 'self'; script-src 'self' 'unsafe-inline'", 'hsts' => 'max-age=31536000; includeSubDomains', 'xFrameOptions' => 'DENY', 'referrerPolicy' => 'strict-origin-when-cross-origin', ]));
Headers included:
- Content-Security-Policy (CSP) - Prevents XSS attacks
- Strict-Transport-Security (HSTS) - Forces HTTPS (only in HTTPS mode)
- X-Frame-Options - Prevents clickjacking (DENY, SAMEORIGIN)
- X-Content-Type-Options - Prevents MIME sniffing (nosniff)
- Referrer-Policy - Controls referrer information
- Permissions-Policy - Controls browser features
- X-XSS-Protection - Legacy XSS protection for older browsers
The middleware automatically detects HTTPS via HTTPS, X-Forwarded-Proto, or port 443.
Performance Middleware
Compression Middleware
Automatically compress HTTP responses with gzip to reduce bandwidth usage.
use JulienLinard\Core\Middleware\CompressionMiddleware; use JulienLinard\Core\Application; $app = Application::create(__DIR__); $router = $app->getRouter(); // Default configuration (compresses responses > 1KB) $router->addMiddleware(new CompressionMiddleware()); // Custom configuration $router->addMiddleware(new CompressionMiddleware([ 'level' => 6, // Compression level (1-9, default: 6) 'minSize' => 1024, // Minimum size to compress in bytes (default: 1024) 'contentTypes' => [ // MIME types to compress 'text/html', 'application/json', 'text/css', ], ]));
Features:
- Automatic gzip compression based on
Accept-Encodingheader - Configurable compression level (1-9)
- Minimum size threshold to avoid compressing small responses
- Content-Type filtering (only compresses specified MIME types
- Adds
Content-Encoding: gzipandVary: Accept-Encodingheaders automatically
Logging with Rotation
The SimpleLogger now supports automatic log rotation to prevent disk space issues.
use JulienLinard\Core\Logging\SimpleLogger; // Default configuration (10MB max, 5 files, compressed) $logger = new SimpleLogger('/var/log/app.log', 'info'); // Custom rotation configuration $logger = new SimpleLogger('/var/log/app.log', 'info', [ 'maxSize' => 10 * 1024 * 1024, // 10MB (default) 'maxFiles' => 5, // Keep 5 archived files (default) 'compress' => true, // Compress archives (default: true) ]); // Log messages (rotation happens automatically when maxSize is reached) $logger->info('Application started'); $logger->error('An error occurred', ['context' => 'value']); // Force rotation manually $logger->rotateNow(); // Get/update rotation configuration $config = $logger->getRotationConfig(); $logger->setRotationConfig(['maxFiles' => 10]);
Features:
- Automatic rotation when file size exceeds
maxSize - File archiving with numbered files (
app.1.log.gz,app.2.log.gz, etc.) - Compression of archived files (optional, saves disk space)
- Automatic cleanup of old files beyond
maxFiles - Configurable log levels (string: 'debug', 'info', etc. or int: 0-7)
- Manual rotation via
rotateNow()method
Rotation behavior:
- When
app.logexceedsmaxSize, it's archived asapp.1.log.gz - Existing archives are shifted (
app.1.log.gz→app.2.log.gz) - Old files beyond
maxFilesare automatically deleted - The current log file is cleared and logging continues
Integration with doctrine-php
Use doctrine-php to manage your entities in your controllers.
use JulienLinard\Core\Controller\Controller; use JulienLinard\Doctrine\EntityManager; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Response; class UserController extends Controller { public function __construct( private EntityManager $em ) {} #[Route(path: '/users/{id}', methods: ['GET'], name: 'user.show')] public function show(int $id): Response { $user = $this->em->getRepository(User::class)->find($id); if (!$user) { return $this->json(['error' => 'User not found'], 404); } return $this->view('user/show', ['user' => $user]); } }
Integration with auth-php
Use auth-php to manage authentication in your controllers.
use JulienLinard\Core\Controller\Controller; use JulienLinard\Auth\AuthManager; use JulienLinard\Router\Attributes\Route; use JulienLinard\Router\Response; class DashboardController extends Controller { public function __construct( private AuthManager $auth ) {} #[Route(path: '/dashboard', methods: ['GET'], name: 'dashboard')] public function index(): Response { if (!$this->auth->check()) { return $this->redirect('/login'); } $user = $this->auth->user(); return $this->view('dashboard/index', ['user' => $user]); } }
Standalone component usage
You can use core-php components independently without Application.
Session standalone
use JulienLinard\Core\Session\Session; // Set a value Session::set('user_id', 123); // Get a value $userId = Session::get('user_id'); // Flash message Session::flash('success', 'Operation successful'); // Remove Session::remove('user_id');
Container standalone
use JulienLinard\Core\Container\Container; $container = new Container(); // Simple binding $container->bind('database', function() { return new Database(); }); // Singleton $container->singleton('logger', function() { return new Logger(); }); // Automatic resolution $service = $container->make(MyService::class);
View standalone
use JulienLinard\Core\View\View; // Full view with layout $view = new View('home/index'); $view->render(['title' => 'Home']); // Partial view (without layout) $view = new View('partials/header', false); $view->render();
Form standalone
use JulienLinard\Core\Form\Validator; $validator = new Validator(); // Validate with rules $result = $validator->validate($data, [ 'email' => 'required|email', 'password' => 'required|min:8' ]); if ($result->hasErrors()) { // Handle errors foreach ($result->getErrors() as $error) { echo $error->getMessage() . "\n"; } }
📚 API Reference
Application
create(string $basePath): self
Creates a new application instance.
$app = Application::create(__DIR__);
getInstance(): ?self
Returns the existing instance or null.
$app = Application::getInstance();
getInstanceOrCreate(?string $basePath = null): self
Returns the existing instance or creates it if it doesn't exist.
$app = Application::getInstanceOrCreate(__DIR__);
getInstanceOrFail(): self
Returns the existing instance or throws an exception.
$app = Application::getInstanceOrFail();
loadEnv(string $file = '.env'): self
Loads environment variables from a .env file.
$app->loadEnv(); $app->loadEnv('.env.local');
setViewsPath(string $path): self
Sets the views path.
$app->setViewsPath(__DIR__ . '/views');
setPartialsPath(string $path): self
Sets the partials path.
$app->setPartialsPath(__DIR__ . '/views/_templates');
getRouter(): Router
Returns the router instance.
$router = $app->getRouter();
start(): void
Starts the application (starts the session).
$app->start();
handle(): void
Processes an HTTP request and sends the response.
$app->handle();
Controller
view(string $template, array $data = []): Response
Renders a view with data.
return $this->view('home/index', ['title' => 'Home']);
json(array $data, int $statusCode = 200): Response
Returns a JSON response.
return $this->json(['message' => 'Success'], 200);
redirect(string $url, int $statusCode = 302): Response
Redirects to a URL.
return $this->redirect('/login');
back(): Response
Redirects to the previous page (if available).
return $this->back();
Important Note: All Controller methods (view(), redirect(), json(), back()) now return a Response instead of calling exit(). This allows middleware chaining and makes testing easier.
Error Handling
The framework includes an improved error handling system with logging and customizable error pages.
ErrorHandler
use JulienLinard\Core\ErrorHandler; use JulienLinard\Core\Exceptions\NotFoundException; use JulienLinard\Core\Exceptions\ValidationException; // ErrorHandler is automatically used by Application $app = Application::create(__DIR__); // Customize ErrorHandler $errorHandler = new ErrorHandler($app, $logger, $debug, $viewsPath); $app->setErrorHandler($errorHandler);
Custom Exceptions
// NotFoundException (404) throw new NotFoundException('User not found'); // ValidationException (422) throw new ValidationException('Validation error', [ 'email' => 'Invalid email', 'password' => 'Password too short' ]);
Customizable Error Pages
Create views in views/errors/ to customize error pages:
views/errors/404.html.php- 404 pageviews/errors/422.html.php- Validation pageviews/errors/500.html.php- Server error page
<!-- views/errors/404.html.php --> <h1><?= htmlspecialchars($title) ?></h1> <p><?= htmlspecialchars($message) ?></p>
Event System
The framework includes an event system (EventDispatcher) for extensibility.
Usage
use JulienLinard\Core\Application; use JulienLinard\Core\Events\EventDispatcher; $app = Application::create(__DIR__); $events = $app->getEvents(); // Listen to an event $events->listen('request.started', function(array $data) { $request = $data['request']; // Log the request, etc. }); $events->listen('exception.thrown', function(array $data) { $exception = $data['exception']; // Send notification, etc. }); // Dispatch a custom event $events->dispatch('user.created', ['user' => $user]);
Built-in Events
request.started: Dispatched at the start of request processingresponse.created: Dispatched after response creationresponse.sent: Dispatched after response is sentexception.thrown: Dispatched when an exception is thrown
Centralized Configuration
The framework allows loading configuration from PHP files in a config/ directory.
use JulienLinard\Core\Application; $app = Application::create(__DIR__); // Load configuration from config/ $app->loadConfig('config'); // Files config/app.php, config/database.php, etc. are automatically loaded // Accessible via $app->getConfig()->get('app.name')
Recommended structure :
config/
app.php # Application configuration
database.php # Database configuration
cache.php # Cache configuration
Example config/app.php :
<?php return [ 'name' => 'My Application', 'debug' => true, 'timezone' => 'Europe/Paris', ];
CSRF Protection
The framework includes a CSRF middleware to protect your forms.
Using CSRF Middleware
use JulienLinard\Core\Middleware\CsrfMiddleware; use JulienLinard\Core\Application; $app = Application::create(__DIR__); $router = $app->getRouter(); // Add CSRF middleware globally $router->addMiddleware(new CsrfMiddleware());
CSRF Helpers in Views
use JulienLinard\Core\View\ViewHelper; // In your forms <form method="POST"> <?= ViewHelper::csrfField() ?> <!-- other fields --> </form> // Or get just the token $token = ViewHelper::csrfToken();
CSRF Configuration
// Customize field name and session key $csrf = new CsrfMiddleware( tokenName: '_csrf_token', // Field name in form sessionKey: '_csrf_token' // Session key );
The CSRF middleware:
- Automatically generates a token for GET requests
- Validates the token for POST, PUT, PATCH, DELETE
- Accepts token via POST data or
X-CSRF-TOKENheader - Generates a new token after each validation
ViewHelper - View Helpers
use JulienLinard\Core\View\ViewHelper; // Escape HTML echo ViewHelper::escape($userInput); echo ViewHelper::e($userInput); // Short alias // Format a date echo ViewHelper::date($date, 'd/m/Y H:i'); // Format a number echo ViewHelper::number(1234.56, 2); // "1,234.56" // Format a price echo ViewHelper::price(99.99); // "99.99 €" // Truncate a string echo ViewHelper::truncate($longText, 100); // CSRF token echo ViewHelper::csrfToken(); echo ViewHelper::csrfField(); // Generate URL from route name $url = ViewHelper::route('user.show', ['id' => 123]); $url = ViewHelper::route('users.index', [], ['page' => 2]); // With query params
📝 License
MIT License - See the LICENSE file for more details.
🤝 Contributing
Contributions are welcome! Feel free to open an issue or a pull request.
💝 Support
If this package is useful to you, consider becoming a sponsor to support the development and maintenance of this open source project.
Developed with ❤️ by Julien Linard