iviphp / cache
Cache stores and cache management for the IviPHP ecosystem.
Requires
- php: >=8.2
- iviphp/support: ^0.1
This package is not auto-updated.
Last update: 2026-07-22 01:30:51 UTC
README
Cache stores and cache management for the IviPHP ecosystem.
Overview
iviphp/cache provides a small, framework-independent cache layer for PHP applications.
It includes:
- a common cache contract;
- in-memory caching;
- persistent filesystem caching;
- named cache stores;
- lazy store resolution;
- configurable default stores;
- cache expiration;
- permanent cache entries;
- atomic file replacement;
- per-key filesystem locks;
- cache integrity checks;
- optional payload compression;
- safe cache exceptions;
- no global static cache state.
The package currently provides two cache drivers:
arrayfile
Installation
composer require iviphp/cache
Requirements
- PHP 8.2 or later
- Composer
iviphp/support
The filesystem store can optionally use the PHP Zlib extension for payload compression.
Package structure
src/
├── Cache.php
├── CacheInterface.php
├── CacheManager.php
├── Exceptions/
│ └── CacheException.php
└── Stores/
├── ArrayStore.php
└── FileStore.php
Basic usage
<?php declare(strict_types=1); use Ivi\Cache\Cache; $cache = Cache::fromConfig([ 'default' => 'array', 'stores' => [ 'array' => [ 'driver' => 'array', ], ], ]); $cache->set( 'user.name', 'Gaspard', 300 ); $name = $cache->get('user.name');
Cache configuration
The recommended configuration structure contains a default store and a map of named stores:
$cache = Cache::fromConfig([ 'default' => 'file', 'stores' => [ 'array' => [ 'driver' => 'array', ], 'file' => [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/cache', ], ], ]);
The configured default store must exist.
Direct store configuration
A direct map of named stores is also accepted:
$cache = Cache::fromConfig([ 'default' => [ 'driver' => 'array', ], ]);
In this example, the store named default becomes the default cache store.
Array store
The array store keeps values in the current PHP process.
$cache = Cache::fromConfig([ 'default' => 'array', 'stores' => [ 'array' => [ 'driver' => 'array', ], ], ]);
The array store is useful for:
- automated tests;
- request-scoped caching;
- command execution;
- temporary runtime values;
- applications that do not require persistent cache data.
Values disappear when the PHP process ends.
Objects are stored directly and retain their normal PHP reference behavior.
File store
The file store persists values on the filesystem.
$cache = Cache::fromConfig([ 'default' => 'file', 'stores' => [ 'file' => [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/cache', ], ], ]);
The configured directory is created automatically by default.
The directory must be readable and writable by the PHP process.
File store configuration
$cache = Cache::fromConfig([ 'default' => 'file', 'stores' => [ 'file' => [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/cache', 'allowed_classes' => false, 'compression_threshold' => 1024, 'compression_level' => 6, 'maximum_entry_bytes' => 67_108_864, 'file_permissions' => 0600, 'directory_permissions' => 0700, 'create_directory' => true, ], ], ]);
Supported file-store options:
| Option | Default | Description |
|---|---|---|
directory |
required | Filesystem cache directory |
path |
none | Alias of directory |
allowed_classes |
false |
Classes allowed during serialization |
compression_threshold |
1024 |
Minimum serialized size before compression |
compression_level |
6 |
Zlib compression level from 0 to 9 |
maximum_entry_bytes |
67108864 |
Maximum cache entry size |
file_permissions |
0600 |
Permissions applied to cache files |
directory_permissions |
0700 |
Permissions applied to cache directories |
create_directory |
true |
Create the cache directory when missing |
Storing values
Store a value for a limited time:
$cache->set( 'user:42', [ 'id' => 42, 'name' => 'Gaspard', ], 300 );
The integer lifetime is expressed in seconds.
Permanent values
Store a value without expiration:
$cache->forever( 'application.version', '1.0.0' );
The following call is equivalent:
$cache->set( 'application.version', '1.0.0', null );
Retrieving values
$user = $cache->get('user:42');
Provide a default value:
$user = $cache->get( 'user:42', [ 'id' => null, 'name' => 'Guest', ] );
The default value is returned when the key:
- does not exist;
- has expired;
- was deleted.
Cached null values
Cached null values are treated as existing entries.
$cache->set( 'optional.value', null, 300 ); $cache->has('optional.value'); // true $cache->get('optional.value'); // null
Use has() when the difference between a missing entry and a cached null value matters.
Checking existence
if ($cache->has('user:42')) { $user = $cache->get('user:42'); }
Expired entries are treated as missing.
Deleting values
$cache->delete('user:42');
Deleting a missing key is considered successful.
Retrieving and deleting
Use pull() to retrieve and remove a value:
$token = $cache->pull( 'password-reset:token', null );
After the operation, the key no longer exists.
Clearing a store
Clear the selected store:
$cache->clear();
For the file store, the root cache directory is preserved.
The global store-lock file is also preserved.
Clearing every store
$cache->clearAll();
Every registered store is resolved and cleared.
Remembering values
Retrieve an existing value or calculate and cache it:
$user = $cache->remember( 'user:42', 300, function (): array { return [ 'id' => 42, 'name' => 'Gaspard', ]; } );
The resolver runs only when the key does not exist or has expired.
Remembering permanent values
$settings = $cache->rememberForever( 'application.settings', function (): array { return [ 'timezone' => 'Africa/Kampala', 'locale' => 'en', ]; } );
This value remains cached until it is explicitly deleted or the store is cleared.
DateInterval expiration
A DateInterval can be used instead of an integer:
use DateInterval; $cache->set( 'report', $report, new DateInterval('PT30M') );
Remember a value for one hour:
$result = $cache->remember( 'expensive.result', new DateInterval('PT1H'), function (): array { return performExpensiveOperation(); } );
Immediate expiration
A lifetime of zero or fewer seconds removes the existing entry:
$cache->set( 'temporary.key', 'value', 0 );
The value is not stored.
Multiple cache stores
$cache = Cache::fromConfig([ 'default' => 'file', 'stores' => [ 'runtime' => [ 'driver' => 'array', ], 'file' => [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/cache', ], ], ]);
Use the default store:
$cache->set( 'application.name', 'IviPHP' );
Use another store:
$runtimeCache = $cache->using('runtime'); $runtimeCache->set( 'request.identifier', 'request-123' );
The original cache instance remains unchanged.
$cache->storeName(); // file $runtimeCache->storeName(); // runtime
Return to the manager default:
$defaultCache = $runtimeCache->usingDefault();
Cache manager
Create a manager directly:
use Ivi\Cache\CacheManager; $manager = new CacheManager( stores: [ 'array' => [ 'driver' => 'array', ], 'file' => [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/cache', ], ], defaultStore: 'file' );
Create a cache entry point:
use Ivi\Cache\Cache; $cache = new Cache($manager);
Resolving stores
Resolve the default store:
$store = $manager->store();
Resolve a named store:
$store = $manager->store('array');
get() is an alias of store():
$store = $manager->get('file');
Stores are resolved lazily and reused after their first creation.
Registering a store
Register a configured store:
$manager->register( 'secondary', [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/secondary-cache', ] );
Register an existing store instance:
use Ivi\Cache\Stores\ArrayStore; $manager->instance( 'local', new ArrayStore() );
Store factories
A closure can create a custom store lazily:
use Ivi\Cache\CacheInterface; use Ivi\Cache\CacheManager; use Ivi\Cache\Stores\ArrayStore; $manager->register( 'custom', function ( CacheManager $manager, string $name ): CacheInterface { return new ArrayStore(); } );
The factory must return an implementation of CacheInterface.
Replacing a store
$manager->replace( 'file', [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/new-cache', ] );
The previously resolved store instance is discarded.
The new definition is resolved lazily.
Removing a store
$manager->forget('file');
When the removed store was the default, the first remaining registered store becomes the new default.
Store information
Check whether a store is registered:
$manager->has('file');
Check whether it has already been resolved:
$manager->isResolved('file');
Return all registered store names:
$names = $manager->names();
Return the default store name:
$name = $manager->defaultName();
Change the default store:
$manager->setDefault('array');
The store must already be registered.
Clearing resolved stores
Clear only stores that were already resolved:
$manager->clearResolved();
Stores that have not yet been resolved are not created.
Clearing all registered stores
$manager->clearAll();
Every registered store is resolved and cleared.
Removing all definitions
$manager->flush();
This removes all registered definitions and resolved store instances.
It does not automatically clear persistent cache data before removing the definitions.
Using stores directly
ArrayStore
use Ivi\Cache\Stores\ArrayStore; $store = new ArrayStore(); $store->set( 'name', 'IviPHP', 60 ); $name = $store->get('name');
FileStore
use Ivi\Cache\Stores\FileStore; $store = new FileStore( directory: __DIR__ . '/storage/cache' ); $store->set( 'name', 'IviPHP', 60 );
Array-store inspection
The array store provides additional inspection methods.
Count unexpired entries:
$count = $store->count();
Return all unexpired keys:
$keys = $store->keys();
Remove expired entries:
$removed = $store->prune();
File-store maintenance
The file store also provides maintenance methods.
Return the canonical cache directory:
$directory = $store->directory();
Remove expired entries:
$removed = $store->prune();
Count valid entries:
$count = $store->count();
File storage format
File cache entries use:
- SHA-256 key hashing;
- two-level directory sharding;
- JSON metadata envelopes;
- Base64-encoded serialized payloads;
- SHA-256 integrity checks;
- optional Zlib compression;
- atomic temporary-file replacement;
- per-key lock files;
- a global store lock.
Original cache keys are not used as filenames.
A key such as:
users:42:profile
is converted to a SHA-256 hash before creating its storage path.
Atomic writes
The file store writes data to a temporary file before replacing the destination cache file.
This reduces the risk of partially written cache entries.
Per-key locks protect concurrent write operations for the same cache key.
Cache stampede protection
FileStore::remember() holds a per-key lock while resolving a missing value.
$value = $fileStore->remember( 'expensive.operation', 300, function (): mixed { return performExpensiveOperation(); } );
Concurrent processes requesting the same missing key wait for the first resolver instead of calculating the value simultaneously.
This protection applies to the filesystem store.
The array store is scoped to one PHP process and does not provide inter-process locking.
Compression
Serialized payloads larger than the configured threshold may be compressed:
$fileStore = new FileStore( directory: __DIR__ . '/storage/cache', compressionThreshold: 1024, compressionLevel: 6 );
Compression is only retained when the compressed payload is smaller than the original serialized payload.
When Zlib is unavailable, values are stored without compression.
Maximum entry size
The default maximum cache entry size is 64 MiB:
$fileStore = new FileStore( directory: __DIR__ . '/storage/cache', maximumEntryBytes: 67_108_864 );
The limit protects the application from unexpectedly large cache files and decompressed payloads.
Caching objects
The file store disables object restoration by default:
'allowed_classes' => false
This is the safest default when cache files could be modified outside the application.
Allow specific classes:
$cache = Cache::fromConfig([ 'default' => 'file', 'stores' => [ 'file' => [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/cache', 'allowed_classes' => [ App\Data\UserData::class, App\Data\ReportData::class, ], ], ], ]);
Allow every serialized class:
'allowed_classes' => true
Allowing every class should only be used when the cache directory is fully trusted and protected.
Unsupported file-cache values
The file store rejects values that cannot be safely serialized, including:
- resources;
- closures;
- objects that are not included in
allowed_classes; - values exceeding the configured size limit.
The array store can keep closures and most runtime values because it does not serialize them.
Cache keys
Cache keys must:
- be non-empty;
- contain no invalid control characters;
- contain no null bytes;
- be no longer than 512 bytes.
Valid examples:
user:42
users.active
application/settings
report-2026-07
Keys are application-defined and may contain common separators.
Cache exceptions
Cache failures throw:
Ivi\Cache\Exceptions\CacheException
Example:
use Ivi\Cache\Exceptions\CacheException; try { $cache->set( 'user:42', $user, 300 ); } catch (CacheException $exception) { error_log( json_encode([ 'message' => $exception->getMessage(), 'context' => $exception->context(), ]) ); }
Safe exception context
CacheException provides diagnostic context without including cached values or serialized payloads.
$context = $exception->context();
Possible context fields include:
[
'store' => 'file',
'key_length' => 12,
]
The original cache key is not included in serialization, read, write and deletion failure contexts.
Exception factories
use Ivi\Cache\Exceptions\CacheException; CacheException::invalidKey( $key, $reason ); CacheException::invalidLifetime( $reason ); CacheException::storeNotFound( $store ); CacheException::invalidConfiguration( $store, $reason ); CacheException::serializationFailed( $key, $previous ); CacheException::deserializationFailed( $key, $previous ); CacheException::readFailed( $key, $store, $previous ); CacheException::writeFailed( $key, $store, $previous ); CacheException::deleteFailed( $key, $store, $previous ); CacheException::clearFailed( $store, $previous ); CacheException::directoryUnavailable( $directory );
Cache interface
Custom cache stores must implement:
Ivi\Cache\CacheInterface
The contract defines:
public function get( string $key, mixed $default = null ): mixed; public function has(string $key): bool; public function set( string $key, mixed $value, int|DateInterval|null $ttl = null ): bool; public function forever( string $key, mixed $value ): bool; public function pull( string $key, mixed $default = null ): mixed; public function delete(string $key): bool; public function clear(): bool; public function remember( string $key, int|DateInterval|null $ttl, callable $resolver ): mixed;
Custom cache store
<?php declare(strict_types=1); namespace App\Cache; use DateInterval; use Ivi\Cache\CacheInterface; final class CustomStore implements CacheInterface { public function get( string $key, mixed $default = null ): mixed { return $default; } public function has(string $key): bool { return false; } public function set( string $key, mixed $value, int|DateInterval|null $ttl = null ): bool { return true; } public function forever( string $key, mixed $value ): bool { return $this->set( $key, $value ); } public function pull( string $key, mixed $default = null ): mixed { $value = $this->get( $key, $default ); $this->delete($key); return $value; } public function delete(string $key): bool { return true; } public function clear(): bool { return true; } public function remember( string $key, int|DateInterval|null $ttl, callable $resolver ): mixed { if ($this->has($key)) { return $this->get($key); } $value = $resolver(); $this->set( $key, $value, $ttl ); return $value; } }
Register the custom store:
$manager->instance( 'custom', new App\Cache\CustomStore() );
Complete example
<?php declare(strict_types=1); use Ivi\Cache\Cache; use Ivi\Cache\Exceptions\CacheException; $cache = Cache::fromConfig([ 'default' => 'file', 'stores' => [ 'runtime' => [ 'driver' => 'array', ], 'file' => [ 'driver' => 'file', 'directory' => __DIR__ . '/storage/cache', 'compression_threshold' => 1024, 'maximum_entry_bytes' => 67_108_864, 'file_permissions' => 0600, 'directory_permissions' => 0700, ], ], ]); try { $user = $cache->remember( 'users:42', 300, function (): array { return [ 'id' => 42, 'name' => 'Gaspard', 'active' => true, ]; } ); $runtime = $cache->using('runtime'); $runtime->set( 'current.user', $user ); if ($runtime->has('current.user')) { $currentUser = $runtime->get( 'current.user' ); } } catch (CacheException $exception) { error_log( json_encode([ 'message' => $exception->getMessage(), 'context' => $exception->context(), ]) ); throw $exception; }
Security considerations
Protect the cache directory
The file cache directory must not be writable by untrusted users.
Recommended permissions:
directories: 0700
files: 0600
The cache directory should not be publicly accessible through the web server.
Do not cache secrets unnecessarily
Avoid caching sensitive values unless required.
Examples include:
- passwords;
- access tokens;
- payment credentials;
- private encryption keys;
- authentication secrets;
- personal information.
Serialized data
PHP serialized data must only be restored from trusted cache files.
The default file-store configuration disables object restoration:
'allowed_classes' => false
Prefer arrays and scalar values for persistent cache entries.
Cache is not permanent storage
Cache data may be:
- deleted;
- expired;
- corrupted;
- unavailable;
- cleared during deployment.
Do not use cache storage as the only source of important application data.
Cache keys are not authorization
The presence of a cache entry does not prove that a user is authorized to access the underlying data.
Authorization checks must remain in the application.
Design principles
- Small framework-independent API
- Explicit named stores
- Lazy store resolution
- No global static cache state
- Cached null-value support
- Clear expiration semantics
- Safe default object deserialization
- Atomic filesystem writes
- Per-key filesystem locks
- Cache stampede reduction
- Hashed filesystem paths
- Integrity-checked cache envelopes
- Optional transparent compression
- Protected cache directory permissions
- Safe diagnostic context
- Extensible cache-store contract
Package boundaries
This package is responsible for:
- cache contracts;
- cache value storage;
- expiration handling;
- named store management;
- array caching;
- filesystem caching;
- file-lock coordination;
- cache exceptions.
It is not responsible for:
- database queries;
- HTTP sessions;
- authentication;
- authorization;
- queue processing;
- distributed locking;
- Redis protocols;
- Memcached protocols;
- application configuration loading;
- framework bootstrapping.
Additional cache drivers can be provided by separate IviPHP packages.
Current limitations
The initial release does not include:
- Redis;
- Memcached;
- database-backed caching;
- distributed cache locks;
- cache tags;
- cache namespaces;
- increment and decrement operations;
- PSR-6 adapters;
- PSR-16 adapters;
- asynchronous cache operations.
These features can be added later without expanding the core package unnecessarily.
Ecosystem
This package is part of the IviPHP ecosystem:
iviphp/contractsiviphp/supportiviphp/configiviphp/containeriviphp/httpiviphp/validationiviphp/databaseiviphp/cacheiviphp/viewiviphp/authiviphp/framework
Contributing
Contributions should preserve the focused and secure design of the package.
Changes should:
- preserve cached null-value behavior;
- preserve cache-key validation;
- avoid exposing cached values in exceptions;
- preserve atomic filesystem writes;
- preserve file-lock coordination;
- maintain safe object-deserialization defaults;
- remain independent from the full framework;
- avoid introducing hidden global state;
- include clear failures for invalid configuration;
- work on supported PHP 8.2 environments.
Security
Please report security vulnerabilities privately to the Softadastra maintainers instead of opening a public issue.
License
This project is licensed under the MIT License.
Maintainer
Created and maintained by Softadastra.