Commands and Command Handlers

Improvements?

Let us know by posting here.

Commands and Command Handlers

Commands and Command Handlers are core-supported objects for structuring actions or events.

From Wikipedia on the Command Pattern:

An object-oriented design pattern where an object encapsulates necessary information for performing an action or triggering an event later. This includes the method name, owning object, and method parameters.

In Concrete CMS, Command (or Message) objects hold all data for a command, while handlers automatically execute these commands when triggered. Benefits include the ability to activate code from multiple sources (like Dashboard controllers or console commands) and improved code clarity.

Creating and Executing Commands

Commands in Concrete CMS are PHP classes. The terms Command and Message are interchangeable, though 'Command' is a specific term in CQRS. These are referred to as Command classes in this context.

Examples of Core Commands

Core commands include:

  • Concrete\Core\Cache\Command\ClearCacheCommand

  • Concrete\Core\Page\Command\ReindexPageCommand

  • Concrete\Core\File\Command\RescanFileCommand

ClearCacheCommand has no properties. ReindexPageCommand stores a page ID, and RescanFileCommand is used with a file ID.

How to Execute a Command

Execute a command by calling executeCommand on the Application object, often available in controllers as $app.

$this->app->executeCommand(new ClearCacheCommand());

Alternatively, use the app() method to get the Application object:

$app = app();
$command = new ReindexPageCommand(1);
$app->executeCommand($command);

Wiring Commands to Handlers

Commands and their corresponding handlers in Concrete CMS:

  • Concrete\Core\Cache\Command\ClearCacheCommandConcrete\Core\Cache\Command\ClearCacheCommandHandler
  • Concrete\Core\Page\Command\ReindexPageCommandConcrete\Core\Page\Command\ReindexPageCommandHandler
  • Concrete\Core\File\Command\RescanFileCommandConcrete\Core\File\Command\RescanFileCommandHandler

Handlers have an __invoke() method accepting the Command object and __construct() methods for automatic dependency instantiation. The RescanFileCommandHandler example demonstrates this:

namespace Concrete\Core\File\Command;

use Concrete\Core\Config\Repository\Repository;
use Concrete\Core\Entity\File\File as FileEntity;
use Concrete\Core\File\Rescanner;
use Doctrine\ORM\EntityManager;

class RescanFileCommandHandler
{
    protected $entityManager;
    protected $config;
    protected $rescanner;

    public function __construct(Repository $config, EntityManager $em, Rescanner $rescanner)
    {
        $this->config = $config;
        $this->entityManager = $em;
        $this->rescanner = $rescanner;
    }

    public function __invoke(RescanFileCommand $command)
    {
        $f = $this->entityManager->find(FileEntity::class, $command->getFileID());
        if ($f) {
            $fv = $f->getApprovedVersion();
            if ($fv) {
                $this->rescanner->rescanFile($f);
            }
        }
    }
}

Dependencies are resolved in __construct and utilized in __invoke(), which converts command data for the handler's operations.

Auto-Handled Commands

Commands extending Concrete\Core\Foundation\Command\Command automatically pair with handlers sharing their name, plus "Handler" suffix. For example, ClearCacheCommand pairs with ClearCacheCommandHandler.

HandlerAwareCommandInterface

For more flexibility, use Concrete\Core\Foundation\Command\HandlerAwareCommandInterface. This requires implementing a static getHandler method to specify the handler.

Configuring Handlers

Manually route commands to handlers using the app.command_handlers configuration key:

return [
    'command_handlers' => [
        'Concrete\Core\Cache\Command\ClearCacheCommand' => 'Path\To\Custom\Command\Handler',
    ],
];

Asynchronous Buses

Commands in Concrete CMS default to synchronous execution but can be set to operate asynchronously.

Asynchronous Execution

Asynchronous commands are queued and processed in the background, typically when an admin uses the Dashboard. For better performance and reliability, activate the manual queue listener.

Implementing Asynchronous Commands

AsyncCommandInterface

Commands implementing Concrete\Core\Foundation\Command\AsyncCommandInterface are processed asynchronously. This interface, without methods, signals Concrete to handle the command asynchronously.

Configuration

Asynchronous or synchronous processing can also be set via configuration. Use concrete.messenger.routing in your application/config/concrete.php file to route commands to the desired transport. Example for routing ClearCacheCommand to asynchronous:

'messenger' => [
    'routing' => [
        'Concrete\Core\Cache\Command\ClearCacheCommand' => ['async'],
    ],
],

Explore Symfony Messenger

Concrete CMS's command and transport system is based on Symfony Messenger. While Concrete adds configuration layers for ease of use in its context, the core components remain unaltered from Symfony.

Advanced configurations allow creating custom buses, routing commands creatively, and adding middlewares for logging or other purposes. To fully leverage these capabilities in Concrete's command system, familiarize yourself with the Symfony Messenger documentation.