Search by

webpagestudio / wps-micro

victor8730

Lightweight PHP framework for building web applications

Package info

github.com/Victor8730/Wps-Micro

pkg:composer/webpagestudio/wps-micro

Statistics

Installs: 30

Dependents: 1

Suggesters: 0

Stars: 8

Open Issues: 0

v3.1.0 2026-09-15 07:58 UTC

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 APIs
  • src/Console - console application and reusable commands
  • src/Exceptions - framework and HTTP exceptions
  • src/Middleware - built-in middleware
  • tests - framework unit and integration tests

All framework classes use the WpsMicro\Core\ namespace.

Application Boundary

The framework package owns infrastructure:

  • Request, Response, Router, and Dispatcher
  • Container and Kernel
  • 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, and confirmed
  • string, array, and boolean
  • email, url, integer, and numeric
  • min, max, and in

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.