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.