Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Content Api Client Laravel Package

storyblok/php-content-api-client

A PHP client for Storyblok's Content Delivery and Management APIs, giving Laravel and plain PHP apps typed access to stories, components, datasources, and assets. Includes request handling, pagination, caching support, and a fluent query builder for headless content.

Deep Wiki
Context7

Getting Started

Install with Composer and confirm the HTTP transport is available:

composer require storyblok/php-content-api-client

Instantiate the client with the Content Delivery API token from Storyblok (Settings → Access Tokens). Use the public token for production and the preview token for draft content:

use Storyblok\Client;

$client = new Client($_ENV['STORYBLOK_TOKEN']);
// preview/draft content requires the *preview* token:
$client->setVersion('draft');

Your first useful call is getStories() for a folder listing, or getStory() for a single page. Both return a Storyblok\ApiResponse, not a raw array — call getBody() (or access it like an array, since the response implements ArrayAccess):

$response = $client->getStory('about');
$story    = $response->getBody()['story'];      // full story envelope
$content  = $story['content'];                  // the component tree

$list = $client->getStories(['starts_with' => 'blog/'])->getBody()['stories'];

Where to look first:

  1. The Client class — every endpoint is a thin wrapper (getStories, getStory, getLinks, getDatasourceEntries, getTags, getSpace).
  2. Storyblok\ApiResponse — the only thing you need to understand to get data out (getBody(), getHeaders(), getStatusCode(), getResponse()).
  3. The query-string parameter list in the Content Delivery API docs — the PHP client passes $params through verbatim, so Storyblok's own documentation is the client's documentation for filtering, pagination, and resolution.

A typical first integration: a catch-all route/controller that maps a slug to getStory($slug) and a partial-per-component renderer. That is the minimum viable Storyblok site.

Implementation Patterns

Wrap the client in a service, not a global. Bind a single configured instance in your container (or use Storyblok\ClientFactory::create($token)), then inject Client into repositories/controllers. Keep token retrieval, version selection, and resolveLinks()/resolveRelations() defaults in that factory so every call site behaves consistently.

// config/services.php / bootstrap
$client = new Client(config('storyblok.token'));
$client->setVersion(config('storyblok.version'))   // 'published' | 'draft'
       ->resolveLinks('url');                      // resolve link fields to hrefs

Repository layer returning plain arrays. Convert ApiResponse to arrays at the boundary. Domain code should never touch a PSR-7 response. This also makes caching trivial because arrays serialize cleanly.

final class StoryRepository
{
    public function __construct(private Client $client) {}

    public function bySlug(string $slug): ?array
    {
        $res = $this->client->getStory($slug);
        return $res->getStatusCode() === 200 ? $res->getBody()['story'] : null;
    }

    public function folder(string $prefix, int $page = 1, int $perPage = 100): array
    {
        return $this->client->getStories([
            'starts_with' => $prefix,
            'page'        => $page,
            'per_page'    => $perPage,
            'sort_by'     => 'content.published_at:desc',
        ])->getBody();
    }
}

Pagination and headers. Storyblok caps per_page at 100 (default 25). The total counts live in the response headers, so read them rather than looping until an empty page if you need an accurate count:

$res = $client->getStories(['starts_with' => 'blog/', 'per_page' => 100, 'page' => 1]);
$total = (int) ($res->getHeaders()['total'][0] ?? 0);

Relation resolution is two-step. Collect UUIDs from your component fields and pass them back on a second request, or set them on the client before the call:

$client->resolveRelations('uuid-a,uuid-b');   // or an array, depending on version
$story = $client->getStory('blog/post')->getBody()['story'];

For lists, collect UUIDs across all returned stories, then call getStories(['by_uuids' => implode(',', $ids)]) or re-request with resolve_relations set. The client does not recursively walk the tree for you.

Navigation via getLinks(). Fetch once per layout, key by uuid, and use it to build menus and breadcrumbs without N+1 story lookups.

