baukasten/cache

There is no license information available for the latest version (0.0.4) of this package.

Flexible multi-bucket caching system with support for Redis, file, and in-memory storage, featuring attribute-based method caching and tag-based invalidation

Installs: 33

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Forks: 0

pkg:composer/baukasten/cache

0.0.4 2025-10-09 14:12 UTC

This package is auto-updated.

Last update: 2025-10-09 14:16:01 UTC


README

A flexible, framework-agnostic PHP caching library with support for multiple storage backends (Redis, File, Memory), attribute-based method caching, and tag-based invalidation.

Features

  • Multiple Storage Backends: Redis, File-based, In-Memory
  • Bucket System: Organize cache into separate buckets with different configurations
  • Attribute-Based Caching: Cache method results using PHP 8.1+ attributes
  • Tag-Based Invalidation: Group and invalidate cache entries by tags
  • TTL Support: Set time-to-live for cache entries
  • Custom Key Generators: Define custom strategies for cache key generation
  • Framework Agnostic: Works with any PHP project

Requirements

  • PHP 8.1 or higher
  • Predis for Redis support

Installation

composer require baukasten/cache

Quick Start

Basic Configuration

Configure cache buckets by passing instantiated bucket objects:

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\MemoryBucket;
use Baukasten\Cache\Buckets\RedisBucket;
use Baukasten\Cache\Buckets\FileBucket;

Cache::config([
    'default' => [
        'bucket' => new MemoryBucket(),
        'default' => true  // This bucket will be used when no bucket is specified
    ],
    'redis' => [
        'bucket' => new RedisBucket(
            host: '127.0.0.1',
            port: 6379,
            prefix: 'myapp:',
            defaultTtl: 3600
        ),
    ],
    'file' => [
        'bucket' => new FileBucket(
            cachePath: '/tmp/cache',
            defaultTtl: 3600
        ),
    ]
]);

// Without specifying a bucket, the default bucket is used
Cache::set('key', 'value');  // Stored in 'default' (MemoryBucket)

// Specify a bucket explicitly
Cache::set('key', 'value', null, 'redis');  // Stored in 'redis' bucket

Simple Single Bucket Setup

For simple use cases, just configure one bucket as default:

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\FileBucket;

Cache::config([
    'cache' => [
        'bucket' => new FileBucket('/var/www/cache'),
        'default' => true
    ]
]);

// All operations now use the file bucket
Cache::set('user:123', $userData);
$user = Cache::get('user:123');

Usage Examples

1. Basic Cache Operations

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\MemoryBucket;

// Setup
Cache::config([
    'default' => [
        'bucket' => new MemoryBucket(),
        'default' => true
    ]
]);

// Store a value (TTL in seconds)
Cache::set('user:123', ['name' => 'John', 'email' => 'john@example.com'], 3600);

// Retrieve a value
$user = Cache::get('user:123');

// Check if a key exists
if (Cache::exists('user:123')) {
    echo "User is cached";
}

// Delete a specific key
Cache::delete('user:123');

// Clear all cache entries
Cache::clear();

2. Using Multiple Buckets

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\MemoryBucket;
use Baukasten\Cache\Buckets\FileBucket;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'memory' => [
        'bucket' => new MemoryBucket(),
        'default' => true  // Default for operations without bucket specified
    ],
    'persistent' => [
        'bucket' => new FileBucket('/var/cache/myapp')
    ],
    'shared' => [
        'bucket' => new RedisBucket(
            host: 'redis.example.com',
            port: 6379,
            prefix: 'myapp:'
        )
    ]
]);

// Uses default (memory) bucket
Cache::set('temp:data', $data);

// Explicitly use file bucket for persistent cache
Cache::set('config:settings', $settings, 86400, 'persistent');

// Use Redis for shared cache across servers
Cache::set('session:' . $sessionId, $sessionData, 1800, 'shared');

// Retrieve from specific buckets
$settings = Cache::get('config:settings', null, 'persistent');
$sessionData = Cache::get('session:' . $sessionId, null, 'shared');

// Clear specific bucket
Cache::clear('persistent');

3. Default Bucket Behavior

When you don't specify a bucket, operations use the default bucket:

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\FileBucket;

