php-redis-om: an object mapper for Redis
Published on July 25, 2024
Redis is an ultra-fast, performant, in-memory NoSQL database system, highly appreciated for its speed and simplicity of use. Mainly used as a cache, message queuing system, or key-value database, Redis offers unmatched flexibility for a multitude of other use cases and comes with numerous tools that allow for much more advanced usage, particularly for creating indexes and storing complex objects within it.
In the PHP ecosystem, developers are accustomed to using ORMs (Object-Relational Mappers) like Doctrine or Eloquent to map objects with relational tables and traditional SQL databases. However, currently, there is no equivalent for Redis written in PHP (unlike Java, Node.js, or Python, which each have such libraries), forcing developers to either write specific code to persist and query objects in Redis or, more often, use it as a simple storage server for serialized JSON objects. It is in this context that the development of the php-redis-om library started, a new library that allows you to persist, query, and map objects directly in Redis, fully leveraging its capabilities through Hash and JSON formats.
Easily manage your data persistence with ObjectManager
php-redis-om is a PHP library that facilitates interaction with Redis by offering an object mapper largely inspired by Doctrine. Developers familiar with Symfony's traditional ORM (or its ODM for MongoDB) will find similar methods and classes, making it quick and intuitive to use.
The RedisObjectManager class provided by the library implements the methods defined in the ObjectManager interface of Doctrine, allowing for the same functionalities and using php-redis-om in a nearly interchangeable manner with Doctrine.
The first step is to install the library via Composer:
composer require talleu/php-redis-om
Next, use the PHP attributes provided by the library to map your classes:
<?php
use Talleu\RedisOm\Om\Mapping as RedisOm;
#[RedisOm\Entity]
class User
{
#[RedisOm\Id]
#[RedisOm\Property]
public int $id;
#[RedisOm\Property(index:true)]
public string $name;
#[RedisOm\Property]
public \DateTimeImmutable $createdAt;
}
- The Entity attribute indicates to the bundle that this class should be considered as an entity mapped by the ObjectManager.
- The Id attribute is used to specify the property used as an identifier (each object to be persisted must have one).
- The Property attribute specifies which property will be persisted in Redis and provides additional information (name, type, indexed property, etc.).
Finally, persist your data in your Redis server with a simple persist/flush:
$objectManager = new RedisObjectManager();
$objectManager->persist($user);
$objectManager->flush();
And done đ„ł, your object is persisted in your Redis server!
JSON and HASH formats in Redis
Redis supports several types of data structures, but the two most relevant formats for object mapping are JSON and Hash models.
The JSON format needs no introduction, allowing you to store complex objects as JSON strings. It is ideal for cases where you have nested structures and want to maintain the flexibility of JSON. However, while your Redis server is perfectly capable of storing a JSON string as a value, it does not natively support indexing, making it impossible to query the stored data.
To use it, you will need to install the RedisJSON module on your Redis architecture.
The HASH format may be unfamiliar to you; it is a native Redis format: Hash allows you to âflattenâ your objects. It is very performant for partial reads and writes because it enables storing each field or sub-property of your object individually as a key-value pair. This is particularly useful for data that is frequently modified or partially read.
For example, here is how a Product object will be represented in the Redis datastore:
$product = new Product();
$product->name = 'Super t-shirt';
$product->color = "blue"
$product->category = new Category(id: 4, name: 't-shirts');
// When persisting it
> HSET product1 name "Super t-shirt" color "blue"
> HSET product1 category.id 4
> HSET product1 category.name "t-shirts"
// When retrieving the full object
> HGETALL product1
1) "name"
2) "Super t-shirt"
3) "color"
4) "bleue"
5) "category.id"
6) "4"
7) "category.name"
8) "t-shirts"
// Only retrieving a field
> HGET product1 name
"Super t-shirt"
Both formats are supported by php-redis-om and have the same persistence and retrieval methods. Performance differences are negligible; in a benchmark, the JSON format was slightly more performant with a high number of successive persist/flush operations (over 1000).
Data Mapper, Repository pattern, and Active Record
Several design patterns are commonly used for data access: Repository, Data Mapper and Active Record. .
The Data Mapper is a design pattern that separates the application's interface from the data persistence layer. It acts as an intermediary between our PHP objects and the Redis database, managing data conversion and ensuring that objects do not need to know persistence details. Here, it uses repository classes to handle these operations. This is the model chosen by Doctrine and adopted here in php-redis-om because it offers great flexibility and maintainability. By centralizing data access logic, it becomes easier to test and modify code without affecting business logic.
Active Record, on the other hand, integrates data access logic directly into business objects (as is the case in Eloquent, the default ORM of Laravel). While this can simplify code for simple applications, it can make maintenance more difficult in more complex architectures. This option was excluded.
Querying and sorting your data with the ObjectRepository
(To create indexes and then perform your first queries, your Redis server must have the Redisearch module previously added. By default, these modules are available in the complete Redis-stack and in the Docker provided by the library. To add it in production, refer to this documentation)
Once your objects are saved in the Redis datastore, you will naturally want to retrieve them. To do this, php-redis-om proposes instantiating a repository for each of your classes and provides a suite of methods to filter and query your resources.
use Talleu\RedisOm\Om\RedisObjectManager;
// Retrieve your repository
$userRepository = $objectManager->getRepository(User::class);
// Take advantage of the various provided methods
$userRepository->findAll();
$userRepository->findOneBy(['name' => 'John Doe']);
$userRepository->findBy(['name' => 'John'], ['age' => 'ASC'], 5);
Doctrine users will feel at home here, as these methods have the same names and arguments. Refer to the documentation for a more exhaustive list of available functions.
Repositories also allow you to instantiate a QueryBuilder to write more complex queries as you would directly in the command line in redis-cli.
Using different Redis clients
Although php-redis-om uses the PHP ext-redis extension by default, which is faster than the Predis library, it is possible to use other Redis clients by implementing the provided interface in your class: RedisClientInterface. This flexibility allows you to choose the client that best meets your needs and specific constraints.
Both implementations are directly available in the library.
Current state of the library
A Docker integration is provided for your development, with the necessary modules (RedisJSON and RediSearch) and benefits from the high performance provided by FrankenPHP.
php-redis-om is currently in version 0.2.*, meaning it is usable but still in the development phase. Initially developed for a specific need, the library is open to feedback and community contributions. You can report issues, propose improvements, or contribute to the code on the GitHub repository.