Storyblok
Search Storyblok's Documentation
  1. Integrate Symfony with Storyblok

Integrate Symfony with Storyblok

Use Storyblok to manage the content of your website built with Symfony.

This guide has been tested with the following package versions:

  • symfony/framework-bundle@7.3.*
  • storyblok/symfony-bundle@1.5.*
  • storyblok/php-content-api-client@1.3.*
  • PHP 8.4

Setup

Create a new Symfony project by following the Installation page from its official documentation.

If you already have a Storyblok account, go to app.storyblok.com or log in with GitHub to continue.

Create a new blank space to follow the tutorial from scratch, or start from the core blueprint.


Installation

In the terminal, cd into your Symfony project and install the Storyblok packages.

composer require storyblok/php-content-api-client storyblok/symfony-bundle

This guide uses Symfony Flex recipe, which automatically adds configuration files to your project.

In the root of your project, create a .env file with the access token from your space.

.env
###> storyblok/symfony-bundle ###
STORYBLOK_API_BASE_URI=https://api.storyblok.com
STORYBLOK_API_TOKEN=your_storyblok_api_token
STORYBLOK_VERSION=draft # or published
###< storyblok/symfony-bundle ###

Learn how to get an access token for your Storyblok project.

Fetch a single story

Create a Page content type to fetch and render all stories of the page content type.

src/ContentType/Page.php
<?php

declare(strict_types=1);

namespace App\\\\ContentType;

use Storyblok\\\\Bundle\\\\ContentType\\\\ContentType;

final readonly class Page extends ContentType
{
	public string $uuid;
	public string $name;
	public string $slug;
	public array $body;
	public \\\\DateTimeImmutable $publishedAt;

	public function __construct(array $values)
	{
		$this->uuid = $values['uuid'];
		$this->name = $values['name'];
		$this->slug = $values['full_slug'];
		$this->body = $values['content']['body'] ?? [];
		$this->publishedAt = new \\\\DateTimeImmutable($values['published_at']);
	}

	public function publishedAt(): \\\\DateTimeImmutable
	{
		return $this->publishedAt;
	}
}

Create a controller for your content type using the #[AsContentTypeController] attribute.

src/Controller/PageController.php
<?php

declare(strict_types=1);

namespace App\\\\Controller;

use App\\\\ContentType\\\\Page;
use Storyblok\\\\Bundle\\\\ContentType\\\\Attribute\\\\AsContentTypeController;
use Symfony\\\\Bundle\\\\FrameworkBundle\\\\Controller\\\\AbstractController;
use Symfony\\\\Component\\\\HttpFoundation\\\\Request;
use Symfony\\\\Component\\\\HttpFoundation\\\\Response;

#[AsContentTypeController(contentType: Page::class)]
final class PageController extends AbstractController
{
	public function __invoke(Request $request, Page $page): Response
	{
		return $this->render('page.html.twig', [
			'page' => $page,
		]);
	}
}

The #[AsContentTypeController] attribute automatically registers the controller to handle requests for the specified content type.

Add the Twig templates used during rendering. Create a base layout at templates/base.html.twig and a page template at templates/page.html.twig.

templates/base.html.twig
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		{% block stylesheets %}
		{% endblock %}
		{% block javascripts %}
		{% endblock %}
	</head>
	<body>
		<div class="container">
			{% block body %}{% endblock %}
		</div>
	</body>
</html>
templates/base.html.twig
{% extends 'base.html.twig' %}

{% block body %}
	{% if page.body is defined and page.body is not empty %}
		{% for block in page.body %}
			{% if block is not null %}
				{{ block|render_block }}
			{% endif %}
		{% endfor %}
	{% endif %}
{% endblock %}

The render_block Twig filter automatically renders blocks using their registered templates.

Create blocks and templates

Stories might contain a body or similar field which consists of an array with several blocks of custom types (e.g. Feature, Teaser, Grid) in it.

src/Block/Feature.php
<?php

declare(strict_types=1);

namespace App\\\\Block;

use Storyblok\\\\Bundle\\\\Block\\\\Attribute\\\\AsBlock;

#[AsBlock]
final readonly class Feature
{
	public string $name;

	public function __construct(array $values)
	{
		$this->name = $values['name'] ?? '';
	}
}
src/Block/Teaser.php
<?php

declare(strict_types=1);

namespace App\\\\Block;

use Storyblok\\\\Bundle\\\\Block\\\\Attribute\\\\AsBlock;

#[AsBlock]
final readonly class Teaser
{
	public string $headline;

	public function __construct(array $values)
	{
		$this->headline = $values['headline'] ?? '';
	}
}
src/Block/Grid.php
<?php

declare(strict_types=1);

namespace App\\\\Block;

use Storyblok\\\\Bundle\\\\Block\\\\Attribute\\\\AsBlock;

#[AsBlock]
final readonly class Grid
{
	public array $columns;

	public function __construct(array $values)
	{
		$this->columns = $values['columns'] ?? [];
	}
}

The #[AsBlock] attribute automatically registers blocks for rendering. The template for each block will default to blocks/{name}.html.twig.

The bundle will look for templates in the templates/blocks/ directory by default, create them as follow.

src/Block/Feature.php
<div class="feature">
	<span>{{ block.name }}</span>
</div>
src/Block/Teaser.php
<div class="teaser">
	<h2>{{ block.headline }}</h2>
</div>
src/Block/Grid.php
<div class="grid">
	{% for column in block.columns %}
		<div>
			{{ column|render_block }}
		</div>
	{% endfor %}
</div>

Similar to the Page template, the Grid template iterates over the columns block field and renders nested blocks.

Run your Symfony development server and go to the /home route in your browser.

symfony server:start