Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

Commands, cronjob parts and module views as classes

Read this page if you maintain code that reads, includes or copies kernel scripts, cronjob parts or module views, or if you write your own. From Exponential 6.0.15 the code of every command line script, cronjob part and…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/bc/6.0/cli_cronjob_view_abstractions.md

On this page

Commands, cronjob parts and module views as classes

Read this page if you maintain code that reads, includes or copies kernel scripts, cronjob parts or module views, or if you write your own. From Exponential 6.0.15 the code of every command line script, cronjob part and module view lives in a class. The file you know (bin/php/ezcache.php, cronjobs/workflow.php, kernel/content/view.php) keeps its path and does one thing: it calls that class. A view, a command and a cronjob part can now share one implementation, be tested, and be re-implemented by a site or an extension without copying files.

In short

What changedThe code moved to kernel/private/classes/{commands,cronjobs,views}/ (namespaces Exponential\Command\Kernel, Exponential\Cronjob\Kernel, Exponential\View\Kernel). Shared work moved to services. New settings block site.ini [RunnableSettings] (Implementation[], Listeners[]) and the events runnable/<kind>/before and runnable/<kind>/after.
Who is affectedCode that included or parsed a kernel script, cronjob part or view file for its logic: the logic is now in the class. Sites that keep a copied and edited kernel view or cronjob file. Command line usage, --help output and exit codes are unchanged.
How to checkphp vendor/bin/phpunit tests/tests/kernel/classes/runnable/
How to fixLook for the code in kernel/private/classes/. Replace a copied kernel file with a subclass registered in [RunnableSettings] Implementation[] (see Re-implementing one). Regenerate the autoloads after the update.

The old way still works. A script, cronjob part or view written as a plain PHP file runs exactly as before; nothing in the kernel requires the new form. It is no longer what the product uses or documents, because a plain file cannot be extended, called or tested on its own. Write new code as classes.

The class hierarchy

Exponential\Runnable\Runnable            kernel/private/classes/runnable/runnable.php
├── Exponential\Runnable\Command          a command line script      run()
├── Exponential\Runnable\CronjobPart      a cronjob part             run( array $scope )
└── Exponential\Runnable\ModuleView       a module view              run( array $scope )
KindThe fileThe class
Commandbin/php/<name>.phpExponential\Command\Kernel\<Name> in kernel/private/classes/commands/<name>.php
Cronjob partcronjobs/<name>.phpExponential\Cronjob\Kernel\<Name> in kernel/private/classes/cronjobs/<name>.php
Module viewkernel/<module>/<view>.phpExponential\View\Kernel\<Module>\<View> in kernel/private/classes/views/<module>/<view>.php
Extension commandextension/<ext>/bin/.../<name>.phpExponential\Command\Extension\<Ext>\<Name> in extension/<ext>/classes/runnable/commands/
Extension cronjob partextension/<ext>/cronjobs/<name>.phpExponential\Cronjob\Extension\<Ext>\<Name> in extension/<ext>/classes/runnable/cronjobs/
Extension module viewextension/<ext>/modules/<module>/<view>.phpExponential\View\Extension\<Ext>\<Module>\<View> in extension/<ext>/classes/runnable/views/<module>/

Work that a view, a command and a cronjob part share lives in a service class beside them, in kernel/private/classes/services/ (Exponential\Service\...) or in an existing kernel class; see Services.

What the files look like now

A command (bin/php/ezcache.php):

#!/usr/bin/env php
<?php
/** ...the script's header... */

require_once 'autoload.php';

// The code is in kernel/private/classes/commands/ezcache.php (#207); this file is the entry point.
\Exponential\Command\Kernel\Ezcache::main( __FILE__ );

A cronjob part (cronjobs/workflow.php) and a module view (kernel/content/history.php):

<?php
return \Exponential\Cronjob\Kernel\Workflow::main( __FILE__, get_defined_vars() );
<?php
return \Exponential\View\Kernel\Content\History::main( __FILE__, get_defined_vars() );

