Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
  .idea
  examples
  scripts
  src
  tests
  .gitignore
  .travis.yml
  README.md
  phpunit.xml
  composer.json
Size: Mime:
  README.md

php-jobs

Toolkit to handle running commandline tasks from PHP7+.

  • Simple to use
  • Highly configurable
  • Handles async process manages and relays exit codes correctly
  • Offers simple post processing and runtime hooks

Install

Run composer require crazyfactory/jobs to install the latest version into your composer powered project.

Usage

The JobManager-class manages your configurations and runs your tasks. It can be configured with plain arrays or instances of JobConfig.

Running a JobConfig will return a JobResult. You can add ResultProcessor- and RuntimeProcessor-instances to it to be called automatically whenever you're running a JobConfig.

For working examples have a look at the /examples-folder.

Basic usage

Create an instance of JobManager, add a JobConfig and run it!

// instantiate a JobManager
$jobManager = new JobManager();

// create a JobConfig to run some arbitrary php file
$jobConfig = new JobConfig([
    'name' => 'my-task',
    'cmd' => 'php /my-absolute-path/my-task.php'
]);

// add it to the manager
$jobManager->withJob($jobConfig);

// run!
$jobManager->run('my-task');

Adding jobs

Add multiple JobConfig instances at once

$jobManager->withJobs([
    $jobConfigOne,
    $jobConfigTwo,
    $jobConfigThree
]);

You can also provide plain arrays, they will be converted on the fly.

$jobManager->withJobs([
    ['name' => 'foo', 'cmd' => './bar'],
    ['name' => 'apple', 'cmd' => './pie'],
    ['name' => 'bonnie', 'cmd' => './clyde']
]);

If it's an array and there's no name defined, the index will be used instead

$jobManager->withJobs([
    'foo' => ['cmd' => './bar'],
    'apple' => ['cmd' => './pie'],
    'bonnie' => ['cmd' => './clyde']
]);

You can easily add your configuration from a .json-file.

$myConfig = json_decode(file_get_contents('my-jobs-config.json'), true);
$jobManager->withJobs($myConfig);

Note that using withJobs() and/or withJob() multiple will overwrite jobs with the same name, but will not discard other jobs already present.

Setting defaults

It's likely that you want to change the default options for all jobs. You can do this with withDefaultConfig().

$jobManager->withDefaultConfig(new JobConfig([
    'singleton' => true,
]);

When using withJobs() to add multiple jobs, by convention if a key matches the $treatKeyAsDefault-argument, it will be used as the default config for all configuration and not be added as a task. You can adjust this by providing a different $treatKeyAsDefault-argument. Or pass null to deactivate this altogether.

$jobManager->withJobs([
    'default' => ['singleton' => true],
    $configOne,
    $configTwo,
    $configThree
]);

Processors

The JobManager is intended to be integrated into other systems (like a crontab schedule, a CLI-tool, etc.) and therefor offers two types of hooks to simplify integration.

Runtime processors

A runtime processor is a class, which implements IJobRuntimeProcessor and will be called periodically during the execution of the script.

You can create any number of classes to work with the elapsed runtime, the time passed since the last call and the original JobConfig

class MyRuntimeProcessor implements IJobRuntimeProcessor 
{
    public function process(int $seconds, int $deltaSeconds, JobConfig $jobConfig)
    {
        $timeString = Format::secondsToTimeElapsedString($seconds);
        echo "\n[RUNTIME] running for {$seconds} seconds already...\n";
    }
}

Or send a warning to your fellow devs if a task takes longer than expected.

class SendMailRuntimeProcessor implements IJobRuntimeProcessor 
{
    protected $warningSent = false;
    public function process(int $seconds, int $deltaSeconds, JobConfig $jobConfig)
    {
        if ($seconds > 60 && !$this->warningSent) {
            sendmail('admin@example.com', $jobConfig->name . ' is running too long?!?');
            $this->warningSent = true;
        }
        
        // still running? 
        if ($seconds > 600) {
            // returning false from process() will quit the process!
            return false;
        }
    }
}

Just add any number of runtime processors to your JobManager before calling run()

$jobManager->withRuntimeProcessor(new SendMailRuntimeProcessor());
$jobManager->withRuntimeProcessor(new MyRuntimeProcessor());

If you don't want to create a class, you can use SimpleRuntimeProcessor instead.

$jobManager->withRuntimeProcessor(new SimpleRuntimeProcessor(function ($s, $d, $jobConfig) {
    echo "\n[RUNTIME] running for {$seconds} seconds already...\n";
}));

Result processors

A result processor is a class, which implements IJobResultProcessor and will be called after a job has finished execution.

By default any result is discarded and the exit code passed through, but it's a common scenario to log the output of the Job to a file for later review.

For this you can use the LogToDiskResultProcessor-class easily. Just provide a path to store the log files.

$jobManager->withResultProcessor(new LogToDiskResultProcessor('/my-app/log/jobs'));

We can create the directory for you if it helps :)

