PHPackages                             devvime/modpath - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. devvime/modpath

ActiveLibrary[Framework](/categories/framework)

devvime/modpath
===============

A Minimal and Expressive PHP Micro Routing Framework

v1.0.32(1mo ago)161MITPHP

Since May 31Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/devvime/modpath-php)[ Packagist](https://packagist.org/packages/devvime/modpath)[ RSS](/packages/devvime-modpath/feed)WikiDiscussions main Synced today

READMEChangelog (10)Dependencies (22)Versions (34)Used By (0)

ModPath PHP
===========

[](#modpath-php)

### A Minimal and Expressive PHP Micro Routing Framework

[](#a-minimal-and-expressive-php-micro-routing-framework)

---

Installation
------------

[](#installation)

```
composer require devvime/modpath
```

---

Initial Setup
-------------

[](#initial-setup)

```
require dirname(__DIR__) . '/vendor/autoload.php';

use ModPath\Router\Router;
use ModPath\Middleware\SecurityHeadersMiddleware;
use ModPath\Middleware\CorsMiddleware;
use ModPath\Middleware\RateLimitMiddleware;
use ModPath\Middleware\RequestSizeLimitMiddleware;
use App\Controllers\UserController;
use App\Controllers\ProductController;

$router = new Router();

$router->setGlobalMiddlewares([
    SecurityHeadersMiddleware::class,
    CorsMiddleware::class,
    RateLimitMiddleware::class,
    RequestSizeLimitMiddleware::class,
]);

$router->registerRoutes([
    UserController::class,
    ProductController::class,
]);

$router->dispatch();
```

Unhandled exceptions in controllers are caught automatically, logged via `error_log`, and returned as a generic `500 Internal Server Error` — stack traces are never exposed to the client.

---

Controllers
-----------

[](#controllers)

Use `#[Controller]` to define a route prefix and optional middleware applied to all methods in the class. Use `#[Route]` on each method to define the HTTP path and method.

```
namespace App\Controllers;

use ModPath\Http\Request;
use ModPath\Http\Response;
use ModPath\Attribute\Route;
use ModPath\Attribute\Controller;
use ModPath\Attribute\Middleware;
use ModPath\Contract\ControllerInterface;
use App\Middleware\AuthMiddleware;

#[Controller(path: '/users', middleware: AuthMiddleware::class)]
class UserController implements ControllerInterface
{
    #[Route(path: '', method: 'GET')]
    public function index(Request $request, Response $response): void
    {
        $response->json(['message' => 'Users list']);
    }

    #[Route(path: '/{id:int}', method: 'GET')]
    public function show(Request $request, Response $response): void
    {
        $response->json(['id' => $request->params['id']]);
    }

    #[Route(path: '', method: 'POST')]
    public function store(Request $request, Response $response): void
    {
        $response->json(['message' => 'User created'], 201);
    }

    #[Route(path: '/{id:int}', method: 'PUT')]
    public function update(Request $request, Response $response): void
    {
        $response->json(['message' => 'User updated']);
    }

    #[Route(path: '/{id:int}', method: 'DELETE')]
    public function destroy(Request $request, Response $response): void
    {
        $response->json(['message' => 'User deleted']);
    }
}
```

---

Route Parameters
----------------

[](#route-parameters)

Parameters are defined with `{name}` or `{name:type}`.

TypePatternPHP type`string``[^/]+` (default)`string``int``\d+``int``uuid``[0-9a-fA-F\-]{36}``string``slug``[a-z0-9\-]+``string````
#[Route(path: '/posts/{slug:slug}', method: 'GET')]
public function show(Request $request, Response $response): void
{
    $slug = $request->params['slug']; // string
}

#[Route(path: '/orders/{id:int}', method: 'GET')]
public function order(Request $request, Response $response): void
{
    $id = $request->params['id']; // int (cast automatically)
}
```

---

Request
-------

[](#request)

```
$request->params;   // URL parameters — typed according to route definition
$request->body;     // Parsed request body (stdClass|array|null)
$request->query;    // Query string ($_GET)
$request->headers;  // HTTP request headers only (keys: UPPER-CASE-HYPHEN)
```

`$request->body` is parsed automatically based on `Content-Type`:

Content-TypeResult`application/json``stdClass` or `null``application/x-www-form-urlencoded``array` or `null``multipart/form-data``array` or `null`*(unknown / empty)*`null``php://input` is read at most once per request, regardless of how many times `Request` is instantiated.

---

Response
--------

[](#response)

```
$response->json(['key' => 'value']);            // 200 JSON
$response->json(['error' => 'Not found'], 404); // JSON with explicit status
$response->status(204);                         // Status only (no body)
$response->send('plain text');                  // Plain text output
$response->render('templates.index', $data);    // Render template
$response->redirect('/login');                  // Redirect (terminates)
$response->header('X-Custom', 'value');         // Set arbitrary header
```

---

Middleware
----------

[](#middleware)

Implement `MiddlewareInterface`. `handle()` must return `true` to continue the request. To block, send a response and `exit`.

```
namespace App\Middleware;

use ModPath\Http\Request;
use ModPath\Http\Response;
use ModPath\Contract\MiddlewareInterface;
use ModPath\Helpers\Token;

class AuthMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, Response $response): bool
    {
        try {
            Token::decode(Token::get());
        } catch (\Exception $e) {
            $response->json(['error' => 'Unauthorized'], 401);
            exit;
        }

        return true;
    }
}
```

Apply per-method with `#[Middleware]`, or to all methods via `#[Controller(middleware: ...)]`. Multiple middleware on the same method execute in declaration order.

```
#[Route(path: '', method: 'POST')]
#[Middleware(AuthMiddleware::class)]
public function store(Request $request, Response $response): void { ... }
```

---

Guards
------

[](#guards)

`#[Guard]` works identically to `#[Middleware]` but is semantically reserved for authorization (roles, permissions). Keeping auth and authorization separate improves readability.

```
#[Route(path: '/admin/users', method: 'DELETE')]
#[Guard(AdminGuard::class)]
public function destroy(Request $request, Response $response): void { ... }
```

---

DTO Validation
--------------

[](#dto-validation)

Extend `Dto`, declare `$allowed` fields and `$rules`, then attach with `#[Dto]`.

```
namespace App\Dto;

use ModPath\Dto\Dto;

class CreateUserDto extends Dto
{
    public array $allowed = ['name', 'email', 'password'];

    public array $rules = [
        'name'     => [['required'], ['lengthMin', 3]],
        'email'    => [['required'], ['email']],
        'password' => [['required'], ['lengthMin', 8]],
    ];
}
```

```
use ModPath\Attribute\Dto;
use App\Dto\CreateUserDto;

#[Route(path: '', method: 'POST')]
#[Dto(CreateUserDto::class)]
public function store(Request $request, Response $response): void
{
    // Body is guaranteed valid and contains only allowed fields
    $name = $request->body->name;
}
```

Validation failure sends `422 Unprocessable Entity` and halts the request. Unknown fields are also rejected.

---

Helpers
-------

[](#helpers)

### Env

[](#env)

Safe access to environment variables. Throws `RuntimeException` with a clear message if a required variable is absent, instead of crashing with a generic PHP notice.

```
use ModPath\Helpers\Env;

$value   = Env::get('OPTIONAL_VAR', 'default');  // returns default if absent
$secret  = Env::require('SECRET');                // throws if absent or empty
Env::validate(['DB_HOST', 'DB_NAME', 'DB_USER']); // throws listing all missing vars
```

---

### Token (JWT)

[](#token-jwt)

```
use ModPath\Helpers\Token;

$token   = Token::encode(['user_id' => 1, 'role' => 'admin']); // payload must be an array
$payload = Token::decode($token);  // throws on invalid or expired token
$raw     = Token::get();           // reads Bearer from Authorization header; throws on missing/malformed
```

Requires `SECRET` in the environment.

---

### Database

[](#database)

Wraps [Medoo](https://medoo.in/) with a single connection per process. Validates required environment variables on first connection.

```
use ModPath\Helpers\Database;

$db = new Database();
$db->connect()->select('users', ['id', 'name'], ['active' => 1]);
```

Required environment variables:

```
DATABASE_TYPE=mysql
DATABASE_NAME=mydb
DATABASE_SERVER=127.0.0.1
DATABASE_USER=root
DATABASE_PASSWORD=secret
DATABASE_PORT=3306

```

---

### Repository

[](#repository)

Extend `Repository` for pagination, find, create, update, and delete out of the box.

```
namespace App\Repositories;

use ModPath\Helpers\Repository;

class UserRepository extends Repository
{
    protected string $table = 'users';
    protected array $fields = ['id', 'name', 'email', 'created_at'];
}
```

```
$repo = new UserRepository();

$result = $repo->getAll(filters: ['active' => 1], page: 2, perPage: 20);
// Returns: ['data' => [...], 'total' => 80, 'page' => 2, 'per_page' => 20, 'last_page' => 4, 'pages' => [...]]

$rows = $repo->getById(5);
$user = $repo->find(filter: ['email' => 'ana@example.com']);
$id   = $repo->create(['name' => 'Ana', 'email' => 'ana@example.com']);
$repo->update(5, ['name' => 'Ana Lima']);
$repo->delete(5);
```

---

### Model

[](#model)

Extend `Model` for a static factory that maps an array to constructor parameters.

```
namespace App\Models;

use ModPath\Helpers\Model;

class User extends Model
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $email,
    ) {}
}

$user   = User::create(['id' => 1, 'name' => 'Ana', 'email' => 'ana@example.com']);
$fields = Model::fields(User::class); // ['id', 'name', 'email']
```

---

### Message

[](#message)

Shorthand for a JSON status/message response.

```
use ModPath\Helpers\Message;

Message::send(200, 'Operation successful');
Message::send(403, 'Forbidden');
```

---

### Sanitizer

[](#sanitizer)

Clean user input before processing or storing.

```
use ModPath\Helpers\Sanitizer;

$name  = Sanitizer::string($request->body->name);    // trim + htmlspecialchars
$email = Sanitizer::email($request->body->email);    // sanitize email characters
$age   = Sanitizer::int($request->body->age);        // cast to int
$price = Sanitizer::float($request->body->price);    // cast to float
$url   = Sanitizer::url($request->body->url);        // sanitize URL
$bio   = Sanitizer::stripTags($request->body->bio);  // strip HTML tags
$data  = Sanitizer::array((array) $request->body);   // sanitize all strings recursively
```

---

### Rate Limiting

[](#rate-limiting)

Direct helper for use outside middleware. Requires Redis.

```
use ModPath\Helpers\RateLimit;

// 10 requests per 60 seconds, keyed by IP
RateLimit::execute(time: 60, maxRequests: 10);

// Custom identifier (e.g., authenticated user ID)
RateLimit::execute(identifier: (string) $userId, time: 3600, maxRequests: 100);
```

Sends `429 Too Many Requests` with a `Retry-After` header and halts execution when the limit is exceeded. Falls back to allowing requests if Redis is unavailable.

---

### Mailer

[](#mailer)

Wraps [PHPMailer](https://github.com/PHPMailer/PHPMailer) with SMTP via environment variables.

```
use ModPath\Helpers\Mailer;

$mailer = new Mailer();
$mailer->send([
    'subject'    => 'Welcome!',
    'msgHTML'    => 'Hello, Ana',
    'recipients' => [
        ['email' => 'ana@example.com', 'name' => 'Ana'],
    ],
]);
```

Required environment variables:

```
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_USER=no-reply@example.com
EMAIL_PASSWORD=secret

```

---

### View (Mustache)

[](#view-mustache)

Renders Mustache templates from the `VIEWS_DIR` directory.

```
use ModPath\Helpers\View;

View::render('emails/welcome', ['name' => 'Ana']); // echoes output
$html = View::get('emails/welcome', ['name' => 'Ana']); // returns string
```

---

Security
--------

[](#security)

ModPath ships with a set of security middlewares. Apply them globally via `setGlobalMiddlewares()` — they run on every request, including unmatched routes.

### SecurityHeadersMiddleware

[](#securityheadersmiddleware)

Sets HTTP security headers on every response. Extend to customize.

HeaderDefault value`X-Content-Type-Options``nosniff``X-Frame-Options``DENY``X-XSS-Protection``1; mode=block``Referrer-Policy``strict-origin-when-cross-origin``Content-Security-Policy``default-src 'self'``Permissions-Policy``geolocation=(), microphone=(), camera=()``Strict-Transport-Security``max-age=31536000; includeSubDomains``X-Powered-By`*(removed)*```
class AppSecurityHeaders extends SecurityHeadersMiddleware
{
    protected string $csp = "default-src 'self'; script-src 'self' https://cdn.example.com";
}
```

---

### RateLimitMiddleware

[](#ratelimitmiddleware)

Limits requests per IP using Redis. Responds `429` + `Retry-After` when exceeded.

```
class ApiRateLimit extends RateLimitMiddleware
{
    protected int $maxRequests = 30;
    protected int $time = 60; // seconds
}
```

Default: 60 requests / 60 seconds / IP.

---

### CorsMiddleware

[](#corsmiddleware)

Handles preflight `OPTIONS` requests and sets CORS headers. Validates and sanitizes the `Origin` header to prevent header injection.

```
class AppCors extends CorsMiddleware
{
    protected string|array $allowedOrigins = ['https://app.example.com'];
    protected bool $allowCredentials = true;
}
```

`allowCredentials: true` requires a specific origin (not `*`).

---

### BruteForce Protection

[](#bruteforce-protection)

Apply `#[Guard(BruteForceMiddleware::class)]` to sensitive routes (login, password reset). After N failed attempts from the same IP, the route returns `429` for a configurable lockout period.

```
use ModPath\Helpers\BruteForce;
use ModPath\Middleware\BruteForceMiddleware;

#[Route(path: '/login', method: 'POST')]
#[Guard(BruteForceMiddleware::class)]
public function login(Request $request, Response $response): void
{
    $ip = $_SERVER['REMOTE_ADDR'];
    $user = $this->userRepo->find(filter: ['email' => $request->body->email]);

    if (!$user || !password_verify($request->body->password, $user['password'])) {
        BruteForce::failed($ip);
        $response->json(['error' => 'Invalid credentials'], 401);
        return;
    }

    BruteForce::reset($ip);
    $response->json(['token' => Token::encode(['id' => $user['id']])]);
}
```

Extend to adjust limits:

```
class LoginGuard extends BruteForceMiddleware
{
    protected int $maxAttempts  = 3;
    protected int $decaySeconds = 1800; // 30 min
}
```

Default: 5 attempts, 15-minute lockout. Requires Redis.

---

### RequestSizeLimitMiddleware

[](#requestsizelimitmiddleware)

Rejects payloads above the configured limit before the body is processed. Responds `413 Payload Too Large`.

```
class TightSizeLimit extends RequestSizeLimitMiddleware
{
    protected int $maxBytes = 512_000; // 500 KB
}
```

Default: 2 MB.

---

### Path Traversal Protection

[](#path-traversal-protection)

Template and view paths are validated against `[a-zA-Z0-9._\-\/]` before touching the filesystem. Any path containing `..` or invalid characters throws `InvalidArgumentException`.

---

Template Engine
---------------

[](#template-engine)

`Response::render()` uses a custom template engine with auto-escaped output. Templates are `.php` files stored in `VIEWS_DIR`.

### Variables

[](#variables)

```
{{ $name }}
```

Output is wrapped in `htmlspecialchars()` automatically.

### Conditions

[](#conditions)

```

    Admin panel

    Editor panel

    Access denied

```

### Loops

[](#loops)

```

    {{ $user['name'] }}

```

### For

[](#for)

```
 ... `Elseif````Else````Loop` ... `` ... `For` ... `` ... `Include``*(compiled and inlined)*---

Testing
-------

[](#testing)

ModPath ships with a [Pest](https://pestphp.com/) test suite covering all framework components that do not require external services (database, Redis, SMTP).

### Running the tests

[](#running-the-tests)

```
composer require --dev   # installs pestphp/pest if not present
./vendor/bin/pest
```

### Test coverage

[](#test-coverage)

SuiteFileWhat is testedAttribute`tests/Unit/Attribute/AttributeTest.php``Route`, `Controller`, `Middleware`, `Guard`, `Dto` constructorsCore`tests/Unit/Core/ContainerTest.php`DI resolution, singletons, defaults, nullable params, circular depsDto`tests/Unit/Dto/DtoTest.php`Field allowlist, rule validation, `handle()` happy pathEnv`tests/Unit/Helpers/EnvTest.php``get`, `require`, `validate` — present, missing, empty valuesModel`tests/Unit/Helpers/ModelTest.php``fields()` introspection, `create()` factorySanitizer`tests/Unit/Helpers/SanitizerTest.php`All sanitizer methods including recursive array sanitizationToken`tests/Unit/Helpers/TokenTest.php`JWT encode/decode round-trip, Bearer header extraction, error pathsRequest`tests/Unit/Http/RequestTest.php`Params, query, headers, JSON/form body parsingResponse`tests/Unit/Http/ResponseTest.php`JSON output, status code, plain text sendMiddlewareManager`tests/Unit/Middleware/MiddlewareManagerTest.php``verify()` dispatch, `getMiddlewares()` prefix mergingRouterParams`tests/Unit/Router/RouterParamsTest.php`Regex generation for all param types, typed param extractionView`tests/Unit/View/ViewTest.php`Template compilation (`{{ }}`, ``, ``, ``, escaping, path traversal guard)Components excluded from unit tests (require external services): `Database`, `Repository`, `BruteForce`, `RateLimit`, `Mailer`.

### Writing application tests

[](#writing-application-tests)

```
use ModPath\Http\Request;
use ModPath\Http\Response;

it('returns 200 with user data', function () {
    $request          = new Request(['id' => 42]);
    $request->body    = ['name' => 'Alice'];
    $response         = new Response();

    ob_start();
    (new UserController())->show($request, $response);
    $output = ob_get_clean();

    $data = json_decode($output, true);
    expect($data['id'])->toBe(42);
});
```

---

Production Checklist
--------------------

[](#production-checklist)

- All required environment variables are set (missing variables throw `RuntimeException` with a clear message)
- `display_errors = Off` and `log_errors = On` in `php.ini`
- HTTPS active (`Strict-Transport-Security` header is already being sent)
- `SecurityHeadersMiddleware` in `setGlobalMiddlewares()`
- `CORS` configured with an explicit origin allowlist (not `*`)
- Redis available for `RateLimitMiddleware` and `BruteForceMiddleware`
- `VIEWS_DIR` points to a directory without external write access
- Uploaded files never stored inside `VIEWS_DIR`

### Required environment variables

[](#required-environment-variables)

VariableUsed by`SECRET``Token``DATABASE_TYPE`, `DATABASE_NAME`, `DATABASE_SERVER`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_PORT``Database``EMAIL_HOST`, `EMAIL_PORT`, `EMAIL_USER`, `EMAIL_PASSWORD``Mailer``VIEWS_DIR``Response::render()`, `View`

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance92

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~11 days

Total

33

Last Release

38d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/36117649?v=4)[Victor A. Mendes](/maintainers/devvime)[@devvime](https://github.com/devvime)

---

Top Contributors

[![devvime](https://avatars.githubusercontent.com/u/36117649?v=4)](https://github.com/devvime "devvime (46 commits)")

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/devvime-modpath/health.svg)

```
[![Health](https://phpackages.com/badges/devvime-modpath/health.svg)](https://phpackages.com/packages/devvime-modpath)
```

###  Alternatives

[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.8M507](/packages/pimcore-pimcore)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[chameleon-system/chameleon-base

The Chameleon System core.

1028.6k5](/packages/chameleon-system-chameleon-base)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M574](/packages/shopware-core)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k15](/packages/tempest-framework)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