get_defined_vars() hands the class the variables the kernel gives a part or a view: $cli, $isQuiet for a cronjob part (from eZRunCronjobs::runScript()), $Params, $Module and the view parameters for a view (from eZProcess::runFile()). A view's class returns what the kernel used to take from the file: its $Result, or what it returned (ModuleView::viewResult()).

Writing a new one

A command

<?php
// extension/myext/classes/runnable/commands/report.php
namespace Exponential\Command\Extension\Myext;

class Report extends \Exponential\Runnable\Command
{
    public function run()
    {
        $script = $this->script( array( 'description' => "Writes the monthly report",
                                        'use-session' => false, 'use-modules' => true, 'use-extensions' => true ) );
        $options = $this->startup( '[month:]', '', array( 'month' => 'The month, YYYY-MM' ) );

        $this->output( \myReportService::build( $options['month'] ) );   // the work: a class you can call anywhere

        $this->shutdown( 0 );
    }
}
#!/usr/bin/env php
<?php
// extension/myext/bin/php/report.php
require_once 'autoload.php';
\Exponential\Command\Extension\Myext\Report::main( __FILE__ );

Then php bin/php/ezpgenerateautoloads.php -e and php extension/myext/bin/php/report.php --help.

A cronjob part

<?php
// extension/myext/classes/runnable/cronjobs/cleanup.php
namespace Exponential\Cronjob\Extension\Myext;

class Cleanup extends \Exponential\Runnable\CronjobPart
{
    public function run( array $scope )
    {
        $cli = $scope['cli'];
        $removed = \myCleanupService::removeExpired();
        if ( empty( $scope['isQuiet'] ) )
            $cli->output( "Removed $removed expired items" );
    }
}
<?php
// extension/myext/cronjobs/cleanup.php
return \Exponential\Cronjob\Extension\Myext\Cleanup::main( __FILE__, get_defined_vars() );

Register the part in cronjob.ini as before ([CronjobPart-cleanup] Scripts[]=cleanup.php).

A module view

<?php
// extension/myext/classes/runnable/views/report/show.php
namespace Exponential\View\Extension\Myext\Report;

class Show extends \Exponential\Runnable\ModuleView
{
    public function run( array $scope )
    {
        $Module = $scope['Params']['Module'];
        $tpl = \eZTemplate::factory();
        $tpl->setVariable( 'report', \myReportService::build( $scope['Params']['Month'] ) );
        return array( 'content' => $tpl->fetch( 'design:report/show.tpl' ),
                      'path' => array( array( 'text' => 'Report', 'url' => false ) ) );
    }
}
<?php
// extension/myext/modules/report/show.php (named in module.php ViewList as before)
return \Exponential\View\Extension\Myext\Report\Show::main( __FILE__, get_defined_vars() );

$scope is what eZProcess::runFile() defines: $scope['Params'] (with the module as $scope['Params']['Module']) and $scope['Result'] always, the view's parameters as variables of their own ($scope['Module'] among them) only when the module's module.php sets 'variable_params' => true. Read the module as $scope['Params']['Module'], which every module has.

Shared option parsing, help and output

What every command does the same way is in Exponential\Runnable\Command, once. The helpers hand their arguments to eZScript and eZCLI unchanged, so --help, -q/--quiet, -s/--siteaccess, -l/--login, -d/--debug, -v/--verbose and the exit codes are exactly what eZScript::getOptions() and eZScript::shutdown() make of them.

HelperDoes
$this->cli()the terminal, eZCLI::instance()
$this->script( array $settings )creates the script, eZScript::instance( $settings ) (description, use-session, use-modules, use-extensions, site-access, ...); without settings it returns the script
$this->options( $config, $argumentConfig, $optionHelp, ... )parses the command line, eZScript::getOptions(): shows --help and exits, applies the standard options
$this->startup( $config, $argumentConfig, $optionHelp, ... )the three steps after script(): the script's startup(), options() and the script's initialize() (siteaccess, extensions, session, database)
$this->start( $settings, $config, ... )script( $settings ) and startup() in one call
$this->output(), error(), warning()write a line, an error, a warning; output() writes nothing with -q
$this->isQuiet()-q was given
$this->log( $message, $logName = null )eZLog::write() to var/log/<script name>.log by default
$this->shutdown( $exitCode, $exitText )eZScript::shutdown(), which exits

