21torr/storyblok
API helpers and infrastructure for working with Storyblok from Symfony, simplifying content delivery and management.
First, the reality check: this is a Symfony bundle, not a Laravel package. It ships a Symfony bundle class, DI compiler passes, config tree, and (as of 5.3.0) a dependency on the 21TORR Hosting Bundle. There is no service provider, no Blade integration, no config/storyblok.php. If your day job is Laravel, you will not composer require this into an app and get value — you'd adopt it only if you also run a Symfony service (or are willing to stand one up as a content sidecar).
Assuming a Symfony context:
3.21.x line — pin accordingly and read the UPGRADE doc referenced in the 5.0.0 release notes, not the release blog posts.21torr-docs.pages.dev/docs/php/symfony/storyblok/. The README is a stub pointing there; there is no in-repo API reference. Budget time to read source and tests for anything the docs gloss over — with ~5 stars and 0 dependents, Stack Overflow will not save you.ContentApi/ManagementApi services were removed. Everything is now scoped to an adapter class extending AbstractStoryblokAdapter, identified by a key. Create exactly one adapter per space/environment combination (e.g. marketing_preview, marketing_live) and inject the adapter-scoped services.ContentApi fetch of a single story by slug, and render it through your normalized component classes (see next section). Don't start with the Management API or definitions sync — get read-path rendering working first.One adapter per space/environment, aliased by key. Adapter keys must be valid "snails" (there's an app validator that fails the container build otherwise), and since 5.1.1 the adapter key — not the space ID — is the webhook token. Keep a preview adapter strictly separate from the live one so draft content can never leak into production rendering; the adapter boundary is your safety rail, not a runtime flag.
Model content as typed component classes, not arrays. This is where the package actually pays for itself. Define a PHP class per Storyblok component, declare its fields with the bundle's field types, and let ComponentNormalizer hydrate JSON into typed objects:
NumberField — minValue / maxValue / decimals / steps, validated at hydrationChoiceField — single and multi-select, honours allowMissingDataRichTextField — allowLinksOpeningInNewWindow, allow_target_blank, allow_custom_attributesLinkField — allow_target_blank, allow_custom_attributes, fullData in every variantMarkdownField — allow_multilineBooleanField — inline_labelAssetField — useDescriptionAsAltThe leverage is that field config doubles as validation. Configure the range once and every render gets a properly typed value or a loud failure, instead of $block['price'] ?? 0 scattered across templates.
Definitions sync belongs in deploy, not in a request. Use the sync command in your deploy hook and subscribe to StoryblokDefinitionsSyncedEvent to clear caches / warm whatever you derive from the definitions. Treat the synced definitions as build artifacts.
Read path: prefer the fast endpoints and explicit content versions. ContentApi::fetchFoldersInPath() and fetchFolderTitleMap() use a faster endpoint and replaced the ManagementApi equivalents (deprecated in 5.2.0) — migrate if you have older code. When fetching single stories, pass cv explicitly; multiple patch releases exist purely because omitting it caused redirects and extra requests. Query params are sorted automatically now to avoid the same problem.
Asset handling through the proxy. AssetProxyUrlGenerator plus the extracted Storyblok asset URL parser service/DTO keep your access token out of templates and let you serve private/signed assets. If you're currently pasting a.storyblok.com URLs into markup, move to the proxy — it's the difference between a leaked token and not.
Routing and identity. StoryblokIdSlugMapper handles id↔slug resolution for routing. Pair it with the 5.1.2 change: all IDs from Storyblok are strings, so type your DTOs and route params as string and never cast to int.
Management API for write/edit tooling. fetchAllAssets() for asset audits, updateStory() with an array payload (the JSON-string form is deprecated), and sendRequest() is public since 5.1.0 for endpoints the bundle hasn't wrapped yet. Use sendPaginatedRequest() rather than hand-rolling pagination — 5.1.4 fixed invalid pagination handling there.
If you're translating this to Laravel: lift the pattern, not the code. A Storyblok service bound as a singleton per space, one hydrator/DTO class per component with a validation rules array mirroring the field config, a storyblok:sync-definitions artisan command in your deploy script, and an asset proxy route. That gets you ~80% of the value; the remaining 20% is the definitions diffing, which is genuinely fiddly to rebuild.
ContentApi/ManagementApi are gone. Any snippet that injects a global ContentApi or configures storyblok: at the bundle root is v3-era. Look for adapter-scoped injection.ContentApi and StoryblokIdSlugMapper." If you're on Messenger workers or anything with a services-resetter, verify the reset actually fires for your adapter services, and regression-test after upgrading.DateTimeField was fixed to parse as UTC; do the timezone conversion at your presentation edge and don't assume the field gives you local time.NumberField validation is finicky. Floating-point step checks were fixed twice (5.2.2 line and 5.3.2), steps <= 0 now rejects instead of dividing by zero, default values are normalized in 5.3.1, and negative minValue only works from 5.3.4 — where the range is also validated at hydration time. Pick a version deliberately and add a test story per configured range.int type hint on a Storyblok-sourced ID, including folder data (FolderData only gained id/uuid in 5.3.3, so older code may not surface them at all).DateTimeFields as UTC" and "explicit cv" entries in both). Decide which line you're on by runtime, pin exactly, and read UPGRADE on every major bump.How can I help you explore Laravel packages today?