nonsoniyi/the-guardian-api-client
A Laravel and PHP client for The Guardian's Open Platform (Content) API. Query articles, sections, tags, and contributors with a fluent interface, so you can pull Guardian content into your app without hand-rolling HTTP requests.
Install and wire up
composer require nonsoniyi/the-guardian-api-client
test key works for exploration but is rate-limited and returns a restricted content set — don't ship with it..env as GUARDIAN_API_KEY (or whatever key the package's config file expects — publish the config first if the package ships one: php artisan vendor:publish --provider="Nonsoniyi\Guardian\...", then read config/guardian.php).php artisan tinker session and make one call before writing any application code. You want to see the raw response shape once.Where to look first
search, content, tags, sections, editions (The Guardian's Content API surface).src/ for the main client class. This is a thin wrapper — reading the whole package takes ten minutes and is the fastest way to know exactly which endpoints are covered and which aren't.content.guardianapis.com) and how it appends api-key.First real use case
A "latest from The Guardian" sidebar: fetch the 10 newest items in a section (e.g. world), render webTitle, webPublicationDate, and webUrl as links. That single call exercises auth, the response envelope, and date formatting — the three things that trip people up later.
Wrap it, don't sprinkle it. Bind a single GuardianService in the container that owns the client and returns your own DTOs. The package's response objects are an external contract; mapping them into ArticleData/TagData objects means a package swap (likely, given the maintenance profile) touches one file.
final class GuardianService
{
public function __construct(private GuardianApi $client) {}
/** @return ArticleData[] */
public function latest(string $section, int $limit = 10): array
{
$response = $this->client->search([
'section' => $section,
'page-size' => $limit,
'order-by' => 'newest',
'show-fields' => 'trailText,byline,thumbnail',
]);
return collect($response['results'])
->map(fn (array $item) => ArticleData::fromGuardian($item))
->all();
}
}
Cache hard. Editorial content is read-mostly and every call burns quota. Cache::remember("guardian:{$section}:{$limit}", now()->addMinutes(10), ...) is the single highest-value line you'll write. Use tags or a versioned key prefix so you can invalidate everything at once.
Treat search as your query builder. Most day-to-day needs are parameter combinations rather than new endpoints: tag=technology/apple, from-date/to-date, order-by=relevance, show-fields for trail text and thumbnails, show-tags=contributor for bylines. Build a small fluent helper around it rather than passing raw arrays from controllers.
Pagination. The envelope carries currentPage, pages, pageSize, and total (nested one level under the response key — see Gotchas). For feeds, paginate one page at a time and let the user drive. For backfills (building an archive, seeding a search index), loop in a queued job with a sleep between pages and a stored cursor; never loop synchronously in a request.
Ingest, don't proxy. If your app depends on Guardian articles long-term, persist what you need (id, headline, url, publication date, section, body if licensed) via a scheduled command. The API is a live index, not your database — items can be edited or withdrawn. Persisting also decouples page loads from Guardian's uptime and your quota.
Testing. Inject the client rather than resolving it statically, then swap in a stub returning fixture arrays saved from a real call. Keep one "contract" test that hits the live API behind an env flag, so you notice when Guardian changes the response shape.
Presentation. Never call the API from Blade/Inertia renders — a slow Guardian response becomes a slow page and a cache-miss stampede. Fetch in a controller/action (cached) or a view composer with a cached backing store.
The double-nested envelope. The Guardian wraps everything in { "response": { "status": "ok", "results": [...] } }. Depending on how the package returns things you may get the outer array, the inner one, or a decoded object. Check once, write a helper, and stop guessing.
Check status explicitly. The API can return HTTP 200 with "status": "error" and a message. If the package doesn't throw on that, a failed request silently becomes an empty article list — the classic "why is the sidebar blank in prod" bug.
API key is a query parameter. It ends up in URLs, so it ends up in logs, exception traces, and anything you dd(). Scrub it, and never let a browser or mobile client call Guardian directly with your key.
Rate limits and quota. Free/developer tiers have a daily call budget and a per-second ceiling, and 429s are the failure mode you'll actually hit. Cache aggressively, add a short backoff/retry on the HTTP client, and alert on repeated 429s rather than retrying forever.
Deep paging is capped. page-size has a maximum (commonly 50) and very deep pages are rejected. For large pulls, slice by from-date/to-date windows instead of incrementing page indefinitely.
Dates are ISO-8601 UTC strings. Parse with Carbon::parse() and convert at the edge. Don't assume webPublicationDate is a DateTime object — it's a string in the payload.
show-fields is a payload-size lever. Default responses are lean; adding body, bodyText, or show-elements=all multiplies response size and latency. Request only the fields the view needs, and cache the fat requests separately if you need them at all.
Package maturity is the main risk. Six stars, zero dependents, last release May 2024. Pin an exact version in composer.json, commit the lock file, and assume you may need to fork it. The upside: it's a thin HTTP wrapper, so replacing it with a hand-rolled Guzzle client or Laravel's Http facade is an afternoon's work — which is another argument for keeping it behind your own service class.
Extension points. Extend the client class to add endpoints the wrapper doesn't cover (e.g. /content/{id} for a single item, or the tags/editions endpoints), or swap the underlying Guzzle handler to add logging, retries, or request recording in tests. Both are cleaner than reaching around the package with raw Http::get() calls.
Watch PHP/Laravel constraints. Check the composer.json requirement before upgrading PHP or Laravel — a stale wrapper is a common cause of a blocked framework upgrade, and forking to widen the constraint is usually trivial.
How can I help you explore Laravel packages today?