A Symfony bundle to resolve [shortcode]
markup in Twig templates, using the thunderer/Shortcode library.
It allows you to define shortcodes and their replacements in a jiffy. Shortcodes are special text fragments that can be replaced with other content or markup. E.g. a user could use the following in a comment:
[image url="https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png"]
[text color="red"]This is red text.[/text]
In analogy to living style guides, this bundle provides a shortcode guide that lists all registered shortcodes with an optional description and example.
As usual, install via Composer and register the bundle in your application:
composer require webfactory/shortcode-bundle
<?php
// config/bundles.php
public function registerBundles()
{
return [
// ...
Webfactory\ShortcodeBundle\WebfactoryShortcodeBundle::class => ['all' => true],
// ...
];
// ...
}
The bundle will set up a shortcodes
Twig filter. What you pass through this filter will be processed by the Processor
class (see docs).
{% apply shortcodes %}
[image url="https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png"]
[text color="red"]This is red text.[/text]
{% endapply %}
{{ some_content |shortcodes }}
This bundle comes with a helper class that allows to use Symfony's Fragment Sub-Framework and the technique of embedding controllers to have controllers generate the replacement output for shortcodes.
To give an example, assume the following configuration:
# config.yml
webfactory_shortcode:
shortcodes:
image: AppBundle\Controller\EmbeddedImageController::show
Then, when doing something like this in Twig:
{{ '[image url="https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png"]' |shortcodes }}
... the AppBundle\Controller\EmbeddedImageController::show()
controller method will be called. Additional shortcode attributes, like url
in the above example, will be passed as parameters to the controller. The response returned by the controller will be used to replace the shortcode in the given content. The controller can generate the response directly, or use Twig to render a template to create it.
You can also use ESI rendering for particular shortcodes. The advantage of ESI is that single shortcode replacements can be stored in edge caches and/or reverse proxies like Varnish and possibly be reused on multiple pages.
Request
visible to controllers is no longer the one where the shortcode was used. Keep that in mind when you, for example, want to log the URLs where shortcodes are being used.
To use ESI-based embedding for a particular shortcode, use a configuration like the following:
# config.yml
webfactory_shortcodes:
shortcodes:
image:
controller: AppBundle\Controller\EmbeddedImageController::showAction
method: esi
In the thunderer/Shortcode package, handlers transform shortcodes into desired replacements. You can register services from the Symfony Dependency Injection Container to be used as shortcode handlers by tagging them with webfactory.shortcode
and adding a shortcode
attribute to the tag indicating the shortcode name.
services:
My\Shortcode\Handler\Service:
tags:
- { name: 'webfactory.shortcode', shortcode: 'my-shortcode-name' }
By default, the RemoveWrappingParagraphElementsEventHandler
contained in this bundle will be used to remove <p>...</p>
tags around shortcodes, if the shortcode is the only text content in that paragraph.
The optional Shortcode Guide is a controller providing an overview page of all configured shortcodes. For every shortcode, there is also a detail page including a rendered example.
To use the Shortcode Guide, include the routing configuration from @WebfactoryShortcodeBundle/Resources/config/guide-routing.xml
.
dev
and/or test
environment, and possibly restrict access in your security configuration in addition to that.
# src/routing.yml
_shortcode-guide:
prefix: /shortcodes
resource: "@WebfactoryShortcodeBundle/Resources/config/guide-routing.xml"
With the route prefix defined as above, visit /shortcodes/
to see a list of all defined shortcodes. If you want to add descriptions to shortcodes and/or provide the example shortcode that shall be rendered on the detail page, you can add this information when configuring shortcodes:
# config.yml
webfactory_shortcodes:
shortcodes:
image:
controller: AppBundle\Controller\EmbeddedImageController::showAction
description: "Renders an image tag with the {url} as it's source."
example: "image url=https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png"
In most cases, the default values should work fine. But you might want to configure something else, e.g. if the default parser needs too much memory for a large snippet. See thunderer's documentation on parsing and configuration so you understand the advantages, disadvantages and limitations:
# config.yml
webfactory_shortcode:
parser: 'regex' # default: regular
recursion_depth: 2 # default: null
max_iterations: 2 # default: null
This section provides a few hints and starting pointers on testing your shortcode handlers and bundle configuration.
In general, try to start with unit testing your shortcode handlers directly.
No matter whether your handler is a simple class implementing the __invoke
magic method or a Symfony Controller with one or several methods: Direct unit tests are the easiest way to have full control over the handler's (or controller's) input, and to get immediate access to its return value. This allows you to test also a broader range of input parameters and verify the outcomes. In this case, you will typically use Mock Objects to substitute some or all other classes and services your handler depends upon.
If your shortcode handler produces HTML output, the Symfony DomCrawler might be helpful to perform assertions on the HTML structure and content.
When using a controller to handle a shortcode, and the controller uses Twig for rendering, you might want to do a full functional (integration) test instead of mocking the Twig engine.
The Symfony documentation describes how Application Tests can be performed. This approach, however, is probably not suited for your shortcode controllers since these typically are not reachable through routes and so you cannot perform direct HTTP requests against them.
Instead, write an integration test where you retrieve the controller as a service from the Dependency Injection Container and invoke the appropriate method on it directly. Then, just like described in the section before, perform assertions on the Response returned by the controller.
Here is an example of what a test might look like.
<?php
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class MyShortcodeControllerTest extends KernelTestCase
{
public function test_renderImageAction_returns_img(): void
{
// Assume the controller is used to turn `[img id=42]` into some HTML markup
// create fixture/setup image with ID 42 in the database or similar
// ...
// Exercise controller method
$container = static::getContainer();
$controller = $container->get(MyShortcodeController::class);
$response = $controller->renderImageAction(42);
// Verify outcome
self::assertStringContainsString('<img src="..." />', (string) $response->getContent());
}
}
After you have written some tests that verify your handlers work as expected for different input parameters or other circumstances (e. g. database content), you also want to make sure a given handler is registered correctly and connected with the right shortcode name. Since we are now concerned with how this bundle, your configuration and your handlers all play together, we're in the realm of integration testing. These tests will be slower, since we need to boot a Symfony Kernel, fetch services from the Dependency Injection Container and test how various parts play together.
This bundle contains the \Webfactory\ShortcodeBundle\Test\ShortcodeDefinitionTestHelper
class and a public service of the same name. Depending on the degree of test specifity you prefer, you can use this service to verify that...
- A given shortcode name is known, i. e. a handler has been set up for it
- Retrieve the handler for a given shortcode name, so you can for example perform assertions on the class being used
- When using controllers as shortcode handlers, test if the controller reference for a given shortcode can be resolved (the controller actually exists)
- Retrieve an instance of the controller to perform assertions on it.
For all these tests, you probably need to use KernelTestCase
as your test base class (documentation). Basically, you will need to boot the kernel, then get the ShortcodeDefinitionTestHelper
from the container and use its methods to check your shortcode configuration.
Maybe you want to have a look at the tests for ShortcodeDefinitionTestHelper
itself to see a few examples of how this class can be used.
Remember – this type of test should not test all the possible inputs and outputs for your handlers; you've already covered that with more specific, direct tests. In this test layer, we're only concerned with making sure all the single parts are connected correctly.
If, for some reason, you would like to do a full end-to-end test for shortcode processing, from a given string containing shortcode markup to the processed result, have a look at the \Webfactory\ShortcodeBundle\Test\EndToEndTestHelper
class.
This helper class can be used in integration test cases and will do the full shortcode processing on a given input, including dispatching sub-requests to controllers used as shortcode handlers.
A test might look like this:
<?php
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Webfactory\ShortcodeBundle\Test\EndToEndTestHelper;
class MyFullScaleTest extends KernelTestCase
{
/** @test */
public function replace_text_color(): void
{
self::bootKernel();
$result = EndToEndTestHelper::createFromContainer(static::$container)->processShortcode('[text color="red"]This is red text.[/text]');
self::assertSame('<span style="color: red;">This is red text.</span>', $result);
}
}
Assuming that your application configuration registers a handler for the text
shortcode, which might also be a controller, this test will perform a full-stack test.
This bundle was started at webfactory GmbH, Bonn.
Copyright 2018-2023 webfactory GmbH, Bonn. Code released under the MIT license.