Chain authentication storage #2718

Closed
wants to merge 7 commits into
from
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Authentication
+ */
+
+namespace Zend\Authentication\Storage;
+
+use Zend\Stdlib\PriorityQueue;
+use Zend\Authentication\Storage\StorageInterface;
+
+/**
+ * @category Zend
+ * @package Zend_Authentication
+ * @subpackage Storage
+ */
+class Chain implements StorageInterface
+{
+ /**
+ * Contains all storage that this authentication method uses. A storage
+ * placed in the priority queue with a higher priority is always used
+ * before using a storage with a lower priority.
+ *
+ * @var PriorityQueue
+ */
+ protected $storageChain;
+
+ /**
+ * Initializes the priority queue.
+ */
+ public function __construct()
+ {
+ $this->storageChain = new PriorityQueue();
+ }
+
+ /**
+ * @param StorageInterface $storage
+ * @param integer $priority
+ */
+ public function add( StorageInterface $storage, $priority = 1 )
+ {
+ $this->storageChain->insert($storage, $priority);
+ }
+
+ /**
+ * Loop over the queue of storage until a storage is found that is non-empty. If such
+ * storage is not found, then this chain storage itself is empty.
+ *
+ * In case a non-empty storage is found then this chain storage is also non-empty. Report
+ * that, but also make sure that all storage with higher priorty that are empty
+ * are filled.
+ *
+ * @see StorageInterface::isEmpty()
+ */
+ public function isEmpty()
+ {
+ $storageWithHigherPriority = array();
+
+ // Loop invariant: $storageWithHigherPriority contains all storage with higher priorty
+ // than the current one.
+ foreach ($this->storageChain as $storage) {
+ if ( $storage->isEmpty() ) {
+ $storageWithHigherPriority[] = $storage;
+ continue;
+ }
+
+ $storageValue = $storage->read();
+ foreach ($storageWithHigherPriority as $higherPriorityStorage) {
+ $higherPriorityStorage->write($storageValue);
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * If the chain is non-empty then the storage with the top priority is guaranteed to be
+ * filled. Return its value.
+ *
+ * @see StorageInterface::read()
+ */
+ public function read()
+ {
+ return $this->storageChain->top()->read();
+ }
+
+ /**
+ * Write the new $contents to all storage in the chain.
+ *
+ * @see StorageInterface::write()
+ */
+ public function write( $contents )
+ {
+ foreach ($this->storageChain as $storage) {
+ $storage->write($contents);
+ }
+ }
+
+ /**
+ * Clear all storage in the chain.
+ *
+ * @see StorageInterface::clear()
+ */
+ public function clear()
+ {
+ foreach ($this->storageChain as $storage) {
+ $storage->clear();
+ }
+ }
+}
@@ -0,0 +1,150 @@
+<?php
+/**
+ * Zend Framework (http://framework.zend.com/)
+ *
+ * @link http://github.com/zendframework/zf2 for the canonical source repository
+ * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ * @package Zend_Uri
+ */
+
+namespace ZendTest\Authentication\Storage;
+
+use Zend\Authentication\Storage\Chain,
+ Zend\Authentication\Storage\StorageInterface,
+ Zend\Authentication\Storage\NonPersistent;
+
+use PHPUnit_Framework_TestCase as TestCase;
+
+/**
+ * @category Zend
+ * @package Zend_Auth
+ * @subpackage UnitTests
+ * @group Zend_Auth
+ */
+class ChainTest extends TestCase
+{
+ const ID = 1337;
+
+ /**
+ * Ensure chain without storage behavious as empty storage.
+ */
+ public function testEmptyChain()
+ {
+ $chain = new Chain;
+
+ $this->assertTrue($chain->isEmpty());
+ }
+
+ /**
+ * Ensure chain with single empty storage behavious as expected.
+ */
+ public function testSingularChainEmpty()
+ {
+ $chain = new Chain;
+ $chain->add($this->storageFactory());
+
+ $this->assertTrue($chain->isEmpty());
+ }
+
+ /**
+ * Ensure chain with single non-empty storage behavious as expected.
+ */
+ public function testSingularChainNonEmpty()
+ {
+ $chain = new Chain;
+ $chain->add($this->storageFactory(self::ID));
+
+ $this->assertFalse($chain->isEmpty());
+ $this->assertEquals(self::ID, $chain->read());
+ }
+
+ /**
+ * Ensure the priority of storage engines is correctly used.
+ */
+ public function testChainPriority()
+ {
+ $storageA = $this->storageFactory();
+ $storageB = $this->storageFactory(self::ID);
+
+ $chain = new Chain;
+ $chain->add($storageA); // Defaults to 1
+ $chain->add($storageB, 10);
+ $chain->isEmpty();
+
+ // Storage B has higher priority AND is non-empty. Thus
+ // storage A should been used at all and remain empty.
+ $this->assertTrue($storageA->isEmpty());
+ }
+
+ /**
+ * Ensure that a chain with empty storages is considered empty and
+ * won't populated any of its underlying storages.
+ */
+ public function testEmptyChainIsEmpty()
+ {
+ $emptyStorageA = $this->storageFactory();
+ $emptyStorageB = $this->storageFactory();
+
+ $chain = new Chain;
+ $chain->add($emptyStorageA);
+ $chain->add($emptyStorageB);
+
+ $this->assertTrue($chain->isEmpty());
+
+ // Storage A and B remain empty
+ $this->assertTrue($emptyStorageA->isEmpty());
+ $this->assertTrue($emptyStorageB->isEmpty());
+ }
+
+ /**
+ * Ensure that chain will yield non-empty if one of its underlying storage
+ * engines is non-empty.
+ *
+ * Make sure that storage engines with higher priority then the first non-empty
+ * storage engine get populated with that same content.
+ */
+ public function testSuccessfullReadWillPopulateStoragesWithHigherPriority()
+ {
+ $emptyStorageA = $this->storageFactory();
+ $emptyStorageB = $this->storageFactory();
+ $storageC = $this->storageFactory(self::ID);
+ $emptyStorageD = $this->storageFactory();
+
+ $chain = new Chain;
+ $chain->add($emptyStorageA);
+ $chain->add($emptyStorageB);
+ $chain->add($storageC);
+ $chain->add($emptyStorageD);
+
+ // Chain is non empty
+ $this->assertFalse($chain->isEmpty());
+ $this->assertEquals(self::ID, $chain->read());
+
+ // Storage A and B are filled
+ $this->assertFalse($emptyStorageA->isEmpty());
+ $this->assertEquals(self::ID, $emptyStorageA->read());
+ $this->assertFalse($emptyStorageA->isEmpty());
+ $this->assertEquals(self::ID, $emptyStorageB->read());
+
+ // Storage C and D remain identical
+ $this->assertFalse($storageC->isEmpty());
+ $this->assertEquals(self::ID, $storageC->read());
+ $this->assertTrue($emptyStorageD->isEmpty());
+ }
+
+ /**
+ * @param mixed $identity
+ * @return StorageInterface
+ */
+ protected function storageFactory( $identity = null )
+ {
+ $storage = new NonPersistent();
+
+ if ($identity !== null) {
+ $storage->write($identity);
+ }
+
+ return $storage;
+ }
+}