PHPackages                             luany/core - 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. luany/core

ActiveLibrary[Framework](/categories/framework)

luany/core
==========

Luany Core — Router, middleware pipeline and DI resolver hook for the Luany ecosystem.

v1.0.0(1mo ago)055↓33.3%1MITPHPPHP &gt;=8.2CI passing

Since Feb 25Pushed 1mo agoCompare

[ Source](https://github.com/luany-ecosystem/luany-core)[ Packagist](https://packagist.org/packages/luany/core)[ Docs](https://github.com/luany-ecosystem/luany-core)[ RSS](/packages/luany-core/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (3)Versions (10)Used By (1)

luany/core
==========

[](#luanycore)

**HTTP Request/Response, Router, Middleware Pipeline, CORS, Rate Limiting, and Route Caching for the Luany ecosystem.**

**Version**: v1.0.0 | **PHP**: &gt;= 8.2 | **License**: MIT **Author**: António Ambrósio Ngola | **Org**: [luany-ecosystem](https://github.com/luany-ecosystem)

---

Table of Contents
-----------------

[](#table-of-contents)

1. [Installation](#1-installation)
2. [Request](#2-request)
3. [Response](#3-response)
4. [Router &amp; Route Facade](#4-router--route-facade)
5. [Middleware](#5-middleware)
6. [Rate Limiters](#6-rate-limiters)
7. [Exceptions](#7-exceptions)
8. [Changelog](#8-changelog)

---

1. Installation
---------------

[](#1-installation)

```
composer require luany/core
```

---

2. Request
----------

[](#2-request)

**Class**: `Luany\Core\Http\Request`

```
$request = Request::fromGlobals();

$request->method();                          // 'GET'
$request->uri();                             // '/users/42'
$request->input('name', 'default');          // body or query value
$request->query('page');                     // query string only
$request->post('email');                     // body only
$request->all();                             // merged query + body$request->body();                            // body only$request->only(['name', 'email']);
$request->except(['password']);
$request->has('name');                       // bool
$request->filled('name');                    // bool — exists and non-empty
$request->file('avatar');                    // ?array
$request->hasFile('avatar');                 // bool
$request->header('Authorization');           // case-insensitive
$request->server('REMOTE_ADDR');
$request->cookie('app_locale', 'en');
$request->hasCookie('app_locale');
$request->ip();                              // X-Forwarded-For → REMOTE_ADDR
$request->userAgent();
$request->url();                             // full URL with scheme+host
$request->isGet(); $request->isPost();       // method shortcuts
$request->isMethod('DELETE');
$request->isAjax();
$request->expectsJson();
```

**Method override**: HTML forms may include a hidden `_method` field (`PUT`, `PATCH`, `DELETE`). `fromGlobals()` handles this automatically.

**JSON body**: If `Content-Type: application/json`, the body is parsed from `php://input` automatically.

**Body-only helper**: `Request::body()` returns only parsed body fields (POST/JSON), unlike `all()` which merges body + query.

---

3. Response
-----------

[](#3-response)

**Class**: `Luany\Core\Http\Response`

```
// Factories
Response::make('Hello', 200);
Response::json(['user' => $user]);
Response::json(['error' => 'Not found'], 404);
Response::redirect('/dashboard');
Response::redirect('/permanent', 301);
Response::notFound();
Response::unauthorized();
Response::forbidden();
Response::serverError();

// Fluent building
(new Response())
    ->status(422)
    ->body(json_encode(['errors' => $errors]))
    ->header('Content-Type', 'application/json')
    ->withHeaders(['X-Request-Id' => $id]);

// Inspection
$response->getStatusCode();  // int
$response->getBody();        // string
$response->getHeaders();     // array
$response->isRedirect();     // bool (3xx)
$response->isSuccessful();   // bool (2xx)

// Send to client (call once at end of lifecycle)
$response->send();
```

---

4. Router &amp; Route Facade
----------------------------

[](#4-router--route-facade)

**Classes**: `Luany\Core\Routing\Router`, `Luany\Core\Routing\Route`

`Route` is a static facade over the singleton `Router` instance.

### Basic Registration

[](#basic-registration)

```
use Luany\Core\Routing\Route;

Route::get('/users',       [UserController::class, 'index']);
Route::post('/users',      [UserController::class, 'store']);
Route::put('/users/{id}',  [UserController::class, 'update']);
Route::patch('/users/{id}',[UserController::class, 'update']);
Route::delete('/users/{id}',[UserController::class, 'destroy']);
Route::any('/ping', fn() => Response::make('pong'));

// Closure actions
Route::get('/hello', function (Request $request) {
    return Response::make('Hello!');
});
```

Controllers may return `Response`, `string`, or `array` (auto-JSON).

### Route Parameters

[](#route-parameters)

```
Route::get('/users/{id}', [UserController::class, 'show']);
// Controller: public function show(Request $request, string $id): Response

Route::get('/posts/{post}/comments/{comment}', [CommentController::class, 'show']);
// Parameters passed in order of appearance. Never written to $_GET.
```

### Named Routes

[](#named-routes)

```
Route::get('/users/{id}', [UserController::class, 'show'])->name('users.show');

$url = Route::router()->getNamedRoute('users.show', ['id' => 42]);
// → '/users/42'
```

### Route Groups

[](#route-groups)

```
// Prefix
Route::prefix('/admin')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'dashboard']); // → /admin/dashboard
});

// Middleware
Route::middleware(AuthMiddleware::class)->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
});

// Combined shorthand
Route::group(['prefix' => '/api/v1', 'middleware' => [AuthMiddleware::class]], function () {
    Route::get('/users', [UserController::class, 'index']);
    Route::post('/users', [UserController::class, 'store']);
});

// Nested
Route::prefix('/api')->group(function () {
    Route::prefix('/v1')->group(function () {
        Route::get('/status', fn() => Response::json(['ok' => true]));
        // → /api/v1/status
    });
});
```

### Resource Routes

[](#resource-routes)

```
Route::resource('products', ProductController::class);
// GET    /products           → index
// GET    /products/create    → create
// POST   /products           → store
// GET    /products/{id}      → show
// GET    /products/{id}/edit → edit
// PUT    /products/{id}      → update
// PATCH  /products/{id}      → update
// DELETE /products/{id}      → destroy

Route::apiResource('posts', PostController::class);
// Same but without create/edit

Route::resource('users', UserController::class, ['only' => ['index', 'show']]);
Route::resource('users', UserController::class, ['except' => ['create', 'edit']]);
```

### View Routes

[](#view-routes)

```
Route::setViewRenderer(fn($view, $data) => $engine->render($view, $data));

Route::view('/welcome', 'pages.welcome');
Route::view('/about', 'pages.about', ['title' => 'About Us']);
```

### Model Binding

[](#model-binding)

Automatically resolve route parameters to model instances before the action is called.

```
// Custom resolver
Route::bind('user', fn($id) => User::find($id));

// Shorthand — uses the model's static find() method
Route::model('post', Post::class);

// Action receives a resolved instance (or null if not found)
Route::get('/users/{user}', function (Request $request, ?User $user) {
    if ($user === null) {
        return Response::notFound();
    }
    return Response::json($user->toArray());
});
```

Unbound parameters pass through as raw strings. Bindings match by parameter name.

### Route Caching

[](#route-caching)

Serializes the compiled route table to a PHP file. Only array-action routes (`[Controller::class, 'method']`) are cached — closure routes cannot be serialized.

```
// In production bootstrap:
if (!Route::loadCache(base_path('storage/cache/routes.php'))) {
    require base_path('routes/http.php');
    Route::cache(base_path('storage/cache/routes.php'));
}

// Invalidate after deployment:
Route::clearCache(base_path('storage/cache/routes.php'));
```

---

5. Middleware
-------------

[](#5-middleware)

### Pipeline

[](#pipeline)

```
use Luany\Core\Middleware\Pipeline;

$response = (new Pipeline())
    ->send($request)
    ->through([AuthMiddleware::class, LogMiddleware::class])
    ->then(fn(Request $req) => $controller->action($req));
```

Implementing middleware:

```
use Luany\Core\Middleware\MiddlewareInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, callable $next): Response
    {
        if (!isset($_SESSION['user_id'])) {
            return Response::redirect('/login');
        }
        return $next($request);
    }
}
```

### CorsMiddleware

[](#corsmiddleware)

```
use Luany\Core\Middleware\CorsMiddleware;

// Default — allow all origins (public API, no credentials)
$cors = new CorsMiddleware();

// Production — specific origins with credentials
$cors = new CorsMiddleware(
    allowedOrigins:   ['https://app.example.com', '*.staging.example.com'],
    allowedMethods:   ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders:   ['Content-Type', 'Authorization'],
    exposedHeaders:   ['X-Total-Count'],
    allowCredentials: true,
    maxAge:           3600,
);
```

Behaviour: `OPTIONS` requests short-circuit with 204. Wildcard `['*']` + credentials echoes the actual `Origin`. Disallowed origins get no CORS headers. Subdomain wildcards (`*.example.com`) are supported.

### RateLimitMiddleware

[](#ratelimitmiddleware)

```
use Luany\Core\Middleware\RateLimitMiddleware;
use Luany\Core\RateLimit\FileRateLimiter;

$limiter = new FileRateLimiter(base_path('storage/rate-limits'));

// On a route
Route::post('/login', [AuthController::class, 'login'])
    ->middleware(new RateLimitMiddleware($limiter, maxAttempts: 5, decaySeconds: 60));
```

Exceeding the limit returns `429` with `X-RateLimit-Limit`, `X-RateLimit-Remaining: 0`, and `Retry-After` headers. Allowed requests receive `X-RateLimit-Limit` and `X-RateLimit-Remaining`.

Override `keyFor(Request $request): string` to key by user ID instead of IP.

---

6. Rate Limiters
----------------

[](#6-rate-limiters)

### InMemoryRateLimiter

[](#inmemoryratelimiter)

Per-process static store. For tests and development only.

```
use Luany\Core\RateLimit\InMemoryRateLimiter;

$limiter = new InMemoryRateLimiter();
$limiter->attempt('key', 5, 60);           // bool
$limiter->remaining('key', 5);             // int
$limiter->tooManyAttempts('key', 5);       // bool
$limiter->availableAt('key');              // int (Unix timestamp)
$limiter->reset('key');                    // void
InMemoryRateLimiter::flush();             // clear all keys (use in test tearDown)
```

### FileRateLimiter

[](#fileratelimiter)

JSON file-backed store. Safe for single-server production. Uses `flock()` for concurrency safety. Keys are SHA-256 hashed — no path traversal possible.

```
use Luany\Core\RateLimit\FileRateLimiter;

$limiter = new FileRateLimiter(base_path('storage/rate-limits'));
$limiter->attempt('api:ip:127.0.0.1', 60, 60);
$limiter->reset('api:ip:127.0.0.1');
$limiter->flush(); // removes all rl_*.json files
```

### Custom RateLimiter

[](#custom-ratelimiter)

Implement `Luany\Core\RateLimit\RateLimiterInterface`:

```
interface RateLimiterInterface
{
    public function attempt(string $key, int $maxAttempts, int $decaySeconds): bool;
    public function remaining(string $key, int $maxAttempts): int;
    public function availableAt(string $key): int;
    public function tooManyAttempts(string $key, int $maxAttempts): bool;
    public function reset(string $key): void;
}
```

---

7. Exceptions
-------------

[](#7-exceptions)

ExceptionCodeWhen thrown`RouteNotFoundException`404No route URI matches the request`MethodNotAllowedException`405URI matches a route but the HTTP method does not```
use Luany\Core\Exceptions\MethodNotAllowedException;
use Luany\Core\Exceptions\RouteNotFoundException;

// In your application's Exception Handler:
public function render(\Throwable $e): Response
{
    if ($e instanceof MethodNotAllowedException) {
        return Response::make('Method Not Allowed', 405)
            ->header('Allow', $e->getAllowHeaderValue());
    }
    if ($e instanceof RouteNotFoundException) {
        return Response::notFound();
    }
    return parent::render($e);
}
```

`MethodNotAllowedException::getAllowedMethods(): string[]` — e.g. `['GET', 'POST']``MethodNotAllowedException::getAllowHeaderValue(): string` — e.g. `'GET, POST'`

---

8. Changelog
------------

[](#8-changelog)

### v1.0.0 — Phase 4: Core Hardening

[](#v100--phase-4-core-hardening)

**New — `src/Routing/RouteCache.php`**

- `store()` — serialize route table to PHP file (closure routes excluded)
- `load()` — load cached route table
- `clear()` — delete cache file

**Modified — `src/Routing/Router.php`**

- Two-pass dispatch: URI match + wrong method → `MethodNotAllowedException` (405) instead of 404
- `bind(string $param, callable $resolver)` — register route model binding
- `getBindings()`, `getRoutes()`, `getNamedRoutes()` — expose state for cache and testing
- `saveToCache()` / `loadFromCache()` — route cache integration

**Modified — `src/Routing/Route.php`**

- `bind()`, `model()` — model binding API
- `cache()`, `loadCache()`, `clearCache()` — cache API
- `group(array $attributes, callable $callback)` — combined prefix+middleware shorthand
- `setRouter()`, `reset()` — testing helpers

**Existing (shipped before Phase 4, now fully tested):**`MethodNotAllowedException`, `CorsMiddleware`, `RateLimitMiddleware`, `RateLimiterInterface`, `InMemoryRateLimiter`, `FileRateLimiter`

**Tests added:** `MethodNotAllowedTest` (12), `CorsMiddlewareTest` (16), `RateLimiterTest` (15), `FileRateLimiterTest` (14), `RateLimitMiddlewareTest` (9), `RouteCacheTest` (15), `RouteModelBindingTest` (10)

**Total: OK (180 tests, 261 assertions)**

---

### v0.2.4 and earlier

[](#v024-and-earlier)

HTTP Request/Response, Router with groups and named routes, resource/apiResource, Pipeline, RouteRegistrar, RouteGroup, method override, `$_GET` isolation.

###  Health Score

43

—

FairBetter than 91% of packages

Maintenance89

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 96% 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 ~4 days

Total

7

Last Release

57d ago

Major Versions

v0.2.4 → v1.0.02026-03-23

PHP version history (2 changes)v0.1.0PHP &gt;=8.1

v1.0.0PHP &gt;=8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/9842ff34f898f8b92c95715d56ae48a2162eca040543183fc264391c554013ef?d=identicon)[Ngola-Programador-Full-Stack](/maintainers/Ngola-Programador-Full-Stack)

---

Top Contributors

[![antoniongoladev-design](https://avatars.githubusercontent.com/u/264429300?v=4)](https://github.com/antoniongoladev-design "antoniongoladev-design (24 commits)")[![Ngola-Programador-Full-Stack](https://avatars.githubusercontent.com/u/192051935?v=4)](https://github.com/Ngola-Programador-Full-Stack "Ngola-Programador-Full-Stack (1 commits)")

---

Tags

phpmiddlewareroutercoremvcluany

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/luany-core/health.svg)

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

PHPackages © 2026

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