christianjbrown / met-office-weather-datahub-api-sdk
A strongly-typed PHP client for the Met Office Weather DataHub APIs (currently the Site-Specific / Global Spot forecast, the Site-Specific Blended Probabilistic Forecast, Observation (Land), Atmospheric Models, and Map Images), returning typed models instead of raw GeoJSON / CoverageJSON.
Package info
github.com/christianjbrown/met-office-weather-datahub-api-sdk-php
pkg:composer/christianjbrown/met-office-weather-datahub-api-sdk
Requires
- php: ^8.5
- christianjbrown/api-client: ^1.0
- psr/container: ^2.0
- symfony/dependency-injection: ^8.0
Requires (Dev)
- christianjbrown/code-quality-scripts: ^1.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-28 15:47:48 UTC
README
A strongly-typed, read-only PHP client for the Met Office Weather DataHub APIs. It returns plain, typed model objects rather than raw GeoJSON / CoverageJSON arrays. The library is structured to host multiple DataHub APIs side by side; its supported APIs are Site-Specific (Global Spot), Blended Probabilistic Forecast, Observation (Land), Atmospheric Models (Gridded), and Map Images.
π‘ Supported APIs
| API | Entry point | API version | Status |
|---|---|---|---|
| Site-Specific (Global Spot) | MetOffice::siteSpecific() |
v0 |
β Supported |
| Blended Probabilistic Forecast | MetOffice::blendedProbForecast() |
2.0.0 |
β Supported |
| Observation (Land) | MetOffice::observationLand() |
1 |
β Supported |
| Atmospheric Models (Gridded) | MetOffice::atmosphericModels() |
1.0.0 |
β Supported |
| Map Images | MetOffice::mapImages() |
1.0.0 |
β Supported |
The API version column is the DataHub API version each module targets, taken verbatim from the upstream
URL path β the Met Office versions each product independently and inconsistently, hence the mix of v0, 1
and 1.0.0. It is not related to this package's own version: the package version describes the PHP
contract (class names, method signatures, return types), which is what breaks your build, while the upstream
version is an implementation detail living only in each module's Api\ApiInterface URL constants. A new
upstream major does not imply a new package major, or vice versa.
See Coverage & limitations for what this library deliberately does not cover.
Site-Specific (Global Spot)
Given a latitude and longitude it fetches the hourly, three-hourly, or daily point forecast and returns typed model objects. It supports the three forecast resolutions:
- Hourly (
getHourlyForecastApi()) β per-hour "instant" steps: temperature, feels-like, wind, visibility, humidity, pressure, UV, weather code, precipitation rate/amount, and probability of precipitation. - Three-hourly (
getThreeHourlyForecastApi()) β per-three-hour steps: max/min air temperature, feels-like, wind, visibility, humidity, pressure, UV, weather code, precipitation/snow amounts, and the full set of precipitation-type probabilities (rain, heavy rain, snow, heavy snow, hail, sferics). - Daily (
getDailyForecastApi()) β day/night split steps: midday & midnight wind, visibility, humidity and pressure, day-max/night-min temperature and feels-like (with upper/lower bounds), max UV, day & night weather codes, and day & night precipitation-type probabilities.
Every forecast carries the resolved location name (and its licence attribution), the model run date (as a Unix timestamp), the elevation of the resolved point (from the response geometry), and the distance from the requested point in metres. Pass true as the third argument to getForecast() to additionally request per-parameter metadata (unit label/symbol, description and type), returned as a keyed getParameters(): array<string, ParameterMetadataInterface> β this also switches the API's excludeParameterMetadata query parameter to false, so it is opt-in and does not change existing calls.
Observation (Land)
Fetches recent (past 48 hours) hourly land surface observations. It exposes two clients:
- Nearest (
getNearestApi()) β resolves the nearest observation locations from either a latitude/longitude pair (getByCoordinates(), coordinates are rounded to two decimal places for the API) or a geohash (getByGeohash()). Both accept an optional?int $max(1β5, API default 1) to return more than one nearest location. EachNearestLocationInterfacecarries itsgeohash,area,region,country, andolsonTimeZone. - Observation (
getObservationApi()) β given a six-character geohash,getByGeohash()returns the array of hourlyObservationInterfacevalues (datetime as a Unix timestamp, plus optional temperature, humidity, wind speed/gust/direction, weather code, visibility, mean sea-level pressure, and pressure tendency). Results are cached per geohash; passtrueas the second argument to bypass the cache.
use ChristianBrown\MetOffice\Coordinates; use ChristianBrown\MetOffice\MetOffice; $observationLand = (new MetOffice())->observationLand('your-observation-land-apikey'); // London: latitude 51.55, longitude -0.18. $nearest = $observationLand->getNearestApi()->getByCoordinates(new Coordinates(51.55, -0.18)); // NearestLocationInterface[] $observations = $observationLand->getObservationApi()->getByGeohash('gcpvj0'); // ObservationInterface[]
Atmospheric Models (Gridded)
Retrieves orders for Atmospheric Model ("Gridded") data. Unlike the other APIs β which return typed weather values β the model data itself is delivered as binary GRIB files; this client returns typed metadata for the runs, orders and files, and hands you the raw GRIB bytes for a file (no GRIB parsing is performed). Base URL https://data.hub.api.metoffice.gov.uk/atmospheric-models/1.0.0, same apikey header. It exposes two clients:
- Runs (
getRunsApi()) βgetRuns(?string $sort = null)lists every available model run (RunInterface[], each with amodelIdsuch asmo-uk,mo-global,mo-mogrepsg, and itscompleteRunsβRunDetailInterface[]carrying the run hour, the run date-time as a Unix timestamp, and therunFilter), optionally sorted (RUNorRUNDATETIME).getRunsByModel(string $modelId, ?string $sort = null)narrows the list to a single model. - Orders (
getOrdersApi()) βgetOrders(?string $detail = null)lists the orders configured for your organisation (OrderInterface[]), optionallyMINIMALorFULL;getOrderFiles(string $orderId, ?string $detail = null, ?string $runFilter = null)lists the latest available files for an order (OrderFileInterface[]);getOrderFile(string $orderId, string $fileId)returns the detailed metadata for one file (OrderFileDetailsInterface, including itsParameterDetailInterface[]); andgetOrderFileData(string $orderId, string $fileId)downloads the file and returns the raw GRIB bytes as astring(302 redirects are followed and the file id is URL-encoded for you).
use ChristianBrown\MetOffice\MetOffice; $atmosphericModels = (new MetOffice())->atmosphericModels('your-atmospheric-models-apikey'); $runs = $atmosphericModels->getRunsApi()->getRuns(); // RunInterface[] $grib = $atmosphericModels->getOrdersApi()->getOrderFileData($orderId, $fileId); // raw GRIB bytes (string)
Map Images
Retrieves orders for Map Images data. As with Atmospheric Models, the imagery itself is delivered as binary files β here PNG map images β so this client returns typed metadata for the runs, orders and files, and hands you the raw PNG bytes for a file (no image decoding is performed). Base URL https://data.hub.api.metoffice.gov.uk/map-images/1.0.0, same apikey header. It exposes two clients:
- Runs (
getRunsApi()) βgetRuns(?string $sort = null)lists every available model run (RunInterface[], each with amodelIdsuch asmo-uk-mimg, and itscompleteRunsβRunDetailInterface[]carrying the run hour, the run date-time as a Unix timestamp, and therunFilter), optionally sorted (RUNorRUNDATETIME). Unlike Atmospheric Models, Map Images has no per-model runs endpoint. - Orders (
getOrdersApi()) βgetOrders()lists the orders configured for your organisation (OrderInterface[]);getOrderFiles(string $orderId, ?string $detail = null, ?string $runFilter = null)lists the latest available files for an order (OrderFileInterface[]);getOrderFile(string $orderId, string $fileId)returns the detailed metadata for one file (OrderFileDetailsInterface, including itsParameterDetailInterface[]); andgetOrderFileData(string $orderId, string $fileId, ?bool $includeLand = null, ?bool $legend = null)downloads the file and returns the raw PNG bytes as astring(302 redirects are followed and the file id is URL-encoded for you) β$includeLandmerges in the optional land-cover base layer and$legendincludes a legend in the image, both API defaultsfalse.
use ChristianBrown\MetOffice\MetOffice; $mapImages = (new MetOffice())->mapImages('your-map-images-apikey'); $runs = $mapImages->getRunsApi()->getRuns(); // RunInterface[] $png = $mapImages->getOrdersApi()->getOrderFileData($orderId, $fileId); // raw PNG bytes (string)
Blended Probabilistic Forecast
The Met Office Blended Probabilistic Forecast (BPF) is a newer product for consuming site-specific forecasts probabilistically (a range of probabilities and percentiles rather than a single "most likely" value). Unlike Global Spot, it is an OGC Environmental Data Retrieval (EDR) API and returns CoverageJSON β so it has its own module rather than reusing the Global Spot models. Base URL https://data.hub.api.metoffice.gov.uk/mo-blended-prob-forecast-feature-svc/2.0.0, same apikey header.
v2 only. This module targets BPF v2, which is a different service on a different context path β not a version bump. v1 (
/mo-site-specific-blended-probabilistic-forecast/1.0.0) is retired on 11 November 2026, its collection ids were renamed, and a v1 API key will not authenticate against v2. See Migrating from BPF v1.
Data is reached in four steps β collection β instance β location β data β and the two data queries accept an optional DataQuery filter so you fetch only what you need. It exposes five clients:
- Capabilities (
getCapabilitiesApi()) βgetLandingPage()returns the API landing metadata (LandingPageInterface);getConformance()returns the conformance-class URIs (string[]). - Collections (
getCollectionsApi()) βgetCollections()lists the four available collections (CollectionInterface[]:global-spot-percentiles,global-spot-probabilities,uk-spot-percentiles,uk-spot-probabilities);getCollection(string $collectionId)returns one collection's metadata. Each carries itscrs,outputFormats,links,dataQueries, and a keyed map ofParameterInterface(getParameters()β 73β79 parameters per collection, each with anobservedPropertyLabel,unit, and the Met Officeheight/fileSuffixextras). Note the collection-levelextentis empty in v2 β the real extent lives on the instance. - Instances (
getInstancesApi()) βgetInstances(string $collectionId)lists the model runs (InstanceInterface[]; in practice a single instance,blended);getInstance(string $collectionId, string $instanceId)returns one. The instance carries the populatedExtentInterface:getSpatialBbox(),getTemporalInterval()/getTemporalValues()(241 hourly steps), andgetCustom()β anExtentCustomInterface[]publishing the statistical axis (for percentile collections,idpercentilewith values5β¦95). - Locations (
getLocationsApi()) βgetLocations(string $collectionId, string $instanceId)lists the instance's spot sites (LocationInterface[], thousands of them; each with anid,latitude,longitude, andaltitude);getLocation(string $collectionId, string $instanceId, string $locationId, ?DataQueryInterface $query = null)fetches one site's forecast as a typedCoverageCollectionInterface. - Position (
getPositionApi()) βgetPosition(string $collectionId, string $instanceId, CoordinatesInterface $coordinates, ?DataQueryInterface $query = null)fetches the forecast for the nearest site to a latitude/longitude, skipping the locations lookup entirely. It sends the requiredcoordsparameter as WKTPOINT(longitude latitude), built for you from the sharedCoordinatesvalue object.
Both data queries return a CoverageJSON CoverageCollectionInterface, which carries:
getDomainType()βPointSeries.getReferencing()β aReferenceSystemInterface[]describing each coordinate. TheIdentifierRSentries exposegetIdentifiers(), a map of axis value to human label (e.g.50β50th percentile), which is the only place those labels are published.getCoverages()β oneCoverageInterfaceper requested parameter, each with its owngetId()(the parameter name), its owngetParameters()map, aDomainInterface, and a map ofNdArrayInterfaceranges (dataType,axisNames,shape, and position-alignedvaluesthat may containnullgaps).
getDomain()->getAxes() is a map of named AxisInterface β t, x/y/z, locationId, plus the statistical axis (percentiles for percentile collections, or a per-parameter probabilityOfβ¦Values threshold axis for probability collections). Each axis exposes getFloatValues() / getStringValues() (the values are homogeneous, so exactly one is populated), and, for period parameters such as airTemperature1p5mMaximumPt12h, getBounds() β a flat array of 2n timestamps where the lower bound of step i is at index 2i and the upper at 2i + 1.
DataQuery
DataQuery bundles the three optional filters shared by getLocation() and getPosition(). Every argument is optional; omitted filters are simply not sent.
use ChristianBrown\MetOffice\BlendedProbForecast\DataQuery; new DataQuery( ['airTemperature1p5m', 'airTemperature1p5mMaximumPt12h'], // parameter-name (comma-joined for you) ['50', '90'], // percentiles (comma-joined for you) '2026-08-13T00:00:00Z/2026-08-14T00:00:00Z', // datetime );
datetime is passed through verbatim and accepts the full v2 grammar: a single instant, a comma-separated list, a closed range <start>/<end>, an open-ended range (<start>/.. or ../<end>), or a repeating interval R{n}/{start}/{duration}.
use ChristianBrown\MetOffice\BlendedProbForecast\DataQuery; use ChristianBrown\MetOffice\Coordinates; use ChristianBrown\MetOffice\MetOffice; $blended = (new MetOffice())->blendedProbForecast('your-blended-prob-forecast-apikey'); $collections = $blended->getCollectionsApi()->getCollections(); // CollectionInterface[] $instances = $blended->getInstancesApi()->getInstances('uk-spot-percentiles'); // InstanceInterface[] $query = new DataQuery(['airTemperature1p5m'], ['50', '90'], '2026-08-13T00:00:00Z/2026-08-14T00:00:00Z'); // Nearest site to London, no locations lookup needed. $coverageCollection = $blended->getPositionApi()->getPosition( 'uk-spot-percentiles', 'blended', new Coordinates(51.55, -0.18), $query, ); // CoverageCollectionInterface foreach ($coverageCollection->getCoverages() as $coverage) { $timeAxis = $coverage->getDomain()->getAxes()['t'] ?? null; // AxisInterface|null foreach ($coverage->getRanges() as $parameterId => $range) { // $range->getValues() is a flat float array aligned to $range->getShape() // (e.g. shape [2, 25] = 2 percentiles x 25 time steps); nulls mark gaps. $unit = $coverage->getParameters()[$parameterId]?->getUnit(); printf("%s: %d values in %s\n", $parameterId, count($range->getValues()), $unit ?? '?'); } }
Migrating from BPF v1
| v1 (retired 11 Nov 2026) | v2 | |
|---|---|---|
| Entry point | MetOffice::siteSpecificBlended() |
MetOffice::blendedProbForecast() |
| Namespace | β¦\SiteSpecificBlended\ |
β¦\BlendedProbForecast\ |
| Base path | /mo-site-specific-blended-probabilistic-forecast/1.0.0 |
/mo-blended-prob-forecast-feature-svc/2.0.0 |
| API key | v1 subscription key | new v2 key required |
| Collection ids | improver-percentiles-spot-global, β¦ |
global-spot-percentiles, global-spot-probabilities, uk-spot-percentiles, uk-spot-probabilities |
| Locations | getLocations($collectionId) |
getLocations($collectionId, $instanceId) |
| Location data | getCoverage($collectionId, $locationId, ?$parameterName, ?$datetime) |
getLocation($collectionId, $instanceId, $locationId, ?DataQueryInterface) |
| Nearest point | β | getPositionApi()->getPosition(β¦) |
| Parameter names | Collection::getParameterNames(): string[] |
Collection::getParameters(): ParameterInterface[] (keyed) |
| Coverage parameters | CoverageCollection::getParameters() |
Coverage::getParameters() (per coverage) plus CoverageCollection::getReferencing() |
| Parameter ids | snake_case (feels_like_temperature) |
camelCase (feelsLikeTemperature1p5m) |
Parameter ids were renamed wholesale for v2. The full mapping is reproduced as a searchable table in docs/bpf-v1-to-v2-parameter-names.md β 157 entries, transcribed from the Met Office's v1 β v2 parameter name changes PDF and verified against the live API (the PDF itself lists two spurious rows, documented there). This library treats parameter names as opaque strings, so no code change is needed beyond updating the names you pass to DataQuery.
π« Coverage & limitations
This library aims for full parity with the DataHub API products, but is deliberately scoped. What it does not cover:
- Radar β the Met Office radar composites (UK / NW-European surface rain-rate, HDF5) are not part of the DataHub REST API; they are distributed separately via AWS Open Data (an S3 object store, no
apikeyheader). They are out of scope for this DataHub client. Confirmed against the current Atmospheric Models, Map Images, Site-Specific and Observation (Land) OpenAPI specifications, none of which reference radar. - Order creation / management β the library is read-only. Both the Atmospheric Models (
v2.1.0) and Map Images (v1.1.0) OpenAPI specifications confirm every/orderspath isGET-only β there is noPOST,PUT,PATCHorDELETEoperation to implement. It reads existing orders (/orders,/orders/{id}/latest, files, and file data) but never creates, modifies, or deletes them. Orders are configured in the DataHub portal. - No binary decoding β Atmospheric Models GRIB and Map Images PNG payloads are returned as raw bytes; the library does not parse GRIB or decode images. (Blended Probabilistic Forecast data is JSON/CoverageJSON/GeoJSON and is returned as typed models.)
dataSourceis fixed toBD1β the only value the Site-Specific Forecast OpenAPI specification's live gateway currently permits, so there is nothing to make configurable.- Map Images has no per-model runs endpoint β only
getRuns()is available (there is nogetRunsByModel()); this mirrors the real API, where Map Images genuinely lacks/runs/{modelId}. dataSpecis not exposed β every Atmospheric Models order/run endpoint has an optionaldataSpecquery parameter, but its OpenAPI schema lists exactly one valid value (1.1.0, also the default), so omitting it already produces identical behaviour.
βοΈ Prerequisites
π‘ If you're on MacOS and have Homebrew, PHP and Composer will install with brew install composer.
ποΈ Installation
For your composer-enabled project:
composer require christianjbrown/met-office-weather-datahub-api-sdk
π» Usage
First, create a Met Office Weather DataHub account and subscribe to the Site-Specific API to obtain an API key. The key is sent to the API as the apikey HTTP header.
The MetOffice umbrella facade is the entry point for every DataHub API. Call siteSpecific($apiKey) to get the Site-Specific client, which builds the three forecast clients (and their transformer chains) for you through a dependency-injection container:
use ChristianBrown\MetOffice\MetOffice; $siteSpecific = (new MetOffice())->siteSpecific('your-site-specific-apikey'); $hourlyForecastApi = $siteSpecific->getHourlyForecastApi(); // HourlyForecastApiInterface $threeHourlyForecast = $siteSpecific->getThreeHourlyForecastApi(); // ThreeHourlyForecastApiInterface $dailyForecastApi = $siteSpecific->getDailyForecastApi(); // DailyForecastApiInterface
You can also construct the Site-Specific facade directly, without going through the umbrella facade:
use ChristianBrown\MetOffice\SiteSpecific\SiteSpecific; $siteSpecific = new SiteSpecific('your-site-specific-apikey'); $hourlyForecastApi = $siteSpecific->getHourlyForecastApi();
If you'd rather wire the clients by hand, see Wiring the clients below.
Each client exposes a single getForecast(CoordinatesInterface $coordinates, bool $skipCache = false) method returning a ForecastInterface. The lat/lon pair is a single Coordinates value object rather than two positional floats, so it cannot be silently transposed. Results are cached per "latitude,longitude" pair; pass true as the second argument to bypass the cache and re-fetch.
use ChristianBrown\MetOffice\Coordinates; // London: latitude 51.5074, longitude -0.1278. $forecast = $hourlyForecastApi->getForecast(new Coordinates(51.5074, -0.1278)); // ForecastInterface echo $forecast->getLocationName(), "\n"; // e.g. "London" echo date('c', $forecast->getModelRunDate() ?? 0), "\n"; // model run date (Unix -> ISO) foreach ($forecast->getTimeSteps() as $step) { // Every step implements ForecastTimeStepInterface (getTime(): int, a Unix timestamp). // The hourly client yields HourlyForecastTimeStepInterface instances. if ($step instanceof \ChristianBrown\MetOffice\SiteSpecific\Model\HourlyForecastTimeStepInterface) { printf( "%s %.1fΒ°C wind %.1f m/s\n", date('H:i', $step->getTime()), $step->getScreenTemperature() ?? 0.0, $step->getWindSpeed10m() ?? 0.0, ); } }
Wind direction and weather codes
Wind direction is stored as raw degrees (getWindDirectionFrom10m() / getMidday10MWindDirection(), an ?int). Convert a bearing to a 16-point compass value with the WindDirection enum:
use ChristianBrown\MetOffice\Enums\WindDirection; $direction = WindDirection::fromDegrees(200); // WindDirection::SOUTH_SOUTH_WEST echo $direction->value; // "SSW"
Weather codes are decoded to the WeatherType enum (getSignificantWeatherCode(), getDaySignificantWeatherCode(), β¦, an ?WeatherType). The enum is the readable, debuggable form of the raw Met Office code β use ->value for the numeric code and ->name for a stable string token. Display wording (a human-readable name or emoji) is intentionally not provided here; it is a locale-sensitive presentation concern and belongs to the consumer:
use ChristianBrown\MetOffice\Enums\WeatherType; $type = WeatherType::SUNNY_DAY; echo $type->value; // 1 (raw Met Office significant weather code) echo $type->name; // "SUNNY_DAY" (stable token to map to a display string / emoji)
π¨ Error handling
Everything this library throws implements ChristianBrown\MetOffice\Exception\ExceptionInterface, so a single catch covers it all:
use ChristianBrown\MetOffice\Coordinates; use ChristianBrown\MetOffice\Exception\ExceptionInterface; try { $forecast = $hourlyForecastApi->getForecast(new Coordinates(51.5074, -0.1278)); } catch (ExceptionInterface $exception) { // Anything this library throws lands here. }
There are two concrete types:
UnexpectedResponseException(extendsRuntimeException) β the API returned a body the client or a transformer couldn't parse (a missing/mis-typed field, an emptyfeaturescollection, an unparseabletime).MissingInputException(extendsInvalidArgumentException) β reserved for bad caller input.
Both live in src/Exception/. Request-level failures (network errors, non-2xx responses) still surface as RequestExceptionInterface from christianjbrown/api-client, which is outside this library's exception hierarchy.
Two Blended Probabilistic Forecast responses are worth calling out:
204 No Contentβ returned bygetLocation()/getPosition()when aparameter-nameorpercentilesfilter matches nothing. The body is empty, so the JSON request sender raisesChristianBrown\ApiClient\Exception\Parse\ParseJsonExceptionInterfacerather than returning an emptyCoverageCollectionInterface. Treat it as "no data for that filter", and check your parameter names against the collection'sgetParameters()map.400 Bad Requestβ the body is{"message": "β¦", "transaction": "<uuid>"}. The Met Office service desk asks for thattransactionid when reporting a problem, so capture it from the response before discarding the error.
Under the hood, every facade (SiteSpecific, ObservationLand, AtmosphericModels, MapImages, BlendedProbForecast) builds a Symfony dependency-injection container from a fixed, ordered list of small registrar classes, run through a shared RegistrarContainerFactory. See Composition root and registrars for how that's put together, and Wiring the clients by hand if you don't want the container at all.
Composition root and registrars
Each facade's constructor is its composition root: it builds an ApiHost (or takes the one you passed in), lists the registrars for that product in dependency order, and hands them to RegistrarContainerFactory. A registrar is anything implementing ChristianBrown\MetOffice\Container\ServiceRegistrarInterface, a single-method interface (register(ContainerBuilder $container): void). Two kinds are shared across every product:
CoreRegistrarβ the boilerplate every facade needs regardless of product: the transport-wrappingApiClient, the JSON request sender built from it, and theApiKeycredential value object. This exists exactly once and every facade's registrar list starts with it.RawRequestSenderRegistrarβ the raw (non-JSON) request sender used only by the two coverage-order products (Atmospheric Models, Map Images) to download binary GRIB/PNG order files.
Everything else is a small, final registrar scoped to one API resource group or one cohesive transformer chain within a product β for example SiteSpecific\Container\HourlyForecastRegistrar wires the hourly time-step transformer, the shared ForecastApi, and the HourlyForecastApi wrapper; BlendedProbForecast\Container\CoverageTransformerRegistrar wires the CoverageJSON transformer chain shared by LocationsApi and PositionApi. Adding a new DataHub API to an existing product, or a new DataHub product entirely, means adding a registrar (or a new product namespace with its own registrars and facade) and listing it in the composition root β never editing an existing registrar's register() method.
Injectable API host
Every product's base URL is a public const string API_URL... on its Api\ApiInterface, defaulting to the real DataHub host (ChristianBrown\MetOffice\ApiInterface::API_HOST). Each facade's constructor takes an optional, last ?ChristianBrown\MetOffice\Host\ApiHostInterface $apiHost = null parameter; when omitted, it defaults to production, so every existing call site that only passes an API key is unaffected. Pass your own ApiHost to point a facade at a different host β a sandbox, a local stub server, a test double β without touching any of the URL constants:
use ChristianBrown\MetOffice\Host\ApiHost; use ChristianBrown\MetOffice\SiteSpecific\SiteSpecific; $siteSpecific = new SiteSpecific('your-site-specific-apikey', new ApiHost('https://sandbox.example'));
ApiHost::rewrite(string $url): string replaces the production host prefix on a URL with the configured one and leaves the path and query untouched, so it works uniformly across every product's URL constants (all of which share the same data.hub.api.metoffice.gov.uk prefix). The Met Office DataHub itself does not publish a separate sandbox host at the time of writing β this exists for local/CI stubs and for whenever one is introduced.
Wiring the clients by hand
If you don't want the container, you can build the same chain yourself. The HTTP request sender comes from christianjbrown/api-client; ApiHost defaults to production when omitted.
use ChristianBrown\ApiClient\ApiClient; use ChristianBrown\MetOffice\ApiKey; use ChristianBrown\MetOffice\Host\ApiHost; use ChristianBrown\MetOffice\SiteSpecific\Api\ForecastApi; use ChristianBrown\MetOffice\SiteSpecific\Api\HourlyForecastApi; use ChristianBrown\MetOffice\SiteSpecific\Transformer\ForecastTimeStepsTransformer; use ChristianBrown\MetOffice\SiteSpecific\Transformer\ForecastTransformer; use ChristianBrown\MetOffice\SiteSpecific\Transformer\HourlyForecastTimeStepTransformer; $apiKey = new ApiKey('your-site-specific-apikey'); // Shared JSON request sender (wires Guzzle for you). $requestSender = (new ApiClient())->getJsonApiRequestSender(); // The resolution-agnostic forecast client, given the hourly transformer chain. $forecastApi = new ForecastApi( $requestSender, new ForecastTransformer( new ForecastTimeStepsTransformer( new HourlyForecastTimeStepTransformer() ) ), $apiKey ); // The thin hourly wrapper: supplies the hourly URL and lets you override the host. $hourlyForecastApi = new HourlyForecastApi($forecastApi, new ApiHost());
The three-hourly and daily clients follow the same shape with their own time-step transformer and wrapper class.
π License
Released under the MIT License.