A lightweight MVC (Model-View-Controller) framework component for PHP 8.4+ that provides core MVC functionality including controllers, views, routing integration, request handling, and a powerful view caching system.
- Installation
- Quick Start
- Core Components
- Configuration
- Usage Examples
- Advanced Features
- CLI Commands
- API Reference
- Testing
- More Information
- PHP 8.4 or higher
- Composer
Install php composer from https://getcomposer.org/
Install the neuron MVC component:
composer require neuron-php/mvcCreate a public/index.php file:
<?phprequire_once'../vendor/autoload.php';
// Bootstrap the application$app = boot('../config');
// Dispatch the current requestdispatch($app);Create a public/.htaccess file to route all requests through the front controller:
IndexIgnore *
Options +FollowSymlinks
RewriteEngineon# Redirect all requests to index.php# except for actual files and directoriesRewriteCond%{REQUEST_FILENAME}!-dRewriteCond%{REQUEST_FILENAME}!-fRewriteRule^(.*)$index.php?route=$1 [L,QSA]If using Nginx, add this to your server configuration:
location / {try_files$uri$uri/ /index.php?route=$uri&$args;}Create a config/neuron.yaml file:
system:
base_path: .views:
path: resources/viewsrouting:
controller_paths:
- path: 'app/Controllers'namespace: 'App\Controllers'The main application class (Neuron\Mvc\Application) handles:
- Route discovery from controller attributes
- Request routing and controller execution
- Event dispatching for HTTP errors
- Output capture for testing
- Cache management
Controllers handle incoming requests and return responses. All controllers should extend Neuron\Mvc\Controllers\Base and implement the IController interface.
namespaceApp\Controllers;
useNeuron\Mvc\Controllers\Base;
useNeuron\Mvc\Responses\HttpResponseStatus;
class HomeController extends Base
{
publicfunctionindex(): string
{
return$this->renderHtml(
HttpResponseStatus::OK,
['title' => 'Welcome'],
'index', // view file'default'// layout file
);
}
}Available render methods:
renderHtml()- Render HTML views with layoutsrenderJson()- Return JSON responsesrenderXml()- Return XML responsesrenderMarkdown()- Render Markdown content with CommonMark
Views support multiple formats and are stored in the configured views directory:
// resources/views/home/index.php
<h1><?phpecho$title; ?></h1>// resources/views/layouts/default.php
<!DOCTYPE html>
<html>
<head>
<title><?phpecho$title ?? 'My App'; ?></title>
</head>
<body>
<?phpecho$Content; ?>
</body>
</html>Routes are defined using PHP attributes on controller methods:
useNeuron\Routing\Attributes\Get;
useNeuron\Routing\Attributes\Post;
useNeuron\Routing\Attributes\RouteGroup;
#[RouteGroup(prefix: '/api', filters: ['auth'])]
class UserController extends Base
{
#[Get('/user/:id', name: 'user_profile')]
publicfunctionprofile(Request$request): string
{
$userId = $request->getRouteParameter('id');
// ...
}
#[Post('/users', name: 'api_users', filters: ['csrf'])]
publicfunctioncreate(Request$request): string
{
// ...
}
}Create request DTO definitions for validation. You can define DTOs inline or reference external DTO files:
Inline DTO Definition:
# config/requests/user_profile.yamlrequest:
method: GETproperties:
id:
type: integerrequired: truerange:
min: 1Referenced DTO:
# config/requests/user_create.yamlrequest:
method: POSTdto: user # References config/Dtos/user.yaml or src/Dtos/user.yaml# config/Dtos/user.yaml (or src/Dtos/user.yaml)dto:
username:
type: stringrequired: truelength:
min: 3max: 20email:
type: emailrequired: trueAccess validated data in controllers:
publicfunctionprofile(Request$request): string
{
$dto = $request->getDto();
$userId = $dto->id;
// ...
}The framework provides Rails-style URL helpers for generating URLs from named routes. This makes it easy to generate consistent URLs throughout your application.
Routes are automatically named based on their configuration key in the YAML file:
routes:
user_profile: # This becomes the route nameroute: /users/{id}method: GETcontroller: App\Controllers\UserController@profileadmin_user_posts:
route: /admin/users/{user_id}/posts/{post_id}method: GETcontroller: App\Controllers\AdminController@userPostsControllers can use URL helpers directly via magic methods:
class UserController extends Base
{
publicfunctionshow($id): string
{
$user = User::find($id);
// Magic methods for URL generation$editUrl = $this->userEditPath(['id' => $id]);
$absoluteUrl = $this->userProfileUrl(['id' => $id]);
// Use in redirectsif (!$user) {
returnredirect($this->userIndexPath());
}
return$this->renderHtml(HttpResponseStatus::OK, [
'user' => $user,
'edit_url' => $editUrl
]);
}
publicfunctioncreate(): string
{
// After creating user, redirect using magic method$user = newUser($request->all());
$user->save();
returnredirect($this->userProfilePath(['id' => $user->id]));
}
}Controllers also provide direct helper methods:
// Generate relative URLs$profileUrl = $this->urlFor('user_profile', ['id' => 123]);
// Generate absolute URLs $absoluteUrl = $this->urlForAbsolute('user_profile', ['id' => 123]);
// Check if route existsif ($this->routeExists('user_profile')) {
// Route is available
}
// Get UrlHelper instance for advanced usage$urlHelper = $this->urlHelper();URL helpers are automatically available in all views through the injected $urlHelper variable:
<!-- resources/views/user/profile.php -->
<div class="user-profile"> <h1><?= $user->name ?></h1> <!-- Magic methods in views --> <a href="<?= $urlHelper->userEditPath(['id' => $user->id]) ?>" class="btn">Edit</a>
<a href="<?=$urlHelper->userPostsPath(['user_id' => $user->id]) ?>" class="btn">View Posts</a>
<!-- Complex routes work too -->
<a href="<?=$urlHelper->adminUserReportsPath(['id' => $user->id, 'year' => date('Y')]) ?>">
Admin Reports
</a>
<!-- Direct method calls -->
<a href="<?=$urlHelper->routePath('user_profile', ['id' => $user->id]) ?>">Profile</a>
<a href="<?=$urlHelper->routeUrl('user_profile', ['id' => $user->id]) ?>">Share Link</a>
</div>The magic methods follow Rails naming conventions:
| Route Name in YAML | Magic Method (Relative) | Magic Method (Absolute) | Generated URL |
|---|---|---|---|
user_profile | userProfilePath() | userProfileUrl() | /users/123 |
user_edit | userEditPath() | userEditUrl() | /users/123/edit |
admin_user_posts | adminUserPostsPath() | adminUserPostsUrl() | /admin/users/1/posts/2 |
blog_category | blogCategoryPath() | blogCategoryUrl() | /blog/category/tech |
| Method | Description | Example |
|---|---|---|
routePath($name, $params) | Generate relative URL | $urlHelper->routePath('user_profile', ['id' => 123]) |
routeUrl($name, $params) | Generate absolute URL | $urlHelper->routeUrl('user_profile', ['id' => 123]) |
routeExists($name) | Check if route exists | $urlHelper->routeExists('user_profile') |
getAvailableRoutes() | List all named routes | $urlHelper->getAvailableRoutes() |
{routeName}Path($params) | Magic method for relative URL | $urlHelper->userProfilePath(['id' => 123]) |
{routeName}Url($params) | Magic method for absolute URL | $urlHelper->userProfileUrl(['id' => 123]) |
URL helpers gracefully handle missing routes:
// Returns null if route doesn't exist$url = $urlHelper->nonExistentRoutePath(['id' => 123]);
if ($url === null) {
// Handle missing route$url = $urlHelper->userIndexPath(); // fallback
}// Get all available routes for debugging$routes = $urlHelper->getAvailableRoutes();
foreach ($routesas$route) {
echo"Route: {$route['name']} -> {$route['method']}{$route['path']}\n";
}
// Custom UrlHelper instance$customHelper = newUrlHelper($customRouter);All YAML config file parameters can be overridden by environment variables in the form of <CATEGORY>_<KEY>, e.g.
SYSTEM_BASE_PATH.
# System settingssystem:
timezone: US/Easternbase_path: .# View settingsviews:
path: resources/views# Logginglogging:
destination: \Neuron\Log\Destination\Fileformat: \Neuron\Log\Format\PlainTextfile: app.loglevel: debug# Cache configurationcache:
enabled: truestorage: filepath: cache/viewsttl: 3600# Default TTL in secondshtml: true # Enable HTML view cachingmarkdown: true # Enable Markdown view cachingjson: false # Disable JSON response cachingxml: false # Disable XML response caching# Garbage collection settings (optional)gc_probability: 0.01# 1% chance to run GC on cache writegc_divisor: 100# Fine-tune probability calculationRouting configuration is now handled in a dedicated config/routing.yaml file. This separates routing concerns from the main application configuration.
# config/routing.yaml# URL Rewrites (transparent, no HTTP redirect)rewrites:
'/': '/home'# Root goes to homepage'/index': '/home'# Legacy URL support'/index.php': '/home'# Handle old PHP URLs# Controller paths for route scanningcontroller_paths:
- path: 'app/Controllers'namespace: 'App\Controllers'
- path: 'app/Admin/Controllers'namespace: 'App\Admin\Controllers'Key Features:
URL Rewrites: Transparently rewrite URLs before route matching
- No HTTP redirects (faster, invisible to client)
- Override package-provided routes
- Support legacy URLs without duplicate routes
Controller Paths: Specify where to scan for route attributes
- Order matters: first paths take precedence
- Allows overriding routes from packages
Backward Compatibility:
For backward compatibility, controller_paths can still be configured in neuron.yaml:
routing:
controller_paths:
- path: 'app/Controllers'namespace: 'App\Controllers'If both files exist, routing.yaml takes precedence.
| Option | Description | Default |
|---|---|---|
enabled | Enable/disable caching globally | true |
storage | Storage type (currently only 'file') | file |
path | Directory for cache files | cache/views |
ttl | Default time-to-live in seconds | 3600 |
views.* | Enable caching per view type | varies |
gc_probability | Probability of running garbage collection | 0.01 |
gc_divisor | Divisor for probability calculation | 100 |
namespaceApp\Controllers;
useNeuron\Mvc\Controllers\Base;
useNeuron\Mvc\Requests\Request;
useNeuron\Mvc\Responses\HttpResponseStatus;
class ProductController extends Base
{
publicfunctionlist(): string
{
$products = $this->getProducts();
return$this->renderHtml(
HttpResponseStatus::OK,
['products' => $products],
'list',
'default'
);
}
publicfunctionapiList(): string
{
$products = $this->getProducts();
return$this->renderJson(
HttpResponseStatus::OK,
['products' => $products]
);
}
publicfunctiondetails(Request$request): string
{
$id = $request->getRouteParameter('id');
$product = $this->getProduct($id);
if (!$product) {
return$this->renderHtml(
HttpResponseStatus::NOT_FOUND,
['message' => 'Product not found'],
'error',
'default'
);
}
return$this->renderHtml(
HttpResponseStatus::OK,
['product' => $product],
'details',
'default'
);
}
}Define request DTOs in YAML:
# config/requests/product_create.yamlrequest:
method: POSTheaders:
Content-Type: application/jsonproperties:
name:
type: stringrequired: truelength:
min: 3max: 100price:
type: currencyrequired: truerange:
min: 0category_id:
type: integerrequired: truedescription:
type: stringrequired: falselength:
max: 1000Available property types:
string,integer,float,booleanemail,url,uuiddate,date_time,timecurrency,us_phone_number,intl_phone_numberarray,objectip_address,ein,upc,name,numeric
The framework automatically handles 404 errors:
// Custom 404 handlerclass NotFoundController extends HttpCodes
{
publicfunctionrender404(): string
{
return$this->renderHtml(
HttpResponseStatus::NOT_FOUND,
['message' => 'Page not found'],
'404',
'error'
);
}
}The framework includes a sophisticated view caching system with multiple storage backends:
- File Storage (Default): Uses the local filesystem for cache storage
- Redis Storage: High-performance in-memory caching with Redis
- Automatic Cache Key Generation: Based on controller, view, and data
- Selective Caching: Enable/disable per view type
- TTL Support: Configure expiration times
- Garbage Collection: Automatic cleanup of expired entries
- Multiple Storage Backends: Choose between file or Redis storage
cache:
enabled: truestorage: filepath: cache/viewsttl: 3600views:
html: truemarkdown: truejson: falsexml: falsecache:
enabled: truestorage: redis # Use Redis instead of file storagettl: 3600# Redis configuration (flat structure for env variable compatibility)redis_host: 127.0.0.1redis_port: 6379redis_database: 0redis_prefix: neuron_cache_redis_timeout: 2.0redis_auth: null # Optional: Redis passwordredis_persistent: false # Optional: Use persistent connections# View-specific cache settingshtml: truemarkdown: truejson: falsexml: falseThis flat structure ensures compatibility with environment variables:
CACHE_STORAGE=redisCACHE_REDIS_HOST=127.0.0.1CACHE_REDIS_PORT=6379- etc.
// Cache is automatically used when enabled$html = $this->renderHtml(
HttpResponseStatus::OK,
$data,
'cached-view',
'layout'
);// Clear all expired cache entries (file storage only)$removed = ClearExpiredCache($app);
echo"Removed $removed expired cache entries";
// Clear all cache$app->getViewCache()->clear();useNeuron\Mvc\Cache\Storage\CacheStorageFactory;
// Create storage based on configuration$storage = CacheStorageFactory::create([
'storage' => 'redis',
'redis_host' => 'localhost',
'redis_port' => 6379,
'redis_database' => 0,
'redis_prefix' => 'neuron_cache_'
]);
// Auto-detect best available storage$storage = CacheStorageFactory::createAutoDetect();
// Check storage availabilityif (CacheStorageFactory::isAvailable('redis')) {
echo"Redis cache is available";
}You can also manage cache using the CLI commands. See CLI Commands section for details.
Create custom view types by implementing IView:
namespaceApp\Views;
useNeuron\Mvc\Views\IView;
class PdfView implements IView
{
publicfunctionrender(array$Data): string
{
// Generate PDF contentreturn$pdfContent;
}
}Listen for HTTP events:
# config/events.yamllisteners:
http_404:
class: App\Listeners\NotFoundLoggermethod: logNotFoundThe MVC component includes several CLI commands for managing cache and routes. These commands are available when using the Neuron CLI tool.
Clear view cache entries.
Options:
--type, -t VALUE- Clear specific cache type (html, json, xml, markdown)--expired, -e- Only clear expired entries--force, -f- Clear without confirmation--config, -c PATH- Path to configuration directory
Examples:
# Clear all cache entries (with confirmation)
neuron mvc:cache:clear
# Clear only expired entries
neuron mvc:cache:clear --expired
# Clear specific cache type
neuron mvc:cache:clear --type=html
# Force clear without confirmation
neuron mvc:cache:clear --force
# Specify custom config path
neuron mvc:cache:clear --config=/path/to/configDisplay comprehensive cache statistics.
Options:
--config, -c PATH- Path to configuration directory--json, -j- Output statistics in JSON format--detailed, -d- Show detailed breakdown by view type
Examples:
# Display cache statistics
neuron mvc:cache:stats
# Show detailed statistics with view type breakdown
neuron mvc:cache:stats --detailed
# Output as JSON for scripting
neuron mvc:cache:stats --json
# Detailed JSON output
neuron mvc:cache:stats --detailed --jsonSample Output:
MVC View Cache Statistics
==================================================
Configuration:
Cache Path: /path/to/cache/views
Cache Enabled: Yes
Default TTL: 3600 seconds (1 hour)
Overall Statistics:
Total Cache Entries: 247
Valid Entries: 189
Expired Entries: 58
Total Cache Size: 2.4 MB
Average Entry Size: 10.2 KB
Oldest Entry: 2025-08-10 14:23:15
Newest Entry: 2025-08-13 09:45:32
Recommendations:
- 58 expired entries can be cleared (saving ~580 KB)
Run: neuron mvc:cache:clear --expired
The MVC component includes integrated rate limiting support through the routing component. Rate limiting helps protect your application from abuse and ensures fair resource usage.
Rate limiting is configured in your neuron.yaml file using two categories:
rate_limit:
enabled: false # Enable/disable rate limitingglobal: false # Apply to all routes globallystorage: file # Storage backend: file, redis, memory (testing only)requests: 100# Maximum requests per windowwindow: 3600# Time window in seconds (1 hour)file_path: cache/rate_limits# Redis configuration (if storage: redis)# redis_host: 127.0.0.1# redis_port: 6379api_limit:
enabled: falsestorage: filerequests: 1000# 1000 requests per hourwindow: 3600file_path: cache/api_limitsConfiguration maps to environment variables using the {category}_{name} pattern:
RATE_LIMIT_ENABLED=trueRATE_LIMIT_STORAGE=redisRATE_LIMIT_REQUESTS=100API_LIMIT_ENABLED=trueAPI_LIMIT_REQUESTS=1000
Set global: true in configuration to apply rate limiting to all routes:
rate_limit:
enabled: trueglobal: truerequests: 100window: 3600Apply rate limiting to specific routes using the filters parameter in route attributes:
useNeuron\Routing\Attributes\Get;
class HomeController extends Base
{
// Public page - no rate limiting
#[Get('/', name: 'home')]
publicfunctionindex(Request$request): string
{
// ...
}
}
class UserController extends Base
{
// Standard protected route with rate limiting
#[Get('/user/profile', name: 'user_profile', filters: ['rate_limit'])]
publicfunctionprofile(Request$request): string
{
// Apply rate_limit (100/hour)// ...
}
}
class ApiController extends Base
{
// API endpoint with higher limits
#[Get('/api/users', name: 'api_users', filters: ['api_limit'])]
publicfunctionusers(Request$request): string
{
// Apply api_limit (1000/hour)// ...
}
}Best for single-server deployments:
rate_limit:
storage: filefile_path: cache/rate_limits # Directory for rate limit filesBest for distributed systems and high traffic:
rate_limit:
storage: redisredis_host: 127.0.0.1redis_port: 6379redis_database: 0redis_prefix: rate_limit_redis_auth: password # Optionalredis_persistent: true # Use persistent connectionsFor unit tests and development. Data is lost when PHP process ends:
rate_limit:
storage: memoryWhen rate limiting is active, the following headers are included in responses:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remaining in current windowX-RateLimit-Reset: Unix timestamp when limit resets
When limit is exceeded (HTTP 429):
Retry-After: Seconds until retry is allowed
- Enable rate limiting in
neuron.yaml:
rate_limit:
enabled: trueglobal: falsestorage: redisrequests: 100window: 3600redis_host: 127.0.0.1api_limit:
enabled: truestorage: redisrequests: 1000window: 3600redis_host: 127.0.0.1- Apply to routes using attributes:
useNeuron\Routing\Attributes\Post;
useNeuron\Routing\Attributes\Get;
class AuthController extends Base
{
#[Post('/auth/login', name: 'login', filters: ['rate_limit'])]
publicfunctionlogin(Request$request): string
{
// Strict limit for login attempts// ...
}
}
class ApiController extends Base
{
#[Get('/api/data', name: 'api_data', filters: ['api_limit'])]
publicfunctiongetData(Request$request): string
{
// Higher limit for API access// ...
}
}For advanced use cases, you can extend the rate limiting system by creating custom filters in your application. The rate limiting system automatically detects if the routing component version supports it and gracefully degrades if not available.
List all registered routes with filtering options.
Options:
--config, -c PATH- Path to configuration directory--controller VALUE- Filter by controller name--method, -m VALUE- Filter by HTTP method (GET, POST, PUT, DELETE, etc.)--pattern, -p VALUE- Search routes by pattern--json, -j- Output routes in JSON format
Examples:
# List all routes
neuron mvc:routes:list
# Filter by controller
neuron mvc:routes:list --controller=UserController
# Filter by HTTP method
neuron mvc:routes:list --method=POST
# Search by pattern
neuron mvc:routes:list --pattern=/api/
# Combine filters
neuron mvc:routes:list --controller=Api --method=GET
# Output as JSON for processing
neuron mvc:routes:list --jsonSample Output:
MVC Routes
======================================================================================
Name | Pattern | Method | Controller | Action
--------------------------------------------------------------------------------------
home | / | GET | HomeController | index
user_profile | /user/{id} | GET | UserController | profile
api_users_list | /api/users | GET | Api\UserController | list
api_users_create | /api/users | POST | Api\UserController | create
products_list | /products | GET | ProductController | list
product_details | /products/{id} | GET | ProductController | details
Total routes: 6
Named routes: 6
Methods: GET: 4, POST: 2
Initialize the application with configuration.
$app = Boot('/path/to/config');Process the current HTTP request.
Dispatch($app);Remove expired cache entries.
$removed = ClearExpiredCache($app);All controllers must implement this interface:
renderHtml()- Render HTML with layoutrenderJson()- Render JSON responserenderXml()- Render XML response
Views must implement:
render(array $Data): string- Render the view
Cache storage implementations must provide:
read(),write(),exists(),delete()clear()- Clear all entriesisExpired()- Check expirationgc()- Garbage collection
Run the test suite:
# Run all tests
vendor/bin/phpunit -c tests/phpunit.xml
# Run with coverage
vendor/bin/phpunit -c tests/phpunit.xml --coverage-html coverage
# Run specific test file
vendor/bin/phpunit -c tests/phpunit.xml tests/Mvc/ApplicationTest.phpYou can read more about the Neuron components at neuronphp.com