8000 [2.2][WIP] Cache Component by beberlei · Pull Request #3211 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[2.2][WIP] Cache Component #3211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/Symfony/Component/Cache/ApcCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <kontakt@beberlei.de>
*/
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);
}
}
58 changes: 58 additions & 0 deletions src/Symfony/Component/Cache/CacheInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <kontakt@beberlei.de>
*/
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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with the idea of returning false if not found: what if data is boolean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if data is NULL? same problem, fetch($id, $found) is not supported by all cache drivers though.

Thats what ->contains is for. The problem here is that we have to find the most common denominator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exceptions make sense here imo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @alexandresalome disagreeing here, the first requirement should be the ability to store any type of value (this is one of the PR I have submitted against Doctrine common.)

So the interface could look like:

<?php
interface CacheInterface
{
    function fetch($id, &$success);
    function contains($id);
    function save($id, $data, $lifeTime = 0);
    function delete($id);
}

I think there is one more problem with providing an interface with both fetch and contains as it encourages flawed code as

<?php
if  (!$cache->contains('id')) {
   $cache->save('id', compute());
}
doSomething($cache->fetch('id'));

So what about dropping contains ?

<?php
interface CacheInterface
{
    function fetch($id, &$value, $fetchValue = true);
    function save($id, $data, $lifeTime = 0);
    function delete($id);
}
  • fetch would returns true when the value is in cache,
  • the third parameter could be set to false when you really need the contains behavior

The above example would become:

<?php
if  (!$cache->fetch('id', $value)) {
   $cache->save('id', $value = compute());
}
doSomething($value);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither Memcache, Xcache nor Zends Cache support an API for checking if a key really exists or not. (Memcached does support it). An API that works by reference for the values is really unintuitive, an API interface should take expectations of users into account.

Throwing an exception is misusing exceptions as a control-flow and will lead to ugly code in practice. I guess fetch($id, &$found) does make sense and could get rid of contains(), but personally saving false/null/emptystring into a cache entry screams for trouble anyways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rande see the discussion page as things like that have already been suggested. And IMO, users should not have to bother with an extra object wrapping the data to be able to store them.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vicb

I commented in the pull request and it's somewhat relevant to this. The basic idea in the request was that I agreed with @stof that we should consider the CacheInterface and the DriverInterface as separate pieces. The reason I bring this up is that my answer depends on the approach.

  • For the CacheInterface I think returning a null is critical. The users should be able to put any object they want in (null, false, objects, etc) and pull them out again directly- they shouldn't have to do type checking to confirm that their object is what they expect it to be. Additionally there is a huge difference between a "miss" and the object not existing- if the object is not there it's a miss, but there are cases where there will be an object and it will still be a miss.
  • For the DriverInterface I feel a CacheElement (or something) should be returned for every request, because the higher level caching library is going to need to know various pieces of metadata such as the TTL. The users (doctrine, etc) will never have to interact with the DriverInterface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tedivm

  • I agree, my question was about the "DriverInterface". One question though, could you eplain: "there are cases where there will be an object and it will still be a miss" ?
  • You assume that the TTL is global for all the items here, right ? If this is the case we might be able to retrieve it from a different place. I am not saying that always returning an object is a bad, just trying to understand.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, my question was about the "DriverInterface". One question though, could you eplain: "there are cases where there will be an object and it will still be a miss" ?

On object can be present and stale, at which point it would be considered a miss even though it still exists. If you let the higher level caching layer that the drives feed into make the decisions about what to do with stale data (reuse it, discard it, etc) then you have a very powerful tool. Stash has some examples of this.

You assume that the TTL is global for all the items here, right ? If this is the case we might be able to retrieve it from a different place. I am not saying that always returning an object is a bad, just trying to understand.

To be clear, the TTL I'm referring to is the remaining TTL for that specific object. If the start TTL was 180 seconds, but only 20 seconds remain to expire, then it would be very useful to know that. Additionally it may be useful to know that it expired 120 minutes ago- using a datetime (or timestamp) internally for that expire date is important.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your first reply also answered my second question. That's a very interesting discussion, many thanks for you inputs ! I really need to find some time to take a deeper look at Stash


/**
* 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);
}

28 changes: 28 additions & 0 deletions src/Symfony/Component/Cache/README.md
Original file line number Diff line number Diff line change
@@ -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");
26 changes: 26 additions & 0 deletions src/Symfony/Component/Cache/composer.json
Original file line number Diff line number Diff line change
@@ -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"
}
102 changes: 102 additions & 0 deletions src/Symfony/Component/ClassLoader/CacheUniversalClassLoader.php
10000
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implies requiring the cache implementation and the interface first

* $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 <fabien@symfony.com>
* @author Kris Wallsmith <kris@symfony.com>
* @author Benjamin Eberlei <kontakt@beberlei.de>
*
* @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 10000 ($this->prefix.$class)) {
$this->cache->save($this->prefix.$class, $file = parent::findFile($class));
}

return $file;
}
}
65 changes: 65 additions & 0 deletions src/Symfony/Component/Validator/Mapping/Cache/SymfonyCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <kontakt@beberlei.de>
*/
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
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use @inheritdoc

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);
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is an extra line at the end

Loading
BBA
0