Skip to content

Add a persistent Job. #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 21, 2019
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
],
"require-dev": {
"phpunit/phpunit": "^7.5"
},
"require": {
"getdkan/contracts": "dev-loosen-storage-contracts"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once PR GetDKAN/contracts#10 (comment) gets merge, the above should be switched back to master

}
}
7 changes: 5 additions & 2 deletions src/Hydratable.php → src/HydratableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

namespace Procrastinator;

trait Hydratable
/**
* @todo Change name to HydratableTrait.
*/
trait HydratableTrait
{
public static function hydrate($json)
public static function hydrate(string $json, $instance = null)
{
$data = json_decode($json);

Expand Down
72 changes: 72 additions & 0 deletions src/Job/AbstractPersistentJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Procrastinator\Job;

use Contracts\StorerInterface;
use Contracts\RetrieverInterface;
use Contracts\HydratableInterface;

abstract class AbstractPersistentJob extends Job implements HydratableInterface
{
private $identifier;
private $storage;

public static function get(string $identifier, $storage, array $config = null)
{
if ($storage instanceof StorerInterface && $storage instanceof RetrieverInterface) {
$new = new static($identifier, $storage, $config);

$json = $storage->retrieve($identifier);
if ($json) {
return static::hydrate($json, $new);
}

$storage->store(json_encode($new), $identifier);
return $new;
}
return false;
}

protected function __construct(string $identifier, $storage, array $config = null)
{
$this->identifier = $identifier;
$this->storage = $storage;
}

public function setTimeLimit(int $seconds): bool
{
$return = parent::setTimeLimit($seconds);
$this->selfStore();
return $return;
}

public function jsonSerialize()
{
$object = parent::jsonSerialize();
$object->identifier = $this->identifier;
return $object;
}

protected function setStatus($status)
{
parent::setStatus($status);
$this->selfStore();
}

protected function setError($message)
{
parent::setError($message);
$this->selfStore();
}

protected function setState($state)
{
parent::setState($state);
$this->selfStore();
}

private function selfStore()
{
$this->storage->store(json_encode($this), $this->identifier);
}
}
99 changes: 54 additions & 45 deletions src/Job/Job.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,74 @@

use Procrastinator\Result;

/**
* @todo Change name to AbstractJob.
*/
abstract class Job implements \JsonSerializable
{
private $result;
private $timeLimit;
private $timeLimit = PHP_INT_MAX;

public function __construct()
{
$this->result = new Result(Result::STOPPED);
$this->timeLimit = PHP_INT_MAX;
}
abstract protected function runIt();

public function run(): Result
{
if($this->getResult()->getStatus() == Result::DONE) {
return $this->getResult();
if ($this->getResult()->getStatus() == Result::DONE) {
return $this->getResult();
}

// Trying again, clear the previous error.
if ($this->getResult()->getStatus() == Result::ERROR) {
$this->getResult()->setError("");
}

$this->result->setStatus(Result::IN_PROGRESS);
$this->setStatus(Result::IN_PROGRESS);

try {
$data = $this->runIt();
} catch (\Exception $e) {
$this->result->setStatus(Result::ERROR);
$this->result->setError($e->getMessage());
return $this->result;
$this->setError($e->getMessage());
return $this->getResult();
}

if ($data) {
if ($data instanceof Result) {
$this->result = $data;
} elseif (is_string($data)) {
$this->result->setData($data);
$this->result->setStatus(Result::DONE);
} else {
throw new \Exception("Invalid result or data format.");
}
$this->processDataFromRunIt($data);
} else {
$this->result->setStatus(Result::DONE);
$this->setStatus(Result::DONE);
}

return $this->result;
}

abstract protected function runIt();
private function processDataFromRunIt($data)
{
if ($data instanceof Result) {
$this->result = $data;
} elseif (is_string($data)) {
$this->result->setData($data);
$this->setStatus(Result::DONE);
} else {
throw new \Exception("Invalid result or data format.");
}
}

public function setTimeLimit(int $seconds): bool
{
$this->timeLimit = $seconds;
return true;
}

/**
* @todo Check why we need to allow external parties to affect our state.
* @todo Should this be renamed to setDataProperty? Should it be in Result?
*/
public function setStateProperty($property, $value)
{
$state = $this->getState();
$state[$property] = $value;
$this->setState($state);
}

public function getTimeLimit()
{
return $this->timeLimit;
Expand All @@ -80,41 +97,33 @@ public function getStateProperty(string $property, $default = null)

public function getResult(): Result
{
if (!isset($this->result)) {
$this->result = new Result();
}
return $this->result;
}

private function setState($state)
public function jsonSerialize()
{
$this->getResult()->setData(json_encode($state));
return (object) [
'timeLimit' => $this->timeLimit,
'result' => $this->getResult()->jsonSerialize()
];
}

public function setStateProperty($property, $value)
protected function setStatus($status)
{
$state = $this->getState();
$state[$property] = $value;
$this->setState($state);
$this->getResult()->setStatus($status);
}

public function jsonSerialize()
protected function setError($message)
{
return (object) [
'timeLimit' => $this->timeLimit,
'result' => $this->getResult()->jsonSerialize()
];
$this->result->setError($message);
$this->setStatus(Result::ERROR);
}

/**
* Hydrate an object from the json created by jsonSerialize().
* You will want to override this method when implementing specific jobs.
* You can use this function for the initial JSON decoding by calling
* parent::hydrate() in your implementation.
*
* @param string $json
* JSON string used to hydrate a new instance of the class.
*/
public static function hydrate($json)
protected function setState($state)
{
$data = json_decode($json);
return $data;
$this->getResult()->setData(json_encode($state));
}
}
21 changes: 0 additions & 21 deletions src/Job/Method.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class Method extends Job

public function __construct($object, $methodName)
{
parent::__construct();
$this->object = $object;
$this->methodName = $methodName;
}
Expand All @@ -21,24 +20,4 @@ protected function runIt()
{
return call_user_func([$this->object, $this->methodName]);
}

public static function hydrate($json): Method
{
$data = parent::hydrate($json);

$reflector = new \ReflectionClass(self::class);
$object = $reflector->newInstanceWithoutConstructor();

$reflector = new \ReflectionClass($object);

$p = $reflector->getParentClass()->getProperty('timeLimit');
$p->setAccessible(true);
$p->setValue($object, $data->timeLimit);

$p = $reflector->getParentClass()->getProperty('result');
$p->setAccessible(true);
$p->setValue($object, Result::hydrate(json_encode($data->result)));

return $object;
}
}
11 changes: 6 additions & 5 deletions src/Result.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<?php


namespace Procrastinator;

class Result implements \JsonSerializable
use Contracts\HydratableInterface;

class Result implements HydratableInterface
{
use Hydratable;
use HydratableTrait;

const STOPPED = 'stopped';
const IN_PROGRESS ='in_progress';
const IN_PROGRESS = 'in_progress';
const ERROR = 'error';
const DONE = 'done';

private $status = self::STOPPED;
private $data = "";
private $error = null;
private $error = "";

public function setStatus($status)
{
Expand Down
63 changes: 63 additions & 0 deletions test/Job/AbstractPersistentJobTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace ProcrastinatorTest\Job;

use Contracts\Mock\Storage\Memory;
use PHPUnit\Framework\TestCase;
use ProcrastinatorTest\Job\Mock\Persistor;

class AbstractPersistentJobTest extends TestCase
{
public function testSerialization()
{
$storage = new Memory();

$timeLimit = 10;
$job = Persistor::get("1", $storage);
$job->setStateProperty("ran", false);

$job->setTimeLimit($timeLimit);
$job->run();

$json = json_encode($job);

/* @var $job2 \Procrastinator\Job\AbstractPersistentJob */
$job2 = Persistor::hydrate($json);

$data = json_decode($job2->getResult()->getData());
$this->assertEquals(true, $data->ran);
$this->assertEquals($timeLimit, $job2->getTimeLimit());

$job3 = Persistor::get("1", $storage);

$data = json_decode($job3->getResult()->getData());
$this->assertEquals(true, $data->ran);
$this->assertEquals(true, $job3->getStateProperty("ran"));
$this->assertEquals(true, $job3->getStateProperty("ran2", true));
$this->assertEquals($timeLimit, $job3->getTimeLimit());
}

public function testBadStorage()
{
$this->assertFalse(Persistor::get("1", new class {
}));
}

public function testJobError()
{
$storage = new Memory();

$timeLimit = 10;
$job = Persistor::get("1", $storage);
$job->errorOut();

$job->setTimeLimit($timeLimit);
$job->run();

$this->assertEquals("ERROR", $job->getResult()->getError());

$job2 = Persistor::get("1", $storage);
$job2->run();
$this->assertEquals(true, $job2->getStateProperty("ran"));
}
}
Loading