6

I need to implement a bunch of background processes that do things like send emails, query 3rd pary api etc.

I have the task in place, but i dont fully understand how steps work, docs are a little thin on this, can someone explain how this works?

10

You can implement your entire Task with a single step if you want. The only point of breaking it into multiple steps is so the task progress bar has an idea of how far along the task is.

In your case, since your task is broken up into multiple unrelated processes, you might want to implement it with subtasks. So the top-level task only has a couple of steps based on the different high-level aspects of the task, and create separate task classes for those individual steps.

So let's say you have two main steps:

  • call an external API
  • send emails

and (I’m guessing, just to illustrate how this could work), let’s say that the email recipients are being determined by that API call, so you need a little bit of cross-communication between those processes.

Here’s the gist of how you’d set those classes up:

Primary task class

class MyPlugin_PrimaryTask extends BaseTask
{
    private $_emails;

    public function getDescription()
    {
        return 'Blasting emails';
    }

    public function getTotalSteps()
    {
        return 2;
    }

    public function runStep($step)
    {
        switch ($step)
        {
            case 0:
            {
                return $this->runSubTask('MyPlugin_ApiTask', null, array(
                    'primaryTaskId' => $this->model->id
                ));
            }
            case 1:
            {
                return $this->runSubTask('MyPlugin_EmailTask', null, array(
                    'emails' => $this->_emails;
                ));
            }
        }
    }

    public function setEmails($emails)
    {
        $this->_emails = $emails;
    }
}

API-calling task

class MyPlugin_ApiTask extends BaseTask
{
    private $_parentTask;
    private $_emails;

    public function getDescription()
    {
        return 'Calling the external API';
    }

    public function getTotalSteps()
    {
        return 1;
    }

    public function runStep($step)
    {
        // Call the API and get a list of emails
        // ...
        // $emails = ...

        // Pass the emails along to the primary task
        $primaryTask = craft()->tasks->getTaskById($this->getSettings()->primaryTaskId);
        $primaryTask->getTaskType()->setEmails($emails);

        return true;
    }

    protected function defineSettings()
    {
        return array(
            'primaryTaskId' => AttributeType::Number
        );
    }
}

Email-sending task

class MyPlugin_EmailTask extends BaseTask
{
    public function getDescription()
    {
        return 'Sending the emails';
    }

    public function getTotalSteps()
    {
        return count($this->getSettings()->emails);
    }

    public function runStep($step)
    {
        $email = new EmailModel();
        $email->toEmail = $this->getSettings()->emails[$step];
        $email->subject = '...';
        $email->body = '...';
        craft()->email->sendEmail($email);

        return true;
    }

    protected function defineSettings()
    {
        return array(
            'emails' => AttributeType::Mixed
        );
    }
}
  • And taking my scenario, would i use sql limit and offset using the $step argument to break it into steps? – BillyMedia Apr 6 '16 at 7:58
  • @BillyMedia Just updated my answer with an example of how you might set this up, based on your question. – Brandon Kelly Apr 6 '16 at 10:14
2

Check out the example PowerNapTask provided by Pixel & Tonic — It implements a Task (and Subtask) with multiple steps. It should be fairly straightforward to modify that sample code to run your own custom logic.

  • I looked at the powernap plugin, but couldnt take that and turn it into what i needed, e.g. i have to send out an email to over a thousand users. Do i have the 1000 users loop within there being the email send in my run step with step set to 1? – BillyMedia Apr 5 '16 at 21:47
  • And after each iteration, return true to trigger the next step? – BillyMedia Apr 5 '16 at 21:49
  • @BillyMedia Yes, you would return true. You can structure it however you'd like.. Ideally, you would make every user a step within your task. That way you have some visual feedback as to how far along in the task process you are. – Chase Giunta Apr 5 '16 at 21:57
  • If you're wanting to fire all these off at once in sequence (emails, query 3rd party api, etc.) you'll want to look into subtasks, also shown in the PowerNap plugin. – Chase Giunta Apr 5 '16 at 22:00
  • Still a little confused as to how to send over 1000 emails? do I set getTotalSteps to a high number to allow the sending in chunks, then have a database query in runStep that sets a limit and an offset that sends out the emails? – BillyMedia Apr 5 '16 at 22:52