Caching with webhook invalidation. Cache the getBody() payload under a key that includes token scope, version, locale, slug/prefix, and a hash of the params. In Laravel, use tagged cache keys or a versioned prefix bumped by a Storyblok webhook on story.published / story.unpublished:

$key = "sb:{$version}:{$locale}:story:{$slug}";
$story = Cache::remember($key, now()->addHour(), fn () => $repo->bySlug($slug));

Localisation. Pass language (and optionally fallback_lang) as query params per request, and cache each locale separately. Don't mutate a shared client's language between requests in a long-running worker — set it explicitly per call or clone the client.

Rendering. This package is data-only. Keep a map of component name → template/Blade partial and recurse through content.body arrays. Unknown components should render a visible placeholder in dev and nothing in prod.

Testing. Point the client's endpoint at a local stub, or inject a PSR-18 mock client and return fixture JSON captured from real API responses. Fixtures from the Storyblok UI ("JSON" tab) are the fastest path to realistic tests.

Gotchas and Tips

  • Public vs preview tokens are not interchangeable. setVersion('draft') with a public token returns 401/404 rather than an obvious error. Keep two configured clients (a cached production one, an uncached preview one) and never expose the preview token to a browser.
  • Don't cache the ApiResponse object. It wraps a consumed PSR-7 response/stream and will not serialize usefully. Always extract getBody() first.
  • 404s may be exceptions, not responses. Depending on the HTTP transport and your error middleware, getStory() on a missing slug can throw a ClientException/HTTP exception before you can inspect the status code. Wrap lookups in try/catch if you need "not found → 404 page" behaviour, or install a non-throwing HTTP client.
  • story can be null, stories can be []. Guard for isset($body['story']); don't assume the key exists on failed-but-200 responses.
  • filter_query is Storyblok's own mini-language (content.price:gt:100, content.tag:in:['a','b']) and is not validated client-side. A malformed query silently returns unfiltered or empty results — test these against the API directly before blaming the client.
  • resolveLinks('url') vs 'story' vs '0'/'1' produce different shapes (href string vs full story array). Pick one convention project-wide or your templates will fork.
  • Long-lived processes leak client state. Under Octane/RoadRunner/Swoole, setVersion, setLanguage, resolveRelations, and resolveLinks mutate the instance. Reset (or re-resolve from the container) at the start of each request/job.
  • Set a timeout and add retries. Storyblok occasionally returns 429/5xx. Configure the transport timeout (a few seconds is enough) and retry with exponential backoff; don't let a CDN hiccup stall a page render.
  • Relation UUIDs are usually nested inside component arrays, often inside blocks and repeated fields. Walk the tree recursively to collect them; a top-level scan misses most relations.
  • per_page above 100 is silently clamped, so pagination loops based on "fewer results than requested" terminate early and skip data. Use the total header with an explicit page counter.
  • The cv (cache version) query param can be used to bust Storyblok's CDN when a webhook fires; without it you may serve stale content for a short window even after publishing.
  • Extension points are the HTTP client and endpoint. Injecting a custom PSR-18 client is the clean way to add logging, tracing, retries, or a circuit breaker; setting a custom endpoint is the clean way to stub responses in tests. Subclassing Client for per-endpoint helpers is fine, but keep the transport concerns in the injected client.
  • Debugging tip: $response->getResponse() exposes the underlying PSR-7 request/response — log the final URI to see exactly which params the client serialised. That resolves 90% of "why is my filter ignored" questions immediately.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
ably/pubsub-server
mstfkhazaal/defender
mfonte/base62x
karnoweb/metapilot
tobya/bcsapiwrapper
swalker2/cpanel
vish4395/laravel-translation-manager
morningtrain/economic
teksite/systeminfo-laravel
atomcms/voting
run6/weather
siokas/laravelembeddirectives
adamlien/pkuuids
gilsonsouza/lara-doc-generator