Skip to content
Merged

V1 ! #132

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 115 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ with Redis.
- High performance and scalability with Redis®
- Support for Redis JSON module
- Automatic schema generation
- Search and query capabilities
- Search and query capabilities with range filters
- Auto-expiration of your objects
- PHP enum support (backed enums)
- Identity map and dirty tracking with partial updates
- Atomic transactions (MULTI/EXEC)
- Pagination with total count
- GEO queries (radius search)
- Pipeline batch reads
- API Platform support (beta)

## Requirements ⚙️
Expand All @@ -36,6 +42,7 @@ with Redis.
## Supported types ✅

- scalar (string, int, float, bool, double)
- PHP backed enums (string and int)
- timestamp
- json
- null
Expand Down Expand Up @@ -172,6 +179,113 @@ $users = $this->redisObjectManager->getRepository(User::class)->findBy(['name' =
```


## Enum Support 🏷️

PHP backed enums are natively supported:

```php
enum Status: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
}

#[RedisOm\Entity]
class Task
{
#[RedisOm\Id]
#[RedisOm\Property]
public int $id;

#[RedisOm\Property(index: true)]
public Status $status;
}
```

Search by enum value:
```php
$activeTasks = $repository->findBy(['status' => 'active']);
```

## Range Queries 🔢

Use MongoDB-style operators for numeric range searches:

```php
// Age between 18 and 65
$users = $repository->findBy(['age' => ['$gte' => 18, '$lte' => 65]]);

// Price greater than 100
$products = $repository->findBy(['price' => ['$gt' => 100]]);

// Score less than 50
$results = $repository->findBy(['score' => ['$lt' => 50]]);

// Combine with exact match
$results = $repository->findBy(['name' => 'John', 'age' => ['$gte' => 18]]);
```

Supported operators: `$gte` (>=), `$gt` (>), `$lte` (<=), `$lt` (<).

> **Note:** Range queries work automatically with HASH format (NUMERIC index is auto-generated for int/float).
> For JSON format, you must explicitly declare a NUMERIC index: `#[Property(index: ['age' => 'NUMERIC'])]`.

## Pagination 📄

```php
$paginator = $repository->paginate(
criteria: ['status' => 'active'],
page: 2,
itemsPerPage: 20,
orderBy: ['createdAt' => 'DESC']
);

$paginator->getItems(); // Current page items
$paginator->getTotalItems(); // Total matching count
$paginator->getTotalPages(); // Total number of pages
$paginator->getCurrentPage(); // Current page number
$paginator->hasNextPage(); // bool
$paginator->hasPreviousPage(); // bool

// Iterable
foreach ($paginator as $item) {
// ...
}
```

## Partial Updates (Merge) ⚡

Instead of re-persisting the entire object, use `merge()` to only update changed fields:

```php
$user = $objectManager->find(User::class, 1);
$user->name = 'New Name'; // Only this field changed

$objectManager->merge($user); // Detects change, updates only 'name'
$objectManager->flush();
```

For new objects (not loaded via `find()`), `merge()` falls back to a full `persist()`.

## Batch Reads (Pipeline) 🚀

Load multiple objects by ID in a single Redis pipeline call:

```php
$users = $repository->findMultiple([1, 2, 3, 4, 5]);
```

## GEO Queries 🌍

Search objects within a geographic radius (requires a GEO-indexed property):

```php
#[RedisOm\Property(index: ['location' => 'GEO'])]
public string $location; // Format: "longitude,latitude"

$nearby = $repository->findByGeoRadius('location', 2.3522, 48.8566, 10, 'km');
```

## Advanced documentation 📚
- [Installation](https://github.com/clementtalleu/php-redis-om/blob/main/docs/installation.md)
- [Configuration](https://github.com/clementtalleu/php-redis-om/blob/main/docs/configuration.md)
Expand Down
91 changes: 89 additions & 2 deletions src/Client/PredisClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Talleu\RedisOm\Client;

use Predis\Client as Predis;
use Predis\Command\RawCommand;
use Predis\Connection\StreamConnection;
use Talleu\RedisOm\Client\Helper\Converter;
use Talleu\RedisOm\Command\PropertyToIndex;
Expand Down Expand Up @@ -73,6 +74,14 @@ private function getLastError()
}


/**
* @inheritdoc
*/
public function hSet(string $key, string $field, string $value): void
{
$this->redis->hset(Converter::prefix($key), $field, $value);
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -143,6 +152,14 @@ public function jsonSet(string $key, ?string $path = '$', ?string $value = '{}')
}
}

/**
* @inheritdoc
*/
public function jsonSetProperty(string $key, string $property, string $value): void
{
$this->redis->executeRaw([RedisCommands::JSON_SET->value, Converter::prefix($key), '$.' . $property, $value]);
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -321,6 +338,72 @@ public function expireTime(string $key): int
return $timestamp;
}

/**
* @inheritdoc
*/
public function hGetAllMultiple(array $keys): array
{
$pipeline = $this->redis->pipeline();
foreach ($keys as $key) {
$pipeline->hgetall(Converter::prefix($key));
}
$results = $pipeline->execute();

$data = [];
foreach ($keys as $i => $key) {
if (!empty($results[$i])) {
$data[$key] = $results[$i];
}
}

return $data;
}

/**
* @inheritdoc
*/
public function jsonGetMultiple(array $keys): array
{
$results = $this->redis->pipeline(function ($pipeline) use ($keys) {
foreach ($keys as $key) {
// Predis pipelines have no executeRaw(); queue a RawCommand via executeCommand()
// so we can invoke JSON.GET (a non-core command) without the factory resolving it.
$pipeline->executeCommand(new RawCommand(RedisCommands::JSON_GET->value, [Converter::prefix($key)]));
}
});

$data = [];
foreach ($keys as $i => $key) {
$data[$key] = $results[$i] !== false ? $results[$i] : null;
}

return $data;
}

/**
* @inheritdoc
*/
public function multi(): void
{
$this->redis->multi();
}

/**
* @inheritdoc
*/
public function exec(): void
{
$this->redis->exec();
}

/**
* @inheritdoc
*/
public function discard(): void
{
$this->redis->discard();
}

/**
* @inheritdoc
*/
Expand All @@ -332,11 +415,11 @@ public function keys(string $pattern): array
/**
* @inheritdoc
*/
public function search(string $prefixKey, array $search, array $orderBy, ?string $format = RedisFormat::HASH->value, ?int $numberOfResults = null, int $offset = 0, ?string $searchType = Property::INDEX_TAG): array
public function search(string $prefixKey, array $search, array $orderBy, ?string $format = RedisFormat::HASH->value, ?int $numberOfResults = null, int $offset = 0, ?string $searchType = Property::INDEX_TAG, array $rangeFilters = []): array
{
$arguments = [RedisCommands::SEARCH->value, Converter::prefix($prefixKey)];

if ($search === []) {
if ($search === [] && $rangeFilters === []) {
$arguments[] = '*';
} else {
$criteria = '';
Expand All @@ -351,6 +434,10 @@ public function search(string $prefixKey, array $search, array $orderBy, ?string
}
}

foreach ($rangeFilters as $rangeQuery) {
$criteria .= $rangeQuery;
}

$arguments[] = $criteria;
}

Expand Down
88 changes: 86 additions & 2 deletions src/Client/RedisClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ public function hget(string $key, string $property): string
return $result;
}

/**
* @inheritdoc
*/
public function hSet(string $key, string $field, string $value): void
{
$this->redis->hSet(Converter::prefix($key), $field, $value);
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -130,6 +138,14 @@ public function jsonSet(string $key, ?string $path = '$', ?string $value = '{}')
}
}

