Cache Class

The Cache class is a file-based caching system with a two-layer architecture: an L1 in-memory layer for sub-millisecond reads within a request, and a disk layer for persistence across requests.

It supports tag-based invalidation for grouping related entries, stampede protection via file locking, stale-while-revalidate fallback for resilience under load, and a choice of PHP or JSON serialization.

Loading

Load the library inside your controller:

<?php
$this->load->library('cache');

Once loaded it is available as $this->cache.

Configuration

Add the following keys to app/config/config.php:

<?php
// Absolute path to the cache directory (created automatically if missing)
$config['cache_dir'] = APPPATH . 'cache/';

// Default TTL in seconds. 0 = no expiry.
$config['cache_default_expires'] = 3600;

// Serializer: 'php' (default) or 'json'
// Use 'php' when caching PHP objects or arrays with integer keys.
// Use 'json' for human-readable cache files and interoperability.
$config['cache_driver'] = 'php';

// Stampede protection — max seconds to wait for a lock
$config['cache_lock_timeout'] = 5;

// Microseconds between lock retry attempts (100 000 = 100 ms)
$config['cache_lock_sleep'] = 100000;

Key

Default

Description

cache_dir

app/cache/

Directory where .cache files are stored

cache_default_expires

0

Default TTL in seconds. 0 means no expiry.

cache_driver

php

Serializer: 'php' or 'json'

cache_lock_timeout

5

Seconds to wait when acquiring a write lock

cache_lock_sleep

100000

Microseconds between lock retry attempts

How the Two Layers Work

Every read checks the L1 memory layer first:

read('key')
     
     
┌────────────────┐   hit   ┌──────────────────────────┐
  L1 Memory     │────────▶│  Return value immediately 
  (per-request)            (no disk I/O)            
└───────┬────────┘         └──────────────────────────┘
         miss
        
┌────────────────┐   hit   ┌──────────────────────────┐
  Disk (.cache) │────────▶│  Populate L1, return     
  flock(LOCK_SH)           value                    
└───────┬────────┘         └──────────────────────────┘
         miss / expired
        
     FALSE

Every write updates both layers atomically: the disk file is written with flock(LOCK_EX) and the result is immediately stored in $_memory.

Note

The L1 memory layer is a static PHP array. It lives for the duration of the current request only and is automatically cleared by delete_all().

Basic Operations

Writing to the cache

<?php
// Cache for 10 minutes
$this->cache->write($data, 'homepage_posts', 600);

// Cache using the default TTL from config
$this->cache->write($data, 'homepage_posts');

// Cache with no expiry (persists until explicitly deleted)
$this->cache->write($data, 'app_settings', 0);

Reading from the cache

<?php
$data = $this->cache->get('homepage_posts');

if ($data === FALSE)
{
    // Cache miss — regenerate
    $data = $this->post_model->get_homepage();
    $this->cache->write($data, 'homepage_posts', 600);
}

Deleting a single entry

<?php
$this->cache->delete('homepage_posts');

Deleting everything

<?php
// Delete all files in the cache directory
$this->cache->delete_all();

// Delete all files in a subdirectory
$this->cache->delete_all('products');

Method

Signature

Returns

write()

write($contents, $filename, $expires, $dependencies)

void

get()

get($filename = NULL, $use_expires = TRUE)

mixed|false

delete()

delete($filename = NULL)

void

delete_all()

delete_all($dirname = '')

void

Model & Library Caching

model() and library() automatically cache the return value of any model or library method. The cache key is derived from the class name, method name, and serialised arguments — different argument combinations are stored as separate entries.

<?php
// Cache Post_model::get_all() for 5 minutes
$posts = $this->cache->model('post_model', 'get_all', [], 300);

// Different arguments produce separate cache entries
$user = $this->cache->model('user_model', 'find', [42], 600);

// Bust a cached entry by passing a negative TTL
$this->cache->model('post_model', 'get_all', [], -1);

// Cache a library method
$rates = $this->cache->library('currency', 'get_rates', ['USD'], 3600);

Stampede protection

When two requests hit a cache miss simultaneously, only one acquires a write lock. The second waits up to cache_lock_timeout seconds. If the lock is never acquired within that window, the class falls back to serving stale data rather than hammering the underlying source.

Method

Signature

Returns

model()

model($model, $method, $arguments = [], $expires = NULL)

mixed

library()

library($library, $method, $arguments = [], $expires = NULL)

mixed

Tag-Based Invalidation

Tags let you group related cache entries and invalidate them all in one call without knowing the individual keys.

Writing with tags

<?php
// Attach tags before calling write()
$this->cache->tags(['products', 'homepage'])->write($data, 'featured_products', 3600);
$this->cache->tags('products')->write($list, 'product_list', 3600);

// Tags also work with model() / library()
$this->cache->tags('users')->model('user_model', 'get_all', [], 600);

Invalidating tags

Incrementing a tag’s version counter marks every cache entry written with that tag as stale. Entries are removed lazily on the next read.

<?php
// Invalidate everything tagged 'products'
$this->cache->invalidate_tags('products');

// Invalidate multiple tags at once
$this->cache->invalidate_tags(['products', 'homepage']);

Note

Tag versions are stored in a single JSON file (_tag_versions.cache) written with an atomic tmp-file-and-rename strategy. The file is separate from regular .cache entries so it is never accidentally deleted by delete_all() patterns that target only content files.

Method

Signature

Returns

