Creating tasks

Creating tasks

How to create tasks, that can either be called from the url or as a clientscript, is not very well documented. I wanted to create a task that could add unique URLSegments to thousands of Product records and that was the perfect opportunity to delve into this. So first I'll take a look at some existing tasks. If you're not that interested in the how and why, skip this and jump right away to A truly simple task that says hi.

How does the task /dev/build/ work?

Parsing of the url http://mydomain/dev/build/ in fact starts in /sapphire/_config.php where the URLSegment 'dev' is set to call the controller class called 'DevelopmentAdmin':

Director::addRules(10, array(
	...
	'dev' => 'DevelopmentAdmin',
	...
));

DevelopmentAdmin checks whether we're in a commandline environment, or in a browser situation (in which case the user needs to be logged in or the site in dev-mode) and will redirect to the loginscreen if need be.

The second param, 'build', calls the DevelopmentAdmin::build() method, that performs the task of (re)building database tables and uses an instance of the DebugView class to write the results to screen. Build is a task 'build-in' to the DevelopmentAdmin - to add your own task you'd have to extend or decorate the DevelopmentAdmin class - or create a separate task

How does the task i18nTextCollectorTask work?

One of the tasks I use a lot: it collects translations from a module and creates an en_US.php languagefile. This starts in the same way, this time by calling the DevelopmentAdmin::tasks() method instead of the build() method - but then it gets more complicated.

The tasks method creates an instance of the TaskRunner class - yet another controller. If a second param was provided (in this case i18TextCollectorTask) it triggers the TaskRunner::runTask() method, that tries to instantiate an instance of the requested class, in this case i18TextCollectorTask class.

BuildTask - the tasks base-class

The i18TextCollectorTask class itself is an extension of the BuildTask class. The TaskRunner::runTask() method really only wants to run tasks that are extensions of the BuildTask class - a static 'generic' class, that provides a title, a description, and a run($request) Method. Besides that, any BuildTask extension can be disabled by setting it's protected $enabled property to false.

 A truly simple task that says hi

Just to make sure I get all this, I'll start by creating a supersimple task. It'll say 'hi'. Store it somewhere (mysite/code) as SayHiTask.php

<?php
class SayHiTask extends BuildTask {

    protected $title = 'Say Hi';

    protected $description = 'A class that says <strong>Hi</strong>';

    protected $enabled = true;

    function run($request) {
        echo "I'm trying to say hi...";
    }
}

Nice! Check out http://mydomain/dev/tasks You might have to do a ?flush=1, but after that you'll see a list of available development tasks, and it has already found the SayHiTask and shows it's title and description. Clicking the link you'll see a screen that says Running task 'Say Hi'... and the expected output 'I'm trying to say hi', which means the run() method has been executed. Setting $enabled=false will remove the task from the list effectively. Can't be any simpler, once you know it!

Updating products

So now for adding the URLSegment to 1868 products. I found that I could do this in one run, since it didn't take up much memory after all, and it all happens on my testing server. The actual adding of the URLSegment takes place in the onBeforeWrite() method of my Product class. SSBits has a fine tutorial that includes how to do this here. In my task I just needed to write the objects.

<?php
class ProductUpdateTask extends BuildTask {

    protected $title = 'Update Products';

    protected $description = 'Update products: update all products that have no URLSegment';

    protected $enabled = true;

    function run($request) {
        $this->updateProducts();
    }

    function updateProducts() {
    	$Products = DataObject::get(
			$callerClass = 'BalbusProduct',
			$filter = '',
			$sort = '',
			$join = '',
			$limit = ''
		);
        $count = 0;
		if ($Products && !empty($Products)) {
            $count = $Products->Count();
			foreach ($Products as $product) {
				echo $product->Name . "<br />";
				$product->write();
			}
			echo "<br /><br /><strong>{$count} Products processed...</strong><br />";
		}
		else echo 'No products found...';
    }
}

This works in my situation. But if you have thousands of products or you want to perform lots of complicated actions, it might be useful to think about using batches.

Processing a task in batches

Hamish Friedlander has created a SilverStripe Recipes repository here. The reason I mention it is that he posted a SanitiseTasks.php file there,  where he processes a large number of versioned pages in batches of 50, run as separate tasks. As far as I understand it, this task can only be run from the commandline, as it's using the php command..?

Other options: keep track of progress in a datatable/file, and create a cron-job 

Don't read this, it's confusing: I was thinking of a simple way to do this from the commandline - use an extra param to sort of 'paginate' (using limit) the records. It would mean executing a script a number of times, but hey... I could even create a (database)table to keep track of pagination, so you I wouldn't have to change the url all the time... Do a Javascript onload event that reloads the page until it is ready (check on the existence of some echood element... Eh... OK, I have access to my servers, so do I need to?

Comments

  • Thanks João, I've updated the url.

    Verstuurd door Martine, 27/05/2012 7:54pm (5 jaar geleden)

  • Thanks, for this hit, that is not documented.
    Only one correction, the link for tasks is:
    http://mydomain/dev/tasks

    Verstuurd door João Martins, 20/05/2012 2:20pm (6 jaar geleden)

  • Nice write up, thanks!

    Verstuurd door Frank, 30/01/2012 2:19am (6 jaar geleden)

Het versturen van reacties is uitgeschakeld.

RSS feed voor reacties op deze pagina