Skip to content

Symfony Integration

This guide covers Symfony integration options for djot-php.

For most projects, use the official symfony-djot bundle which provides:

  • Twig filters (|djot, |djot_text) and function (djot())
  • Service injection via DjotConverterInterface
  • Multiple converter profiles with different configurations
  • Form type (DjotType) for Djot-enabled form fields
  • Validation constraint (ValidDjot)
  • Output caching
  • All djot-php extensions configurable via YAML
bash
composer require php-collective/symfony-djot
twig
{# Use immediately in templates #}
{{ article.body|djot }}
yaml
# config/packages/symfony_djot.yaml
symfony_djot:
    converters:
        default:
            safe_mode: false
            extensions:
                - type: autolink
                - type: external_links
        user_content:
            safe_mode: true

See the symfony-djot documentation for full configuration options.


Manual Integration

For advanced use cases or full control over the integration, you can set up djot-php manually.

Installation

bash
composer require php-collective/djot

Basic Setup

1. Create the Converter Factory

The factory creates configured DjotConverter instances and allows extensions to be injected via Symfony's service container:

php
// src/Djot/DjotConverterFactory.php
<?php

declare(strict_types=1);

namespace App\Djot;

use Djot\DjotConverter;
use Djot\Extension\ExtensionInterface;

final class DjotConverterFactory
{
    /**
     * @param iterable<ExtensionInterface> $extensions
     */
    public function __construct(
        private iterable $extensions = [],
        private bool $safeMode = true,
    ) {
    }

    public function __invoke(): DjotConverter
    {
        $converter = new DjotConverter(safeMode: $this->safeMode);

        foreach ($this->extensions as $extension) {
            $converter->addExtension($extension);
        }

        return $converter;
    }
}

2. Create the Twig Extension

php
// src/Twig/DjotExtension.php
<?php

declare(strict_types=1);

namespace App\Twig;

use App\Djot\DjotConverterFactory;
use Djot\DjotConverter;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

final class DjotExtension extends AbstractExtension
{
    private ?DjotConverter $converter = null;

    public function __construct(
        private DjotConverterFactory $converterFactory,
    ) {
    }

    public function getFilters(): array
    {
        return [
            new TwigFilter('djot_to_html', $this->convert(...), ['is_safe' => ['html']]),
        ];
    }

    public function convert(string $content): string
    {
        $this->converter ??= ($this->converterFactory)();

        return $this->converter->convert($content);
    }
}

3. Configure Services

yaml
# config/services.yaml
services:
    # Auto-tag any Djot extension classes
    _instanceof:
        Djot\Extension\ExtensionInterface:
            tags: ['app.djot_extension']

    App\Djot\DjotConverterFactory:
        arguments:
            $extensions: !tagged app.djot_extension
            $safeMode: true

    App\Twig\DjotExtension:
        tags: ['twig.extension']

4. Use in Templates

twig
{{ article.body|djot_to_html }}

{# Or with a variable #}
{% set content %}
# Hello World

This is *bold* and _italic_ text.
{% endset %}
{{ content|djot_to_html }}

Adding Extensions

Djot-php includes several built-in extensions. To use them, register them as services:

yaml
# config/services.yaml
services:
    Djot\Extension\ExternalLinksExtension:
        arguments:
            $openInNewTab: true
            $addNofollow: true
        tags: ['app.djot_extension']

    Djot\Extension\TableOfContentsExtension:
        tags: ['app.djot_extension']

    Djot\Extension\HeadingPermalinksExtension:
        tags: ['app.djot_extension']

Extensions are automatically injected into the factory via the !tagged app.djot_extension argument.

Configuration Options

The DjotConverter constructor accepts many options. Some are simple scalars (configurable directly in YAML), while others are objects that must be injected as services.

OptionTypeYAML scalar?
xhtmlbool
warningsbool
strictbool
safeModebool
safeModeSafeMode✗ (service)
significantNewlinesbool
roundTripModebool
profileProfile✗ (service)
softBreakModeSoftBreakMode✗ (service)
parserBlockParser✗ (service)
rendererRendererInterface✗ (service)

Simple Scalar Options

For boolean options, extend the factory constructor:

php
// src/Djot/DjotConverterFactory.php
public function __construct(
    private iterable $extensions = [],
    private bool $safeMode = true,
    private bool $xhtml = false,
    private bool $warnings = false,
    private bool $strict = false,
    private bool $significantNewlines = false,
    private bool $roundTripMode = false,
) {
}

public function __invoke(): DjotConverter
{
    $converter = new DjotConverter(
        xhtml: $this->xhtml,
        warnings: $this->warnings,
        strict: $this->strict,
        safeMode: $this->safeMode,
        significantNewlines: $this->significantNewlines,
        roundTripMode: $this->roundTripMode,
    );

    foreach ($this->extensions as $extension) {
        $converter->addExtension($extension);
    }

    return $converter;
}
yaml
# config/services.yaml
App\Djot\DjotConverterFactory:
    arguments:
        $extensions: !tagged app.djot_extension
        $safeMode: true
        $xhtml: false
        $warnings: false
        $strict: false
        $significantNewlines: false
        $roundTripMode: false

WARNING

Only disable safe mode when rendering content from trusted sources (e.g., admin users, your own database). Never disable it for user-submitted content.

Advanced Configuration

For object-typed options, define them as services and inject them into the factory.

Here's a factory that supports all configuration options:

php
// src/Djot/DjotConverterFactory.php
<?php

declare(strict_types=1);

namespace App\Djot;

use Djot\DjotConverter;
use Djot\Extension\ExtensionInterface;
use Djot\Parser\BlockParser;
use Djot\Profile;
use Djot\Renderer\RendererInterface;
use Djot\Renderer\SoftBreakMode;
use Djot\SafeMode;

final class DjotConverterFactory
{
    /**
     * @param iterable<ExtensionInterface> $extensions
     */
    public function __construct(
        private iterable $extensions = [],
        // Scalar options
        private bool $xhtml = false,
        private bool $warnings = false,
        private bool $strict = false,
        private bool $safeMode = true,
        private bool $significantNewlines = false,
        private bool $roundTripMode = false,
        // Object options (injected as services)
        private ?SafeMode $safeModeConfig = null,
        private ?Profile $profile = null,
        private ?SoftBreakMode $softBreakMode = null,
        private ?BlockParser $parser = null,
        private ?RendererInterface $renderer = null,
    ) {
    }

    public function __invoke(): DjotConverter
    {
        // Use SafeMode object if provided, otherwise use bool
        $safeMode = $this->safeModeConfig ?? $this->safeMode;

        $converter = new DjotConverter(
            xhtml: $this->xhtml,
            warnings: $this->warnings,
            strict: $this->strict,
            safeMode: $safeMode,
            profile: $this->profile,
            significantNewlines: $this->significantNewlines,
            softBreakMode: $this->softBreakMode,
            roundTripMode: $this->roundTripMode,
            parser: $this->parser,
            renderer: $this->renderer,
        );

        foreach ($this->extensions as $extension) {
            $converter->addExtension($extension);
        }

        return $converter;
    }
}

Custom SafeMode

Configure SafeMode with custom settings:

yaml
# config/services.yaml
services:
    # Use default safe mode settings
    app.djot.safe_mode.default:
        class: Djot\SafeMode
        factory: ['Djot\SafeMode', 'defaults']

    # Or create custom safe mode
    app.djot.safe_mode.custom:
        class: Djot\SafeMode
        factory: ['Djot\SafeMode', 'create']
        arguments:
            $blockedSchemes: ['javascript', 'vbscript', 'data']
            $blockedAttributes: ['onclick', 'onerror', 'onload']

    App\Djot\DjotConverterFactory:
        arguments:
            $extensions: !tagged app.djot_extension
            $safeModeConfig: '@app.djot.safe_mode.default'

Profiles

Restrict available Djot features using profiles:

yaml
# config/services.yaml
services:
    # Basic profile - minimal features
    app.djot.profile.basic:
        class: Djot\Profile
        factory: ['Djot\Profile', 'basic']

    # Standard profile - common features
    app.djot.profile.standard:
        class: Djot\Profile
        factory: ['Djot\Profile', 'standard']

    # Comments profile - for comment sections
    app.djot.profile.comments:
        class: Djot\Profile
        factory: ['Djot\Profile', 'comments']

    App\Djot\DjotConverterFactory:
        arguments:
            $extensions: !tagged app.djot_extension
            $safeMode: true
            $profile: '@app.djot.profile.comments'

SoftBreakMode

Control how soft line breaks are rendered:

yaml
# config/services.yaml
services:
    # Ignore soft breaks (default)
    app.djot.soft_break.ignore:
        class: Djot\Renderer\SoftBreakMode
        factory: ['Djot\Renderer\SoftBreakMode', 'from']
        arguments: ['ignore']

    # Render as <br>
    app.djot.soft_break.br:
        class: Djot\Renderer\SoftBreakMode
        factory: ['Djot\Renderer\SoftBreakMode', 'from']
        arguments: ['br']

    # Preserve as newlines
    app.djot.soft_break.preserve:
        class: Djot\Renderer\SoftBreakMode
        factory: ['Djot\Renderer\SoftBreakMode', 'from']
        arguments: ['preserve']

    App\Djot\DjotConverterFactory:
        arguments:
            $extensions: !tagged app.djot_extension
            $safeMode: true
            $softBreakMode: '@app.djot.soft_break.br'

Custom Parser

Inject a pre-configured parser:

yaml
# config/services.yaml
services:
    app.djot.parser:
        class: Djot\Parser\BlockParser
        arguments:
            $collectWarnings: true
            $strictMode: false
            $significantNewlines: true

    App\Djot\DjotConverterFactory:
        arguments:
            $extensions: !tagged app.djot_extension
            $safeMode: true
            $parser: '@app.djot.parser'

TIP

When providing a custom parser, the warnings, strict, and significantNewlines scalar options are ignored—the parser's own configuration takes precedence.

Custom Renderer

Use a different renderer (e.g., Markdown, PlainText, ANSI):

yaml
# config/services.yaml
services:
    # HTML renderer with XHTML output
    app.djot.renderer.html:
        class: Djot\Renderer\HtmlRenderer
        arguments:
            $xhtml: true

    # Markdown renderer
    app.djot.renderer.markdown:
        class: Djot\Renderer\MarkdownRenderer

    # Plain text renderer
    app.djot.renderer.plaintext:
        class: Djot\Renderer\PlainTextRenderer

    # ANSI renderer (for terminal output)
    app.djot.renderer.ansi:
        class: Djot\Renderer\AnsiRenderer

    App\Djot\DjotConverterFactory:
        arguments:
            $extensions: !tagged app.djot_extension
            $safeMode: true
            $renderer: '@app.djot.renderer.html'

TIP

When providing a custom renderer, the xhtml, safeMode, softBreakMode, and roundTripMode scalar options are ignored—configure them on the renderer directly.

Custom Extensions with Services

If you create a custom extension that requires injected services, the factory pattern handles this cleanly:

php
// src/Djot/Extension/MyEmbedExtension.php
<?php

namespace App\Djot\Extension;

use App\Service\EmbedService;
use Djot\DjotConverter;
use Djot\Extension\ExtensionInterface;

final class MyEmbedExtension implements ExtensionInterface
{
    public function __construct(
        private EmbedService $embedService,
    ) {
    }

    public function register(DjotConverter $converter): void
    {
        $converter->on('render.link', function ($event) {
            // Use $this->embedService to process embeds
        });
    }
}
yaml
# config/services.yaml
services:
    App\Djot\Extension\MyEmbedExtension:
        arguments:
            $embedService: '@App\Service\EmbedService'
        tags: ['app.djot_extension']

The extension is autowired with its dependencies and automatically added to the converter.

Multiple Converters

For different rendering contexts (e.g., full HTML vs. excerpt), create multiple factories:

yaml
# config/services.yaml
services:
    App\Djot\DjotConverterFactory:
        arguments:
            $extensions: !tagged app.djot_extension
            $safeMode: true

    app.djot.excerpt_factory:
        class: App\Djot\DjotConverterFactory
        arguments:
            $extensions: !tagged app.djot_extension.excerpt
            $safeMode: true

Then create separate Twig filters or use different factories in your services.

Released under the MIT License.