paigejulianne/nanomvc
Composer 安装命令:
composer require paigejulianne/nanomvc
包简介
NanoMVC: a lightweight MVC framework with Blade, Smarty, and Twig template support
README 文档
README
A lightweight MVC framework for PHP 8.0+ with support for Blade, Smarty, and Twig templates.
Version 1.0.1 | Changelog | License: GPL-3.0
by Paige Julianne Sullivan paigejulianne.com | GitHub
Features
- Enterprise-scale performance: O(1) route lookup, route caching, middleware caching
- Multiple template engines: PHP, Blade, Smarty, and Twig support
- Simple routing: Clean URL routing with parameters and groups
- Zero dependencies: Only requires PHP 8.0+ (template engines optional)
- Built-in validation: Request validation with helpful error messages
- Session management: Secure sessions with CSRF protection
- Rate limiting: Configurable throttling for API endpoints
- Response compression: Automatic gzip compression
- Integrates with NanoORM: Seamlessly works with NanoORM for database operations
Prerequisites
- PHP 8.0+ with the following extensions:
json(usually enabled by default)mbstring(recommended)
- Apache with
mod_rewriteenabled, or Nginx - Composer (for installation and autoloading)
Installation
Via Composer (Recommended)
composer require paigejulianne/nanomvc
Manual Installation
Download NanoMVC.php and include it in your project:
require_once 'NanoMVC.php';
Examples
NanoMVC includes four example applications demonstrating each template engine:
| Example | Template Engine | Directory |
|---|---|---|
| PHP (Native) | Native PHP | example/ |
| Blade | Laravel Blade | example-blade/ |
| Smarty | Smarty | example-smarty/ |
| Twig | Twig | example-twig/ |
Running the Examples
-
Install dependencies:
composer install
-
For Blade example, also install:
composer require jenssegers/blade
-
For Smarty example, also install:
composer require smarty/smarty
-
For Twig example, also install:
composer require twig/twig
-
Set file permissions:
chmod 644 example*/.htaccess chmod 755 example*/cache
-
Access in your browser:
- PHP:
http://localhost/path/to/nanomvc/example/ - Blade:
http://localhost/path/to/nanomvc/example-blade/ - Smarty:
http://localhost/path/to/nanomvc/example-smarty/ - Twig:
http://localhost/path/to/nanomvc/example-twig/
- PHP:
Note: Ensure Apache
mod_rewriteis enabled andAllowOverride Allis set for your directory. See Apache Configuration for details.
Quick Start
1. Create Your Entry Point
Create index.php:
<?php require_once 'vendor/autoload.php'; use PaigeJulianne\NanoMVC\App; App::run(__DIR__);
2. Create Configuration (Optional)
Create .config:
[app] debug=true [views] engine=php path=views cache=cache
3. Define Routes
Create routes.php:
<?php use PaigeJulianne\NanoMVC\Router; Router::get('/', [HomeController::class, 'index']); Router::get('/users/{id}', [UsersController::class, 'show']); Router::post('/users', [UsersController::class, 'store']);
4. Create a Controller
Create controllers/HomeController.php:
<?php use PaigeJulianne\NanoMVC\Controller; use PaigeJulianne\NanoMVC\Request; use PaigeJulianne\NanoMVC\Response; class HomeController extends Controller { public function index(Request $request): Response { return $this->view('home', [ 'title' => 'Welcome', 'message' => 'Hello, World!', ]); } }
5. Create a View
Create views/home.php:
<!DOCTYPE html> <html> <head> <title><?= htmlspecialchars($title) ?></title> </head> <body> <h1><?= htmlspecialchars($message) ?></h1> </body> </html>
Configuration
Using a .config File
[app] debug=true name=My Application [views] engine=php # php, blade, smarty, or twig path=views cache=cache [routes] file=routes.php
Programmatic Configuration
use PaigeJulianne\NanoMVC\App; use PaigeJulianne\NanoMVC\View; App::setDebug(true); App::setConfig('app.name', 'My App'); View::configure('/path/to/views', '/path/to/cache', 'blade');
Routing
Basic Routes
use PaigeJulianne\NanoMVC\Router; Router::get('/path', $handler); Router::post('/path', $handler); Router::put('/path', $handler); Router::patch('/path', $handler); Router::delete('/path', $handler); Router::any('/path', $handler); // Matches any method
Route Parameters
// Required parameter Router::get('/users/{id}', function (Request $request) { $id = $request->param('id'); return "User ID: $id"; }); // Multiple parameters Router::get('/posts/{year}/{month}/{slug}', function (Request $request) { return $request->params(); // ['year' => '2024', 'month' => '12', 'slug' => 'hello'] });
Route Groups
Router::group(['prefix' => 'api'], function () { Router::get('/users', [ApiController::class, 'users']); Router::get('/posts', [ApiController::class, 'posts']); }); // Creates: /api/users and /api/posts
Nested Groups
Router::group(['prefix' => 'api'], function () { Router::group(['prefix' => 'v1'], function () { Router::get('/users', [ApiV1Controller::class, 'users']); }); }); // Creates: /api/v1/users
Route Handlers
Routes can use closures or controller methods:
// Closure Router::get('/hello', function (Request $request) { return 'Hello, World!'; }); // Controller method [ClassName, 'methodName'] Router::get('/users', [UsersController::class, 'index']);
Middleware
// Inline middleware Router::get('/admin', [AdminController::class, 'index'], [ function (Request $request) { if (!isLoggedIn()) { return Response::redirect('/login'); } return null; // Continue to handler } ]); // Middleware class Router::get('/dashboard', [DashboardController::class, 'index'], [ AuthMiddleware::class ]);
Custom Error Handlers
// Custom 404 handler Router::setNotFoundHandler(function (Request $request) { return View::make('errors.404', [], 404); }); // Custom error handler Router::setErrorHandler(function (\Throwable $e, Request $request) { return View::make('errors.500', ['error' => $e->getMessage()], 500); });
Route Caching (Production)
For large applications, cache compiled routes for faster startup:
// Generate route cache (run during deployment) require 'routes.php'; Router::cacheRoutes('/path/to/cache/routes.php'); // Load cached routes in production if (file_exists('/path/to/cache/routes.php')) { Router::loadCachedRoutes('/path/to/cache/routes.php'); } else { require 'routes.php'; }
Route Statistics
Monitor route performance:
$stats = Router::getStats(); // Returns: [ // 'total_routes' => 150, // 'static_routes' => 80, // 'dynamic_routes' => 70, // 'by_method' => ['GET' => 100, 'POST' => 50], // 'cached_middleware' => 5, // 'routes_cached' => true // ]
Controllers
Creating Controllers
Extend the base Controller class:
use PaigeJulianne\NanoMVC\Controller; use PaigeJulianne\NanoMVC\Request; use PaigeJulianne\NanoMVC\Response; class UsersController extends Controller { public function index(Request $request): Response { $users = Users::getAllObjects(); return $this->view('users.index', ['users' => $users]); } public function show(Request $request): Response { $id = $request->param('id'); $user = new Users($id); return $this->view('users.show', ['user' => $user]); } public function store(Request $request): Response { $data = $this->validate([ 'name' => 'required|min:2|max:100', 'email' => 'required|email', ]); $user = new Users(); $user->setMulti($data); $user->save(); return $this->redirect('/users/' . $user->getId()); } }
Response Methods
// Render a view $this->view('template', ['data' => 'value'], 200); // JSON response $this->json(['key' => 'value'], 200); // Redirect $this->redirect('/path', 302); // Plain text $this->text('Plain text', 200); // HTML $this->html('<h1>HTML</h1>', 200);
Validation
$data = $this->validate([ 'name' => 'required|min:2|max:100', 'email' => 'required|email', 'age' => 'numeric|min:1', 'role' => 'in:admin,user,guest', ]);
Available Rules:
required- Field must be present and not emptyemail- Must be a valid email addressnumeric- Must be numericinteger- Must be an integermin:n- Minimum string lengthmax:n- Maximum string lengthin:a,b,c- Must be one of the specified valuesurl- Must be a valid URLalpha- Must contain only lettersalphanumeric- Must contain only letters and numbers
Request Object
Getting Input
// Query parameters (?foo=bar) $request->query('foo'); $request->query('foo', 'default'); $request->allQuery(); // POST data $request->input('field'); $request->input('field', 'default'); // All input (POST + GET) $request->all(); $request->only(['field1', 'field2']); $request->except(['password']); // Check if exists $request->has('field');
Route Parameters
$request->param('id'); $request->param('id', 'default'); $request->params(); // All route params
Request Info
$request->method(); // GET, POST, etc. $request->path(); // /users/123 $request->header('Accept'); $request->cookie('session'); $request->isAjax(); $request->expectsJson(); $request->getContent(); // Raw body $request->json(); // JSON decoded body
Response Object
Creating Responses
use PaigeJulianne\NanoMVC\Response; // JSON $response = Response::json(['data' => 'value'], 200); // Redirect $response = Response::redirect('/path', 302); // Plain text $response = Response::text('Content', 200); // HTML $response = Response::html('<h1>Hello</h1>', 200);
Modifying Responses
$response = new Response(); $response->setContent('Hello') ->setStatusCode(200) ->header('X-Custom', 'value') ->withHeaders(['X-A' => '1', 'X-B' => '2']);
Response Compression
Responses are automatically gzip-compressed when the client supports it:
// Configure compression (optional) Response::configureCompression( threshold: 1024, // Min bytes to compress (default 1KB) level: 6 // Compression level 0-9 (default 6) ); // Disable compression for specific response return Response::json($data)->withoutCompression(); // Re-enable compression return Response::html($content)->withCompression();
Views
Template Engines
PHP (Native) - Default, no dependencies:
View::configure('/path/to/views', '/path/to/cache', 'php');
Blade - Requires jenssegers/blade:
composer require jenssegers/blade
View::configure('/path/to/views', '/path/to/cache', 'blade');
Smarty - Requires smarty/smarty:
composer require smarty/smarty
View::configure('/path/to/views', '/path/to/cache', 'smarty');
Twig - Requires twig/twig:
composer require twig/twig
View::configure('/path/to/views', '/path/to/cache', 'twig');
Rendering Views
// In controller return $this->view('users.index', ['users' => $users]); // Directly $html = View::render('template', ['data' => 'value']); // As response $response = View::make('template', ['data' => 'value'], 200);
Shared Data
// Share with all views View::share('appName', 'My App'); View::share(['key1' => 'value1', 'key2' => 'value2']);
Template Examples
PHP Template (views/users/index.php):
<h1><?= htmlspecialchars($title) ?></h1> <ul> <?php foreach ($users as $user): ?> <li><?= htmlspecialchars($user->name) ?></li> <?php endforeach; ?> </ul>
Blade Template (views/users/index.blade.php):
<h1>{{ $title }}</h1> <ul> @foreach ($users as $user) <li>{{ $user->name }}</li> @endforeach </ul>
Smarty Template (views/users/index.tpl):
<h1>{$title}</h1> <ul> {foreach $users as $user} <li>{$user->name}</li> {/foreach} </ul>
Twig Template (views/users/index.twig):
<h1>{{ title }}</h1> <ul> {% for user in users %} <li>{{ user.name }}</li> {% endfor %} </ul>
Integration with NanoORM
Creating Models
use PaigeJulianne\NanoORM; class Users extends NanoORM { // Maps to 'users' table automatically } class BlogPost extends NanoORM { const TABLE_OVERRIDE = 'blog_posts'; }
Using Models in Controllers
class UsersController extends Controller { public function index(Request $request): Response { $users = Users::getAllObjects(); return $this->view('users.index', ['users' => $users]); } public function show(Request $request): Response { $user = new Users($request->param('id')); return $this->view('users.show', ['user' => $user]); } public function store(Request $request): Response { $data = $this->validate([ 'name' => 'required', 'email' => 'required|email', ]); $user = new Users(); $user->setMulti($data); $user->save(); return $this->redirect('/users/' . $user->getId()); } public function destroy(Request $request): Response { $user = new Users($request->param('id')); $user->delete(); return $this->redirect('/users'); } }
Session Management
Basic Usage
use PaigeJulianne\NanoMVC\Session; // Set and get values Session::set('user_id', 123); $userId = Session::get('user_id'); $name = Session::get('name', 'Guest'); // With default // Check and remove if (Session::has('user_id')) { Session::forget('user_id'); } // Get all session data $all = Session::all(); // Clear all data Session::flush(); // Destroy session completely Session::destroy();
Flash Messages
// Set flash message (available only on next request) Session::flash('success', 'User created successfully!'); // Get flash message (automatically removed) $message = Session::getFlash('success');
CSRF Protection
// Get CSRF token (for forms) $token = Session::csrfToken(); // In your form <input type="hidden" name="_token" value="<?= Session::csrfToken() ?>"> // Verify token (done automatically by CsrfMiddleware) if (Session::verifyCsrfToken($request->input('_token'))) { // Valid token }
Session Configuration
Session::configure([ 'name' => 'my_app_session', 'lifetime' => 7200, // 2 hours 'path' => '/', 'domain' => '', 'secure' => true, // HTTPS only 'httponly' => true, // No JavaScript access 'samesite' => 'Lax', // CSRF protection ]);
Custom Session Storage
use PaigeJulianne\NanoMVC\FileSessionDriver; // Use file-based sessions with custom path $driver = new FileSessionDriver('/path/to/sessions', 7200); Session::setDriver($driver);
Rate Limiting
Basic Usage
use PaigeJulianne\NanoMVC\RateLimiter; use PaigeJulianne\NanoMVC\FileRateLimitStore; // Configure storage (required for production) RateLimiter::setStore(new FileRateLimitStore('/path/to/storage')); // Check rate limit $key = 'api:' . $userId; if (RateLimiter::attempt($key, maxAttempts: 60, decaySeconds: 60)) { // Within limit - process request } else { // Rate limited $retryAfter = RateLimiter::availableIn($key); } // Get remaining attempts $remaining = RateLimiter::remaining($key, 60); // Clear rate limit RateLimiter::clear($key);
Throttle Middleware
use PaigeJulianne\NanoMVC\ThrottleMiddleware; // 60 requests per minute Router::get('/api/users', [ApiController::class, 'index'], [ new ThrottleMiddleware(60, 1) ]); // 100 requests per hour with custom key Router::post('/api/search', [ApiController::class, 'search'], [ new ThrottleMiddleware(100, 60, fn($req) => $req->header('X-API-Key')) ]); // Apply to route group Router::group(['prefix' => 'api', 'middleware' => [new ThrottleMiddleware(120, 1)]], function () { Router::get('/users', [ApiController::class, 'users']); Router::get('/posts', [ApiController::class, 'posts']); });
Security Middleware
CSRF Middleware
use PaigeJulianne\NanoMVC\CsrfMiddleware; // Apply to all POST/PUT/DELETE routes Router::group(['middleware' => [new CsrfMiddleware()]], function () { Router::post('/users', [UsersController::class, 'store']); Router::delete('/users/{id}', [UsersController::class, 'destroy']); }); // Exclude specific paths (e.g., webhooks) $csrf = new CsrfMiddleware(['/api/webhooks/*', '/api/stripe/*']);
CORS Middleware
use PaigeJulianne\NanoMVC\CorsMiddleware; // Allow all origins Router::group(['prefix' => 'api', 'middleware' => [new CorsMiddleware()]], function () { Router::get('/users', [ApiController::class, 'users']); }); // Configure specific origins $cors = new CorsMiddleware([ 'allowed_origins' => ['https://example.com', 'https://app.example.com'], 'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'], 'allowed_headers' => ['Content-Type', 'Authorization', 'X-API-Key'], 'exposed_headers' => ['X-RateLimit-Remaining'], 'max_age' => 86400, 'supports_credentials' => true, ]);
Directory Structure
myapp/
├── .config # Configuration
├── .htaccess # Apache rewrite rules
├── index.php # Entry point
├── routes.php # Route definitions
├── controllers/
│ ├── HomeController.php
│ └── UsersController.php
├── models/
│ └── Users.php
├── views/
│ ├── layout.php
│ ├── home.php
│ └── users/
│ ├── index.php
│ └── show.php
└── cache/ # Template cache
Apache Configuration
Basic .htaccess
Create .htaccess in your application directory:
<IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule .* index.php [L] </IfModule>
Note: Do not use
RewriteBaseunless your application is installed at the web root. The above configuration works for subdirectory installations.
File Permissions
Ensure Apache can read the .htaccess file:
chmod 644 .htaccess
Enabling mod_rewrite
If mod_rewrite is not enabled, run:
sudo a2enmod rewrite sudo systemctl restart apache2
Apache User Directories (~username)
If running NanoMVC in a user directory (e.g., http://localhost/~username/myapp/), you need to configure Apache to allow .htaccess overrides.
Edit /etc/apache2/mods-available/userdir.conf:
<Directory /home/*/public_html> AllowOverride All Options All Require all granted </Directory>
Then restart Apache:
sudo systemctl restart apache2
Subdirectory Installation
NanoMVC automatically detects when installed in a subdirectory and adjusts routing accordingly. For links in your views, calculate the base URL:
<?php $baseUrl = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'); ?> <a href="<?= $baseUrl ?>/about">About</a>
Nginx Configuration
location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }
Testing
Run tests with PHPUnit:
composer test
API Reference
Router
| Method | Description |
|---|---|
get($path, $handler, $middleware) |
Register GET route |
post($path, $handler, $middleware) |
Register POST route |
put($path, $handler, $middleware) |
Register PUT route |
patch($path, $handler, $middleware) |
Register PATCH route |
delete($path, $handler, $middleware) |
Register DELETE route |
any($path, $handler, $middleware) |
Register route for all methods |
match($methods, $path, $handler, $middleware) |
Register route for specific methods |
group($options, $callback) |
Create route group |
dispatch($request) |
Dispatch request to handler |
setNotFoundHandler($handler) |
Set 404 handler |
setErrorHandler($handler) |
Set error handler |
Controller
| Method | Description |
|---|---|
view($template, $data, $status) |
Render view response |
json($data, $status) |
JSON response |
redirect($url, $status) |
Redirect response |
text($content, $status) |
Plain text response |
html($content, $status) |
HTML response |
validate($rules) |
Validate request input |
request() |
Get current request |
Request
| Method | Description |
|---|---|
method() |
Get HTTP method |
path() |
Get request path |
query($key, $default) |
Get query parameter |
input($key, $default) |
Get input value |
all() |
Get all input |
only($keys) |
Get specific keys |
except($keys) |
Get all except keys |
has($key) |
Check if key exists |
param($name, $default) |
Get route parameter |
params() |
Get all route parameters |
header($name, $default) |
Get header |
cookie($name, $default) |
Get cookie |
isAjax() |
Check if AJAX request |
expectsJson() |
Check if expects JSON |
json() |
Get JSON body |
Response
| Method | Description |
|---|---|
setContent($content) |
Set response body |
setStatusCode($code) |
Set HTTP status |
header($name, $value) |
Add header |
withHeaders($headers) |
Add multiple headers |
send() |
Send response |
json($data, $status) |
Create JSON response |
redirect($url, $status) |
Create redirect response |
text($content, $status) |
Create text response |
html($content, $status) |
Create HTML response |
View
| Method | Description |
|---|---|
configure($viewsPath, $cachePath, $engine) |
Configure view system |
render($template, $data) |
Render template to string |
make($template, $data, $status) |
Create response with view |
share($key, $value) |
Share data with all views |
engineAvailable($engine) |
Check if engine available |
App
| Method | Description |
|---|---|
run($basePath) |
Run the application |
config($key, $default) |
Get config value |
setConfig($key, $value) |
Set config value |
isDebug() |
Check debug mode |
setDebug($debug) |
Set debug mode |
basePath($path) |
Get base path |
Session
| Method | Description |
|---|---|
start() |
Start the session |
get($key, $default) |
Get session value |
set($key, $value) |
Set session value |
has($key) |
Check if key exists |
forget($key) |
Remove session value |
all() |
Get all session data |
flush() |
Clear all session data |
destroy() |
Destroy session completely |
flash($key, $value) |
Flash value for next request |
getFlash($key, $default) |
Get and remove flash value |
csrfToken() |
Get CSRF token |
verifyCsrfToken($token) |
Verify CSRF token |
configure($config) |
Configure session settings |
setDriver($driver) |
Set custom session driver |
regenerate() |
Regenerate session ID |
RateLimiter
| Method | Description |
|---|---|
attempt($key, $max, $decay) |
Check/increment rate limit |
remaining($key, $max) |
Get remaining attempts |
availableIn($key) |
Seconds until limit resets |
clear($key) |
Clear rate limit for key |
hits($key) |
Get current hit count |
setStore($store) |
Set storage backend |
Router (Additional Methods)
| Method | Description |
|---|---|
cacheRoutes($file) |
Cache routes to file |
loadCachedRoutes($file) |
Load routes from cache |
isRouteCached() |
Check if routes are cached |
getStats() |
Get route statistics |
clearMiddlewareCache() |
Clear middleware cache |
Response (Additional Methods)
| Method | Description |
|---|---|
withCompression() |
Enable gzip compression |
withoutCompression() |
Disable gzip compression |
configureCompression($threshold, $level) |
Configure compression settings |
Request (Additional Methods)
| Method | Description |
|---|---|
getContentStream() |
Get stream handle for body |
readContentChunked($callback, $size) |
Read body in chunks |
setMaxBodySize($bytes) |
Set max request body size |
getMaxBodySize() |
Get max request body size |
Troubleshooting
404 Errors on All Routes
-
Check mod_rewrite is enabled:
sudo a2enmod rewrite sudo systemctl restart apache2
-
Check AllowOverride is set: Ensure your Apache configuration allows
.htaccessfiles. See Apache User Directories above. -
Check .htaccess permissions:
chmod 644 .htaccess
403 Forbidden / "Unable to read htaccess file"
This is a file permissions issue. Apache cannot read the .htaccess file:
chmod 644 .htaccess chmod 755 /path/to/your/app
500 Internal Server Error
-
Check PHP error logs: Usually at
/var/log/apache2/error.log -
Enable debug mode: Set
debug=truein your.configfile -
Check .htaccess syntax: Some directives (like
Options) may not be allowed in your Apache configuration
Views Not Loading
Ensure view paths in .config are relative to your application directory:
[views] path=views cache=cache
NanoMVC automatically resolves relative paths against the application's base directory.
Links Not Working in Subdirectory
When installed in a subdirectory, use $baseUrl for all links:
<?php $baseUrl = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'); ?> <a href="<?= $baseUrl ?>/users">Users</a>
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request
License
NanoMVC is released under the GPL-3.0-or-later license.
Copyright 2024-present Paige Julianne Sullivan
paigejulianne/nanomvc 适用场景与选型建议
paigejulianne/nanomvc 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 4 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 01 月 04 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「templating」 「framework」 「twig」 「smarty」 「mvc」 「router」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 paigejulianne/nanomvc 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 paigejulianne/nanomvc 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 paigejulianne/nanomvc 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
PHP Framework HLEB2 is the foundation of the web application. Provides ease of development and application performance.
Shoot aims to make providing data to your templates more manageable
This Symfony bundle integrates PhpSpreadsheet into Symfony using Twig.
A Twig extension to insert css as inline styles with a tag
Caching and compression for Twig assets (JavaScript and CSS).
Twig extensions for Tracy Debugger
统计信息
- 总下载量: 4
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 27
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: GPL-3.0-or-later
- 更新时间: 2026-01-04