iabduul7/laravel-themepark-booking-adapters
A Laravel package that provides adapter classes for integrating theme park booking systems. It offers a consistent interface for connecting booking providers and handling availability, reservations, and ticketing workflows in your application.
Install & configure
composer require iabduul7/laravel-themepark-booking-adaptersphp artisan vendor:publish and pick the package's tag) — this is where each adapter's credentials live: Disney, SeaWorld, and SmartOrder each have their own block..env, never in the committed config. SmartOrder needs OAuth client credentials; the token is fetched and cached by the package, and it self-heals on a 401.php artisan config:cache after any credential change — the config is read through the cached copy at runtime.Look here first
ThemeParkApiException class. In v4.0.0 it is the API surface for failure — getCode() gives the HTTP status, getResponseData() gives the decoded provider body. Read its factory methods (productNotFound(), orderNotFound()) before anything else; they tell you exactly which failure modes the package distinguishes.First use case
Don't start with a controller. Write a throwaway artisan command (or a tinker session) that calls validateCredentials(), then getProduct() for a known ID. You want to see three things before wiring anything real: a successful payload shape, a productNotFound() throw on a bad ID, and a ThemeParkApiException on a forced 5xx. That 15 minutes defines your whole error-handling strategy.
Upgrading from v3
v4 is a real break. Anything that relied on failed calls resolving to an empty array is now silently wrong — or loudly broken, which is better. Grep your codebase for ?? [], if (empty($result)), and count($response) around adapter calls and re-evaluate every one.
Split your call sites by read vs write. The retry policy is asymmetric and it should shape your architecture. Reads are retried on connection drops and 5xx; writes never are. So read paths can be optimistic and fire-and-forget-ish, while write paths (bookings, order creation) need deliberate thought about what happens on failure.
Catch narrow before wide. productNotFound() and orderNotFound() are the specific cases; ThemeParkApiException is the umbrella. Order your catch blocks accordingly:
try {
$product = $adapter->getProduct($id);
} catch (ProductNotFoundException $e) {
// 404 — a genuine "this doesn't exist", render a 404 page
} catch (ThemeParkApiException $e) {
// 4xx/5xx — provider problem, not a missing product
report($e);
// $e->getCode(), $e->getResponseData()
}
That distinction matters most in a booking flow: a 404 on an order means "bad reference", a 500 means "we don't know" — and those demand different user messaging.
Validate credentials once, at deploy time. Now that validateCredentials() genuinely returns false on auth failure, put it in a health-check command or a scheduled job rather than in the request path. Treat false as a hard config error worth alerting on, and still wrap the call — a 5xx during validation throws rather than returning false.
Log getResponseData(), not just the message. The decoded provider body is usually where the actual reason lives ("rate limit exceeded", "invalid park date"). Attach it to your exception reports — it turns a support ticket into a five-second diagnosis.
Queue your writes, but don't let the queue retry them blindly. Since the package won't retry a write, a $tries = 3 booking job is your retry, not the package's. Only do that if your booking call is genuinely idempotent — most park APIs key off a client-supplied reference, so use it. Otherwise set $tries = 1 and route failures to a manual-reconciliation queue.
Fallback on availability reads. Park availability is now a retried, status-checked read, so transient blips are mostly absorbed. For a customer-facing calendar, still wrap it: on ThemeParkApiException, serve a cached/stale calendar rather than a hard error.
You only ever see the final response. The retry loop internalises intermediate failures — a request that succeeded on the third attempt is indistinguishable from a first-try success in your code. If you want retry-rate metrics, you'll have to infer them from latency or instrument at the HTTP client level.
getResponseData() can be null or empty. A connection drop that exhausts retries has no provider body to decode. Always null-check before indexing into it — $e->getResponseData()['message'] ?? $e->getMessage().
Not every throw comes from an HTTP call. OptionCodeResolver and the supplier/availability paths can throw too. Don't assume the exception's stack trace ends at a PendingRequest::send().
Empty list ≠ not found. The dedicated 404 exceptions apply to single-entity lookups (getProduct(), getExistingOrder()). A list/availability endpoint returning zero results is a valid empty array, not an exception. Don't collapse those two into one code path.
Catch order is a real bug source. If the specific not-found exceptions extend ThemeParkApiException, catching the umbrella first will swallow every 404 into your generic error handler. Verify the hierarchy in the source and order your blocks narrowest-first.
Testing with Http::fake() and retries interact. Faked sequences need enough responses queued to satisfy the retry policy, or you'll get confusing "no fake response" errors. Prefer Http::fake([... => Http::response($body, 500)]) patterns that assert on the thrown exception type rather than on call counts.
Config caching. env() calls inside the package config resolve to null once config is cached outside of config files. If credentials suddenly vanish in production but work locally, that's almost always it.
Don't add a global swallow. It's tempting to register a handler that turns ThemeParkApiException into a friendly flash message. Do it at the controller or job boundary instead, so the exception still reaches your reporter with its status and provider body intact.
Extension point: the adapter classes are the seam. If you need per-park behaviour (different timeout budgets, different fallbacks), bind your own thin wrapper around each adapter rather than branching on park name in controllers.
How can I help you explore Laravel packages today?