Cache::config([
    'main' => [
        'bucket' => new FileBucket('/tmp/cache'),
        'default' => true
    ]
]);

// These all use the 'main' bucket automatically
Cache::set('key1', 'value1');
Cache::set('key2', 'value2', 600);
$value = Cache::get('key1');
Cache::delete('key2');
Cache::clear();

4. Remember Pattern (Cache or Compute)

The remember pattern retrieves from cache or computes and stores the value:

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'cache' => [
        'bucket' => new RedisBucket(),
        'default' => true
    ]
]);

// Get from cache, or execute callback and cache the result
$user = Cache::remember('user:123', function() {
    // This expensive operation only runs if not cached
    return fetchUserFromDatabase(123);
}, 3600);

// With specific bucket
$products = Cache::remember('products:active', fn() => Product::active()->get(), 600, 'cache');

5. Tag-Based Caching and Invalidation

Tags allow you to group cache entries and invalidate them together:

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\FileBucket;

Cache::config([
    'cache' => [
        'bucket' => new FileBucket('/tmp/cache'),
        'default' => true
    ]
]);

// Set entries with tags (5th parameter is tags array)
Cache::set('user:123', $user1, 3600, null, ['users', 'user:123']);
Cache::set('user:456', $user2, 3600, null, ['users', 'user:456']);
Cache::set('product:1', $product, 3600, null, ['products', 'featured']);
Cache::set('product:2', $product2, 3600, null, ['products']);

// Invalidate all entries tagged with 'users'
Cache::invalidateTag('users');  // Both user:123 and user:456 are deleted

// Invalidate multiple tags at once
Cache::invalidateTags(['products', 'featured']);

// Tags work with remember pattern too
$data = Cache::remember('expensive:computation', function() {
    return performExpensiveComputation();
}, 3600, null, ['computations', 'expensive']);

// Later, invalidate all computations
Cache::invalidateTag('computations');

Attribute-Based Method Caching

Use the #[Cached] attribute to automatically cache method results.

Option 1: Using Proxy (Recommended - Call Methods Directly)

use Baukasten\Cache\Attributes\Cached;
use Baukasten\Cache\CacheInterceptor;

class UserService
{
    #[Cached(ttl: 3600, bucket: 'redis', tags: ['users'])]
    public function getUser(int $id): User
    {
        // This will only execute if not cached
        return User::find($id);
    }

    #[Cached(ttl: 3600, keyGenerator: 'generateUserCacheKey')]
    public function getUserWithRole(int $id, string $role): User
    {
        return User::where('id', $id)->where('role', $role)->first();
    }

    private function generateUserCacheKey(string $method, array $args): string
    {
        [$id, $role] = $args;
        return "user:{$id}:{$role}";
    }
}

// Create a proxy that intercepts method calls
$service = new UserService();
$cachedService = CacheInterceptor::proxy($service);

// Call methods directly - caching happens automatically!
$user = $cachedService->getUser(123);           // Executes and caches
$user = $cachedService->getUser(123);           // Returns from cache
$user = $cachedService->getUserWithRole(123, 'admin');  // Custom key

Option 2: Using Execute Method

// Execute specific methods with caching
$service = new UserService();
$user = CacheInterceptor::execute($service, 'getUser', [123]);

Wrapping Callables

use Baukasten\Cache\CacheInterceptor;

// Wrap a callable with caching
$cachedFunction = CacheInterceptor::wrap(
    callback: fn($id) => fetchUserFromDatabase($id),
    ttl: 3600,
    bucket: 'redis',
    keyGenerator: fn($id) => "user:{$id}",
    tags: ['users']
);

// Call it like a normal function
$user = $cachedFunction(123); // Fetches from database
$user = $cachedFunction(123); // Returns from cache

6. Bulk Operations

Efficiently handle multiple cache entries at once:

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'cache' => [
        'bucket' => new RedisBucket(),
        'default' => true
    ]
]);

// Get multiple values at once
$values = Cache::getMultiple(['user:1', 'user:2', 'user:3'], 'default_value');
// Returns: ['user:1' => $value1, 'user:2' => $value2, 'user:3' => 'default_value']

