shelfwood/php-pms-clients
PHP clients for property management system (PMS) APIs, giving Laravel and PHP applications a consistent way to connect to PMS platforms and work with property, reservation, and related data.
This is a framework-agnostic PHP library — there is no Laravel service provider or facade. You wire it in yourself in about four steps:
composer require shelfwood/php-pms-clients
/api/connector/v1/services/getAvailability/2024-01-22, /reservations/getAll/2023-06-06), so the method signatures and response shapes in src/ are what matters, not older blog snippets. Grep for __construct, public function, and the endpoint constants to map the surface.config/services.php and .env so they don't leak into code:
'mews' => [
'base_url' => env('MEWS_BASE_URL'),
'client_token'=> env('MEWS_CLIENT_TOKEN'),
'access_token'=> env('MEWS_ACCESS_TOKEN'),
],
reservations table so you can join them to CRM/billing data instead of hitting the PMS on every page load. Note that getAll now uses the versioned /reservations/getAll/2023-06-06 endpoint with pagination/extent alignment, so expect to page through results rather than get one giant array:$client = new MewsClient(config('services.mews.base_url'), config('services.mews.access_token'));
$reservations = $client->getReservations(['startUtc' => now()->subWeek(), 'endUtc' => now()]);
Bind the client as a singleton — it now caches state internally. The Mews client fetches enterprise timezone from configuration/get and caches it per client instance to do its UTC-normalization of availability/pricing boundaries. Reusing one instance avoids an extra round-trip per call:
$this->app->singleton(MewsClient::class, fn () => new MewsClient(
config('services.mews.base_url'),
config('services.mews.access_token'),
));
Wrap it behind your own interface. Mews and Booking Manager return different shapes and the Mews availability response changed in v5 — it now returns ResourceCategoryAvailabilities with a per-category Metrics map, not a flat availability list. A PmsClient contract with reservations(), availability(), pricing() adapters insulates the rest of the app from these provider contract churn events.
Supply Metrics explicitly. In v5 the Mews availability request requires Metrics (omitting it defaults to all metrics, which is heavier and slower than you probably want). Decide per call-site what you need and pass it — don't rely on the default:
$client->getAvailability([
'StartUtc' => $start->copy()->utc(),
'EndUtc' => $end->copy()->utc(),
'Metrics' => ['AvailableRooms', 'Occupancy'], // be explicit
]);
Let the client own time-unit boundaries, but verify tenant timezone. v5 normalizes availability/pricing boundaries to the Mews-required "00:00 converted to UTC" using the enterprise timezone pulled from configuration/get. That's a genuine win — the old assessment told you to hand-roll UTC conversion. But it's cached per client instance, so if you run Octane/long-lived queue workers and the enterprise timezone changes, restart workers. For multi-tenant setups, one client instance per tenant.
Sync via queued jobs, not requests. PMS APIs are slow and rate-limited. Pattern:
SyncMewsReservations job — accepts a date range, calls the client, upserts into reservations. Since getAll is now paginated, make the job paging-aware or split ranges across jobs.SyncPmsReservationsCommand — scheduled in routes/console.php, dispatches the job daily (or hourly for high-turnover properties).upsert() keyed on the PMS reservation ID so re-running is safe.Normalize into DTOs, then Eloquent. Map raw responses into a readonly DTO (ReservationData, AvailabilityData) via fromPms(array $data) factories. v5 changed the availability response envelope — if you were reading the old shape directly you'll want the DTO boundary to absorb that blast radius next time.
Handle webhooks if the provider offers them. Mews supports push notifications. Prefer a webhook controller dispatching a job over polling. Store raw payloads in webhook_logs so you can replay failed syncs.
Fire domain events after upsert. ReservationCreated / ReservationUpdated so notifications, billing, and availability cache invalidation react without knowing about the PMS.
getAvailability/2024-01-22; the endpoint path changed and the response now nests ResourceCategoryAvailabilities with a per-category Metrics map. Any code reading the old flat structure breaks.Metrics is now required on availability requests (defaulting to all metrics if omitted) — your calls will "work" but pull more than you asked for and cost more latency.OccupancyConfiguration on rates/getPricing. If you relied on the library populating it, you now need to pass whatever the current contract requires yourself.customers/getAll + Extent + Limitation. Behaviour (pagination, fields returned) may differ from what your code assumed about the old dedicated search call — re-check that path.Cache::remember it rather than re-authenticating per call.RateLimiter::attempt() or ->delay() on dispatch. v5's paginated getAll means more calls per sync than before — a nightly range that used to be one request is now N. Spread large ranges across jobs.getAll is explicitly versioned and pagination-aligned in v5. Check whether the client returns a generator (ideal) or requires a manual loop; if manual, wrap it in a generator in your adapter.cancelled status updates — don't just insert.Http::fake() if it rides Laravel's HTTP layer, otherwise inject a mocked PSR-18 client. Record redacted real responses as fixtures; the docs are still thin and response shapes are undocumented.Metrics omission or boundary normalization is causing an unexpected response.composer.json, run your fixture/snapshot tests against the new availability response shape, audit every availability call for an explicit Metrics value, check anything that depended on OccupancyConfiguration in pricing, and re-verify customers-by-email call-sites.How can I help you explore Laravel packages today?