Extending

Depending on the use case extending Herbie is really simple.

Basically, it provides the following extension points to change the flow of the application lifecycle.

Extension points

How-to Difficulty How often? When?
Console command Medium Rarely Access via CLI
Event listener Medium Rarely Hook the system
Application middleware Medium Rarely Change HTTP request/response
Route middleware Medium Rarely Change HTTP request/response
Twig filter Very easy Frequently Value transformation
Twig global Very easy Frequently Helper object
Twig function Very easy Frequently Content generation
Twig test Very easy Frequently Boolean decision

And changing the flow of the application lifecycle can be done in four different ways from easy to difficult.

# Extension workflow Difficulty Reusability
1 Using the filesystem Very easy Low
2 Using a programmatic approach Easy Low
3 Using a plugin Medium Medium
4 Using a distributed plugin High High

Extending using the filesystem

Using the file system means that we work in the site directory of the project. More precisely, only simple PHP files need to be created and placed in the appropriate directories.

Directory Description
site/extend The directory with various customized extensions.
site/extend/commands Directory with customized console commands.
site/extend/events Directory with customized event listeners.
site/extend/middlewares_app Directory with customized application middlewares.
site/extend/middlewares_route Directory with customized route middlewares.
site/extend/twig_filters Directory with customized Twig Filters.
site/extend/twig_functions Directory with customized Twig Functions.
site/extend/twig_globals Directory with customized Twig Globals.
site/extend/twig_tests Directory with customized Twig Tests.

So, let's get started.

Console commands

For adding a command you can create a PHP file in the directory site/extend/commands that returns a Command class. The command is automatically registered and available in the CLI application.

<?php

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CustomCommand extends Command
{
    protected static $defaultName = 'custom';
    protected static $defaultDescription = 'A custom command.';

    protected function configure(): void
    {
        $this->setHelp('This is a custom command.');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('Message from custom command.');
        return Command::SUCCESS;
    }
}

return CustomCommand::class;

Event listeners

For adding an event listener you can create a PHP file in the directory site/extend/events that returns an array{string, callable} with the name of the event and a valid PHP callback. The event is then registered automatically.

<?php

use herbie\EventInterface;

$event = function (EventInterface $event): void {
    // do something with $event
};

return ['onTwigInitialized', $event];

Application middlewares

For adding an application middleware you can create a PHP file in the directory site/extend/middlewares_app that returns a valid PHP callback. The application middleware is then registered automatically.

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

$middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
    // do something with the request
    $response = $handler->handle($request);
    // do something with the response
    return $response;
};

return $middleware;

Route middlewares

For adding an route middleware you can create a PHP file in the directory site/extend/middlewares_app that returns an array{string, callable} with a route regex expression and a valid PHP callback. The route middleware is then registered automatically.

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

$middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
    // do something with the request
    $response = $handler->handle($request);
    // do something with the response
    return $response;
};

return ['route/regex/expression', $middleware];

Twig filters

For adding a Twig filter you can create a PHP file in the directory site/extend/twig_filters that returns an array{string, callable} with the name of the filter and a valid PHP callback. The filter is then registered automatically.

<?php

$twigFilter = function (string $string): string {
    return strrev($string);
};

return ['reverse', $twigFilter];

Twig functions

For adding a Twig function you can create a PHP file in the directory site/extend/twig_functions that returns an array{string, callable} with the name of the function and a valid PHP callback. The function is then registered automatically.

<?php

$twigFunction = function (string $name): string {
    return "Hello {$name}!";
};

return ['hello', $twigFunction];

Twig globals

For adding a Twig global you can create a PHP file in the directory site/extend/twig_globals that returns an array{string, mixed}. The global is then registered automatically.

<?php

return ['hello', 'world'];

Twig tests

For adding a Twig test you can create a PHP file in the directory site/extend/twig_tests that returns an array{string, callable} with the name of the test and a valid PHP callback. The test is then registered automatically.

<?php

$twigTest = function (int $value): bool {
    return ($value % 2) !== 0;
};

return ['odd', $twigTest];

Extending using a programmatic approach

With the programmatic approach, you can achieve exactly the same. The difference is that you have to customize the index.php bootstrap file and add the extensions programmatically. For this purpose there are several add methods available in the herbie\Application class. A simplified bootstrap file would then look like this.

<?php

use herbie\Application;
use herbie\ApplicationPaths;

$app = new Application(
    new ApplicationPaths(__DIR__)
);

// --> start adding your extensions

$app->addConsoleCommand();
$app->addEventListener();
$app->addApplicationMiddleware();
$app->addRouteMiddleware();
$app->addTwigFilter();
$app->addTwigFunction();
$app->addTwigGlobal();
$app->addTwigTest();

// <-- finish adding your extensions

$app->run();

So, in the end, you have to do exactly the same with the programmatic approach as you do with the file system approach. In detail, it then looks like this.

Adding a console command:

class CustomCommand extends Command
{
    // see class definition above
}

$app->addConsoleCommand(CustomCommand::class);

Adding an event listener:

$event = function (herbie\EventInterface $event): void {
    // do something with $event
};

