Skip to content

Commit

Permalink
Fixing functions coverage.
Browse files Browse the repository at this point in the history
  • Loading branch information
cmatosbc committed Dec 15, 2024
1 parent 6d98409 commit d545c02
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 2 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":"07fb00eb221304d5fd7797f4d106abdb33204","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":37,"81-120":6}},"Line indent":{"values":{"spaces":18}},"PHP keyword case":{"values":{"lower":9}},"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":3}},"PHP type case":{"values":{"lower":5}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":255},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/CacheTrait.php":{"hash":"318533789daa0830ab55df82d1dcfc2433204","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":182,"81-120":6}},"Line indent":{"values":{"spaces":145}},"PHP keyword case":{"values":{"lower":49}},"Multiple statements on same line":{"values":{"no":40}},"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":1480},"\/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}}
{"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":"3dae1fc2c9aa671290655a5903f1ce2933204","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":44,"81-120":6}},"Line indent":{"values":{"spaces":18}},"PHP keyword case":{"values":{"lower":9}},"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":3}},"PHP type case":{"values":{"lower":5}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":288},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/CacheTrait.php":{"hash":"318533789daa0830ab55df82d1dcfc2433204","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":182,"81-120":6}},"Line indent":{"values":{"spaces":145}},"PHP keyword case":{"values":{"lower":49}},"Multiple statements on same line":{"values":{"no":40}},"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":1480},"\/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},"\/home\/carlos\/Projects\/test\/mnemosyne\/src\/CacheManager.php":{"hash":"cfbaee9751707b6990f3b6b9413ace7933188","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":71,"81-120":5}},"Line indent":{"values":{"spaces":63}},"PHP keyword case":{"values":{"lower":42}},"Multiple statements on same line":{"values":{"no":17}},"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}},"CamelCase method name":{"values":{"yes":4}},"PHP type case":{"values":{"lower":7}},"Function opening brace placement":{"values":{"new line":4}},"Spaces after control structure open parenthesis":{"values":[5]},"Spaces before control structure close parenthesis":{"values":[5]},"Blank lines at start of control structure":{"values":[5]},"Blank lines at end of control structure":{"values":[5]},"Control structure defined inline":{"values":{"no":5}}},"errorCount":0,"warningCount":0,"fixableCount":0,"numTokens":557}}
85 changes: 84 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,89 @@ Tags support parameter interpolation just like cache keys, allowing you to creat
4. Use cache invalidation when data is modified
5. Consider using cache tags for group invalidation

## Function Caching

In addition to method caching, Mnemosyne supports caching standalone functions using the `CacheManager`. This is useful when you need to cache results of functions outside of classes.

#### Basic Function Caching

```php
use Mnemosyne\Cache;
use Mnemosyne\CacheManager;

// Set up the cache implementation
CacheManager::setCache($cache);

// Create a cached version of your function
$expensiveCalculation = function(int $n) {
return CacheManager::wrap(
fn() => fibonacci($n), // Original function call
['n' => $n], // Parameters for key interpolation
new Cache(
key: 'fib:{n}', // Cache key template
ttl: 3600 // Time-to-live in seconds
)
);
};

// Use the cached function
$result = $expensiveCalculation(10);
```

#### Function Caching with Tags

```php
// Cache function results with tags for group invalidation
$getCityWeather = function(string $city) {
return CacheManager::wrap(
fn() => fetchWeatherData($city),
['city' => $city],
new Cache(
key: 'weather:{city}',
ttl: 1800,
tags: ['weather', "city-{city}"]
)
);
};

// Cache multiple cities
$nyWeather = $getCityWeather('new-york');
$laWeather = $getCityWeather('los-angeles');

// Later, invalidate all weather caches
$cache->delete('tag:weather');
```

#### Caching Complex Functions

```php
// Example with multiple parameters and complex key generation
$searchUsers = function(string $query, int $page, int $limit) {
return CacheManager::wrap(
fn() => performUserSearch($query, $page, $limit),
[
'q' => $query,
'page' => $page,
'limit' => $limit
],
new Cache(
key: 'search:users:{q}:p{page}:l{limit}',
ttl: 1800,
serialize: true // Enable serialization for complex results
)
);
};

// Use the cached search function
$results = $searchUsers('john', 1, 20);
```

This approach to function caching provides:
- Easy integration with existing functions
- Support for all Cache attribute features (TTL, tags, serialization)
- Flexible cache key generation
- Clear separation of caching logic from business logic

## Testing