/**
* @inheritdoc
*/
public function jsonSetProperty(string $key, string $property, string $value): void
{
$this->redis->rawCommand(RedisCommands::JSON_SET->value, Converter::prefix($key), '$.' . $property, $value);
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -312,6 +328,70 @@ public function expireTime(string $key): int
return (int) $timestamp;
}

/**
* @inheritdoc
*/
public function hGetAllMultiple(array $keys): array
{
$pipeline = $this->redis->pipeline();
foreach ($keys as $key) {
$pipeline->hGetAll(Converter::prefix($key));
}
$results = $pipeline->exec();

$data = [];
foreach ($keys as $i => $key) {
if (!empty($results[$i])) {
$data[$key] = $results[$i];
}
}

return $data;
}

/**
* @inheritdoc
*/
public function jsonGetMultiple(array $keys): array
{
$pipeline = $this->redis->pipeline();
foreach ($keys as $key) {
$pipeline->rawCommand(RedisCommands::JSON_GET->value, Converter::prefix($key));
}
$results = $pipeline->exec();

$data = [];
foreach ($keys as $i => $key) {
$data[$key] = $results[$i] !== false ? $results[$i] : null;
}

return $data;
}

/**
* @inheritdoc
*/
public function multi(): void
{
$this->redis->multi();
}

/**
* @inheritdoc
*/
public function exec(): void
{
$this->redis->exec();
}

/**
* @inheritdoc
*/
public function discard(): void
{
$this->redis->discard();
}

/**
* @inheritdoc
*/
Expand All @@ -323,11 +403,11 @@ public function keys(string $pattern): array
/**
* @inheritdoc
*/
public function search(string $prefixKey, array $search, array $orderBy, ?string $format = RedisFormat::HASH->value, ?int $numberOfResults = null, int $offset = 0, ?string $searchType = Property::INDEX_TAG): array
public function search(string $prefixKey, array $search, array $orderBy, ?string $format = RedisFormat::HASH->value, ?int $numberOfResults = null, int $offset = 0, ?string $searchType = Property::INDEX_TAG, array $rangeFilters = []): array
{
$arguments = [RedisCommands::SEARCH->value, Converter::prefix($prefixKey)];

if ($search === []) {
if ($search === [] && $rangeFilters === []) {
$arguments[] = '*';
} else {
$criteria = '';
Expand All @@ -342,6 +422,10 @@ public function search(string $prefixKey, array $search, array $orderBy, ?string
}
}

foreach ($rangeFilters as $rangeQuery) {
$criteria .= $rangeQuery;
}

$arguments[] = $criteria;
}

Expand Down
Loading
Loading