laulamanapps / apple-passbook-bundle
Generate Apple Wallet passes (PassKit) from your Symfony application
Package info
github.com/LauLamanApps/apple-passbook-bundle
pkg:composer/laulamanapps/apple-passbook-bundle
Requires
- php: ^8.1
- ext-openssl: *
- laulamanapps/apple-passbook: ^2.0
- symfony/event-dispatcher: ^6.4|^7.0|^8.0
- symfony/framework-bundle: ^6.4|^7.0|^8.0
- symfony/http-foundation: ^6.4|^7.0|^8.0
- symfony/routing: ^6.4|^7.0|^8.0
Requires (Dev)
- ext-zip: *
- matthiasnoback/symfony-config-test: ^4.3|^5.0
- phpunit/phpunit: ^10.0|^11.0
- symfony/browser-kit: ^6.4|^7.0|^8.0
- symfony/yaml: ^6.4|^7.0|^8.0
This package is auto-updated.
Last update: 2026-07-08 21:38:57 UTC
README
This package provides Symfony integration for the LauLamanApps Apple Passbook Package.
Requirements
- PHP 8.1+
- Symfony 6.4, 7.x, or 8.x
Installation
composer require laulamanapps/apple-passbook-bundle
Run Tests
make tests-unit make tests-functional
Get Certificate
Head over to the Apple Developer Portal to get yourself a certificate to sign your passbooks with.
Export the certificate and key to a .p12 file using Keychain Access.
Configure Bundle
# config/packages/laulamanapps_apple_passbook.yaml laulamanapps_apple_passbook: certificate: '%env(APPLE_PASSBOOK_CERTIFICATE)%' password: '%env(APPLE_PASSBOOK_CERTIFICATE_PASSWORD)%' team_identifier: '%env(APPLE_PASSBOOK_TEAM_IDENTIFIER)%' pass_type_identifier: '%env(APPLE_PASSBOOK_PASS_TYPE_IDENTIFIER)%' environment: 'production' # or 'sandbox'
Add the ENV variables to the .env file:
###> laulamanapps/apple-passbook-bundle ### APPLE_PASSBOOK_CERTIFICATE=config/certificates/pass.p12 APPLE_PASSBOOK_CERTIFICATE_PASSWORD=password APPLE_PASSBOOK_PASS_TYPE_IDENTIFIER=pass.com.your.pass.identifier APPLE_PASSBOOK_TEAM_IDENTIFIER=YOUR_TEAM_ID APPLE_PASSBOOK_WEB_SERVICE_URL='https://example.com/' ###< laulamanapps/apple-passbook-bundle ###
Security note: always reference the certificate password through
%env(...)%as shown above. A literal password in the bundle configuration would be written into Symfony's compiled container invar/cache.
Configuration reference
| Key | Required | Default | Description |
|---|---|---|---|
certificate |
yes | — | Path to the .p12 or .pem certificate file |
password |
no | null |
Certificate password (required for .p12 files) |
team_identifier |
no | null |
Apple Team Identifier |
pass_type_identifier |
no | null |
Pass Type Identifier |
environment |
no | 'production' |
APNs environment: 'production' or 'sandbox' |
Create & Compile Passbook
namespace App\Controller; use LauLamanApps\ApplePassbook\Build\Compiler; use LauLamanApps\ApplePassbook\GenericPassbook; use LauLamanApps\ApplePassbook\MetaData\Barcode; use LauLamanApps\ApplePassbook\Style\BarcodeFormat; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; final class PassbookController { public function __construct( private readonly Compiler $passbookCompiler, ) { } #[Route('/download/passbook/', name: 'download_passbook')] public function download(): Response { $passbook = new GenericPassbook('8j23fm3'); $passbook->setTeamIdentifier('<TeamId>'); $passbook->setPassTypeIdentifier('<PassTypeId>'); $passbook->setOrganizationName('Toy Town'); $passbook->setDescription('Toy Town Membership'); $barcode = new Barcode(); $barcode->setFormat(BarcodeFormat::Pdf417); $barcode->setMessage('123456789'); $passbook->setBarcode($barcode); $data = $this->passbookCompiler->compile($passbook); $response = new Response($data); $response->headers->set('Content-Description', 'File Transfer'); $response->headers->set('Content-Type', 'application/vnd.apple.pkpass'); $response->headers->set('Content-Disposition', 'filename="passbook.pkpass"'); return $response; } }
Push Notifications
The bundle registers the Notifier service from the core library for sending push notifications to devices when a pass is updated:
use LauLamanApps\ApplePassbook\Build\Notifier; final class PassUpdateService { public function __construct( private readonly Notifier $notifier, ) { } public function notifyDevice(string $pushToken): void { $this->notifier->notify($pushToken); } }
The notifier uses Apple's HTTP/2 APNs API. It uses the same certificate configured for the bundle. Set environment: 'sandbox' in the bundle configuration when testing against the APNs sandbox.
Built-in Web Service Controllers
This package comes with built-in controllers for all Apple PassKit Web Service endpoints.
It uses Symfony's EventDispatcher to delegate request handling to your application.
Enable routes
Add the following to your route configuration:
// config/routes/apple_passbook.php use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return function (RoutingConfigurator $routes): void { $routes->import('@ApplePassbookBundle/Resources/config/routes.php'); };
Or in YAML:
# config/routes/apple_passbook.yaml apple_passbook: resource: '@ApplePassbookBundle/Resources/config/routes.php'
Events
The controllers dispatch events that you handle by implementing listeners or subscribers. Each event starts with Status::Unhandled — your listener must set the appropriate status.
| Event | Dispatched when |
|---|---|
DeviceRegisteredEvent |
A device registers for pass updates |
DeviceUnregisteredEvent |
A device unregisters from pass updates |
DeviceRequestUpdatedPassesEvent |
A device requests a list of updated pass serial numbers |
RetrieveUpdatedPassbookEvent |
A device requests an updated pass |
Logging endpoint
Apple devices POST diagnostic messages to the /v1/log endpoint. The bundle's LogController handles these automatically and writes them to the PSR LoggerInterface (Symfony's logger service) at info level — no event or listener is required.
Example subscriber
namespace App\EventSubscriber; use DateTimeImmutable; use LauLamanApps\ApplePassbook\GenericPassbook; use LauLamanApps\ApplePassbookBundle\Event\DeviceRegisteredEvent; use LauLamanApps\ApplePassbookBundle\Event\DeviceRequestUpdatedPassesEvent; use LauLamanApps\ApplePassbookBundle\Event\DeviceUnregisteredEvent; use LauLamanApps\ApplePassbookBundle\Event\RetrieveUpdatedPassbookEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; final class ApplePassbookSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ DeviceRegisteredEvent::class => 'onDeviceRegistered', DeviceRequestUpdatedPassesEvent::class => 'onDeviceRequestUpdatedPasses', RetrieveUpdatedPassbookEvent::class => 'onRetrieveUpdatedPassbook', DeviceUnregisteredEvent::class => 'onDeviceUnregistered', ]; } public function onDeviceRegistered(DeviceRegisteredEvent $event): void { $passbook = $this->passbookRepository->getBySerial($event->getSerialNumber()); // isAuthenticatedBy() uses hash_equals() internally: a timing-safe comparison. // Never compare authentication tokens with === or !==. if (!$event->isAuthenticatedBy($passbook->getAuthToken())) { $event->notAuthorized(); return; } // Save device registration to database $event->deviceRegistered(); } public function onDeviceRequestUpdatedPasses(DeviceRequestUpdatedPassesEvent $event): void { $passbooks = $this->passbookRepository->getSerialsSince( $event->getPassTypeIdentifier(), $event->getDeviceLibraryIdentifier(), $event->getPassesUpdatedSince(), ); if ($passbooks) { $serials = []; foreach ($passbooks as $passbook) { $serials[] = $passbook->getSerialNumber(); } $event->setSerialNumbers($serials, new DateTimeImmutable()); return; } $event->notFound(); } public function onRetrieveUpdatedPassbook(RetrieveUpdatedPassbookEvent $event): void { $entity = $this->passbookRepository->findBySerial($event->getSerialNumber()); if ($entity === null) { $event->notFound(); return; } if (!$event->isAuthenticatedBy($entity->getAuthToken())) { $event->notAuthorized(); return; } if ($event->getUpdatedSince() && $entity->getUpdatedAt() < $event->getUpdatedSince()) { $event->notModified(); return; } $passbook = new GenericPassbook($event->getSerialNumber()); // Build the passbook... $event->setPassbook($passbook, $entity->getUpdatedAt()); } public function onDeviceUnregistered(DeviceUnregisteredEvent $event): void { $passbook = $this->passbookRepository->getBySerial($event->getSerialNumber()); if (!$event->isAuthenticatedBy($passbook->getAuthToken())) { $event->notAuthorized(); return; } // Remove device registration from database $event->deviceUnregistered(); } }
Upgrading
See UPGRADE.md for the migration guide from 1.x to 2.0 (PHP 8.1+, Symfony 6.4+, native Status enum, moved Notifier, PHP-based route/service config) and CHANGELOG.md for the full list of changes.
Credits
This package has been developed by LauLaman.