Before and after, in one of the kernel's commands (bin/php/trashpurge.php):

// before
$script = eZScript::instance( array( 'description' => "Empty Exponential trash.", 'use-session' => false,
                                     'use-modules' => false, 'use-extensions' => true ) );
$script->startup();
$options = $script->getOptions( "[iteration-sleep:][iteration-limit:][memory-monitoring][trashed-days:]", "", $help );
$script->initialize();

// after
$script = $this->script( array( 'description' => "Empty Exponential trash.", 'use-session' => false,
                                'use-modules' => false, 'use-extensions' => true ) );
$options = $this->startup( "[iteration-sleep:][iteration-limit:][memory-monitoring][trashed-days:]", "", $help );

A command that does something between the steps (sets a debug option before initialize(), picks the siteaccess itself) keeps calling $script->startup(), $this->options() and $script->initialize() one by one. The 61 kernel commands that create a script do it with $this->script() and parse their options with $this->options() or $this->startup() (40 of them); they and kickstarter.php get their terminal with $this->cli(). (The autoload generator, the installers and the Velocity warm-up create no script.) Their --help output and exit code are the same as before, byte for byte.

Services: one implementation for the view, the command and the cronjob part

Where a view and a command or cronjob part do the same work, the work is in one class and all of them call it.

WorkServiceCalled by
Emptying the trashExponential\Service\Trash (kernel/private/classes/services/trash.php)content/trash, bin/php/trashpurge.php, cronjobs/trashpurge.php
Removing expired sessionsExponential\Service\SessionGarbageCollector (kernel/private/classes/services/sessiongarbagecollector.php)setup/session, bin/php/ezsessiongc.php, cronjobs/session_gc.php
Clearing cachesexpCacheManager (kernel/classes/expcachemanager.php)setup/cache, bin/php/cache.php (exp:cache), setup/cachetoolbar (the clear-cache toolbar)
Removing old draftsExponential\Service\DraftsCleanup (kernel/private/classes/services/draftscleanup.php)cronjobs/old_drafts_cleanup.php (user drafts), cronjobs/internal_drafts_cleanup.php (internal drafts)
The static cacheexpStaticCacheRunner, expCacheManager::regenerateStaticCache()setup/cache, setup/staticcachestream, bin/php/makestaticcache.php
PreloadingexpPreloadRunner, expPreloadJobsetup/preload, setup/preloadjob, bin/php/preload.php, bin/php/preloadjob.php
Maintenance modeexpMaintenancesetup/maintenance, bin/php/maintenance.php
Running cronjobs from the adminexpCronjobRunnersetup/cronjobs, setup/cronjobsstream
// the trash
if ( \Exponential\Service\Trash::canEmpty( \eZUser::currentUser() ) )   // content/cleantrash
    \Exponential\Service\Trash::purgeObjects( $objectIDs );                // the selected objects
\Exponential\Service\Trash::emptyTrash();                                 // the Empty button: all of it, in batches
\Exponential\Service\Trash::purgeInBatches( 100, 1, $trashedBefore );     // 100 at a time, a transaction each, 1 s between
\Exponential\Service\Trash::purge( $cli, false, false, $script, 100, 1, 30 );  // the command: the same batches with progress,
                                                                          // only what has been in the trash 30 days

// expired sessions
\Exponential\Service\SessionGarbageCollector::collect();          // and the baskets they leave (view, command, cronjob part)
\Exponential\Service\SessionGarbageCollector::collect( false );   // the sessions only

The trash view's Empty button and the command purge the same way: purgeInBatches(), each batch of 100 in a transaction of its own, the content cache cleared and a one second pause between batches (eZScriptTrashPurge, behind the command and the cronjob part, runs its batches through the service too). After the batches the button also purges any archived object left without a trash entry, so the trash is empty afterwards. Removing timed out sessions on setup/session removes the baskets they leave, as the command and the cronjob part do. The bin/php/ezcache.php command keeps eZCacheHelper, which writes the progress output it has always written.