$app->addEventListener('onTwigInitialized', $event);

Adding an application middleware:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

$middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
    // do something with the request
    $response = $handler->handle($request);
    // do something with the response
    return $response;
};

$app->addApplicationMiddleware($middleware);

Adding a route middleware:

// the route middleware can be the same as the application middleware

$app->addRouteMiddleware('route/to/page', $middleware);

Adding a Twig filter:

$twigFilter = function (string $string): string {
    return strrev($string);
};

$app->addTwigFilter('reverse', $twigFilter);

Adding a Twig global:

$twigGlobal = new My\Custom\DateTime();

$app->addTwigGlobal('datetime', $twigGlobal);

Adding a Twig function:

$twigFunction = function (string $name): string {
    return "Hello {$name}!";
};

$app->addTwigFunction('hello', $twigFunction);

Adding a Twig test:

$twigTest = function (int $value): bool {
    return ($value % 2) !== 0;
};

$app->addTwigTest('odd', $twigTest);

Extending using a plugin

With the approach of creating a plugin, you can achieve exactly the same as before. I know, I repeat myself. The main differences are:

  • this approach of extending is the most flexible and powerful
  • you need to know a little more about programming with PHP
  • within the plugin, you have access to all objects of the application

The latter is achieved through dependency injection.

A plugin needs the following structure.

myplugin
├── config.php
└── MyPlugin.php

The config file must look like this:

<?php

require_once 'MyPlugin.php';

return [
    'apiVersion' => 2,
    'pluginName' => 'myplugin',
    'pluginClass' => MyPlugin::class,
    'pluginPath' => __DIR__,
];

The plugin itself is a PHP class with the following methods:

<?php

class MyPlugin implements herbie\PluginInterface
{
    public function apiVersion(): int
    {
        return 2;
    }

    public function consoleCommands(): array
    {
        return [];
    }

    public function eventListeners(): array
    {
        return [];
    }

    public function applicationMiddlewares(): array
    {
        return [];
    }

    public function routeMiddlewares(): array
    {
        return [];
    }

    public function twigFilters(): array
    {
        return [];
    }

    public function twigGlobals(): array
    {
        return [];
    }

    public function twigFunctions(): array
    {
        return [];
    }

    public function twigTests(): array
    {
        return [];
    }
}

To install the plugin, you need to place the plugin folder into the site/extend/plugins directory.

To enable the plugin, you need to edit the configuration file site/config/main.php and add an entry to the comma-separated list of the enabledPlugins setting.

<?php 
return [
    'enabledPlugins' => 'markdown,myplugin,textile',
];

Of course, you need to implement the methods according to the requirements. So the complete class looks like:

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CustomCommand extends Command
{
    protected static $defaultName = 'custom';
    protected static $defaultDescription = 'A custom command.';

    protected function configure(): void
    {
        $this->setHelp('This is a custom command.');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('Message from custom command.');
        return Command::SUCCESS;
    }
}

class MyPlugin implements herbie\PluginInterface
{
    public function apiVersion(): int
    {
        return 2;
    }

    public function consoleCommands(): array
    {
        return [
            CustomCommand::class
        ];
    }

    public function eventListeners(): array
    {
        $event = function (herbie\EventInterface $event): void {
            // do something with $event
        };
        return [
            ['onTwigInitialized', $event]
        ];
    }

    public function applicationMiddlewares(): array
    {
        $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
            // do something with the request
            $response = $handler->handle($request);
            // do something with the response
            return $response;
        };
        return [
            $middleware
        ];
    }

    public function routeMiddlewares(): array
    {
        $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
            // do something with the request
            $response = $handler->handle($request);
            // do something with the response
            return $response;
        };
        return [
            ['route/to/page', $middleware]
        ];
    }

    public function twigFilters(): array
    {
        $twigFilter = function (string $string): string {
            return strrev($string);
        };
        return [
            ['reverse', $twigFilter]
        ];
    }

    public function twigGlobals(): array
    {
        return [
            ['hello', 'world']
        ];
    }

    public function twigFunctions(): array
    {
        $twigFunction = function (string $name): string {
            return "Hello {$name}!";
        };
        return [
            ['hello', $twigFunction]
        ];
    }

    public function twigTests(): array
    {
        $twigTest = function (int $value): bool {
            return ($value % 2) !== 0;
        };
        return [
            ['odd', $twigTest]
        ];
    }
}

That's all you have to do.

A good example is the dummy system plugin, by the way.

Extending using a distributed plugin

To distribute your plugin now, you need to make it a Composer package. To do this, you need to create a JSON file composer.json in the plugin folder. The file must contain a type with herbie-plugin.

{
    "name": "myvendor/myplugin",
    "type": "herbie-plugin",
    "require-dev": {
        "getherbie/herbie": "dev-2.x-develop"
    }
}

How to deploy the plugin on https://packagist.org can be found on https://getcomposer.org.

After deployment, the plugin can be installed using Composer. All you need to do is run the following CLI command in your project root directory.

composer require myvendor/myplugin

After that, the plugin needs to be enabled in the configuration.

Good examples are the plugins simple contact and simple search.