Boosting up PHP-project with cache
Tags: php, beginners, tutorial, webdev
Published: 2021-10-01 10:22:37

The growth of the project and its load can be a real challenge for the developer. The website begins responding with a delay and the question of scaling is being raised more often. There are many effective solutions to increase the stability of the project and the load capability, and one of the most basic is caching.

Caching is the saving of data in highly accessible places temporarily so that their retrieval is performed faster than from the source. The most common example of using a cache is getting data from a database. When a product is first received from the database, for example, it is stored in the cache for a certain time, so each subsequent request to this product will not disturb the database, since the data will be received from another storage.

### What are the approaches?

There are many approaches to caching. You can check the list of PHP-compatible tools on the [php-cache](http://www.php-cache.com/en/latest/#cache-pool-implementations) page. However, the most common ones are:

- APCu
- Array
- Memcached
- Redis

Let's look at their differences and features.

### APCu

One of the most common and easy-to-configure caching tools. Saves the data into RAM (and also knows how to cache intermediate code, but that's a completely different story ...). To get started with APCu, you need to make sure that it is installed. To do that, run the following command on the command line:

```bash
php -i | grep 'apc.enabled'
# We're expecting to see:
# apc.enabled => On => On
```

Another way to check it: create a file **index.php** and put the `phpinfo()` function call in it. Make sure that you have a web server configured for the directory you are using and open the script in the browser via the server address. We are interested in the APCu section, if there is an APCu **Support: Enabled** item inside it, then everything is fine, we can move on.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46uboso08ks9243bov6b.png) 

If you don't have APCu installed, you can do it in the following way:

1. Launch a terminal window (Linux/macOS) or a command prompt (Windows. Enter "cmd" in the search)
2. Run the command: `pecl install apcu apcu_bc`
3. Open the **php.ini** configuration file via any text editor and make sure that the following lines are present:
    ```bash
    # Windows
    extension=php_apcu.dll
    extension=php_apcu_bc.dll
    
    apc.enabled=1
    apc.enable_cli=1
    
    #Linux / MacOS
    extension="apcu.so"
    extension="apc.so"
    
    apc.enabled=1
    apc.enable_cli=1
    ```
4. If there are no specified lines, add them and save the configuration file
5. Repeat the check for the installed APCu

To use this caching approach, we will need the basic functions. Here is an example of their application.

```php
$cacheKey = 'product_1';
$ttl = 600; // 10 minutes.

// Checking APCu availability
$isEnabled = apcu_enabled();

// Checks if there is data in the cache by key
$isExisted = apcu_exists($cacheKey);

// Saves data to the cache. Returns true if successful
// The $ttl argument determines how long the cache will be stored (seconds)
$isStored = apcu_store($cacheKey, ['name' => 'Demo product'], $ttl);

// Retrieves data from the cache by key. If not, returns false
$data = apcu_fetch($cacheKey);

// Deletes data from the cache by key
$isDeleted = apcu_delete($cacheKey);

var_dump([
    'is_enabled'   => $isEnabled,
    'is_existed'   => $isExisted,
    'is_stored'    => $isStored,
    'is_deleted'   => $isDeleted,
    'fetched_data' => $data,
]);
```

Any cache works on the principle of **key-value** storage, which means that the stored data is stored with a special key, which is accessed. In this case, the key is stored in the `$cacheKey` variable.

> **Important!** This approach only works in the website mode, i.e. when running from the command line, you will not receive data from the cache, and everything that you have saved to it will be cleared when the script completes. However, this will not cause any errors.
> 

### Array-cache

A simpler, but not always applicable, caching method. If the APCu saves data and makes it available for subsequent execution by all processes, then the Array cache stores it only for the request being processed.

What does it mean? Imagine that you have a page with user comments. One user can leave several messages, and when we collect an array of this data, we do not want to fetch the same user from the database several times. What we can do is set the received data to an array so that if there is one, we don't make a request again. This principle is very simple and just as easy to implement. Let's write a class that will perform such a save.

```php
class CustomArrayCache
{
    /**
     * Array is static and private
     * – private – so that it can be accessed only from the
     * methods of the class
     * – static – so that the property is available in all instances
     */
    private static array $memory = [];

    // Method for storing data in memory
    public function store(string $key, $value): bool
    {
        self::$memory[$key] = $value;

        return true;
    }

    // Method for getting data from memory
    public function fetch(string $key)
    {
        return self::$memory[$key] ?? null;
    }

    // Method for deleting data from memory
    public function delete(string $key): bool
    {
        unset(self::$memory[$key]);

        return true;
    }

    // Method for checking the availability of key data
    public function exists(string $key): bool
    {
        return array_key_exists($key, self::$memory);
    }
}
```

This approach is very simple, but due to its limitations, it is rarely used. However, it is useful to know about it.

### Memcached & Redis

The most advanced caching approaches. They require a server (daemon) running separately. From PHP, we connect to this server at the address and port. The configuration of these solutions is more complicated than setting up an APCu, but the data storage method is very similar (RAM). Their most important advantages are:

- Isolation from PHP. Separate services are responsible for the cache;
- Clustering capability. If the load on your project is very high, clustering of caching services will help to cope with it;

In this article, we will not go into the details of configuring Memcached and Redis. At this stage, we need to remember that if the load is very high– we should look towards these solutions since they have a good potential for scaling.

### PSR-16 Standard

PSR has two standards dedicated to caching: [PSR-6](https://www.php-fig.org/psr/psr-6) (normal caching interface) and [PSR-16](https://www.php-fig.org/psr/psr-16) (Simple caching interface) – we will focus on PSR-16.

This standard offers a special interface (`CacheInterface`) that classes that perform the caching function can satisfy. According to it, such classes should implement the following methods:

- `get($key, $default)` - Getting data from the cache. The second argument passes the value that will be returned if this data is missing.;
- `set($key, $value, $ttl = null)` - Saving data to the cache. As we saw earlier, the third parameter is passed the storage time in seconds. If left empty (null), the default value from the cache configuration will be substituted;
- `delete($key)` - Deletes key data;
- `clear()` - Clears the entire storage;
- `getMultiple($keys, $default)` - Allows you to get data for several keys at once;
- `setMultiple($values, $ttl = null)` - Allows you to record multiple values at once. As `$value` we pass an associative array, where the key is `$key` for the cache, and the value is the data to save;
- `deleteMultiple($keys)` - Deletes data for multiple keys;
- `has($key)` - Checks the availability of key data.

As you can see, the interface is very simple, and even the functions that we considered in the example for APCu are enough to write your cache service following PSR-16. But why is it necessary?

The main advantages of compliance with PSR standards:

- They are supported by the most popular libraries;
- Many PHP programmers adhere to PSR and will easily get used to your code;
- Thanks to the interface, we can easily change the service used to any other that supports PSR-16.

Let's take a closer look at the last point and its benefits.

### Usage of PSR-16 libraries

Libraries that create a "wrapper" over existing caching tools to match the interface are called adapters. For example, consider the adapters of the methods that we have already discussed:

- [APCu Adapter](https://github.com/php-cache/apcu-adapter)
- [Array Adapter](https://github.com/php-cache/array-adapter)
- [Memcached Adapter](https://github.com/php-cache/memcached-adapter)
- [Redis Adapter](https://github.com/php-cache/redis-adapter)

All of them satisfy PSR-16 and therefore are applied the same way, but each has its own logic "under the hood".

For an example of usage, let's load **APCu** and **array** adapters using **composer** into our project.

```bash
composer require cache/array-adapter
composer require cache/apcu-adapter
# Or
composer req cache/apcu-adapter cache/array-adapter
```

Let's imagine that we have a special class for getting products from a database. Let's call it ProductRepository, it has a `find($id)` method that returns a product by its ID, and if there is no such product, null.

```php
class ProductRepository
{
    /**
     * In order not to complicate the example, we will stipulate that
     * an array is returned as a product, and if it is not, null
     */
    public function find(int $id): ?array
    {
        // ...
        // Getting data from the database
        return $someProduct;
    }
}
```

If we want to enable caching, we should not do it inside the repository, because its responsibility is to return data from the database. Where then will we add the cache? There are several popular solutions, the simplest is an additional provider class. All he will do is try to get data from the cache, and if it didn't work out, he will contact the repository. To do this, we will define two dependencies in the constructor of such a class – our repository and `CacheInterface`. Why the interface? Because we will be able to use absolutely any of the mentioned adapters or other classes that satisfy PSR-16.

```php
use Psr\SimpleCache\CacheInterface;

class ProductDataProvider
{
    private ProductRepository $productRepository;
    private CacheInterface $cache;

    public function __construct(ProductRepository $productRepository, CacheInterface $cache)
    {
        $this->productRepository = $productRepository;
        $this->cache             = $cache;
    }

    public function get(int $productId): ?array
    {
        $cacheKey = sprintf('product_%d', $productId);

        // Trying to get the product from the cache
        $product = $this->cache->get($cacheKey);
        if ($product !== null) {
            // If there is a product, we return it
            // Temporarily output echo to understand that the data is from the cache
            echo 'Data from cache' . PHP_EOL; // PHP_EOL is a line break
            return $product;
        }
        // If there is no product, we get it from the repository
        $product = $this->productRepository->find($productId);

        if ($product !== null) {
            // Now we will save the received product to the cache for future requests
            // Also temporarily output echo
            echo 'Data from DB' . PHP_EOL;
            $this->cache->set($cacheKey, $product);
        }

        return $product;
    }
}
```

Our class is ready. Now let's look at its application in combination with an APCu adapter.

```php
use Cache\Adapter\Apcu\ApcuCachePool;

// Connect the Composer autoloader
require_once 'vendor/autoload.php';

// Our repository
$productRepository = new ProductRepository();
// APCu-cache adapter. Does not require any additional settings
$cache = new ApcuCachePool();

// Creating a provider, passing dependencies
$productDataProvider = new ProductDataProvider(
    $productRepository,
    $cache
);

// If there is such a product in the database, it will come back to us
$product = $productDataProvider->get(1);
var_dump($product);
```

If we want to replace APCu caching with an Array adapter or any other approach, we'll just pass it to the provider instead, because they all implement `CacheInterface`.

```php
use Cache\Adapter\PHPArray\ArrayCachePool;
// ...
$productRepository = new ProductRepository();
//$cache = new ApcuCachePool();
$cache = new ArrayCachePool();
$productDataProvider = new ProductDataProvider(
    $productRepository,
    $cache
);
// ...
```

### Race condition and data updating

The cache works as long as we keep it up to date. This means that if a user wants to update a product, it must be updated both in the database and in our cache. However, there is one important nuance here.

Imagine that our project is used by a very large number of users. And two of them update the same entity at the same time. In this case, the following situation may occur:

- User 1 received an entity from the cache
- User 1 updated the entity in the DB
- User 2 got the entity from the cache
- User 1 has updated the data in the cache
- User 2 updated the entity in the database but overwritten it with old data because the entity was irrelevant at the time of receipt
- and so on...

This situation is called a race condition when several processes access the same resource at the same time and a version conflict may occur. To avoid such a problem, one simple rule should be followed:

> When you receive any entity in the code to update it, always use data from the database.
> 

In any situation when we need to get a product and we are not going to update it, we use a cache. If we want to update it - use data from the database.

You can either refer to `ProductRepository` in the right places, instead of `ProductDataProvider` or add an argument to the data provider's method. For example (`$fromCache`):

```php
class ProductDataProvider
{
    // ...
    public function get(int $productId, bool $fromCache = true): ?array
    {
        $cacheKey = sprintf('product_%d', $productId);

        $product = $fromCache 
            ? $this->cache->get($cacheKey) 
            : null;
        if ($product !== null) {
            return $product;
        }
        $product = $this->productRepository->find($productId);

        if ($product !== null) {
            $this->cache->set($cacheKey, $product);
        }

        return $product;
    }
}
```

### Conclusion

Caching requires additional efforts from the developer when making a project and its use may not always be appropriate. The decision to apply it or not should be based on the expected (or actual) load and your expectations of the response speed to the user.

However, regardless of whether you will use these approaches in your current projects or not, they should be studied and put into practice, because this skill will be useful to you when working in large teams.

Summing up the article, let's look at the key ideas:

- Compliance with PSR-16 (or PSR-6) will allow you to easily connect a third-party library for caching and make your code understandable to other developers;
- For small projects, APCu is a good caching solution, because it is easy to configure and uses RAM, access to which is very high;
- For all PHP-compatible caching tools, some adapters can be viewed on the website [php-cache.com](http://php-cache.com/);
- Caching is a separate responsibility. Try to implement working with the cache in separate classes;
- If we are going to update an entity, it should be obtained from the database. If we need an entity only for viewing– we can request it from the cache;
- In large projects, Memcached or Redis are used to get the ability to scale.