디즈니, 씨월드, 유니버설의 예약 API를 하나의 라라벨 패키지로 포장했습니다

작성자

카테고리:

← 피드로
DEV Community · Abdullah · 2026-09-06 개발(SW)
Cover image for I Wrapped Disney, SeaWorld and Universal's Booking APIs in One Laravel Package

Abdullah

TL;DR: iabduul7/laravel-themepark-booking-adapters gives you drop-in Laravel adapters for Disney and SeaWorld (via Redeam) and Universal Orlando (via SmartOrder). Auth, retries and OAuth tokens are handled. Below are the three design decisions I’d reuse in any API-wrapper package.

The problem

If you sell theme park tickets online, you don’t talk to Disney. You talk to a distributor called Redeam. SeaWorld is also on Redeam, but with a different supplier model. Universal runs its own OAuth2 API called SmartOrder.

Three parks, two vendors, three ideas of what a “product” or a “ticket” is. For two years those integrations lived as private client classes inside a Laravel storefront I maintain. Every fix had to be made three times.

So I extracted them into a package. It’s a pure API layer: it talks to the provider and gives you typed DTOs back. Models, queues, margins and voucher PDFs stay in your app.

use Iabduul7\ThemeParkAdapters\Facades\ThemePark;

$disney = ThemePark::provider('disney');

$products = $disney->getAllProducts();                                   // Product[]
$avail    = $disney->checkAvailabilities('PRODUCT_ID', '2026-06-01', '2026-06-30');

$hold    = $disney->createNewHold([...]);
$booking = $disney->createNewBooking([...]);

Enter fullscreen mode Exit fullscreen mode

Resolution uses Laravel’s own Manager class, the same thing behind Cache::store(), so there’s nothing new to learn.

Rule 1: Don’t unify APIs that aren’t unified

My first attempt was one big BookingProviderInterface. It fell apart fast:

  • Disney’s supplier ID is fixed in config.
  • SeaWorld needs a supplier ID on every call.
  • Universal has no concept of a hold at all.

Forcing those into one signature means arguments that two of three adapters silently ignore. Instead, the shared contract has two methods, and everything else is a capability interface:

interface SupportsHolds   { /* createNewHold, createNewBooking, deleteBooking … */ }
interface SupportsEvents  { /* findEvents, placeOrder, cancelOrder … */ }
interface ProvidesTicketArtifacts { public function tickets(?array $response): Collection; }

Enter fullscreen mode Exit fullscreen mode

Redeam adapters implement SupportsHolds. SmartOrder implements SupportsEvents. All three implement ProvidesTicketArtifacts. In your app you type-hint the capability, not the park:

public function __construct(private SupportsHolds $provider) {}

Enter fullscreen mode Exit fullscreen mode

No more “this method exists but throws NotSupported”.

Rule 2: Retry reads. Never retry writes.

Retrying a GET /products that timed out is free. Retrying a POST /bookings that timed out can charge a customer twice for non-refundable tickets.

The base adapter makes the rule impossible to miss by naming the helper after what it’s for:

/**
 * Use for idempotent reads ONLY — writes must never be
 * retried or a hold/booking/order could be duplicated.
 */
protected function retryReads(PendingRequest $request): PendingRequest
{
    return $request->retry(3, 1000, function ($exception) {
        return $exception instanceof ConnectionException
            || ($exception instanceof RequestException && $exception->response->serverError());
    }, throw: false);
}

Enter fullscreen mode Exit fullscreen mode

Writes go straight through with no retry. Any non-2xx becomes a ThemeParkApiException with the HTTP status in getCode() and the provider’s error body in getResponseData(). A persistent 5xx is an exception, never a silent empty array.

Rule 3: Check what serialize() does before someone queues your object

Every read returns a DTO like Product or Booking. Some keep a back-reference to the adapter so $product->getRates() can lazily hit the API.

Convenient. Also dangerous. The adapter holds your API secrets, and the first thing a consuming app does with a Product is dispatch it to a queue job. Laravel serializes the job, the job contains the DTO, the DTO contains the adapter, the adapter contains the credentials. Your API key is now sitting in Redis in plain text.

The fix is five lines in the base DTO:

public function __serialize(): array
{
    return ['data' => $this->data];   // adapter reference dropped
}

public function __unserialize(array $data): void
{
    $this->data = $data['data'] ?? [];
    $this->adapter = null;
}

Enter fullscreen mode Exit fullscreen mode

There’s a test that serializes a Product and asserts the payload contains no secret. I hadn’t thought about this until I saw a queue payload. Go check your own packages.

Bonus: OAuth that heals itself

Universal’s SmartOrder sometimes invalidates tokens early. The old client turned the resulting 401 into an empty catalog. The adapter now caches tokens under a key fingerprinted by credentials (so two accounts never share one), and on a 401 it refreshes exactly once and retries:

if ($response->status() === 401) {
    $this->freshToken = $this->refreshToken();
    try {
        $response = $request();
    } finally {
        $this->freshToken = null;
    }
}

Enter fullscreen mode Exit fullscreen mode

That $freshToken field exists because version 4.0 minted a third token on the retry when caching was off. The test for it is ten lines with Http::sequence() and it’s the whole bug.

How it’s tested

Around 60 contract tests with Http::fake(), no real HTTP in the suite. CI runs PHP 8.2 to 8.4 against Laravel 12 and 13, plus PHPStan and Pint.

Reads for all three parks are also verified against the live sandboxes. The full write lifecycle is proven live for Disney and Universal. SeaWorld writes are contract-tested only, because its sandbox has no bookable inventory. I’d rather you read that here than find out in production.

Try it

composer require iabduul7/laravel-themepark-booking-adapters
php artisan vendor:publish --tag="themepark-adapters-config"

Enter fullscreen mode Exit fullscreen mode

Drop your Redeam or SmartOrder credentials in .env, resolve a provider, and you’re booking. PHP 8.2+ and Laravel 12 or 13.

Source is on GitHub. If you’ve built a wrapper around a messy third-party API, I’d like to hear which of these rules you’d argue with.

원문에서 계속 ↗