Overview

The term cache within Concrete CMS refers to a means of storing data for faster retrieval in the future.

Object Caching

Concrete makes use of a PSR-6 compatible caching layer, with 3 caching pools set up by default:

  • cache - The default cache. In standard configuration, this is an ephemeral cache, intended to live in memory or accessed via a very fast caching driver. (Note: while this cache is ephemeral-only by default it can be configured via the concrete.cache.levels.object configuration and changed to use the expensive cache as well.)
  • cache/request - An ephemeral cache, meant to store objects in memory across a single request. Note: This is the replacement to the CacheLocal api.
  • cache/expensive - This was intended for things that have a performance benefit by going to a slow cache, generally a disk cache. Note: This cache by default is persisted on the filesystem, and loaded into ephemeral cache when it is accessed so that subsequent accesses to the same data within the same request can be quick. The drivers of the expensive cache can be configured via the concrete.cache.levels.expensive configuration.

Basic API – Retrieving and Setting Items

An example of using the object cache to retrieve a "StudentList" object:

Concrete Version 8 and Above

(Note: the API for the cache has changed slightly between version 5.7 and version 8).

/**
 * @return StudentList
 */
public static function getByCourseID($id)
{
    $expensiveCache = \Core::make('cache/expensive');
    $studentCourseList = $expensiveCache->getItem('StudentList/Course' . $id);
    if ($studentCourseList->isMiss()) {
        $studentCourseList->lock();
        $list = StudentList::findBy(array('course_id' => $id));
        $expensiveCache->save($studentCourseList->set($list)->expiresAfter(300)); // expire after 300 seconds
    } else {
        $list = $studentCourseList->get()
    {
    return $list;
}

Concrete Version 5.7

/**
 * @return StudentList
 */
public static function getByCourseID($id)
{
    $expensiveCache = \Core::make('cache/expensive');
    $studentCourseList = $expensiveCache->getItem('StudentList/Course' . $id);
    if ($studentCourseList->isMiss()) {
        $studentCourseList->lock();
        $list = StudentList::findBy(array('course_id' => $id));
        $studentCourseList->set($list, 300); // expire after 300 seconds
    } else {
        $list = $studentCourseList->get()
    {
    return $list;
}

In this example, we are creating a cache based on the StudentList/Course{$id}. In the event that we did not find that item in our expensive cache we go to our database and add it to our expensive cache and then return the retrieved value.

The major differences between version 8 and 5.7 are the lack of a second TTL parameter in $item->set(), and the fact that the entire operation needs to be wrapped in $cache->save() in order for the cache to actually save the item. In version 5.7, the second parameter passed to the set function is the expiration for the cached value. The expiration can either be expressed as an integer value of time in seconds or by a DateTime object specifying when invalidation will occur. In version 8 this is handled by a second call to expiresAt(\Datetime $expires) or expiresAfter(int $seconds).

Notes

Be careful how deeply you nest your cache items, as this creates a directory structure for each / within the item path. This can be particularly harmful on file systems which have a limited number of chracters in a file path.*

Handling the Blackhole Cache

In the past, a popular pattern for working with the Concrete cache has looked like this:

$expensiveCache = \Core::make('cache/expensive');
$cacheObject = $expensiveCache->getItem('Cache/MyObject');
if ($cacheObject->isMiss()) {
    $value = $this->lookupSomeValue();
    $expensiveCache->save($cacheObject->set($value));
}
echo $cacheObject->get();

There is a big flaw in this approach; if a developer disables the cache with custom code (essentially swapping out the expensive cache with the 'BlackHole' cache) the code will never return a value, because the value is set within the cache and retrieved from the cache – but the blackhole cache throws away all the values that it's given. It's far better to rewrite the above code in this way:

$expensiveCache = \Core::make('cache/expensive');
$cacheObject = $expensiveCache->getItem('Cache/MyObject');
if ($cacheObject->isMiss()) {
    $value = $this->lookupSomeValue();
    $expensiveCache->save($cacheObject->set($value));
} else {
    $value = $cacheObject->get();
}
echo $value;

This way, $value will always contain the desired value, and will still continue to work with the cache.

Clearing the Cache

In the event we wanted to invalidate this cache (for instance when a new student is added to the course) we would do this:

public addStudentToCourse($studentId, $courseId)
{
    //code to actually add the student to the course
    $expensiveCache = \Core::make('cache/expensive');
    $studentCourseList = $expensiveCache->getItem('StudentList/Course' . $courseId);
    $studentCourseList->clear();
}

Cache invalidation for the built in Concrete caching pools also happens when a user goes to the dashboard and clears the cache.