Skip to content

Commit

Permalink
Cache tags implemented; Updated README.
Browse files Browse the repository at this point in the history
  • Loading branch information
cmatosbc committed Dec 12, 2024
1 parent ab9598c commit f0b571a
Show file tree
Hide file tree
Showing 6 changed files with 407 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .phpcs-cache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"config":{"phpVersion":80226,"phpExtensions":"74167a1a9fe1ad09c87a54206ee3fec6","tabWidth":4,"encoding":"utf-8","recordErrors":true,"annotations":true,"configData":[],"codeHash":"b5dc58ae9d1a395f86384fd9a1678bb4","rulesetHash":"670f8348153700c3a70fc6127ca0e484"},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/Cache.php":{"hash":"97880d1955eb8b46f6ca52080c211dfc33188","errors":[],"warnings":[],"metrics":{"Declarations and side effects mixed":{"values":{"no":1}},"PHP short open tag used":{"values":{"no":1}},"EOL char":{"values":{"\\n":1}},"Number of newlines at EOF":{"values":{"1":1}},"PHP closing tag at end of PHP-only file":{"values":{"no":1}},"Line length":{"values":{"80 or less":12}},"Line indent":{"values":{"spaces":6}},"PHP keyword case":{"values":{"lower":7}},"Multiple statements on same line":{"values":{"no":1}},"One class per file":{"values":{"yes":1}},"Class defined in namespace":{"values":{"yes":1}},"PascalCase class name":{"values":{"yes":1}},"Class opening brace placement":{"values":{"new line":1}},"PHP constant case":{"values":{"lower":2}},"PHP type case":{"values":{"lower":3}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":89},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/CacheTrait.php":{"hash":"fb0de4cdd837b994da4cf8dffdd5083a33188","errors":[],"warnings":[],"metrics":{"Declarations and side effects mixed":{"values":{"no":1}},"PHP short open tag used":{"values":{"no":1}},"EOL char":{"values":{"\\n":1}},"Number of newlines at EOF":{"values":{"1":1}},"PHP closing tag at end of PHP-only file":{"values":{"no":1}},"Line length":{"values":{"80 or less":75,"81-120":5}},"Line indent":{"values":{"spaces":74}},"PHP keyword case":{"values":{"lower":40}},"Multiple statements on same line":{"values":{"no":30}},"One class per file":{"values":{"yes":1}},"Class defined in namespace":{"values":{"yes":1}},"PascalCase class name":{"values":{"yes":1}},"Class opening brace placement":{"values":{"new line":1}},"PHP type case":{"values":{"lower":11}},"CamelCase method name":{"values":{"yes":4}},"Function opening brace placement":{"values":{"new line":4}},"Spaces after control structure open parenthesis":{"values":[11]},"Spaces before control structure close parenthesis":{"values":[11]},"Blank lines at start of control structure":{"values":[11]},"Blank lines at end of control structure":{"values":[11]},"Control structure defined inline":{"values":{"no":11}},"PHP constant case":{"values":{"lower":5}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":807},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/CacheAttribute.php":{"hash":"48936c1e0dbf098f4ecf0c9458113f2d33188","errors":[],"warnings":[],"metrics":{"Declarations and side effects mixed":{"values":{"no":1}},"PHP short open tag used":{"values":{"no":1}},"EOL char":{"values":{"\\n":1}},"Number of newlines at EOF":{"values":{"1":1}},"PHP closing tag at end of PHP-only file":{"values":{"no":1}},"Line length":{"values":{"80 or less":83,"81-120":5}},"Line indent":{"values":{"spaces":82}},"PHP keyword case":{"values":{"lower":44}},"Multiple statements on same line":{"values":{"no":27}},"One class per file":{"values":{"yes":1}},"Class defined in namespace":{"values":{"yes":1}},"PascalCase class name":{"values":{"yes":1}},"Class opening brace placement":{"values":{"new line":1}},"PHP type case":{"values":{"lower":11}},"Function opening brace placement":{"values":{"new line":4}},"Blank lines at start of control structure":{"values":[13]},"Blank lines at end of control structure":{"values":[13]},"Spaces after control structure open parenthesis":{"values":[12]},"Spaces before control structure close parenthesis":{"values":[12]},"PHP constant case":{"values":{"lower":6}},"Control structure defined inline":{"values":{"no":11}},"CamelCase method name":{"values":{"yes":3}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":822}}
{"config":{"phpVersion":80226,"phpExtensions":"74167a1a9fe1ad09c87a54206ee3fec6","tabWidth":4,"encoding":"utf-8","recordErrors":true,"annotations":true,"configData":[],"codeHash":"b5dc58ae9d1a395f86384fd9a1678bb4","rulesetHash":"670f8348153700c3a70fc6127ca0e484"},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/Cache.php":{"hash":"2d84bc074c6e440368e414857cfd970433204","errors":[],"warnings":[],"metrics":{"Declarations and side effects mixed":{"values":{"no":1}},"PHP short open tag used":{"values":{"no":1}},"EOL char":{"values":{"\\n":1}},"Number of newlines at EOF":{"values":{"1":1}},"PHP closing tag at end of PHP-only file":{"values":{"no":1}},"Line length":{"values":{"80 or less":36,"81-120":5}},"Line indent":{"values":{"spaces":16}},"PHP keyword case":{"values":{"lower":8}},"Multiple statements on same line":{"values":{"no":1}},"One class per file":{"values":{"yes":1}},"Class defined in namespace":{"values":{"yes":1}},"PascalCase class name":{"values":{"yes":1}},"Class opening brace placement":{"values":{"new line":1}},"PHP constant case":{"values":{"lower":2}},"PHP type case":{"values":{"lower":4}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":235},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/CacheTrait.php":{"hash":"305ecb309361d700fdc43d4ae9e50bbb33204","errors":[],"warnings":[],"metrics":{"Declarations and side effects mixed":{"values":{"no":1}},"PHP short open tag used":{"values":{"no":1}},"EOL char":{"values":{"\\n":1}},"Number of newlines at EOF":{"values":{"1":1}},"PHP closing tag at end of PHP-only file":{"values":{"no":1}},"Line length":{"values":{"80 or less":181,"81-120":6}},"Line indent":{"values":{"spaces":144}},"PHP keyword case":{"values":{"lower":49}},"Multiple statements on same line":{"values":{"no":39}},"One class per file":{"values":{"yes":1}},"Class defined in namespace":{"values":{"yes":1}},"PascalCase class name":{"values":{"yes":1}},"Class opening brace placement":{"values":{"new line":1}},"PHP type case":{"values":{"lower":13}},"CamelCase method name":{"values":{"yes":5}},"Function opening brace placement":{"values":{"new line":5}},"Spaces after control structure open parenthesis":{"values":[15]},"Spaces before control structure close parenthesis":{"values":[15]},"Blank lines at start of control structure":{"values":[15]},"Blank lines at end of control structure":{"values":[15]},"Control structure defined inline":{"values":{"no":15}},"PHP constant case":{"values":{"lower":5}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":1446},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/CacheAttribute.php":{"hash":"14a86252da512a54b67bd6dc8da1de0c33204","errors":[],"warnings":[],"metrics":{"Declarations and side effects mixed":{"values":{"no":1}},"PHP short open tag used":{"values":{"no":1}},"EOL char":{"values":{"\\n":1}},"Number of newlines at EOF":{"values":{"1":1}},"PHP closing tag at end of PHP-only file":{"values":{"no":1}},"Line length":{"values":{"80 or less":146,"81-120":5}},"Line indent":{"values":{"spaces":136}},"PHP keyword case":{"values":{"lower":59}},"Multiple statements on same line":{"values":{"no":37}},"One class per file":{"values":{"yes":1}},"Class defined in namespace":{"values":{"yes":1}},"PascalCase class name":{"values":{"yes":1}},"Class opening brace placement":{"values":{"new line":1}},"PHP type case":{"values":{"lower":16}},"Constant name case":{"values":{"upper":1}},"CamelCase method name":{"values":{"yes":5}},"Function opening brace placement":{"values":{"new line":6}},"Spaces after control structure open parenthesis":{"values":[16]},"Spaces before control structure close parenthesis":{"values":[16]},"Blank lines at start of control structure":{"values":[17]},"Blank lines at end of control structure":{"values":[17]},"Control structure defined inline":{"values":{"no":15}},"PHP constant case":{"values":{"lower":6}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":1271}}
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Mnemosyne is a powerful and flexible caching library for PHP 8.0+ that uses attr
- Automatic cache key generation
- Parameter-based cache keys with interpolation
- Automatic and manual cache invalidation
- Cache tags for group invalidation
- PSR-16 (SimpleCache) compatibility
- Flexible cache key templates