// Set multiple values at once
Cache::setMultiple([
    'user:1' => $userData1,
    'user:2' => $userData2,
    'user:3' => $userData3
], 3600);

// Set multiple with tags
Cache::setMultiple([
    'product:1' => $product1,
    'product:2' => $product2
], 3600, null, ['products', 'active']);

// Delete multiple values at once
Cache::deleteMultiple(['user:1', 'user:2', 'user:3']);

7. Enable/Disable Cache Globally

Control caching globally without modifying your code:

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'cache' => [
        'bucket' => new RedisBucket(),
        'default' => true
    ]
]);

// Cache is enabled by default
Cache::set('key', 'value');
$value = Cache::get('key'); // Returns 'value'

// Disable caching globally (useful for debugging or testing)
Cache::disable();

// All cache operations now bypass the cache
Cache::set('key2', 'value2');    // Returns false, nothing is stored
$value = Cache::get('key');      // Returns null (default value)
$value = Cache::get('key', 'default'); // Returns 'default'
Cache::exists('key');            // Returns false

// Check if cache is enabled
if (Cache::isEnabled()) {
    echo "Cache is enabled";
}

// Re-enable caching
Cache::enable();

// Cache works again
$value = Cache::get('key'); // Returns 'value' (the original cached value)

Use cases:

  • Development/Debugging: Disable cache temporarily to test with fresh data
  • Testing: Ensure tests run with or without cache
  • Maintenance: Disable cache during deployments or data migrations
  • Performance Testing: Compare performance with and without cache

Bucket Types

Bucket Type Constants

Each bucket has a TYPE constant that identifies its type:

use Baukasten\Cache\Buckets\MemoryBucket;
use Baukasten\Cache\Buckets\FileBucket;
use Baukasten\Cache\Buckets\RedisBucket;

echo MemoryBucket::TYPE; // 'memory'
echo FileBucket::TYPE;   // 'file'
echo RedisBucket::TYPE;  // 'redis'

// You can also get the type from an instance
$bucket = new FileBucket('/tmp/cache');
echo $bucket->getType(); // 'file'

Memory Bucket

Best for: Request-scoped caching, temporary data that doesn't need persistence.

Characteristics:

  • Extremely fast (in-memory storage)
  • Data is lost when the process ends
  • No external dependencies
  • Perfect for development or single-request caching
use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\MemoryBucket;

Cache::config([
    'memory' => [
        'bucket' => new MemoryBucket(
            defaultTtl: null  // Optional: default TTL in seconds
        ),
        'default' => true
    ]
]);

// Use case: Cache data for the current request only
Cache::set('current_user', $user);
Cache::set('view_data', $data, 60);

File Bucket

Best for: Persistent caching on single-server applications, storing cache across requests.

Characteristics:

  • Persists data to disk
  • Survives process restarts
  • No external dependencies
  • Slower than memory, faster than database queries
use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\FileBucket;

Cache::config([
    'file' => [
        'bucket' => new FileBucket(
            cachePath: '/path/to/cache/directory',  // Required: where to store cache files
            defaultTtl: 3600                         // Optional: default TTL in seconds
        ),
        'default' => true
    ]
]);

// Use case: Cache API responses, computed data, configurations
Cache::set('api:weather', $weatherData, 1800);  // Cache for 30 minutes
Cache::set('site:config', $config);             // Cache indefinitely (or until defaultTtl)

Redis Bucket

Best for: High-performance caching, multi-server applications, distributed systems.

Characteristics:

  • Very fast (in-memory)
  • Persists data (with Redis persistence)
  • Shared across multiple servers
  • Requires Redis server
use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'redis' => [
        'bucket' => new RedisBucket(
            host: '127.0.0.1',        // Redis host
            port: 6379,               // Redis port
            password: null,           // Optional: Redis password
            database: 0,              // Optional: Redis database number
            prefix: 'myapp:',         // Optional: key prefix for isolation
            defaultTtl: 3600          // Optional: default TTL in seconds
        ),
        'default' => true
    ]
]);

// Use case: Shared cache across multiple web servers
Cache::set('user:session:' . $sessionId, $sessionData, 1800);
Cache::set('global:stats', $stats, 300);

Choosing the Right Bucket