Extension points: events, the registry in site.ini, re-implementation

Events around run()

main() of every command, cronjob part and view runs run() between two events of the kernel's event hub (ezpEvent). <kind> is command, cronjob or view:

EventTypeListener getsListener returns
runnable/<kind>/beforenotify$runnable, $class, $scopenothing
runnable/<kind>/afterfilter$result, $runnable, $class, $scopethe result to use

$class is the class that runs (the re-implementation, if there is one); $scope is what the part or view was handed ($Params, $Module, ...; empty for a command). A command that ends with $this->shutdown() exits inside run(), so only its before event fires. Without listeners, main() returns what run() returned.

class myViewTimer
{
    private static $start = array();

    public static function before( $runnable, $class, array $scope )
    {
        self::$start[$class] = microtime( true );
    }

    public static function after( $result, $runnable, $class, array $scope )
    {
        \eZDebug::writeNotice( sprintf( '%s took %.1f ms', $class, 1000 * ( microtime( true ) - self::$start[$class] ) ), __METHOD__ );
        return $result;   // a filter: return the result, or the view loses it
    }
}

The registry in site.ini

# settings/override/site.ini.append.php, or an extension's settings/site.ini.append.php
[RunnableSettings]
# a subclass to run instead of a runnable class
Implementation[Exponential\View\Kernel\Content\History]=myHistoryView
# listeners of the events above
Listeners[]=runnable/view/before@myViewTimer::before
Listeners[]=runnable/view/after@myViewTimer::after

settings/site.ini documents both, with nothing set. [RunnableSettings] Listeners[] is attached for the runnables in every context, the command line included, once per process; [Event] Listeners[] stays attached to web requests only, as before, so existing listeners do not start running in scripts. A listener can also be attached in code: ezpEvent::getInstance()->attach( 'runnable/view/after', $callback ).

Re-implementing one from a site or an extension

Every runnable is created through Runnable::create(), which looks up Implementation[<class>] first. The replacement must extend the class it replaces (is_subclass_of); an entry naming anything else is ignored. So the replacement overrides one method and keeps the rest:

class myHistoryView extends \Exponential\View\Kernel\Content\History
{
    public function run( array $scope )
    {
        $result = parent::run( $scope );
        $result['content'] .= '<p class="note">Versions older than a year are archived.</p>';
        return $result;
    }
}

// Implementation[Exponential\Cronjob\Kernel\Trashpurge]=mySundayTrashpurge: empty the trash on Sundays only
class mySundayTrashpurge extends \Exponential\Cronjob\Kernel\Trashpurge
{
    public function run( array $scope )
    {
        if ( date( 'N' ) == 7 )
            return parent::run( $scope );
    }
}

No file of the kernel is copied or edited, so the next release updates the original underneath. This works for all three kinds. One thing to know about commands: a command is created before its script has read the settings, so for a command Implementation[] and Listeners[] are read from settings/site.ini and settings/override/site.ini.append.php only (Runnable::settings() reads them on its own, without creating the settings instance the script sets up later). Views and cronjob parts see every setting, siteaccess and extension settings included.

Seeing them

setup/rad and its survey (setup/radsurvey, section "Commands, cronjob parts and views as classes") count the runnables as extension points: each one per kind and owner, which are re-implemented, and every Implementation[] entry that cannot work (it names no runnable class, the replacement does not exist, or it does not extend the class). The six events are in the survey's event list. The catalogue on setup/rad explains the mechanism under "Command, cronjob part or view re-implemented".

Calling one from your own code

// a view's work from a script, or a command from a cronjob part
$result = \Exponential\View\Kernel\Content\History::create( 'kernel/content/history.php' )
            ->run( array( 'Params' => $params, 'Module' => $module ) );

create() honours Implementation[]; run() called directly does not fire the events (main() does).

