M-PHP

PHP typé de bout en bout.
Svelte pour l'interface.
Un fichier pour déployer.

Un framework PHP léger, sécurisé par défaut, où le PHP reste la source de vérité : les types TypeScript, les routes et le client d'API sont générés depuis vos contrôleurs.

Pourquoi M-PHP

Types de bout en bout

Une faute de frappe dans un nom de route, un champ de formulaire oublié ou un mauvais type de réponse sont des erreurs de compilation, sans OpenAPI ni serveur Node.

Léger à prendre en main

Un contrôleur, des attributs, une page Svelte. SQLite par défaut : rien à installer pour commencer.

Sécurisé par défaut

Requêtes préparées, CSRF, CSP sans inline, sessions durcies, uploads vérifiés sur le contenu, $fillable obligatoire.

Déploiement trivial

php bin/mphp package pour un zip, php bin/mphp compile pour un unique fichier PHAR. Pas de vendor/ ni de Node en production.

Démarrage

composer install
npm install
cp .env.example .env
php bin/mphp key:generate
php bin/mphp migrate

php bin/mphp dev      # PHP http://127.0.0.1:8000 + Vite (rechargement à chaud)

Avec Docker : docker compose up --build puis localhost:8000.

DossierRôle
app/ControllersContrôleurs ; les routes sont des attributs PHP
app/ModelsModèles ($fillable obligatoire)
app/DataDonnées envoyées au navigateur, converties en types TypeScript
app/RequestsCorps de requête validés et typés (FormRequest)
resources/pagesUne page Svelte par fichier
database/migrationsMigrations
public/La seule racine web
core/Le framework (Mphp\)

Concepts

1. Une entrée validée et typée

final readonly class NameInput extends FormRequest
{
    public function __construct(
        #[Rule('required|string|max:50')]
        public string $firstname,
    ) {}
}

2. Un contrôleur avec ses routes

#[Prefix('/api/names')]
final class NameController
{
    #[Put('/{id:\d+}', name: 'names.update')]
    public function update(NameInput $input, int $id): NameData   // 422 automatique si invalide
    {
        $name = Name::find($id) ?? throw new HttpException(404, 'Name not found.');

        return NameData::fromModel($name->update($input->toArray()));
    }
}

3. Générer les types

php bin/mphp types:generate

4. Les utiliser côté Svelte

const updated = await api('names.update', { params: { id: 3 }, body: { firstname: 'Ada' } });
//    ^ NameData : route, paramètres, corps et réponse viennent du PHP

const form = new Form('names.store', { firstname: '' });
// form.errors.firstname, form.processing, await form.submit()
Un paramètre manquant, un champ de corps oublié ou un nom de route inexistant ne compilent plus.

Ce que le framework fournit

BesoinAPI
Requête / réponseRequest, Response::json/html/redirect/noContent
Retour d'actionResponse ; tableau/objet → JSON ; chaîne → HTML ; null → 204
InjectionTypez la dépendance dans le constructeur ou l'action
Middleware'csrf', 'throttle:5,60', ou votre classe
ValidationFormRequest + #[Rule], ou Validator::validate()
Base de données$db->table('t')->where('a', '>=', 1)->get() ; Model::create/find/update/delete
ChiffrementCrypto::encrypt/decrypt/sign ; Passwords::hash/verify
UploadUploader::store($_FILES['f'], 'image')
Erreursthrow new HttpException(404, '…')

Sécurité par défaut

RisqueProtection
Injection SQLRequêtes préparées partout, identifiants validés, opérateurs en liste blanche, update/delete sans where refusés
CSRFJeton synchronisé en session, middleware global (X-CSRF-Token)
XSSÉchappement par défaut (e(), Svelte), props en JSON sûr, CSP script-src 'self' sans inline
Mass-assignment$fillable obligatoire sur les modèles
Fichiers sensiblesSeul public/ est servi ; .env et storage/ ne sont jamais exposés
UploadsType détecté sur le contenu, nom aléatoire, taille limitée
Chiffrementlibsodium (authentifié), nonce aléatoire, APP_KEY
SessionsHttpOnly, SameSite=Lax, Secure en HTTPS, mode strict
Force bruteMiddleware throttle sans Redis
Fuite d'informationErreur 500 générique en production, détails seulement avec APP_DEBUG=true

Barre de debug

Avec APP_DEBUG=true, chaque page HTML reçoit une barre en bas d'écran : statut, route, temps, mémoire et chaque requête SQL avec sa durée. Les requêtes répétées sont signalées (piste N+1). Elle fonctionne sans JavaScript, donc sous la CSP stricte. Les appels d'API reçoivent un en-tête Server-Timing, visible dans l'onglet Réseau du navigateur. En production, rien n'est ajouté.

Déploiement

Zip

php bin/mphp package produit dist/release.zip : front compilé et cache des routes inclus, sans vendor/ ni node_modules/.

PHAR

php bin/mphp compile produit dist/mphp.phar, un seul fichier. php mphp.phar serve suffit ; .env et storage/ restent à côté.

Docker

docker compose up --build : image PHP 8.5 + Apache, front compilé en amont, SQLite ou MySQL en option.

# Zip ou PHAR, sur le serveur
cp .env.example .env        # APP_ENV=production, APP_DEBUG=false
php bin/mphp key:generate
php bin/mphp migrate
# racine web = public/   (le .htaccess racine suffit sur un mutualisé)
# PHAR seul
php mphp.phar key:generate && php mphp.phar migrate && php mphp.phar serve --port=8000

Nginx : root /chemin/vers/public; location / { try_files $uri /index.php?$query_string; } puis le bloc PHP-FPM habituel.

Commandes

CommandeRôle
php bin/mphp devPHP + Vite ensemble (développement)
php bin/mphp serveServeur PHP seul
php bin/mphp routesListe des routes
make:controller X · make:model X · make:migration create_x_tableGénérateurs
migrate · migrate:rollback · migrate:statusBase de données
types:generateTypes TypeScript et helper de routes depuis le PHP
key:generateClé de chiffrement APP_KEY
buildFront de production + cache des routes
package · compileZip prêt à envoyer · fichier PHAR unique
composer test · npm run checkTests Pest · vérification TypeScript/Svelte