The library includes comprehensive PHPUnit tests. Run them with:
Expand All @@ -232,4 +315,4 @@ This project is licensed under the GNU General Public License v3.0 or later - se

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
7 changes: 7 additions & 0 deletions src/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
* {
* return $this->cacheCall('doGetUser', func_get_args());
* }
*
* // Function caching example
* #[Cache(key: 'factorial:{n}')]
* function factorial(int $n): int {
* if ($n <= 1) return 1;
* return $n * factorial($n - 1);
* }
* ```
*
* @author Carlos Matos <carlos@example.com>
Expand Down
90 changes: 90 additions & 0 deletions src/CacheManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace Mnemosyne;

use Psr\SimpleCache\CacheInterface;

/**
* Manages caching for functions.
*
* This class provides caching functionality for functions using a wrapper approach.
* It uses a PSR-16 compatible cache implementation and handles cache key generation,
* storage, and retrieval.
*/
class CacheManager
{
private static ?CacheInterface $cache = null;

/**
* Set the cache implementation to use.
*/
public static function setCache(CacheInterface $cache): void
{
self::$cache = $cache;
}

/**
* Get the current cache implementation.
*/
public static function getCache(): ?CacheInterface
{
return self::$cache;
}

/**
* Wrap a function call with caching behavior.
*
* @param callable $function The function to cache
* @param array $params Named parameters for key interpolation
* @param Cache $attribute The cache attribute instance
* @return mixed The cached or computed result
*/
public static function wrap(callable $function, array $params, Cache $attribute): mixed
{
if (!self::$cache) {
throw new \RuntimeException('Cache implementation not set. Call CacheManager::setCache() first.');
}

$key = self::resolveCacheKey($attribute->key, $params);

// Check if value is already cached
if (self::$cache->has($key)) {
return self::$cache->get($key);
}

// Call the original function
$result = $function();

// Cache the result
self::$cache->set($key, $result, $attribute->ttl);

// Handle tags if present
if (!empty($attribute->tags)) {
foreach ($attribute->tags as $tag) {
$tagKey = "tag:$tag";
$keys = self::$cache->get($tagKey, []);
$keys[] = $key;
self::$cache->set($tagKey, array_unique($keys));
}
}

return $result;
}

/**
* Resolve a cache key template using parameters.
*/
private static function resolveCacheKey(?string $template, array $params): string
{
if ($template === null) {
// Generate automatic key based on parameters
return md5(serialize($params));
}

return preg_replace_callback(
'/{([^}]+)}/',
fn($m) => $params[$m[1]] ?? '',
$template
);
}
}
109 changes: 109 additions & 0 deletions tests/CacheFunctionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace Mnemosyne\Tests;

use Mnemosyne\Cache;
use Mnemosyne\CacheManager;
use Mnemosyne\Tests\Fixtures\MockCache;
use PHPUnit\Framework\TestCase;

class CacheFunctionTest extends TestCase
{
private MockCache $cache;
private static int $fibonacciCalls = 0;
private static int $greetCalls = 0;

protected function setUp(): void
{
$this->cache = new MockCache();
CacheManager::setCache($this->cache);
self::$fibonacciCalls = 0;
self::$greetCalls = 0;
}

private function fibonacci(int $n): int
{
self::$fibonacciCalls++;
return $n <= 1 ? $n : $this->fibonacci($n - 1) + $this->fibonacci($n - 2);
}

private function greet(string $name): string
{
self::$greetCalls++;
return "Hello, $name!";
}

public function testFibonacciCaching(): void
{
echo "\nTest: Fibonacci Function Caching\n";
echo "------------------------------\n";

$cachedFib = function(int $n) {
return CacheManager::wrap(
fn() => $this->fibonacci($n),
['n' => $n],
new Cache(key: 'fibonacci:{n}', ttl: 3600)
);
};

// First call should compute and cache
echo "First call (should miss cache and compute):\n";
$result1 = $cachedFib(10);
$initialCalls = self::$fibonacciCalls;
echo "Result: $result1\n";
echo "Function calls: " . self::$fibonacciCalls . "\n";
echo "Cache operations: " . count($this->cache->operations) . "\n";

// Second call should use cache
echo "\nSecond call (should hit cache):\n";
$result2 = $cachedFib(10);
echo "Result: $result2\n";
echo "Additional function calls: " . (self::$fibonacciCalls - $initialCalls) . "\n";
echo "Total cache operations: " . count($this->cache->operations) . "\n";

$this->assertEquals($result1, $result2);
$this->assertEquals(55, $result1); // Known fibonacci(10) result
$this->assertEquals($initialCalls, self::$fibonacciCalls, 'Function should not be called again when cached');
}

public function testGreetingWithTags(): void
{
echo "\nTest: Greeting Function with Tags\n";
echo "-------------------------------\n";

$cachedGreet = function(string $name) {
return CacheManager::wrap(
fn() => $this->greet($name),
['name' => $name],
new Cache(key: 'greet:{name}', tags: ['greetings'])
);
};

// First call
echo "First call with name 'John':\n";
$result1 = $cachedGreet('John');
$initialCalls = self::$greetCalls;
echo "Result: $result1\n";
echo "Function calls: " . self::$greetCalls . "\n";
echo "Cache operations: " . count($this->cache->operations) . "\n";

// Second call
echo "\nSecond call with same name:\n";
$result2 = $cachedGreet('John');
echo "Result: $result2\n";
echo "Additional function calls: " . (self::$greetCalls - $initialCalls) . "\n";
echo "Total cache operations: " . count($this->cache->operations) . "\n";

$this->assertEquals('Hello, John!', $result1);
$this->assertEquals($initialCalls, self::$greetCalls, 'Function should not be called again when cached');

// Verify tag was stored
$this->assertTrue(
$this->cache->has('tag:greetings'),
'Tag should be stored in cache'
);

echo "\nTag verification:\n";
echo "Tag 'greetings' exists in cache: " . ($this->cache->has('tag:greetings') ? 'yes' : 'no') . "\n";
}
}

0 comments on commit d545c02

Please sign in to comment.