Skip to main content Headstrong Internet

Improving cohesion in Symfony - storing Twig templates with the code

Published: 2023-11-19 | Updated: 2023-11-19

Whilst I’m a big fan of Symfony, I do find for larger apps the default directory structure leaves a lot to be desired.

The default structure is all about organising code by technical concern, so there are directories for Entities, Controllers, Forms etc.

Improving the default layout with feature directories

I much prefer to organise code by business concern, which keeps all code related to a specific feature or sub-feature together in close quarters.

This means you end up with a feature directory containing a mixture of form types, form data classes (if you use DTO’s for forms, which I also recommend), controllers, maybe a couple of UI related services, or a Twig extension. You get the picture.

This leads to a typical directory structure as follows:

├── src
│   └── UI
│       └── Feature
│           └── SendRegistrationEmail
│           └── ...
└── templates
    └── registration-email
    └── ...

A step further - moving templates into the feature directory

Recently, I have taken this a step further and moved the templates themselves into the feature directory, so that the templates that are directly concerned with providing the feature are right alongside the PHP code that uses them.

This has the advantage that you don’t have to wade through your entire /templates directory to find the file you need, and it’s immediately clear which templates relate to which feature.

├── src
│   └── UI
│       └── Feature
│           └── SendRegistrationEmail
│               └── tpl
└── templates

We keep the original /templates directory to hold global stuff like page layout templates, commonly used widgets etc.

However, anything specific to the feature moves into a tpl sub-directory of the feature directory, thus explicitly showing which templates are used by that feature.

In order to get Twig to find these template files in their new location, the immediate solution is to add the following to the Twig configuration:

# config/packages/twig.yaml
twig:
    # ...
    paths:
        'src/UI/Feature': 'Feature'

Then in your controllers you can refer to these templates using the namespace syntax:

@Feature/SendRegistrationEmail/tpl/hello.html.twig

Any Disadvantages?

Yes, there are two main disadvantages:

1) Specifying a directory that holds PHP code, such as src/UI/Feature means that all code in that directory is now available to Twig, which is potentially a security issue and cause other problems. 2) When rebuilding the Symfony cache, the TemplateCacheWarmer has to trawl through the entire src/UI/Feature directory looking for template files, which adds an overhead and slows down the cache rebuild.

Both of these problems could be fixed by specifying multiple namespaces in the Twig configuration, but who wants to do that? It would be a maintenance nightmare and you’d end up with potentially hundreds of lines of configuration code for the multitude of features in your app.

Compiler Passes To The Rescue!

We can create a Symfony compiler pass that will automatically find and register any tpl directories inside our src/UI/Feature directory, and assign them to their own namespaces.

<?php
declare(strict_types=1);

namespace AppCommonInfrastructureTwig;

use SymfonyComponentDependencyInjectionCompilerCompilerPassInterface;
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentFinderFinder;

/**
 * Locate all "tpl" directories inside the UI/Feature directory, and
 * add them to the Twig configuration as namespaced paths.
 *
 * A template such as "src/UI/Feature/Foo/Bar/tpl/test.twig.html" will
 * be added with namespace "Foo->Bar", and therefore can be referenced
 * using "@Foo->Bar/test.twig.html".
 */
final class TemplateDirectoryCompilerPass implements CompilerPassInterface
{
    private const BASE_DIRECTORY = 'src/UI/Feature';

    private const NAMESPACE_SEPARATOR = '->';

    private const TEMPLATE_DIRECTORY_NAME = 'tpl';

    public function process(ContainerBuilder $container): void
    {
        if ($container->hasDefinition('twig.loader.native_filesystem'))
        {
            $twigFilesystemLoaderDefinition = $container->getDefinition('twig.loader.native_filesystem');
            $projectDir = $container->getParameter('kernel.project_dir');
            $featureDir = sprintf('%s/%s', $projectDir, self::BASE_DIRECTORY);

            foreach ($this->tplDirPaths($featureDir) as $file)
            {
                $tplDirToAdd = str_replace($projectDir . '/', '', $file->getPathname());

                $namespaceToAdd = str_replace(
                    ['src/UI/Feature/', '/tpl', '/'],
                    ['', '', self::NAMESPACE_SEPARATOR],
                    $tplDirToAdd
                );

                $twigFilesystemLoaderDefinition->addMethodCall('addPath', [$tplDirToAdd, $namespaceToAdd]);
            }
        }
    }

    private function tplDirPaths(string $featureDir): Finder
    {
        $finder = new Finder();
        $finder->directories()
            ->in($featureDir)
            ->name(self::TEMPLATE_DIRECTORY_NAME)
        ;

        return $finder;
    }
}

And then register the compiler pass with the Symfony kernel.

// App/Kernel.php
protected function build(ContainerBuilder $container): void
{
    $container->addCompilerPass(
        new TemplateDirectoryCompilerPass()
    );
}

Remove the “Feature” namespace from the Twig configuration:

# config/packages/twig.yaml
twig:
    # ...
    paths:
        'src/UI/Feature': 'Feature' # <---- remove this line

Finally, update your Twig template paths, which can easily be done with a simple Regex search and replace:

For a single level deep:

Search @Feature/([^/]+)/tpl and replace with @$1.

For two levels deep etc:

Search @Feature/([^/]+)/([^/]+)/tpl and replace with @$1->$2.

This then gives us the wonderfully expressive template paths such as:

@SendRegistrationEmail/hello.html.twig

and for sub-features we would have something like:

@Billing->Invoicing->Create/invoice.html.twig

Your view on my choice of the -> may vary, but personally I like it, and it is not possible to use / so an alternative must be chosen. I think this provides good readability.

Summary And Benefits

This approach makes multiple improvements to an app and is very quick and easy to implement:

  • improved cohesion of closely related files, both PHP and Twig
  • fewer lines of YAML (or PHP!) configuration code to manage
  • more succinct template paths, providing better readability and less noise

It may not be necessary for smaller applications, but I’m finding it extremely beneficial in some of the larger apps that I manage.

Going Further - Moving Unit Tests Too

This is not considered a standard approach to locating template files, certainly within the Symfony community. However, for larger apps I do think it provides tangible benefits.

An additional and similar step I’ve also made, again not something widely done in the Symfony community as far as I am aware, is to locate unit tests alongside the production code (I prefer inside a Tests subdirectory).

Again this locates closely related code together in the same place, and it’s very easy to see where you are missing a unit test for a class. There’s also no need to keep the directory structure of the /tests root in sync with the /src root directory.

Currently there is poor support in PHPStorm for locating tests alongside the production code, which some may find annoying. Also, you need to make changes to your deployment or packaging process to remove the test code, but I find this is easy enough if you have tests in a Tests subdirectory as any directory with this name can easily be filtered out when deploying. End to end and integration tests would stay in their traditional location inside the root /tests directory.

Back to top

Application Development

Unlock the value in your business with custom software solutions.

Save time and money, and keep all your customers happy.

Cloud Server Management

We can manage your infrastructure, ensuring your application is always available and performant.

Flexible solutions for all types of app.

Software Consulting

Got a new software project? Don't know where to start?

We can help you plan, design and build a successful application for your business.

Website Design & Build

Development of all types of website from personal blogs to e-commerce sites.

We work with WordPress, CraftCMS, Symfony and more.

Headstrong Logo