Use CaseRecommended BucketWhy
Single request cachingMemoryFastest, no persistence needed
Development/TestingMemory or FileSimple setup, no dependencies
Single server productionFilePersistent, no external services
Multi-server productionRedisShared across servers
High-traffic applicationsRedisBest performance at scale
Session storageRedisShared, fast, with TTL
API response cachingFile or RedisDepends on traffic and server setup

Custom Buckets

Create custom storage backends by implementing BucketInterface:

use Baukasten\Cache\Buckets\BucketInterface;
use Baukasten\Cache\Cache;

class MyCustomBucket implements BucketInterface
{
    public const TYPE = 'custom';

    public function getType(): string
    {
        return self::TYPE;
    }

    // Implement all interface methods
    public function set(string $key, mixed $value, ?int $ttl = null, array $tags = []): bool
    {
        // Your implementation
    }

    // ... other methods
}

// Register custom bucket
Cache::registerBucket('custom', new MyCustomBucket(), true);

Advanced Usage

Direct Bucket Access

// Get bucket instance for advanced operations
$bucket = Cache::bucket('redis');
$bucket->set('key', 'value');

Configuration Array Structure

use Baukasten\Cache\Buckets\MemoryBucket;
use Baukasten\Cache\Buckets\FileBucket;
use Baukasten\Cache\Buckets\RedisBucket;

[
    'bucket_name' => [
        'bucket' => new MemoryBucket() | new FileBucket(...) | new RedisBucket(...),
        'default' => true|false,  // Set as default bucket (optional)
    ]
]

The bucket configuration is now simplified - you pass instantiated bucket objects directly:

// Memory bucket (no configuration needed)
new MemoryBucket(defaultTtl: null)

// File bucket
new FileBucket(
    cachePath: '/cache/path',
    defaultTtl: 3600
)

// Redis bucket
new RedisBucket(
    host: '127.0.0.1',
    port: 6379,
    password: null,
    database: 0,
    prefix: '',
    defaultTtl: 3600
)

Real-World Examples

Example 1: Caching API Responses

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'api' => [
        'bucket' => new RedisBucket(prefix: 'api:'),
        'default' => true
    ]
]);

function getWeatherData(string $city): array
{
    $cacheKey = "weather:{$city}";

    return Cache::remember($cacheKey, function() use ($city) {
        // This expensive API call only happens on cache miss
        $response = file_get_contents("https://api.weather.com/forecast/{$city}");
        return json_decode($response, true);
    }, 1800, null, ['weather', 'external-api']);
}

// Use it
$weather = getWeatherData('London');

// Later, force refresh all weather data
Cache::invalidateTag('weather');

Example 2: Multi-Tenant Application

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\FileBucket;
use Baukasten\Cache\Buckets\RedisBucket;

// Different buckets for different data types
Cache::config([
    'tenant-data' => [
        'bucket' => new RedisBucket(
            host: 'redis.example.com',
            prefix: 'tenant:',
            defaultTtl: 3600
        ),
        'default' => true
    ],
    'static-assets' => [
        'bucket' => new FileBucket('/var/cache/assets', 86400)
    ]
]);

class TenantService
{
    public function getTenantSettings(int $tenantId): array
    {
        $cacheKey = "settings:{$tenantId}";

        return Cache::remember($cacheKey, function() use ($tenantId) {
            return $this->database->getTenantSettings($tenantId);
        }, 3600, null, ['tenants', "tenant:{$tenantId}"]);
    }

    public function updateTenantSettings(int $tenantId, array $settings): void
    {
        $this->database->updateTenantSettings($tenantId, $settings);

        // Invalidate only this tenant's cache
        Cache::invalidateTag("tenant:{$tenantId}");
    }

    public function clearAllTenantsCache(): void
    {
        // Invalidate cache for all tenants
        Cache::invalidateTag('tenants');
    }
}

Example 3: Database Query Caching

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\FileBucket;

Cache::config([
    'queries' => [
        'bucket' => new FileBucket('/var/cache/db', 600),
        'default' => true
    ]
]);

