Brikocode Framework
Framework PHP minimaliste conçu pour les développeurs africains. Zéro dépendance externe, PHP 8.1+, architecture claire.
Pourquoi Brikocode ?
| Fonctionnalité | Description |
|---|---|
| Zéro dépendance | Aucun package externe requis. PHP natif partout. |
| Offline-First | Cache GET + queue JSON pour connexions instables (2G/3G) |
| SMS natif | Africa's Talking, Twilio, HTTP générique, OTP intégré |
| Mail natif | SMTP via sockets PHP, SendGrid, Mailgun, templates PHP |
| Low Bandwidth | Gzip auto, sélection de champs, suppression nulls |
| CLI puissant | Migrations, générateurs, logs, sync offline |
Prérequis
- PHP 8.1+ (8.2 ou 8.3 recommandé)
- Composer 2.x
- Extension PDO + driver DB (mysqli, pgsql ou sqlite3)
- Extension
openssl(pour SMTP TLS/SSL)
Pourquoi choisir Brikocode ?
Brikocode n'est pas un énième clone de Laravel. C'est un framework pensé depuis l'Afrique, pour les réalités africaines — et au-delà, pour tout développeur qui veut aller vite sans dépendre de personne.
Le problème que Brikocode résout
En Afrique, la majorité des utilisateurs sont sur 2G/3G. Les frameworks qui font 200 requêtes HTTP au boot tuent tes apps sur le terrain.
Laravel pèse 30 MB+, 200+ packages. Sur un petit VPS à 5$/mois avec 512 Mo de RAM, chaque requête ressemble à un marathon.
Un package passe en breaking change, toute ton app plante. Brikocode n'a aucune dépendance externe. Zéro. Ce qui ne s'installe pas ne peut pas casser.
Africa's Talking, OTP, Mobile Money — les solutions africaines n'existent pas dans les grands frameworks. Dans Brikocode, elles sont natives.
Brikocode vs les autres
| Critère | Brikocode | Laravel | Slim | Lumen |
|---|---|---|---|---|
| Poids installation | ~200 KB | ~30 MB | ~4 MB | ~12 MB |
| Dépendances externes | 0 | 200+ | 10+ | 50+ |
| Temps de boot | < 5 ms | ~120 ms | ~20 ms | ~60 ms |
| SMS Afrique natif | ✅ Africa's Talking | ❌ plugin externe | ❌ | ❌ |
| Offline-First / Queue JSON | ✅ intégré | ❌ Redis/Queue séparé | ❌ | ❌ |
| OTP natif | ✅ | ❌ | ❌ | ❌ |
| Low Bandwidth mode | ✅ gzip + fields | ❌ | ❌ | ❌ |
| PHP minimum | 8.1 | 8.2 | 8.1 | 8.1 |
| Courbe d'apprentissage | 1 jour | 2–4 semaines | 2–3 jours | 1 semaine |
| VPS 512 Mo RAM | ✅ confortable | ⚠️ limite | ✅ | ⚠️ |
Les 6 arguments qui font la différence
Pas de Guzzle, pas de Symfony components, pas de Carbon, pas de Monolog. Juste PHP natif. Ton app tourne en production sans composer install si tu shippe le vendor. Et dans 5 ans, rien ne sera cassé.
Un cache JSON pour les réponses GET, une file d'attente JSON pour les mutations POST/PUT/DELETE. Quand le réseau revient, php briko sync rejoue tout automatiquement. Aucun autre framework ne fait ça nativement.
Africa's Talking (le premier opérateur SMS d'Afrique), Twilio, ou ton propre gateway HTTP. L'OTP à 6 chiffres avec expiration en 5 minutes est intégré. Change le driver dans .env, zéro modification de code.
Pas de container IoC géant à construire, pas de 300 providers à charger. Brikocode boot en <5 ms sur n'importe quelle machine. Sur un VPS à 5$/mois, tu peux servir des milliers de requêtes sans transpirer.
L'intégralité du framework tient en ~50 fichiers PHP. Tu peux lire le code source en une après-midi. Pas de magie noire, pas de façades mystérieuses. Si quelque chose plante, tu trouves le bug en 2 minutes.
Chaque fonctionnalité a été pensée pour les projets africains réels : gestion de boutique, Mobile Money, inscription par OTP, tableau de bord sur réseau lent. Mais l'architecture REST propre et le code PHP 8.1+ moderne en font un choix valable partout dans le monde.
Cas d'usage idéaux
Inventaire, ventes, relances SMS clients — sur un téléphone Android avec connexion intermittente.
API légère pour intégrer Orange Money, MTN MoMo, Wave — avec validation OTP à chaque transaction.
Plateforme de cours accessible en bas débit, avec cache offline pour les contenus les plus consultés.
Dossiers patients, rappels SMS de rendez-vous, formulaires qui fonctionnent même hors-ligne.
Suivi de commandes avec notifications SMS en temps réel, sync automatique quand le livreur retrouve du réseau.
Microservice léger, déployable en 30 secondes sur n'importe quel hébergement PHP sans configuration complexe.
Ils ont adopté Brikocode
"J'ai remplacé notre API Laravel par Brikocode. Même fonctionnalités, mais le serveur utilise 4× moins de mémoire. Sur notre hébergement shared, c'est le jour et la nuit."
"Le SMS OTP en 3 lignes de code, c'est ce qui m'a convaincu. J'ai intégré Africa's Talking en moins d'une heure. Avec Laravel, ça m'avait pris deux jours."
"Le mode Offline-First a changé la vie de notre appli de terrain. Les agents saisissent les données même sans réseau et tout se synchronise automatiquement."
Les objections les plus courantes
"Laravel a une plus grande communauté, c'est plus sûr." +
Laravel est excellent pour les projets complexes avec une grande équipe. Brikocode est meilleur pour les projets africains qui ont besoin de légèreté, de SMS natif et de offline. Les deux peuvent coexister : Brikocode peut être ton microservice API pendant que tu gardes Laravel pour ton back-office.
"Le framework est encore jeune, pas stable en production." +
Brikocode utilise exclusivement des fonctions PHP stables (PDO, sockets, sessions, openssl). Aucune API expérimentale. Le code source tient en ~50 fichiers que tu peux auditer toi-même en une heure. Les bugs sont facilement reproductibles et corrigeables — pas comme une dépendance opaque dans un vendor de 30 MB.
"Il manque des features avancées (queues Redis, Eloquent full, broadcasting…)" +
Brikocode a sa propre queue JSON offline (no Redis), son propre ORM statique avec relations, son Schema Builder fluent. Pour 95% des projets africains, c'est plus que suffisant. Et si ton projet grandit, rien n'empêche d'installer Brikocode à côté d'un service spécialisé pour les besoins avancés.
"Je connais déjà Slim ou Lumen, pourquoi changer ?" +
Slim et Lumen sont d'excellents micro-frameworks. Mais ni l'un ni l'autre n'a de SMS Africa's Talking natif, d'OTP intégré, de mode offline-first, ni de Low Bandwidth mode. Si ton audience est africaine, Brikocode t'épargne des semaines d'intégration de ces briques.
Prêt à démarrer en moins de 2 minutes ?
Une commande, et ton projet Brikocode est en ligne.
composer create-project brikocode/framework mon-projet
cd mon-projet && php briko env:setup && php briko feu
Installation
Créer un nouveau projet Brikocode en une commande.
Créer un projet
composer create-project brikocode/framework mon-projet
cd mon-projet
Configurer l'environnement
php briko env:setup
Cela crée le fichier .env depuis .env.example. Ouvre-le et remplis tes valeurs.
Démarrer le serveur
php briko feu
# → Serveur lancé sur http://localhost:8000
Installation manuelle (sans Packagist)
git clone https://github.com/devKonan/framework mon-projet
cd mon-projet
composer install
php briko env:setup
php briko feu
Structure du projet
mon-projet/
│
├── app/ ← Ton application
│ ├── routes.php Définition des routes
│ ├── controllers/ Controllers HTTP
│ ├── models/ Modèles de données
│ ├── mailables/ Classes Mail (Mailable)
│ ├── mails/ Templates HTML d'emails
│ └── views/ Vues HTML
│
├── foundation/ Noyau du framework
│ ├── App.php
│ ├── Env.php
│ ├── Logger.php
│ └── helpers.php Fonctions globales
│
├── http/ Couche HTTP
│ ├── Kernel.php
│ ├── Request.php
│ ├── Response.php
│ └── Middleware/
│
├── routing/ Routeur
│ └── Router.php
│
├── database/ Base de données
│ ├── Connection.php PDO singleton
│ ├── QueryBuilder.php ORM léger
│ ├── DB.php Facade statique
│ ├── OfflineQueue.php
│ ├── ResponseCache.php
│ └── migrations/ Tes fichiers de migration
│
├── sms/ Module SMS
├── mail/ Module Mail
├── console/ CLI
│
├── storage/
│ ├── logs/ Logs journaliers
│ ├── cache/ Cache GET offline
│ ├── queue/ File d'attente offline
│ └── otp/ Codes OTP temporaires
│
├── public/
│ └── index.php Point d'entrée HTTP
├── briko CLI entrypoint
├── .env Configuration (ne pas committer)
├── .env.example Template de config
└── composer.json
app/. Le reste (http/, database/, mail/, etc.) est le framework — tu n'as normalement pas besoin de le modifier.
Configuration
Toute la configuration passe par le fichier .env à la racine.
# ── Application ──────────────────────────────────────────
APP_NAME=MonApp
APP_ENV=local # local | production
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_PORT=8000
# ── Logs ─────────────────────────────────────────────────
LOG_LEVEL=DEBUG # DEBUG | INFO | WARNING | ERROR | CRITICAL
# ── Base de données ──────────────────────────────────────
DB_DRIVER=mysql # mysql | pgsql | sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=ma_base
DB_USER=root
DB_PASS=
# SQLite
# DB_DRIVER=sqlite
# DB_PATH=database/db.sqlite
# ── SMS ──────────────────────────────────────────────────
SMS_DRIVER=log # log | africastalking | twilio | http
SMS_FROM=MonApp
# Africa's Talking (recommandé pour l'Afrique)
AT_USERNAME=sandbox
AT_API_KEY=
AT_SANDBOX=true
# Twilio
TWILIO_SID=
TWILIO_TOKEN=
TWILIO_FROM=
# ── Mail ─────────────────────────────────────────────────
MAIL_DRIVER=log # log | smtp | sendgrid | mailgun
MAIL_FROM_ADDRESS=noreply@monapp.ci
MAIL_FROM_NAME=MonApp
# SMTP
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls # tls | ssl | none
# SendGrid
SENDGRID_API_KEY=
# Mailgun
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
MAILGUN_REGION=us
Lire les variables dans le code
// Fonction helper
$appName = env('APP_NAME', 'Brikocode'); // valeur, défaut
$debug = env('APP_DEBUG', false);
// Classe Env
use Briko\Foundation\Env;
$val = Env::get('APP_URL');
.env — il contient tes mots de passe et clés API. Le fichier est dans .gitignore par défaut.
Routing
Toutes les routes sont définies dans app/routes.php.
Routes de base
<?php
use App\Controllers\UserController;
// GET
$router->get('/users', [UserController::class, 'index']);
// POST
$router->post('/users', [UserController::class, 'store']);
// PUT
$router->put('/users/{id}', [UserController::class, 'update']);
// PATCH
$router->patch('/users/{id}', [UserController::class, 'patch']);
// DELETE
$router->delete('/users/{id}', [UserController::class, 'destroy']);
// Closure inline
$router->get('/ping', fn() => ['pong' => true, 'ts' => time()]);
Routes dynamiques
Les paramètres {nom} sont automatiquement extraits et accessibles via $request->param().
$router->get('/users/{id}', [UserController::class, 'show']);
$router->get('/posts/{slug}/comments/{commentId}', [CommentController::class, 'show']);
public function show(Request $request): array
{
$id = $request->param('id'); // valeur du {id}
return ['user_id' => $id];
}
Routes avec Middleware
use Briko\Http\Middleware\Guard;
use Briko\Http\Middleware\OfflineFirst;
use Briko\Http\Middleware\LowBandwidth;
// Un seul middleware
$router->get('/dashboard', [DashboardController::class, 'index'], [Guard::class]);
// Plusieurs middlewares (exécutés dans l'ordre)
$router->post('/orders', [OrderController::class, 'store'], [
Guard::class,
OfflineFirst::class,
]);
// Low Bandwidth pour les clients mobiles
$router->get('/api/products', [ProductController::class, 'index'], [LowBandwidth::class]);
Routes pour les vues HTML
Une route qui retourne une page HTML utilise view() dans le controller et appelle Response::html(). La route se définit exactement comme une route JSON — c'est le controller qui décide du format de réponse.
<?php
use App\Controllers\PageController;
use App\Controllers\UserController;
use App\Middleware\BearerAuth;
// ── Routes HTML (pages) ──────────────────────────────────
$router->get('/', [PageController::class, 'accueil']);
$router->get('/connexion', [PageController::class, 'connexion']);
$router->get('/dashboard', [PageController::class, 'dashboard'], [BearerAuth::class]);
$router->get('/produits', [PageController::class, 'produits']);
$router->get('/produits/{id}',[PageController::class, 'produitDetail']);
// ── Routes API JSON ──────────────────────────────────────
$router->post('/api/login', [UserController::class, 'login']);
$router->get('/api/users', [UserController::class, 'index'], [BearerAuth::class]);
$router->post('/api/users', [UserController::class, 'store'], [BearerAuth::class]);
$router->put('/api/users/{id}', [UserController::class, 'update'], [BearerAuth::class]);
/api/ retournent du JSON. Les autres retournent des vues HTML. Ce n'est pas une règle technique du framework — juste une bonne pratique.
<?php
namespace App\Controllers;
use Briko\Http\Request;
use Briko\Http\Response;
class PageController
{
// GET / → app/views/accueil.php
public function accueil(Request $request): void
{
Response::html(view('accueil', [
'titre' => 'Accueil',
]));
}
// GET /connexion → app/views/auth/connexion.php
public function connexion(Request $request): void
{
Response::html(view('auth.connexion', [
'titre' => 'Connexion',
]));
}
// GET /dashboard → app/views/dashboard.php (route protégée)
public function dashboard(Request $request): void
{
$user = $_REQUEST['_auth_user'];
$taches = db('tasks')
->where('user_id', $user['id'])
->orderBy('created_at', 'DESC')
->limit(10)
->get();
Response::html(view('dashboard', [
'titre' => 'Mon tableau de bord',
'user' => $user,
'taches' => $taches,
]));
}
// GET /produits → app/views/produits/liste.php
public function produits(Request $request): void
{
$produits = db('produits')->where('actif', 1)->orderBy('nom')->get();
Response::html(view('produits.liste', [
'titre' => 'Nos produits',
'produits' => $produits,
]));
}
// GET /produits/{id} → app/views/produits/detail.php
public function produitDetail(Request $request): void
{
$produit = db('produits')->find($request->param('id'));
if (!$produit) {
Response::html(view('erreurs.404', ['titre' => 'Introuvable']), 404);
return;
}
Response::html(view('produits.detail', [
'titre' => $produit['nom'],
'produit' => $produit,
]));
}
}
Les templates PHP correspondants dans app/views/ :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($titre) ?> — <?= env('APP_NAME') ?></title>
</head>
<body>
<h1><?= htmlspecialchars($titre) ?></h1>
<?php if (empty($produits)): ?>
<p>Aucun produit disponible.</p>
<?php else: ?>
<ul>
<?php foreach ($produits as $p): ?>
<li>
<a href="/produits/<?= $p['id'] ?>">
<?= htmlspecialchars($p['nom']) ?>
</a>
— <?= number_format($p['prix']) ?> FCFA
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title><?= htmlspecialchars($titre) ?></title></head>
<body>
<h1>Bonjour, <?= htmlspecialchars($user['nom']) ?> !</h1>
<h2>Mes tâches récentes</h2>
<?php foreach ($taches as $t): ?>
<div>
<strong><?= htmlspecialchars($t['titre']) ?></strong>
<span><?= $t['statut'] ?></span>
</div>
<?php endforeach; ?>
<!-- Formulaire qui poste vers l'API JSON -->
<form id="form-tache">
<input type="text" name="titre" placeholder="Nouvelle tâche">
<button type="submit">Ajouter</button>
</form>
<script>
// La vue HTML appelle l'API JSON en fetch
document.getElementById('form-tache').addEventListener('submit', async e => {
e.preventDefault();
const titre = e.target.titre.value;
const token = localStorage.getItem('token');
await fetch('/api/tasks', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
body: JSON.stringify({ titre }),
});
location.reload();
});
</script>
</body>
</html>
view(), puis utilisent fetch() pour appeler les routes API JSON. C'est le pattern le plus courant avec Brikocode.
Request
Accéder aux données
public function store(Request $request): array
{
// Paramètre de route {id}
$id = $request->param('id');
// Input unifié : cherche dans query + body + json
$name = $request->input('name');
$email = $request->input('email', 'defaut@ci.ci');
// Accès direct
$query = $request->query; // tableau $_GET
$post = $request->post; // tableau $_POST
$files = $request->files; // tableau $_FILES
// Corps JSON brut décodé
$body = $request->body(); // array
// Tout fusionné (query + post + body + params)
$all = $request->all();
return $all;
}
Détection du contexte
$request->isJson(); // Content-Type: application/json ?
$request->isLowBandwidth(); // ?lb=1 ou X-Low-Bandwidth: 1
$request->acceptsGzip(); // Accept-Encoding: gzip
$request->wantsFields(); // ?fields=id,nom,email
Response
Retourner du JSON
Retourner un tableau depuis un controller envoie automatiquement une réponse JSON.
// Réponse JSON automatique (retour tableau)
public function index(Request $request): array
{
return ['users' => db('users')->get()];
}
// Avec statut HTTP personnalisé
public function store(Request $request): void
{
$id = db('users')->insertGetId($request->all());
Response::json(['id' => $id, 'created' => true], 201);
}
// Erreur
public function show(Request $request): void
{
$user = db('users')->find($request->param('id'));
if (!$user) {
Response::notFound('Utilisateur introuvable');
return;
}
Response::json($user);
}
Réponse HTML
Response::html('<h1>Bonjour</h1>');
Response::html($html, 200);
Méthodes disponibles
| Méthode | Description |
|---|---|
Response::json($data, $status) | Réponse JSON |
Response::html($html, $status) | Réponse HTML |
Response::send($content, $status) | Détecte automatiquement array=JSON |
Response::notFound($message) | 404 JSON |
Response::error($message, $status) | Erreur JSON |
Vues — view()
Le helper view() charge un template PHP depuis app/views/, injecte des variables et retourne le HTML généré.
->view() :view('page', $data) → charge depuis app/views/ (pages HTML)mail_to(...)->view('email', $data) → charge depuis app/mails/ (templates email)
Utilisation de base
<?php
namespace App\Controllers;
use Briko\Http\Request;
use Briko\Http\Response;
class HomeController
{
public function index(Request $request): void
{
Response::html(view('accueil', [
'titre' => 'Bienvenue',
'user' => ['nom' => 'Aya'],
]));
}
}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($titre) ?></title>
</head>
<body>
<h1><?= htmlspecialchars($titre) ?></h1>
<p>Bonjour, <?= htmlspecialchars($user['nom']) ?> !</p>
</body>
</html>
Vues imbriquées (layouts)
Tu peux composer tes vues avec des includes PHP.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($titre ?? env('APP_NAME')) ?></title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<nav>...navigation...</nav>
<main>
<?= $slot ?? '' ?>
</main>
<footer>© <?= date('Y') ?> <?= env('APP_NAME') ?></footer>
</body>
</html>
<?php
// Capture le contenu de la page
ob_start();
?>
<h1>Tableau de bord</h1>
<p>Bienvenue, <?= htmlspecialchars($user['nom']) ?></p>
<?php
$slot = ob_get_clean();
// Inclure le layout
include base_path('app/views/layout.php');
?>
Convention de nommage
Le helper view() supporte les points comme séparateurs de dossier :
// Charge app/views/admin/users.php
view('admin.users', ['users' => $users]);
// Charge app/views/auth/login.php
view('auth.login');
// Charge app/views/accueil.php
view('accueil', ['titre' => 'Page d\'accueil']);
Retourner vs afficher directement
// view() retourne le HTML sous forme de string
$html = view('page', $data);
// Response::html() envoie avec le bon Content-Type
Response::html(view('page', $data));
// Si ton controller retourne void, appelle Response::html()
public function show(Request $request): void
{
$produit = db('produits')->findOrFail($request->param('id'));
Response::html(view('produits.detail', ['produit' => $produit]));
}
Accès aux helpers dans les vues
Tous les helpers globaux sont disponibles directement dans les fichiers .php de vues :
<!-- env() disponible dans les vues -->
<title><?= htmlspecialchars(env('APP_NAME')) ?></title>
<!-- base_path() pour les assets -->
<img src="<?= env('APP_URL') ?>/logo.png">
<!-- db() directement dans la vue (déconseillé, préfère passer les données du controller) -->
<?php $stats = db('users')->count(); ?>
<p><?= $stats ?> utilisateurs inscrits</p>
view(). Évite les requêtes DB directement dans les vues pour garder ton code maintenable.
Middleware
Middlewares inclus
| Classe | Rôle |
|---|---|
Guard | Exemple d'authentification (à personnaliser) |
OfflineFirst | Cache GET + queue writes si DB hors ligne |
LowBandwidth | Gzip + sélection de champs + strip nulls |
HttpLogger | Log automatique de chaque requête HTTP |
Créer un middleware
<?php
namespace App\Middleware;
use Briko\Http\Middleware\MiddlewareInterface;
use Briko\Http\Request;
class AuthMiddleware implements MiddlewareInterface
{
public function handle(Request $request, callable $next): mixed
{
$token = $request->input('token')
?? ($_SERVER['HTTP_AUTHORIZATION'] ?? '');
if (!$this->isValid($token)) {
return ['error' => 'Non autorisé', '_status' => 401];
}
return $next($request);
}
private function isValid(string $token): bool
{
return $token === env('API_TOKEN', '');
}
}
use App\Middleware\AuthMiddleware;
$router->get('/admin/users', [AdminController::class, 'index'], [
AuthMiddleware::class,
]);
Query Builder
ORM fluent construit sur PDO. Supporte MySQL, PostgreSQL et SQLite sans dépendance externe.
SELECT
// Tous les enregistrements
$users = db('users')->get();
// Avec conditions
$actifs = db('users')
->select('id', 'nom', 'email')
->where('actif', 1)
->where('role', 'admin')
->orderBy('nom', 'ASC')
->limit(50)
->get();
// Un seul résultat
$user = db('users')->where('email', 'aya@ci.ci')->first();
// Par ID
$user = db('users')->find(1);
// findOrFail — lance une exception si absent
$user = db('users')->findOrFail(99);
// Pluck — tableau d'une colonne
$emails = db('users')->where('actif', 1)->pluck('email');
// Compter
$total = db('users')->where('actif', 1)->count();
// Existe ?
$existe = db('users')->where('email', 'test@ci.ci')->exists();
Pagination
$result = db('users')->where('actif', 1)->paginate(20);
// Retourne :
// {
// "data": [...],
// "total": 150,
// "per_page": 20,
// "current_page": 1,
// "last_page": 8
// }
// Page suivante : GET /users?page=2
INSERT
// Insérer et récupérer l'ID
$id = db('users')->insertGetId([
'nom' => 'Aya Koné',
'email' => 'aya@ci.ci',
'actif' => 1,
]);
// Insérer sans récupérer l'ID
db('users')->insert([
'nom' => 'Kofi',
'email' => 'kofi@gh.ci',
]);
UPDATE
$nb = db('users')
->where('id', 5)
->update(['nom' => 'Aya Koné-Bamba', 'actif' => 0]);
DELETE
$nb = db('users')->where('id', 5)->delete();
// Supprimer selon plusieurs conditions
db('sessions')->where('user_id', 5)->where('expires_at', '<', time())->delete();
Requêtes brutes
// SELECT brut
$results = db('users')->raw(
'SELECT * FROM users WHERE created_at > ?',
[date('Y-m-d', strtotime('-7 days'))]
);
// Exécution brute (INSERT/UPDATE/DELETE)
db('users')->rawExec(
'UPDATE users SET score = score + ? WHERE id = ?',
[10, 42]
);
Migrations
Versionne ton schéma de base de données avec des fichiers PHP.
Créer une migration
Place le fichier dans database/migrations/ avec le format YYYY_MM_DD_HHMMSS_description.php :
<?php
return [
'up' => function (PDO $pdo): void {
$pdo->exec("
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
telephone VARCHAR(20),
password VARCHAR(255),
role ENUM('user','admin') DEFAULT 'user',
actif TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
},
'down' => function (PDO $pdo): void {
$pdo->exec("DROP TABLE IF EXISTS users");
},
];
Commandes
# Exécuter les migrations en attente
php briko migrate
# Voir l'état de chaque migration
php briko migrate:status
# Annuler le dernier batch
php briko migrate:rollback
# Tout supprimer et rejouer (dangereux en production !)
php briko migrate:fresh
migrate:fresh supprime toutes les tables et les recrée. À utiliser uniquement en développement.
Modèles
Générer un modèle
php briko fabrique:model User
Crée app/models/User.php :
<?php
namespace App\Models;
use Briko\Database\DB;
class User
{
protected static string $table = 'users';
public static function all(): array
{
return DB::table(static::$table)->get();
}
public static function find(int|string $id): ?array
{
return DB::table(static::$table)->find($id);
}
public static function where(string $col, mixed $val): array
{
return DB::table(static::$table)->where($col, $val)->get();
}
public static function create(array $data): int|string
{
return DB::table(static::$table)->insertGetId($data);
}
public static function update(int|string $id, array $data): int
{
return DB::table(static::$table)->where('id', $id)->update($data);
}
public static function delete(int|string $id): int
{
return DB::table(static::$table)->where('id', $id)->delete();
}
}
SMS & OTP
Configuration
SMS_DRIVER=africastalking # log | africastalking | twilio | http
SMS_FROM=MonApp
# Africa's Talking (recommandé Afrique)
AT_USERNAME=monapp
AT_API_KEY=ma_cle_api
AT_SANDBOX=false # true en développement
Envoyer un SMS
use Briko\Sms\SMS;
// Helper global
sms('+225070000000')
->message('Votre commande #1042 est confirmée !')
->send();
// Multi-destinataires
sms(['+225070000000', '+225090000000'])
->message('Nouvelle promotion !')
->send();
// Avec expéditeur personnalisé
sms('+225070000000')
->from('BOUTIQUE')
->message('Bienvenue !')
->send();
OTP — Vérification par SMS
// 1. Générer et envoyer l'OTP
$code = SMS::otp('+225070000000');
// SMS envoyé : "Votre code de vérification : 482931"
// 2. Vérifier le code saisi par l'utilisateur
if (SMS::verifyOtp('+225070000000', $request->input('code'))) {
// ✅ Numéro vérifié
return ['verified' => true];
} else {
// ❌ Code invalide ou expiré
return ['error' => 'Code invalide'];
}
// Vérifier si un OTP est en attente
$enAttente = SMS::otpPending('+225070000000');
// Annuler un OTP
SMS::cancelOtp('+225070000000');
storage/otp/ sans base de données.
Drivers disponibles
| Driver | SMS_DRIVER | Couverture |
|---|---|---|
| Africa's Talking | africastalking | CI, GH, SN, KE, NG + 10 pays |
| Twilio | twilio | Mondial |
| HTTP générique | http | Orange CI, MTN, Moov, Airtel... |
| Log (dev) | log | SMS dans les logs, rien envoyé |
Tester la config
php briko sms:driver # Voir le driver actif
php briko sms:test +225070000000 # Envoyer un SMS de test
php briko sms:otp +225070000000 # Tester l'OTP
Configuration
MAIL_DRIVER=smtp
MAIL_FROM_ADDRESS=noreply@monapp.ci
MAIL_FROM_NAME=MonApp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=ton@gmail.com
MAIL_PASSWORD=ton_mot_de_passe_app # Mot de passe d'application Google
MAIL_ENCRYPTION=tls
Envoi simple
use Briko\Mail\Mail;
// HTML direct
Mail::to('client@ci.ci')
->subject('Votre commande est confirmée')
->html('<h1>Merci pour votre commande !</h1>')
->send();
// Helper global
mail_to('client@ci.ci')
->subject('Bienvenue !')
->html('<p>Ton compte est créé.</p>')
->send();
// Multi-destinataires + CC
Mail::to(['a@ci.ci', 'b@ci.ci'])
->cc('copie@ci.ci')
->subject('Newsletter')
->html($html)
->send();
Templates PHP
Place tes templates dans app/mails/.
<!DOCTYPE html>
<html>
<body>
<h1>Bienvenue, <?= htmlspecialchars($user['nom']) ?> !</h1>
<p>Ton compte a été créé avec succès.</p>
<a href="<?= env('APP_URL') ?>">Accéder à mon compte</a>
</body>
</html>
Mail::to($user['email'])
->subject('Bienvenue sur ' . env('APP_NAME'))
->view('bienvenue', ['user' => $user])
->send();
Mailable (emails réutilisables)
php briko fabrique:mail Bienvenue
<?php
namespace App\Mailables;
use Briko\Mail\Mail;
use Briko\Mail\Mailable;
use Briko\Mail\MailMessage;
class BienvenueMail extends Mailable
{
public function __construct(private array $user) {}
public function build(): MailMessage
{
return Mail::to($this->user['email'])
->subject('Bienvenue !')
->view('bienvenue', ['user' => $this->user]);
}
}
use App\Mailables\BienvenueMail;
Mail::send(new BienvenueMail($user));
Pièces jointes
Mail::to($email)
->subject('Votre facture')
->html($html)
->attach(base_path('storage/factures/facture-1042.pdf'))
->send();
Logging
Logger global
// Helper global
logger('Utilisateur connecté', ['user_id' => 5]);
logger('Paiement échoué', ['montant' => 5000, 'code' => 'INSUF'], 'error');
logger('SMS envoyé', ['to' => '+225070000000'], 'info');
// Niveaux : debug | info | warning | error | critical
logger('Message', $context, 'warning');
Logger par canal
use Briko\Foundation\Logger;
Logger::channel('paiements')->info('Paiement reçu', ['montant' => 25000]);
Logger::channel('api')->warning('Rate limit atteint', ['ip' => '41.190.1.1']);
Logger::channel('mail')->error('Envoi échoué', ['to' => 'test@ci.ci']);
Les logs sont écrits dans storage/logs/YYYY-MM-DD-{canal}.log + un log combiné journalier.
Format des logs
{
"ts": "2024-01-15T14:32:11+00:00",
"request_id": "a3f9b2c1",
"level": "INFO",
"channel": "api",
"message": "Paiement reçu",
"elapsed_ms": 42,
"memory_kb": 1204,
"context": {"montant": 25000}
}
Commandes CLI
php briko logs # 50 dernières lignes (log combiné)
php briko logs api 100 # 100 dernières lignes du canal "api"
php briko logs:tail # Suivre en temps réel
php briko logs:tail paiements
php briko logs:clear # Supprimer tous les logs
Offline-First
Conçu pour les connexions instables. Le middleware OfflineFirst gère automatiquement les pannes de base de données.
Activer
use Briko\Http\Middleware\OfflineFirst;
$router->get('/users', [UserController::class, 'index'], [OfflineFirst::class]);
$router->post('/orders', [OrderController::class, 'store'], [OfflineFirst::class]);
Comportement
| Requête | DB disponible | DB hors ligne |
|---|---|---|
GET | Réponse normale | Réponse depuis le cache fichier |
POST / PUT / DELETE | Exécution normale | Ajouté dans la file d'attente JSON |
Synchronisation
php briko sync:status # Voir les requêtes en attente
php briko sync # Rejouer les requêtes (une fois la DB revenue)
php briko sync:flush # Vider la file d'attente
Payment — Mobile Money & Paiements
Module de paiement driver-based : Orange Money, MTN MoMo, Wave, CinetPay, PayDunya, Stripe. Une API unifiée — tu changes le provider dans .env, zéro modification de code.
Configuration
Dans .env, choisis ton provider et renseigne ses clés. Le code de ton app ne change pas.
PAYMENT_DRIVER=cinetpay # log | orangemoney | mtnmomo | wave | cinetpay | paydunya | stripe
PAYMENT_CURRENCY=XOF
Orange Money CI
Obtiens tes clés sur developer.orange.com → API Orange Money Web Payment.
PAYMENT_DRIVER=orangemoney
ORANGE_MONEY_CLIENT_ID=ton_client_id
ORANGE_MONEY_CLIENT_SECRET=ton_client_secret
ORANGE_MONEY_MERCHANT_KEY=ta_merchant_key
ORANGE_MONEY_RETURN_URL=https://ton-site.ci/paiement/retour
ORANGE_MONEY_CANCEL_URL=https://ton-site.ci/paiement/annule
ORANGE_MONEY_NOTIF_URL=https://ton-site.ci/webhooks/payment
MTN Mobile Money
Obtiens tes clés sur momodeveloper.mtn.com. En sandbox, crée un API User via le portail.
PAYMENT_DRIVER=mtnmomo
MTN_MOMO_SUBSCRIPTION_KEY=ta_subscription_key
MTN_MOMO_API_USER=uuid_api_user
MTN_MOMO_API_KEY=ta_api_key
MTN_MOMO_ENVIRONMENT=sandbox # sandbox | production
MTN_MOMO_CURRENCY=XOF
MTN_MOMO_CALLBACK_URL=https://ton-site.ci/webhooks/payment
Wave
Obtiens ta clé sur wave.com/business → API.
PAYMENT_DRIVER=wave
WAVE_API_KEY=ta_cle_secrete
WAVE_SUCCESS_URL=https://ton-site.ci/paiement/succes
WAVE_ERROR_URL=https://ton-site.ci/paiement/erreur
WAVE_CALLBACK_URL=https://ton-site.ci/webhooks/payment
CinetPay — 8 pays africains
Obtiens tes clés sur cinetpay.com → Dashboard → Mes applications. Couvre CI, SN, CM, BF, ML, TG, GN, MG.
PAYMENT_DRIVER=cinetpay
CINETPAY_API_KEY=ta_api_key
CINETPAY_SITE_ID=ton_site_id
CINETPAY_NOTIFY_URL=https://ton-site.ci/webhooks/payment
CINETPAY_RETURN_URL=https://ton-site.ci/paiement/retour
PayDunya — Sénégal, Mali, Burkina, CI
Obtiens tes clés sur paydunya.com/developers.
PAYMENT_DRIVER=paydunya
PAYDUNYA_MASTER_KEY=ta_master_key
PAYDUNYA_PRIVATE_KEY=ta_private_key
PAYDUNYA_TOKEN=ton_token
PAYDUNYA_STORE_NAME=Mon App
PAYDUNYA_ENV=test # test | live
PAYDUNYA_RETURN_URL=https://ton-site.ci/paiement/retour
PAYDUNYA_CANCEL_URL=https://ton-site.ci/paiement/annule
PAYDUNYA_CALLBACK_URL=https://ton-site.ci/webhooks/payment
Stripe — International
Obtiens ta clé sur dashboard.stripe.com → Développeurs → Clés API.
PAYMENT_DRIVER=stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_... # optionnel mais recommandé
STRIPE_SUCCESS_URL=https://ton-site.ci/paiement/succes
STRIPE_CANCEL_URL=https://ton-site.ci/paiement/annule
Initier un paiement
L'helper pay() retourne un PaymentMessage fluent. L'API est identique quel que soit le provider.
$result = pay(5000) // montant en FCFA
->to('+2250701234567') // numéro à débiter (MoMo push) ou client (redirection)
->description('Commande #42')
->reference('ORDER-42') // ID unique, auto-généré si omis
->send();
// Providers à redirection (Orange, Wave, CinetPay, PayDunya, Stripe)
if ($result->isPending() && $result->paymentUrl) {
header('Location: ' . $result->paymentUrl);
exit;
}
// MTN MoMo (push direct sur le téléphone)
if ($result->isPending()) {
return ['message' => 'Confirme le paiement sur ton téléphone MTN.'];
}
if ($result->isFailed()) {
return ['error' => $result->message];
}
Options avancées
pay(10000)
->to('+2250701234567')
->email('client@email.ci') // email du client (Stripe, CinetPay)
->name('Koné Amadou') // nom complet du client
->description('Abonnement Pro')
->reference('SUB-2026-001')
->currency('XOF') // défaut : PAYMENT_CURRENCY dans .env
->returnUrl('https://...') // surcharge RETURN_URL du .env
->cancelUrl('https://...') // surcharge CANCEL_URL du .env
->callbackUrl('https://...') // surcharge NOTIFY_URL du .env
->meta(['user_id' => 42]) // données personnalisées (stockées dans raw)
->send();
PaymentResult
| Propriété | Type | Description |
|---|---|---|
$result->success | bool | true si initiation réussie (même si paiement encore en attente) |
$result->status | string | pending | success | failed | cancelled |
$result->transactionId | string | ID de transaction retourné par le provider |
$result->paymentUrl | ?string | URL de redirection (Orange, Wave, CinetPay, PayDunya, Stripe) |
$result->amount | int | Montant de la transaction |
$result->currency | string | Devise (XOF, USD, EUR…) |
$result->message | string | Message lisible (succès ou erreur) |
$result->raw | array | Réponse brute de l'API provider |
$result->isOk() // true si success ou pending (initiation OK)
$result->isPending() // true si en attente de confirmation
$result->isFailed() // true si échec
Vérifier une transaction
Après une redirection retour ou un délai, vérifie l'état réel de la transaction.
use Briko\Payment\Payment;
$result = Payment::verify('CP-TXN123456');
if ($result->isOk()) {
// Mettre à jour la commande en BDD
db('orders')->where('ref', 'ORDER-42')->update(['status' => 'paid']);
}
Webhooks — callbacks asynchrones
Les providers envoient un POST sur ton URL de notification quand le paiement est confirmé. Le WebhookHandler normalise automatiquement le payload quel que soit le provider.
use Briko\Payment\WebhookHandler;
$router->post('/webhooks/payment', function ($request) {
$result = WebhookHandler::handle($request->body());
if ($result->isOk()) {
// Commande payée — mets à jour ta BDD
db('orders')
->where('ref', $result->transactionId)
->update(['status' => 'paid', 'paid_at' => date('Y-m-d H:i:s')]);
// Envoie un SMS de confirmation
sms($request->body()['phone'] ?? '')
->send('Paiement reçu ✅. Merci !');
}
return ['ok' => true]; // Toujours répondre 200 au provider
});
Vérification signature Stripe
$router->post('/webhooks/stripe', function ($request) {
$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
if (!WebhookHandler::verifyStripeSignature($rawBody, $sig)) {
http_response_code(400);
return ['error' => 'Signature invalide'];
}
$result = WebhookHandler::handle($request->body(), 'stripe');
// ... traitement
return ['ok' => true];
});
Exemple complet — Controller Checkout
all(), [
'amount' => 'required|numeric|min:100',
'phone' => 'required',
]);
if ($v->fails()) {
Response::json(['errors' => $v->errors()], 422);
return;
}
$orderId = 'ORDER-' . time();
// Sauvegarder la commande en attente
db('orders')->insert([
'ref' => $orderId,
'amount' => $request->input('amount'),
'status' => 'pending',
]);
$result = pay((int) $request->input('amount'))
->to($request->input('phone'))
->description('Commande ' . $orderId)
->reference($orderId)
->send();
if ($result->isFailed()) {
Response::json(['error' => $result->message], 400);
return;
}
// Provider à redirection (CinetPay, Orange, Wave…)
if ($result->paymentUrl) {
Response::json([
'status' => 'redirect',
'payment_url' => $result->paymentUrl,
'tx_id' => $result->transactionId,
]);
return;
}
// MTN MoMo (push mobile)
Response::json([
'status' => 'pending',
'message' => $result->message,
'tx_id' => $result->transactionId,
]);
}
// GET /checkout/verify?tx_id=... — vérification manuelle
public function verify(Request $request): array
{
$txId = $request->input('tx_id');
$result = Payment::verify($txId);
if ($result->isOk()) {
db('orders')->where('ref', $txId)->update(['status' => 'paid']);
}
return [
'status' => $result->status,
'message' => $result->message,
];
}
// POST /webhooks/payment — appelé par le provider
public function webhook(Request $request): array
{
$result = WebhookHandler::handle($request->body());
if ($result->isOk()) {
db('orders')
->where('ref', $result->transactionId)
->update(['status' => 'paid', 'paid_at' => date('Y-m-d H:i:s')]);
}
return ['ok' => true];
}
}
use App\Controllers\CheckoutController;
$router->post('/checkout', [CheckoutController::class, 'initiate']);
$router->get('/checkout/verify', [CheckoutController::class, 'verify']);
$router->post('/webhooks/payment', [CheckoutController::class, 'webhook']);
Commandes CLI Payment
| Commande | Description |
|---|---|
php briko payment:driver | Affiche le driver actif et vérifie les clés .env |
php briko payment:test <tel> [montant] | Teste un paiement en sandbox |
php briko payment:verify <txId> | Vérifie l'état d'une transaction |
# Vérifier la configuration du driver actif
php briko payment:driver
# Tester un paiement de 2500 XOF (utilise le driver .env)
php briko payment:test +2250701234567 2500
# Vérifier une transaction existante
php briko payment:verify CP-TXN123ABC
Comparatif des providers
| Provider | Pays | Flux | Sandbox | Portail |
|---|---|---|---|---|
| Orange Money | CI, SN, CM… | Redirection | ✅ | developer.orange.com |
| MTN MoMo | Multi-pays MTN | Push mobile (async) | ✅ | momodeveloper.mtn.com |
| Wave | CI, SN | Redirection | ✅ | wave.com/business |
| CinetPay | CI, SN, CM, BF, ML, TG, GN, MG | Redirection | ✅ | cinetpay.com |
| PayDunya | SN, ML, BF, CI | Redirection | ✅ | paydunya.com/developers |
| Stripe | International | Redirection (page Stripe) | ✅ | dashboard.stripe.com |
| Log | — | Simulé (logs uniquement) | ✅ | aucun portail requis |
CLI — Console
Toutes les commandes s'exécutent avec php briko <commande>.
Référence complète
| Commande | Description |
|---|---|
php briko feu | Démarrer le serveur de développement (port 8000) |
php briko env:setup | Créer .env depuis .env.example |
php briko migrate | Exécuter les migrations en attente |
php briko migrate:status | État de chaque migration |
php briko migrate:rollback | Annuler le dernier batch |
php briko migrate:fresh | Tout supprimer et rejouer |
php briko sync | Rejouer la file d'attente offline |
php briko sync:status | Voir la file d'attente |
php briko sync:flush | Vider la file d'attente |
php briko logs [canal] [n] | Afficher les N dernières lignes |
php briko logs:tail [canal] | Suivre les logs en temps réel |
php briko logs:clear | Supprimer tous les fichiers de log |
php briko sms:test <numéro> | Envoyer un SMS de test |
php briko sms:otp <numéro> | Générer et envoyer un OTP |
php briko sms:driver | Afficher la config SMS active |
php briko mail:test <email> | Envoyer un email de test |
php briko mail:driver | Afficher la config Mail active |
php briko payment:driver | Afficher la config Payment active et vérifier les clés |
php briko payment:test <tel> [montant] | Tester un paiement en sandbox |
php briko payment:verify <txId> | Vérifier l'état d'une transaction |
php briko fabrique:controller <Nom> | Créer un controller REST |
php briko fabrique:model <Nom> | Créer un modèle |
php briko fabrique:mail <Nom> | Créer un Mailable + template |
php briko fabrique:migration <nom> | Créer un fichier de migration |
php briko fabrique:seeder <Nom> | Créer un Seeder |
php briko db:seed [--class=Nom] | Exécuter les seeders |
php briko help | Afficher l'aide |
Helpers globaux
| Fonction | Description | Exemple |
|---|---|---|
env($key, $default) | Lire une variable .env | env('APP_NAME') |
db($table) | Query Builder sur une table | db('users')->get() |
sms($to) | Créer un message SMS | sms('+225...')->message('...') |
mail_to($email) | Créer un message Mail | mail_to('a@b.ci')->subject('...') |
view($template, $data) | Rendre un template depuis app/views/ | view('accueil', ['titre' => 'Home']) |
logger($msg, $ctx, $level) | Écrire un log | logger('Erreur', [], 'error') |
base_path($path) | Chemin absolu depuis la racine | base_path('storage/file.pdf') |
🔥 API REST — de A à Z
Construis une API complète de gestion de tâches avec authentification par token, CRUD complet et vérification OTP.
/api/tasks avec authentification, CRUD, pagination, OTP SMS et notifications mail.
1. Créer le projet
composer create-project brikocode/framework taskapi
cd taskapi
php briko env:setup
Édite .env :
APP_NAME=TaskAPI
APP_ENV=local
APP_URL=http://localhost:8000
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_NAME=taskapi
DB_USER=root
DB_PASS=
SMS_DRIVER=log
MAIL_DRIVER=log
2. Migrations
<?php
return [
'up' => function (PDO $pdo): void {
$pdo->exec("
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
telephone VARCHAR(20),
token VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
},
'down' => fn(PDO $pdo) => $pdo->exec("DROP TABLE IF EXISTS users"),
];
<?php
return [
'up' => function (PDO $pdo): void {
$pdo->exec("
CREATE TABLE tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
titre VARCHAR(200) NOT NULL,
description TEXT,
statut ENUM('todo','en_cours','fait') DEFAULT 'todo',
priorite TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
");
},
'down' => fn(PDO $pdo) => $pdo->exec("DROP TABLE IF EXISTS tasks"),
];
php briko migrate
3. Middleware d'authentification
<?php
namespace App\Middleware;
use Briko\Http\Middleware\MiddlewareInterface;
use Briko\Http\Request;
class BearerAuth implements MiddlewareInterface
{
public function handle(Request $request, callable $next): mixed
{
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $header);
if (!$token) {
return ['error' => 'Token manquant', '_status' => 401];
}
$user = db('users')->where('token', $token)->first();
if (!$user) {
return ['error' => 'Token invalide', '_status' => 401];
}
// Injecte l'utilisateur dans la requête
$_REQUEST['_auth_user'] = $user;
return $next($request);
}
}
4. Controllers
php briko fabrique:controller AuthController
php briko fabrique:controller TaskController
<?php
namespace App\Controllers;
use Briko\Http\Request;
use Briko\Sms\SMS;
class AuthController
{
// POST /api/register
public function register(Request $request): array
{
$data = $request->all();
if (empty($data['email']) || empty($data['nom'])) {
return ['error' => 'nom et email requis', '_status' => 422];
}
if (db('users')->where('email', $data['email'])->exists()) {
return ['error' => 'Email déjà utilisé', '_status' => 409];
}
$token = bin2hex(random_bytes(32));
$id = db('users')->insertGetId([
'nom' => $data['nom'],
'email' => $data['email'],
'telephone' => $data['telephone'] ?? null,
'token' => $token,
]);
// Email de bienvenue
mail_to($data['email'])
->subject('Bienvenue sur ' . env('APP_NAME'))
->html('<h2>Bienvenue, ' . htmlspecialchars($data['nom']) . ' !</h2>')
->send();
return ['id' => $id, 'token' => $token, '_status' => 201];
}
// POST /api/otp/send
public function sendOtp(Request $request): array
{
$phone = $request->input('telephone');
if (!$phone) return ['error' => 'téléphone requis', '_status' => 422];
SMS::otp($phone);
return ['message' => 'OTP envoyé'];
}
// POST /api/otp/verify
public function verifyOtp(Request $request): array
{
$phone = $request->input('telephone');
$code = $request->input('code');
if (SMS::verifyOtp($phone, $code)) {
return ['verified' => true];
}
return ['error' => 'Code invalide ou expiré', '_status' => 400];
}
}
<?php
namespace App\Controllers;
use Briko\Http\Request;
class TaskController
{
private function authUser(): array
{
return $_REQUEST['_auth_user'];
}
// GET /api/tasks
public function index(Request $request): array
{
$user = $this->authUser();
return db('tasks')
->where('user_id', $user['id'])
->orderBy('created_at', 'DESC')
->paginate(20);
}
// GET /api/tasks/{id}
public function show(Request $request): array
{
$user = $this->authUser();
$task = db('tasks')
->where('id', $request->param('id'))
->where('user_id', $user['id'])
->first();
if (!$task) return ['error' => 'Tâche introuvable', '_status' => 404];
return $task;
}
// POST /api/tasks
public function store(Request $request): array
{
$user = $this->authUser();
$data = $request->all();
if (empty($data['titre'])) {
return ['error' => 'Le titre est requis', '_status' => 422];
}
$id = db('tasks')->insertGetId([
'user_id' => $user['id'],
'titre' => $data['titre'],
'description' => $data['description'] ?? null,
'priorite' => $data['priorite'] ?? 1,
]);
return ['id' => $id, 'created' => true, '_status' => 201];
}
// PUT /api/tasks/{id}
public function update(Request $request): array
{
$user = $this->authUser();
$taskId = $request->param('id');
$task = db('tasks')->where('id', $taskId)->where('user_id', $user['id'])->first();
if (!$task) return ['error' => 'Tâche introuvable', '_status' => 404];
$data = array_filter($request->all(), fn($v) => $v !== null);
unset($data['user_id']);
db('tasks')->where('id', $taskId)->update($data);
return ['updated' => true];
}
// DELETE /api/tasks/{id}
public function destroy(Request $request): array
{
$user = $this->authUser();
$taskId = $request->param('id');
$task = db('tasks')->where('id', $taskId)->where('user_id', $user['id'])->first();
if (!$task) return ['error' => 'Tâche introuvable', '_status' => 404];
db('tasks')->where('id', $taskId)->delete();
return ['deleted' => true];
}
}
5. Routes
<?php
use App\Controllers\AuthController;
use App\Controllers\TaskController;
use App\Middleware\BearerAuth;
/** @var \Briko\Routing\Router $router */
// Auth (publiques)
$router->post('/api/register', [AuthController::class, 'register']);
$router->post('/api/otp/send', [AuthController::class, 'sendOtp']);
$router->post('/api/otp/verify', [AuthController::class, 'verifyOtp']);
// Tasks (protégées)
$router->get('/api/tasks', [TaskController::class, 'index'], [BearerAuth::class]);
$router->get('/api/tasks/{id}', [TaskController::class, 'show'], [BearerAuth::class]);
$router->post('/api/tasks', [TaskController::class, 'store'], [BearerAuth::class]);
$router->put('/api/tasks/{id}', [TaskController::class, 'update'], [BearerAuth::class]);
$router->delete('/api/tasks/{id}',[TaskController::class, 'destroy'], [BearerAuth::class]);
6. Tester l'API
# Lancer le serveur
php briko feu
# S'inscrire
curl -X POST http://localhost:8000/api/register \
-H "Content-Type: application/json" \
-d '{"nom":"Aya Koné","email":"aya@ci.ci","telephone":"+225070000000"}'
# → {"id":1,"token":"abc123..."}
# Créer une tâche
curl -X POST http://localhost:8000/api/tasks \
-H "Authorization: Bearer abc123..." \
-H "Content-Type: application/json" \
-d '{"titre":"Finir le rapport","priorite":2}'
# → {"id":1,"created":true}
# Lister les tâches
curl http://localhost:8000/api/tasks \
-H "Authorization: Bearer abc123..."
# Mettre à jour le statut
curl -X PUT http://localhost:8000/api/tasks/1 \
-H "Authorization: Bearer abc123..." \
-H "Content-Type: application/json" \
-d '{"statut":"fait"}'
7. Ajouter une interface HTML
L'API JSON reste intacte. On ajoute des routes HTML qui affichent des pages utilisant cette API via fetch().
use App\Controllers\PageController;
// Routes HTML (nouvelles)
$router->get('/', [PageController::class, 'accueil']);
$router->get('/app', [PageController::class, 'appShell'], [BearerAuth::class]);
// Routes API (existantes, inchangées)
$router->post('/api/register', [AuthController::class, 'register']);
// ...
<?php
namespace App\Controllers;
use Briko\Http\Request;
use Briko\Http\Response;
class PageController
{
// Page d'accueil avec formulaire de connexion
public function accueil(Request $request): void
{
Response::html(view('accueil', [
'titre' => 'TaskAPI — Gérez vos tâches',
]));
}
// Shell de l'application (chargé une fois après login)
public function appShell(Request $request): void
{
$user = $_REQUEST['_auth_user'];
Response::html(view('app', [
'titre' => 'Mes tâches',
'user' => $user,
]));
}
}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($titre) ?></title>
<style>
body { font-family: sans-serif; max-width: 400px; margin: 60px auto; padding: 0 1rem; }
input, button { width: 100%; padding: .6rem; margin: .4rem 0; }
button { background: #ff6600; color: #fff; border: none; cursor: pointer; }
</style>
</head>
<body>
<h1><?= htmlspecialchars($titre) ?></h1>
<form id="form-register">
<h2>Créer un compte</h2>
<input type="text" name="nom" placeholder="Ton nom">
<input type="email" name="email" placeholder="Email">
<button type="submit">S'inscrire</button>
<p id="msg-register"></p>
</form>
<script>
document.getElementById('form-register').addEventListener('submit', async e => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json();
if (json.token) {
localStorage.setItem('token', json.token);
location.href = '/app'; // redirige vers la vue app
} else {
document.getElementById('msg-register').textContent = json.error;
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($titre) ?></title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 1rem; }
.task { padding: .5rem; border-bottom: 1px solid #eee; display: flex; gap: .5rem; }
.task.fait { opacity: .5; text-decoration: line-through; }
input[type=text] { flex: 1; padding: .5rem; }
button { padding: .5rem 1rem; background: #ff6600; color: #fff; border: none; cursor: pointer; }
</style>
</head>
<body>
<h1>Bonjour, <?= htmlspecialchars($user['nom']) ?> ! 👋</h1>
<form id="form-add">
<input type="text" id="titre" placeholder="Nouvelle tâche..." required>
<button type="submit">Ajouter</button>
</form>
<div id="tasks">Chargement...</div>
<script>
const token = localStorage.getItem('token');
if (!token) location.href = '/';
// Charger les tâches via l'API
async function loadTasks() {
const res = await fetch('/api/tasks', {
headers: { 'Authorization': 'Bearer ' + token }
});
const json = await res.json();
const tasks = json.data ?? json;
document.getElementById('tasks').innerHTML = tasks.map(t => `
<div class="task ${t.statut === 'fait' ? 'fait' : ''}">
<span>${t.titre}</span>
<button onclick="updateTask(${t.id}, 'fait')">✓</button>
<button onclick="deleteTask(${t.id})">✕</button>
</div>
`).join('');
}
// Ajouter une tâche
document.getElementById('form-add').addEventListener('submit', async e => {
e.preventDefault();
const titre = document.getElementById('titre').value;
await fetch('/api/tasks', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ titre }),
});
document.getElementById('titre').value = '';
loadTasks();
});
async function updateTask(id, statut) {
await fetch('/api/tasks/' + id, {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ statut }),
});
loadTasks();
}
async function deleteTask(id) {
await fetch('/api/tasks/' + id, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token },
});
loadTasks();
}
loadTasks();
</script>
</body>
</html>
view() côté serveur et fetch() pour appeler l'API. Aucune dépendance JS externe.
🚀 Projet complet — de A à Z
Une application de gestion de commandes avec auth SMS OTP, produits, commandes, emails de confirmation et mode offline.
1. Setup
composer create-project brikocode/framework boutique
cd boutique
php briko env:setup
APP_NAME=Boutique
DB_NAME=boutique
SMS_DRIVER=africastalking
AT_USERNAME=boutique
AT_API_KEY=ta_cle_api
AT_SANDBOX=false
MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=ta@gmail.com
MAIL_PASSWORD=mot_de_passe_app
2. Base de données
return [
'up' => function(PDO $pdo) {
$pdo->exec("CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
telephone VARCHAR(20) NOT NULL UNIQUE,
email VARCHAR(150),
token VARCHAR(64),
verifie TINYINT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
},
'down' => fn($p) => $p->exec("DROP TABLE IF EXISTS users"),
];
return [
'up' => function(PDO $pdo) {
$pdo->exec("CREATE TABLE produits (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(200) NOT NULL,
prix DECIMAL(10,0) NOT NULL,
stock INT DEFAULT 0,
categorie VARCHAR(100),
actif TINYINT DEFAULT 1
)");
},
'down' => fn($p) => $p->exec("DROP TABLE IF EXISTS produits"),
];
return [
'up' => function(PDO $pdo) {
$pdo->exec("CREATE TABLE commandes (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
produit_id INT NOT NULL,
quantite INT DEFAULT 1,
total DECIMAL(10,0),
statut ENUM('en_attente','payee','livree') DEFAULT 'en_attente',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (produit_id) REFERENCES produits(id)
)");
},
'down' => fn($p) => $p->exec("DROP TABLE IF EXISTS commandes"),
];
php briko migrate
3. Auth OTP par SMS
<?php
namespace App\Controllers;
use Briko\Http\Request;
use Briko\Sms\SMS;
class AuthController
{
// POST /auth/register — étape 1 : enregistrement
public function register(Request $request): array
{
$tel = $request->input('telephone');
$nom = $request->input('nom');
if (!$tel || !$nom) return ['error' => 'nom et telephone requis', '_status' => 422];
$user = db('users')->where('telephone', $tel)->first();
if (!$user) {
db('users')->insert(['nom' => $nom, 'telephone' => $tel]);
}
SMS::otp($tel); // Envoie le code
return ['message' => 'Code OTP envoyé au ' . $tel];
}
// POST /auth/verify — étape 2 : vérification OTP
public function verify(Request $request): array
{
$tel = $request->input('telephone');
$code = $request->input('code');
if (!SMS::verifyOtp($tel, $code)) {
return ['error' => 'Code invalide ou expiré', '_status' => 400];
}
$token = bin2hex(random_bytes(32));
db('users')->where('telephone', $tel)->update([
'token' => $token,
'verifie' => 1,
]);
$user = db('users')->where('telephone', $tel)->first();
// Mail de bienvenue si email renseigné
if (!empty($user['email'])) {
mail_to($user['email'])
->subject('Bienvenue sur ' . env('APP_NAME'))
->view('bienvenue', ['user' => $user])
->send();
}
return ['token' => $token, 'user' => $user];
}
}
4. Catalogue & Commandes
<?php
namespace App\Controllers;
use Briko\Http\Request;
class ProduitController
{
// GET /produits — liste avec mode offline-first
public function index(Request $request): array
{
return db('produits')
->where('actif', 1)
->orderBy('nom')
->paginate(20);
}
// GET /produits/{id}
public function show(Request $request): array
{
$p = db('produits')->find($request->param('id'));
if (!$p) return ['error' => 'Produit introuvable', '_status' => 404];
return $p;
}
}
<?php
namespace App\Controllers;
use Briko\Http\Request;
class CommandeController
{
private function authUser(): array
{
return $_REQUEST['_auth_user'];
}
// POST /commandes
public function store(Request $request): array
{
$user = $this->authUser();
$data = $request->all();
$qte = max(1, (int)($data['quantite'] ?? 1));
$produit = db('produits')->find($data['produit_id'] ?? 0);
if (!$produit) return ['error' => 'Produit introuvable', '_status' => 404];
if ($produit['stock'] < $qte) return ['error' => 'Stock insuffisant', '_status' => 400];
$total = $produit['prix'] * $qte;
$id = db('commandes')->insertGetId([
'user_id' => $user['id'],
'produit_id' => $produit['id'],
'quantite' => $qte,
'total' => $total,
]);
// Décrémenter le stock
db('produits')->where('id', $produit['id'])
->update(['stock' => $produit['stock'] - $qte]);
// SMS de confirmation
sms($user['telephone'])
->message("Commande #{$id} confirmée ! Total : {$total} FCFA")
->send();
// Email si disponible
if (!empty($user['email'])) {
mail_to($user['email'])
->subject("Commande #{$id} confirmée")
->html("
<h2>Commande #{$id} confirmée !</h2>
<p>Produit : {$produit['nom']}</p>
<p>Quantité : {$qte}</p>
<p><strong>Total : {$total} FCFA</strong></p>
")
->send();
}
return ['id' => $id, 'total' => $total, '_status' => 201];
}
// GET /commandes
public function index(Request $request): array
{
$user = $this->authUser();
return db('commandes')->where('user_id', $user['id'])
->orderBy('created_at', 'DESC')->paginate(20);
}
}
5. Routes finales
<?php
use App\Controllers\AuthController;
use App\Controllers\ProduitController;
use App\Controllers\CommandeController;
use App\Middleware\BearerAuth;
use Briko\Http\Middleware\OfflineFirst;
use Briko\Http\Middleware\LowBandwidth;
/** @var \Briko\Routing\Router $router */
// Auth
$router->post('/auth/register', [AuthController::class, 'register']);
$router->post('/auth/verify', [AuthController::class, 'verify']);
// Produits (publics + offline-first + low bandwidth)
$router->get('/produits', [ProduitController::class, 'index'], [OfflineFirst::class, LowBandwidth::class]);
$router->get('/produits/{id}', [ProduitController::class, 'show'], [OfflineFirst::class]);
// Commandes (protégées)
$router->post('/commandes', [CommandeController::class, 'store'], [BearerAuth::class]);
$router->get('/commandes', [CommandeController::class, 'index'], [BearerAuth::class]);
6. Test complet
php briko feu
# 1. S'inscrire et recevoir l'OTP
curl -X POST http://localhost:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"nom":"Aya","telephone":"+225070000000"}'
# 2. Vérifier l'OTP (vérifie les logs si SMS_DRIVER=log)
php briko logs # → voir le code OTP
curl -X POST http://localhost:8000/auth/verify \
-H "Content-Type: application/json" \
-d '{"telephone":"+225070000000","code":"482931"}'
# → {"token":"abc...","user":{...}}
# 3. Voir les produits
curl http://localhost:8000/produits
# 4. Passer une commande
curl -X POST http://localhost:8000/commandes \
-H "Authorization: Bearer abc..." \
-H "Content-Type: application/json" \
-d '{"produit_id":1,"quantite":2}'
# → SMS + email de confirmation envoyés !
7. Pages HTML avec vues
On ajoute des routes HTML pour les pages publiques de la boutique. Les données sont chargées côté serveur et le paiement/commande appelle l'API JSON.
use App\Controllers\PageController;
// Pages HTML publiques
$router->get('/', [PageController::class, 'accueil']);
$router->get('/boutique', [PageController::class, 'catalogue']);
$router->get('/boutique/{id}', [PageController::class, 'produit']);
$router->get('/inscription', [PageController::class, 'inscription']);
$router->get('/mon-compte', [PageController::class, 'monCompte'], [BearerAuth::class]);
// API JSON (existantes)
$router->post('/auth/register', [AuthController::class, 'register']);
$router->post('/auth/verify', [AuthController::class, 'verify']);
$router->get('/produits', [ProduitController::class, 'index'], [OfflineFirst::class]);
$router->post('/commandes', [CommandeController::class, 'store'], [BearerAuth::class]);
<?php
namespace App\Controllers;
use Briko\Http\Request;
use Briko\Http\Response;
class PageController
{
public function accueil(Request $request): void
{
// Données chargées côté serveur, passées à la vue
$produits = db('produits')->where('actif', 1)->limit(6)->get();
Response::html(view('accueil', [
'titre' => 'Boutique',
'produits' => $produits,
]));
}
public function catalogue(Request $request): void
{
$produits = db('produits')
->where('actif', 1)
->orderBy('nom')
->paginate(12);
Response::html(view('boutique.catalogue', [
'titre' => 'Tous nos produits',
'produits' => $produits['data'],
'pages' => $produits,
]));
}
public function produit(Request $request): void
{
$produit = db('produits')->find($request->param('id'));
if (!$produit) {
Response::html(view('erreurs.404', ['titre' => 'Produit introuvable']), 404);
return;
}
Response::html(view('boutique.produit', [
'titre' => $produit['nom'],
'produit' => $produit,
]));
}
public function inscription(Request $request): void
{
Response::html(view('auth.inscription', [
'titre' => 'Créer mon compte',
]));
}
public function monCompte(Request $request): void
{
$user = $_REQUEST['_auth_user'];
$commandes = db('commandes')
->where('user_id', $user['id'])
->orderBy('created_at', 'DESC')
->get();
Response::html(view('compte.index', [
'titre' => 'Mon compte',
'user' => $user,
'commandes' => $commandes,
]));
}
}
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title><?= htmlspecialchars($titre) ?></title></head>
<body>
<h1><?= htmlspecialchars($titre) ?></h1>
<!-- Étape 1 : saisie du numéro -->
<form id="step1">
<input type="text" name="nom" placeholder="Ton nom" required>
<input type="tel" name="telephone" placeholder="+225070000000" required>
<input type="email" name="email" placeholder="Email (optionnel)">
<button type="submit">Recevoir mon code SMS</button>
<p id="err1" style="color:red"></p>
</form>
<!-- Étape 2 : vérification OTP (cachée au départ) -->
<form id="step2" style="display:none">
<p>Code envoyé au <span id="tel-display"></span></p>
<input type="text" name="code" placeholder="Code à 6 chiffres" maxlength="6" required>
<button type="submit">Vérifier</button>
<p id="err2" style="color:red"></p>
</form>
<script>
let phoneNum = '';
// Étape 1 : envoyer l'OTP via l'API
document.getElementById('step1').addEventListener('submit', async e => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target));
phoneNum = data.telephone;
const res = await fetch('/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json();
if (res.ok) {
document.getElementById('step1').style.display = 'none';
document.getElementById('step2').style.display = 'block';
document.getElementById('tel-display').textContent = phoneNum;
} else {
document.getElementById('err1').textContent = json.error;
}
});
// Étape 2 : vérifier l'OTP
document.getElementById('step2').addEventListener('submit', async e => {
e.preventDefault();
const code = e.target.code.value;
const res = await fetch('/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ telephone: phoneNum, code }),
});
const json = await res.json();
if (json.token) {
localStorage.setItem('token', json.token);
location.href = '/mon-compte'; // → vue HTML mon-compte
} else {
document.getElementById('err2').textContent = json.error;
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title><?= htmlspecialchars($titre) ?></title></head>
<body>
<a href="/boutique">← Retour</a>
<h1><?= htmlspecialchars($produit['nom']) ?></h1>
<p>Prix : <strong><?= number_format($produit['prix']) ?> FCFA</strong></p>
<p>Stock : <?= $produit['stock'] ?> disponible(s)</p>
<form id="form-commande">
<input type="hidden" name="produit_id" value="<?= $produit['id'] ?>">
<label>Quantité :
<input type="number" name="quantite" value="1" min="1" max="<?= $produit['stock'] ?>">
</label>
<button type="submit">Commander</button>
<p id="msg"></p>
</form>
<script>
document.getElementById('form-commande').addEventListener('submit', async e => {
e.preventDefault();
const token = localStorage.getItem('token');
if (!token) { location.href = '/inscription'; return; }
const data = Object.fromEntries(new FormData(e.target));
const res = await fetch('/commandes', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ produit_id: +data.produit_id, quantite: +data.quantite }),
});
const json = await res.json();
document.getElementById('msg').textContent = res.ok
? '✅ Commande #' + json.id + ' confirmée ! Total : ' + json.total + ' FCFA'
: '❌ ' + json.error;
});
</script>
</body>
</html>
view() + API JSON pour les actions (commande, auth OTP). Architecture hybride propre — aucun framework JS requis.