Repository cache in Symfony

  • avatar
  • 2.2K Views
  • 11 mins read

In today's data-driven world, efficient data management is paramount for any modern web application. Symfony, a popular PHP framework, provides developers with powerful tools and patterns to streamline data handling. Two key components that play a pivotal role in enhancing data management within Symfony applications are the Repository Pattern and Data Caching. In this article, we'll delve into these concepts, exploring how they work together to boost performance and simplify data access in Symfony projects.

Prerequisites

Understanding basic concepts about repository pattern and how it is implemented:

We are going to use a fresh Symfony 6 installation for this guide, take a look on how to install & set up Symfony 6 project.

The Repository Pattern

The Repository Pattern, a fundamental concept in Symfony and many other modern frameworks, offers an elegant solution to manage database interactions. It acts as an intermediary between your application's business logic and the underlying database, abstracting away the complexities of querying and retrieving data. By encapsulating data access logic in repositories, your code becomes more modular and maintainable. Whether you need to retrieve a single record or execute complex queries, repositories provide a clear and consistent API, enhancing code readability and reusability.

Data Caching in Symfony

While efficient database queries are crucial for application performance, minimizing database requests can further optimize response times. This is where data caching comes into play. Symfony offers robust caching mechanisms through its Cache component. By caching frequently accessed data, you can significantly reduce the load on your database and enhance overall application responsiveness. The combination of the Repository Pattern and caching is a powerful one. Repository methods can be configured to check the cache before hitting the database. If the requested data is found in the cache, it can be returned immediately, bypassing the need for a database query. This approach not only speeds up data retrieval but also reduces server load, making your application more scalable and efficient.

Setting up the framework

Execute the commands outlined in the article referenced in the prerequisites section to initiate a new Symfony project. The following command has been used to startup the project for this guide.

symfony new symfony_cache --version="6.3.*" --webapp

Run Symfony's local development server:

symfony server:start

Creating the entry point

We start by establishing a new route that will serve as both the entry point and a testing route. To achieve this, we introduce a route named within the configuration file located at config/routes.yaml.

repository-cache:
path: /repository-cache
controller: App\\Controller\\RepositoryController
methods: GET

Keep in mind that you can access the route on your local development server by combining the provided address when launching the server with the specific path.

The format should be similar to the following one.

http://127.0.0.1:8000/repository-cache

Creating the repository

The purpose of our tutorial is to highlight how links operate and how to configure repositories effectively within Symfony. We will use a simple repository that generates fake data for testing purposes.

Let's define the contract for our users repository, enabling to retrieve a user based on their ID number.

<?php

namespace App\\Repository;

interface UserRepositoryInterface
{
public function getById(int $userId): string;
}

Following that, we create an implementation of the repository contract, which can be adapted to various databases like MySQL, MongoDB, or any other database system.

<?php

namespace App\\Repository\\Database;

use App\\Repository\\UserRepositoryInterface;

class UserRepository implements UserRepositoryInterface
{
public function getById(int $userId): string
{
return sprintf('User #%d', $userId);
}
}

At this stage, it's optional to perform the wiring between interface and implementation. However, we recommend explicitly defining it because it will be essential when configuring the cache repository. To achieve this, please incorporate the following lines into the config/services.yaml file.

App\\Repository\\UserRepositoryInterface:
class: App\\Repository\\Database\\UserRepository

As previously mentioned, for testing purposes and to simplify the tutorial, we won't be fetching data from an actual database; instead, we will mock the returned data.

Testing the repository

To verify the functionality of the setup, we've created the repository controller for the route mentioned in the previous section.

<?php

namespace App\\Controller;

use App\\Repository\\UserRepositoryInterface;
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
use Symfony\\Component\\HttpFoundation\\Response;

class RepositoryController extends AbstractController
{
public function __invoke(UserRepositoryInterface $userRepository): Response
{
$user = $userRepository->getById(1);

return new Response($user, Response::HTTP_OK);
}
}

Accessing the route will show up the following message:

User #1

Establishing Redis connection

The caching layer will rely on Redis, requiring the definition of a connection contract.

<?php

namespace App\\Repository\\Redis;

use Redis;

interface RedisClientInterface
{
public function client(): Redis;
}

And the corresponding implementation.

<?php

namespace App\\Repository\\Redis;

use Redis;

class RedisClient implements RedisClientInterface
{
private Redis $redis;

public function __construct(string $host, int $port)
{
$this->redis = new Redis();
$this->redis->connect($host, $port);
}

public function client(): Redis
{
return $this->redis;
}
}

The host and port for the Redis connection are dynamic parameters sourced from the .env file, thus requiring their definition in config/services.yaml.

App\\Repository\\Redis\\RedisClient:
arguments:
- '%env(REDIS_HOST)%'
- '%env(REDIS_PORT)%'

The Redis connection is now ready to be used.

Creating cache repository

The Cache component provides features covering simple to advanced caching needs. It is designed for performance and resiliency, ships with ready to use adapters for the most common caching backends. To get started, use Composer package manager to add the package to your project's dependencies.

composer require symfony/cache

