Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/workflows/static-analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ on:
branches: [ main, develop ]

jobs:
php-cs-fixer:
name: PHP-CS-Fixer
php-code-sniffer:
name: PHP Code Sniffer
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###
/clover.xml
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ services:
- '@Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver'
```

Decorate predis client with symfony-bundles/redis-bundle:

```yaml
# config/services.yaml

services:
Macpaw\RedisSchemaBundle\Redis\SchemaAwarePRedisClient:
decorates: SymfonyBundles\RedisBundle\Redis\ClientInterface
arguments:
- '@.inner'
- '@Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver'
```

## Usage
Anywhere you use Symfony cache services (injected via CacheInterface or AdapterInterface), your keys will automatically be prefixed based on the current schema.
For example:
Expand Down Expand Up @@ -76,4 +89,3 @@ Feel free to open issues and submit pull requests.

## License
This bundle is released under the MIT license.

8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,23 @@
},
"require": {
"php": ">=8.3",
"macpaw/schema-context-bundle": "^1.1",
"macpaw/schema-context-bundle": "^2.0",
"symfony/cache": "^6.4|^7.0"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "3.7.*"
"squizlabs/php_codesniffer": "3.7.*",
"symfony-bundles/redis-bundle": "^3.1"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"suggest": {
"symfony-bundles/redis-bundle": "For predis client key decarations with schema prefix"
},
"scripts": {
"composer-validate": [
"composer validate"
Expand Down
206 changes: 206 additions & 0 deletions src/Redis/SchemaAwarePRedisClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

declare(strict_types=1);

namespace Macpaw\RedisSchemaBundle\Redis;

use Macpaw\SchemaContextBundle\Service\BaggageSchemaResolver;
use Predis\Command\CommandInterface;
use Predis\Command\FactoryInterface;
use Predis\Configuration\OptionsInterface;
use Predis\Connection\ConnectionInterface;
use SymfonyBundles\RedisBundle\Redis\ClientInterface;

class SchemaAwarePRedisClient implements ClientInterface
{
/**
* @var array<array-key, string>
*/
protected readonly array $decoratedCallMethods;

/**
* @param array<array-key, string> $decoratedCallMethods Method names that should be prefixed with
* the schema which not present in interfaces like a hmget, hset, etc.
*/
public function __construct(
private BaggageSchemaResolver $resolver,
private ClientInterface $decorated,
array $decoratedCallMethods = [],
) {
$this->decoratedCallMethods = count($decoratedCallMethods) === 0 ? $this->getDefaultDecoratedMethods()
: $decoratedCallMethods;
}

public function pop(string $key): ?string
{
return $this->decorated->pop($this->prefixKey($key));
}

public function push(string $key, ...$values): int
{
return $this->decorated->push($this->prefixKey($key), ...$values);
}

public function count(string $key): int
{
return $this->decorated->count($this->prefixKey($key));
}

public function remove(string $key): int
{
return $this->decorated->remove($this->prefixKey($key));
}

public function getCommandFactory(): FactoryInterface
{
return $this->decorated->getCommandFactory();
}

public function getOptions(): OptionsInterface
{
return $this->decorated->getOptions();
}

public function connect(): void
{
$this->decorated->connect();
}

public function disconnect(): void
{
$this->decorated->disconnect();
}

public function getConnection(): ConnectionInterface
{
return $this->decorated->getConnection();
}

/**
* @param array<array-key, mixed> $arguments
*/
public function createCommand($method, $arguments = []): CommandInterface
{
return $this->decorated->createCommand($method, $arguments);
}

public function executeCommand(CommandInterface $command): mixed
{
return $this->decorated->executeCommand($command);
}

/**
* @param array<array-key, mixed> $arguments
*/
public function __call($method, $arguments): mixed
{
if (in_array($method, $this->decoratedCallMethods, true)) {
// Automatically prefix the first argument if it's a string or an array of strings
if (array_key_exists(0, $arguments)) {
if (is_string($arguments[0])) {
$arguments[0] = $this->prefixKey($arguments[0]);
}

if (is_array($arguments[0])) {
$arguments[0] = array_map(
fn(mixed $value): mixed => is_string($value) ? $this->prefixKey($value) : $value,
$arguments[0]
);
}
}
}

return $this->decorated->$method(...$arguments);
}

private function prefixKey(string $key): string
{
$schema = $this->resolver->getSchema();

return $schema === $this->resolver->getEnvironmentSchema() ? $key : $schema . '.' . $key;
}

/**
* @return array<array-key, string>
*/
private function getDefaultDecoratedMethods(): array

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the fact that this will need to be supported, but I don't see a more suitable option.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to answer or do anything here, just think out loud

{
return [
// Basic key ops
'copy', 'dump', 'exists', 'expire', 'expireat', 'expiretime', 'move', 'rename', 'renamenx',
'sort', 'sort_ro', 'ttl', 'pttl', 'type', 'append', 'del', 'watch', 'unwatch', 'persist', 'touch',
'pexpire', 'pexpireat',

// Bloom filter (BF*)
'bfadd', 'bfexists', 'bfinfo', 'bfinsert', 'bfloadchunk', 'bfmadd', 'bfmexists',
'bfreserve', 'bfscandump',

// Bit operations
'bitcount', 'bitfield', 'bitfield_ro', 'bitpos',

// Cuckoo filter (CF*)
'cfadd', 'cfaddnx', 'cfcount', 'cfdel', 'cfexists', 'cfloadchunk', 'cfmexists',
'cfinfo', 'cfinsert', 'cfinsertnx', 'cfreserve', 'cfscandump',

// Count-Min Sketch (CMS*)
'cmsincrby', 'cmsinfo', 'cmsinitbydim', 'cmsinitbyprob', 'cmsmerge', 'cmsquery',

// String counters / String ops
'decr', 'decrby', 'get', 'getbit', 'getex', 'getrange', 'getdel', 'getset', 'incr', 'incrby', 'incrbyfloat',
'mget', 'psetex', 'set', 'setbit', 'setex', 'setnx', 'setrange', 'strlen',

// Hashes (H*)
'hdel', 'hexists', 'hexpire', 'hexpireat', 'hexpiretime', 'hpersist', 'hpexpire',
'hpexpireat', 'hpexpiretime', 'hget', 'hgetex', 'hgetall', 'hgetdel', 'hincrby',
'hincrbyfloat', 'hkeys', 'hlen', 'hmget', 'hmset', 'hrandfield', 'hscan', 'hset',
'hsetex', 'hsetnx', 'httl', 'hpttl', 'hvals', 'hstrlen',

// JSON (ReJSON)
'jsonarrappend', 'jsonarrindex', 'jsonarrinsert', 'jsonarrlen', 'jsonarrpop',
'jsonclear', 'jsonarrtrim', 'jsondel', 'jsonforget', 'jsonget', 'jsonnumincrby',
'jsonmerge', 'jsonmget', 'jsonobjkeys', 'jsonobjlen', 'jsonresp', 'jsonset', 'jsonstrappend',
'jsonstrlen', 'jsontoggle', 'jsontype',

// Lists (L*)
'blmove', 'blpop', 'brpop', 'brpoplpush', 'lcs', 'lindex', 'linsert', 'llen', 'lmove', 'lmpop',
'lpop', 'lpush', 'lpushx',
'lrange', 'lrem', 'lset', 'ltrim', 'rpop', 'rpoplpush', 'rpush', 'rpushx',

// Sets (S*)
'sadd', 'scard', 'sdiff', 'sdiffstore', 'sinter', 'sintercard', 'sinterstore',
'sismember', 'smembers', 'smismember',
'smove', 'spop', 'srandmember', 'srem', 'sscan', 'sunion', 'sunionstore',

// T-Digest (TDIGEST*)
'tdigestadd', 'tdigestbyrank', 'tdigestbyrevrank', 'tdigestcdf', 'tdigestcreate',
'tdigestinfo', 'tdigestmax', 'tdigestmerge', 'tdigestquantile', 'tdigestmin', 'tdigestrank',
'tdigestreset', 'tdigestrevrank', 'tdigesttrimmed_mean',

// TOP-K (TOPK*)
'topkadd', 'topkincrby', 'topkinfo', 'topklist', 'topkquery', 'topkreserve',

// TimeSeries (TS*)
'tsadd', 'tsalter', 'tscreate', 'tscreaterule', 'tsdecrby', 'tsdel', 'tsdeleterule',
'tsget', 'tsincrby', 'tsinfo', 'tsrange', 'tsrevrange',

// Streams (X*)
'xadd', 'xdel', 'xlen', 'xrevrange', 'xrange', 'xtrim',

// Sorted sets (Z*)
'zadd', 'zcard', 'zcount', 'zdiff', 'zdiffstore', 'zincrby', 'zinter', 'zintercard', 'zinterstore', 'zmpop',
'zmscore', 'zpopmin', 'zpopmax', 'bzpopmin', 'bzpopmax', 'bzmpop', 'zrandmember', 'zrange',
'zrangebyscore', 'zrangestore', 'zrank', 'zrem', 'zremrangebyrank', 'zremrangebyscore',
'zrevrange', 'zrevrangebyscore', 'zrevrank', 'zscore', 'zscan', 'zrangebylex',
'zrevrangebylex', 'zremrangebylex', 'zlexcount',

// HyperLogLog
'pfadd', 'pfmerge', 'pfcount',

// Key TTL precise
'pexpiretime',

// Geo
'geoadd', 'geohash', 'geopos', 'geodist', 'georadius', 'georadiusbymember', 'geosearch', 'geosearchstore',
];
}
}
18 changes: 10 additions & 8 deletions src/Redis/SchemaAwareRedisAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ public function __construct(
) {
}

private function prefixKey(string $key): string
{
$schema = $this->resolver->getSchema();

return $schema ? $schema . '.' . $key : $key;
}

public function clear(string $prefix = ''): bool
{
return $this->decorated->clear($prefix);
Expand Down Expand Up @@ -98,6 +91,15 @@ public function delete(string $key): bool

public function reset(): void
{
$this->decorated->clear($this->resolver->getSchema() ?? '');
$schema = $this->resolver->getSchema();

$this->decorated->clear($schema === $this->resolver->getEnvironmentSchema() ? '' : $schema);
}

private function prefixKey(string $key): string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: if SchemaAwarePRedisClient and SchemaAwareRedisAdapter are running at the same time, will they not duplicate the prefix?

{
$schema = $this->resolver->getSchema();

return $schema === $this->resolver->getEnvironmentSchema() ? $key : $schema . '.' . $key;
}
}
Loading
Loading