How the code was moved

The code moved unchanged first (stage 1), so the behaviour is provably the same; five refinement stages followed: the shared option handling (stage 2), the services (stage 3), the extension points (stage 4), the clean-up (stage 5) and one more wave of services and shorter run() methods (stage 6, below).

  • A command's variables were globals, and functions of a script read them with global $cli; the moved code binds its variables to $GLOBALS, so those functions work unchanged. Functions and classes the script declared move to the class file, in the global namespace.
  • __FILE__ and __DIR__ in moved code still mean the original file ($this->scriptFile(), $this->scriptDir()).
  • Class names in the moved code are fully qualified (\eZCLI), because the class lives in a namespace.
  • Three entry points have a form of their own:
    • bin/php/ezpgenerateautoloads.php registers the classes, so it cannot rely on them: it requires the base classes and its own class file directly before calling main().
    • bin/php/ezasynchronouspublisher.php keeps declare( ticks=1 ) in the entry point, where the signal handlers need it.
    • bin/php/velocity-router.php, the router of PHP's built-in web server, runs before any autoloader exists and must require the front controller at the top level: it requires its class file (Exponential\Command\Kernel\VelocityRouter, not a runnable) and calls the static route(), which answers with true, false or the front controller to require.
  • Not moved, on purpose: bin/php/exprepair.php, because it runs without the Composer libraries.
  • The clean-up removed what the moves left behind: the start of 40 commands became $this->startup(), the session basket hook that two class files declared as the same global function (eZSessionBasketGarbageCollector, a redeclaration waiting to happen) is now SessionGarbageCollector::cleanupBaskets(), and variables no code used any more left the global bindings.

Stage 6: more shared code, shorter run() methods

Services. Two more pairs did the same work twice:

  • cronjobs/old_drafts_cleanup.php and cronjobs/internal_drafts_cleanup.php were the same code with other settings. Both now call Exponential\Service\DraftsCleanup::cleanup( DraftsCleanup::USER_DRAFTS ) or ( DraftsCleanup::INTERNAL_DRAFTS ). The service reads content.ini [VersionManagement] (DraftsDuration/DraftsCleanUpLimit, InternalDraftsDuration/InternalDraftsCleanUpLimit, with the same defaults of 90 days and 24 hours) and returns null when no lifetime is set. The parts print what they printed before.
  • The clear-cache toolbar (setup/cachetoolbar) clears All, Template, Content, Template and content, and INI through expCacheManager::clear(), as setup/cache and exp:cache do. Its Static, ContentNode and ContentSubtree choices are unchanged.

Not shared, because the two sides do different work: the search index (updatesearchindex.php reindexes every object; the indexcontent part works through the queue of pending changes; no view does either) and the URL aliases (updateniceurls.php regenerates them all; content/urlalias edits single aliases). The static cache, cache clearing, cluster purge and RSS import cleanup already had one implementation each (expStaticCacheRunner, expCacheManager, eZScriptClusterPurge, expCleanupRSS).

Shorter run(). The longest run() methods were split into named protected methods, as a pure extract-method refactoring. Each method holds whole top-level statements of run(), moved unchanged:

Runnablerun() beforeaftermethods
setup/info1114153systemInfo(), webserverInfo(), siteInfo(), engineSourceInfo(), engineArchiveCheck(), phpCachesInfo(), velocityServerInfo(), responseCacheInfo(), httpCacheInfo(), sqlProfileInfo()
ezpm.php1049124parseArguments(), checkCommands(), connectDatabase(), runCommands()
shop/dashboard906128currencies(), recentOrders(), allTimeFigures(), waitingOrders(), unfinishedBaskets(), products(), shopConfiguration(), nextSteps()
class/edit86327811, from fetchClassVersion() to newAttribute()
role/edit808237applyRole(), addLimitation(), customFunction(), selectLimitationValues(), discardLimitation(), createPolicy()
updateniceurls.php787376importOldUrls()
content/edit524163cancelDraft(), newDraft(), selectLanguage(), chooseEditLanguage(), findEditVersion()
rss/edit_export493145storeExport(), changeItemCount(), loadExport(), opmlImport()
preload.php473309warmSections(), spiderSite()
cache.php (exp:cache)439225runGroup()