We can proceed with creating the cache repository implementation. Maintaining consistency with the user repository contract is a must for this implementation.

<?php

namespace App\\Repository\\Cache;

use App\\Repository\\UserRepositoryInterface;
use App\\Repository\\Redis\\RedisClientInterface;

use Symfony\\Component\\Cache\\Adapter\\RedisAdapter;

class UserRepository implements UserRepositoryInterface
{
private RedisAdapter $cache;
private UserRepositoryInterface $databaseRepository;

const CACHE_PREFIX = 'user_';
const CACHE_SECONDS_TTL = 10;

public function __construct(
RedisClientInterface $redisClient,
UserRepositoryInterface $databaseRepository,
) {
// Init Redis adapter for cache
$this->cache = new RedisAdapter(
$redisClient->client(),
self::CACHE_PREFIX,
self::CACHE_SECONDS_TTL
);

// Set the fallback repository
$this->databaseRepository = $databaseRepository;
}

public function getById(int $userId): string
{
$userFromCache = $this->cache->getItem($userId);

if ($userFromCache->isHit()) {
return $userFromCache->get(); // Cached data
}

$userFromDatabase = $this->databaseRepository->getById($userId);

// Save cache for future requests
$userFromCache->set($userFromDatabase);
$this->cache->save($userFromCache);

return $userFromDatabase;
}
}

For the correct initialization of constructor parameters, it's necessary to modify the repository definition within the services configuration file, as shown below.

App\\Repository\\UserRepositoryInterface:
class: App\\Repository\\Cache\\UserRepository
arguments:
- '@App\\Repository\\Redis\\RedisClient'
- '@DatabaseUserRepository'

DatabaseUserRepository:
class: App\\Repository\\Database\\UserRepository

According to the configuration, our requests will be directed towards the cache repository, followed by the database repository as a fallback in the absence of a cache hit.

Testing the repository

When accessing the identical route established for the standard repository, you should consistently observe the data retrieved for the specified user. There's no need to change the controller since it's already compatible with the repository contract.

User #1

The cached data will automatically expire 10 seconds after storage, as specified in the repository configuration. You have the flexibility to adjust this expiration period, whether you want to extend it, reduce it, or set it to zero if there's no need to invalidate the cache.

Working with Tags

Cache tags are metadata labels or identifiers associated with cached data items. They are used to group and categorize cached data, providing a way to mark and organize cache entries that belong to a specific category or have a certain relationship.

For tag-based cache, you can make use of TagAwareAdapter. When using Redis as the backend, choosing the dedicated RedisTagAwareAdapter is typically more compelling, as it offers improved performance for tag-based invalidation.

<?php

namespace App\\Repository\\Cache;

use App\\Repository\\UserRepositoryInterface;
use App\\Repository\\Redis\\RedisClientInterface;

use Symfony\\Component\\Cache\\Adapter\\RedisTagAwareAdapter;

class UserRepository implements UserRepositoryInterface
{
private RedisTagAwareAdapter $cache;
private UserRepositoryInterface $databaseRepository;

const CACHE_PREFIX = 'user_';
const CACHE_SECONDS_TTL = 10;
const CACHE_TAG = 'users';

public function __construct(
RedisClientInterface $redisClient,
UserRepositoryInterface $databaseRepository,
) {
// Init Redis adapter for cache
$this->cache = new RedisTagAwareAdapter(
$redisClient->client(),
self::CACHE_PREFIX,
self::CACHE_SECONDS_TTL
);

// Set the fallback repository
$this->databaseRepository = $databaseRepository;
}

public function getById(int $userId): string
{
$userFromCache = $this->cache->getItem($userId);

if ($userFromCache->isHit()) {
return $userFromCache->get(); // Cached data
}

$userFromDatabase = $this->databaseRepository->getById($userId);

// Save cache for future requests
$userFromCache->set($userFromDatabase)->tag(self::CACHE_TAG);
$this->cache->save($userFromCache);

return $userFromDatabase;
}
}

There haven't been many alterations in our cache repository implementation: we've replaced the Redis adapter and associated a tag when storing the data. Cache tags are a powerful tool for managing and organizing cached data efficiently, offering a more systematic and fine-tuned approach to cache management and invalidation, leading to improved application performance and maintainability.

Conclusion

In the world of Symfony development, the Repository Pattern and Data Caching are indispensable tools for managing data efficiently. They provide a structured way to handle data access and storage while ensuring optimal performance. By implementing these concepts in your Symfony projects, you can strike a balance between clean, maintainable code and lightning-fast data retrieval. Whether you're building a small web application or a large-scale enterprise system, the Repository Pattern and Data Caching will undoubtedly elevate your Symfony development to new heights, delivering a superior user experience.

Credits

Official GitHub: https://github.com/hibit-dev/symfony6-cache

 Join Our Monthly Newsletter

Get the latest news and popular articles to your inbox every month

We never send SPAM nor unsolicited emails

0 Comments

Leave a Reply

Your email address will not be published.

Replying to the message: View original

Hey visitor! Unlock access to featured articles, remove ads and much more - it's free.