Symfony UX Native
Créer des applications mobiles avec PHP et Symfony
Notions théoriques
Qu'est-ce que Symfony UX Native ?
Symfony UX Native est une initiative expérimentale qui étend la philosophie de Symfony UX au-delà du navigateur web, vers le monde des applications mobiles natives. L'idée centrale est de permettre à un développeur PHP de créer des applications iOS et Android en utilisant ses connaissances Symfony existantes, sans avoir à apprendre Swift, Kotlin, React Native ou Flutter.
Symfony UX Native s'appuie sur NativePHP, un projet open source qui permet d'écrire des applications de bureau et mobiles en PHP. Symfony UX Native est l'intégration officielle de NativePHP dans l'écosystème Symfony UX.
Le projet repose sur une idée provocatrice mais cohérente : si Symfony UX permet déjà de faire du frontend interactif depuis PHP, pourquoi ne pas aller encore plus loin et cibler les plateformes mobiles directement ?
NativePHP : la technologie sous-jacente
Pour comprendre Symfony UX Native, il faut d'abord comprendre NativePHP.
NativePHP est un framework PHP créé par Simon Hamp en 2023. Il permet d'exécuter des applications PHP comme des applications de bureau ou mobiles natives, en embarquant un serveur PHP directement dans le binaire de l'application. Concrètement, NativePHP utilise deux moteurs selon la cible :
- Electron (pour les applications de bureau Windows, macOS, Linux)
- Capacitor (pour les applications mobiles iOS et Android)
Le principe est le suivant : au lieu d'exécuter PHP sur un serveur distant, NativePHP exécute PHP localement sur l'appareil de l'utilisateur. L'interface graphique est rendue par un moteur WebView natif (le même moteur que le navigateur intégré au système).
Le modèle de NativePHP ressemble à celui d'Electron (qui fait tourner Node.js localement pour des apps comme VS Code ou Slack), mais avec PHP au lieu de JavaScript.
L'architecture de Symfony UX Native
Symfony UX Native s'articule autour de 3 couches :
1. Le backend Symfony (PHP) C'est ici que vit toute la logique applicative : contrôleurs, entités Doctrine, services, etc. Ce code PHP s'exécute localement sur l'appareil mobile, via un serveur PHP embarqué.
2. Les vues Twig + Symfony UX L'interface utilisateur est construite avec Twig et les composants Symfony UX habituels (Twig Components, Live Components, Turbo). Le rendu HTML est affiché dans la WebView native de l'appareil.
3. Le pont natif (Native Bridge) C'est la couche qui permet à PHP d'accéder aux fonctionnalités natives de l'appareil : caméra, GPS, notifications push, vibrations, stockage local, accéléromètre, etc.
Application mobile
├── WebView (rendu HTML/CSS)
│ ├── Templates Twig
│ └── Composants Symfony UX
├── Serveur PHP embarqué
│ ├── Contrôleurs Symfony
│ ├── Services
│ └── Doctrine (SQLite local)
└── Native Bridge
├── Caméra
├── GPS
├── Notifications
└── Capteurs
La base de données utilisée dans une application Symfony UX Native est généralement SQLite plutôt que MariaDB, car elle ne nécessite pas de serveur séparé et s'embarque directement dans l'application.
Les composants spécifiques à Symfony UX Native
Symfony UX Native introduit plusieurs composants PHP dédiés aux fonctionnalités mobiles :
NativeCamera
Permet d'accéder à la caméra de l'appareil pour prendre des photos ou scanner des QR codes.
use Symfony\UX\Native\Component\NativeCamera;
#[AsTwigComponent]
class PhotoCapture
{
public function takePhoto(): NativeCamera
{
return new NativeCamera(quality: 80, allowGallery: true);
}
}
NativeGeolocation
Fournit la position GPS de l'appareil en temps réel.
use Symfony\UX\Native\Component\NativeGeolocation;
$location = NativeGeolocation::getCurrentPosition();
echo $location->latitude . ', ' . $location->longitude;
NativeNotification
Envoie des notifications push locales, même lorsque l'application est en arrière-plan.
use Symfony\UX\Native\Notification\NativeNotification;
$notification = new NativeNotification(
title: 'Rappel',
body: 'Votre rendez-vous commence dans 15 minutes.',
badge: 1
);
$notification->schedule(delay: 900); // 15 minutes
NativeStorage
Permet de stocker des données persistantes localement sur l'appareil, en dehors de la base de données.
use Symfony\UX\Native\Storage\NativeStorage;
NativeStorage::set('theme', 'dark');
$theme = NativeStorage::get('theme'); // 'dark'
Symfony UX Native est un projet expérimental au moment de la rédaction de ce cours. L'API peut évoluer entre les versions. Consultez toujours la documentation officielle avant de commencer un projet en production : https://native.symfony.com
Le cycle de développement d'une application Symfony UX Native
Le développement d'une application Symfony UX Native suit un cycle proche de celui d'une application web Symfony classique, avec quelques étapes supplémentaires :
Phase 1 — Développement web On développe et teste l'application dans le navigateur, exactement comme une application Symfony classique. C'est la phase la plus longue et la plus confortable pour un développeur PHP.
Phase 2 — Intégration native
On ajoute les fonctionnalités natives (caméra, GPS, notifications) via les composants Native*. Ces composants se dégradent gracieusement dans le navigateur (ils affichent une interface de simulation).
Phase 3 — Build mobile On compile l'application pour iOS et/ou Android via les commandes CLI de Symfony UX Native. Cette étape nécessite Xcode (macOS, pour iOS) ou Android Studio (pour Android).
Phase 4 — Distribution L'application est publiée sur l'App Store ou le Google Play Store comme n'importe quelle application native.
Pour le développement sur Windows, seul le build Android est possible nativement. Le build iOS nécessite un Mac avec Xcode. Des services de build cloud comme Bitrise ou GitHub Actions permettent de contourner cette limitation.
Comparaison avec les alternatives
| Critère | Symfony UX Native | React Native | Flutter | Ionic |
|---|---|---|---|---|
| Langage principal | PHP | JavaScript | Dart | JavaScript |
| Courbe apprentissage (dev PHP) | Faible | Élevée | Très élevée | Moyenne |
| Performances UI | Moyenne (WebView) | Élevée (natif) | Très élevée (natif) | Moyenne (WebView) |
| Accès APIs natives | Via Native Bridge | Direct | Direct | Via Capacitor |
| Maturité | Expérimental | Mature | Mature | Mature |
| Partage code web/mobile | Très élevé | Moyen | Faible | Élevé |
Les Live Components dans une application mobile
L'un des atouts majeurs de Symfony UX Native est la compatibilité totale avec les Live Components. Un formulaire de recherche avec filtre en temps réel, un panier d'achat réactif, ou un tableau de bord mis à jour automatiquement — tous ces éléments fonctionnent dans une application mobile Symfony UX Native exactement comme dans une application web.
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[AsLiveComponent]
class SearchProduct
{
#[LiveProp(writable: true)]
public string $query = '';
public function getResults(): array
{
// Cette requête Doctrine s'exécute localement sur l'appareil
return $this->productRepository->search($this->query);
}
}
La réactivité des Live Components dans Symfony UX Native ne passe pas par le réseau : tout s'exécute localement sur l'appareil. Les performances sont donc excellentes même sans connexion internet.
Exemple pratique
Création d'une application mobile de liste de tâches
Cet exemple montre comment créer une application mobile simple avec Symfony UX Native : une liste de tâches (todo list) avec ajout, suppression et stockage local.
Pour exécuter cet exemple, Symfony UX Native doit être installé. Le projet de démonstration officiel est disponible sur https://github.com/symfony/ux-native-demo. Si ce dépôt n'est pas disponible, suivre les étapes de création manuelle ci-dessous.
Étape 1 — Créer le projet
cd %USERPROFILE%\Documents
symfony new tp-native-todo --webapp
cd tp-native-todo
composer require symfony/ux-native
code .
Étape 2 — Créer l'entité Tache
php bin/console make:entity Tache
Ajouter les champs :
titre:string, longueur 255, non nullableterminee:boolean, valeur par défautfalsecreeLe:datetime_immutable, non nullable
Étape 3 — Créer la migration et la base de données
Modifier .env pour utiliser SQLite :
DATABASE_URL="sqlite:///%kernel.project_dir%/var/todo.db"
Puis créer la base de données et les tables :
php bin/console doctrine:database:create
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Étape 4 — Créer le Live Component TodoList
Créer src/Twig/Components/TodoList.php :
<?php
namespace App\Twig\Components;
use App\Entity\Tache;
use App\Repository\TacheRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent]
class TodoList
{
use DefaultActionTrait;
#[LiveProp(writable: true)]
public string $nouvelleTache = '';
public function __construct(
private TacheRepository $tacheRepository,
private EntityManagerInterface $em
) {}
public function getTaches(): array
{
return $this->tacheRepository->findBy([], ['creeLe' => 'DESC']);
}
#[LiveAction]
public function ajouter(): void
{
if (trim($this->nouvelleTache) === '') {
return;
}
$tache = new Tache();
$tache->setTitre($this->nouvelleTache);
$tache->setTerminee(false);
$tache->setCreeLe(new \DateTimeImmutable());
$this->em->persist($tache);
$this->em->flush();
$this->nouvelleTache = '';
}
#[LiveAction]
public function basculer(int $id): void
{
$tache = $this->tacheRepository->find($id);
if ($tache) {
$tache->setTerminee(!$tache->isTerminee());
$this->em->flush();
}
}
#[LiveAction]
public function supprimer(int $id): void
{
$tache = $this->tacheRepository->find($id);
if ($tache) {
$this->em->remove($tache);
$this->em->flush();
}
}
}
Étape 5 — Créer le template du composant
Créer templates/components/TodoList.html.twig :
<div {{ attributes }}>
<h2>Ma liste de tâches</h2>
<div style="display: flex; gap: 0.5rem; margin-bottom: 1.5rem;">
<input
type="text"
data-model="nouvelleTache"
placeholder="Nouvelle tâche..."
style="flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;"
>
<button
data-action="live#action"
data-live-action-param="ajouter"
style="padding: 0.5rem 1rem; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
Ajouter
</button>
</div>
{% for tache in this.getTaches() %}
<div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; border-bottom: 1px solid #eee;">
<input
type="checkbox"
{{ tache.terminee ? 'checked' : '' }}
data-action="live#action"
data-live-action-param="basculer"
data-live-id-param="{{ tache.id }}"
style="width: 1.2rem; height: 1.2rem; cursor: pointer;"
>
<span style="{{ tache.terminee ? 'text-decoration: line-through; color: #999;' : '' }} flex: 1;">
{{ tache.titre }}
</span>
<button
data-action="live#action"
data-live-action-param="supprimer"
data-live-id-param="{{ tache.id }}"
style="background: none; border: none; color: #e53e3e; cursor: pointer; font-size: 1.2rem;"
>
×
</button>
</div>
{% else %}
<p style="color: #999; text-align: center; padding: 2rem 0;">Aucune tâche pour le moment.</p>
{% endfor %}
</div>
Étape 6 — Créer la page principale
php bin/console make:controller TodoController
Modifier src/Controller/TodoController.php :
#[Route('/todo', name: 'app_todo')]
public function index(): Response
{
return $this->render('todo/index.html.twig');
}
Modifier templates/todo/index.html.twig :
{% extends 'base.html.twig' %}
{% block title %}Todo List — Symfony UX Native{% endblock %}
{% block body %}
<div style="max-width: 500px; margin: 2rem auto; padding: 0 1rem;">
<h1>Application Todo</h1>
<twig:TodoList />
</div>
{% endblock %}
Étape 7 — Tester en mode web
symfony server:start
Accéder à https://127.0.0.1:8000/todo. L'application fonctionne dans le navigateur : ajout de tâches, cocher/décocher, suppression — tout en temps réel sans rechargement de page grâce aux Live Components.
Cette phase de développement en mode navigateur est la plus importante. Une fois l'application fonctionnelle en mode web, le passage au mode mobile avec Symfony UX Native ne nécessite que peu de modifications supplémentaires.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Dans ce TP, vous allez créer une application de gestion de notes personnelles avec Symfony UX Native. L'application permettra d'ajouter, de modifier le statut et de supprimer des notes, stockées dans une base SQLite locale. Toute l'interactivité sera gérée via un Live Component, sans rechargement de page.
Étape 1 — Créer le projet et configurer SQLite
Ouvrez un terminal PowerShell ou le terminal intégré de VS Code (Ctrl + ù), naviguez vers votre dossier Documents et créez le projet :
cd %USERPROFILE%\Documents
symfony new tp-notes-native --webapp
cd tp-notes-native
code .
Installez ensuite les packages nécessaires pour les Live Components :
composer require symfony/ux-live-component
composer require symfony/ux-twig-component
Puis configurez la base de données SQLite en ouvrant le fichier .env et en remplaçant la ligne DATABASE_URL par :
DATABASE_URL="sqlite:///%kernel.project_dir%/var/notes.db"
Une solution
Vous devez être connecté pour voir le contenu.
Étape 2 — Créer l'entité Note
Générez l'entité Note via la commande MakerBundle :
php bin/console make:entity Note
Ajoutez les champs suivants lors de l'invite interactive :
titre: typestring, longueur255, non nullablecontenu: typetext, non nullableimportante: typeboolean, valeur par défautfalsecreeLe: typedatetime_immutable, non nullable
Une fois l'entité créée, générez la migration et exécutez-la :
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Une solution
Vous devez être connecté pour voir le contenu.
Étape 3 — Créer le Live Component NoteManager
Créez le fichier src/Twig/Components/NoteManager.php. Ce composant doit gérer l'intégralité des interactions avec les notes : affichage de la liste, ajout d'une nouvelle note, basculement du statut "importante", et suppression.
Le composant doit avoir :
- Une propriété
#[LiveProp(writable: true)]$nouveauTitrede typestring - Une propriété
#[LiveProp(writable: true)]$nouveauContenude typestring - Une méthode
getNotes()qui retourne toutes les notes triées par date décroissante - Trois méthodes
#[LiveAction]:ajouter(),basculerImportance(int $id),supprimer(int $id)
Une solution
Vous devez être connecté pour voir le contenu.
Étape 4 — Créer le template du Live Component
Créez le fichier templates/components/NoteManager.html.twig. Ce template doit afficher :
- Un formulaire d'ajout avec deux champs (
nouveauTitreetnouveauContenu) et un bouton d'actionajouter - La liste des notes existantes, chacune avec son titre, son contenu, un bouton pour marquer comme importante (avec un style visuel différent si
importanteest vrai), et un bouton de suppression - Un message "Aucune note" si la liste est vide
Une solution
Vous devez être connecté pour voir le contenu.
Étape 5 — Créer le contrôleur et la page principale
Générez un contrôleur nommé NotesController :
php bin/console make:controller NotesController
Modifiez src/Controller/NotesController.php pour que la route /notes retourne simplement le template notes/index.html.twig sans passer de variables (le composant gère tout lui-même).
Modifiez ensuite templates/notes/index.html.twig pour appeler le composant <twig:NoteManager />.
Une solution
Vous devez être connecté pour voir le contenu.