---
title: Content Modeling in Symfony
description: Learn how to handle custom blocks, render rich text, and use story references to manage content across your Symfony project.
url: https://storyblok.com/docs/guides/symfony/content-modeling
---

# Content Modeling in Symfony

Learn how to handle different content types and nestable blocks, render rich text, and use story references to manage content globally.

## Setup

In the existing space, create the following blocks:

-   An `article` content type block with the following fields:
    -   `title`: Text
    -   `content`: Rich text
-   An `article-overview` content type block with the following field:
    -   `body`: Blocks
-   A `featured-articles` nestable block with the following field:
    -   `articles`: References

> [!NOTE]
> Learn more about fields in the [concept](/docs/concepts/fields).

Next, create an `Articles` folder, open it, and create the follwoing stories:

-   A few stories that use the `article` content type.
-   An article overview story with a `article-overview` content type. Select the option **Define as root for the folder**.

Finally, add the `featured-articles` block to the `body` field of both the **Home** and **Article overview** stories, and select articles to feature.

## Fetch and list all articles

In the Symfony project, create a new controller to get all the stories from the `article-overview` content type.

src/Controller/ArticleOverviewController.php

```php
<?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,
      ]);
    }
  }
```

> [!TIP]
> Learn more about parameters and filter queries in the [Content Delivery API documentation](https://www.storyblok.com/docs/api/content-delivery/v2).

Next, create a new content type and a matching template.

src/ContentType/ArticleOverview.php

```php
<?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';
  }
}
```

> [!WARNING]
> 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.

templates/article-overview.html.twig

```html
{% 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 %}
```

Run the Symfony development server and open `http://localhost:8000/articles` in the browser. The article overview page now shows a list of links to the selected articles.

## Create the article block

Add a new controller and content type class to render individual article-type stories.

src/Controller/ArticleController.php

```php
<?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,
    ]);
  }
}
```

src/ContentType/Article.php

```php
<?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;
    }
}
```

Finally, create the article template.

templates/article.html.twig

```html
{% extends 'base.html.twig' %}

{% block body %}
<article>
  <h1>{{ article.title }}</h1>
  <div class="content">
    {{ article.content|rich_text }}
  </div>
</article>
{% endblock %}
```

Use the `rich_text` Twig filter to propertly render the content in the rich text field.

> [!NOTE]
> Learn more about Storyblok's [rich text package](https://www.storyblok.com/docs/libraries/js/richtext).

## Handle referenced stories

The last step is to render the featured stories referenced in the home story. Update `PageController.php` to use `RelationCollection`.

  

src/Controller/PageController.php

```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 Storyblok\Api\Domain\Value\Resolver\Relation;
 use Storyblok\Api\Domain\Value\Resolver\RelationCollection;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

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

  

> [!NOTE]
> Learn more in the [References concept](https://www.storyblok.com/docs/concepts/references).

In `config/packages/storyblok.yaml`, uncomment `auto_resolve_relations`. Ensure it's set to `true` to get the full object response of referenced stories.

Next, create a block class to handle the featured articles.

src/Block/FeaturedArticles.php

```php
<?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'] ?? [];
  }
}
```

Then, create the corresponding Twig template.

templates/blocks/featured-articles.html.twig

```html
<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>
```

In the browser, navigate to the homepage and view the links to the featured articles.

## Pagination

-   [Previous: Dynamic Routing in Symfony](/docs/guides/symfony/dynamic-routing)
-   [Next: Internationalization in Symfony](/docs/guides/symfony/internationalization)
