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

ActiveLibrary[Framework](/categories/framework)

imamx39/php-framework
=====================

A custom PHP framework inspired by Symfony/Laravel — ORM, Router, Auth, Events, Cache, Forms, Mailer, Queue, Pipeline and more.

10PHPCI failing

Since Apr 19Pushed 1mo agoCompare

[ Source](https://github.com/IMAMx39/Php-framework)[ Packagist](https://packagist.org/packages/imamx39/php-framework)[ RSS](/packages/imamx39-php-framework/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependenciesVersions (7)Used By (0)

PHP Framework
=============

[](#php-framework)

Un framework PHP moderne inspiré de Symfony et Laravel, construit from scratch.

[![CI](https://github.com/IMAMx39/Php-framework/actions/workflows/ci.yml/badge.svg)](https://github.com/IMAMx39/Php-framework/actions/workflows/ci.yml)[![PHP](https://camo.githubusercontent.com/83dd395020c37276225039739320f6c8e7e99963ab21ee3d09282cb48dad2a60/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532422d626c7565)](https://php.net)[![License](https://camo.githubusercontent.com/f8df3091bbe1149f398a5369b2c39e896766f9f6efba3477c63e9b4aa940ef14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e)](LICENSE)

---

Sommaire
--------

[](#sommaire)

- [Installation](#installation)
- [Configuration](#configuration)
- [Structure du projet](#structure-du-projet)
- [Démarrage rapide](#d%C3%A9marrage-rapide)
- [Fonctionnalités](#fonctionnalit%C3%A9s)
    - [Routing](#routing)
    - [Contrôleurs &amp; Injection](#contr%C3%B4leurs--injection)
    - [ORM](#orm)
    - [DTO / Value Objects](#dto--value-objects)
    - [Collection](#collection)
    - [Query Scopes](#query-scopes)
    - [Gates &amp; Policies](#gates--policies)
    - [Pipeline](#pipeline)
    - [Authentication](#authentication)
    - [Formulaires](#formulaires)
    - [Validation](#validation)
    - [Cache](#cache)
    - [Queue / Jobs](#queue--jobs)
    - [HTTP Client](#http-client)
    - [Mailer](#mailer)
    - [Serializer](#serializer)
    - [File Storage](#file-storage)
    - [Rate Limiter](#rate-limiter)
    - [Events](#events)
    - [Logger](#logger)
    - [CSRF Protection](#csrf-protection)
    - [Debug Toolbar](#debug-toolbar)
- [Console](#console)
- [Tests](#tests)

---

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

[](#installation)

```
composer create-project imamx39/php-framework mon-projet
cd mon-projet
```

Le script d'init s'exécute automatiquement : `.env` copié depuis `.env.example`, répertoires `var/` et `storage/` créés.

---

Configuration
-------------

[](#configuration)

Édite `.env` à la racine du projet :

```
APP_NAME=MonApp
APP_ENV=development
APP_DEBUG=true
APP_URL=http://localhost:8000

# Base de données
DATABASE_URL=mysql://root:@127.0.0.1:3306/ma_base
# SQLite (dev) :
# DATABASE_URL=sqlite:////chemin/absolu/vers/db.sqlite

# Mailer (optionnel — NullMailer si vide)
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=xxxxx
MAIL_PASSWORD=xxxxx
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@monapp.com
MAIL_FROM_NAME="${APP_NAME}"

# Queue (optionnel — "file" par défaut)
QUEUE_DRIVER=file   # ou "sync" pour le dev/test
```

Lance le serveur de développement :

```
composer serve
# → http://localhost:8000
```

---

Structure du projet
-------------------

[](#structure-du-projet)

```
mon-projet/
├── app/
│   ├── Controller/        # Contrôleurs de l'application
│   ├── Entity/            # Entités ORM
│   ├── Pipeline/          # Étapes de pipeline métier
│   └── Repository/        # Repositories
├── bin/
│   ├── console            # CLI
│   └── setup              # Script d'initialisation
├── config/
│   ├── routes.php         # Définition des routes
│   └── services.php       # Conteneur de services (DI)
├── migrations/            # Migrations SQL versionnées
├── public/
│   └── index.php          # Point d'entrée HTTP
├── src/                   # Code source du framework
├── templates/             # Templates Twig
├── tests/                 # Tests PHPUnit
├── var/
│   ├── cache/             # Cache compilé (auto)
│   ├── logs/              # Logs (auto)
│   └── queue/             # Jobs en attente (auto)
├── storage/app/           # Fichiers uploadés
├── .env                   # Config locale (ignoré par git)
├── .env.example           # Template à copier
└── composer.json

```

---

Démarrage rapide
----------------

[](#démarrage-rapide)

### 1. Créer une entité

[](#1-créer-une-entité)

```
php bin/console make:entity Product
```

Génère `app/Entity/Product.php` et `app/Repository/ProductRepository.php`.

```
#[Entity(table: 'products', repositoryClass: ProductRepository::class)]
class Product
{
    #[Id] #[GeneratedValue] #[Column(type: 'integer')]
    private ?int $id = null;

    #[Column(type: 'string', length: 255)]
    private string $name;

    #[Column(type: 'decimal')]
    private float $price;
}
```

### 2. Créer et lancer une migration

[](#2-créer-et-lancer-une-migration)

```
php bin/console make:migration CreateProductsTable
```

Édite le fichier généré dans `migrations/` :

```
public function up(): void
{
    $this->execute('
        CREATE TABLE products (
            id    INTEGER PRIMARY KEY AUTOINCREMENT,
            name  VARCHAR(255) NOT NULL,
            price DECIMAL(10,2) NOT NULL
        )
    ');
}

public function down(): void
{
    $this->execute('DROP TABLE products');
}
```

```
php bin/console migrate
php bin/console migrate:status    # état des migrations
php bin/console migrate:rollback  # annuler la dernière
```

### 3. Créer un contrôleur

[](#3-créer-un-contrôleur)

```
php bin/console make:controller Product
```

```
class ProductController extends AbstractController
{
    public function __construct(
        private readonly ProductRepository $repo,
    ) {}

    #[Route('/products', name: 'product.index', methods: ['GET'])]
    public function index(): Response
    {
        return $this->render('product/index.html.twig', [
            'products' => $this->repo->findAll(),
        ]);
    }
}
```

---

Fonctionnalités
---------------

[](#fonctionnalités)

### Routing

[](#routing)

```
// config/routes.php
$router->get('/products',         [ProductController::class, 'index']);
$router->post('/products',        [ProductController::class, 'store']);
$router->get('/products/{id}',    [ProductController::class, 'show']);
$router->put('/products/{id}',    [ProductController::class, 'update']);
$router->delete('/products/{id}', [ProductController::class, 'destroy']);
```

Via attribut PHP 8 directement sur la méthode :

```
#[Route('/products/{id}', name: 'product.show', methods: ['GET'])]
public function show(Request $request, int $id): Response
{
    $product = $this->repo->find($id);
    // ...
}
```

---

### Contrôleurs &amp; Injection

[](#contrôleurs--injection)

Les dépendances sont injectées automatiquement dans le constructeur via le conteneur :

```
class OrderController extends AbstractController
{
    public function __construct(
        private readonly OrderRepository  $orders,
        private readonly Dispatcher       $queue,
        private readonly Gate             $gate,
        private readonly Logger           $logger,
    ) {}
}
```

Helpers disponibles dans tout contrôleur :

```
$this->render('template.html.twig', $data);  // réponse HTML Twig
$this->json($data, 200);                     // réponse JSON
$this->redirect('/url');                     // redirection
```

---

### ORM

[](#orm)

#### Lecture

[](#lecture)

```
$product  = $repo->find(1);
$products = $repo->findAll();
$actifs   = $repo->findBy(['active' => 1], ['name' => 'ASC']);
$one      = $repo->findOneBy(['email' => 'a@b.com']);
$total    = $repo->count(['active' => 1]);

// Pagination
$page = $repo->paginate(page: 1, perPage: 15);
$page->items();     // entités de la page courante
$page->total();     // nombre total d'enregistrements
$page->lastPage();
$page->hasMore();
$page->from();      // rang du 1er élément
$page->to();        // rang du dernier
```

#### Persistance

[](#persistance)

```
$repo->save($product);   // INSERT si id null, UPDATE sinon
$repo->delete($product);
```

#### Relations

[](#relations)

```
#[Entity(table: 'posts')]
class Post
{
    #[ManyToOne(targetEntity: User::class, joinColumn: 'user_id')]
    private ?User $author = null;

    #[OneToMany(targetEntity: Comment::class, mappedBy: 'post_id')]
    private array $comments = [];

    #[ManyToMany(
        targetEntity: Tag::class,
        joinTable:    'post_tags',
        joinColumn:   'post_id',
        inverseJoinColumn: 'tag_id',
    )]
    private array $tags = [];
}

// Chargement explicite
$post = $repo->find(1, relations: ['author', 'tags', 'comments']);

// Gestion ManyToMany
$repo->attach($post, $tag, 'tags');
$repo->detach($post, $tag, 'tags');
$repo->sync($post, [$tag1, $tag2], 'tags');
```

#### Enum Support

[](#enum-support)

Les `BackedEnum` PHP sont castés automatiquement :

```
enum Status: string
{
    case Active   = 'active';
    case Inactive = 'inactive';
}

#[Entity(table: 'users')]
class User
{
    #[Column(type: 'string')]
    private Status $status = Status::Active;
}

// Stocké en DB : 'active' | En PHP : Status::Active
$user->getStatus() === Status::Active; // true
```

---

### DTO / Value Objects

[](#dto--value-objects)

Objets immutables créés et validés automatiquement depuis la `Request` :

```
class CreateUserDTO extends AbstractDTO
{
    public function __construct(
        #[Validate('required|email')]
        public readonly string $email,

        #[Validate('required|min:8')]
        public readonly string $password,

        #[Validate('required|min:2|max:100')]
        public readonly string $name,

        public readonly string $role = 'user',
    ) {}
}
```

Dans le contrôleur, le DTO est injecté directement — zéro boilerplate :

```
public function register(CreateUserDTO $dto): Response
{
    // $dto est déjà validé, castés et prêt à l'emploi
    $user = new User($dto->email, $dto->name, $dto->role);
    $this->repo->save($user);

    return $this->json(['status' => 'created'], 201);
}
```

Si la validation échoue, une `ValidationException` est levée automatiquement avec les erreurs.

**Casts automatiques** : `int`, `float`, `bool`, `string`, `DateTimeImmutable`, `DateTime`.

---

### Collection

[](#collection)

Wrapper fluent et chaînable autour d'un tableau, inspiré de Laravel :

```
$users = collect($repo->findAll());

// Filtrer, transformer, trier
$emails = $users
    ->filter(fn($u) => $u->isActive())
    ->sortBy(fn($u) => $u->getName())
    ->map(fn($u) => $u->getEmail())
    ->values();

// Agrégats
$total    = collect($orders)->sum(fn($o) => $o->getTotal());
$average  = collect($scores)->avg();
$max      = collect($prices)->max();

// Grouper / découper
$byRole   = collect($users)->groupBy(fn($u) => $u->getRole());
$chunks   = collect($items)->chunk(10);

// Recherche
$admin    = collect($users)->first(fn($u) => $u->getRole() === 'admin');
$hasAdmin = collect($users)->some(fn($u) => $u->getRole() === 'admin');
$allOk    = collect($items)->every(fn($i) => $i->isValid());

// Transformation
$flat     = collect([[1, 2], [3, 4]])->flatten();
$names    = collect($users)->pluck('name');
$unique   = collect([1, 2, 2, 3])->unique()->values();

// Sérialisation
$array = $collection->toArray();
$json  = $collection->toJson();
```

Le helper global `collect()` est disponible partout dans l'application.

---

### Query Scopes

[](#query-scopes)

Encapsuler des filtres réutilisables directement dans le repository :

```
class UserRepository extends AbstractRepository
{
    protected function getEntityClass(): string { return User::class; }

    // Définir des scopes — préfixe "scope" + nom en PascalCase
    public function scopeActive(QueryBuilder $qb): QueryBuilder
    {
        return $qb->where('active', 1);
    }

    public function scopeRole(QueryBuilder $qb, string $role): QueryBuilder
    {
        return $qb->where('role', $role);
    }

    public function scopeRecent(QueryBuilder $qb, int $days = 30): QueryBuilder
    {
        return $qb->where('created_at', '>=', date('Y-m-d', strtotime("-{$days} days")));
    }
}
```

Utilisation avec chaînage fluent :

```
// Récupérer
$users = $repo->active()->get();                       // Collection
$admin = $repo->active()->role('admin')->first();      // ?object
$count = $repo->active()->role('user')->count();       // int

// Paginer
$page  = $repo->active()->recent(7)->paginate(page: 1, perPage: 20);

// Avec chargement de relations
$users = $repo->active()->get(relations: ['profile']);
```

---

### Gates &amp; Policies

[](#gates--policies)

Système d'autorisation en deux niveaux : **gates** simples et **policies** par entité.

#### Définir des abilities

[](#définir-des-abilities)

```
// config/services.php (Gate déjà enregistré en singleton)
$gate = $container->get(Gate::class);

$gate->define('admin',     fn(User $u) => $u->getRole() === 'admin');
$gate->define('moderator', fn(User $u) => in_array($u->getRole(), ['admin', 'moderator']));
```

#### Policies par entité

[](#policies-par-entité)

```
class PostPolicy
{
    public function edit(User $user, Post $post): bool
    {
        return $user->getId() === $post->getUserId();
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->getRole() === 'admin'
            || $user->getId() === $post->getUserId();
    }
}

// Enregistrement
$gate->policy(Post::class, PostPolicy::class);
```

#### Vérifications dans les contrôleurs

[](#vérifications-dans-les-contrôleurs)

```
// Vérifier
$gate->allows('admin');                // bool
$gate->allows('edit', $post);         // bool — passe par la policy
$gate->denies('edit', $post);         // bool

// Autoriser (lève ForbiddenException HTTP 403 si refusé)
$gate->authorize('edit', $post);

// Vérifier le rôle
$gate->hasRole('admin');
$gate->hasRole('admin', 'moderator'); // OR
```

---

### Pipeline

[](#pipeline)

Faire transiter une valeur à travers une série d'étapes ordonnées et indépendantes.

```
// Chaque étape reçoit la valeur et un callable $next
class ValidateStock
{
    public function handle(OrderData $order, callable $next): OrderData
    {
        if ($order->quantity > $this->available()) {
            throw new HttpException(422, 'Stock insuffisant.');
            // ← court-circuit : les étapes suivantes ne s'exécutent pas
        }
        return $next($order); // ← passe à l'étape suivante
    }
}

class ApplyDiscount
{
    public function handle(OrderData $order, callable $next): OrderData
    {
        if ($order->total >= 100) {
            $order->total *= 0.90; // -10 %
        }
        return $next($order);
    }
}
```

```
// Dans un contrôleur ou service
$order = Pipeline::send(new OrderData(...))
    ->through([
        ValidateStock::class,
        ApplyDiscount::class,
        SendInvoice::class,
    ])
    ->thenReturn();
```

Fonctionnalités avancées :

```
// Closures inline
Pipeline::send($value)
    ->through([
        fn($v, $next) => $next(trim($v)),
        fn($v, $next) => $next(strtolower($v)),
    ])
    ->thenReturn();

// Ajouter une étape dynamiquement
$pipeline->pipe(fn($v, $next) => $next($v));

// Méthode personnalisée (défaut : handle)
->via('process')

// Destination finale
->then(fn($order) => new JsonResponse($order));

// Immutable — through() et pipe() retournent un clone
$base     = Pipeline::send($v)->through([StepA::class]);
$extended = $base->pipe(StepB::class); // $base inchangé
```

---

### Authentication

[](#authentication)

```
$auth = $container->get(Auth::class);

// Connexion
if ($auth->attempt($email, $password)) {
    return $this->redirect('/dashboard');
}

// Vérifications
$auth->check();   // bool
$auth->user();    // User|null
$auth->id();      // int|null
$auth->logout();
```

#### Middlewares Auth

[](#middlewares-auth)

```
// config/routes.php ou Application
new AuthMiddleware($auth)       // redirige vers /login si non connecté
new GuestMiddleware($auth)      // redirige vers /dashboard si déjà connecté
```

---

### Formulaires

[](#formulaires)

```
class RegisterFormType extends AbstractFormType
{
    public function buildForm(FormBuilder $builder): void
    {
        $builder
            ->add('name',     'text',     ['rules' => 'required|min:2'])
            ->add('email',    'email',    ['rules' => 'required|email'])
            ->add('password', 'password', ['rules' => 'required|min:8|confirmed'])
            ->add('password_confirmation', 'password', []);
    }
}

// Contrôleur
$form = $factory->create(new RegisterFormType());
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    $data = $form->getData(); // tableau des valeurs validées
}
```

Dans Twig :

```
{{ form_start(form, '/register', 'POST') }}
    {{ form_row(form, 'name') }}
    {{ form_row(form, 'email') }}
    {{ form_row(form, 'password') }}
    {{ form_row(form, 'password_confirmation') }}
    {{ csrf_field() }}
    S'inscrire
{{ form_end() }}
```

Types disponibles : `text` `email` `password` `number` `textarea` `select` `checkbox` `hidden`.

---

### Validation

[](#validation)

```
$validator = new Validator();
$errors    = $validator->validate($request->all(), [
    'name'     => 'required|string|min:2|max:100',
    'email'    => 'required|email',
    'age'      => 'required|integer|min:18',
    'role'     => 'required|in:admin,user,moderator',
    'password' => 'required|min:8|confirmed',
    'website'  => 'url',
]);

if (!empty($errors)) {
    // $errors = ['email' => ['Email invalide.'], ...]
}
```

Règles disponibles :

RègleDescription`required`Champ obligatoire`string`Doit être une chaîne`integer`Doit être un entier`numeric`Entier ou flottant`boolean`true / false`email`Format email valide`url`URL valide`min:N`Longueur ou valeur minimale`max:N`Longueur ou valeur maximale`between:N,M`Entre N et M`in:a,b,c`Valeur dans la liste`not_in:a,b`Valeur absente de la liste`confirmed`Doit correspondre au champ `_confirmation``regex:/pattern/`Correspond à l'expression régulière---

### Cache

[](#cache)

```
$cache = $container->get(CacheInterface::class);

$cache->put('key', $value, ttl: 3600);   // TTL en secondes
$cache->get('key', default: null);
$cache->has('key');
$cache->forget('key');
$cache->flush();

// Mémoïsation
$users = $cache->remember('users.all', 600, fn() => $repo->findAll());
```

Drivers disponibles : `FileCache` (production), `ArrayCache` (tests).

---

### Queue / Jobs

[](#queue--jobs)

Traitement asynchrone de tâches lourdes : envois d'emails, exports, notifications.

```
// 1. Définir un job
class SendWelcomeEmailJob implements JobInterface
{
    public function __construct(
        private readonly string $email,
        private readonly string $name,
    ) {}

    // Les dépendances de handle() sont injectées automatiquement
    public function handle(MailerInterface $mailer): void
    {
        $mailer->send(
            (new Message())
                ->to($this->email, $this->name)
                ->subject('Bienvenue !')
                ->html("Bonjour {$this->name} !")
        );
    }
}

// 2. Dispatcher depuis un contrôleur
public function register(CreateUserDTO $dto): Response
{
    $this->dispatcher->dispatch(new SendWelcomeEmailJob($dto->email, $dto->name));

    // Avec délai (en secondes)
    $this->dispatcher->dispatch(new ReminderJob($user->id), delay: 3600);

    return $this->json(['status' => 'created'], 201);
}
```

```
php bin/console queue:work          # worker en boucle infinie
php bin/console queue:work --once   # traite un seul job
php bin/console queue:flush         # vide la queue
```

DriverUsageComportement`file` (défaut)ProductionJobs sérialisés dans `var/queue/``sync`Dev / TestsExécution immédiate, pas de fichierRetry automatique : 3 tentatives. Échec définitif → déplacé dans `var/queue/failed/`.

---

### HTTP Client

[](#http-client)

Client HTTP fluent basé sur cURL pour appeler des APIs externes :

```
$client = new HttpClient(baseUrl: 'https://api.example.com');

// GET
$response = $client->get('/users');
$users    = $response->json();        // array décodé
$status   = $response->status();      // 200
$ok       = $response->ok();          // bool

// POST avec JSON
$response = $client->post('/users', [
    'name'  => 'Alice',
    'email' => 'alice@example.com',
]);

// Headers, auth, timeout
$client = (new HttpClient())
    ->withToken($apiKey)                          // Bearer token
    ->withBasicAuth($user, $password)             // Basic Auth
    ->withHeaders(['X-App-Version' => '1.0'])
    ->withTimeout(10);

// Méthodes disponibles
$client->get('/resource');
$client->post('/resource', $data);
$client->put('/resource/1', $data);
$client->patch('/resource/1', $data);
$client->delete('/resource/1');

// Gestion des erreurs
if ($response->failed()) {
    // clientError() → 4xx  |  serverError() → 5xx
}
$response->throw(); // lève HttpClientException si status >= 400
```

---

### Mailer

[](#mailer)

```
$message = (new Message())
    ->from('noreply@monapp.com', 'Mon App')
    ->to($user->getEmail(), $user->getName())
    ->cc('admin@monapp.com')
    ->subject('Bienvenue !')
    ->html('Bonjour !Ton compte est créé.')
    ->text('Bonjour ! Ton compte est créé.');

$mailer->send($message);
```

En développement, le `NullMailer` absorbe les envois sans les transmettre.

---

### Serializer

[](#serializer)

```
$serializer = $container->get(Serializer::class);

// Objet → tableau / JSON
$array = $serializer->normalize($product);
$json  = $serializer->toJson($product);
$json  = $serializer->toJson($products);  // tableau d'objets

// Groupes — exposer différents champs selon le contexte
$public = $serializer->normalize($user, groups: ['public']);
$admin  = $serializer->normalize($user, groups: ['admin']);
```

Annoter les propriétés :

```
#[SerializeGroup('public', 'admin')]
private string $name;

#[SerializeGroup('admin')]            // invisible pour le groupe 'public'
private string $passwordHash;
```

---

### File Storage

[](#file-storage)

```
$storage = $container->get(LocalStorage::class);

// Écriture / lecture
$storage->put('avatars/user-1.jpg', $imageContent);
$content = $storage->get('avatars/user-1.jpg');
$url     = $storage->url('avatars/user-1.jpg'); // → /storage/avatars/user-1.jpg

// Vérification / suppression
$storage->exists('avatars/user-1.jpg');
$storage->delete('avatars/user-1.jpg');

// Upload depuis un formulaire
$path = $storage->putUpload($_FILES['avatar'], directory: 'avatars');

// Lister
$files = $storage->files('avatars');
$all   = $storage->files('', recursive: true);
```

---

### Rate Limiter

[](#rate-limiter)

```
$limiter = $container->get(RateLimiter::class);

if (!$limiter->attempt("login:{$ip}", maxAttempts: 5, decaySeconds: 60)) {
    return new Response('Trop de tentatives. Réessaie dans 1 minute.', 429);
}

$limiter->remaining("login:{$ip}", 5); // tentatives restantes
$limiter->clear("login:{$ip}");        // remettre à zéro
```

Via middleware (appliqué sur une route ou globalement) :

```
new ThrottleMiddleware($limiter, maxAttempts: 60, decaySeconds: 60)
```

---

### Events

[](#events)

```
$dispatcher = $container->get(EventDispatcher::class);

// S'abonner
$dispatcher->on('user.registered', function (Event $event) {
    // envoyer email de bienvenue
}, priority: 10);

// Émettre
$dispatcher->emit('user.registered', new UserRegisteredEvent($user));

// Événements kernel (court-circuitage possible)
$dispatcher->on(KernelEvents::REQUEST, function (RequestEvent $event) {
    $event->setResponse(new Response('En maintenance.', 503));
});
```

---

### Logger

[](#logger)

Compatible PSR-3, deux handlers configurés par défaut :

```
$logger = $container->get(Logger::class);

$logger->debug('Requête reçue', ['url' => $request->getUri()]);
$logger->info('Utilisateur connecté', ['user_id' => $auth->id()]);
$logger->warning('Tentative suspecte', ['ip' => $ip]);
$logger->error('Erreur critique', ['exception' => $e->getMessage()]);
```

- `var/logs/app.log` — tous les niveaux (DEBUG+ en dev, INFO+ en prod)
- `var/logs/error.log` — ERROR et supérieur uniquement

---

### CSRF Protection

[](#csrf-protection)

```
// Middleware global (déjà configuré)
new CsrfMiddleware($csrfManager)

// Avec exemptions (webhooks, APIs)
new CsrfMiddleware($csrfManager, exemptPaths: ['/api/', '/webhook/'])
```

Dans Twig :

```

    {{ csrf_field() }}
    ...

```

Pour les requêtes AJAX :

```

```

```
fetch('/api/data', {
    method: 'POST',
    headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
    body: JSON.stringify(data),
});
```

---

### Debug Toolbar

[](#debug-toolbar)

Activée automatiquement quand `APP_DEBUG=true`.

Injectée avant `` sur toutes les réponses HTML, la toolbar affiche :

- **Requête** — méthode, URL, durée totale
- **Requêtes SQL** — chaque requête avec ses paramètres et son temps d'exécution
- **Logs** — tous les messages enregistrés durant la requête
- **Mémoire** — pic d'utilisation mémoire

```
APP_DEBUG=true   # active la toolbar
APP_DEBUG=false  # désactivée en production
```

---

Console
-------

[](#console)

```
# Génération de code
php bin/console make:entity     NomEntite
php bin/console make:migration  NomMigration
php bin/console make:controller NomController

# Migrations
php bin/console migrate
php bin/console migrate:status
php bin/console migrate:rollback

# Queue
php bin/console queue:work
php bin/console queue:work --once
php bin/console queue:flush
```

---

Tests
-----

[](#tests)

```
composer test
# ou
vendor/bin/phpunit --testdox
```

**516 tests · 839 assertions** — tout vert sur PHP 8.1 / 8.2 / 8.3 / 8.4.

---

Licence
-------

[](#licence)

MIT — [IMAMx39](https://github.com/IMAMx39)

###  Health Score

22

—

LowBetter than 22% of packages

Maintenance59

Moderate activity, may be stable

Popularity2

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity18

Early-stage or recently created project

 Bus Factor1

Top contributor holds 98.5% 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.

### Community

Maintainers

![](https://www.gravatar.com/avatar/6c643bea9bea67802e91311a030e79d99bb39cb9e75567a4aca085695922987a?d=identicon)[IMAMx39](/maintainers/IMAMx39)

---

Top Contributors

[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (65 commits)")[![IMAMx39](https://avatars.githubusercontent.com/u/15068036?v=4)](https://github.com/IMAMx39 "IMAMx39 (1 commits)")

### Embed Badge

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

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

###  Alternatives

[laravel/socialite

Laravel wrapper around OAuth 1 &amp; OAuth 2 libraries.

5.7k104.3M822](/packages/laravel-socialite)[laravel/dusk

Laravel Dusk provides simple end-to-end testing and browser automation.

1.9k38.6M289](/packages/laravel-dusk)[pinguo/php-msf

Pinguo Micro Service Framework For PHP

1.7k4.2k](/packages/pinguo-php-msf)[nineinchnick/edatatables

Grid widget for the Yii Framework, wrapper for the DataTables jQuery plugin

173.2k](/packages/nineinchnick-edatatables)

PHPackages © 2026

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