$resultProcessor = (new LogToDiskResultProcessor())
    ->withEnsuredDirectory('/my-app/log/jobs');
$jobManager->withResultProcessor($resultProcessor);

Using Slack? How about sending a message to your team whenever a job crashed?

$jobManager->withResultProcessor(new SimpleResultProcessor(function($jR, $jC) {
    if ($jR->getExitCode() > 0) {
        $message = $jC->name . ' broke down after ' . $jR->duration . ' seconds!';
        $mySlackClient->sendMessageToChannel('#dev', $message);
    }
}));

The JobConfig provides some common properties which by default are unused, but can be used by processors like the LogToDiscResultProcessor.

  • logError
  • logSuccess
  • reportError
  • reportSuccess

JobConfig implements ArrayAccess so you may also add any property you like via offsets and use them in your custom processors. If you prefer code-completion you can extend JobConfig and add additional @property-tags.

/**
 * @property int    $myCustomProperty
 */
class MyCustomJobConfig extends JobConfig 
{

}

Singletons

This package does offer a hook to provide Singleton compatibility. Add a class implementing IJobLockProvider and add it with $jobManager->withLockProvider($instance).

Here's an example for a simple file-based lock provider

class MyLockProvider implements IJobLockProvider
{
    protected $dir;
    
    protected $locks = [];
    
    public function __construct($dir) {
        $this->dir = $dir;
    }

    public function acquire($key) : bool {

        // Lock already acquired
        if (isset($this->>$locks[$key]) && self::$locks[$key]) {
            return false;
        }

        $path = self::getPath();
        $filename = $this->dir . DIRECTORY_SEPARATOR . md5($key) . '.lock';

        $res = fopen($filename, 'w');

        if (!is_resource($res)) {
            throw new \Exception("File '$filename' cant be accessed");
        }

        self::$locks[$key] = $res;

        return flock(self::$locks[$key], LOCK_EX + LOCK_NB);
    }

    public function release($key) : bool {

        // No Lock exists
        if (!isset(self::$locks[$key]) || !self::$locks[$key]) {
            return false;
        }

        flock(self::$locks[$key], LOCK_UN);

        unset(self::$locks[$key]);

        return true;
    }
}

Just add it to your JobManager and update your JobConfigs or default config.

// Add provider
$jobManager->withLockProvider(new MyLockProvider('/my-app/temp/lock'));

// Set singleton => true as default
$jobManager->withDefaultConfig(new JobConfig(['singleton => true']);

Now all your Jobs will run as singletons. You could also hook this to a database to manage locks for a clustered application, Neat!

Deploy this repo

The CI runner will take care of publishing after the build and tests where successful. To trigger this mechanism, just set a github tag with the new version number and push it as usual. Note: Please ensure that you push the tags to origin (github) first, otherwise the packaging service will not be able to find set the new version and therfor fail.