PHPackages                             fennectra/framework - 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. fennectra/framework

ActiveLibrary[Framework](/categories/framework)

fennectra/framework
===================

Fennectra — PHP 8.3+ high-performance MVC API framework with built-in compliance (SOC 2, ISO 27001, NF525, GDPR)

v1.0.8(yesterday)023↑2639.1%1MITPHPPHP &gt;=8.2CI passing

Since Mar 23Pushed yesterdayCompare

[ Source](https://github.com/fennectra/framework)[ Packagist](https://packagist.org/packages/fennectra/framework)[ Docs](https://github.com/fennectra/framework)[ RSS](/packages/fennectra-framework/feed)WikiDiscussions main Synced today

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

    ![Fennectra Framework](fennec_logo.png)

 [![PHP 8.3+](https://camo.githubusercontent.com/ec0b560791416424f69195e344bde164f0e1aae17a6dd87539be4cd5cd7952f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e332b2d3838393242463f6c6f676f3d706870266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/ec0b560791416424f69195e344bde164f0e1aae17a6dd87539be4cd5cd7952f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e332b2d3838393242463f6c6f676f3d706870266c6f676f436f6c6f723d7768697465) [![PHPStan Level 5](https://camo.githubusercontent.com/dbededbca3bf520fe140c5904a819c06b8dddf4e0f66500db6bf1d31fbe207fd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230352d627269676874677265656e3f6c6f676f3d706870)](https://camo.githubusercontent.com/dbededbca3bf520fe140c5904a819c06b8dddf4e0f66500db6bf1d31fbe207fd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230352d627269676874677265656e3f6c6f676f3d706870) [![PHPUnit 320+ tests](https://camo.githubusercontent.com/c5a70b577b8a31163b7404dcb25aff110454131c07aae05d7565d2034f9e9786/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f504850556e69742d33323025324225323074657374732d627269676874677265656e3f6c6f676f3d706870)](https://camo.githubusercontent.com/c5a70b577b8a31163b7404dcb25aff110454131c07aae05d7565d2034f9e9786/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f504850556e69742d33323025324225323074657374732d627269676874677265656e3f6c6f676f3d706870) [![Multi-DB](https://camo.githubusercontent.com/644e3384045052c6715a66ef9f8444fe70ac0f46dce13cc67fd9ab03fe68dd56/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f44422d506f737467726553514c2532302537432532304d7953514c25323025374325323053514c6974652d3333363739313f6c6f676f3d706f737467726573716c266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/644e3384045052c6715a66ef9f8444fe70ac0f46dce13cc67fd9ab03fe68dd56/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f44422d506f737467726553514c2532302537432532304d7953514c25323025374325323053514c6974652d3333363739313f6c6f676f3d706f737467726573716c266c6f676f436f6c6f723d7768697465) [![FrankenPHP](https://camo.githubusercontent.com/ee7d5425e945105ac22db794a3b27cf13266761fda8e08a31b9c5010477e9031/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4672616e6b656e5048502d776f726b65722d626c756576696f6c65743f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30784d6941795154457749444577494441674d434179494449674d544a684d5441674d5441674d434177494441674d5441674d5441674d5441674d5441674d434177494441674d5441744d5442424d5441674d5441674d434177494441674d5449674d6e6f694c7a34384c334e325a7a343d)](https://camo.githubusercontent.com/ee7d5425e945105ac22db794a3b27cf13266761fda8e08a31b9c5010477e9031/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4672616e6b656e5048502d776f726b65722d626c756576696f6c65743f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30784d6941795154457749444577494441674d434179494449674d544a684d5441674d5441674d434177494441674d5441674d5441674d5441674d5441674d434177494441674d5441744d5442424d5441674d5441674d434177494441674d5449674d6e6f694c7a34384c334e325a7a343d) [![GKE](https://camo.githubusercontent.com/ff24627115d1c9aac9a00330f7b9841d02dce1816dcdcd52cc74bf17e9c84b27/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4465706c6f792d474b452d3432383546343f6c6f676f3d676f6f676c65636c6f7564266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/ff24627115d1c9aac9a00330f7b9841d02dce1816dcdcd52cc74bf17e9c84b27/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4465706c6f792d474b452d3432383546343f6c6f676f3d676f6f676c65636c6f7564266c6f676f436f6c6f723d7768697465) [![SOC 2 Compliant](https://camo.githubusercontent.com/a2b7c71f3b33292fffcbe3ba3ef8094323da74ae7f561b6ac9c40a7fb4f5dc39/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f534f435f322d436f6d706c69616e742d3030423134303f6c6f676f3d736563757269747973636f726563617264266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/a2b7c71f3b33292fffcbe3ba3ef8094323da74ae7f561b6ac9c40a7fb4f5dc39/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f534f435f322d436f6d706c69616e742d3030423134303f6c6f676f3d736563757269747973636f726563617264266c6f676f436f6c6f723d7768697465) [![ISO 27001](https://camo.githubusercontent.com/8e47756ea68c4662fa9c607bea2aeb8c3de01c27dc8a8456387a15dba9ead7d8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f49534f5f32373030312d52656164792d3030353243433f6c6f676f3d69736f266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/8e47756ea68c4662fa9c607bea2aeb8c3de01c27dc8a8456387a15dba9ead7d8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f49534f5f32373030312d52656164792d3030353243433f6c6f676f3d69736f266c6f676f436f6c6f723d7768697465) [![NF525](https://camo.githubusercontent.com/154f62db27ee22c7941614effcb9cd2012a10f5e342fef1e3d6d5ddf50ecb486/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4e463532352d4365727469666961626c652d4646363630303f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30784e43417953445a6a4c5445754d5341774c5449674c6a6b744d694179646a4532597a41674d5334784c6a6b674d69417949444a6f4d544a6a4d533478494441674d6930754f5341794c544a574f4777744e693032656d30304944453453445a574e476733646a566f4e5859784d586f694c7a34384c334e325a7a343d266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/154f62db27ee22c7941614effcb9cd2012a10f5e342fef1e3d6d5ddf50ecb486/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4e463532352d4365727469666961626c652d4646363630303f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30784e43417953445a6a4c5445754d5341774c5449674c6a6b744d694179646a4532597a41674d5334784c6a6b674d69417949444a6f4d544a6a4d533478494441674d6930754f5341794c544a574f4777744e693032656d30304944453453445a574e476733646a566f4e5859784d586f694c7a34384c334e325a7a343d266c6f676f436f6c6f723d7768697465) [![GDPR](https://camo.githubusercontent.com/9255533e726a96a55d99b4d1237116dc10c84f055d01972da5f764e419a65331/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f474450522d436f6d706c69616e742d3030423134303f6c6f676f3d736869656c6473646f74696f266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/9255533e726a96a55d99b4d1237116dc10c84f055d01972da5f764e419a65331/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f474450522d436f6d706c69616e742d3030423134303f6c6f676f3d736869656c6473646f74696f266c6f676f436f6c6f723d7768697465)

 [![Multi-tenant](https://camo.githubusercontent.com/d78faa23f51a962e4031735ce203d70c7cf19133db4d96b7f8b4d474fa47aa31/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d756c74692d2d74656e616e742d52656164792d3641314239413f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30784d694133566a4e494e6e59784f4767784d6c593361433032656d30744d6941784d6b67346469307961444a324d6e70744d433030534468324c544a6f4d6e5979656d30774c5452494f46593561444a324d6e70744e694134614330796469307961444a324d6e70744d433030614330796469307961444a324d6e70744d43303061433079566a6c6f4d6e5979656d30774c54526f4c544a574e576779646a4a364969382b5043397a646d632b266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/d78faa23f51a962e4031735ce203d70c7cf19133db4d96b7f8b4d474fa47aa31/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d756c74692d2d74656e616e742d52656164792d3641314239413f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30784d694133566a4e494e6e59784f4767784d6c593361433032656d30744d6941784d6b67346469307961444a324d6e70744d433030534468324c544a6f4d6e5979656d30774c5452494f46593561444a324d6e70744e694134614330796469307961444a324d6e70744d433030614330796469307961444a324d6e70744d43303061433079566a6c6f4d6e5979656d30774c54526f4c544a574e576779646a4a364969382b5043397a646d632b266c6f676f436f6c6f723d7768697465) [![Webhooks](https://camo.githubusercontent.com/25bb6538532c1bcdcec5d05907831115e79ac2c806628d092b7edb111223486a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f576562686f6f6b732d484d41432d2d5348413235362d4536353130303f6c6f676f3d776562686f6f6b266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/25bb6538532c1bcdcec5d05907831115e79ac2c806628d092b7edb111223486a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f576562686f6f6b732d484d41432d2d5348413235362d4536353130303f6c6f676f3d776562686f6f6b266c6f676f436f6c6f723d7768697465) [![Image Transforms](https://camo.githubusercontent.com/847573ac594002dd099becb4ce925acf809a3c7599cd2346e2fc3f838ec8870c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f496d6167655f5472616e73666f726d732d496e74657276656e74696f6e2d4646364630303f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30794d5341784f556731566a4e494e4859784e3067794d585978656b30784f434135534468734e434131494451744e586f694c7a34384c334e325a7a343d266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/847573ac594002dd099becb4ce925acf809a3c7599cd2346e2fc3f838ec8870c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f496d6167655f5472616e73666f726d732d496e74657276656e74696f6e2d4646364630303f6c6f676f3d646174613a696d6167652f7376672b786d6c3b6261736536342c50484e325a79423462577875637a30696148523063446f764c336433647935334d793576636d63764d6a41774d43397a646d636949485a705a58644362336739496a41674d4341794e4341794e43492b50484268644767675a6d6c7362443069643268706447556949475139496b30794d5341784f556731566a4e494e4859784e3067794d585978656b30784f434135534468734e434131494451744e586f694c7a34384c334e325a7a343d266c6f676f436f6c6f723d7768697465) [![S3 Object Storage](https://camo.githubusercontent.com/729de9ad2b1d4c5265b695736b5abdfab6194d9e5a6ee4b8dd20a9c23a9b3d62/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53332d4f626a65637425323053746f726167652d4534373931313f6c6f676f3d616d617a6f6e7333266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/729de9ad2b1d4c5265b695736b5abdfab6194d9e5a6ee4b8dd20a9c23a9b3d62/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53332d4f626a65637425323053746f726167652d4534373931313f6c6f676f3d616d617a6f6e7333266c6f676f436f6c6f723d7768697465) [![GCS Cloud Storage](https://camo.githubusercontent.com/427a2c53983dd9138d3d5fd2140575c2fd113c3dd634fcbfc89367eb11625e1a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4743532d436c6f756425323053746f726167652d3432383546343f6c6f676f3d676f6f676c65636c6f7564266c6f676f436f6c6f723d7768697465)](https://camo.githubusercontent.com/427a2c53983dd9138d3d5fd2140575c2fd113c3dd634fcbfc89367eb11625e1a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4743532d436c6f756425323053746f726167652d3432383546343f6c6f676f3d676f6f676c65636c6f7564266c6f676f436f6c6f723d7768697465)

 **High-performance PHP 8.3+ framework**
 JWT · ORM · Events · Worker · Profiler · Scheduler · Queue · Notifications · Webhooks · Images · Feature Flags · Multi-tenant · Storage · PDF · SOC 2 · ISO 27001 · NF525 · GDPR · PostgreSQL · MySQL · SQLite

---

> High-performance PHP 8.3+ framework with Dependency Injection, JWT, auto-generated OpenAPI, CLI, ORM with eager loading, **Multi-database** (PostgreSQL, MySQL, SQLite), Event Dispatcher with multiple brokers, Profiler, Rate Limiting, Migrations, K8s-safe Scheduler, Job Queue, Feature Flags, multi-channel Notifications, **HMAC-SHA256 signed Webhooks**, **Image Transforms** (Intervention/Image), SSE Broadcasting, OAuth, multi-driver Storage (Local, S3, GCS), PDF generation, **GDPR** (consent, DPO dashboard, data subject rights), and FrankenPHP worker support.

---

Quickstart
----------

[](#quickstart)

```
# Install the CLI (once)
composer global require fennectra/installer

# Create a new project
fennectra new my-api
cd my-api
cp .env.example .env        # configure DB_DRIVER + credentials + SECRET_KEY
./forge serve               # http://localhost:8080
```

---

Architecture
------------

[](#architecture)

```
┌───────────────────────────────────────────────────────────────┐
│                      public/index.php                         │
├───────────────────────────────────────────────────────────────┤
│  Request → CORS → Tenant → Profiler → Logging → Auth → Ctrl  │
├─────────┬─────────┬─────────┬──────────┬─────────┬───────────┤
│  Router │   DI    │  ORM    │  Events  │  JWT    │   Rate    │
│         │Container│  Model  │Dispatcher│ Service │  Limiter  │
├─────────┼─────────┼─────────┼──────────┼─────────┼───────────┤
│ Cache   │Scheduler│  Queue  │ Feature  │  State  │Notificat. │
│ Redis   │+RedLock │  Jobs   │  Flags   │ Machine │Multi-chan │
├─────────┼─────────┼─────────┼──────────┼─────────┼───────────┤
│Webhooks │  Image  │ Storage │   SSE    │  OAuth  │    PDF    │
│HMAC-sign│Transform│L/S3/GCS │Broadcast │ G + GH  │  dompdf   │
├─────────┼─────────┼─────────┼──────────┼─────────┼───────────┤
│ Audit   │Encrypti.│ Security│  NF525   │  GDPR   │           │
│ SOC 2   │AES-256  │ Logger  │ Fiscal   │ Consent │           │
├─────────┴─────────┴─────────┴──────────┴─────────┴───────────┤
│          src/Core/ — The engine                               │
├───────────────────────────────────────────────────────────────┤
│      PostgreSQL / MySQL / SQLite        Redis                 │
└───────────────────────────────────────────────────────────────┘

```

```
framework/              ← framework core (Packagist: fennectra/framework) (do not modify)
  src/
    Attributes/         ← validation, API docs, ORM, RateLimit, StateMachine, Broadcast, Auditable, Encrypted, Nf525
    Commands/           ← CLI (serve, make:*, quality, migrate, seed, queue, schedule, deploy, tinker)
    Core/               ← App, Router, Container, ORM, Events, JWT...
      Database/         ← DB drivers (PostgreSQL, MySQL, SQLite) + DriverFactory
      Profiler/         ← per-request debug profiler
      Relations/        ← eager loading (BelongsTo, HasMany, HasOne)
      RateLimiter/      ← rate limiting (Redis + InMemory stores)
      Redis/            ← RedisConnection, RedisLock
      Cache/            ← RedisCache, TaggedCache
      Migration/        ← MigrationRunner, Seeder, FakeDataGenerator
      Scheduler/        ← Schedule, CronExpression, Redis Lock K8s-safe
      Queue/            ← Job dispatch, QueueWorker, FailedJobHandler
      Feature/          ← Feature Flags with Redis cache
      StateMachine/     ← controlled transitions on Models
      Notification/     ← multi-channel (Mail, Slack, Database, Webhook)
      Webhook/          ← outgoing HMAC-SHA256 webhooks + delivery jobs
      Image/            ← image transformations (GD-based)
      Broadcasting/     ← SSE via Redis Pub/Sub
      OAuth/            ← Google, GitHub providers
      Audit/            ← HasAuditTrail (SOC 2)
      Encryption/       ← AES-256-GCM at rest (SOC 2)
      Security/         ← SecurityLogger, PasswordPolicy, AccountLockout (ISO 27001)
      Logging/          ← LogMaskingProcessor (SOC 2)
      Nf525/            ← HasNf525, ClosingService, FecExporter, HashChainVerifier
    Middleware/         ← Auth, CORS, Profiler, RateLimit, Security, Logging, IpAllowlist
  database/             ← migrations and seeders
  config/               ← phpstan, phpunit, cs-fixer
  docker/               ← Dockerfile, docker-compose, Caddyfile, kubernetes

app/                    ← your application code
  Controllers/          ← HTTP handlers
  Models/               ← ORM models
  Dto/                  ← input/output validation
  Routes/               ← route files (auto-loaded)
  Jobs/                 ← job classes for the queue
  config/tenants.php    ← multi-tenancy mapping (domain/port → database)
  Schedule.php          ← scheduled tasks

storage/                ← uploaded files (local driver)

public/                 ← web root
  index.php             ← HTTP entry point (worker + classic)
  router.php            ← router script for the built-in PHP server
  storage               ← symlink to storage/ (created by storage:link)

```

---

**Routing**Routes are defined in `app/Routes/` — one file per domain, loaded automatically.

```
// app/Routes/admin.php
$router->group([
    'prefix' => '/admin',
    'description' => 'Administration',
    'middleware' => [[Auth::class, ['admin']]],
], function ($router) {
    $router->get('/users', [AdminController::class, 'listUsers']);
    $router->post('/users', [AdminController::class, 'create']);
    $router->put('/users/{id}', [AdminController::class, 'update']);
    $router->delete('/users/{id}', [AdminController::class, 'delete']);
});
```

**Available methods:** `get()`, `post()`, `put()`, `delete()`**Dynamic parameters:** `/users/{id}` — automatically injected into the controller **Middleware:** per route or per group **OpenAPI:** auto-generated documentation from attributes

**ORM &amp; Query Builder**### ORM Model

[](#orm-model)

```
#[Table('users')]
class User extends Model
{
    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class, 'role_id');
    }
}
```

### Fluent Queries

[](#fluent-queries)

```
// Search
User::where('active', true)
    ->where('role_id', '>', 5)
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->get();                    // Collection of Models

User::find(123);               // or null
User::findOrFail(123);         // or HttpException 404

// Create
$user = new User(['email' => 'x@y.com', 'name' => 'Ali']);
$user->save();
// or
User::create(['email' => 'x@y.com']);

// Update
$user->email = 'new@y.com';
$user->save();                 // UPDATE only modified fields

// Delete
$user->delete();               // soft delete (deleted_at)
$user->forceDelete();          // actual DELETE
$user->restore();              // undo soft delete
```

### Relations

[](#relations)

MethodTypeExample`belongsTo()`Many-to-OneUser → Role`hasMany()`One-to-ManyRole → Users`hasOne()`One-to-OneUser → Profile### Eager Loading (N+1 prevention)

[](#eager-loading-n1-prevention)

```
// BEFORE: N+1 queries (1 + N queries)
$users = User::where('active', true)->get();
foreach ($users as $user) {
    echo $user->role->name;  // 1 query per user!
}

// AFTER: 2 queries total
$users = User::with('role')->where('active', true)->get();
foreach ($users as $user) {
    echo $user->role->name;  // already loaded, 0 queries
}

// Multiple relations
User::with('role', 'profile')->paginate(20);
```

### Query Builder (without ORM)

[](#query-builder-without-orm)

```
DB::table('users')->where('active', true)->get();        // main database
DB::table('clients', 'job')->limit(10)->get();           // secondary database
DB::raw('SELECT * FROM users WHERE id = :id', ['id' => 1]);
DB::transaction(function () { /* ... */ });
```

**JWT Authentication &amp; RBAC**### Token Generation

[](#token-generation)

```
// POST /token — generate a JWT
$jwt = $jwtService->generate([
    'email' => $user->email,
    'role'  => $user->role()->name,
    'id'    => $user->id,
]);
```

### Route Protection

[](#route-protection)

```
// Accessible to all authenticated users
$router->get('/profile', [UserController::class, 'me'], [[Auth::class]]);

// Restricted to admins
$router->get('/admin', [AdminController::class, 'index'], [[Auth::class, ['admin']]]);

// Restricted to admin + manager
$router->group([
    'middleware' => [[Auth::class, ['admin', 'manager']]],
], function ($router) { /* ... */ });
```

### Get the Authenticated User

[](#get-the-authenticated-user)

```
$user = Auth::user();  // ['email' => ..., 'role' => ..., 'id' => ...]
```

**Tokens:** Access (15min) + Refresh (24h) — configurable via `JWT_ACCESS_TTL` and `JWT_REFRESH_TTL`

**Event Dispatcher**Event system with 3 interchangeable brokers via `EVENT_BROKER`:

BrokerTransportDependencyUsage`sync`Same processNoneDev / default`redis`Redis Pub/Sub`REDIS_*`Async production`database`PostgreSQL table`EVENT_DB_*`Async without Redis### Usage

[](#usage)

```
// Dispatch an event
Event::dispatch('user.created', $userData);

// Listen
Event::listen('user.created', function ($data) {
    // send email, log, notify...
}, priority: 10);

// Listen once
Event::once('user.verified', fn($data) => /* ... */);

// Check if listeners exist
Event::hasListeners('user.created');  // bool
```

**DTOs &amp; Validation**Validation via PHP 8.1+ attributes — auto-documented in OpenAPI.

```
readonly class ProductRequest
{
    public function __construct(
        #[Required]
        #[MinLength(3)]
        #[Description('Product name')]
        public string $name,

        #[Required]
        #[Email]
        public string $contact_email,

        #[Min(0)]
        public float $price,
    ) {}
}
```

### Available Attributes

[](#available-attributes)

AttributeDescription`#[Required]`Required field`#[Email]`Valid email`#[MinLength]`Minimum length`#[MaxLength]`Maximum length`#[Min]`Minimum numeric value`#[Max]`Maximum numeric value`#[Regex]`Custom regex pattern`#[ArrayOf]`Array element typing`#[Description]`Field documentation```
$errors = Validator::validate(ProductRequest::class, $requestData);
```

**CLI — Commands**```
./forge                              # list all commands
./forge serve                        # PHP dev server
./forge serve --frankenphp           # native FrankenPHP
./forge serve --frankenphp --worker  # worker mode (max perf)
./forge serve --port=3000            # custom port
```

### CRUD Generation

[](#crud-generation)

```
./forge make:all Product --roles=admin,manager     # full CRUD
./forge make:all Invoice --connection=job           # on secondary database
./forge make:all Article --no-auth                  # without auth
```

### Individual Generation

[](#individual-generation)

```
./forge make:model Product
./forge make:controller ProductController --crud
./forge make:dto ProductRequest --request
./forge make:dto ProductResponse --response
./forge make:route Product --prefix=/product --middleware=auth
./forge make:event UserCreated
./forge make:listener SendWelcomeEmail
```

### Migrations &amp; Seeding

[](#migrations--seeding)

```
./forge migrate                      # apply migrations
./forge migrate --rollback           # rollback the last batch
./forge migrate --status             # view current status
./forge make:migration add_phone     # create a migration
./forge make:audit                   # full audit module (SOC 2)
./forge make:webhook                 # full webhooks module
./forge make:nf525                   # full NF525 module (fiscal)
./forge make:rgpd                    # full GDPR module (consent)
./forge db:seed                      # run seeders
./forge make:seeder UserSeeder       # create a seeder
```

### Queue &amp; Scheduler

[](#queue--scheduler)

```
./forge queue:work                   # consume jobs
./forge queue:work --queue=emails    # specific queue
./forge queue:retry --id=5           # retry a failed job
./forge schedule:run                 # run due tasks
./forge make:job SendWelcomeEmail    # create a job
```

### Feature Flags &amp; Deploy

[](#feature-flags--deploy)

```
./forge feature list                 # list flags
./forge feature enable dark-mode     # enable a flag
./forge feature disable dark-mode    # disable a flag
./forge deploy                       # build + push + K8s rollout
./forge deploy --dry-run             # preview without executing
```

### Tinker (Interactive SQL)

[](#tinker-interactive-sql)

```
./forge tinker --sql="SELECT * FROM users LIMIT 5"
./forge tinker --sql="\dt"                         # list tables
./forge tinker --sql="\d users"                    # describe table
./forge tinker --sql="SELECT 1" --connection=job   # secondary database
```

### Storage

[](#storage)

```
./forge storage:link                 # symlink public/storage → storage/
```

### Quality &amp; Cache

[](#quality--cache)

```
./forge quality                      # lint + PHPStan + tests
./forge quality --fix                # auto-fix style
./forge cache:clear                  # clear cache
./forge cache:routes                 # cache routes
```

**FrankenPHP Worker**High-performance mode where the application boots **once** and handles requests in a loop.

```
┌─────────────────────────────────────────┐
│           FrankenPHP Worker             │
│                                         │
│   Boot (1x) ──→ ┌───────────────────┐  │
│                  │  Request Loop     │  │
│                  │  req → handle     │  │
│                  │  req → handle     │  │
│                  │  req → handle     │  │
│                  │  ...              │  │
│                  └───────────────────┘  │
│                                         │
│   10-100x faster than PHP-FPM           │
└─────────────────────────────────────────┘

```

### Starting

[](#starting)

```
# Native dev
./forge serve --frankenphp --worker

# Docker (uses Caddyfile + frankenphp run)
docker build -f Dockerfile.frankenphp -t php-api:franken .
docker run -p 8080:8080 --env-file .env php-api:franken
```

### Monitoring &amp; Probes

[](#monitoring--probes)

```
GET /health          → basic health check
GET /healthz         → liveness probe (K8s)
GET /readyz          → readiness probe (DB + Redis)
GET /debug/worker    → worker stats (requests, memory, trend)
GET /debug/profiler  → profiler (last 50 requests with SQL, events, timing)

```

- `index.php` automatically detects worker mode via `frankenphp_handle_request()`
- Caddyfile for worker routing (not `php-server`)
- Monolog logger (14-day rotation, stderr for Docker/K8s)
- WorkerStats: memory delta, trend analysis (stable/growing/spiky), error tracking
- Guaranteed cleanup (try/finally): DB flush, auth reset, GC
- Configurable request limit (`MAX_REQUESTS`)

**Multi-database &amp; Multi-driver**### Supported Drivers

[](#supported-drivers)

The framework supports **3 database drivers** via the `DB_DRIVER` variable:

Driver`DB_DRIVER`Env prefixDefault portPostgreSQL`pgsql` (default)`POSTGRES_`5432MySQL`mysql``MYSQL_`3306SQLite`sqlite``SQLITE_`—### PostgreSQL Configuration (default)

[](#postgresql-configuration-default)

```
DB_DRIVER=pgsql

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=fennectra
POSTGRES_USER=fennectra
POSTGRES_PASSWORD=secret
```

### MySQL Configuration

[](#mysql-configuration)

```
DB_DRIVER=mysql

MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DB=myapp
MYSQL_USER=root
MYSQL_PASSWORD=secret
```

### SQLite Configuration

[](#sqlite-configuration)

```
DB_DRIVER=sqlite

SQLITE_DB=var/database.sqlite
# or in-memory for tests:
# SQLITE_DB=:memory:
```

### Multiple Connections

[](#multiple-connections)

```
# Secondary database (any name)
POSTGRES_JOB_HOST=10.0.0.50
POSTGRES_JOB_DB=job_database
POSTGRES_JOB_USER=user
POSTGRES_JOB_PASSWORD=pass
```

```
DB::table('users')->get();                     // main database
DB::table('invoices', 'job')->get();           // "job" database
DB::table('test_data', 'test')->get();         // "test" database
```

### Custom Driver

[](#custom-driver)

```
use Fennec\Core\Database\DriverFactory;

// Register a custom driver (CockroachDB, etc.)
DriverFactory::register('cockroach', MyCockroachDriver::class);
```

Models generated with `--connection=job` automatically use the correct connection.

**Multi-tenancy**Database isolation per tenant, automatically resolved from the HTTP **domain** or **port**.

### Configuration

[](#configuration)

**1. Declare tenants in `app/config/tenants.php`:**

```
return [
    'domains' => [
        'client1.example.com' => 'client1',
        'client2.example.com' => 'client2',
        '*.client3.com'       => 'client3',   // wildcard subdomains
    ],

    'ports' => [
        8081 => 'client1',   // useful for local dev
        8082 => 'client2',
    ],

    'tenants' => [
        'client1' => [
            'host'     => 'POSTGRES_TENANT_CLIENT1_HOST',
            'port'     => 'POSTGRES_TENANT_CLIENT1_PORT',
            'db'       => 'POSTGRES_TENANT_CLIENT1_DB',
            'user'     => 'POSTGRES_TENANT_CLIENT1_USER',
            'password' => 'POSTGRES_TENANT_CLIENT1_PASSWORD',
        ],
        'client2' => [
            'host'     => 'POSTGRES_TENANT_CLIENT2_HOST',
            'port'     => 'POSTGRES_TENANT_CLIENT2_PORT',
            'db'       => 'POSTGRES_TENANT_CLIENT2_DB',
            'user'     => 'POSTGRES_TENANT_CLIENT2_USER',
            'password' => 'POSTGRES_TENANT_CLIENT2_PASSWORD',
        ],
    ],
];
```

**2. Add environment variables in `.env`:**

```
POSTGRES_TENANT_CLIENT1_HOST=localhost
POSTGRES_TENANT_CLIENT1_PORT=5432
POSTGRES_TENANT_CLIENT1_DB=client1_db
POSTGRES_TENANT_CLIENT1_USER=client1
POSTGRES_TENANT_CLIENT1_PASSWORD=secret

POSTGRES_TENANT_CLIENT2_HOST=10.0.0.50
POSTGRES_TENANT_CLIENT2_PORT=5432
POSTGRES_TENANT_CLIENT2_DB=client2_db
POSTGRES_TENANT_CLIENT2_USER=client2
POSTGRES_TENANT_CLIENT2_PASSWORD=secret
```

### How it Works

[](#how-it-works)

- The `TenantMiddleware` detects the tenant on each request (domain &gt; wildcard &gt; port)
- The `default` connection is automatically redirected to the tenant's database
- Named connections (`job`, `test`, etc.) are **not** affected
- Compatible with worker mode: the tenant is reset between each request
- If no tenant matches and multi-tenancy is configured, a 400 error is returned

### Resolution Priority

[](#resolution-priority)

1. Exact domain (`client1.example.com`)
2. Wildcard (`*.client3.com`)
3. Port (`8081`)

### Accessing the Current Tenant

[](#accessing-the-current-tenant)

```
// In a controller (via the Container)
$tenantManager = Container::getInstance()->get(TenantManager::class);
$tenantManager->current();         // 'client1' or null

// In a middleware (via request attributes)
$tenantId = $request->getAttribute('tenant');
```

### Local Multi-tenant Development

[](#local-multi-tenant-development)

```
# Start 2 instances on different ports
./forge serve --port=8081   # → client1
./forge serve --port=8082   # → client2
```

**Dependency Injection Container**```
// Register a singleton
$container->singleton(JwtService::class, fn() => new JwtService($secret));

// Resolve automatically
$jwt = $container->get(JwtService::class);

// Factory (new instance on each call)
$container->bind(Logger::class, fn() => new Logger('app'));
```

Automatic resolution of constructor dependencies. The Container is accessible via `$app->container()` or `Container::getInstance()`.

**Auto-generated API Documentation**OpenAPI documentation automatically generated from code:

- **Scalar UI:**
- **OpenAPI JSON:**

Automatic introspection:

- Routes and HTTP methods
- `#[ApiDescription]`, `#[ApiStatus]` attributes
- DTO schemas (fields, types, validation)
- Required roles and authentication

**Quality &amp; Tests**```
# Check everything at once
./forge quality              # lint + PHPStan + tests
./forge quality --fix        # auto-fix style

# Individually
composer test                    # PHPUnit
composer analyse                 # PHPStan (level 5)
composer lint                    # PHP-CS-Fixer (PSR-12)
composer lint:fix                # auto-fix
```

- **PHPUnit 11** — tests in `tests/`
- **PHPStan** — static analysis
- **PHP-CS-Fixer** — PSR-12 style

**Docker &amp; Deployment**### PHP-FPM + Nginx (classic)

[](#php-fpm--nginx-classic)

```
docker build -f docker/Dockerfile -t php-api .
docker run -p 8080:8080 --env-file .env php-api
```

### FrankenPHP Worker (max perf)

[](#frankenphp-worker-max-perf)

```
docker build -f docker/Dockerfile -t php-api:franken .
docker run -p 8080:8080 --env-file .env php-api:franken
```

FrankenPHP configuration via Caddyfile in `docker/`.

### Docker Compose (local dev)

[](#docker-compose-local-dev)

```
docker compose -f docker/docker-compose.yml up -d     # starts API + PostgreSQL + Redis
docker compose logs -f   # follow logs
```

### Kubernetes

[](#kubernetes)

Production manifests in `docker/kubernetes/` with liveness/readiness probes.

```
./forge deploy              # build + push + K8s rollout
./forge deploy --dry-run    # preview without executing
```

**Debug Profiler**Per-request profiler built into the worker. Automatically collects:

- **SQL queries**: SQL, bindings, duration in ms
- **Dispatched events**: name, listener duration
- **Middleware**: each middleware with its execution time
- **DI resolutions**: services resolved by the Container
- **Memory**: peak, delta per request
- **N+1 detection**: warning if the same query runs &gt; 3 times

```
# Enable (automatic if APP_ENV=dev)
PROFILER_ENABLED=1
```

```
GET /debug/profiler        → list of the last 50 requests
GET /debug/profiler/{id}   → details of a request

```

The ring buffer persists in memory within the worker (zero I/O).

**Rate Limiting**```
// Per route or group
$router->group([
    'middleware' => [[RateLimitMiddleware::class, ['limit' => 30, 'window' => 60]]],
], function ($router) { /* ... */ });
```

- **Redis** in production (shared across K8s pods)
- **InMemory** in dev (zero deps)
- Automatic headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
- 429 response with `Retry-After`

**Migrations &amp; Seeding**### Migrations

[](#migrations)

```
./forge make:migration create_products    # generate a file
./forge migrate                           # apply pending migrations
./forge migrate --rollback                # rollback the last batch
./forge migrate --status                  # current status
```

Format: `database/migrations/2026_03_21_143022_create_products.php`

```
return [
    'up' => 'CREATE TABLE products (id SERIAL PRIMARY KEY, name VARCHAR(255))',
    'down' => 'DROP TABLE products',
];
```

> **Note:** The `migrations` table is automatically created with driver-appropriate SQL (`SERIAL` for PostgreSQL, `AUTO_INCREMENT` for MySQL, `AUTOINCREMENT` for SQLite).

### Seeding

[](#seeding)

```
class UserSeeder extends Seeder
{
    public function run(): void
    {
        for ($i = 0; $i < 50; $i++) {
            User::create([
                'name'  => $this->fake()->name(),
                'email' => $this->fake()->email(),
            ]);
        }
    }
}
```

Built-in `FakeDataGenerator`: `name()`, `email()`, `number()`, `date()`, `uuid()`, `phone()` — zero external dependencies.

**Scheduler (K8s-safe)**Scheduled tasks with Redis Lock — **single execution per pod** in K8s.

```
// app/Schedule.php
return (new Schedule())
    ->call(fn() => DB::raw('DELETE FROM logs WHERE created_at < NOW() - INTERVAL \'30 days\''))
        ->daily()->name('clean-logs')
    ->command('cache:clear')
        ->everyFiveMinutes()->name('cache-refresh');
```

The scheduler runs **inside the FrankenPHP worker** (60s throttle, zero external cron).

MethodFrequency`everyMinute()`Every minute`everyFiveMinutes()`Every 5 minutes`hourly()`Every hour`daily()` / `dailyAt('08:00')`Daily`weekly()` / `weekdays()`Weekly`cron('*/10 * * * *')`Custom`SCHEDULER_ENABLED=1` + `REDIS_HOST` to activate.

**Job Queue**```
// Dispatch a job
Job::dispatch(SendWelcomeEmail::class, ['user_id' => 123]);

// Define a job
class SendWelcomeEmail implements JobInterface
{
    public function handle(array $payload): void { /* ... */ }
    public function retries(): int { return 3; }
    public function failed(array $payload, \Throwable $e): void { /* ... */ }
}
```

```
./forge queue:work                # consume (Redis BLPOP or DB polling)
./forge queue:retry --id=5        # retry a failed job
```

Drivers: `QUEUE_DRIVER=redis` (BLPOP) or `database` (FOR UPDATE SKIP LOCKED). Failed jobs are stored in `failed_jobs`.

**Feature Flags**```
// Simple check
if (FeatureFlag::enabled('new-checkout')) { /* ... */ }

// Per user/role (progressive rollout)
if (FeatureFlag::for('beta-ui')->whenRole('admin')->enabled()) { /* ... */ }

// Activate/deactivate
FeatureFlag::activate('dark-mode');
FeatureFlag::deactivate('dark-mode');
```

Redis cache (60s TTL) + fallback to `feature_flags` table in DB.

```
./forge feature list
./forge feature enable dark-mode
```

**State Machine**Controlled transitions on Models via attribute:

```
#[StateMachine(column: 'status', transitions: [
    'draft->submitted', 'submitted->approved', 'submitted->rejected',
    'approved->shipped', 'shipped->delivered',
])]
class Order extends Model
{
    use HasStateMachine;
}

$order->transitionTo('submitted');       // draft → submitted OK
$order->transitionTo('shipped');         // submitted → shipped FAIL Exception
$order->canTransitionTo('approved');     // true
$order->availableTransitions();          // ['approved', 'rejected']
```

Events automatically dispatched: `Order.transitioned:submitted:approved`

**Multi-channel Notifications**```
// Send
$user->notify(new OrderShippedNotification($order));

// Define
class OrderShippedNotification extends Notification
{
    public function via(): array { return ['database', 'mail', 'slack']; }
    public function toMail(): MailMessage { /* ... */ }
    public function toSlack(): SlackMessage { /* ... */ }
    public function toDatabase(): array { return ['order_id' => $this->order->id]; }
}
```

Channels: **Database**, **Mail** (built-in SMTP, zero deps), **Slack** (webhook), **Webhook** (signed HTTP POST). `HasNotifications` trait: `notify()`, `notifications()`, `unreadNotifications()`.

**Outgoing Webhooks**Webhook system for notifying external URLs on internal events, with HMAC-SHA256 signatures and automatic retry.

### Automatic Dispatch via Events

[](#automatic-dispatch-via-events)

```
// All registered webhooks listening to 'order.shipped' will be notified
Event::dispatch('order.shipped', ['order_id' => 42, 'tracking' => 'ABC123']);
```

The `WebhookManager` listens to all events and automatically dispatches to matching webhooks via the Job Queue.

### Register a Webhook (`webhooks` table)

[](#register-a-webhook-webhooks-table)

```
INSERT INTO webhooks (name, url, secret, events, is_active) VALUES (
    'Partner API',
    'https://partner.com/webhooks/orders',
    'whsec_MySharedSecret',
    '["order.shipped", "order.cancelled"]',
    true
);
```

A webhook can listen for specific events or `["*"]` to receive everything.

### HMAC-SHA256 Signature

[](#hmac-sha256-signature)

Each request is signed — the recipient can verify authenticity:

```
// Receiver side — verify the signature
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
$timestamp = (int) $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];

$isValid = WebhookManager::verify($payload, $secret, $signature, $timestamp);
// Automatically rejects requests older than 5 minutes (replay protection)
```

**Sent headers:**

HeaderDescription`X-Webhook-Event`Event name (`order.shipped`)`X-Webhook-Signature``sha256=hmac(timestamp.payload, secret)``X-Webhook-Timestamp`Unix timestamp`User-Agent``Fennec-Webhook/1.0`### Via the Notification System

[](#via-the-notification-system)

```
class OrderShippedNotification extends Notification
{
    public function via(): array
    {
        return ['database', 'webhook'];  // multi-channel
    }

    public function toWebhook(): WebhookMessage
    {
        return (new WebhookMessage())
            ->url('https://partner.com/hook')
            ->secret('whsec_MySecret')
            ->event('order.shipped')
            ->payload(['order_id' => $this->order->id]);
    }
}

$user->notify(new OrderShippedNotification($order));
```

### Automatic Retry

[](#automatic-retry)

Failed deliveries are retried **5 times** with exponential backoff via the Job Queue. Each attempt is logged in the `webhook_deliveries` table:

ColumnDescription`webhook_id`Reference to `webhooks.id``event`Event name`status``pending` / `delivered` / `failed``http_status`HTTP response code`response_body`Response body (max 2000 chars)`attempt`Attempt number### Setup

[](#setup)

```
./forge make:webhook   # generates the full module (migration + Models + DTOs + Controller + Routes)
./forge migrate        # apply the migration
```

**Generated API (admin only):**

```
GET    /webhooks                        Paginated list
GET    /webhooks/{id}                   Details
POST   /webhooks                        Create
PUT    /webhooks/{id}                   Update
DELETE /webhooks/{id}                   Delete
PATCH  /webhooks/{id}/toggle            Enable/disable
GET    /webhooks/{id}/deliveries        Deliveries
GET    /webhooks/stats                  Statistics
GET    /webhooks/failures               Recent failures
POST   /webhooks/deliveries/{id}/retry  Retry

```

**Image Transforms**Image transformation via [Intervention/Image v3](https://image.intervention.io/v3) — resize, crop, blur, watermark, format conversion and more. Integrates with the existing Storage system.

### Quick Transforms

[](#quick-transforms)

```
// Resize (preserves aspect ratio)
ImageTransformer::resize('photos/avatar.jpg', 800);
ImageTransformer::resize('photos/avatar.jpg', 800, 600);

// Square thumbnail 150x150
ImageTransformer::thumbnail('photos/avatar.jpg', 150);

// Cover (fills exact dimensions)
ImageTransformer::fit('photos/banner.jpg', 1200, 630);

// Crop a region
ImageTransformer::crop('photos/photo.jpg', 400, 400, 50, 50);

// Convert to WebP
ImageTransformer::convert('photos/large.png', 'webp', 85);
```

Each method returns the **path of the transformed file** in Storage.

### Chainable Pipeline

[](#chainable-pipeline)

For complex transformations, the pipeline lets you chain operations:

```
$outputPath = ImageTransformer::make('photos/original.jpg')
    ->orient()                              // automatic EXIF correction
    ->resize(1200)                          // max 1200px wide
    ->crop(800, 600, 100, 50)              // crop a region
    ->blur(3)                               // gaussian blur
    ->sharpen(15)                           // sharpness
    ->brightness(10)                        // brightness
    ->contrast(5)                           // contrast
    ->greyscale()                           // black and white
    ->watermark('My App', 'bottom-right', 20, 'ffffff', 50)
    ->format('webp', 85)                   // conversion + quality
    ->apply();                              // save to Storage

// $outputPath = 'photos/transforms/original_a1b2c3d4.webp'
```

### Direct Buffer (HTTP response)

[](#direct-buffer-http-response)

```
// Return the transformed image without saving it
$buffer = ImageTransformer::make('photos/avatar.jpg')
    ->fit(200, 200)
    ->format('webp')
    ->toBuffer();

header('Content-Type: image/webp');
echo $buffer;
```

### Available Operations

[](#available-operations)

MethodDescription`resize(w, h)`Resize (preserves aspect ratio)`resizeExact(w, h)`Resize (stretch, no ratio)`crop(w, h, x, y)`Crop a region`fit(w, h)`Cover — fills exact dimensions`blur(amount)`Gaussian blur (1-100)`sharpen(amount)`Increase sharpness (1-100)`brightness(level)`Brightness (-100 to +100)`contrast(level)`Contrast (-100 to +100)`rotate(angle)`Rotation in degrees`flip()`Horizontal mirror`flop()`Vertical mirror`greyscale()`Greyscale`orient()`Automatic EXIF correction`watermark(text, pos, size, color, opacity)`Text watermark`format(fmt, quality)`Output format (jpg, png, webp, gif)`quality(q)`Output quality (1-100)### Pipeline Cache

[](#pipeline-cache)

Each pipeline generates a **unique cache key** based on the path + operations:

```
$pipeline = ImageTransformer::make('photo.jpg')->resize(800)->format('webp');
$cacheKey = $pipeline->cacheKey();  // 'img:a1b2c3d4e5f6...'

// Use with the framework's Cache
$result = Cache::remember($cacheKey, 86400, fn() => $pipeline->apply());
```

### Attribute for Controllers

[](#attribute-for-controllers)

```
#[ImageTransform(maxWidth: 2000, maxHeight: 2000, allowedFormats: ['jpg', 'png', 'webp'])]
public function transform(string $path): void
{
    $buffer = ImageTransformer::make($path)
        ->resize((int) ($_GET['w'] ?? 800))
        ->format($_GET['fmt'] ?? 'webp', (int) ($_GET['q'] ?? 85))
        ->toBuffer();

    header('Content-Type: ' . ImageTransformer::mimeType($_GET['fmt'] ?? 'webp'));
    header('Cache-Control: public, max-age=86400');
    echo $buffer;
}
```

### Supported Formats

[](#supported-formats)

FormatReadWriteMIMEJPEGYesYes`image/jpeg`PNGYesYes`image/png`WebPYesYes`image/webp`GIFYesYes`image/gif`> **Driver**: GD by default. For optimal performance and animated GIF support, install the Imagick extension and instantiate with `new ImageTransformer(ImageManager::imagick())`.

**SSE Broadcasting**Server-Sent Events for real-time:

```
// Server side
Broadcaster::broadcast('orders', 'shipped', ['order_id' => 42]);

// Client side (JavaScript)
const es = new EventSource('/events/stream?channels=orders');
es.onmessage = (e) => console.log(JSON.parse(e.data));
```

- Redis Pub/Sub for cross-pod communication
- Heartbeat every 15s
- `#[Broadcast('channel')]` attribute for auto-broadcast

**OAuth (Google, GitHub)**The framework provides an OAuth engine (`src/Core/OAuth/`) with Google and GitHub providers. To use it, generate a controller and routes:

```
./forge make:controller OAuthController
```

Then configure routes in `app/Routes/`:

```
// app/Routes/public.php
$router->get('/auth/{provider}/redirect', [OAuthController::class, 'redirect']);
$router->get('/auth/{provider}/callback', [OAuthController::class, 'callback']);
```

Example usage in the controller:

```
use Fennec\Core\OAuth\OAuthManager;

class OAuthController
{
    public function __construct(private OAuthManager $oauth) {}

    public function redirect(string $provider): array
    {
        $driver = $this->oauth->driver($provider);
        $url = $driver->getAuthorizationUrl($state);
        return ['redirect_url' => $url];
    }

    public function callback(string $provider): array
    {
        $driver = $this->oauth->driver($provider);
        $token = $driver->getAccessToken($_GET['code']);
        $user = $driver->getUserInfo($token->accessToken);
        // create/find the user, generate a JWT...
    }
}
```

```
OAUTH_GOOGLE_CLIENT_ID=...
OAUTH_GOOGLE_CLIENT_SECRET=...
OAUTH_GOOGLE_REDIRECT_URI=http://localhost:8080/auth/google/callback
```

Zero external dependencies — HTTP via `stream_context_create`.

**SOC 2 Compliance**The framework includes the technical controls required for SOC 2 Type II.

### Audit Trail

[](#audit-trail)

Automatic tracking of create/update/delete on Models:

```
#[Table('users'), Auditable(except: ['password'])]
class User extends Model
{
    use HasAuditTrail;
}

// Generate the full module (migration + Model + DTOs + Controller + Routes)
./forge make:audit
./forge migrate
```

Each mutation records: `auditable_type`, `auditable_id`, `action`, `old_values`, `new_values`, `user_id`, `ip_address`, `request_id`, `created_at`.

### Security Event Logger

[](#security-event-logger)

Dedicated Monolog channel `security` → `var/logs/security.log` + stderr (K8s ready):

```
SecurityLogger::alert('auth.failed', ['email' => $email]);
SecurityLogger::track('token.revoked', ['user_id' => 42]);
SecurityLogger::critical('brute_force.detected', ['attempts' => 100]);
```

Events automatically logged by middleware:

- `auth.missing_token`, `auth.invalid_token`, `auth.revoked_token`, `auth.insufficient_role`
- `rate_limit.exceeded`

Each entry is enriched with: `request_id`, `ip`, `uri`, `method`, `user`, `timestamp`.

### Encryption at Rest (AES-256-GCM)

[](#encryption-at-rest-aes-256-gcm)

Transparent encryption of sensitive fields in the database:

```
#[Table('users')]
class User extends Model
{
    use HasEncryptedFields;

    #[Encrypted]
    public string $phone;

    #[Encrypted]
    public string $address;
}

// Values are encrypted in DB (enc: prefix) and decrypted on read
$user->phone = '+33612345678';  // stored: enc:base64(iv+tag+cipher)
$user->save();
echo $user->phone;               // +33612345678
```

```
# Generate: php -r "echo base64_encode(random_bytes(32));"
ENCRYPTION_KEY=your_base64_32_byte_key
```

### Token Hardening

[](#token-hardening)

Configurable TTL (SOC 2 compliant defaults):

```
JWT_ACCESS_TTL=900       # 15 minutes (default)
JWT_REFRESH_TTL=86400    # 24 hours (default)
```

### CORS Whitelist

[](#cors-whitelist)

Controlled origins in production:

```
CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
```

- In dev (`APP_ENV=dev`): everything allowed
- In prod: only listed origins receive CORS headers
- Without config in prod: no origins allowed

### Log Masking

[](#log-masking)

Automatic masking of sensitive data in all logs:

```
password, token, secret, authorization, credit_card, ssn, api_key → ***

```

Configurable via `LOG_MASK_FIELDS` to add custom keys.

### SOC 2 Summary

[](#soc-2-summary)

CriterionControlComponentTraceabilityAudit trail on Models + admin API`#[Auditable]` + `HasAuditTrail` + `make:audit`Incident detectionSecurity event logging`SecurityLogger`ConfidentialityEncryption at rest`#[Encrypted]` + AES-256-GCMLimited sessionsToken TTL 15min/24h`JWT_ACCESS_TTL` / `JWT_REFRESH_TTL`Access controlCORS whitelist`CORS_ALLOWED_ORIGINS`Log protectionSensitive data masking`LogMaskingProcessor`**ISO 27001 Compliance**ISO 27001 Annex A technical controls built into the framework.

### Password Policy (A.8.5)

[](#password-policy-a85)

Password strength validation:

```
$errors = PasswordPolicy::validate($password);
// Checks: length (12+), uppercase, lowercase, digit, special, common words

PasswordPolicy::assertValid($password); // RuntimeException if invalid

$score = PasswordPolicy::strength($password); // 0-5
```

```
PASSWORD_MIN_LENGTH=12    # configurable
```

### Account Lockout (A.8.5)

[](#account-lockout-a85)

Automatic lockout after N failed attempts:

```
if (AccountLockout::isLocked($email)) {
    // account locked, return 429
}

AccountLockout::recordFailure($email);  // +1 attempt
AccountLockout::reset($email);          // after successful login
```

```
LOCKOUT_MAX_ATTEMPTS=5    # attempts before lockout
LOCKOUT_DURATION=900      # 15 min lockout
```

Automatically integrated into `TokenController` — each login failure/success is logged in `SecurityLogger`.

### IP Allowlist (A.8.5)

[](#ip-allowlist-a85)

IP-based access restriction for sensitive routes:

```
$router->group([
    'middleware' => [[IpAllowlistMiddleware::class]],
], function ($router) {
    // admin routes only accessible from allowed IPs
});
```

```
IP_ALLOWLIST=10.0.0.0/8,192.168.1.0/24,127.0.0.1
```

Supports exact IPs and CIDRs. Without configuration, the middleware allows everything (opt-in).

### Log Integrity HMAC (A.8.15)

[](#log-integrity-hmac-a815)

Each `SecurityLogger` entry includes a chained HMAC SHA-256:

```
{"event": "auth.failed", ..., "_hmac": "a1b2c3..."}

```

Each entry's HMAC depends on the previous HMAC — any deletion or modification of a line breaks the chain. Key derived from `SECRET_KEY`.

### Data Retention (A.5.33)

[](#data-retention-a533)

Automatic purge of old audit logs:

```
./forge audit:purge                  # purge > 365 days (default)
./forge audit:purge --days=90        # purge > 90 days
./forge audit:purge --dry-run        # preview without deleting
```

```
AUDIT_RETENTION_DAYS=365
```

### Login Auditing (A.8.15)

[](#login-auditing-a815)

All authentication events are logged in `security.log`:

EventWhen`auth.login_success`Successful login`auth.login_failed`Incorrect password or unknown user`auth.account_locked`Account locked (too many attempts)`auth.missing_token`Request without Bearer token`auth.invalid_token`Invalid or expired JWT`auth.revoked_token`Token revoked in database`auth.insufficient_role`Insufficient role`rate_limit.exceeded`Rate limit exceeded`access.ip_blocked`Unauthorized IP### ISO 27001 Summary

[](#iso-27001-summary)

Annex A ControlImplementationA.8.5 Secure authentication`PasswordPolicy` + `AccountLockout`A.8.5 Access control`IpAllowlistMiddleware`A.8.12 Data leak prevention`LogMaskingProcessor` on all loggersA.8.15 Integrated loggingHMAC chain + complete auth event auditingA.5.33 Data retention`audit:purge` command**NF525 — Certified Invoicing**NF525 compliance module for invoicing software in France. Covers the 4 pillars: immutability, security, preservation, and archiving.

### Setup

[](#setup-1)

```
./forge make:nf525    # generates the full module (migration + 4 Models + DTOs + Controller + Routes)
./forge migrate       # create the tables
```

**Generated API (admin only):**

```
GET    /nf525/invoices              List invoices
GET    /nf525/invoices/{id}         Details with lines
POST   /nf525/invoices              Create an invoice
POST   /nf525/invoices/{id}/credit  Create a credit note
GET    /nf525/closings              List closings
POST   /nf525/closings              Trigger a closing
GET    /nf525/verify                Verify the hash chain
GET    /nf525/fec/export            Export the FEC
GET    /nf525/journal               Event journal
GET    /nf525/stats                 NF525 statistics

```

### Immutable Invoice Model

[](#immutable-invoice-model)

```
#[Table('invoices'), Nf525(prefix: 'FA')]
class Invoice extends Model
{
    use HasNf525;
}

// Create an invoice — automatic numbering + hash chain
$invoice = Invoice::create([
    'client_name' => 'SARL Dupont',
    'total_ht' => 1000.00,
    'tva' => 200.00,
    'total_ttc' => 1200.00,
]);
// number: FA-2026-000001
// hash: sha256(previous_hash + data)

// Modify? FORBIDDEN
$invoice->total_ht = 500;
$invoice->save();  // RuntimeException: NF525 — modification forbidden

// Delete? FORBIDDEN
$invoice->delete();  // RuntimeException: NF525 — deletion forbidden

// Correct? Via credit note
$credit = $invoice->createCredit('Amount error');
// FA-2026-000002 (credit note, negative amounts, references the original invoice)
```

### Signed Periodic Closings

[](#signed-periodic-closings)

```
./forge nf525:close --daily=2026-03-22     # daily closing
./forge nf525:close --monthly=2026-03       # monthly closing
./forge nf525:close --annual=2026           # annual closing
```

Each closing generates: HT/VAT/TTC totals, document count, cumulative grand total, HMAC-SHA256 hash chained with the previous closing.

### FEC Export (Accounting Entries File)

[](#fec-export-accounting-entries-file)

```
./forge nf525:export --year=2026                    # export in FEC format
./forge nf525:export --year=2026 --output=FEC.txt   # to a specific file
```

Standardized TSV format: JournalCode, EcritureDate, CompteNum, CompteLib, Debit, Credit, etc.

### Integrity Verification

[](#integrity-verification)

```
./forge nf525:verify                    # verify the invoices table
./forge nf525:verify --table=invoices   # specific table
```

Traverses the entire hash chain and detects anomalies (modification, deletion, insertion).

### The 4 NF525 Pillars

[](#the-4-nf525-pillars)

PillarImplementation**Immutability**`HasNf525` trait — blocks update/delete, corrections via credit notes**Security**Sequential SHA-256 hash chain on each invoice**Preservation**`ClosingService` — signed closings (daily/monthly/annual)**Archiving**`FecExporter` — standardized FEC export for the tax authority### Tables Generated by make:nf525

[](#tables-generated-by-makenf525)

TablePurpose`invoices`Invoices with hash chain (number, hash, previous\_hash)`invoice_lines`Invoice lines (description, quantity, price, VAT)`nf525_closings`Periodic closings (totals, HMAC hash, cumulative)`nf525_journal`NF525 event journal**GDPR — Consent &amp; Compliance**Complete GDPR consent management module with legal document versioning, traceability, and data subject rights.

### Setup

[](#setup-2)

```
./forge make:rgpd    # generates the full module (migration + Models + DTOs + Controller + Routes)
./forge migrate      # create the tables
```

**11 files generated:**

```
database/migrations/..._create_rgpd_tables.php
app/Models/ConsentObject.php          # versioned legal documents
app/Models/UserConsent.php            # consents + DPO functions
app/Dto/ConsentObject*.php            # 4 DTOs
app/Dto/UserConsentRequest.php
app/Dto/RgpdStatsResponse.php
app/Controllers/ConsentController.php # 14 endpoints
app/Routes/consent.php                # 4 route groups by role

```

### Versioned Legal Documents

[](#versioned-legal-documents)

```
// Create a new version (automatic chaining)
ConsentObject::createNewVersion('tos', 'Terms of Service v3', '...', isRequired: true);
// object_version: 3, object_previous_version: id_v2

// Latest version by key
ConsentObject::latestByKey('tos');      // latest active version
ConsentObject::allLatest();             // all keys, latest version
```

### User Consent

[](#user-consent)

```
// Record a consent
UserConsent::recordConsent($userId, $docId, status: true, objectVersion: 3, way: 'web');

// Check compliance
UserConsent::hasAcceptedAll($userId);   // true if all accepted (latest version)

// History (GDPR right of access)
UserConsent::userHistory($userId);

// Export (GDPR right to portability)
UserConsent::exportForUser($userId);

// Withdrawal (GDPR right to erasure)
UserConsent::withdrawAll($userId);
```

### DPO Dashboard

[](#dpo-dashboard)

```
// Compliance rate
UserConsent::complianceRate();
// { total_active_users: 15000, compliant_users: 14950, compliance_rate: 99.67 }

// Stats per document
UserConsent::statsByDocument();
// [{ object_name: 'ToS', accepted: 15304, refused: 26 }, ...]

// Non-compliant users
UserConsent::nonCompliantUsers(limit: 50);
```

### API (14 endpoints)

[](#api-14-endpoints)

```
Public:
  GET    /consent/documents/{key}/latest       Latest version of a document

Authenticated user (all roles):
  POST   /consent/me                           Give consent
  GET    /consent/me                           My consent status
  DELETE /consent/me                           Withdraw my consents

Admin:
  GET    /consent/documents                    List documents
  GET    /consent/documents/{id}               Document details
  POST   /consent/documents                    Create a new version

DPO / Admin:
  GET    /consent/dpo/dashboard                Full dashboard
  GET    /consent/dpo/stats                    Stats per document
  GET    /consent/dpo/compliance               Compliance rate
  GET    /consent/dpo/non-compliant            Non-compliant users
  GET    /consent/dpo/users/{id}/history       User history
  GET    /consent/dpo/users/{id}/export        Portability export
  DELETE /consent/dpo/users/{id}/consents      Right to erasure

```

### Tables

[](#tables)

TablePurpose`consent_objects`Versioned legal documents (ToS, legal notices, privacy policy)`user_consents`User consents with traceability (status, method, version, dates)### GDPR Rights Covered

[](#gdpr-rights-covered)

RightEndpointRight of access (art. 15)`GET /consent/dpo/users/{id}/history`Right to portability (art. 20)`GET /consent/dpo/users/{id}/export`Right to object (art. 21)`DELETE /consent/me`Right to erasure (art. 17)`DELETE /consent/dpo/users/{id}/consents`Proof of consent (art. 7)`user_consents` table (consent\_way, timestamp, version)**Redis Cache**```
// Simple API
$user = Cache::remember("user:{$id}", 3600, fn() => User::find($id));
Cache::forget("user:{$id}");

// Tags (group invalidation)
Cache::tags(['users'])->set("user:{$id}", $user, 3600);
Cache::tags(['users'])->flush();  // invalidate the entire group
```

`RedisConnection` shared with RateLimiter, Scheduler, Queue and EventDispatcher.

**PDF Generation (dompdf)**PDF generation from HTML/CSS via [dompdf](https://github.com/dompdf/dompdf).

```
use Dompdf\Dompdf;
use Dompdf\Options;

$options = new Options();
$options->set('defaultFont', 'Helvetica');

$dompdf = new Dompdf($options);
$dompdf->loadHtml('My InvoiceHTML content...');
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();

header('Content-Type: application/pdf');
echo $dompdf->output();
```

### Demo

[](#demo)

```
GET /pdf/demo   → sample invoice generated as PDF

```

### Useful Options

[](#useful-options)

OptionDescription`defaultFont`Default font (Helvetica, Courier, Times)`isRemoteEnabled`Load external images/CSS (`false` by default)`setPaper('A4', 'landscape')`Format and orientation`$dompdf->output()`Returns PDF content as string`stream('file.pdf')`Forces browser download### Tips

[](#tips)

- Use `` for layouts (better dompdf support than flexbox/grid)
- `position: fixed; bottom: 30px;` to stick a footer at the bottom of the page
- Prefer standard fonts (Helvetica, Courier, Times) or embed custom fonts

**Tutorials**12 tutorials with corrected exercises in `Tutos/` — open `Tutos/index.html` in a browser.

**Worker Mode:**

\#TitleLevel01Lifecycle &amp; Golden RulesBeginner02Patterns &amp; ArchitectureIntermediate03Memory &amp; MonitoringIntermediate04Worker ExercisesAll levels**Framework:**

\#TitleLevel05Routing &amp; MiddlewareBeginner06ORM &amp; Query BuilderIntermediate07Auth, DI &amp; EventsIntermediate08Framework ExercisesAll levels**Advanced Features:**

\#TitleLevel09Profiler, Rate Limiting &amp; SecurityIntermediate10Migrations, Seeding &amp; CacheBeginner-Intermediate11Scheduler, Queue &amp; Feature FlagsIntermediate12Notifications, SSE &amp; OAuthIntermediate-Advanced**Configuration (.env)**```
# Database driver (pgsql | mysql | sqlite)
DB_DRIVER=pgsql

# PostgreSQL (if DB_DRIVER=pgsql)
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=fennectra
POSTGRES_USER=fennectra
POSTGRES_PASSWORD=secret

# MySQL (if DB_DRIVER=mysql)
# MYSQL_HOST=localhost
# MYSQL_PORT=3306
# MYSQL_DB=myapp
# MYSQL_USER=root
# MYSQL_PASSWORD=secret

# SQLite (if DB_DRIVER=sqlite)
# SQLITE_DB=var/database.sqlite

# JWT
SECRET_KEY=your_jwt_key_32_chars_minimum

# Event Broker (sync | redis | database)
EVENT_BROKER=sync

# Redis (events, cache, rate limit, scheduler, queue)
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=0
REDIS_PREFIX=app:events:

# Queue
QUEUE_DRIVER=redis

# Scheduler (inside the FrankenPHP worker)
SCHEDULER_ENABLED=1

# Profiler
PROFILER_ENABLED=1

# OAuth
OAUTH_GOOGLE_CLIENT_ID=
OAUTH_GOOGLE_CLIENT_SECRET=
OAUTH_GOOGLE_REDIRECT_URI=http://localhost:8080/auth/google/callback

# Notifications
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USER=
MAIL_PASSWORD=
MAIL_FROM=noreply@example.com
SLACK_WEBHOOK_URL=

# Deploy
DEPLOY_REGISTRY=europe-west9-docker.pkg.dev/project/repo
DEPLOY_IMAGE=php-app
DEPLOY_NAMESPACE=default

# SOC 2 — Security & Compliance
# JWT_ACCESS_TTL=900
# JWT_REFRESH_TTL=86400
# ENCRYPTION_KEY=
# CORS_ALLOWED_ORIGINS=https://app.example.com
# LOG_MASK_FIELDS=custom_field

# ISO 27001 — Access Controls
# PASSWORD_MIN_LENGTH=12
# LOCKOUT_MAX_ATTEMPTS=5
# LOCKOUT_DURATION=900
# IP_ALLOWLIST=10.0.0.0/8,127.0.0.1
# AUDIT_RETENTION_DAYS=365

# Environment
APP_ENV=dev
```

---

**Tutorial: Full CRUD in 1 Command**```
./forge make:all Product --roles=admin,manager
```

**5 files generated:**

```
app/Models/Product.php                ← ORM Model
app/Dto/ProductRequest.php            ← input DTO (validation)
app/Dto/ProductResponse.php           ← output DTO
app/Controllers/ProductController.php ← CRUD controller
app/Routes/product.php                ← REST Routes

```

**Created routes:**

```
GET    /product          →  index   (paginated list)
GET    /product/{id}     →  show    (details)
POST   /product          →  store   (create)
PUT    /product/{id}     →  update  (modify)
DELETE /product/{id}     →  delete  (delete)

```

**Variants:**

```
./forge make:all Invoice --connection=job --roles=admin   # secondary database
./forge make:all Article --no-auth                        # without auth
```

### Full Business Modules

[](#full-business-modules)

The following `make:*` commands generate a complete module (migration + Models + DTOs + Controller + Routes with roles):

```
./forge make:rgpd       # GDPR consent (14 endpoints, DPO dashboard)
./forge make:audit      # SOC 2 audit trail (6 endpoints, admin)
./forge make:webhook    # HMAC-SHA256 webhooks (10 endpoints, admin)
./forge make:nf525      # NF525 invoicing (10 endpoints, admin)
```

Each command is **idempotent** — re-running does not duplicate anything.

---

**Dependencies:** `monolog/monolog` · `firebase/php-jwt` · `dompdf/dompdf` · `intervention/image` · `aws/aws-sdk-php` · `google/cloud-storage`**PHP:** &gt;= 8.3 | **Runtime:** FrankenPHP Worker or PHP-FPM | **License:** Proprietary

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance100

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity51

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 ~0 days

Total

9

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/34e387a5ca5ebd9e2f56f31d69047e13ef765b96d8550ede4f79e558513b300f?d=identicon)[djdevpro](/maintainers/djdevpro)

---

Top Contributors

[![djdevpro](https://avatars.githubusercontent.com/u/270788?v=4)](https://github.com/djdevpro "djdevpro (1 commits)")

---

Tags

phpjwtapiframeworkormmvcfrankenphp

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/fennectra-framework/health.svg)

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

###  Alternatives

[letsdrink/ouzo

Ouzo PHP MVC framework

7210.5k1](/packages/letsdrink-ouzo)[mirekmarek/php-jet

PHP Jet is modern, powerful, real-life proven, really fast and secure, small and light-weight framework for PHP8 with great clean and flexible modular architecture containing awesome developing tools. No magic, just clean software engineering.

241.3k](/packages/mirekmarek-php-jet)

PHPackages © 2026

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