class ProductRepository
{
    public function getFeaturedProducts(): array
    {
        return Cache::remember('products:featured', function() {
            // Expensive database query
            return $this->db->query("
                SELECT * FROM products
                WHERE featured = 1 AND active = 1
                ORDER BY priority DESC
            ")->fetchAll();
        }, 600, null, ['products', 'featured']);
    }

    public function getProductsByCategory(int $categoryId): array
    {
        return Cache::remember("products:category:{$categoryId}", function() use ($categoryId) {
            return $this->db->query("
                SELECT * FROM products WHERE category_id = ?
            ", [$categoryId])->fetchAll();
        }, 600, null, ['products', "category:{$categoryId}"]);
    }

    public function onProductUpdated(int $productId): void
    {
        // Invalidate all product-related caches
        Cache::invalidateTag('products');
    }
}

Example 4: Session Management with Redis

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'sessions' => [
        'bucket' => new RedisBucket(
            host: 'redis.example.com',
            prefix: 'session:',
            defaultTtl: 1800  // 30 minutes
        ),
        'default' => true
    ]
]);

class SessionManager
{
    private const SESSION_TTL = 1800;

    public function createSession(string $userId, array $data): string
    {
        $sessionId = bin2hex(random_bytes(16));
        $sessionKey = "user:{$userId}:{$sessionId}";

        Cache::set($sessionKey, [
            'user_id' => $userId,
            'data' => $data,
            'created_at' => time(),
            'last_activity' => time()
        ], self::SESSION_TTL, null, ['sessions', "user:{$userId}"]);

        return $sessionId;
    }

    public function getSession(string $sessionId, string $userId): ?array
    {
        $sessionKey = "user:{$userId}:{$sessionId}";
        return Cache::get($sessionKey);
    }

    public function refreshSession(string $sessionId, string $userId): void
    {
        $sessionKey = "user:{$userId}:{$sessionId}";

        if ($session = Cache::get($sessionKey)) {
            $session['last_activity'] = time();
            Cache::set($sessionKey, $session, self::SESSION_TTL, null, ['sessions', "user:{$userId}"]);
        }
    }

    public function destroySession(string $sessionId, string $userId): void
    {
        $sessionKey = "user:{$userId}:{$sessionId}";
        Cache::delete($sessionKey);
    }

    public function destroyAllUserSessions(string $userId): void
    {
        // Invalidate all sessions for a specific user
        Cache::invalidateTag("user:{$userId}");
    }
}

Example 5: Computed Values and Expensive Operations

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\FileBucket;

Cache::config([
    'computations' => [
        'bucket' => new FileBucket('/var/cache/compute', 3600),
        'default' => true
    ]
]);

class ReportGenerator
{
    public function generateMonthlyReport(int $year, int $month): array
    {
        $cacheKey = "report:monthly:{$year}:{$month}";

        return Cache::remember($cacheKey, function() use ($year, $month) {
            // This takes 30+ seconds to compute
            $data = $this->computeRevenueMetrics($year, $month);
            $data['charts'] = $this->generateCharts($data);
            $data['statistics'] = $this->calculateStatistics($data);

            return $data;
        }, 86400, null, ['reports', 'monthly', "year:{$year}"]);
    }

    public function generateDashboardStats(): array
    {
        return Cache::remember('dashboard:stats', function() {
            return [
                'total_users' => $this->countUsers(),
                'active_sessions' => $this->countActiveSessions(),
                'revenue_today' => $this->calculateDailyRevenue(),
                'pending_orders' => $this->countPendingOrders(),
            ];
        }, 300, null, ['dashboard']);
    }

    public function clearReportsForYear(int $year): void
    {
        Cache::invalidateTag("year:{$year}");
    }
}

Example 6: Multi-Bucket Strategy

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\MemoryBucket;
use Baukasten\Cache\Buckets\FileBucket;
use Baukasten\Cache\Buckets\RedisBucket;

// Configure different buckets for different use cases
Cache::config([
    'memory' => [
        'bucket' => new MemoryBucket(),
        'default' => true  // Default for request-scoped caching
    ],
    'persistent' => [
        'bucket' => new FileBucket('/var/cache/app', 3600)
    ],
    'shared' => [
        'bucket' => new RedisBucket(
            host: 'redis.example.com',
            prefix: 'app:',
            defaultTtl: 1800
        )
    ]
]);