Expand Down Expand Up @@ -127,6 +128,51 @@ class UserService
}
```

### Cache Tags

Cache tags allow you to group related cache entries and invalidate them together. This is useful for managing cache dependencies and bulk invalidation.

```php
class UserService
{
use CacheTrait;

#[Cache(
key: 'user:{id}',
ttl: 3600,
tags: ['user', 'user-{id}']
)]
public function getUser(int $id): array
{
return $this->cacheCall('doGetUser', func_get_args());
}

#[Cache(
key: 'user:profile:{id}',
ttl: 3600,
tags: ['user', 'user-{id}']
)]
public function getUserProfile(int $id): array
{
return $this->cacheCall('doGetUserProfile', func_get_args());
}

public function updateUser(int $id): void
{
// Invalidate all caches for a specific user
$this->invalidateTag("user-$id");
}

public function clearAllUserCaches(): void
{
// Invalidate all user-related caches
$this->invalidateTag('user');
}
}
```

Tags support parameter interpolation just like cache keys, allowing you to create dynamic tag names. When a tag is invalidated, all cache entries associated with that tag are automatically removed.

## Best Practices

1. Split cached methods into two parts:
Expand Down
29 changes: 29 additions & 0 deletions src/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,42 @@

namespace Mnemosyne;

/**
* Cache attribute for configuring method-level caching behavior.
*
* This attribute can be applied to methods to define their caching strategy,
* including cache key template, TTL, invalidation rules, and tags.
*
* @see \Mnemosyne\CacheTrait For the implementation of caching behavior
*
* @example
* ```php
* #[Cache(key: 'user:{id}', ttl: 3600, tags: ['user', 'user-{id}'])]
* public function getUser(int $id): array
* {
* return $this->cacheCall('doGetUser', func_get_args());
* }
* ```
*
* @author Carlos Matos <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
class Cache
{
/**
* Configure caching behavior for a method.
*
* @param string|null $key Cache key template. Supports parameter interpolation using {param} syntax.
* If null, a key will be auto-generated based on class, method and arguments.
* @param int|null $ttl Time-to-live in seconds. If null, cache will not expire.
* @param array $invalidates List of cache key templates to invalidate when this method is called.
* @param array $tags List of tags to associate with this cache entry. Supports parameter interpolation.
*/
public function __construct(
public ?string $key = null,
public ?int $ttl = null,
public array $invalidates = [],
public array $tags = [],
) {
}
}
73 changes: 71 additions & 2 deletions src/CacheAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,66 @@

use Psr\SimpleCache\CacheInterface;

/**
* Legacy class for backwards compatibility.
*
* @deprecated Use Cache attribute and CacheTrait instead
* @see \Mnemosyne\Cache For the new attribute-based caching configuration
* @see \Mnemosyne\CacheTrait For the new trait-based implementation
*
* @internal This class will be removed in version 2.0
*/
class CacheAttribute
{
/** @var array<string, string> Cache of compiled key templates */
private array $keyTemplates = [];

/** @var string Prefix for tag keys in cache */
private const TAG_PREFIX = 'tag:';

/**
* @param CacheInterface $cache PSR-16 cache implementation
*/
public function __construct(
private CacheInterface $cache
) {
}

/**
* Store tag references for a cache key
*
* @param string $key The cache key to tag
* @param array $tags List of tags to associate with the key
*/
private function storeTags(string $key, array $tags): void
{
foreach ($tags as $tag) {
$tagKey = self::TAG_PREFIX . $tag;
$taggedKeys = $this->cache->get($tagKey, []);
if (!in_array($key, $taggedKeys)) {
$taggedKeys[] = $key;
$this->cache->set($tagKey, $taggedKeys);
}
}
}

/**
* Invalidate all cache entries with the given tag
*
* @param string $tag The tag to invalidate
*/
public function invalidateTag(string $tag): void
{
$tagKey = self::TAG_PREFIX . $tag;
$taggedKeys = $this->cache->get($tagKey, []);

foreach ($taggedKeys as $key) {
$this->cache->delete($key);
}

$this->cache->delete($tagKey);
}

public function __call(string $name, array $arguments)
{
try {
Expand Down Expand Up @@ -50,17 +101,31 @@ public function __call(string $name, array $arguments)
}
}

// Execute the original method
// Execute the method
$result = $reflection->invokeArgs($this, $arguments);

// Cache the result if we have a key
// Store the result in cache if we have a key
if ($key !== null) {
$this->cache->set($key, $result, $config->ttl);

// Store tag references if any tags are defined
if (!empty($config->tags)) {
$this->storeTags($key, $config->tags);
}
}

return $result;
}

/**
* Resolve a cache key from a template or method signature
*
* @param string|null $template The cache key template
* @param \ReflectionMethod $method The method being called
* @param array $args The method arguments
*
* @return string|null The resolved cache key
*/
private function resolveCacheKey(?string $template, \ReflectionMethod $method, array $args): ?string
{
if ($template === null) {
Expand Down Expand Up @@ -91,6 +156,8 @@ private function resolveCacheKey(?string $template, \ReflectionMethod $method, a

/**
* Manually invalidate a cache key
*
* @param string $key The cache key to invalidate
*/
protected function invalidateCache(string $key): void
{
Expand All @@ -99,6 +166,8 @@ protected function invalidateCache(string $key): void

/**
* Manually invalidate multiple cache keys
*
* @param array $keys The cache keys to invalidate
*/
protected function invalidateCacheKeys(array $keys): void
{
Expand Down
114 changes: 113 additions & 1 deletion src/CacheTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,67 @@

use Psr\SimpleCache\CacheInterface;

/**
* Trait providing caching functionality for classes.
*
* This trait implements the caching behavior defined by the Cache attribute.
* It handles cache key generation, storage, retrieval, and invalidation,
* including support for cache tags.
*
* @see \Mnemosyne\Cache For the attribute that configures caching behavior
* @see \Psr\SimpleCache\CacheInterface For the underlying cache implementation
*
* @example
* ```php
* class UserService
* {
* use CacheTrait;
*
* public function __construct(CacheInterface $cache)
* {
* $this->cache = $cache;
* }
*
* #[Cache(key: 'user:{id}', ttl: 3600)]
* public function getUser(int $id): array
* {
* return $this->cacheCall('doGetUser', func_get_args());
* }
*
* private function doGetUser(int $id): array
* {
* // Expensive operation here
* return ['id' => $id, 'name' => 'John'];
* }
* }
* ```
*
* @author Carlos Matos <[email protected]>
*/
trait CacheTrait
{
private array $keyTemplates = [];
/** @var CacheInterface PSR-16 cache implementation */
private CacheInterface $cache;

/** @var array<string, string> Cache of compiled key templates */
private array $keyTemplates = [];

/**
* Handle caching for a method call.
*
* This method is responsible for:
* - Retrieving cache configuration from the calling method's attributes
* - Generating cache keys based on templates and parameters
* - Handling cache invalidation
* - Storing and retrieving cached values
* - Managing cache tags
*
* @param string $method Name of the private method to call if cache misses
* @param array $args Arguments to pass to the method
* @return mixed The cached or freshly computed result
* @throws \ReflectionException If the method doesn't exist
* @throws \BadMethodCallException If the method is not accessible
*/
private function cacheCall(string $method, array $args)
{
// Get the public method that called us
Expand Down Expand Up @@ -53,11 +109,35 @@ private function cacheCall(string $method, array $args)
// Cache the result if we have a key
if ($key !== null) {
$this->cache->set($key, $result, $config->ttl);

// Store tags if any are defined
if (!empty($config->tags)) {
foreach ($config->tags as $tag) {
$resolvedTag = $this->resolveCacheKey($tag, $reflection, $args);
$tagKey = 'tag:' . $resolvedTag;
$taggedKeys = $this->cache->get($tagKey) ?? [];
if (!in_array($key, $taggedKeys)) {
$taggedKeys[] = $key;
$this->cache->set($tagKey, $taggedKeys);
}
}
}
}

return $result;
}

/**
* Resolve a cache key template using method parameters.
*
* Handles both automatic key generation (when template is null) and
* parameter interpolation in key templates.
*
* @param string|null $template The key template with {param} placeholders
* @param \ReflectionMethod $method The method being cached
* @param array $args The arguments passed to the method
* @return string|null The resolved cache key
*/
private function resolveCacheKey(?string $template, \ReflectionMethod $method, array $args): ?string
{
if ($template === null) {
Expand Down Expand Up @@ -86,15 +166,47 @@ private function resolveCacheKey(?string $template, \ReflectionMethod $method, a
return $key;
}

/**
* Invalidate a single cache key.
*
* @param string $key The cache key to invalidate
*/
protected function invalidateCache(string $key): void
{
$this->cache->delete($key);
}

/**
* Invalidate multiple cache keys.
*
* @param array $keys List of cache keys to invalidate
*/
protected function invalidateCacheKeys(array $keys): void
{
foreach ($keys as $key) {
$this->cache->delete($key);
}
}

/**
* Invalidate all cache entries associated with a tag.
*
* This will:
* 1. Retrieve all cache keys associated with the tag
* 2. Delete each cache entry
* 3. Remove the tag registry itself
*
* @param string $tag The tag to invalidate
*/
public function invalidateTag(string $tag): void
{
$tagKey = 'tag:' . $tag;
$taggedKeys = $this->cache->get($tagKey) ?? [];

foreach ($taggedKeys as $key) {
$this->cache->delete($key);
}

$this->cache->delete($tagKey);
}
}
Loading

0 comments on commit f0b571a

Please sign in to comment.