iviphp/cache

Cache stores and cache management for the IviPHP ecosystem.

Maintainers

Package info

github.com/iviphp/cache

pkg:composer/iviphp/cache

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.1.0 2026-07-21 12:36 UTC

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:

  • array
  • file

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/contracts
  • iviphp/support
  • iviphp/config
  • iviphp/container
  • iviphp/http
  • iviphp/validation
  • iviphp/database
  • iviphp/cache
  • iviphp/view
  • iviphp/auth
  • iviphp/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.