Loading content

Stenope offers tools to fetch contents from local or distant sources, parse them and hydrate them as domain PHP objects.

To illustrate how that works, let's code a simple blog.

Setup

Create your model

Create a simple class that describe your model, here a blog Article:

<?php

namespace App\Model;

class Article {
    public string $slug;
    public string $title;
    public string $content;
    public \DateTimeInterface $created;
    public \DateTimeInterface $lastModified;
}

Register content provider

Register your model in config/packages/stenope.yaml by attributing a path to the model class:

stenope:
  providers:
    App\Model\Article: '%kernel.project_dir%/content/articles' # Local directory

Articles sources files are now expected to be found in the content/articles path.

Note: See other possible type of sources.

Write your first content

Write your first article in content/articles/how-to-train-your-dragon.md:

---
title: "How to train your dragon"
---

# This is Berk

It's twelve days north of Hopeless and a few degrees south of Freezing to Death. It's located solidly on the Meridian of Misery. My village. In a word, sturdy. It's been here for seven generations, but every single building is new. We've got hunting, fishing, and a charming view of the sunsets. The only problems are the pests. Most places have mice or mosquitoes. We have... dragons.

By default, the content of the source file are mapped on the content property (Stenope supports Markdown out of the box) and the name of the file is mapped on the slug property._

Note: Check out all the natively supported formats.

Usage

Listing contents

In your controller (or service):

<?php

namespace App\Controller;

use App\Model\Article;
use Stenope\Bundle\ContentManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/blog")
 */
class BlogController extends AbstractController
{
    /**
     * @Route("/", name="blog")
     */
    public function index(ContentManagerInterface $contentManager)
    {
        return $this->render(
            'blog/index.html.twig',
            ['articles' => $contentManager->getContents(Article::class)]
        );
    }

Note: contents of the same type can very well be writen in different formats.

Fetching a specific content

The content manager uses slugs to identify your content.

The slug argument must exactly match the static file name in your content directory.

Example: $contentManager->getContent(Article::class, 'how-to-train-your-dragon'); will fetch the content/articles/how-to-train-your-dragon.md article.

<?php

namespace App\Controller;

use App\Model\Article;
use Stenope\Bundle\ContentManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

#[Route('/blog')]
class BlogController extends AbstractController
{
    // ...

    #[Route('/{slug}', name: 'article')]
    public function article(ContentManagerInterface $contentManager, string $slug)
    {
        return $this->render(
            'blog/article.html.twig',
            ['article' => $contentManager->getContent(Article::class, $slug)]
        );
    }

Sorting contents

The getContents method have a second parameters $sortBy that allows sorting the content list.

It accepts:

A property name (string)

Alphabetically sorted categories:

$categories = $contentManager->getContents(
    Category::class,
    'title'
);

An array of properties and sorting mode

All articles sorted by descending date (most recent first) and then by ascending title (Alphabetically):

$latestArticles = $contentManager->getContents(
    Category::class,
    ['date' => false, 'title' => true]
);

A custom callable supported by the PHP usort function

$tasks = $contentManager->getContents(
    Task::class,
    fn($a, $b) => $a->priority <=> $b->priority
);

Filtering contents

The getContents method have a third parameters $filterBy that allows filtering the content list.

It accepts:

An array of properties and values

$articles = $contentManager->getContents(
    Article::class,
    null,
    ['category' => 'symfony']
);

When passing multiple requirements, all must be met:

$myDrafts = $contentManager->getContents(
    Article::class,
    null,
    ['author' => 'ogizanagi', 'draft' => true]
);

A custom callable supported by the PHP usort function

$taggedMobileArticles = $contentManager->getContents(
    Article::class,
    null,
    fn (Article $article): bool => in_array('mobile', $article->tags)
);

An ExpressionLanguage expression

use function Stenope\Bundle\ExpressionLanguage\expr;

$taggedMobileArticles = $contentManager->getContents(
    Article::class,
    null,
    expr('"mobile" in _.tags')
);

// expr() and exprOr() are optional syntax sugar,
// but you can also provide the expression as a string directly:
$activeUsers = $contentManager->getContents(User::class, '_.active'); // Equivalent to ['active' => true]
$activeDevUsers = $contentManager->getContents(User::class, '_.active and _.dev');

Note

expr accepts multiple expressions it'll combine using and.
Use exprOr to combine expressions using or.

See the ExpressionLanguage syntax.

You may also want to extend the expression language capabilities for your own contents by registering a custom expression provider tagged with stenope.expression_language_provider.

Built-in functions are:

  • date
  • datetime
  • upper
  • lower
  • keys

The following functions are also available:

  • contains
  • starts_with
  • ends_with

Note

but as of Symfony 6.1+, you should use the new starts with, ends with and contains operators instead.

Debug

See CLI - Debug

Advanced usage and extension

Register a custom denormalizer

Unless specified otherwise, Stenope will denormalize your objects using the default Symfony serializer.

For more control over your model denormalization, you can register your own Denormalizer.

Simply, create a service that implements Symfony's DenormalizerInterface and supports your model:

<?php

namespace App\Stenope\Denormalizer;

use App\Model\Article;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

class ArticleDenormalizer implements DenormalizerInterface
{
    /**
     * Must return true for supported models.
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        return is_a($type, Article::class, true);
    }

    /**
     * Instanciate your model from the denormalized data array.
     */
    public function denormalize($data, $class, $format = null, array $context = [])
    {
        return new Article(
            $data['title'],
            $data['slug'],
            $data['content'],
            new \DateTimeImmutable($data['date']),
            new \DateTimeImmutable($data['lastModified'])
        );
    }
}

Note: Using autowiring, denormalizers are automaticaly registered in Symfony serializer.