PHPackages                             andydefer/laravel-otp - 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. andydefer/laravel-otp

ActiveLibrary

andydefer/laravel-otp
=====================

Laravel package providing OTP (One-Time Password) management with Repository pattern, DTOs, Enums, and Value Objects.

v1.1.32(1w ago)0211↑31.3%1MITPHPPHP ^8.2

Since Jun 19Pushed 1w agoCompare

[ Source](https://github.com/andydefer/laravel-otp)[ Packagist](https://packagist.org/packages/andydefer/laravel-otp)[ RSS](/packages/andydefer-laravel-otp/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (33)Versions (46)Used By (1)

Laravel OTP
===========

[](#laravel-otp)

> Système de mots de passe à usage unique (OTP) polymorphique pour applications Laravel

[![Latest Version](https://camo.githubusercontent.com/ab159ec599c18133453e43ec82a4f021d30aca70f4d7af748f45f77e5cba93e4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f616e647964656665722f6c61726176656c2d6f74702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/andydefer/laravel-otp)[![Total Downloads](https://camo.githubusercontent.com/e05c4a755d05a8f53d06edfd9eaefc2ab70a45821e61dfdcbccdb389a763bc3c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f616e647964656665722f6c61726176656c2d6f74702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/andydefer/laravel-otp)[![PHP Version](https://camo.githubusercontent.com/749c9913f4d30401e03e639ac5ab24b51bd8d643a767aef1bd29fda7b7dc14ae/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f616e647964656665722f6c61726176656c2d6f74702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/andydefer/laravel-otp)[![License](https://camo.githubusercontent.com/c5e7d4dfd19a1daf4742cb7f8c55e9855463864263b8bf2092c864c54a48cb39/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f616e647964656665722f6c61726176656c2d6f74702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/andydefer/laravel-otp)

Un package Laravel complet pour gérer des mots de passe à usage unique (OTP) polymorphiques avec le pattern Repository, des DTOs, des Value Objects, le support TOTP (RFC 6238), des codes de récupération et une gestion des tentatives et du rate limiting.

---

📋 Table des matières
--------------------

[](#-table-des-matières)

- [Fonctionnalités](#fonctionnalit%C3%A9s)
- [Prérequis](#pr%C3%A9requis)
- [Installation](#installation)
- [Configuration](#configuration)
- [Structure du package](#structure-du-package)
- [Utilisation](#utilisation)
    - [Créer un OTP avec un type prédéfini](#cr%C3%A9er-un-otp-avec-un-type-pr%C3%A9d%C3%A9fini)
    - [Créer un OTP avec un type personnalisé](#cr%C3%A9er-un-otp-avec-un-type-personnalis%C3%A9)
    - [Créer un OTP avec un code personnalisé](#cr%C3%A9er-un-otp-avec-un-code-personnalis%C3%A9)
    - [Créer un OTP avec un TTL personnalisé](#cr%C3%A9er-un-otp-avec-un-ttl-personnalis%C3%A9)
    - [Vérifier un OTP](#v%C3%A9rifier-un-otp)
    - [Vérifier un OTP sans le marquer comme utilisé](#v%C3%A9rifier-un-otp-sans-le-marquer-comme-utilis%C3%A9)
    - [Invalider un OTP](#invalider-un-otp)
    - [Récupérer tous les OTPs](#r%C3%A9cup%C3%A9rer-tous-les-otps)
    - [Récupérer les OTPs valides](#r%C3%A9cup%C3%A9rer-les-otps-valides)
    - [Compter les OTPs](#compter-les-otps)
    - [Supprimer les OTPs expirés](#supprimer-les-otps-expir%C3%A9s)
    - [Gestion des tentatives](#gestion-des-tentatives)
    - [TOTP (Time-based OTP)](#totp-time-based-otp)
    - [Codes de récupération](#codes-de-r%C3%A9cup%C3%A9ration)
    - [Rate Limiting](#rate-limiting)
- [Référence de l'API](#r%C3%A9f%C3%A9rence-de-lapi)
    - [OtpService](#otpservice)
    - [OtpRepository](#otprepository)
    - [OtpGenerator](#otpgenerator)
    - [PurposeVO](#purposevo)
- [Value Objects](#value-objects)
- [Structure de la base de données](#structure-de-la-base-de-donn%C3%A9es)
- [Tests](#tests)
- [Journal des modifications](#journal-des-modifications)
- [Contribuer](#contribuer)
- [Licence](#licence)

---

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

[](#-fonctionnalités)

- ✅ **Polymorphisme** - OTPs attachables à n'importe quel modèle (User, Admin, etc.)
- ✅ **Types d'OTP flexibles** - Utilisez les types prédéfinis ou créez vos propres types via `PurposeVO`
- ✅ **6 types prédéfinis** - Authentification, Réinitialisation, Vérification email/téléphone, 2FA, Transaction
- ✅ **TOTP Support** - RFC 6238 compatible avec génération de secrets Base32
- ✅ **Codes de récupération** - Génération de codes de secours (configurables)
- ✅ **Gestion des tentatives** - Limitez les tentatives par OTP (configurable par type)
- ✅ **Expiration automatique** - TTL configurable par type d'OTP
- ✅ **Rate Limiting** - Limitez le nombre d'OTP par période (15 min par défaut)
- ✅ **DTO Pattern** - OtpRecord et OtpFilterRecord pour un typage fort
- ✅ **Value Objects** - PurposeVO pour une gestion flexible des types
- ✅ **Repository Pattern** - Séparation propre de la logique d'accès aux données
- ✅ **Multi-bases de données** - Support SQLite, MySQL, PostgreSQL pour les filtres JSON
- ✅ **Support des métadonnées** - Stockez des données supplémentaires au format JSON
- ✅ **Suppression douce** - SoftDeletes pour une suppression sécurisée
- ✅ **Tests complets** - Couverture complète des tests d'intégration

---

🚀 Prérequis
-----------

[](#-prérequis)

- PHP 8.2 ou supérieur
- Laravel 12.0, 13.0, 14.0 ou 15.0

---

📦 Installation
--------------

[](#-installation)

Installez le package via Composer :

```
composer require andydefer/laravel-otp
```

### Publier les migrations

[](#publier-les-migrations)

```
php artisan vendor:publish --tag=Otp-migrations
```

### Exécuter les migrations

[](#exécuter-les-migrations)

```
php artisan migrate
```

---

⚙️ Configuration
----------------

[](#️-configuration)

Le package est automatiquement découvert par Laravel. Aucune configuration supplémentaire n'est requise.

Si vous devez personnaliser le Service Provider, ajoutez-le manuellement dans `config/app.php` :

```
'providers' => [
    // ...
    AndyDefer\LaravelOtp\OtpServiceProvider::class,
],
```

---

🏗️ Structure du package
-----------------------

[](#️-structure-du-package)

```
laravel-otp/
├── src/
│   ├── OtpServiceProvider.php
│   ├── Models/
│   │   └── Otp.php
│   ├── Records/
│   │   ├── OtpRecord.php
│   │   └── OtpFilterRecord.php
│   ├── Repositories/
│   │   └── OtpRepository.php
│   ├── Services/
│   │   ├── OtpService.php
│   │   └── OtpGenerator.php
│   └── ValueObjects/
│       └── PurposeVO.php
├── database/
│   └── migrations/
│       └── create_otps_table.php
└── tests/
    ├── Integration/
    │   └── Services/
    │       └── OtpServiceIntegrationTest.php
    ├── Fixtures/
    └── IntegrationTestCase.php

```

---

📖 Utilisation
-------------

[](#-utilisation)

### Créer un OTP avec un type prédéfini

[](#créer-un-otp-avec-un-type-prédéfini)

Le package fournit des constantes prédéfinies via la classe `PurposeVO` :

```
use AndyDefer\LaravelOtp\Services\OtpService;
use AndyDefer\LaravelOtp\ValueObjects\PurposeVO;

class AuthController extends Controller
{
    public function sendOtp(OtpService $otpService)
    {
        $user = auth()->user();

        // Créer un PurposeVO pour l'authentification
        $purpose = new PurposeVO(
            value: 'authentication',
            label: 'Authentification',
            ttl: 300,
            maxAttempts: 3
        );

        $otp = $otpService->create(
            identifier: $user,
            purpose: $purpose
        );

        // Envoyer le code par SMS ou email
        Notification::send($user, new OtpNotification($otp->code));

        return response()->json([
            'message' => 'OTP envoyé',
            'expires_in' => $otp->expires_at->diffInSeconds(now())
        ]);
    }
}
```

### Créer un OTP avec un type personnalisé

[](#créer-un-otp-avec-un-type-personnalisé)

Vous pouvez créer vos propres types d'OTP :

```
$customPurpose = new PurposeVO(
    value: 'api_access',
    label: 'Accès API',
    ttl: 180,        // 3 minutes
    maxAttempts: 3
);

$otp = $otpService->create($user, $customPurpose);
```

### Créer un OTP avec un code personnalisé

[](#créer-un-otp-avec-un-code-personnalisé)

```
$purpose = new PurposeVO('password_reset', 'Réinitialisation', 900, 5);

$otp = $otpService->create(
    identifier: $user,
    purpose: $purpose,
    code: 'SECURE-CODE-123' // Code personnalisé
);
```

### Créer un OTP avec un TTL personnalisé

[](#créer-un-otp-avec-un-ttl-personnalisé)

```
$purpose = new PurposeVO('two_factor', 'Double authentification', 120, 3);

// Le TTL du purpose est utilisé par défaut
$otp = $otpService->create($user, $purpose);

// Ou surcharge avec un TTL personnalisé
$otp = $otpService->create(
    identifier: $user,
    purpose: $purpose,
    code: null,
    ttl: 60 // 1 minute
);
```

### Vérifier un OTP

[](#vérifier-un-otp)

```
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

$valid = $otpService->verify(
    identifier: $user,
    code: $request->input('code'),
    purpose: $purpose
);

if ($valid) {
    // OTP valide, connecter l'utilisateur
    return response()->json(['message' => 'Authentification réussie']);
}

return response()->json(['message' => 'Code invalide ou expiré'], 400);
```

### Vérifier un OTP sans le marquer comme utilisé

[](#vérifier-un-otp-sans-le-marquer-comme-utilisé)

```
// Permet de vérifier un OTP sans le consommer
// Utile pour les pré-vérifications
$valid = $otpService->verify(
    identifier: $user,
    code: $code,
    purpose: $purpose,
    markAsUsed: false
);
```

### Invalider un OTP

[](#invalider-un-otp)

```
// Invalide tous les OTPs d'un type pour un identifiant
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);
$otpService->invalidate($user, $purpose);
```

### Récupérer tous les OTPs

[](#récupérer-tous-les-otps)

```
// Récupère tous les OTPs (valides et invalides) pour un identifiant et un type
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);
$otps = $otpService->getAllFor($user, $purpose);
```

### Récupérer les OTPs valides

[](#récupérer-les-otps-valides)

```
// Récupère uniquement les OTPs valides (non expirés et non utilisés)
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);
$validOtps = $otpService->getValidFor($user, $purpose);
```

### Compter les OTPs

[](#compter-les-otps)

```
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

// Compter tous les OTPs
$count = $otpService->countFor($user, $purpose);

// Compter les OTPs valides
$validCount = $otpService->countValidFor($user, $purpose);

// Compter les OTPs expirés
$expiredCount = $otpService->countExpiredFor($user, $purpose);
```

### Supprimer les OTPs expirés

[](#supprimer-les-otps-expirés)

```
// Supprime tous les OTPs expirés de la base de données
$deletedCount = $otpService->deleteExpired();
```

### Gestion des tentatives

[](#gestion-des-tentatives)

```
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

// Incrémenter les tentatives pour un OTP
$otpService->incrementAttempts($user, $code, $purpose);

// Récupérer le nombre de tentatives
$attempts = $otpService->getAttempts($user, $code, $purpose);
```

### TOTP (Time-based OTP)

[](#totp-time-based-otp)

```
// 1. Générer un secret pour l'utilisateur
$secret = $otpService->generateSecret();

// 2. Stocker le secret chez l'utilisateur
$user->otp_secret = $secret;
$user->save();

// 3. Créer un OTP basé sur TOTP
$purpose = new PurposeVO('two_factor', 'Double authentification', 120, 3);

$otp = $otpService->createWithTOTP(
    identifier: $user,
    purpose: $purpose,
    secret: $secret
);

// Le code est généré automatiquement selon RFC 6238
// Code valide pendant 30 secondes (intervalle TOTP standard)
```

### Codes de récupération

[](#codes-de-récupération)

```
// Générer 10 codes de récupération de 8 caractères
$recoveryCodes = $otpService->generateRecoveryCodes(
    identifier: $user,
    count: 10,
    length: 8
);

// Stocker les codes hashés chez l'utilisateur
$user->recovery_codes = array_map('hash', $recoveryCodes);
$user->save();

// Afficher les codes à l'utilisateur
return response()->json(['recovery_codes' => $recoveryCodes]);
```

### Rate Limiting

[](#rate-limiting)

```
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

// Vérifier si l'utilisateur a dépassé la limite
$isRateLimited = $otpService->isRateLimited(
    identifier: $user,
    purpose: $purpose,
    limit: 5,    // 5 tentatives
    window: now()->subMinutes(15)  // sur 15 minutes
);

if ($isRateLimited) {
    return response()->json([
        'message' => 'Trop de tentatives. Veuillez réessayer plus tard.'
    ], 429);
}
```

---

📚 Référence de l'API
--------------------

[](#-référence-de-lapi)

### OtpService

[](#otpservice)

MéthodeDescriptionRetourne`create(Model $identifier, PurposeVO $purpose, ?string $code = null, ?int $ttl = null)`Créer un OTP`Model``createWithTOTP(Model $identifier, PurposeVO $purpose, string $secret, ?int $ttl = null)`Créer un OTP TOTP`Model``verify(Model $identifier, string $code, PurposeVO $purpose, bool $markAsUsed = true)`Vérifier un OTP`bool``findValid(Model $identifier, string $code, PurposeVO $purpose)`Trouver un OTP valide`?Model``invalidate(Model $identifier, PurposeVO $purpose)`Invalider tous les OTPs d'un type`void``getAllFor(Model $identifier, PurposeVO $purpose)`Récupérer tous les OTPs`Collection``getValidFor(Model $identifier, PurposeVO $purpose)`Récupérer les OTPs valides`Collection``deleteExpired()`Supprimer les OTPs expirés`int``getAttempts(Model $identifier, string $code, PurposeVO $purpose)`Récupérer les tentatives`int``incrementAttempts(Model $identifier, string $code, PurposeVO $purpose)`Incrémenter les tentatives`void``countFor(Model $identifier, PurposeVO $purpose)`Compter les OTPs`int``countValidFor(Model $identifier, PurposeVO $purpose)`Compter les OTPs valides`int``countExpiredFor(Model $identifier, PurposeVO $purpose)`Compter les OTPs expirés`int``generateRecoveryCodes(Model $identifier, int $count = 10, int $length = 8)`Générer des codes de récupération`array``generateSecret()`Générer un secret TOTP`string``isRateLimited(Model $identifier, PurposeVO $purpose, int $limit = 5, ?CarbonInterface $window = null)`Vérifier le rate limiting`bool`### OtpRepository

[](#otprepository)

MéthodeDescriptionRetourne`incrementAttempts(int $id)`Incrémenter les tentatives d'un OTP`Otp``markAsUsed(int $id)`Marquer un OTP comme utilisé`Otp``delete(int $id)`Supprimer un OTP (hérité)`bool``find(int $id)`Trouver un OTP par ID (hérité)`?Otp``count(AbstractRecord $criteria)`Compter les OTPs (hérité)`int`### OtpGenerator

[](#otpgenerator)

MéthodeDescriptionRetourne`generate(int $length = 6, bool $numeric = true)`Générer un code OTP`string``generateTOTP(string $secret, int $digits = 6)`Générer un code TOTP`string``generateSecret(int $length = 32)`Générer un secret Base32`string``generateRecoveryCodes(int $count = 10, int $length = 8)`Générer des codes de récupération`array`### PurposeVO

[](#purposevo)

MéthodeDescriptionRetourne`__construct(string $value, ?string $label, ?int $ttl, ?int $maxAttempts)`Créer un PurposeVO-`getValue()`Récupérer les données`StrictDataObject``getLabel()`Récupérer le label`?string``getTtl()`Récupérer le TTL`?int``getMaxAttempts()`Récupérer les tentatives max`?int``__toString()`Convertir en JSON`string`---

🎯 Value Objects
---------------

[](#-value-objects)

Le package supporte les Value Objects suivants :

Value ObjectDescriptionExemple`PurposeVO`Type d'OTP avec label, TTL et maxAttempts`new PurposeVO('auth', 'Auth', 300, 3)``DateTimeVO`Date/heure`new DateTimeVO('2024-01-01 12:00:00')``StrictDataObject`Métadonnées typées`StrictDataObject::from(['ip' => '127.0.0.1'])`### Accesseurs dans le modèle Otp

[](#accesseurs-dans-le-modèle-otp)

```
$otp = Otp::find(1);

// Accès sous forme de Value Objects
$purpose = $otp->getPurpose();        // PurposeVO
$createdAt = $otp->getCreatedAt();    // DateTimeVO
$updatedAt = $otp->getUpdatedAt();    // DateTimeVO
$expiresAt = $otp->getExpiresAt();    // DateTimeVO
$usedAt = $otp->getUsedAt();          // DateTimeVO
$deletedAt = $otp->getDeletedAt();    // DateTimeVO
$metadata = $otp->getMetadata();      // StrictDataObject

// Méthodes utilitaires
$otp->isValid();      // bool
$otp->isUsed();       // bool
$otp->isExpired();    // bool

// Relations
$identifier = $otp->identifier;  // User, Admin, etc.
```

---

📝 Structure de la base de données
---------------------------------

[](#-structure-de-la-base-de-données)

```
CREATE TABLE otps (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    identifier_type VARCHAR(255) NOT NULL,    -- Type de l'identifiant (morph)
    identifier_id BIGINT UNSIGNED NOT NULL,   -- ID de l'identifiant
    code VARCHAR(255) NOT NULL,               -- Code OTP
    purpose JSON NOT NULL,                    -- Type d'OTP (JSON)
    expires_at TIMESTAMP NOT NULL,            -- Date d'expiration
    used_at TIMESTAMP NULL,                   -- Date d'utilisation
    attempts INT DEFAULT 0,                   -- Nombre de tentatives
    metadata JSON NULL,                       -- Métadonnées
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    deleted_at TIMESTAMP NULL,

    INDEX idx_identifier (identifier_type, identifier_id),
    INDEX idx_code (code),
    INDEX idx_expires_at (expires_at),
    INDEX idx_used_at (used_at)
);
```

### Structure du champ `purpose` (JSON)

[](#structure-du-champ-purpose-json)

```
{
    "value": "authentication",
    "label": "Authentification",
    "ttl": 300,
    "maxAttempts": 3
}
```

---

🔍 Exemple complet
-----------------

[](#-exemple-complet)

```
use AndyDefer\LaravelOtp\Services\OtpService;
use AndyDefer\LaravelOtp\ValueObjects\PurposeVO;

class AuthController extends Controller
{
    public function __construct(
        private readonly OtpService $otpService
    ) {}

    public function requestOtp(Request $request)
    {
        $user = User::where('email', $request->email)->firstOrFail();

        $purpose = new PurposeVO(
            value: 'authentication',
            label: 'Authentification',
            ttl: 300,
            maxAttempts: 3
        );

        // Rate limiting
        if ($this->otpService->isRateLimited($user, $purpose)) {
            return response()->json([
                'message' => 'Too many attempts. Please try again later.'
            ], 429);
        }

        $otp = $this->otpService->create($user, $purpose);

        // Envoyer par SMS ou email
        Notification::send($user, new OtpNotification($otp->code));

        return response()->json([
            'message' => 'OTP sent',
            'expires_in' => $otp->expires_at->diffInSeconds(now())
        ]);
    }

    public function verifyOtp(Request $request)
    {
        $user = User::where('email', $request->email)->firstOrFail();

        $purpose = new PurposeVO(
            value: 'authentication',
            label: 'Authentification',
            ttl: 300,
            maxAttempts: 3
        );

        $valid = $this->otpService->verify(
            identifier: $user,
            code: $request->code,
            purpose: $purpose
        );

        if (!$valid) {
            $this->otpService->incrementAttempts(
                $user,
                $request->code,
                $purpose
            );

            return response()->json([
                'message' => 'Invalid or expired OTP'
            ], 400);
        }

        auth()->login($user);

        return response()->json([
            'message' => 'Authenticated successfully',
            'user' => $user
        ]);
    }

    public function enableTwoFactor(Request $request)
    {
        $user = $request->user();

        // Générer un secret TOTP
        $secret = $this->otpService->generateSecret();
        $user->otp_secret = $secret;
        $user->save();

        // Générer des codes de récupération
        $recoveryCodes = $this->otpService->generateRecoveryCodes($user, 10, 8);

        return response()->json([
            'secret' => $secret,
            'recovery_codes' => $recoveryCodes,
            'qr_code_url' => $this->generateQrCode($user->email, $secret)
        ]);
    }

    public function verifyTwoFactor(Request $request)
    {
        $user = $request->user();

        $purpose = new PurposeVO(
            value: 'two_factor',
            label: 'Double authentification',
            ttl: 120,
            maxAttempts: 3
        );

        $otp = $this->otpService->createWithTOTP(
            identifier: $user,
            purpose: $purpose,
            secret: $user->otp_secret
        );

        $valid = $this->otpService->verify(
            identifier: $user,
            code: $request->code,
            purpose: $purpose
        );

        if (!$valid) {
            return response()->json(['message' => 'Invalid 2FA code'], 400);
        }

        $user->two_factor_enabled = true;
        $user->save();

        return response()->json(['message' => '2FA enabled successfully']);
    }

    public function customPurpose()
    {
        $user = User::find(1);

        // Créer un type d'OTP personnalisé
        $customPurpose = new PurposeVO(
            value: 'api_access',
            label: 'Accès API',
            ttl: 180,
            maxAttempts: 3
        );

        $otp = $this->otpService->create($user, $customPurpose);

        return $otp;
    }
}
```

---

🧪 Tests
-------

[](#-tests)

### Exécuter les tests

[](#exécuter-les-tests)

```
composer test
```

### Exécuter uniquement les tests unitaires

[](#exécuter-uniquement-les-tests-unitaires)

```
composer test-unit
```

### Exécuter uniquement les tests d'intégration

[](#exécuter-uniquement-les-tests-dintégration)

```
composer test-integration
```

### Configuration des tests

[](#configuration-des-tests)

Le package utilise `orchestra/testbench` pour les tests d'intégration avec une base de données SQLite en mémoire.

---

🔧 Développement
---------------

[](#-développement)

### Style de code

[](#style-de-code)

```
./vendor/bin/pint
```

### Analyse statique

[](#analyse-statique)

```
./vendor/bin/phpstan analyse
./vendor/bin/psalm
```

---

📄 Journal des modifications
---------------------------

[](#-journal-des-modifications)

Veuillez consulter le [CHANGELOG](CHANGELOG.md) pour plus d'informations sur les modifications récentes.

---

🤝 Contribuer
------------

[](#-contribuer)

Veuillez consulter [CONTRIBUTING](CONTRIBUTING.md) pour plus de détails.

### Flux de développement

[](#flux-de-développement)

1. Forkez le dépôt
2. Créez une branche de fonctionnalité (`git checkout -b feature/amazing-feature`)
3. Apportez vos modifications
4. Exécutez les tests (`composer test`)
5. Committez vos modifications (`git commit -m 'Ajouter une fonctionnalité géniale'`)
6. Poussez vers la branche (`git push origin feature/amazing-feature`)
7. Ouvrez une Pull Request

---

📦 Dépendances
-------------

[](#-dépendances)

- [`andydefer/php-vo`](https://github.com/andydefer/php-vo) - Value Objects (DateTimeVO)
- [`andydefer/laravel-repository`](https://github.com/andydefer/laravel-repository) - Implémentation du pattern Repository
- [`andydefer/domain-structures`](https://github.com/andydefer/domain-structures) - Structures de domaine (AbstractRecord, AbstractData, AbstractValueObject)

---

👨‍💻 Auteur
----------

[](#‍-auteur)

**Andy Kani**

- GitHub: [@andydefer](https://github.com/andydefer)
- Email:

---

⭐ Support
---------

[](#-support)

Si vous trouvez ce package utile, n'hésitez pas à lui donner une ⭐ sur GitHub !

---

🙏 Remerciements
---------------

[](#-remerciements)

- Framework Laravel
- Tous les contributeurs et utilisateurs de ce package

---

**Construit avec ❤️ pour la communauté Laravel**

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance98

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity59

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

Total

45

Last Release

13d ago

Major Versions

v0.0.1 → v1.0.42026-07-07

### Community

Maintainers

![](https://www.gravatar.com/avatar/2170ec3fbad9eb4b002661ab4f58b1cc374eae4293b92904c6a74bc2818bd570?d=identicon)[andydefer](/maintainers/andydefer)

---

Top Contributors

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

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/andydefer-laravel-otp/health.svg)

```
[![Health](https://phpackages.com/badges/andydefer-laravel-otp/health.svg)](https://phpackages.com/packages/andydefer-laravel-otp)
```

###  Alternatives

[backpack/crud

Quickly build admin interfaces using Laravel, Bootstrap and JavaScript.

3.4k3.8M231](/packages/backpack-crud)[code16/sharp

Laravel Content Management Framework

79466.8k10](/packages/code16-sharp)[statamic-rad-pack/runway

Eloquently manage your database models in Statamic.

138249.0k8](/packages/statamic-rad-pack-runway)[duncanmcclean/statamic-cargo

Comprehensive e-commerce addon for Statamic. Build bespoke e-commerce sites without the complexity.

3622.8k](/packages/duncanmcclean-statamic-cargo)[ecotone/laravel

Ecotone for Laravel — CQRS, Event Sourcing, Sagas, Durable Workflows, and Outbox on top of Laravel Queue, via PHP attributes.

21336.4k4](/packages/ecotone-laravel)[api-platform/laravel

API Platform support for Laravel

58190.1k22](/packages/api-platform-laravel)

PHPackages © 2026

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