diff --git a/src/Symfony/Component/Cache/ApcCache.php b/src/Symfony/Component/Cache/ApcCache.php new file mode 100644 index 0000000000000..e927625287445 --- /dev/null +++ b/src/Symfony/Component/Cache/ApcCache.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +/** + * APC Cache implementation + * + * Very simple implementation that can be used as default with various Symfony components + * that support caching, such as Validation, ClassLoader. + * + * @author Benjamin Eberlei + */ +class ApcCache implements CacheInterface +{ + public function __construct() + { + if (!extension_loaded('apc')) { + throw new \RuntimeException("You need the APC php extension installed to use this cache driver."); + } + } + + /** + * {@inheritdoc} + */ + public function fetch($id) + { + return apc_fetch($id); + } + + /** + * {@inheritdoc} + */ + public function contains($id) + { + $found = false; + + apc_fetch($id, $found); + + return $found; + } + + /** + * {@inheritdoc} + */ + public function save($id, $data, $lifeTime = 0) + { + return (bool) apc_store($id, $data, (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + public function delete($id) + { + return apc_delete($id); + } +} diff --git a/src/Symfony/Component/Cache/CacheInterface.php b/src/Symfony/Component/Cache/CacheInterface.php new file mode 100644 index 0000000000000..5e2ed1199df19 --- /dev/null +++ b/src/Symfony/Component/Cache/CacheInterface.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +/** + * Generic Caching interface that any caching provider can implement/provide. + * + * Used inside Symfony is suggested to be used by any third-party + * bundle to allow central cache management. + * + * @author Benjamin Eberlei + */ +interface CacheInterface +{ + /** + * Fetches an entry from the cache. + * + * @param string $id cache id The id of the cache entry to fetch. + * @return string The cached data or FALSE, if no cache entry exists for the given id. + */ + function fetch($id); + + /** + * Test if an entry exists in the cache. + * + * @param string $id cache id The cache id of the entry to check for. + * @return boolean TRUE if a cache entry exists for the given cache id, FALSE otherwise. + */ + function contains($id); + + /** + * Puts data into the cache. + * + * @param string $id The cache id. + * @param string $data The cache entry/data. + * @param int $lifeTime The lifetime. If != 0, sets a specific lifetime for this cache entry (0 => infinite lifeTime). + * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. + */ + function save($id, $data, $lifeTime = 0); + + /** + * Deletes a cache entry. + * + * @param string $id cache id + * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. + */ + function delete($id); +} + diff --git a/src/Symfony/Component/Cache/README.md b/src/Symfony/Component/Cache/README.md new file mode 100644 index 0000000000000..292bb894ae8f4 --- /dev/null +++ b/src/Symfony/Component/Cache/README.md @@ -0,0 +1,28 @@ +# Symfony Cache + +This component provides a very simple cache interface to be used +with other Symfony components the framework or third party bundles. +Complexity in the cache interface is intentionaly left out to provide +a common denominator that clients of this interface can rely on. + +A lightweight APC implementation is also shipped to support the most +commonly available caching implementation. + +For other cache drivers or more complex caching needs you should +use any cache provider that has an implementation of the +`Symfony\Component\Cache\CacheInterface`. + +If you are using Doctrine\Common in your project you can use the +delegate cache driver `Symfony\Component\Cache\DoctrineCache`. + +## Usage + + use Symfony\Component\Cache\ApcCache; + + $cache = new ApcCache(); + $cache->save("key", "value"); + + if ($cache->contains("key")) { + echo $cache->fetch("key"); + } + $cache->delete("key"); diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json new file mode 100644 index 0000000000000..59bcb2b8b9eed --- /dev/null +++ b/src/Symfony/Component/Cache/composer.json @@ -0,0 +1,26 @@ +{ + "name": "symfony/cache", + "type": "library", + "description": "Symfony Cache Component", + "keywords": [], + "homepage": "http://symfony.com", + "version": "2.1.0", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.2" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\Cache": "" } + }, + "target-dir": "Symfony/Component/Cache" +} diff --git a/src/Symfony/Component/ClassLoader/CacheUniversalClassLoader.php b/src/Symfony/Component/ClassLoader/CacheUniversalClassLoader.php new file mode 100644 index 0000000000000..1ead931b9d179 --- /dev/null +++ b/src/Symfony/Component/ClassLoader/CacheUniversalClassLoader.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ClassLoader; + +use Symfony\Component\Cache\CacheInterface; + +/** + * CacheUniversalClassLoader implements a "universal" autoloader cached in any + * Symfony Cache implementation + * + * It is able to load classes that use either: + * + * * The technical interoperability standards for PHP 5.3 namespaces and + * class names (https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md); + * + * * The PEAR naming convention for classes (http://pear.php.net/). + * + * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be + * looked for in a list of locations to ease the vendoring of a sub-set of + * classes for large projects. + * + * Example usage: + * + * require 'vendor/symfony/src/Symfony/Component/Cache/CacheInterface.php'; + * require 'vendor/symfony/src/Symfony/Component/Cache/ApcCache.php'; + * require 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php'; + * require 'vendor/symfony/src/Symfony/Component/ClassLoader/CacheUniversalClassLoader.php'; + * + * use Symfony\Component\ClassLoader\ApcUniversalClassLoader; + * use Symfony\Component\Cache\ApcCache; + * + * $apc = new ApcCache(); + * $loader = new CacheUniversalClassLoader($cache, 'cache.prefix.'); + * + * // register classes with namespaces + * $loader->registerNamespaces(array( + * 'Symfony\Component' => __DIR__.'/component', + * 'Symfony' => __DIR__.'/framework', + * 'Sensio' => array(__DIR__.'/src', __DIR__.'/vendor'), + * )); + * + * // register a library using the PEAR naming convention + * $loader->registerPrefixes(array( + * 'Swift_' => __DIR__.'/Swift', + * )); + * + * // activate the autoloader + * $loader->register(); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * @author Fabien Potencier + * @author Kris Wallsmith + * @author Benjamin Eberlei + * + * @api + */ +class CacheUniversalClassLoader extends UniversalClassLoader +{ + private $cache; + private $prefix; + + /** + * Constructor. + * + * @param string $prefix A prefix to create a namespace in APC + * + * @api + */ + public function __construct(CacheInterface $cache, $prefix) + { + $this->cache = $cache; + $this->prefix = $prefix; + } + + /** + * Finds a file by class name while caching lookups to APC. + * + * @param string $class A class name to resolve to file + */ + public function findFile($class) + { + if (false === $file = $this->cache->fetch($this->prefix.$class)) { + $this->cache->save($this->prefix.$class, $file = parent::findFile($class)); + } + + return $file; + } +} diff --git a/src/Symfony/Component/Validator/Mapping/Cache/SymfonyCache.php b/src/Symfony/Component/Validator/Mapping/Cache/SymfonyCache.php new file mode 100644 index 0000000000000..98c3e7a537972 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Cache/SymfonyCache.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping\Cache; + +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Cache\CacheInterface as ComponentCacheInterface; + +/** + * Persists ClassMetadata instances with Symfony Cache Component + * + * @author Benjamin Eberlei + */ +class SymfonyCache implements CacheInterface +{ + private $cache; + private $prefix; + + public function __construct(ComponentCacheInterface $cache, $prefix) + { + $this->cache = $cache; + $this->prefix = $prefix; + } + + /** + * Returns whether metadata for the given class exists in the cache + * + * @param string $class + */ + public function has($class) + { + return $this->cache->contains($this->prefix.$class); + } + + /** + * Returns the metadata for the given class from the cache + * + * @param string $class Class Name + * + * @return ClassMetadata + */ + public function read($class) + { + return $this->cache->fetch($this->prefix.$class); + } + + /** + * Stores a class metadata in the cache + * + * @param ClassMetadata $metadata A Class Metadata + */ + public function write(ClassMetadata $metadata) + { + $this->cache->save($this->prefix.$metadata->getClassName(), $metadata); + } +} + diff --git a/tests/Symfony/Tests/Component/Cache/ApcCacheTest.php b/tests/Symfony/Tests/Component/Cache/ApcCacheTest.php new file mode 100644 index 0000000000000..68b21e086c3d5 --- /dev/null +++ b/tests/Symfony/Tests/Component/Cache/ApcCacheTest.php @@ -0,0 +1,75 @@ +markTestSkipped('APC needs to be installed for this tests.'); + } + $this->cache = new ApcCache(); + } + + public function testSaveReturnsTrueOnSuccess() + { + $ret = $this->cache->save('id', 1); + + $this->assertTrue($ret); + } + + public function testFetchUnknownReturnsFalse() + { + $value = $this->cache->fetch('unknown'); + $this->assertFalse($value); + } + + public function testFetchKnownReturnsValue() + { + $this->cache->save('known', 1); + $value = $this->cache->fetch('known'); + + $this->assertSame(1, $value); + } + + public function testFetchKnownFalse() + { + $this->cache->save('false', false); + $value = $this->cache->fetch('false'); + + $this->assertFalse($value); + } + + public function testContainsFalseValue() + { + $this->cache->save('contains_false', false); + + $this->assertTrue($this->cache->contains('contains_false')); + } + + public function testContainsValue() + { + $this->cache->save('contains', 1234); + + $this->assertTrue($this->cache->contains('contains')); + } + + public function testDelete() + { + $this->cache->save('delete', 1234); + + $this->assertTrue($this->cache->contains('delete')); + + $this->cache->delete('delete'); + $this->assertFalse($this->cache->contains('delete')); + } +} + diff --git a/tests/Symfony/Tests/Component/ClassLoader/CacheUniversalClassLoaderTest.php b/tests/Symfony/Tests/Component/ClassLoader/CacheUniversalClassLoaderTest.php new file mode 100644 index 0000000000000..b6bcf6d1ff3ec --- /dev/null +++ b/tests/Symfony/Tests/Component/ClassLoader/CacheUniversalClassLoaderTest.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Tests\Component\ClassLoader; + +use Symfony\Component\ClassLoader\CacheUniversalClassLoader; +use Symfony\Component\Cache\ApcCache; + +/** + * @group GH-1513 + */ +class CacheUniversalClassLoaderTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!extension_loaded('apc')) { + $this->markTestSkipped('The apc extension is not available.'); + } + + if (!(ini_get('apc.enabled') && ini_get('apc.enable_cli'))) { + $this->markTestSkipped('The apc extension is available, but not enabled.'); + } else { + apc_clear_cache('user'); + } + } + + protected function tearDown() + { + if (ini_get('apc.enabled') && ini_get('apc.enable_cli')) { + apc_clear_cache('user'); + } + } + + public function testConstructor() + { + $loader = new CacheUniversalClassLoader(new ApcCache(), 'test.prefix.'); + $loader->registerNamespace('Apc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); + + $this->assertEquals($loader->findFile('\Apc\Namespaced\FooBar'), apc_fetch('test.prefix.\Apc\Namespaced\FooBar'), '__construct() takes a prefix as its first argument'); + } + + /** + * @dataProvider getLoadClassTests + */ + public function testLoadClass($className, $testClassName, $message) + { + $loader = new CacheUniversalClassLoader(new ApcCache(), 'test.prefix.'); + $loader->registerNamespace('Apc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); + $loader->registerPrefix('Apc_Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); + $loader->loadClass($testClassName); + $this->assertTrue(class_exists($className), $message); + } + + public function getLoadClassTests() + { + return array( + array('\\Apc\\Namespaced\\Foo', '\\Apc\\Namespaced\\Foo', '->loadClass() loads Apc\Namespaced\Foo class'), + array('Apc_Pearlike_Foo', 'Apc_Pearlike_Foo', '->loadClass() loads Apc_Pearlike_Foo class'), + array('\\Apc\\Namespaced\\Bar', '\\Apc\\Namespaced\\Bar', '->loadClass() loads Apc\Namespaced\Bar class with a leading slash'), + array('Apc_Pearlike_Bar', '\\Apc_Pearlike_Bar', '->loadClass() loads Apc_Pearlike_Bar class with a leading slash'), + ); + } + + /** + * @dataProvider getLoadClassFromFallbackTests + */ + public function testLoadClassFromFallback($className, $testClassName, $message) + { + $loader = new CacheUniversalClassLoader(new ApcCache(), 'test.prefix.fallback'); + $loader->registerNamespace('Apc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); + $loader->registerPrefix('Apc_Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); + $loader->registerNamespaceFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/fallback')); + $loader->registerPrefixFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/fallback')); + $loader->loadClass($testClassName); + $this->assertTrue(class_exists($className), $message); + } + + public function getLoadClassFromFallbackTests() + { + return array( + array('\\Apc\\Namespaced\\Baz', '\\Apc\\Namespaced\\Baz', '->loadClass() loads Apc\Namespaced\Baz class'), + array('Apc_Pearlike_Baz', 'Apc_Pearlike_Baz', '->loadClass() loads Apc_Pearlike_Baz class'), + array('\\Apc\\Namespaced\\FooBar', '\\Apc\\Namespaced\\FooBar', '->loadClass() loads Apc\Namespaced\Baz class from fallback dir'), + array('Apc_Pearlike_FooBar', 'Apc_Pearlike_FooBar', '->loadClass() loads Apc_Pearlike_Baz class from fallback dir'), + ); + } + + /** + * @dataProvider getLoadClassNamespaceCollisionTests + */ + public function testLoadClassNamespaceCollision($namespaces, $className, $message) + { + $loader = new CacheUniversalClassLoader(new ApcCache(), 'test.prefix.collision'); + $loader->registerNamespaces($namespaces); + + $loader->loadClass($className); + + $this->assertTrue(class_exists($className), $message); + } + + public function getLoadClassNamespaceCollisionTests() + { + return array( + array( + array( + 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', + 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', + ), + '\Apc\NamespaceCollision\A\Foo', + '->loadClass() loads NamespaceCollision\A\Foo from alpha.', + ), + array( + array( + 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', + 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', + ), + '\Apc\NamespaceCollision\A\Bar', + '->loadClass() loads NamespaceCollision\A\Bar from alpha.', + ), + array( + array( + 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', + 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', + ), + '\Apc\NamespaceCollision\A\B\Foo', + '->loadClass() loads NamespaceCollision\A\B\Foo from beta.', + ), + array( + array( + 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', + 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', + ), + '\Apc\NamespaceCollision\A\B\Bar', + '->loadClass() loads NamespaceCollision\A\B\Bar from beta.', + ), + ); + } + + /** + * @dataProvider getLoadClassPrefixCollisionTests + */ + public function testLoadClassPrefixCollision($prefixes, $className, $message) + { + $loader = new CacheUniversalClassLoader(new ApcCache(), 'test.prefix.collision.'); + $loader->registerPrefixes($prefixes); + + $loader->loadClass($className); + $this->assertTrue(class_exists($className), $message); + } + + public function getLoadClassPrefixCollisionTests() + { + return array( + array( + array( + 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', + 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', + ), + 'ApcPrefixCollision_A_Foo', + '->loadClass() loads ApcPrefixCollision_A_Foo from alpha.', + ), + array( + array( + 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', + 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', + ), + 'ApcPrefixCollision_A_Bar', + '->loadClass() loads ApcPrefixCollision_A_Bar from alpha.', + ), + array( + array( + 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', + 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', + ), + 'ApcPrefixCollision_A_B_Foo', + '->loadClass() loads ApcPrefixCollision_A_B_Foo from beta.', + ), + array( + array( + 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', + 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', + ), + 'ApcPrefixCollision_A_B_Bar', + '->loadClass() loads ApcPrefixCollision_A_B_Bar from beta.', + ), + ); + } +}