tags()

tags($tags)

$this

invalidate_tags()

invalidate_tags($tags)

void

Dependencies

A cache entry can declare other cache files it depends on. If a dependency file no longer exists or was updated after the dependent entry was written, the dependent entry is treated as a miss.

<?php
// Write the dependency first
$this->cache->write($nav_links, 'nav_links', 3600);

// Write the page that depends on it
$this->cache
    ->set_dependencies(['nav_links'])
    ->write($page_data, 'homepage', 3600);

// Chain multiple dependencies
$this->cache
    ->set_dependencies('nav_links')
    ->add_dependencies(['sidebar', 'footer'])
    ->write($data, 'full_page', 3600);

// Read the dependencies on the last-read entry
$deps = $this->cache->get_dependencies();

Method

Signature

Returns

set_dependencies()

set_dependencies($dependencies)

$this

add_dependencies()

add_dependencies($dependencies)

$this

get_dependencies()

get_dependencies()

array

Serializer / Driver

Two serializers are available. Switch between them via cache_driver in config.php. The serializer applies globally to all read and write operations.

Value

Format

When to use

php

PHP serialize()

Default. Supports all PHP types including objects, resources, and arrays with non-string keys. Slightly faster for complex structures.

json

json_encode()

Human-readable cache files. Good for debugging or when cache data must be readable by non-PHP tools. Only supports JSON-compatible types (no objects, no integer-keyed arrays with mixed types).

Warning

Changing the serializer on a live system without clearing existing cache files will cause decode() failures on stale files written with the old format. Run delete_all() after switching serializers.

Metadata

After a successful get() call the class stores metadata about the entry. Read it with get_created():

<?php
$data = $this->cache->get('homepage_posts');

if ($data !== FALSE)
{
    $created_at = $this->cache->get_created(); // Unix timestamp
    echo 'Cached at: ' . date('Y-m-d H:i:s', $created_at);
}

Method

Signature

Returns

get_created()

get_created()

int|null Unix timestamp or NULL if no entry has been read

Stale Data Fallback

When get() is called with $use_expires = FALSE, the expiry check is skipped and the stored content is returned even if the TTL has passed. The class uses this internally during stampede protection to serve stale content rather than sending all concurrent requests to the database simultaneously.

<?php
// Returns content even if the TTL has expired
$stale = $this->cache->get('homepage_posts', FALSE);

This is also useful for graceful degradation when a backend service is down:

<?php
$data = $this->cache->get('api_response');

if ($data === FALSE)
{
    try {
        $data = $this->api->fetch();
        $this->cache->write($data, 'api_response', 300);
    }
    catch (Exception $e) {
        // Serve stale data rather than showing an error
        $data = $this->cache->get('api_response', FALSE);
    }
}

Subdirectory Keys

Cache filenames may contain forward slashes to create a nested directory structure inside the cache root. This is useful for organising entries by model or feature area.

<?php
// Stored at: cache/products/42.cache
$this->cache->write($product, 'products/42', 600);
$product = $this->cache->get('products/42');

// Delete all entries in the products subdirectory
$this->cache->delete_all('products');

model() and library() use this automatically — entries are stored under a subdirectory named after the class (e.g. cache/post_model/<hash>) to keep model caches isolated from each other.

Complete Example

<?php
// app/controllers/Shop.php
class Shop extends Controller
{
    public function __construct()
    {
        parent::__construct();
        $this->load->library('cache');
        $this->load->model('Product_model', 'product_model');
    }

    public function index()
    {
        // Use model() for automatic key generation and stampede protection
        $products = $this->cache
            ->tags(['products', 'homepage'])
            ->model('product_model', 'get_featured', [], 600);

        $this->load->view('shop/index', ['products' => $products]);
    }

    public function update($id)
    {
        // ... save product ...

        // Invalidate everything tagged 'products' across all cache keys
        $this->cache->invalidate_tags('products');

        // Also bust any individually-keyed entry for this product
        $this->cache->delete('products/' . $id);

        $this->response->redirect('/shop');
    }

    public function category($slug)
    {
        // Manual get/write with a dependency on the category index
        $key  = 'category/' . $slug;
        $data = $this->cache->get($key);

        if ($data === FALSE)
        {
            $data = $this->product_model->get_by_category($slug);
            $this->cache
                ->set_dependencies(['category_index'])
                ->write($data, $key, 1800);
        }

        $this->load->view('shop/category', ['products' => $data]);
    }
}

Tips and Best Practices

  • Use model() and library() for the cleanest caching code — they handle key generation, locking, and stale fallback automatically.

  • Always use tags() when writing entries that belong to a logical group (e.g. all product-related caches share a 'products' tag). Call invalidate_tags() after any write that changes that group.

  • Use subdirectory keys (e.g. 'products/42') to organise entries. You can then call delete_all('products') to flush the entire group.

  • Pass a negative $expires to model() or library() to bust a specific cached method call without affecting other entries.

  • Use get($key, FALSE) explicitly in error-handling code to serve stale data rather than showing an error page when a backend service fails.

  • Keep the cache_dir outside the web root so .cache files are never directly accessible from a browser.

  • Avoid the 'json' driver when caching complex PHP structures (objects, mixed-type arrays). Use 'php' for reliability and only switch to 'json' when human-readability of cache files is a requirement.

  • Run a periodic cron job that calls delete_all() on the cache directory to prevent unbounded disk growth on sites with many unique cache keys.