Caching in the PHP Delivery SDK
Content types, environment and space data change rarely, but you want to access them very often. They are a perfect candidate for caching: you save API calls and speed up execution. Let's take a look at how to locally cache their data using the PHP CDA SDK.
Delivery SDK and PSR-6
The SDK supports any PSR-6 cache pool. We suggest using either the Symfony Cache component for a comprehensive solution, or one of the packages in the PHP cache organization to solve a specific need; for our example we'll be using the latter, specifically the Filesystem adapter:
composer require cache/filesystem-adapter
After this step is done, you have two options for triggering a cache warmup: using the CLI utility bundled in the SDK, or manually handling the cache warmer for a better integration with your workflow. Let's take a look at both.
CLI utility
Because PSR-6 cache pools can wrap almost anything, the SDK can not know how to properly instantiate them; for this reason, the actual instantiation of the the cache pool must be handled by the user. The SDK will, in fact, expect the user to create a class which implements Contentful\Delivery\Cache\CacheItemPoolFactoryInterface
:
namespace Contentful\Delivery\Cache;
/**
* CacheItemPoolFactoryInterface.
*
* This interface must be implemented by a factory
* in order to be compatible with CLI commands for warming up
* and clearing the cache.
*/
interface CacheItemPoolFactoryInterface
{
/**
* Returns a PSR-6 CacheItemPoolInterface object.
* The method receives two parameters, which can be used
* for things like using a different directory in the filesystem,
* but the implementation can simply not use them if they're not necessary.
*
* @param string $api A string representation of the API in use,
* it's the result of calling $client->getApi()
* @param string $spaceId The ID of the space
* @param string $environmentId The ID of the environment
*
* @return CacheItemPoolInterface
*/
public function getCacheItemPool($api, $spaceId, $environmentId);
}
Let's create a simple implementation:
namespace App\Cache;
use Cache\Adapter\Filesystem\FilesystemCachePool;
use Contentful\Delivery\Cache\CacheItemPoolFactoryInterface;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
class AppCacheFactory implements CacheItemPoolFactoryInterface
{
public function getCacheItemPool($api, $spaceId, $environmentId)
{
$path = \sprintf('%s/cache/%s-%s-%s/', __DIR__, $api, $spaceId, $environmentId);
$filesystem = new Filesystem(new Local($path));
return new FilesystemCachePool($filesystem);
}
}
Now that we have everything we need, let's trigger a cache warmup:
php vendor/bin/contentful delivery:cache:warmup --access-token=$ACCESS_TOKEN --space-id=$SPACE_ID --environment-id=$ENVIRONMENT_ID --factory-class="App\\Cache\\AppCacheFactory"
A few notes about the command:
- The environment ID parameter is optional, and will default to
master
. --factory-class
expects a fully-qualified class name (FQCN). The SDK uses the system autoloader, so anything in your project should work.- The command takes an optional
--use-preview
parameter, which will make the SDK use the Preview API instead of the Delivery API. - Use
php vendor/bin/contentful help delivery:cache:warmup
for a complete description. - A corresponding
delivery:cache:clear
command is provided, and it accepts the same parameters.
Manually calling the cache warmer
The CLI utility behind the scenes uses the class Contentful\Delivery\Cache\CacheWarmer
. If you need to have a tighter integration in your code with cache warming/clearing, you can use yourself that class directly. The constructor expects two arguments: a Contentful\Delivery\Client
object (configured without a cache setting) and a PSR-6 cache pool:
use App\Cache\AppCacheFactory;
use Contentful\Delivery\Cache\CacheWarmer;
use Contentful\Delivery\Client;
$client = new Client($accessToken, $spaceId, $environmentId);
// Here we use the class we defined above, but the important thing is to have
// a PSR-6 cache item pool object instantiated.
// Ideally, in a complete app, you would use some sort of dependency injection
// to get access to the pool
$cacheFactory = new AppCacheFactory();
$cacheItemPool = $cacheFactory->getCacheItemPool($client->getApi(), $spaceId, $environmentId);
$warmer = new CacheWarmer($client, $cacheItemPool);
if (!$wamer->warmUp()) {
throw new \RuntimeException('Could not warm up the cache');
}
Configuring the client
Regardless how you configure the cache, you need to tell the client to use the cache pool. To do so, set the cache
parameter to the PSR-6 cache pool in the options array of the client constructor:
use Contentful\Delivery\Client;
use Contentful\Delivery\ClientOptions;
$options = ClientOptions::create()
->withCache($cacheItemPool);
$client = new Client($accessToken, $spaceId, $environmentId, $options);
Now, the SDK will check the cache for space, environment, and content type information before querying the API. Bear in mind that when calling the collection endpoint for content types ($client->getContentTypes($query)
) the cache can not be used, since the SDK can't reliably predict which content types will be returned.
Runtime, dynamic cache warm up
There is another option, which doesn't require you to statically cache data beforehand, and instead delegates that process to runtime when a new space, environment, or content type is found. This is useful when working with very dynamic applications or cache stores, or where data about Contentful (such as space ID or environment ID) is not known at build time.
To enable dynamic cache warm up, pass true
as second parameter to withCache
:
use Contentful\Delivery\Client;
use Contentful\Delivery\ClientOptions;
$options = ClientOptions::create()
->withCache($cacheItemPool, true);
$client = new Client($accessToken, $spaceId, $environmentId, $options);
When using this configuration, the SDK will automatically save in the configured PSR-6 cache pool any new instance of space, environment, and content types that is found. While this is a convenient and easy option, we still recommend statically warming up the cache instead, to avoid increased runtime complexity and a possible performance hit (for instance if the cache is slow on write).
Caching actual entries and assets
The SDK provides a method of caching content (entries and assets) besides structure (content types, etc). However, this method has a huge limitation: it works only when calling $client->getEntry($entryId)
, $client->getAsset($assetId)
and when resolving a link. This means that calling $client->getEntries()
and $client->getAssets()
will skip the cache lookup altogether.
The reason for this is simple: when fetching a single entry or asset, the SDK can reliably know if it is in the local cache. In fact, to identify a cache element, its ID (alongside other elements) is used. This means when fetching a collection of entries or assets, the SDK can't really know which IDs will be returned, so the local cache will not be used.
If you still want to enable caching of entries and assets (it might be useful in certain scenarios), pass true
as the third parameter to withCache
:
use Contentful\Delivery\Client;
use Contentful\Delivery\ClientOptions;
$options = ClientOptions::create()
->withCache($cacheItemPool, true, true);
$client = new Client($accessToken, $spaceId, $environmentId, $options);
As a final note, remember one thing: should you make changes such as adding or removing a locale, or changing fields in a content type, you would need to regenerate the cache, as the SDK behavior is to use the cache whenever that's available, even at the cost of ignoring newer info. In the event of a new content type, its info would be picked up correctly — there would be nothing about it in the cache. Our suggestion is to warmup the cache at every change to the content types, so you can be sure to always have the best performance and never risk working with obsolete data. Should you make changes to a content type without purging its obsolete cache data, the SDK will still try to build the entry, but you may find yourself in a situation where some field is not built correctly, and is instead returned "raw", for instance a string rather than a Contentful\Core\Api\DateTimeImmutable
object, or an array rather than a Contentful\Core\Api\Link
object. For this reason, you should always try to do a cache purge on content type changes, either manually or using webhooks.