PHP Logosymfony/cache

The `symfony/cache` component provides a powerful and flexible way to add caching capabilities to any PHP application, not just Symfony projects. It acts as an abstraction layer over various caching backends, allowing developers to switch between different caching technologies (like Redis, Memcached, APCu, files, or even an in-memory array) with minimal code changes. This is crucial for improving application performance, reducing load on databases or external services, and enhancing user experience.

Key Features and Concepts:

1. PSR-6 and PSR-16 Compliance: The component adheres to the PHP-FIG's PSR-6 (Caching Interface) and PSR-16 (Common Interface for Caching Libraries) standards. This ensures interoperability with other libraries and frameworks that also implement these standards, making your caching logic portable.
2. Adapters: `symfony/cache` provides numerous adapters to integrate with different caching solutions. Common adapters include:
* `FilesystemAdapter`: Stores cache items in files on the disk.
* `RedisAdapter`: Connects to a Redis server.
* `MemcachedAdapter`: Connects to a Memcached server.
* `ApcuAdapter`: Leverages PHP's APCu (Alternative PHP Cache) extension for in-memory caching.
* `DoctrineDbalAdapter`: Stores cache items in a database using Doctrine DBAL.
* `PdoAdapter`: Stores cache items in a database via PDO.
* `ArrayAdapter`: An in-memory cache, useful for testing or simple request-level caching.
3. Cache Pools: Cache items are organized into 'pools'. A pool is an instance of an adapter, and you can have multiple pools for different types of data or varying lifetimes. Each pool manages its own collection of cache items.
4. Cache Items: The actual data stored in the cache. Each item has a unique key, a value, and can have an expiration time (Time-To-Live or TTL). Items implement `Symfony\Contracts\Cache\ItemInterface`.
5. `get()` with a Callable: One of the most common and powerful patterns is using the `get()` method with a callable. If the item exists in the cache and is not expired, it's returned immediately. If not, the callable is executed to compute the value, which is then stored in the cache and returned. This pattern elegantly handles cache misses and ensures data is only computed when necessary.
6. Cache Tagging: For advanced invalidation, `symfony/cache` allows you to tag cache items. This means you can associate one or more tags with an item and then invalidate all items associated with a specific tag, regardless of their individual keys. This is useful for clearing entire categories of cached data (e.g., all products when a product category changes).
7. Cache Stampede Protection: The component includes mechanisms to prevent cache stampedes (also known as the 'thundering herd problem'), where multiple processes simultaneously try to regenerate the same expired cache item, leading to increased load. It achieves this by locking the cache item during regeneration, ensuring only one process computes the value.
8. Lazy Caching: Supports fetching multiple items efficiently, potentially in a single backend call, by 'lazy' loading them only when they are actually accessed.

By abstracting the caching logic, `symfony/cache` simplifies cache management, makes applications more robust, and allows developers to leverage the most appropriate caching backend for their specific needs without rewriting core application logic.

Example Code

```php
<?php

require 'vendor/autoload.php';

use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Contracts\Cache\ItemInterface;

// --- Installation (run these commands in your project root) ---
// composer require symfony/cache

// If using FilesystemAdapter, ensure a cache directory is writable.
// For Redis/Memcached, require the specific adapter (e.g., composer require symfony/redis-adapter).

// 1. Initialize the Cache Pool
// Using FilesystemAdapter for a simple example that doesn't require external services.
// For production, you'd typically use RedisAdapter, MemcachedAdapter, etc.
$cache = new FilesystemAdapter(
    // $namespace: A string that will be prepended to all cache keys.
    //            Useful for isolating caches for different parts of an application.
    $namespace = 'my_app_cache',
    // $defaultLifetime: The default maximum lifetime (in seconds) for items stored in this pool.
    //                 0 means infinite lifetime unless explicitly set per item.
    $defaultLifetime = 3600, // 1 hour
    // $directory: The main cache directory (where cache files will be stored).
    //            Defaults to the system temp directory if not specified.
    $directory = __DIR__ . '/cache'
);

echo "--- Caching Data Example ---\n";

$dataKey = 'product_list';

// 2. Try to get data from cache or compute it if not found
// The `get()` method is central. It accepts a key and a callable.
// - If the item is in cache and valid, it's returned immediately.
// - If not, the callable is executed to compute the value.
//   The callable receives a `CacheItem` object, allowing you to set specific expiration, tags, etc.
$data = $cache->get($dataKey, function (ItemInterface $item) {
    // This code block runs ONLY if the cache misses or the item is expired.

    // You can set a specific expiration for this item, overriding the pool's defaultLifetime.
    $item->expiresAfter(300); // Cache this specific item for 5 minutes

    // Optionally, add tags for easy invalidation of groups of items.
    // $item->tag(['products', 'featured_items']);

    // Simulate a time-consuming operation (e.g., database query, API call, complex calculation)
    echo "Cache MISS for '{$item->getKey()}'. Simulating data retrieval... (2 seconds delay)\n";
    sleep(2); // Simulate delay

    $retrievedData = [
        ['id' => 1, 'name' => 'Laptop', 'price' => 1200],
        ['id' => 2, 'name' => 'Mouse', 'price' => 25],
        ['id' => 3, 'name' => 'Keyboard', 'price' => 75]
    ];

    // The value returned by the callable is stored in the cache.
    return $retrievedData;
});

echo "Retrieved Data (first fetch):\n";
print_r($data);
echo "\n";

// 3. Get the data again - it should now be served from cache (no 'Cache MISS' message)
echo "Fetching data again (should be from cache)...
";
$dataAgain = $cache->get($dataKey, function (ItemInterface $item) {
    // This message should NOT be printed during the second fetch if caching works.
    echo "This should NOT be printed if cache hit occurred during second fetch.\n";
    return ['error' => 'should not happen if cache works'];
});

echo "Retrieved Data (second fetch - from cache):\n";
print_r($dataAgain);
echo "\n";

// 4. Invalidate (delete) a specific cache item
echo "Invalidating cache for '{$dataKey}'...\n";
$cache->delete($dataKey);

// 5. Try to get data after invalidation - it should be a cache miss again
echo "Fetching data after invalidation (should be a cache miss again)...
";
$dataAfterInvalidation = $cache->get($dataKey, function (ItemInterface $item) {
    echo "Cache MISS for '{$item->getKey()}' after invalidation. Re-simulating data retrieval... (1 second delay)\n";
    sleep(1);
    return [
        ['id' => 4, 'name' => 'Monitor', 'price' => 300],
        ['id' => 5, 'name' => 'Webcam', 'price' => 80]
    ];
});

echo "Retrieved Data (after invalidation):\n";
print_r($dataAfterInvalidation);
echo "\n";

// You can also clear the entire cache pool:
// echo "Clearing entire cache pool...\n";
// $cache->clear();

echo "--- Example Finished ---\n";

// Optional: Clean up the created cache directory for demonstration purposes
// To do this, you might need to iterate and delete files then directories.
// For simplicity in a real app, let the cache manager handle it or just clear the pool.
// Example of manual cleanup (use with caution):
// if (is_dir(__DIR__ . '/cache/my_app_cache')) {
//     array_map('unlink', glob(__DIR__ . '/cache/my_app_cache/*'));
//     rmdir(__DIR__ . '/cache/my_app_cache');
// }
// if (is_dir(__DIR__ . '/cache')) {
//     rmdir(__DIR__ . '/cache');
// }

?>
```