From 1b6759084ffca0cf26f37ca87fa90c2c6587b482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 3 Jul 2019 14:48:55 +0200 Subject: [PATCH] Add Predis support (#59) * wip * Add Predis support * Enable phpredis extension only for some builds * Add note about phpredis * Fix if command * Revert travis changes & add a version check to predis tests --- .travis.yml | 6 ++-- README.md | 5 +-- composer.json | 3 +- .../PhpRedisNotInstalledException.php | 8 +++++ src/StorageDrivers/RedisStorageDriver.php | 23 +++++++------ src/Traits/BootstrapsTenancy.php | 12 +++++-- src/config/tenancy.php | 1 + tests/BootstrapsTenancyTest.php | 32 +++++++++++++++++++ tests/TestCase.php | 3 +- 9 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 src/Exceptions/PhpRedisNotInstalledException.php diff --git a/.travis.yml b/.travis.yml index f92d5e2c..94a09dd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ env: - - LARAVEL_VERSION="5.7.*" TESTBENCH_VERSION="~3.7" - - LARAVEL_VERSION="5.8.*" TESTBENCH_VERSION="~3.8" + - LARAVEL_VERSION="5.7.*" TESTBENCH_VERSION="~3.7" REDIS_DRIVER=phpredis + - LARAVEL_VERSION="5.8.*" TESTBENCH_VERSION="~3.8" REDIS_DRIVER=phpredis language: php php: @@ -14,7 +14,7 @@ before_install: - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini install: - - composer require "laravel/framework:$LARAVEL_VERSION" "orchestra/testbench:$TESTBENCH_VERSION" + - travis_retry composer require "laravel/framework:$LARAVEL_VERSION" "orchestra/testbench:$TESTBENCH_VERSION" - travis_retry composer install --no-interaction before_script: diff --git a/README.md b/README.md index 6ecd0fba..152b9e46 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ You won't have to change a thing in your application's code.\* ### Requirements - Laravel 5.7 or 5.8 -- phpredis (predis is not supported) ### Installing the package @@ -99,6 +98,8 @@ config('tenancy.redis.prefix_base') . $uuid These changes will only apply for connections listed in `prefixed_connections`. +> **Note: *If you want Redis to be multi-tenant, you must use phpredis. Predis does not support prefixes.*** + #### `cache` Cache keys will be tagged with a tag: @@ -328,7 +329,7 @@ The entire application will use a new database connection. The connection will b Connections listed in the `tenancy.redis.prefixed_connections` config array use a prefix based on the `tenancy.redis.prefix_base` and the tenant UUID. -**Note: You *must* use phpredis. Predis doesn't support prefixes.** +**Note: You *must* use phpredis if you want mutli-tenant Redis. Predis doesn't support prefixes.** ## Cache diff --git a/composer.json b/composer.json index 3dedc135..0d77a1de 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ ], "require": { "illuminate/support": "5.7.*||5.8.*", - "webpatser/laravel-uuid": "^3.0" + "webpatser/laravel-uuid": "^3.0", + "predis/predis": "^1.1" }, "require-dev": { "vlucas/phpdotenv": "^2.2||^3.3", diff --git a/src/Exceptions/PhpRedisNotInstalledException.php b/src/Exceptions/PhpRedisNotInstalledException.php new file mode 100644 index 00000000..6c9da3e7 --- /dev/null +++ b/src/Exceptions/PhpRedisNotInstalledException.php @@ -0,0 +1,8 @@ +redis->getOption($this->redis->client()::OPT_PREFIX); - $hashes = $hashes ?: $this->redis->scan(null, $redis_prefix.'tenants:*'); - - return array_map(function ($tenant) use ($redis_prefix) { - // Left strip $redis_prefix from $tenant - if (substr($tenant, 0, strlen($redis_prefix)) == $redis_prefix) { - $tenant = substr($tenant, strlen($redis_prefix)); + if (! $hashes) { + // Apparently, the PREFIX is applied to all functions except scan(). + // Therefore, if the `tenancy` Redis connection has a prefix set + // (and PhpRedis is used), prepend the prefix to the search. + $redis_prefix = ''; + if (config('database.redis.client') === 'phpredis') { + $redis_prefix = $this->redis->getOption($this->redis->client()::OPT_PREFIX); } - + $hashes = array_map(function ($hash) use ($redis_prefix) { + // Left strip $redis_prefix from $hash + return substr($hash, strlen($redis_prefix)); + }, $this->redis->scan(null, $redis_prefix.'tenants:*')); + } + + return array_map(function ($tenant) { return $this->redis->hgetall($tenant); }, $hashes); } diff --git a/src/Traits/BootstrapsTenancy.php b/src/Traits/BootstrapsTenancy.php index d09be0ff..f8441b66 100644 --- a/src/Traits/BootstrapsTenancy.php +++ b/src/Traits/BootstrapsTenancy.php @@ -5,6 +5,8 @@ namespace Stancl\Tenancy\Traits; use Stancl\Tenancy\CacheManager; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; +use Symfony\Component\Debug\Exception\FatalThrowableError; +use Stancl\Tenancy\Exceptions\PhpRedisNotInstalledException; trait BootstrapsTenancy { @@ -13,7 +15,9 @@ trait BootstrapsTenancy public function bootstrap() { $this->switchDatabaseConnection(); - $this->setPhpRedisPrefix($this->app['config']['tenancy.redis.prefixed_connections']); + if ($this->app['config']['tenancy.redis.tenancy']) { + $this->setPhpRedisPrefix($this->app['config']['tenancy.redis.prefixed_connections']); + } $this->tagCache(); $this->suffixFilesystemRootPaths(); } @@ -28,7 +32,11 @@ trait BootstrapsTenancy foreach ($connections as $connection) { $prefix = $this->app['config']['tenancy.redis.prefix_base'] . $this->tenant['uuid']; $client = Redis::connection($connection)->client(); - $client->setOption($client::OPT_PREFIX, $prefix); + try { + $client->setOption($client::OPT_PREFIX, $prefix); + } catch (\Throwable $t) { + throw new PhpRedisNotInstalledException(); + } } } diff --git a/src/config/tenancy.php b/src/config/tenancy.php index f1d9207f..c414a688 100644 --- a/src/config/tenancy.php +++ b/src/config/tenancy.php @@ -12,6 +12,7 @@ return [ 'suffix' => '', ], 'redis' => [ + 'tenancy' => false, 'prefix_base' => 'tenant', 'prefixed_connections' => [ 'default', diff --git a/tests/BootstrapsTenancyTest.php b/tests/BootstrapsTenancyTest.php index 6fdab20c..a3751a1c 100644 --- a/tests/BootstrapsTenancyTest.php +++ b/tests/BootstrapsTenancyTest.php @@ -3,6 +3,8 @@ namespace Stancl\Tenancy\Tests; use Illuminate\Support\Facades\Redis; +use Illuminate\Support\Facades\Config; +use Stancl\Tenancy\Exceptions\PhpRedisNotInstalledException; class BootstrapsTenancyTest extends TestCase { @@ -30,6 +32,36 @@ class BootstrapsTenancyTest extends TestCase } } + /** @test */ + public function predis_is_supported() + { + if (app()->version() < 'v5.8.27') { + $this->markTestSkipped(); + } + + Config::set('database.redis.client', 'predis'); + Redis::setDriver('predis'); + Config::set('tenancy.redis.tenancy', false); + + // assert no exception is thrown from initializing tenancy + $this->assertNotNull($this->initTenancy()); + } + + /** @test */ + public function predis_is_not_supported_without_disabling_redis_multitenancy() + { + if (app()->version() < 'v5.8.27') { + $this->markTestSkipped(); + } + + Config::set('database.redis.client', 'predis'); + Redis::setDriver('predis'); + Config::set('tenancy.redis.tenancy', true); + + $this->expectException(PhpRedisNotInstalledException::class); + $this->initTenancy(); + } + /** @test */ public function filesystem_is_suffixed() { diff --git a/tests/TestCase.php b/tests/TestCase.php index 180600d9..8a81121c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -36,7 +36,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase public function initTenancy($domain = 'localhost') { - tenancy()->init($domain); + return tenancy()->init($domain); } /** @@ -72,6 +72,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'public', 's3', ], + 'tenancy.redis.tenancy' => true, 'tenancy.migrations_directory' => database_path('../migrations'), ]); }