Browse Source

Merge pull request #10726 from jeremyharris/cache-fallback

[RFC] 3.next Cache fallback
Mark Story 8 years ago
parent
commit
aa69b564fb
3 changed files with 198 additions and 25 deletions
  1. 25 1
      src/Cache/Cache.php
  2. 7 5
      src/Cache/Engine/FileEngine.php
  3. 166 19
      tests/TestCase/Cache/CacheTest.php

+ 25 - 1
src/Cache/Cache.php

@@ -168,7 +168,31 @@ class Cache
         }
 
         $config = static::$_config[$name];
-        $registry->load($name, $config);
+
+        try {
+            $registry->load($name, $config);
+        } catch (RuntimeException $e) {
+            if (!array_key_exists('fallback', $config)) {
+                $registry->set($name, new NullEngine());
+                trigger_error($e->getMessage(), E_USER_WARNING);
+
+                return;
+            }
+
+            if ($config['fallback'] === $name) {
+                throw new InvalidArgumentException(
+                    sprintf('"%s" cache configuration cannot fallback to itself.', $name)
+                );
+            }
+
+            $fallbackEngine = clone static::engine($config['fallback']);
+            $newConfig = $config + ['groups' => [], 'prefix' => null];
+            $fallbackEngine->setConfig('groups', $newConfig['groups'], false);
+            if ($newConfig['prefix']) {
+                $fallbackEngine->setConfig('prefix', $newConfig['prefix'], false);
+            }
+            $registry->set($name, $fallbackEngine);
+        }
 
         if ($config['className'] instanceof CacheEngine) {
             $config = $config['className']->getConfig();

+ 7 - 5
src/Cache/Engine/FileEngine.php

@@ -420,21 +420,23 @@ class FileEngine extends CacheEngine
     {
         $dir = new SplFileInfo($this->_config['path']);
         $path = $dir->getPathname();
+        $success = true;
         if (!is_dir($path)) {
-            mkdir($path, 0775, true);
+            //@codingStandardsIgnoreStart
+            $success = @mkdir($path, 0775, true);
+            //@codingStandardsIgnoreEnd
         }
 
-        if ($this->_init && !($dir->isDir() && $dir->isWritable())) {
+        $isWritableDir = ($dir->isDir() && $dir->isWritable());
+        if (!$success || ($this->_init && !$isWritableDir)) {
             $this->_init = false;
             trigger_error(sprintf(
                 '%s is not writable',
                 $this->_config['path']
             ), E_USER_WARNING);
-
-            return false;
         }
 
-        return true;
+        return $success;
     }
 
     /**

+ 166 - 19
tests/TestCase/Cache/CacheTest.php

@@ -17,9 +17,12 @@ namespace Cake\Test\TestCase\Cache;
 use Cake\Cache\Cache;
 use Cake\Cache\CacheRegistry;
 use Cake\Cache\Engine\FileEngine;
+use Cake\Cache\Engine\NullEngine;
 use Cake\Core\Configure;
 use Cake\Core\Plugin;
 use Cake\TestSuite\TestCase;
+use InvalidArgumentException;
+use PHPUnit\Framework\Error\Error;
 
 /**
  * CacheTest class
@@ -65,6 +68,139 @@ class CacheTest extends TestCase
     }
 
     /**
+     * tests Cache::engine() fallback
+     *
+     * @return void
+     */
+    public function testCacheEngineFallback()
+    {
+        $filename = tempnam(TMP, 'tmp_');
+
+        Cache::setConfig('tests', [
+            'engine' => 'File',
+            'path' => $filename,
+            'prefix' => 'test_',
+            'fallback' => 'tests_fallback'
+        ]);
+        Cache::setConfig('tests_fallback', [
+            'engine' => 'File',
+            'path' => TMP,
+            'prefix' => 'test_',
+        ]);
+
+        $engine = Cache::engine('tests');
+        $path = $engine->getConfig('path');
+        $this->assertSame(TMP, $path);
+
+        Cache::drop('tests');
+        Cache::drop('tests_fallback');
+        unlink($filename);
+    }
+
+    /**
+     * tests handling misconfiguration of fallback
+     *
+     * @return void
+     */
+    public function testCacheEngineFallbackToSelf()
+    {
+        $filename = tempnam(TMP, 'tmp_');
+
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('cannot fallback to itself');
+
+        Cache::setConfig('tests', [
+            'engine' => 'File',
+            'path' => $filename,
+            'prefix' => 'test_',
+            'fallback' => 'tests'
+        ]);
+
+        Cache::engine('tests');
+
+        Cache::drop('tests');
+        unlink($filename);
+    }
+
+    /**
+     * tests Cache::engine() fallback when using groups
+     *
+     * @return void
+     */
+    public function testCacheFallbackWithGroups()
+    {
+        $filename = tempnam(TMP, 'tmp_');
+
+        Cache::setConfig('tests', [
+            'engine' => 'File',
+            'path' => $filename,
+            'prefix' => 'test_',
+            'fallback' => 'tests_fallback',
+            'groups' => ['group1', 'group2'],
+        ]);
+        Cache::setConfig('tests_fallback', [
+            'engine' => 'File',
+            'path' => TMP,
+            'prefix' => 'test_',
+            'groups' => ['group3', 'group1'],
+        ]);
+
+        $result = Cache::groupConfigs('group1');
+        $this->assertSame(['group1' => ['tests', 'tests_fallback']], $result);
+
+        $result = Cache::groupConfigs('group2');
+        $this->assertSame(['group2' => ['tests']], $result);
+
+        Cache::drop('tests');
+        Cache::drop('tests_fallback');
+        unlink($filename);
+    }
+
+    /**
+     * tests cache fallback
+     *
+     * @return void
+     */
+    public function testCacheFallbackIntegration()
+    {
+        $filename = tempnam(TMP, 'tmp_');
+
+        Cache::setConfig('tests', [
+            'engine' => 'File',
+            'path' => $filename,
+            'fallback' => 'tests_fallback',
+            'groups' => ['integration_group', 'integration_group_2']
+        ]);
+        Cache::setConfig('tests_fallback', [
+            'engine' => 'File',
+            'path' => $filename,
+            'fallback' => 'tests_fallback_final',
+            'groups' => ['integration_group'],
+        ]);
+        Cache::setConfig('tests_fallback_final', [
+            'engine' => 'File',
+            'path' => TMP,
+            'groups' => ['integration_group_3'],
+        ]);
+
+        $this->assertTrue(Cache::write('grouped', 'worked', 'tests'));
+        $this->assertTrue(Cache::write('grouped_2', 'worked', 'tests_fallback'));
+        $this->assertTrue(Cache::write('grouped_3', 'worked', 'tests_fallback_final'));
+
+        $this->assertTrue(Cache::clearGroup('integration_group', 'tests'));
+
+        $this->assertFalse(Cache::read('grouped', 'tests'));
+        $this->assertFalse(Cache::read('grouped_2', 'tests_fallback'));
+
+        $this->assertSame('worked', Cache::read('grouped_3', 'tests_fallback_final'));
+
+        Cache::drop('tests');
+        Cache::drop('tests_fallback');
+        Cache::drop('tests_fallback_final');
+        unlink($filename);
+    }
+
+    /**
      * Check that no fatal errors are issued doing normal things when Cache.disable is true.
      *
      * @return void
@@ -90,39 +226,43 @@ class CacheTest extends TestCase
         Cache::disable();
 
         $result = Cache::engine('tests');
-        $this->assertInstanceOf('Cake\Cache\Engine\NullEngine', $result);
+        $this->assertInstanceOf(NullEngine::class, $result);
     }
 
     /**
      * Test configuring an invalid class fails
      *
-     * @expectedException \RuntimeException
-     * @expectedExceptionMessage Cache engines must use Cake\Cache\CacheEngine
      * @return void
      */
     public function testConfigInvalidClassType()
     {
+        $this->expectException(Error::class);
+        $this->expectExceptionMessage('Cache engines must use Cake\Cache\CacheEngine');
+
         Cache::setConfig('tests', [
             'className' => '\StdClass'
         ]);
-        Cache::engine('tests');
+        $result = Cache::engine('tests');
+        $this->assertInstanceOf(NullEngine::class, $result);
     }
 
     /**
-     * Test engine init failing causes an error
+     * Test engine init failing triggers an error but falls back to NullEngine
      *
-     * @expectedException \RuntimeException
-     * @expectedExceptionMessage not properly configured
      * @return void
      */
     public function testConfigFailedInit()
     {
+        $this->expectException(Error::class);
+        $this->expectExceptionMessage('is not properly configured');
+
         $mock = $this->getMockForAbstractClass('Cake\Cache\CacheEngine', [], '', true, true, true, ['init']);
         $mock->method('init')->will($this->returnValue(false));
         Cache::setConfig('tests', [
             'engine' => $mock
         ]);
-        Cache::engine('tests');
+        $result = Cache::engine('tests');
+        $this->assertInstanceOf(NullEngine::class, $result);
     }
 
     /**
@@ -315,12 +455,13 @@ class CacheTest extends TestCase
             'groups' => ['posts', 'comments'],
         ]);
 
-        $expected = [
-            'posts' => ['latest'],
-            'comments' => ['latest'],
-        ];
         $result = Cache::groupConfigs();
-        $this->assertEquals($expected, $result);
+
+        $this->assertArrayHasKey('posts', $result);
+        $this->assertContains('latest', $result['posts']);
+
+        $this->assertArrayHasKey('comments', $result);
+        $this->assertContains('latest', $result['comments']);
 
         $result = Cache::groupConfigs('posts');
         $this->assertEquals(['posts' => ['latest']], $result);
@@ -332,12 +473,18 @@ class CacheTest extends TestCase
         ]);
 
         $result = Cache::groupConfigs();
-        $expected = [
-            'posts' => ['latest', 'page'],
-            'comments' => ['latest'],
-            'archive' => ['page']
-        ];
-        $this->assertEquals($expected, $result);
+
+        $this->assertArrayHasKey('posts', $result);
+        $this->assertContains('latest', $result['posts']);
+        $this->assertContains('page', $result['posts']);
+
+        $this->assertArrayHasKey('comments', $result);
+        $this->assertContains('latest', $result['comments']);
+        $this->assertNotContains('page', $result['comments']);
+
+        $this->assertArrayHasKey('archive', $result);
+        $this->assertContains('page', $result['archive']);
+        $this->assertNotContains('latest', $result['archive']);
 
         $result = Cache::groupConfigs('archive');
         $this->assertEquals(['archive' => ['page']], $result);