21torr/storyblok
Symfony bundle providing API helpers and infrastructure to work with Storyblok. Simplifies fetching content, integrating Storyblok services, and building Storyblok-powered Symfony apps. Includes documentation for setup and usage.
Installation:
composer require 21torr/storyblok
Configure in config/packages/storyblok.yaml:
storyblok:
adapters:
default:
space_id: 'your-space-id'
token: 'your-preview-token'
cache: true
cache_lifetime: 3600
First Use Case: Fetch a story in a controller:
use Storyblok\Bundle\StoryblokBundle\Api\ContentApi;
class PageController extends AbstractController
{
public function show(ContentApi $contentApi, string $slug): Response
{
$story = $contentApi->fetchStory('page', $slug);
return $this->render('page/show.html.twig', ['story' => $story]);
}
}
Twig Integration:
Enable the Twig extension in config/packages/twig.yaml:
twig:
globals:
storyblok: '@storyblok.twig.storyblok_extension'
Use in templates:
{{ storyblok.story('page', 'home')|raw }}
$story = $contentApi->fetchStory('page', 'home', ['version' => 'draft']);
php bin/console storyblok:clear-cache
config/packages/storyblok.yaml:
storyblok:
components:
page:
template: 'page/_page.html.twig'
hero:
template: 'components/hero.html.twig'
{% for block in story.body %}
{% include storyblok.component(block._uid) %}
{% endfor %}
$assetUrl = $assetProxyUrlGenerator->generateUrl($assetData, ['expires' => '+1 hour']);
<img src="{{ storyblok.asset_url(asset) }}" alt="{{ asset.name }}">
config/packages/storyblok.yaml:
storyblok:
webhooks:
default:
endpoint: '/storyblok/webhook'
events: ['published', 'unpublished']
use Storyblok\Bundle\StoryblokBundle\Event\StoryblokStoryPublishedEvent;
public function onStoryPublished(StoryblokStoryPublishedEvent $event): void
{
$this->cache->clear();
}
$richText = new RichTextField($story->body, [
'allowLinksOpeningInNewWindow' => true,
'allowCustomAttributes' => ['data-custom' => true],
]);
$assetField = new AssetField($story->image, ['useDescriptionAsAlt' => true]);
// src/Twig/StoryblokExtension.php
public function getFilters(): array
{
return [
new TwigFilter('storyblok_markdown', [$this->markdownConverter, 'convert']),
];
}
$builder->add('title', TextType::class, [
'data' => $story->title,
'attr' => ['storyblok_field' => 'title'],
]);
ContentApi for custom logic:
class CustomContentApi extends ContentApi
{
public function fetchLocalizedStory(string $component, string $slug, array $options = []): StoryData
{
$options['lang'] = $this->requestStack->getCurrentRequest()->getLocale();
return parent::fetchStory($component, $slug, $options);
}
}
$this->mock(ContentApi::class)
->shouldReceive('fetchStory')
->once()
->andReturn($mockStoryData);
ContentApi and ManagementApi are now adapter-specific.
// Old (v3/v4):
$story = $this->get('storyblok.content_api')->fetchStory(...);
// New (v5+):
$story = $this->get('storyblok.adapter.default.content_api')->fetchStory(...);
ContentApi::fetchFoldersInPath() instead of ManagementApi::fetchFoldersInPath().php bin/console storyblok:clear-cache
cache_lifetime in config to avoid stale data (default: 3600 seconds).allowMissingData is set if Storyblok fields might be empty:
$choiceField = new ChoiceField($story->tags, ['allowMissingData' => true]);
min/max options as strings (not integers) for ChoiceField.$url = $assetProxyUrlGenerator->generateUrl($asset, ['expires' => '+1 hour']);
filename is empty but the asset structure exists.storyblok:
adapters:
default:
adapter_key: 'your-adapter-key' # <-- This is the token!
storyblok:
debug: true
$response = $contentApi->sendRequest('GET', '/spaces/me/stories', []);
$this->logger->debug('Raw response:', ['data' => $response->getData()]);
php bin/console debug:container storyblok.component_discovery
php bin/console storyblok:debug
RichText parsing errors:
fullData is passed for links:
$richText = new RichTextField($story->body, ['fullData' => true]);
table requires table_styling config).NumberField validation:
minValue, maxValue, and decimals:
$numberField = new NumberField($story->price, [
'minValue' => '0',
'maxValue' => '1000',
'decimals' => '2',
]);
$stories = $contentApi->fetchStories(['component' => 'page'], ['per_page' => 100]);
fetchFoldersInPath() for faster folder traversal (v5.2.0+):
$folders = $contentApi->fetchFoldersInPath('/blog');
AbstractField:
class CustomField extends AbstractField
{
public function __construct(array $data, array $options = [])
{
parent::__construct($data, $options);
}
public function getValue(): mixed
{
return $this->data['custom_key'] ?? null;
}
}
storyblok:
fields:
custom:
class: App\Storyblok\CustomField
How can I help you explore Laravel packages today?