Custom override matcher

In this tutorial we will look at how you can create your own matcher for overrides.

For this example we will create a matcher that uses a regular expressions to check the path which is called. We will get into the reason behind this in the first step.

The code I'm about to showcase is currently used as is for the landingpage of this site.
The bundle serving the matcher is the IntProgCoreBundle whilst the matcher is used by IntProgDesignBundle.

Steps

Simple Matcher (UrlAliasRegEx)

The matcher itself is pretty straight forward.

UrlAliasRegEx.php (PHP)
<?php

namespace IntProg\CoreBundle\Core;

use eZ\Publish\API\Repository\Values\Content\ContentInfo;
use eZ\Publish\API\Repository\Values\Content\Location;
use eZ\Publish\Core\MVC\Symfony\Matcher\ContentBased\MultipleValued;
use eZ\Publish\Core\MVC\Symfony\View\LocationValueView;
use eZ\Publish\Core\MVC\Symfony\View\View;

/**
 * Class UrlAliasRegEx.
 *
 * @package   IntProg\CoreBundle\Core
 */
class UrlAliasRegEx extends MultipleValued
{
    /**
     * Checks if a Location object matches.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Location $location
     *
     * @return boolean
     */
    public function matchLocation(Location $location)
    {
        // first up we need the url alias service in this case.
        $urlAliasService = $this->repository->getURLAliasService();

        // now we load all aliases (disregarding the language) as we want to match a specific pattern for a location if
        // it matches in any language.
        $locationUrls = array_merge(
            $urlAliasService->listLocationAliases($location),
            $urlAliasService->listLocationAliases($location, false)
        );

        // now let us loop over the found aliases.
        foreach ($locationUrls as $locationUrl) {
            // we need to loop the regex-list as we will be able to define multiple regular expressions.
            foreach (array_keys($this->values) as $regex) {
                // now we clean the regex as it's very likely to contain a forward slash.
                $cleanRegex = str_replace('/', '\/', $regex);

                // if we have a match, we return true.
                if (preg_match(sprintf('/%s/i', $cleanRegex), $locationUrl->path)) {
                    return true;
                }
            }
        }

        // if none of the urls matches with any of entries in the regex list. we return false to tell the system that
        // the location did not match the pattern.
        return false;
    }

    /**
     * Checks if a ContentInfo object matches.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo
     *
     * @return false
     */
    public function matchContentInfo(ContentInfo $contentInfo)
    {
        // we do not match for content as the url alias might differ if multiple locations are defined.
        return false;
    }

    /**
     * Matches against view (by regular expression).
     *
     * @param View $view
     *
     * @return boolean
     */
    public function match(View $view)
    {
        if (!$view instanceof LocationValueView) {
            // this check prevents $this->matchContentInfo from being called.
            return false;
        }

        // now that we know it's a location we are matching against. lets match.
        return $this->matchLocation($view->getLocation());
    }
}

If your IDE (e.g. PHPStorm) does display method-overrides you may notice that the method "match" is unique to this class. This is correct as the method is not defined in the base-class we are extending (why not? beats me...). The method is used by the system though. So... just keep it.

Now we can create the configuration and load it.

override.yml (YAML)
system:
    global:
        content_view:
            full:
                landing_page_root:
                    controller: "IntProgDesignBundle:PathOverride/LandingPageRoot:fullView"
                    template: "IntProgDesignBundle:override/full/path:landing_page_root.html.twig"
                    match:
                        \IntProg\CoreBundle\Core\UrlAliasRegEx: "^/$"

IntProgDesignExtension.php (PHP)
<?php

namespace IntProg\DesignBundle\DependencyInjection;

use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Yaml\Yaml;

class IntProgDesignExtension extends Extension implements PrependExtensionInterface
{
    public function load(array $configs, ContainerBuilder $container)
    {
        ...
    }

    public function prepend(ContainerBuilder $container)
    {
        // loading and prepending the configuration to add the settings.
        $configFile = __DIR__ . '/../Resources/config/override/override.yml';
        $config = Yaml::parse(file_get_contents($configFile));
        $container->prependExtensionConfig('ezpublish', $config);
        $container->addResource(new FileResource($configFile));
    }
}

That's it. As mentioned in the actual the matchLocation-method you can match multiple values in one matcher. You can just change the value in the configuration to an array to match for multiple regular expressions.

Complex Matcher (Environment)

Now we want to match against available configuration.

This example is also used on this page and uses the environment to deliver a different fallback view. This way I can deliver useful code-samples when a view is not yet created.

When we are in a production or staging environment we don't want to deliver debug. So we have a 404-Controller prepared.
When the system uses the development environment however, we want to display some debug as to what has happened. And maybe some more information.

The output of this content type (folder) differs depending on which environment we are on.

To achieve this result we first need to create another matcher.

Environment.php (PHP)
<?php

namespace IntProg\CoreBundle\Core;

use eZ\Publish\Core\MVC\Symfony\Matcher\ViewMatcherInterface;
use eZ\Publish\Core\MVC\Symfony\View\View;
use InvalidArgumentException;

class Environment implements ViewMatcherInterface
{
    protected $matchingEnvironments = [];

    protected $currentEnvironment = null;

    /**
     * Registers the matching configuration for the matcher.
     *
     * @param array $matchingConfig
     *
     * @throws InvalidArgumentException Should be thrown if $matchingConfig is not valid.
     */
    public function setMatchingConfig($matchingConfig)
    {
        if (!isset($matchingConfig['current']) || !isset($matchingConfig['matches'])) {
            throw new InvalidArgumentException('Environment-matching requires [current] and [matching] to be required');
        }

        $matching                 = $matchingConfig['matches'];
        $this->currentEnvironment = $matchingConfig['current'];

        $this->matchingEnvironments = !is_array($matching) ? [$matching] : $matching;
    }

    /**
     * Check if current environment is matching one of the defined environments to match.
     *
     * @param View $view
     *
     * @return boolean
     */
    public function match(View $view)
    {
        return in_array($this->currentEnvironment, $this->matchingEnvironments);
    }
}

As you can see, we are not using the class we extended before. Instead we are implementing the ViewMatcherInterface.
In this interface we have the match-method defined and also need to define the setMatchingConfig-method.

The setMatchingConfig takes one parameter which is equivalent to the value you give to the matcher. This time you can define a structure yourself.

Now we can create the config file and add load it as described in step #1.

override.yml (YAML)
system:
    global:
        content_view:
            full:
                default_view_not_found:
                    controller: "IntProgDesignBundle:DefaultView:viewNotFound"
                    template: "IntProgDesignBundle::not_found.html.twig"
                    match:
                        \IntProg\CoreBundle\Core\Environment:
                            current: "%kernel.environment%"
                            matches: ["stage", "prod"]

We have to use the configuration structure as defined in the php-file of the matcher. In this context we are also using a parameter from the kernel. The to match against environment to be specific.