The rules that make it safe:

  • run()'s variables reach the method by reference (protected function newDraft( &$http, &$obj, ... )), so the method reads and changes the same variables. A variable the method alone uses stays local to it only when nothing else could have given it a value: for a view, no name of the view's parameters (module.php) or of the variables eZProcess::runFile() hands over.
  • A command's variables are globals, which functions of the script read with global. The method binds those the same way run() does (${$__name} = &$GLOBALS[$__name]) instead of taking them as parameters.
  • A block that returns from run() (a view's return $Module->redirectToView( ... )) returns the same value from the method, and $this when it ends without returning. The call passes on anything else: if ( ( $__return = $this->newDraft( ... ) ) !== $this ) return $__return;. $this is the one value run() never returned before, because the object did not exist then.
  • Blocks with global, static variables, variable variables, compact()/extract(), __LINE__/__METHOD__, break/continue that leave the block, or that rebind (= &, unset()) a variable run() also has, stay in run(). That is why velocity.php (proved only by running it), ezsubtreecopy.php and content/action (both reworked elsewhere) and updateniceurls.php's node alias update (it unsets a global) are not split.

Proved by putting every method's body back in place of its call, which gives the file run() had before, byte for byte, for all ten. The pages of every split view are the same before and after (main content text and form fields, numbers aside), as are the real runs of ezpm.php list|help, exp:cache list|tags|status|clear --dry-run, and the --help of every kernel command.

Extension commands now start the same way as the kernel's: in cjw_newsletter, enhancedselection2, explayouts, ezprestapi, eztags, ezupdate, git_manager and xrowextract (25 commands), run() uses $this->cli(), $this->script(), $this->options() and, where the three steps follow each other, $this->startup(). Their --help output and exit codes are the same as before.

Proof, per kind and after every stage:

KindProved by
Commands--help output and exit code identical before and after, for every kernel command; commands run for real where that changes nothing (maintenance.php status, ezsessiongc.php, trashpurge.php --trashed-days=100000, ezcache.php --list-tags, ...)
Cronjob partsruncronjobs.php runs the default and frequent groups as before; parts in no group are run through eZRunCronjobs::runScript()
Module viewsthe pages of every module fetched before and after (status, title, errors, content); the edit-mode test of every content class

The stream views under Velocity

The views that answer with their own body instead of a page: setup/preloadstream, setup/staticcachestream, setup/cronjobsstream, and the Ajax answer of setup/cronjobs. They empty the output buffers with eZExecution::discardOutputBuffers() (since 2026-10-05). The loop they used before, while ( ob_get_level() > 0 ) ob_end_clean();, never ended under Velocity, because the worker keeps a buffer that cannot be removed, and the request hung.

Under PHP-FPM the progress events stream as they happen. Under Velocity (:8080) a response is sent when the script ends, so a progress page shows nothing while the work runs and then gets every event at once. The result is complete; only the live progress is missing. Use the site's PHP-FPM address to watch a long run.

Tests

php vendor/bin/phpunit tests/tests/kernel/classes/runnable/
TestCovers
RunnableTestthe base classes: main() of each kind, create(), the script file, the view result rule
MovedEntryPointsTestevery moved entry point: the class it calls exists, is mapped by the autoload arrays, parses and extends the right base class; the router form
CommandHelpersTestthe shared helpers, and that no kernel command creates eZScript or eZCLI itself
ServicesTestthe trash and session services, the drafts cleanup service and that their callers (the cache toolbar too) use them
ExtractedRunMethodsTestthe split run() methods: protected, by reference, called once, early returns passed on, the global bindings of a command, how long run() may be
RunnableExtensionPointsTestthe events of each kind, re-implementation of each kind through the settings, the listeners of [RunnableSettings], the private settings read of a command
RadSurveyRunnablesTestthe runnables in the extension point survey and its counts

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x