diff --git a/composer.json b/composer.json index cb4d085..8aa6e41 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,9 @@ "email": "fmizzell.dev@gmail.com" } ], + "require": { + "getdkan/contracts": "^0.2.0" + }, "require-dev": { "phpunit/phpunit": "^7.5" } diff --git a/src/Hydratable.php b/src/HydratableTrait.php similarity index 77% rename from src/Hydratable.php rename to src/HydratableTrait.php index 37358f7..391428e 100644 --- a/src/Hydratable.php +++ b/src/HydratableTrait.php @@ -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); diff --git a/src/Job/AbstractPersistentJob.php b/src/Job/AbstractPersistentJob.php new file mode 100644 index 0000000..75e1ed1 --- /dev/null +++ b/src/Job/AbstractPersistentJob.php @@ -0,0 +1,80 @@ +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); + } +} diff --git a/src/Job/Job.php b/src/Job/Job.php index bdfd719..47b94ef 100644 --- a/src/Job/Job.php +++ b/src/Job/Job.php @@ -5,50 +5,56 @@ 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 { @@ -56,6 +62,17 @@ public function setTimeLimit(int $seconds): bool 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; @@ -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)); } } diff --git a/src/Job/Method.php b/src/Job/Method.php index 8b63ee5..caf7381 100644 --- a/src/Job/Method.php +++ b/src/Job/Method.php @@ -12,7 +12,6 @@ class Method extends Job public function __construct($object, $methodName) { - parent::__construct(); $this->object = $object; $this->methodName = $methodName; } @@ -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; - } } diff --git a/src/Result.php b/src/Result.php index 6dc8192..ef619c7 100644 --- a/src/Result.php +++ b/src/Result.php @@ -1,20 +1,21 @@ 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")); + } +} diff --git a/test/Job/JobTest.php b/test/Job/JobTest.php index 62af68b..4a97015 100644 --- a/test/Job/JobTest.php +++ b/test/Job/JobTest.php @@ -2,11 +2,12 @@ namespace ProcrastinatorTest\Job; -use Procrastinator\Job\Job; +use PHPUnit\Framework\TestCase; use Procrastinator\Job\Method; use Procrastinator\Result; +use ProcrastinatorTest\Job\Mock\TwoStage; -class RunnerTest extends \PHPUnit\Framework\TestCase +class JobTest extends TestCase { public function test() { @@ -68,28 +69,6 @@ public function testTwoStage() $this->assertEquals(json_encode(['a', 'b', 'c', 'd']), $result->getData()); } - public function testSerialization() - { - $statePropertyA = 1; - $statePropertyB = 2; - $timeLimit = 10; - - $job = new Method($this, "callMe"); - $job->setTimeLimit($timeLimit); - $job->setStateProperty('a', $statePropertyA); - $job->setStateProperty('b', $statePropertyB); - $job->run(); - - $json = json_encode($job->jsonSerialize()); - - $job2 = Method::hydrate($json); - - $this->assertEquals($statePropertyA, $job2->getStateProperty('a')); - $this->assertEquals($statePropertyB, $job2->getStateProperty('b')); - - $this->assertEquals($timeLimit, $job2->getTimeLimit()); - } - public function callMe() { } diff --git a/test/Job/Mock/Persistor.php b/test/Job/Mock/Persistor.php new file mode 100644 index 0000000..d95e618 --- /dev/null +++ b/test/Job/Mock/Persistor.php @@ -0,0 +1,55 @@ +newInstanceWithoutConstructor(); + } + + $object = json_decode($json); + + $job = $class->getParentClass()->getParentClass(); + + $result = $job->getProperty("result"); + $result->setAccessible(true); + $result->setValue($instance, Result::hydrate(json_encode($object->result))); + + $timeLimit = $job->getProperty("timeLimit"); + $timeLimit->setAccessible(true); + $timeLimit->setValue($instance, $object->timeLimit); + + $persistentJob = $class->getParentClass(); + + $identifier = $persistentJob->getProperty("identifier"); + $identifier->setAccessible(true); + $identifier->setValue($instance, $object->identifier); + + return $instance; + } + + public function errorOut() + { + $this->errorOut = true; + } + + protected function runIt() + { + if ($this->errorOut) { + throw new \Exception("ERROR"); + } + $this->setStateProperty("ran", true); + return; + } +} diff --git a/test/Job/TwoStage.php b/test/Job/Mock/TwoStage.php similarity index 94% rename from test/Job/TwoStage.php rename to test/Job/Mock/TwoStage.php index fc4305b..554a227 100644 --- a/test/Job/TwoStage.php +++ b/test/Job/Mock/TwoStage.php @@ -1,6 +1,6 @@