iabduul7 / laravel-themepark-booking-adapters
Self-contained, drop-in Laravel booking adapters for Walt Disney World & SeaWorld/United Parks (Redeam) and Universal Orlando (SmartOrder): catalog, pricing, availability, holds/bookings, orders and voucher data.
Package info
github.com/iabduul7/laravel-themepark-booking-adapters
pkg:composer/iabduul7/laravel-themepark-booking-adapters
Fund package maintenance!
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.5
- illuminate/contracts: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.15
Requires (Dev)
- brianium/paratest: ^7.4
- larastan/larastan: ^3.0
- laravel/pint: ^1.13
- mockery/mockery: ^1.6
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^10.0|^11.0
- phpstan/extension-installer: ^1.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.5.3|^12.0
- spatie/laravel-ray: ^1.32
README
A Laravel package providing self-contained, drop-in booking adapters for major theme park distribution APIs:
| Park | Provider | Adapter |
|---|---|---|
| Walt Disney World | Redeam | DisneyRedeamAdapter |
| SeaWorld / United Parks | Redeam | SeaWorldRedeamAdapter |
| Universal Orlando | SmartOrder ("SmartOrder2") | UniversalSmartOrder2Adapter |
Each adapter exposes the provider's real API surface (catalog, rates, availability, pricing schedules, holds/bookings for Redeam; events/orders for SmartOrder) and returns lightweight DTOs over the raw responses. Auth, retries and OAuth token management are handled for you.
What's working today: all three adapters are contract-tested with
Http::fakeand analysed with PHPStan (level 3). Read paths (catalog, rates, pricing, availability) are additionally verified against the live provider sandboxes, and the full write lifecycle is proven live for Disney (Redeam hold → book → cancel) and Universal (SmartOrder place → cancel). SeaWorld / United Parks is contract-tested only — its sandbox (Discovery Cove) exposes no bookable availability.
Features
- 🎢 Three production parks supported: Disney (Redeam), SeaWorld/United Parks (Redeam), Universal (SmartOrder)
- 🧩 Driver-based resolution via a Laravel
Manager+ThemeParkfacade - 🔐 Auth handled: Redeam
X-API-Key/X-API-Secret; SmartOrder OAuth2 client-credentials with token caching and 401 self-heal - ♻️ Resilient transport: idempotent reads retried on connection drops / 5xx; writes never retried (no double-booking)
- 📦 Typed result objects:
Supplier,Product,Rate,PriceSchedule,RatePriceSchedule,Availability,Hold,Booking, … - 🎫 Voucher data exposed:
tickets()normalises each booking/order response into typedTicketArtifacts (redeemable identifier + format + validity); barcode/PDF rendering stays in your app - 🧩 Capability interfaces:
SupportsHolds,SupportsEvents,ProvidesTicketArtifacts— type-hint a capability instead of a concrete park - 🧱 No app coupling: pure API integration; persistence, jobs, commission/margins and voucher rendering stay in your app
- 🧪 Contract-tested with
Http::fake(), analysed with PHPStan, and exercised live against the provider sandboxes
Requirements
- PHP 8.2, 8.3 or 8.4
- Laravel 12.x or 13.x (
illuminate/contracts ^12.0|^13.0)
Earlier releases targeted PHP 8.1 / Laravel 10–11. Support for those was dropped in 3.0.0 to stay on the security-patched Laravel line (≥ 12.60 / ≥ 13.10); see the changelog.
Installation
composer require iabduul7/laravel-themepark-booking-adapters
Publish the config file:
php artisan vendor:publish --tag="themepark-adapters-config"
Add credentials to your .env (see .env.example for the full list):
THEMEPARK_DEFAULT_PROVIDER=disney # Disney (Redeam) REDEAM_DISNEY_SUPPLIER_ID=your_disney_supplier_id REDEAM_DISNEY_API_KEY=your_disney_api_key REDEAM_DISNEY_API_SECRET=your_disney_api_secret # SeaWorld / United Parks (Redeam) — supplier is passed per call REDEAM_UNITED_PARKS_API_KEY=your_seaworld_api_key REDEAM_UNITED_PARKS_API_SECRET=your_seaworld_api_secret # Universal (SmartOrder) SMARTORDER_CUSTOMER_ID=134853 SMARTORDER_APPROVED_SUFFIX=-2KNOW SMARTORDER_CLIENT_USERNAME=your_client_username SMARTORDER_CLIENT_SECRET=your_client_secret
Usage
Resolving an adapter
use Iabduul7\ThemeParkAdapters\Facades\ThemePark; $disney = ThemePark::provider('disney'); // DisneyRedeamAdapter $seaworld = ThemePark::provider('seaworld'); // SeaWorldRedeamAdapter $universal = ThemePark::provider('universal'); // UniversalSmartOrder2Adapter
You can also resolve via the container (app('themepark') / ThemeParkManager) or construct an
adapter directly with a config array:
use Iabduul7\ThemeParkAdapters\Providers\Disney\DisneyRedeamAdapter; $disney = new DisneyRedeamAdapter(config('themepark-adapters.providers.disney'));
Disney (Redeam) — supplier fixed from config
$products = $disney->getAllProducts(); // Product[] $product = $disney->getProduct('PRODUCT_ID'); // Product $rates = $disney->getProductRates('PRODUCT_ID'); // Rate[] $schedule = $disney->getProductPricingSchedule('PRODUCT_ID', '2026-06-01', '2026-06-30'); // PriceSchedule $rateSched = $disney->getProductRatePricingSchedule('PRODUCT_ID', '2026-06-01', '2026-06-30', 'RATE_ID'); $avail = $disney->checkAvailabilities('PRODUCT_ID', '2026-06-01', '2026-06-30'); // Hold → book → cancel $hold = $disney->createNewHold(['hold' => ['items' => [/* … */]]]); $booking = $disney->createNewBooking(['booking' => [/* … */]]); $disney->deleteBooking('BOOKING_ID'); echo $product->getName(); echo $product->getId();
SeaWorld / United Parks (Redeam) — supplier passed per call
$products = $seaworld->getAllProducts('SUPPLIER_ID'); $product = $seaworld->getProduct('SUPPLIER_ID', 'PRODUCT_ID'); $rates = $seaworld->getProductRates('SUPPLIER_ID', 'PRODUCT_ID'); $schedule = $seaworld->getProductPricingSchedule('SUPPLIER_ID', 'PRODUCT_ID', '2026-06-01', '2026-06-30');
Universal (SmartOrder)
$catalog = $universal->getAllProducts(); // GET smartorder/MyProductCatalog $months = $universal->getAvailableMonths(); // next 12 months $events = $universal->findEvents([/* plu, dates, … */]); // POST smartorder/FindEvents $order = $universal->placeOrder([/* order lines, … */]);// POST smartorder/PlaceOrder if ($universal->canCancelOrder(['ExternalOrderId' => 'E1'])) { $universal->cancelOrder(['ExternalOrderId' => 'E1']); }
The adapters mirror the method names and signatures of the production
LaravelRedeamForWaltDisney,LaravelRedeamForUnitedParksandSmartOrderClientclients so they can serve as a drop-in replacement. A normalised, provider-agnostic interface is proposed for a future major version.
Capability interfaces
Contracts\Capabilities\SupportsHolds—createNewHold,getHold,deleteHold,createNewBooking,getBooking,deleteBooking(Redeam adapters)Contracts\Capabilities\SupportsEvents—findEvents,placeOrder,getExistingOrder,canCancelOrder,cancelOrder(SmartOrder adapter)
Type-hint these when you only need a capability rather than a specific park.
Optional building blocks
Business logic that is specific to a deployment is kept out of the core adapters and shipped as
opt-in helpers under Support/:
Support\Redeam\OptionCodeResolver— maps a Walt Disney World ticket name to its Redeam option code (also exposed on the Redeam adapters asgetOptionCode()for drop-in parity).
Persistence (Eloquent models, migrations), queue/sync jobs, commission/operator margins and voucher rendering (barcode images,
templates, PDF, delivery) are intentionally left to the consuming application. The package does expose
the provider-native voucher data — tickets() normalises each booking/order response into typed
TicketArtifacts (the redeemable identifier + format + validity).
Authentication
- Redeam —
X-API-Key/X-API-Secretheaders; GET is form-encoded, writes are JSON. - SmartOrder — OAuth2 client-credentials (
/connect/token,scope=SmartOrder); bearer token cached via aTokenRepository(setSMARTORDER_TOKEN_CACHE=falseto always refresh, matching upstream);customerIdinjected into every request; transparent refresh-and-retry on401.
Error handling
Any non-2xx provider response is raised as a ThemeParkApiException — reads and writes alike (a
failure is never silently returned as an empty array). The exception carries the HTTP status via
getCode() and the decoded provider error body via getResponseData(). Single-entity lookups raise
a dedicated 404: getProduct() throws productNotFound(), getExistingOrder() throws
orderNotFound().
use Iabduul7\ThemeParkAdapters\Exceptions\ThemeParkApiException; try { $product = ThemePark::provider('disney')->getProduct('PRODUCT_ID'); } catch (ThemeParkApiException $e) { $status = $e->getCode(); // e.g. 404, 422, 503 $body = $e->getResponseData(); // decoded provider error payload (nullable) report($e); }
Idempotent reads are retried on connection drops / 5xx before the failure surfaces; writes are never retried (no double-booking).
Development
composer install
Testing
# Adapter contract tests (Http::fake) composer test:adapters # Static analysis composer analyse # Code style composer format # fix composer format:check # check only
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Credits
License
The MIT License (MIT). Please see License File for more information.