Skip to content

Commit

Permalink
Add a persistent Job. (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
fmizzell authored Oct 21, 2019
1 parent 688264a commit bd36810
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 98 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"email": "fmizzell.dev@gmail.com"
}
],
"require": {
"getdkan/contracts": "^0.2.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5"
}
Expand Down
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
80 changes: 80 additions & 0 deletions src/Job/AbstractPersistentJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Procrastinator\Job;

use Contracts\StorerInterface;
use Contracts\RetrieverInterface;
use Contracts\HydratableInterface;
use Procrastinator\Result;

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

public function run(): Result
{
$result = parent::run();
$this->selfStore();
return $result;
}

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

0 comments on commit bd36810

Please sign in to comment.