webpagestudio / wps-micro
Lightweight PHP framework for building web applications
Requires
- php: ^8.3
- ext-json: *
- ext-mbstring: *
- ext-pdo: *
- psr/container: ^2.0
- twig/twig: ^3.0
Requires (Dev)
- ext-dom: *
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-15 08:36:46 UTC
README
Lightweight PHP framework core for building focused web applications.
Version 3.1.0. See CHANGELOG.md for release notes and UPGRADING.md for upgrade guidance.
WPS Micro provides the reusable request lifecycle, container, routing, middleware, validation, sessions, Twig integration, database access, migrations, and console primitives. Application controllers, models, routes, views, frontend assets, and deployment files live in a separate application skeleton.
Requirements
- PHP 8.3 or higher
- Composer
- PDO and mbstring PHP extensions
The test suite runs against PHP 8.3, 8.4, and 8.5 in GitHub Actions.
Start A New Application
Use the application skeleton instead of installing the framework into an empty directory:
composer create-project webpagestudio/wps-micro-skeleton my-site
The skeleton requires this package and keeps application code outside
vendor/. Framework updates therefore do not replace controllers, models,
routes, migrations, or templates:
composer update webpagestudio/wps-micro
For an existing Composer project, install only the core:
composer require webpagestudio/wps-micro
Package Structure
src- framework runtime and public APIssrc/Console- console application and reusable commandssrc/Exceptions- framework and HTTP exceptionssrc/Middleware- built-in middlewaretests- framework unit and integration tests
All framework classes use the WpsMicro\Core\ namespace.
Application Boundary
The framework package owns infrastructure:
Request,Response,Router, andDispatcherContainerandKernel- middleware pipeline and CSRF protection
- validation, sessions, and error handling
- Twig rendering and Vite manifest integration
- PDO connection, models, migrations, and migrator
- console application and generator commands
The application owns product behavior:
- controllers and application middleware
- models, repositories, and business services
- routes and configuration
- database migrations
- Twig templates and frontend assets
- public entry point and deployment configuration
Core classes never import App\ classes or assume an application directory.
The application passes routes, middleware, paths, and error handlers through
configuration.
Request Lifecycle
Request -> Global Middleware -> Router -> Route Middleware -> Controller -> Response
The Kernel registers framework services in the PSR-11 container. The
Dispatcher executes the middleware pipeline, resolves a controller through
the container, invokes the matched action, and normalizes the result to a
Response.
Bootstrap
A minimal application bootstrap can load environment values and create a kernel from a PHP configuration file:
<?php declare(strict_types=1); use WpsMicro\Core\Env; use WpsMicro\Core\Kernel; $rootPath = dirname(__DIR__); require $rootPath . '/vendor/autoload.php'; Env::load($rootPath . '/.env'); return Kernel::fromConfigFile($rootPath . '/config/app.php');
When router.routes_path is configured, the file must be readable and return
a callable. Invalid configuration fails during kernel boot instead of leaving
the application with an empty router.
The public front controller handles globals and sends the response:
/** @var \WpsMicro\Core\Kernel $kernel */ $kernel = require dirname(__DIR__) . '/bootstrap/app.php'; $kernel->handleGlobals()->send();
Container Overrides
Framework services are registered as defaults. Explicit application bindings in a supplied container are preserved, so infrastructure can be replaced without changing the core:
use WpsMicro\Core\Config; use WpsMicro\Core\Container; use WpsMicro\Core\Kernel; use WpsMicro\Core\Router; $container = new Container(); $container->instance(Router::class, new Router()); $kernel = new Kernel(new Config($config), $container);
Use Container::bound() to check for an explicit factory or instance binding.
Container::has() also reports concrete classes that can be autowired.
Routing
Route files return a callable that receives the router:
use App\Controllers\ControllerProduct; use WpsMicro\Core\Router; return static function (Router $router): void { $router->get('/products/{id}', [ControllerProduct::class, 'actionShow']) ->whereNumber('id') ->name('products.show'); $router->post('/cart/add', [ControllerCart::class, 'actionAdd']); $router->delete('/cart/{id}', [ControllerCart::class, 'actionRemove']); };
Routes are compiled when they are registered. Static paths use a direct method-and-path lookup, while dynamic paths reuse their compiled regular expression during matching. Static routes take priority over parameterized routes regardless of registration order.
Use where() for custom parameter constraints, or the built-in numeric and
UUID helpers:
$router->get('/posts/{slug}', [ControllerPost::class, 'actionShow']) ->where('slug', '[a-z0-9-]+') ->name('posts.show'); $router->get('/orders/{order}', [ControllerOrder::class, 'actionShow']) ->whereUuid('order') ->name('orders.show');
Groups combine URL prefixes, route-name prefixes, and middleware. Groups can be nested, and their attributes are inherited:
$router->group([ 'prefix' => '/admin', 'name' => 'admin.', 'middleware' => AuthMiddleware::class, ], static function (Router $router): void { $router->get('/products', [ControllerProduct::class, 'actionIndex']) ->name('products.index'); $router->post('/products', [ControllerProduct::class, 'actionStore']) ->middleware(CsrfMiddleware::class) ->name('products.store'); });
Generate paths from route names in PHP or complete application URLs in Twig:
$path = $router->url('products.show', ['id' => 42], ['tab' => 'details']); // /products/42?tab=details
<a href="{{ route('products.show', {id: product.id}) }}">View product</a>
Missing parameters, values that fail a constraint, duplicate method/path
pairs, and duplicate names fail with an InvalidArgumentException during
registration or URL generation.
Parameter constraints are evaluated against URL-decoded values, including
Unicode text. Values are decoded exactly once. An encoded slash (%2F) is
accepted inside a parameter only when its constraint permits / (for example,
->where('path', '.+')); it cannot replace a literal route separator.
Matching splits the URL at literal / separators before decoding each segment.
For /files/{folder}/{file}, parameters folder=a and file=b/c produce
/files/a/b%2Fc and retain those values even if both constraints are .+.
Only the final parameter, when it occupies its entire segment, may consume
additional unencoded segments if its constraint allows /. Constraints operate
within their own segment; use encoded slashes for earlier parameters.
Explicit HEAD routes are supported. When no explicit route exists, a HEAD
request falls back to the matching GET route and returns the same status and
headers without a response body.
Controllers And Responses
Application controllers may extend the framework controller:
namespace App\Controllers; use WpsMicro\Core\Controller; use WpsMicro\Core\Response; final class ControllerProduct extends Controller { public function actionShow(string $id): Response { return $this->render('products/show.twig', ['id' => $id]); } }
Controller actions must return a Response instance. Returning strings or
using echo as the response body is not supported in v3. This keeps response
status, headers, middleware, and error handling deterministic.
Controller helpers return HTML, JSON, and redirects:
return $this->render('products/index.twig', ['products' => $products]); return $this->json(['products' => $products]); return $this->redirect('/products');
Response supports case-insensitive header lookup and multiple values:
$response ->setHeader('Cache-Control', 'no-store') ->addHeader('Set-Cookie', 'theme=dark; Path=/; SameSite=Lax') ->addHeader('Set-Cookie', 'locale=en; Path=/; SameSite=Lax');
Middleware
Middleware implements WpsMicro\Core\Middleware:
use WpsMicro\Core\Middleware; use WpsMicro\Core\Request; use WpsMicro\Core\Response; final class AuthMiddleware implements Middleware { public function handle(Request $request, callable $next): Response { return $next($request); } }
Middleware can be configured globally, as default route middleware, or attached to one route.
Validation
The validator supports:
required,nullable, andconfirmedstring,array, andbooleanemail,url,integer, andnumericmin,max, andin
For fields using integer or numeric, min and max compare numeric
values. For string fields, they compare UTF-8 character lengths:
$validated = $this->validate([ 'quantity' => 'required|integer|min:1|max:100', 'price' => 'required|numeric|min:0.01', 'title' => 'required|string|min:3|max:120', ]);
Integer, decimal, and exponent-form strings are compared without floating-point
rounding, including numeric strings beyond PHP_INT_MAX. Exponents are compared
without expanding numbers into large strings. For example, 9007199254740993.0
and 9.007199254740993e15 both fail max:9007199254740992.
For precision-sensitive input, pass strings: PHP floats are already approximate
before validation and are compared using their JSON decimal representation.
numeric rejects NAN, infinity, booleans, values that overflow the finite
floating-point range, and exponents whose decimal order cannot safely fit in
a PHP integer. Invalid min or max limits are configuration errors.
Applications can register reusable custom rules. A rule receives the value,
field name, complete input, and optional parameter. Return true or null
when valid, false for the default message, or a custom error string:
$validator->addRule( 'divisible_by', static function (mixed $value, string $field, array $data, ?string $parameter): bool|string { $divisor = (int) $parameter; return $divisor > 0 && (int) $value % $divisor === 0 ?: $field . ' must be divisible by ' . $parameter . '.'; }, ); $validated = $validator->validate($input, [ 'quantity' => 'required|integer|divisible_by:3', ]);
A closure, invokable object, or callable array may also be placed directly in
a field's rule array for one-off validation. Strings always refer to registered
or built-in validation rule names, even when a PHP function has the same name.
To use a function as an inline callback, wrap it in a closure or use first-class
callable syntax. Invalid rule definitions and unknown named rules throw an
InvalidArgumentException instead of silently passing.
Browser validation failures flash sanitized input and redirect only to a
same-origin location. JSON requests receive a 422 response without starting
or changing the session.
Database And Migrations
Database creates a configured PDO connection. Model provides that connection
to application persistence classes, while business workflows remain in
application services. The built-in migrator is tested with SQLite and MariaDB;
the MySQL-compatible migration path is also suitable for MySQL.
Migration files return a Migration instance and implement both directions:
use WpsMicro\Core\Migration; return new class extends Migration { public function up(PDO $db): void { $db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY)'); } public function down(PDO $db): void { $db->exec('DROP TABLE products'); } };
Console Commands
The framework provides migration commands, route inspection, and configurable
generators. Applications decide where generated files are written and pass the
booted application router to RouteListCommand:
use WpsMicro\Core\Console\Commands\RouteListCommand; use WpsMicro\Core\Router; /** @var Router $router */ $router = $kernel->getContainer()->get(Router::class); $console ->add(new RouteListCommand($router)) ->add(new MakeControllerCommand($root . '/app/Controllers', 'App\\Controllers')) ->add(new MakeModelCommand($root . '/app/Models', 'App\\Models')) ->add(new MakeMigrationCommand($root . '/database/migrations'));
Running php application/console.php route:list prints each route's method,
path, name, controller action, and middleware. The exact console entry-point
path is owned by the application skeleton.
No generator writes inside the installed framework package.
Development
Install dependencies and run the framework suite:
composer install
composer test
Run static analysis, check or apply formatting, and generate a coverage report:
composer analyse composer format:check composer format composer test:coverage composer quality
PHPStan runs at level 8. PHP CS Fixer enforces the project style, and the
coverage command writes Clover XML to build/coverage.xml and enforces a 70%
statement coverage floor. A PCOV or Xdebug coverage driver is required for the
coverage command. GitHub Actions provides PCOV automatically.
Validate package metadata:
composer validate --strict --no-check-publish
The regular local suite skips the MariaDB integration test when its test environment variables are not configured. GitHub Actions runs that test against a real MariaDB service.
Versioning And Security
WPS Micro follows Semantic Versioning. Breaking public API changes are reserved for major releases, backward-compatible features for minor releases, and fixes for patch releases.
See CHANGELOG.md for release history, UPGRADING.md for migration instructions, and SECURITY.md for supported versions and private vulnerability reporting.