secretary/secretary-bundle
SecretaryBundle packages the Secretary library for Symfony, providing service configuration, dependency injection, and sensible defaults so you can integrate its features into PHP applications with minimal setup and clear extension points. It streamlines installation and keeps configuration flexible...
Secretary is a lightweight markup language (a Markdown-like syntax with its own .sct conventions) and this bundle is the thin Symfony integration around php-secretary. Treat it as a markup-rendering service, not as a full CMS.
Install the parser and the bundle
composer require secretary/secretary-bundle
The bundle depends on secretary/php-secretary, so the core parser comes along with it.
Register the bundle
config/bundles.php.app/AppKernel.php inside registerBundles().Look at the bundle's DI extension first. There is essentially no user documentation for a 2-star bundle. The authoritative reference is src/SecretaryBundle/DependencyInjection/*Extension.php (or config/ on newer releases) plus the Twig extension class. That tells you the service id, the config tree, and the filter name.
First use case — render stored markup in a template. The canonical day-to-day pattern is: content is authored as .sct (in a CMS field, a file, or a Doctrine column), and the bundle exposes a Twig filter so the template stays dumb:
{{ article.body|secretary }}
The filter runs the Secretary parser and returns an HTML string. Because it returns markup, follow it with |raw if your Twig config does not already treat it as safe HTML.
First use case without Twig — inject the parser service into a controller or service and compile manually:
$html = $this->secretary->compile($markup);
The exact method name depends on the php-secretary version pinned in your composer.lock; check the vendor class once and wrap it behind your own adapter (see below).
Do not go looking for a bin/console command or an editor integration — the bundle is purely a service/filter bridge.
Wrap the parser behind your own small service.
This bundle is read-only and unmaintained. Do not reference its service id from 40 places across the codebase. Define one App\Markup\Renderer that depends on the Secretary service, exposes render(string $markup): string, and let everything else depend on that. If you later swap to CommonMark, league/commonmark, or Parsedown, you change one class.
final class MarkupRenderer
{
public function __construct(private object $secretary) {}
public function render(string $markup): string
{
return $this->secretary->compile($markup);
}
}
Cache compiled HTML, not the markup. Parsing is cheap-ish per document but pointless to repeat on every request for immutable content. Cache the rendered HTML keyed by a hash of the source (plus a parser version key), not just by content id — otherwise edits to the markup serve stale HTML.
$key = 'sct:'.hash('xxh3', $markup);
return $this->cache->get($key, fn () => $this->renderer->render($markup));
Store both forms if you need search.
The parser gives you HTML; it does not give you a plain-text index. If you need search, strip tags at write time and store a content_text column alongside the raw .sct. Do the stripping in a Doctrine lifecycle listener so it stays consistent.
Do the transform on write for CMS-like content, on read for user input. Two valid pipelines:
prePersist/preUpdate listener or a messenger handler, persist body_html, serve it directly. Cheapest at request time.Twig usage — keep the filter at the leaf.
Prefer {{ article.body|secretary|raw }} only where you control the input. If markup can come from untrusted users, route it through HTML sanitisation first (e.g. twig/html-extra or an HTML purifier), because Secretary will happily emit whatever the markup produces.
Integrate with asset/static-site tooling via a command.
For a static site or docs export, write a small console command that walks .sct files, calls the renderer, and writes .html to the output dir. This is where a bundle with no commands still earns its keep — it wires the parser correctly for you.
Configuration is intentionally thin.
Expect a config tree exposing the parser options (extensions/features). Set them once under config/packages/secretary.yaml; do not scatter options across environments. If the config key is not obvious, dump the container: bin/console debug:container secretary --show-arguments.
Compose with the rest of Symfony. The parser is pure PHP with no request context, so it is safe to call from Messenger workers, commands, and API controllers alike. Put the rendering call in a service, not in a controller action, so it can be reused by the async path.
[READ-ONLY], 2 stars, 0 dependents. No security patches, no PHP 8.x/Symfony 6+ guarantees. Pin the version in composer.json and plan an exit strategy (usually migrating to league/commonmark). Do not build a product-critical pipeline on it without an owner.league/commonmark output is the fastest way to find divergence.|secretary on user-controlled input is an XSS vector until you sanitise. Escaping (|e) after the filter destroys the markup, so sanitise before rendering or accept only trusted content.|raw is often required. If your Twig config escapes by default, forgetting |raw shows literal <p> tags. Conversely, if the bundle marks the output as safe HTML, adding |raw is harmless but the "safe" marking is a trust decision you should verify in the Twig extension source.bin/console debug:container | grep -i secretary and a glance at vendor/secretary/php-secretary/src/) and record them in the wrapper service. Guessing from README examples of other versions is a common time sink.debug:container shows nothing, the bundle is not registered.php-secretary, bump the cache namespace or old HTML will survive the fix.php-secretary's extension mechanism and must be registered with the parser service — usually by decorating the service or defining a compiler pass. The bundle exposes no first-class "add extension" config in most versions..sct files, snapshot the HTML, and treat any change as a regression.How can I help you explore Laravel packages today?