Page MenuHomeDevCentral

D522.diff
No OneTemporary

D522.diff

diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -5,7 +5,8 @@
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
-abstract class Controller extends BaseController
-{
- use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+use AuthGrove\Undo\UndoesOperations;
+
+abstract class Controller extends BaseController {
+ use AuthorizesRequests, DispatchesJobs, ValidatesRequests, UndoesOperations;
}
diff --git a/app/Undo/UndoStack.php b/app/Undo/UndoStack.php
new file mode 100644
--- /dev/null
+++ b/app/Undo/UndoStack.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace AuthGrove\Undo;
+
+use InvalidArgumentException;
+use OutOfBoundsException;
+use SplDoublyLinkedList;
+
+/**
+ * A stack of undo stores to maintain a LIFO collection of undoable operations.
+ */
+class UndoStack extends SplDoublyLinkedList {
+
+ ///
+ /// Constructor
+ ///
+
+ /**
+ * Initializes a new instance of an UndoStack object.
+ */
+ public function __construct () {
+ $this->setIteratorMode(self::IT_MODE_LIFO | self::IT_MODE_KEEP);
+ }
+
+ ///
+ /// List helper methods
+ ///
+
+ /**
+ * @return bool
+ */
+ protected function isLIFO () {
+ $mode = $this->getIteratorMode();
+ return ($mode & self::IT_MODE_LIFO) == self::IT_MODE_LIFO;
+ }
+
+ /**
+ * Removes an element.
+ * Behaves like offsetUnset, fixed for LIFO list, like a stack.
+ *
+ * @param mixed $index The offset, ascending for FILO, descending for LIFO
+ */
+ protected function offsetForeachIndexUnset ($index) {
+ if ($index === -1) {
+ // Lifo, element doesn't exist
+ return;
+ }
+
+ if ($this->isLIFO()) {
+ $fixedIndex = $this->count() - 1 - $index;
+ } else {
+ $fixedIndex = $index;
+ }
+
+ $this->offsetUnset($fixedIndex);
+ }
+
+
+ ///
+ /// Stack helper methods
+ ///
+
+ /**
+ * Gets a specified store.
+ *
+ * @param string $hash The hash of the store to get
+ * @param out mixed $index The index of the found store [facultative]
+ * @var mixed|null
+ */
+ public function getStore ($hash, &$index = null) {
+ foreach ($this as $index => $store) {
+ if ($store->getControlHash() === $hash) {
+ return $store;
+ }
+ }
+
+ $index = -1;
+ return null;
+ }
+
+ /**
+ * Gets a specified store, and discards it from the stack.
+ *
+ * @param string $hash The hash of the store to pull
+ * @var mixed|null
+ */
+ public function pullStore ($hash) {
+ $store = $this->getStore($hash, $index);
+ $this->offsetForeachIndexUnset($index);
+
+ return $store;
+ }
+
+ ///
+ /// Undo helper methods
+ ///
+
+ /**
+ * Undoes the last stacked operation.
+ */
+ public function undoLast () {
+ if ($this->isEmpty()) {
+ throw new OutOfBoundsException("UndoStack is empty.");
+ }
+
+ $store = $this->pop();
+ if (!is_a($store, UndoStore::class)) {
+ throw new InvalidArgumentException("UndoStack contained an item of unexpected type.");
+ }
+
+ $store->restoreState();
+ }
+
+ /**
+ * Undoes a specified operation.
+ *
+ * @param string $hash The hash of the store to restore instance state
+ * @param out mixed $return The value returned by the method called to undo the operation
+ * @return Undoable The store's instance, after its state is restored
+ */
+ public function undo ($hash, &$return = null) {
+ $store = $this->pullStore($hash);
+ if ($store === null) {
+ throw new OutOfBoundsException("Hash not found.");
+ }
+
+ return $store->restoreState($return);
+ }
+
+}
diff --git a/app/Undo/UndoStore.php b/app/Undo/UndoStore.php
new file mode 100644
--- /dev/null
+++ b/app/Undo/UndoStore.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace AuthGrove\Undo;
+
+use Hash;
+use InvalidArgumentException;
+use OutOfBoundsException;
+
+class UndoStore {
+
+ ///
+ /// Private members
+ ///
+
+ /**
+ * The serialized instance of the object stored.
+ *
+ * @var string
+ */
+ private $serializedInstance;
+
+ /**
+ * The operation control ID.
+ *
+ * @var string
+ */
+ private $controlHash = '';
+
+ ///
+ /// Public properties
+ ///
+
+ /**
+ * The method to call to restore the state.
+ * This method must belongs to the stored object.
+ *
+ * @var string
+ */
+ public $restoreMethod = 'save';
+
+ /**
+ * The parameters of the method to call to restore the state.
+ *
+ * @var array
+ */
+ public $restoreMethodParameters = [];
+
+ /**
+ * Gets the stored instance.
+ *
+ * @return mixed
+ */
+ public function getInstance() {
+ return unserialize($this->serializedInstance);
+ }
+
+ /**
+ * Sets a new instance of an object to store.
+ *
+ * @param mixed The instance to store
+ */
+ public function setInstance ($instance) {
+ $this->serializedInstance = serialize($instance);
+ }
+
+ /**
+ * Gets the control ID of the instance. This allows to ensure its integrity.
+ *
+ * @return string An hash of the instance properties
+ */
+ public function getControlHash () {
+ if ($this->controlHash === '') {
+ $this->computeControlHash();
+ }
+ return $this->controlHash;
+ }
+
+ ///
+ /// Constructor
+ ///
+
+ /**
+ * Initializes a new instance of the UndoStore object.
+ *
+ * @param mixed $instance The instance of the object to store
+ */
+ public function __construct ($instance) {
+ $this->setInstance($instance);
+ }
+
+ ///
+ /// Control ID
+ ///
+
+ /**
+ * Determines if the operation control identifiant is the same than defined in the store.
+ *
+ * This allows for example to avoid to restore stale session data and ensure the user wants really to restore this instance.
+ *
+ * @param string $actualControlHash The operation control id to compare
+ * @return bool
+ */
+ public function isSameControlHash ($actualControlHash) {
+ return $this->controlHash !== '' && $this->controlHash === $actualControlHash;
+ }
+
+ /**
+ * Determines the object integrity is intact, ie properties has not been modified since last control id computation
+ */
+ public function checkIntegrity () {
+ $hash = $this->getControlHashForCurrentData();
+ return hash_equals($hash, $this->controlHash);
+ }
+
+ /**
+ * Computes a control id from the stored information
+ */
+ public function computeControlHash () {
+ $this->controlHash = $this->getControlHashForCurrentData();
+ }
+
+ /**
+ * @return string The control hash
+ */
+ protected function getControlHashForCurrentData () {
+ $data = $this->getDataForControlHash();
+ return hash("ripemd160", $data);
+ }
+
+ /**
+ * Gets a unique string representation of the current store.
+ *
+ * @return string
+ */
+ protected function getDataForControlHash () {
+ return $this->serializedInstance
+ . $this->restoreMethod
+ . serialize($this->restoreMethodParameters);
+ }
+
+ ///
+ /// Restore
+ ///
+
+ /**
+ * Restores previous state of the stored instance.
+ *
+ * @param out mixed $return The restore method's return value
+ * @return mixed The restored instance
+ */
+ public function restoreState (&$return = null) {
+ $instance = $this->getInstance();
+
+ if ($instance === null) {
+ throw new OutOfBoundsException;
+ }
+
+ if (!method_exists($instance, $this->restoreMethod)) {
+ throw new InvalidArgumentException;
+ }
+
+ $return = call_user_func_array(
+ [$instance, $this->restoreMethod],
+ $this->restoreMethodParameters
+ );
+ return $instance;
+ }
+
+}
diff --git a/app/Undo/Undoable.php b/app/Undo/Undoable.php
new file mode 100644
--- /dev/null
+++ b/app/Undo/Undoable.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace AuthGrove\Undo;
+
+interface Undoable {
+
+ /**
+ * Undoes a destructive operation.
+ *
+ * @return bool
+ */
+ public static function undo (UndoStore $undoOperation, $operationControlHash, &$restored);
+
+ /**
+ * @return UndoStore
+ */
+ public function prepareUndoStore ();
+
+}
diff --git a/app/Undo/UndoesOperations.php b/app/Undo/UndoesOperations.php
new file mode 100644
--- /dev/null
+++ b/app/Undo/UndoesOperations.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace AuthGrove\Undo;
+
+use App;
+
+trait UndoesOperations {
+
+ /*
+ |--------------------------------------------------------------------------
+ | Undoes operations
+ |-------------------------------------------------------------------------
+ |
+ | This trait for a Laravel controller allows to maintain an UndoStack
+ | instance stored at the 'undo' key in the user session.
+ |
+ */
+
+ /**
+ * @return UndoStack
+ */
+ public function getUndoStack() {
+ return App::make('request')
+ ->session()
+ ->get('undo', new UndoStack);
+ }
+
+ /**
+ * @return string The stored operation control hash
+ */
+ public function allowUndo (Undoable $undoable) {
+ $store = $undoable->prepareUndoStore();
+ $this->getUndoStack()->push($store);
+ return $store->getControlHash();
+ }
+
+}
diff --git a/app/Undo/WithUndo.php b/app/Undo/WithUndo.php
new file mode 100644
--- /dev/null
+++ b/app/Undo/WithUndo.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace AuthGrove\Undo;
+
+/**
+ * Trait offering an implementation for Undoable for default UndoStore
+ * parameters.
+ */
+trait WithUndo {
+
+ /**
+ * Undoes a destructive operation.
+ *
+ * @param UndoStore $store
+ * @param string $storeHash
+ * @param mixed $restored The stored instance, to be able to further interact with it after undo
+ * @return bool true if the operation is undone successfully; otherwise, false
+ */
+ public static function undo (UndoStore $store, $storeHash, &$restored) {
+ // Ensures we undo the operation required by the user
+ if (!$store->isSameControlHash($storeHash)) {
+ return false;
+ }
+
+ if (!$store->checkIntegrity()) {
+ return false;
+ }
+
+ $restored = $store->restoreState($return);
+
+ return (bool)$return;
+ }
+
+ /**
+ * Prepares an undo store, ie a glass coffin with a serialized copy of our
+ * instance and instructions how to undo the destructive operation.
+ *
+ * @return UndoStore
+ */
+ public function prepareUndoStore () {
+ return new UndoStore($this);
+ }
+
+}
diff --git a/resources/lang/en/undo.php b/resources/lang/en/undo.php
new file mode 100644
--- /dev/null
+++ b/resources/lang/en/undo.php
@@ -0,0 +1,17 @@
+<?php
+
+return [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Undo
+ |--------------------------------------------------------------------------
+ |
+ | The following language lines are the default messages to undo operations.
+ |
+ */
+
+ "success" => "Operation undone.",
+ "failure" => "Can't undo the operation.",
+
+];
diff --git a/tests/Undo/UndoStackTest.php b/tests/Undo/UndoStackTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Undo/UndoStackTest.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace AuthGrove\Tests\Undo;
+
+use AuthGrove\Undo\UndoStack;
+use AuthGrove\Undo\UndoStore;
+use AuthGrove\Tests\TestCase;
+
+/**
+ * Tests for the UndoStack class
+ */
+class UndoStackTest extends TestCase {
+
+ /**
+ * The stack to test
+ * @var AuthGrove\Undo\UndoStack
+ */
+ protected $stack;
+
+ /**
+ * An array of three operations to store
+ * @var AuthGrove\Undo\UndoStore[]
+ */
+ protected $stores = [];
+
+ ///
+ /// Test preparation
+ ///
+
+ public function setUp () {
+ $this->stack = new UndoStack;
+
+ parent::setUp();
+ }
+
+ /**
+ * Mocks some undoable operations, stores them,
+ * fills the stack with the stores.
+ *
+ * @param int $amount The amoutn of store to stack
+ */
+ public function fillStack ($amount = 3) {
+ for ($i = 0 ; $i < $amount ; $i++) {
+ $store = static::getUndoStore($i);
+ $this->stack->push($store);
+ }
+ }
+
+ /**
+ * Mocks an undoable object and stores it.
+ *
+ * @param int $id The undoable object's value of the id property
+ * @return \AuthGrove\Undo\UndoStore
+ */
+ protected static function getUndoStore ($id = 0) {
+ return new UndoStore(new UndoableMock($id));
+ }
+
+ ///
+ /// Tests
+ ///
+
+ public function testUndoLastEmptiesTheStack () {
+ $this->fillStack();
+ for ($i = 0 ; $i < 3 ; $i++) {
+ $this->stack->undoLast();
+ }
+ $this->assertEquals(0, $this->stack->count());
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testUndoLastOnAnEmptyStackThrowsException () {
+ $this->stack->undoLast();
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testCorruptedUndoStackThrowsException () {
+ $this->stack->push(1); // uh oh, this is an integer, not an UndoStore item!
+ $this->stack->undoLast();
+ }
+
+ public function testStackIsLIFO () {
+ $this->fillStack(); // Stored instance id is 0, 1, 2
+
+ $i = 2; // Should be 2, 1 0
+ foreach ($this->stack as $store) {
+ $this->assertEquals($i--, $store->getInstance()->id);
+ }
+ }
+
+ public function testUndo () {
+ $store = static::getUndoStore();
+ $store->getInstance()->delete();
+ $hash = $store->getControlHash();
+
+ $this->stack->push($store);
+ $this->stack->undo($hash);
+
+ $this->assertEquals(true, $store->getInstance()->enabled);
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testUndoThrowsExceptionWhenHashIsInvalid () {
+ $this->stack->undo("invalidhash");
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testUndoThrowsExceptionWhenHashIsNull () {
+ $this->stack->undo(null);
+ }
+
+}
diff --git a/tests/Undo/UndoStoreTest.php b/tests/Undo/UndoStoreTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Undo/UndoStoreTest.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace AuthGrove\Tests\Undo;
+
+use AuthGrove\Undo\UndoStore;
+use AuthGrove\Tests\TestCase;
+
+use stdClass;
+
+class UndoStoreTest extends TestCase {
+
+ protected $instance;
+
+ protected $undoStore;
+
+ ///
+ /// Test preparation
+ ///
+
+ public function setUp () {
+ $this->instance = static::mockInstanceToStore();
+ $this->undoStore = $this->getUndoStore();
+
+ parent::setUp();
+ }
+
+ protected static function mockInstanceToStore () {
+ return new UndoableMock();
+ }
+
+ protected function getUndoStore () {
+ return new UndoStore($this->instance);
+ }
+
+ ///
+ /// Tests
+ ///
+
+ public function testSetInstance () {
+ $instance = clone $this->instance;
+ $this->undoStore->setInstance($instance);
+
+ $this->assertEquals(
+ $instance,
+ $this->undoStore->getInstance()
+ );
+ }
+
+ public function testGetInstance () {
+ $this->assertEquals(
+ $this->instance,
+ $this->undoStore->getInstance()
+ );
+ }
+
+ public function testCheckIntegrityWhenNotYetComputed () {
+ // If there is no integrity check, integrity can't be verified
+ $this->assertFalse($this->undoStore->checkIntegrity());
+ }
+
+ public function testCheckIntegrity () {
+ // Compute it. Check it.
+ $controlHash = $this->undoStore->getControlHash();
+ $this->assertInternalType("string", $controlHash);
+ $this->assertTrue($this->undoStore->checkIntegrity());
+ }
+
+ public function testCheckIntegrityWhenDataIsTampered () {
+ $controlHash = $this->undoStore->getControlHash();
+
+ $tamperedInstance = clone $this->instance;
+ $tamperedInstance->foo = 'quux';
+ $this->undoStore->setInstance($tamperedInstance);
+
+ $this->assertFalse($this->undoStore->checkIntegrity());
+
+ $this->undoStore->computeControlHash();
+ $afterTamperControlHash = $this->undoStore->getControlHash();
+ $this->assertNotEquals($controlHash, $afterTamperControlHash);
+ }
+
+ public function testIsSameControlHash () {
+ $controlHash = $this->undoStore->getControlHash();
+ $this->assertTrue(
+ $this->undoStore->isSameControlHash($controlHash)
+ );
+ }
+
+ public function testIsSameControlHashWhenItIsNot () {
+ $this->undoStore->computeControlHash();
+ $this->assertFalse(
+ $this->undoStore->isSameControlHash("somethingelse")
+ );
+ }
+
+ public function testIsSameControlHashWhenEmpty () {
+ $this->assertFalse(
+ $this->undoStore->isSameControlHash("")
+ );
+ }
+
+ public function testRestoreState () {
+ $instance = clone $this->instance;
+
+ $instance->enabled = false;
+ $this->undoStore->setInstance($instance);
+
+ $this->assertFalse(
+ $this->undoStore->getInstance()->enabled
+ );
+
+ $this->assertTrue(
+ $this->undoStore->restoreState()->enabled
+ );
+ }
+
+ /**
+ * @expectedException OutOfBoundsException
+ */
+ public function testRestoreStateWhenThereIsNoInstance () {
+ $this->undoStore->setInstance(null);
+ $this->undoStore->restoreState();
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testRestoreStateWhenTheRestoreMethodDoesNotExist () {
+ $this->undoStore->setInstance(new stdClass);
+ $this->undoStore->restoreState();
+ }
+
+}
diff --git a/tests/Undo/UndoableMock.php b/tests/Undo/UndoableMock.php
new file mode 100644
--- /dev/null
+++ b/tests/Undo/UndoableMock.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace AuthGrove\Tests\Undo;
+
+use AuthGrove\Undo\Undoable;
+use AuthGrove\Undo\WithUndo;
+
+class UndoableMock implements Undoable {
+
+ use WithUndo;
+
+ ///
+ /// Some dummy properties
+ ///
+
+ public $foo = 'bar';
+
+ public $bar = [7, 21, 42];
+
+ public $id;
+
+ ///
+ /// Constructor
+ ///
+
+ public function __construct ($id = 0) {
+ $this->id = $id;
+ }
+
+ ///
+ /// To mock restore process
+ ///
+
+ public $enabled = true; // never deleted or restored
+
+ public function save () {
+ $this->enabled = true;
+ }
+
+ public function delete () {
+ $this->enabled = false;
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Dec 20, 06:47 (20 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2307722
Default Alt Text
D522.diff (19 KB)

Event Timeline