Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3912748
D522.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
D522.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D522: Undo mechanism
Attached
Detach File
Event Timeline
Log In to Comment