π«π· Read in French | π¬π§ Read in English
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.
composer require julienlinard/core-phpRequirements: PHP 8.0 or higher
<?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();- β 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
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();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']);
}
}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);- 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']);
core-php includes php-validator for advanced form validation with custom rules, multilingual messages, and sanitization.
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
}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);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'));
}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');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);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();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'];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());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]);
}
}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.
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
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.
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
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
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]);
}
}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]);
}
}You can use core-php components independently without Application.
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');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);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();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";
}
}Creates a new application instance.
$app = Application::create(__DIR__);Returns the existing instance or null.
$app = Application::getInstance();Returns the existing instance or creates it if it doesn't exist.
$app = Application::getInstanceOrCreate(__DIR__);Returns the existing instance or throws an exception.
$app = Application::getInstanceOrFail();Loads environment variables from a .env file.
$app->loadEnv();
$app->loadEnv('.env.local');Sets the views path.
$app->setViewsPath(__DIR__ . '/views');Sets the partials path.
$app->setPartialsPath(__DIR__ . '/views/_templates');Returns the router instance.
$router = $app->getRouter();Starts the application (starts the session).
$app->start();Processes an HTTP request and sends the response.
$app->handle();Renders a view with data.
return $this->view('home/index', ['title' => 'Home']);Returns a JSON response.
return $this->json(['message' => 'Success'], 200);Redirects to a URL.
return $this->redirect('/login');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.
The framework includes an improved error handling system with logging and customizable error pages.
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);// NotFoundException (404)
throw new NotFoundException('User not found');
// ValidationException (422)
throw new ValidationException('Validation error', [
'email' => 'Invalid email',
'password' => 'Password too short'
]);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>The framework includes an event system (EventDispatcher) for extensibility.
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]);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
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',
];The framework includes a CSRF middleware to protect your forms.
use JulienLinard\Core\Middleware\CsrfMiddleware;
use JulienLinard\Core\Application;
$app = Application::create(__DIR__);
$router = $app->getRouter();
// Add CSRF middleware globally
$router->addMiddleware(new CsrfMiddleware());use JulienLinard\Core\View\ViewHelper;
// In your forms
<form method="POST">
<?= ViewHelper::csrfField() ?>
<!-- other fields -->
</form>
// Or get just the token
$token = ViewHelper::csrfToken();// 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
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 paramsMIT License - See the LICENSE file for more details.
Contributions are welcome! Feel free to open an issue or a pull request.
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