akyos / native-push
Symfony Notifier push transport sending straight to APNs (iOS) and FCM v1 (Android), with device token storage for any Doctrine entity
Package info
github.com/akyoscommunication/native-push
Type:symfony-bundle
pkg:composer/akyos/native-push
Requires
- php: >=8.2
- ext-openssl: *
- doctrine/doctrine-bundle: ^2.12|^3.0
- doctrine/orm: ^3.0
- symfony/console: ^7.3|^8.0
- symfony/framework-bundle: ^7.3|^8.0
- symfony/http-client: ^7.3|^8.0
- symfony/notifier: ^7.3|^8.0
Requires (Dev)
None
Suggests
- akyos/ux-native-cli: native:init --notification génère les apps iOS/Android qui fournissent le token et ouvrent l’url au toucher
- symfony/security-bundle: Résolveur par défaut : rattache l’appareil à l’utilisateur connecté
Provides
None
Conflicts
None
Replaces
None
README
Envoyez des notifications push à vos apps iOS et Android depuis Symfony, avec un simple PushMessage.
Le bundle stocke les téléphones de vos utilisateurs et choisit tout seul entre APNs (Apple) et FCM (Google). Il appelle leurs API directement, sans service tiers ni dépendance en dehors de Symfony. Quand on touche la notification, l'app peut s'ouvrir sur la page de votre choix.
flowchart LR
subgraph enregistrement [1. Le téléphone s'enregistre]
Page["Page web dans l'app"] -->|"token"| Endpoint["POST /native-push/devices"]
Endpoint --> Device[("PushDevice")]
end
subgraph envoi [2. Symfony envoie]
Code["PushMessage"] --> Transport["Transport native-push"]
Transport --> Device
Transport -->|iOS| Apns[APNs]
Transport -->|Android| Fcm[FCM]
end
Apns --> Phone[Téléphone]
Fcm --> Phone
Phone -->|"toucher"| Url["page url()"]
Loading
Côté mobile, il faut les apps générées par akyos/ux-native-cli avec php bin/console native:init --notification. Ce sont elles qui fournissent le token du téléphone et qui ouvrent la bonne page au toucher.
Sommaire
- Démarrage rapide
- Identifiants iOS et Android
- Enregistrer le téléphone
- Envoyer une notification
- Ce que le bundle gère tout seul
- Commandes
- Dépannage
- Référence de configuration
- Limites
Démarrage rapide
1. Activer le bundle
// config/bundles.php Akyos\NativePushBundle\AkyosNativePushBundle::class => ['all' => true],
2. Importer les routes (endpoint d'enregistrement des téléphones)
# config/routes/akyos_native_push.yaml akyos_native_push: resource: '@AkyosNativePushBundle/config/routes.php'
3. Déclarer le transport Notifier
# config/packages/notifier.yaml framework: notifier: texter_transports: native_push: '%env(NATIVE_PUSH_DSN)%'
# .env NATIVE_PUSH_DSN=native-push://default
4. Créer la table des appareils
php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate
5. Rendre votre entité destinataire
N'importe quelle entité Doctrine fait l'affaire : un User, un Customer, une Team…
use Akyos\NativePushBundle\Model\PushSubjectInterface; use Akyos\NativePushBundle\Model\PushSubjectTrait; #[ORM\Entity] class User implements UserInterface, PushSubjectInterface { use PushSubjectTrait; // fournit getPushSubjectId() à partir de getId() // ... }
6. Vérifier
php bin/console native-push:check
Sans identifiants, la commande affiche « non configuré » pour chaque plateforme. C'est normal : ajoutez-les à l'étape suivante.
Identifiants iOS et Android
Les deux plateformes sont facultatives et indépendantes. Vous pouvez n'activer qu'Android, qu'iOS, ou les deux. Les appareils d'une plateforme non configurée sont ignorés : l'envoi ne plante pas, et un avertissement est écrit dans le log.
Rangez les fichiers de clés dans config/push/, et ajoutez ce dossier au .gitignore.
iOS : clé APNs (.p8)
- Sur developer.apple.com, allez dans Certificates, Identifiers & Profiles → Keys → « + ».
- Cochez Apple Push Notifications service (APNs), validez, puis téléchargez
AuthKey_XXXXXXXXXX.p8. Apple ne le propose qu'une seule fois. - Notez le Key ID (les 10 caractères du nom du fichier) et votre Team ID (rubrique Membership details).
# config/packages/akyos_native_push.yaml akyos_native_push: apns: team_id: 8WS3LS7L99 # exemple key_id: '%env(APNS_KEY_ID)%' private_key: '%kernel.project_dir%/config/push/AuthKey.p8' topic: com.akyos.nativecli # le bundle ID de l'app iOS sandbox: '%kernel.debug%'
sandbox doit correspondre à la façon dont l'app a été installée :
truepour une app lancée depuis Xcode ounative:build(build de développement) ;falsepour TestFlight et l'App Store.
Il faut une Team Apple Developer payante. Une Personal Team gratuite ne peut pas recevoir de push.
Android : compte de service Firebase
- Dans la console Firebase, ouvrez Paramètres du projet → Comptes de service.
- Cliquez sur Générer une nouvelle clé privée et enregistrez le JSON dans
config/push/firebase-service-account.json.
akyos_native_push: fcm: service_account: '%kernel.project_dir%/config/push/firebase-service-account.json'
Ne confondez pas les deux fichiers Firebase :
- le compte de service (ci-dessus) va sur le serveur ;
google-services.jsonva dans l'app Android (native/android/app/), et son nom de package doit être celui deapplication_id.
Relancez php bin/console native-push:check : chaque plateforme configurée doit afficher OK.
Enregistrer le téléphone
L'app mobile donne son token à la page web. Le contrôleur Stimulus native-push l'envoie ensuite au serveur.
1. Charger le contrôleur
// importmap.php '@akyos/native-push' => [ 'path' => '@akyos/native-push/controllers/native_push_controller.js', ],
// assets/stimulus_bootstrap.js import NativePushController from '@akyos/native-push'; app.register('native-push', NativePushController);
2. L'utiliser dans une page
<div data-controller="bridge--notification-token native-push" data-action="bridge--notification-token:retrieved->native-push#register"> <button data-action="bridge--notification-token#get">Activer les notifications</button> <p data-native-push-target="status"></p> </div>
Au clic, le système demande l'autorisation, l'app renvoie le token, et le contrôleur le poste sur POST /native-push/devices. La plateforme est déduite automatiquement du User-Agent de l'app.
3. À qui appartient le téléphone ?
Par défaut, le téléphone est rattaché à l'utilisateur connecté, s'il implémente PushSubjectInterface. Sans utilisateur connecté, l'endpoint répond 401.
Pour un autre choix, implémentez SubjectResolverInterface et déclarez-le :
use Akyos\NativePushBundle\SubjectResolver\SubjectResolverInterface; final class CustomerResolver implements SubjectResolverInterface { public function __construct(private CustomerRepository $customers) {} public function resolve(Request $request): ?PushSubjectInterface { return $this->customers->findOneByCookie($request->cookies->get('customer')); } }
akyos_native_push: subject_resolver: App\Push\CustomerResolver
Désinscrire à la déconnexion : DELETE /native-push/devices avec {"token": "..."}.
Depuis PHP (sans passer par la page) : $pushDeviceManager->register($user, $token, Platform::Ios).
Envoyer une notification
use Akyos\NativePushBundle\Transport\NativePushOptions; use Symfony\Component\Notifier\Message\PushMessage; use Symfony\Component\Notifier\TexterInterface; // Toucher la notification ouvre l'app sur /orders/42 $texter->send(new PushMessage( 'Commande prête', 'Votre commande #42 est prête', NativePushOptions::to($user)->url('/orders/42'), )); // Sans url() : toucher ouvre simplement l'app là où elle en était $texter->send(new PushMessage('Rappel', 'Pensez à valider', NativePushOptions::to($user)));
Ou avec le Notifier et le canal push :
use Akyos\NativePushBundle\Notification\PushNotification; $notifier->send(new PushNotification('Commande prête', 'Votre commande #42 est prête', url: '/orders/42'), $user);
Options de NativePushOptions
| Méthode | Rôle |
|---|---|
to($subject) |
Envoie à tous les téléphones de ce sujet. |
tokens([...], Platform::Ios) |
Envoie à des tokens précis, sans passer par un sujet. |
url('/orders/42') |
Page ouverte au toucher : un chemin, résolu sur l'URL de l'app, ou une URL complète. |
data(['orderId' => 42]) |
Données personnalisées transmises à l'app. |
badge(3) |
Pastille sur l'icône de l'app. |
sound(null) |
Notification silencieuse. Par défaut, le son est default. |
Sur iOS comme sur Android, toucher une notification ouvre toujours l'app : c'est le système qui décide. url() choisit seulement la page affichée.
Ce que le bundle gère tout seul
-
Pas de doublons. Un token n'existe qu'une fois en base. Si un téléphone s'enregistre pour un autre compte, il est déplacé vers ce compte au lieu d'être dupliqué.
-
Nettoyage des téléphones morts. Quand Apple ou Google répond que le token n'existe plus (app désinstallée…), l'appareil est supprimé :
- chez Apple, la réponse
410 Unregistered; - chez Google,
UNREGISTEREDou « invalid registration token ».
La réponse APNs
BadDeviceTokenn'efface rien : elle arrive aussi quandsandboxest mal réglé, et on ne veut pas vider la base à cause d'une erreur de config. - chez Apple, la réponse
-
Choix de la plateforme. Chaque téléphone part vers APNs ou FCM selon sa plateforme. Une plateforme non configurée est ignorée.
-
Envoi en parallèle. Tous les téléphones d'un sujet sont contactés en même temps.
-
Pas de plantage inutile. Un sujet sans téléphone n'est pas une erreur. Une exception n'est levée que si tous les envois tentés ont échoué.
-
Compatible Messenger. On peut router
PushMessagevers un transport asynchrone, car les options ne contiennent que des valeurs simples. -
Jetons d'authentification en cache. Le JWT APNs est gardé 50 minutes et le jeton OAuth Google 55 minutes, puis ils sont renouvelés.
Commandes
| Commande | À quoi elle sert |
|---|---|
native-push:check |
Vérifie les identifiants de chaque plateforme, sans rien envoyer. |
native-push:devices [--subject='App\Entity\User#1'] |
Liste les téléphones enregistrés. |
native-push:send 'App\Entity\User' 1 [--title] [--body] [--url] [--data=cle=valeur] |
Envoie à tous les téléphones d'un sujet, comme le ferait votre code. |
native-push:test <token> [--platform=ios|android] [--title] [--body] [--url] |
Envoie à un token brut et affiche la réponse brute d'Apple ou de Google (pratique pour déboguer). |
Tester de bout en bout
php bin/console native-push:check # 1. les identifiants sont OK # 2. dans l'app, touchez « Activer les notifications » php bin/console native-push:devices # 3. le téléphone apparaît # 4. mettez l'app en arrière-plan, puis : php bin/console native-push:send 'App\Entity\User' 1 --url=/profile
Touchez la notification : l'app s'ouvre sur /profile.
Dépannage
| Symptôme | Cause | Solution |
|---|---|---|
| Log « Push ignoré : android non configuré » | La section fcm (ou apns) est absente. |
Ajoutez-la (Identifiants). |
| « Aucun appareil joignable » | Le sujet n'a pas de téléphone enregistré, ou sa plateforme n'est pas configurée. | Vérifiez avec native-push:devices et native-push:check. |
APNs BadDeviceToken |
Token de build de dev envoyé en production, ou l'inverse. | Réglez apns.sandbox : true pour Xcode, false pour TestFlight ou App Store. |
APNs DeviceTokenNotForTopic |
apns.topic ne correspond pas au bundle ID de l'app. |
Mettez le même bundle ID que dans native.yaml. |
| iOS : aucun token, rien ne se passe | Simulateur, ou app signée sans Team payante. | Testez sur un vrai iPhone, signé avec une Team Apple Developer payante. |
Xcode : No Account for Team |
Le compte de cette Team n'est pas chargé dans Xcode, ou ses conditions Apple ne sont pas acceptées. | Dans Xcode → Settings → Accounts, la Team doit apparaître sans croix rouge. |
| Android : build en échec « No matching client found for package name » | google-services.json a été créé pour un autre application_id. |
Ajoutez une app Android avec le bon package dans Firebase, puis téléchargez son google-services.json. |
| Android : aucun token | google-services.json manquant dans native/android/app/. |
Ajoutez-le, puis rebuildez. |
Texter::send() renvoie toujours null |
Normal quand Messenger est installé : le Texter confie le message au bus. | Utilisez native-push:send ou le service texter.transports pour voir le résultat réel. |
Endpoint en 401 |
Aucun utilisateur connecté, ou l'utilisateur n'implémente pas PushSubjectInterface. |
Connectez-vous, ou configurez un subject_resolver. |
Endpoint en 403 |
Requête qui n'est pas en JSON ou qui vient d'un autre domaine. | Utilisez le contrôleur native-push, qui envoie la bonne requête. |
Référence de configuration
# config/packages/akyos_native_push.yaml akyos_native_push: # iOS, facultatif. Si la section est présente, toutes ses clés sont obligatoires. apns: team_id: XXXXXXXXXX # Team ID Apple key_id: XXXXXXXXXX # Key ID de la clé APNs private_key: '...' # chemin du .p8, ou son contenu PEM (pratique avec %env()%) topic: com.exemple.app # bundle ID de l'app iOS sandbox: true # true = api.sandbox.push.apple.com (builds de dev) # Android, facultatif. fcm: service_account: '...' # chemin du JSON du compte de service, ou son contenu # Qui possède le téléphone qui s'enregistre (service qui implémente SubjectResolverInterface). subject_resolver: Akyos\NativePushBundle\SubjectResolver\SecurityUserResolver
Un fichier de clé introuvable ne bloque pas le démarrage de l'app : l'erreur apparaît au premier envoi et dans native-push:check.
Limites
- Un seul environnement APNs par app. Avec
sandbox, on ne peut pas mélanger des builds Xcode et des builds TestFlight sur le même serveur. - Renouvellement du token. Quand Firebase renouvelle le token d'un téléphone, le nouveau n'est envoyé au serveur que la prochaine fois qu'une page appelle
bridge--notification-token#get. - Push iOS. Il faut une Team Apple Developer payante, une app signée et un vrai iPhone : le simulateur ne reçoit pas de token.
Tests
php bin/phpunit lib/akyos/AkyosNativePushBundle/tests
Ils couvrent :
- les doublons et la réattribution des tokens, sur SQLite en mémoire ;
- le transport : plateforme ignorée, tokens morts supprimés ;
- les payloads APNs et FCM, et les signatures JWT, vérifiées avec
openssl_verify.