1

I'm following Brandon's example here but am having trouble passing the variable that is set from the first subtask to the second subtask. Here's my code:

Primary Task

<?php
namespace Craft;

class Products_TestPrimaryTask extends BaseTask
{
    public $myVar;

    public function getDescription()
    {
        return 'Running Primary Task';
    }

    public function getTotalSteps()
    {
        return 2;
    }

    public function runStep($step)
    {
        switch ($step)
        {
            case 0:
            {
                return $this->runSubTask('Products_TestSub1', null, array(
                    'primaryTaskId' => $this->model->id
                ));
            }
            case 1:
            {
                return $this->runSubTask('Products_TestSub2', null, array(
                    'subVar' => $this->myVar,
                ));
            }
        }

        return true;
    }

    public function setPrimaryTaskVar($var)
    {
        $this->myVar = $var;

        ProductsPlugin::log('Variable in Primary Task: ' . print_r($this->myVar, true));
    }
}

Sub Task 1

<?php
namespace Craft;

class Products_TestSub1Task extends BaseTask
{
    protected function defineSettings()
    {
        return [
            'primaryTaskId' => AttributeType::Number
        ];
    }

    public function getDescription()
    {
        return 'Running Sub Task 1';
    }

    public function getTotalSteps()
    {
        return 1;
    }

    public function runStep($step)
    {
        $primaryTask = craft()->tasks->getTaskById($this->getSettings()->primaryTaskId);
        $primaryTask->getTaskType()->setPrimaryTaskVar('hello');

        return true;
    }
}

Sub Task 2

<?php
namespace Craft;

class Products_TestSub2Task extends BaseTask
{
    protected function defineSettings()
    {
        return [
            'subVar' => AttributeType::String
        ];
    }

    public function getDescription()
    {
        return 'Running Sub Task 2';
    }

    public function getTotalSteps()
    {
        return 1;
    }

    public function runStep($step)
    {
        ProductsPlugin::log('Variable in Sub Task 2: ' . print_r($this->getSettings()->subVar, true));

        return true;
    }
}

So Sub Task 1 calls the setPrimaryTaskVar method in the Primary Task, which sets the variable that then gets passed to Sub Task 2 in step 2 of the Primary Task. However, when I try to log that variable in Sub Task 2 it is showing as null.

Am I missing something obvious here?

WORKAROUND

After playing around with this a bit more, the only way I could persist the data between steps in the Primary Task was to save the variable in question to a SESSION var. So the Primary Task class has been amended to the following. Hope this helps someone else:

<?php
namespace Craft;

class Products_TestPrimaryTask extends BaseTask
{
    /**
     * Returns the default description for this task.
     *
     * @return string
     */
    public function getDescription()
    {
        return 'Running Primary Task';
    }

    /**
     * Gets the total number of steps for this task.
     *
     * @return int
     */
    public function getTotalSteps()
    {
        return 2;
    }

    /**
     * Runs a task step.
     *
     * @param int $step
     * @return bool
     */
    public function runStep($step)
    {
        switch ($step)
        {
            case 0:
            {
                return $this->runSubTask('Products_TestSub1', null, array(
                    'primaryTaskId' => $this->model->id
                ));
            }
            case 1:
            {
                return $this->runSubTask('Products_TestSub2', null, array(
                    'subVar' => $_SESSION['subVar']
                ));
            }
        }

        return true;
    }

    public function setPrimaryTaskVar($var)
    {
        $_SESSION['subVar'] = $var;

        ProductsPlugin::log('Variable in Primary Task: ' . print_r($_SESSION['subVar'], true));
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.