Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Passbook Bundle Laravel Package

eo/passbook-bundle

Symfony2 bundle that integrates the php-passbook library to generate signed .pkpass files for Apple Wallet. Configure your pass type and team identifiers, certificates, and output path, then create passes from your app.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. eo/passbook-bundle is a Symfony 2.1+ bundle (Eo\PassbookBundle\EoPassbookBundle) registered through AppKernel::registerBundles(). There is no service provider, no facade, no publishable config — it will not install or boot in a Laravel app. Do not run composer require eo/passbook-bundle and expect progress; the Symfony 2.x constraints will either fail resolution against a modern symfony/* tree or require --ignore-platform-reqs hacks you should never ship.

What you actually want is the thing the bundle wraps: eymengunay/php-passbook, which does the .pkpass assembly, manifest hashing, and PKCS#7 signing. The bundle is a thin DI wrapper around a pass_factory service. In Laravel you rebuild that wrapper yourself — roughly 60 lines of service provider — and skip Symfony entirely.

Where to look first, in order:

  1. The eymengunay/php-passbook documentation and API docs (linked from the bundle README), not the bundle README. The bundle README is only a config table plus "see php-passbook documentation."
  2. The bundle's Resources/config/services.xml on GitHub — it shows exactly how pass_factory is constructed (which constructor args are certs, password, identifiers, output path). That's your porting blueprint.
  3. Apple's PassKit Package Format Reference — because the library's assumptions about manifest/signature/zip layout are the part most likely to have drifted since 2018.

Provision certificates before writing code — this is the long pole, not the integration:

  • An Apple Developer Pass Type ID and its .p12 (with password).
  • Apple's WWDR G4 intermediate (valid to 2030). Older WWDR generations expired in 2023; if the library or its docs reference an older intermediate, the signature will fail silently on device.
  • An icon.png (29×29) and [email protected] (58×58). These are mandatory even when the docs make them sound optional.

Store the .p12 and password outside the repo — storage/app/private/passbook/ in local dev, a secrets manager in production.

Config file (config/wallet.php) — mirror the bundle's config keys so the mapping is obvious:

return [
    'pass_type_identifier' => env('PASSBOOK_PASS_TYPE_ID'),
    'team_identifier'      => env('PASSBOOK_TEAM_ID'),
    'organization_name'    => env('PASSBOOK_ORG_NAME'),
    'p12_path'             => env('PASSBOOK_P12_PATH'),
    'p12_password'         => env('PASSBOOK_P12_PASSWORD'),
    'wwdr_path'            => env('PASSBOOK_WWDR_PATH'),
    'output_path'          => storage_path('app/passes'),
    'icon_path'            => resource_path('passbook/icon.png'),
];

First use case worth shipping: a static coupon or membership card generated in a queued job, stored in S3, and delivered by a signed temporary URL. No Wallet web service, no APNs, no device registration table — generation only. That is the smallest thing that proves the pipeline works end to end.

Implementation Patterns

Build a seam immediately. Wrap everything behind an interface so the 2018 library is swappable:

interface WalletPassGenerator
{
    public function generate(PassDefinition $pass): GeneratedPass;
}

PassDefinition is your own DTO (serial number, description, fields, barcode, colors, relevant date, assets). GeneratedPass carries the bytes plus the serial number. The php-passbook adapter implements the interface; a future port or a maintained library replaces one class.

Service provider binds the adapter and nothing else resolves the library directly:

$this->app->singleton(WalletPassGenerator::class, function ($app) {
    return new PhpPassbookGenerator($app['config']['wallet']);
});

Generate in a job for anything bulk. Building 10k passes synchronously in a request is a timeout waiting to happen (zip + RSA sign per pass). Queue it, write to S3 or storage/app/passes, persist the serial number.

Persist serial numbers and pass metadata from day one — passes table with serial_number, pass_type, user_id, generated_at, last_updated_at. Even if you ship generation-only, this is the table that lets you add updates later without a migration scramble. serialNumber must be unique per pass and is how Wallet identifies it forever.

Delivery pattern — stream, don't redirect:

return response()->streamDownload(
    fn () => print($bytes),
    "{$serial}.pkpass",
    [
        'Content-Type'        => 'application/vnd.apple.pkpass',
        'Content-Disposition' => 'attachment; filename="pass.pkpass"',
        'Cache-Control'       => 'no-store, no-cache, must-revalidate',
    ]
);

iOS ignores the response entirely if the MIME type isn't application/vnd.apple.pkpass, so set it explicitly even though streamDownload guesses.

Keep pass JSON as a plain array built from your DTO, then hand it to the generator. Don't let the library's object model leak into controllers — you want pass.json shape to be reviewable in a diff.

Testing. Golden-file tests are the only reliable ones: generate a pass with fixed inputs, unzip it, assert manifest.json hashes match each file, and verify the signature with OpenSSL rather than trusting the library:

openssl smime -verify -in signature -inform DER \
  -content manifest.json -noverify

Add a second test that shells out to unzip -l and asserts the archive contains exactly pass.json, icon.png, [email protected], manifest.json, signature — no __MACOSX, no .DS_Store, no nested folder.

If you port the signing yourself (likely, given the library's age): build the zip with ZipArchive with all files at the archive root, compute SHA-1 of each file into manifest.json, write the manifest, then produce a detached PKCS#7 signature over manifest.json using openssl_pkcs7_sign with the WWDR intermediate included as an extra cert, output as DER (not PEM, not base64-armored).

Gotchas and Tips

  • Composer will fight you. A 2018 Symfony bundle against Laravel 10/11 means PHP 8.x and modern symfony/* constraints conflict. Requiring the sibling library directly is easier but still dormant. Budget time for a fork or a vendored copy — and never resolve it with --ignore-platform-reqs in production.
  • manifest.json must list every file except manifest.json and signature. Forgetting an asset, or including the manifest itself, produces a pass that installs on the simulator but is rejected on device with no useful error.
  • The signature is detached PKCS#7 with the WWDR cert embedded. openssl_pkcs7_sign omits the intermediate unless you pass the extra-certs file. Without the WWDR G4 in the chain, iOS refuses the pass — again, silently.
  • Zip layout is strict. All files at archive root, no parent directory, no hidden files. ZipArchive::addFile preserves paths inconsistently; always addFromString with a bare filename.
  • icon.png is mandatory, logo.png is not. Apple's docs bury this; a pass without an icon fails to display.
  • formatVersion must be the number 1, not the string "1".
  • Certificate expiry is the silent killer. An expired .p12 or WWDR chain does not throw at generation time in a way that alerts you — passes just stop installing or stop updating. Add a scheduled command that parses the cert's validTo daily and alerts at 60/30/7 days. This is the single highest-value piece of operational code you'll write around this package.
  • APNs is likely dead in this library. Apple retired the legacy binary provider protocol for HTTP/2. If you eventually add pass updates, write the HTTP/2 push sender yourself; don't inherit it from a 2018 dependency.
  • The bundle's output_path config is a footgun. It writes generated passes to disk by default. In Laravel, prefer generating to memory and streaming, or to a temp path you clean up in a finally.
  • Debugging workflow: unzip the .pkpass manually, diff pass.json against expectations, re-verify manifest.json hashes with sha1sum, then verify the signature with the OpenSSL command above. If all three pass and the device still rejects it, it's the certificate chain.
  • Extension points, in priority order: (1) the WalletPassGenerator interface, (2) the passes table with serial numbers, (3) an WalletPassUpdater interface you stub now and implement later. Those three decisions cost almost nothing today and make the difference between a prototype you can grow and a rewrite.
  • Treat this as a reference implementation, not a dependency. Read the bundle's services.xml and the library's signing code, port the ~200 meaningful lines into a Laravel-native service, and own it. Given 10 stars, zero dependents, and no release since January 2018, you are the maintainer the moment it enters your composer.json.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel