Skip to content

PHP Process Scheduler

Intro

A new tool has been created that allows to schedule PHP code execution from the administration. This new tool allows us to replicate the operation of CRON processes that are created on the server in order to avoid code uploads and to be able to update these processes without the need for a deploy.

The tool has been registered as a symfony CRUD component and is therefore accessible from the general URL /dashboard -> Scheduler or the list can be consulted directly /app/cms/scheduler/list.

Following the CRUD options it is possible to create, query, edit and delete any scheduled entry from the administration panel.

Quick start

Following is an example code to test your first Scheduler process:

Title - Scheduler Test Active - Yes Type - set to "Repeat" Trigger - 10 seconds

php
$now = new DateTime();

$return = sprintf('Last call: %s', $now->format('H:m:s d/m/y'));

This will create (enque) this process and will be called every 10 seconds until deactivated. The output of the process will be shown in the Scheduler list view.

The $return variable

In your code you can assign a value to the $return variable. This value will be shown in the schedulers list, under the column list.label_exec_response after the code has been executed.

Process configuration

The information presented in the listing and editing/creation panel is similar.

NameActiveTypeTriggerLast ExecutionResponse

While some values are simple and require no explanation, here is a breakdown of the most interesting ones:

  • Name (form.label_title, list.label_title) - process name for "human" reference no internal use
  • Active (form.label_active, list.label_active) - boolean value in database, indicates the status of the process
  • Type (form.label_type, list.label_active) - consists of two possible values established in the entity:
    • TYPE_REPEAT - the execution would be carried out continually until the process is manually canceled or fails.
    • TYPE_ONCE - establishes a single time execution
  • Trigger (form.label_execution, list.label_execution) - date value following the PHP DateTime format, the literal value that is specified in this field is used e.g.:
    13:36:45, 2 seconds, 5 minutes, 1 hour

IMPORTANT: In the current version there is no automatic format check for date values. The format supported in the creation of scheduled processes of type TYPE_INTERVAL is the one specified as a string for the PHP DateTime modify() functions. Reference

  • Response - this field contains the result of the last execution.
  • Code - the lines of code we put in this field will be executed directly by the PHP function eval. (Reference)

Internal functionality

The automatic process queue is managed by the internal Symfony/Messenger component.

The functionality consists of two processes.

SchedulerMessage - is responsible for generating a rhythm or clock according to which the next execution time is checked.

SchedulerJobMessage - responsible for launching the code in question when called by the first process.

The registry of messages is done in the asynchronous queue of redis messenger:wfcms_async__queue. When SchedulerMessage is triggered, all the processes with the active status are retrieved from the database to calculate which process is the closest to the current date to be triggered. Knowing this date, the delay for the clock process is calculated. Whenever a scheduler element is created or modified, the current queue is deleted and the evaluation of the closest element to be launched is performed again.

In order to avoid blocking the clock, the execution of the corresponding code falls on the second process SchedulerJobMessage. It is responsible for executing the code and updating the entity values according to the result of the execution. In case of an error in the execution, the process is disabled.

A path has been established that allows for the immediate execution of a processes wfadmin_scheduler_run. This path is used in editing panel to provide immediate code execution with on-screen result.

yml
wfadmin_scheduler_run:
    path: /run/{scheduler_id}
    controller: "wf_cms_admin.controller.scheduler:runAction"
    requirements:
        sheduler_id: \d+

An additional relevant feature is that the execution action is accessible to the processes themselves. It is possible to integrate the call of stored code within one process as part of the execution of another.

php
// Recover scheduler content from DB based on its ID
$schedulerRepository = $this->container->get("wf_cms.repository.scheduler");
$scheduler = $schedulerRepository->find($schedulerId);
$schedulerExecResponse = $this->executePHP($scheduler);
// Evaluate $schedulerExecResponse for further actions

Logging

A log is created at the runtime of a process. The logs are stored in messenger prefix files since it is the latter that is responsible for executing the processes.

Inside a scheduler code, you can use $this->logger to log things.

php
$this->logger->info(sprintf('Schedule executed %s - %s', $scheduler->getTitle(), $now->format('H:i:s')));

PHP Code

For correct code execution all functions must be declared anonymously.

Example of an anonymous function to which the $pageRepository parameter is passed and another function &$getArticlesFromBoards:

php
$getHomepageArticles = function ($pageRepository) use (&$getArticlesFromBoards) {
    $homePage = $pageRepository->findOneBySlug('home-boards');
    $homeBoards = $homePage->getModules();

    $articles = [];
    foreach ($homeBoards['ids'] as $board) {
        $pageBoard = $pageRepository->find($board);
        if (!$pageBoard) {
            continue;
        }

        $articles = array_merge($articles, $getArticlesFromBoards($pageBoard->getModules()));
    }

    return $articles;
};

You can find more information about anonymous functions here

IMPORTANT: The only globally available resource is $this->container from which we will have to retrieve any necessary resources.

Testing

After saving the scheduler process one of the bottom options allow you to run the code inmediatelly to see its execution. Bear in mind that the executed code is the SAVED one, any unsaved changes would not be reflected.

In case a thorough debugging is required you can create a command with the code before programming its execution. Below is an example of a command that once created can be called from server console of your application app/admin/console app:test_php:scheduler.

php
<?php

namespace App\Bundle\CmsAdminBundle\Command;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class TestPHPSchedulerCommand extends Command
{
    private ContainerInterface $container;

    public function __construct(ContainerInterface $container)
    {
        parent::__construct();
        $this->container = $container;
    }

    protected function configure()
    {
        $this
            ->setName('app:test_php:scheduler')
            ->setDescription('Comando provisional para probar código que se establece para la ejecución jobs automáticos.')
            ->addArgument(
                'param',
                InputArgument::OPTIONAL,
                'Need any parameter to test?'
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // $param = $input->getArgument('param');
        $now = new \DateTime();
        try {
            // Here goes the code that would be pasted into PHP code field in the admin.
            // Code Start
            // $localDomain = $this->container->getParameter('wf_cms.admin.publicUrl');
            // Code End

            // Replace `1` with the ID of the scheduler entity created through the UI
            $scheduler = $this->container->get('wf_cms.repository.scheduler')->find(1);
            eval($scheduler->getCode());
            $execResponse = sprintf('[%s] OK: Local domain - %s', $now->format('H:i:s'), $localDomain);
        } catch (\Throwable | \Exception $e) {
            $execResponse = sprintf('[%s] ERROR: %s', $now->format('H:i:s'), $e->getMessage());
            if ($e->getLine()) {
                $execResponse .= ' Line:' . $e->getLine();
            }
        }

        $output->writeln($execResponse);
    }
}

Do not forget to register the command in the corresponding .xml file.

xml
    <service id="App\Bundle\CmsAdminBundle\Command\TestPHPSchedulerCommand">
        <argument type="service" id="service_container" />

        <tag name="console.command" />
    </service>