paigejulianne / picomvc
PicoMVC: a lightweight MVC framework with Blade and Smarty template support
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/paigejulianne/picomvc
Requires
- php: >=8.0
- paigejulianne/picoorm: ^2.0
Requires (Dev)
- phpunit/phpunit: ^10.0 || ^11.0
Suggests
- jenssegers/blade: Required for Blade templating support (composer require jenssegers/blade)
- smarty/smarty: Required for Smarty templating support (composer require smarty/smarty)
README
A lightweight MVC framework for PHP 8.0+ with support for Blade and Smarty templates.
Version 1.0.0 | Changelog | License: GPL-3.0
by Paige Julianne Sullivan paigejulianne.com | GitHub
Features
- Minimal footprint: Single-file framework (~800 lines)
- Multiple template engines: PHP, Blade, and Smarty support
- Simple routing: Clean URL routing with parameters
- Zero dependencies: Only requires PHP 8.0+ (template engines optional)
- Built-in validation: Request validation with helpful error messages
- Integrates with PicoORM: Seamlessly works with PicoORM 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/picomvc
Manual Installation
Download PicoMVC.php and include it in your project:
require_once 'PicoMVC.php';
Examples
PicoMVC includes three example applications demonstrating each template engine:
| Example | Template Engine | Directory |
|---|---|---|
| PHP (Native) | Native PHP | example/ |
| Blade | Laravel Blade | example-blade/ |
| Smarty | Smarty | example-smarty/ |
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
-
Set file permissions:
chmod 644 example*/.htaccess chmod 755 example*/cache
-
Access in your browser:
- PHP:
http://localhost/path/to/picomvc/example/ - Blade:
http://localhost/path/to/picomvc/example-blade/ - Smarty:
http://localhost/path/to/picomvc/example-smarty/
- 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\PicoMVC\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\PicoMVC\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\PicoMVC\Controller; use PaigeJulianne\PicoMVC\Request; use PaigeJulianne\PicoMVC\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, or smarty path=views cache=cache [routes] file=routes.php
Programmatic Configuration
use PaigeJulianne\PicoMVC\App; use PaigeJulianne\PicoMVC\View; App::setDebug(true); App::setConfig('app.name', 'My App'); View::configure('/path/to/views', '/path/to/cache', 'blade');
Routing
Basic Routes
use PaigeJulianne\PicoMVC\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); });
Controllers
Creating Controllers
Extend the base Controller class:
use PaigeJulianne\PicoMVC\Controller; use PaigeJulianne\PicoMVC\Request; use PaigeJulianne\PicoMVC\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\PicoMVC\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']);
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');
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>
Integration with PicoORM
Creating Models
use PaigeJulianne\PicoORM; class Users extends PicoORM { // Maps to 'users' table automatically } class BlogPost extends PicoORM { 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'); } }
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 PicoMVC 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
PicoMVC 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 |
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
PicoMVC 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
PicoMVC is released under the GPL-3.0-or-later license.
Copyright 2024-present Paige Julianne Sullivan