Content Modeling in Symfony
Learn how to handle different content type and nestable blocks, render rich text, and use story references to manage content globally.
Setup
Copy this reference space, providing the intended structure to follow this guide. Make sure to update the access token.
Alternatively, in the existing space, create a new content type block article
and an “Articles” folder with content. The article content type block should have the following fields:
title
: Textcontent
: Rich text
Learn more about fields in the concept.
Create an article-overview
story as a page type content.
Finally, create a featured-articles
nestable block with the following field:
articles
: References
Add a new featured-articles
block to the body
field of the home story and select some articles to be featured.
Fetch and list all articles
Create a new controller to get all stories from this new content type.
<?php
declare(strict_types=1);
namespace App\\\\Controller;
use App\\\\ContentType\\\\ArticleOverview;
use Storyblok\\\\Api\\\\StoriesApiInterface;
use Storyblok\\\\Api\\\\Request\\\\StoriesRequest;
use Storyblok\\\\Bundle\\\\ContentType\\\\Attribute\\\\AsContentTypeController;
use Symfony\\\\Bundle\\\\FrameworkBundle\\\\Controller\\\\AbstractController;
use Symfony\\\\Component\\\\HttpFoundation\\\\Request;
use Symfony\\\\Component\\\\HttpFoundation\\\\Response;
#[AsContentTypeController(contentType: ArticleOverview::class)]
final class ArticleOverviewController extends AbstractController
{
public function __construct(
private readonly StoriesApiInterface $storiesApi
) {
}
public function __invoke(Request $request, ArticleOverview $articleOverview): Response
{
$response = $this->storiesApi->allByContentType('article');
return $this->render('article_overview.html.twig', [
'articleOverview' => $articleOverview,
'articles' => $response->stories,
]);
}
}
Learn more about parameters and filter queries in the Content Delivery API documentation.
Create a new content type and template file.
<?php
declare(strict_types=1);
namespace App\\\\ContentType;
use Storyblok\\\\Bundle\\\\ContentType\\\\ContentType;
final readonly class ArticleOverview extends ContentType
{
public string $uuid;
public string $name;
public string $slug;
public \\\\DateTimeImmutable $publishedAt;
public function __construct(array $values)
{
$this->uuid = $values['uuid'];
$this->name = $values['name'];
$this->slug = $values['full_slug'];
$this->publishedAt = new \\\\DateTimeImmutable($values['published_at']);
}
public function publishedAt(): \\\\DateTimeImmutable
{
return $this->publishedAt;
}
public static function type(): string
{
return 'article-overview';
}
}
Override the type()
method to specify this content type’s name. By default, the type()
method returns the class name in snake_case
, which in our example would be incorrect.
{% extends 'base.html.twig' %}
{% block title %}Articles - {{ parent() }}{% endblock %}
{% block body %}
<h1>Articles</h1>
<ul>
{% for article in articles %}
{% if article is iterable and article.full_slug is defined %}
<li>
<a href="/{{ article.full_slug }}">{{ article.name }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endblock %}
Now, the article overview page shows a list of links to all articles.
Create the article block
Add a new content type class and controller to render article stories.
<?php
declare(strict_types=1);
namespace App\\\\Controller;
use App\\\\ContentType\\\\Article;
use Storyblok\\\\Bundle\\\\ContentType\\\\Attribute\\\\AsContentTypeController;
use Symfony\\\\Bundle\\\\FrameworkBundle\\\\Controller\\\\AbstractController;
use Symfony\\\\Component\\\\HttpFoundation\\\\Request;
use Symfony\\\\Component\\\\HttpFoundation\\\\Response;
#[AsContentTypeController(contentType: Article::class)]
final class ArticleController extends AbstractController
{
public function __invoke(Request $request, Article $article): Response
{
return $this->render('article.html.twig', [
'article' => $article,
]);
}
}
<?php
declare(strict_types=1);
namespace App\\\\ContentType;
use Storyblok\\\\Bundle\\\\ContentType\\\\ContentType;
final readonly class Article extends ContentType
{
public string $uuid;
public string $name;
public string $slug;
public string $title;
public array $content;
public \\\\DateTimeImmutable $publishedAt;
public function __construct(array $values)
{
$this->uuid = $values['uuid'];
$this->name = $values['name'];
$this->slug = $values['full_slug'];
$this->title = $values['content']['title'] ?? '';
$this->content = $values['content']['content'] ?? [];
$this->publishedAt = new \\\\DateTimeImmutable($values['published_at']);
}
public function publishedAt(): \\\\DateTimeImmutable
{
return $this->publishedAt;
}
}
Create the article template.
{% extends 'base.html.twig' %}
{% block body %}
<article>
<h1>{{ article.title }}</h1>
<div class="content">
{{ article.content|rich_text }}
</div>
</article>
{% endblock %}
To render rich text fields, the rich_text
Twig filter provided by the Storyblok Symfony bundle is used.
Learn more about handling rich text in Storyblok in the fields concept.
Handle referenced stories
In the PageController.php
file, inject the StoriesApiInterface
and set the resolve_relations
parameter to get the full object response of referenced stories.
<?php
declare(strict_types=1);
namespace App\\\\Controller;
use App\\\\ContentType\\\\Page;
use Storyblok\\\\Api\\\\StoriesApiInterface;
use Storyblok\\\\Api\\\\Request\\\\StoryRequest;
use Storyblok\\\\Api\\\\Domain\\\\Value\\\\Resolver\\\\RelationCollection;
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 __construct(
private readonly StoriesApiInterface $storiesApi
) {
}
public function __invoke(Request $request, Page $page): Response
{
$response = $this->storiesApi->bySlug(
$page->slug,
new StoryRequest(
withRelations: new RelationCollection(['featured-articles.articles'])
)
);
$resolvedPage = new Page($response->story);
return $this->render('page.html.twig', [
'page' => $page,
'page' => $resolvedPage,
]);
}
}
Learn more in the references concept documentation.
Next, create a block class to handle the featured articles.
<?php
declare(strict_types=1);
namespace App\\\\Block;
use Storyblok\\\\Bundle\\\\Block\\\\Attribute\\\\AsBlock;
#[AsBlock(name: 'featured-articles', template: 'blocks/featured_articles.html.twig')]
final readonly class FeaturedArticles
{
public array $articles;
public function __construct(array $values)
{
$this->articles = $values['articles'] ?? [];
}
}
Create the corresponding Twig template.
<section class="featured-articles">
<h2>Featured Articles</h2>
<ul>
{% for article in block.articles %}
{% if article is iterable and article.full_slug is defined %}
<li>
<a href="/{{ article.full_slug }}">{{ article.name }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</section>
Now, this component will render links to the featured articles in the home page of your project.
Next Part
Internatiolization with StoryblokPrevious Part
Dynamic Routing with Storyblok