// Request-scoped: Use memory bucket (default)
Cache::set('current_user', $user);
Cache::set('request_data', $data);

// Server-scoped: Use file bucket for data that persists across requests
Cache::set('app_config', $config, 3600, 'persistent');
Cache::set('compiled_templates', $templates, 7200, 'persistent');

// Cluster-scoped: Use Redis for data shared across multiple servers
Cache::set('feature_flags', $flags, 600, 'shared');
Cache::set('rate_limit:' . $userId, $count, 60, 'shared');
Cache::set('global_announcements', $announcements, 1800, 'shared');

Example 7: Using Proxy for Service Caching

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;
use Baukasten\Cache\Attributes\Cached;
use Baukasten\Cache\CacheInterceptor;

Cache::config([
    'services' => [
        'bucket' => new RedisBucket(prefix: 'service:'),
        'default' => true
    ]
]);

class ProductService
{
    #[Cached(ttl: 600, tags: ['products'])]
    public function getProduct(int $id): array
    {
        return $this->database->query("SELECT * FROM products WHERE id = ?", [$id]);
    }

    #[Cached(ttl: 300, tags: ['products', 'search'])]
    public function searchProducts(string $query): array
    {
        return $this->database->query("SELECT * FROM products WHERE name LIKE ?", ["%{$query}%"]);
    }

    #[Cached(ttl: 3600, tags: ['products', 'categories'])]
    public function getProductsByCategory(int $categoryId): array
    {
        return $this->database->query("SELECT * FROM products WHERE category_id = ?", [$categoryId]);
    }

    public function updateProduct(int $id, array $data): void
    {
        $this->database->update('products', $id, $data);

        // Invalidate caches after update
        Cache::invalidateTag('products');
    }

    // This method has no #[Cached] attribute, so it won't be cached
    public function logProductView(int $productId): void
    {
        $this->database->insert('product_views', ['product_id' => $productId, 'viewed_at' => time()]);
    }
}

// Create a cached proxy - all methods with #[Cached] are automatically cached!
$productService = new ProductService();
$cachedService = CacheInterceptor::proxy($productService);

// Use the service naturally - caching happens automatically
$product = $cachedService->getProduct(123);        // Executes query, caches result
$product = $cachedService->getProduct(123);        // Returns from cache
$results = $cachedService->searchProducts('laptop'); // Executes and caches
$results = $cachedService->searchProducts('laptop'); // From cache

// Methods without #[Cached] work normally without caching
$cachedService->logProductView(123);  // Always executes, never cached

// When updating, invalidate caches
$cachedService->updateProduct(123, ['price' => 999.99]);  // Clears all product caches

Example 8: Conditional Caching for Testing/Development

use Baukasten\Cache\Cache;
use Baukasten\Cache\Buckets\RedisBucket;

Cache::config([
    'cache' => [
        'bucket' => new RedisBucket(),
        'default' => true
    ]
]);

// Disable cache in development or during testing
if (getenv('APP_ENV') === 'development' || getenv('DISABLE_CACHE') === 'true') {
    Cache::disable();
}

// Your application code works the same regardless
class DataService
{
    public function getExpensiveData(): array
    {
        return Cache::remember('expensive-data', function() {
            // This always runs in development (cache disabled)
            // But uses cache in production (cache enabled)
            return $this->fetchFromDatabase();
        }, 3600);
    }
}

// In PHPUnit tests
class MyTest extends TestCase
{
    public function testWithoutCache(): void
    {
        Cache::disable();

        $service = new DataService();
        $result = $service->getExpensiveData();

        // Data is fetched fresh, not from cache
        $this->assertNotNull($result);
    }

    public function testWithCache(): void
    {
        Cache::enable();

        $service = new DataService();
        $result1 = $service->getExpensiveData();
        $result2 = $service->getExpensiveData();

        // Both should be identical (from cache)
        $this->assertEquals($result1, $result2);
    }
}

// In console commands
class ClearDataCommand
{
    public function execute(): void
    {
        // Temporarily disable cache during data migration
        Cache::disable();

        try {
            $this->migrateData();
            $this->updateRecords();
        } finally {
            // Always re-enable cache
            Cache::enable();
        }
    }
}