From 9df78eb9c255aacc5ae8daa2efa7f89141248395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 14:36:49 +0200 Subject: [PATCH 01/32] [1.7.0] Add DB storage driver (#82) --- README.md | 30 +++++- src/config/tenancy.php => assets/config.php | 16 ++- ...2019_08_08_000000_create_tenants_table.php | 35 ++++++ src/Interfaces/StorageDriver.php | 1 + src/StorageDrivers/DatabaseStorageDriver.php | 84 +++++++++++++++ src/StorageDrivers/RedisStorageDriver.php | 4 +- src/TenancyServiceProvider.php | 8 +- src/Tenant.php | 102 ++++++++++++++++++ src/TenantManager.php | 38 +++++-- test | 6 +- tests/CommandsTest.php | 1 + tests/TenantStorageTest.php | 29 +++++ tests/TestCase.php | 46 ++++++-- 13 files changed, 375 insertions(+), 25 deletions(-) rename src/config/tenancy.php => assets/config.php (72%) create mode 100644 assets/migrations/2019_08_08_000000_create_tenants_table.php create mode 100644 src/StorageDrivers/DatabaseStorageDriver.php create mode 100644 src/Tenant.php diff --git a/README.md b/README.md index f893f82c..092d3210 100644 --- a/README.md +++ b/README.md @@ -366,7 +366,35 @@ Note that deleting a tenant doesn't delete his database. You can do this manuall ## Storage driver -Currently, only Redis is supported, but you're free to code your own storage driver which follows the `Stancl\Tenancy\Interfaces\StorageDriver` interface. Just point the `tenancy.storage_driver` setting at your driver. +### Database + +The database storage driver lets you store information about tenants in a relational database like MySQL, PostgreSQL and SQLite. + +To use this storage driver, publish the `create_tenants_table` migration: + +``` +php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=migrations +``` + +By default, the table contains only `uuid`, `domain` and `data` columns. + +The `data` column is used to store information about a tenant, such as their selected plan, in JSON form. This package does not store anything in the column by default. + +You can store specific keys in your own columns. This is useful if you want to use RDBMS features like indexes. + +If you don't need any custom columns, you can skip the next section and run: + +``` +php artisan migrate +``` + +#### Adding your own columns + +To add your own columns, TODO. + +### Redis + +Using Redis as your storage driver is recommended due to its low overhead compared to a relational database like MySQL. **Note that you need to configure persistence on your Redis instance** if you don't want to lose all information about tenants. diff --git a/src/config/tenancy.php b/assets/config.php similarity index 72% rename from src/config/tenancy.php rename to assets/config.php index 6ebd7251..b8f27925 100644 --- a/src/config/tenancy.php +++ b/assets/config.php @@ -1,13 +1,25 @@ 'Stancl\Tenancy\StorageDrivers\RedisStorageDriver', + 'storage_driver' => 'Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver', + 'storage' => [ + 'db' => [ // Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver + 'data_column' => 'data', + 'custom_columns' => [ + // 'plan', + ], + 'connection' => 'central', + ], + 'redis' => [ // Stancl\Tenancy\StorageDrivers\RedisStorageDriver + 'connection' => 'tenancy', + ], + ], 'tenant_route_namespace' => 'App\Http\Controllers', 'exempt_domains' => [ // 'localhost', ], 'database' => [ - 'based_on' => 'mysql', + 'based_on' => 'sqlite', 'prefix' => 'tenant', 'suffix' => '', ], diff --git a/assets/migrations/2019_08_08_000000_create_tenants_table.php b/assets/migrations/2019_08_08_000000_create_tenants_table.php new file mode 100644 index 00000000..fc07702e --- /dev/null +++ b/assets/migrations/2019_08_08_000000_create_tenants_table.php @@ -0,0 +1,35 @@ +string('uuid', 36)->primary(); // don't change this + $table->string('domain', 255)->index(); // don't change this + + // your indexed columns go here + + $table->json('data')->default('{}'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('tenants'); + } +} diff --git a/src/Interfaces/StorageDriver.php b/src/Interfaces/StorageDriver.php index 83dcae4a..1bda4976 100644 --- a/src/Interfaces/StorageDriver.php +++ b/src/Interfaces/StorageDriver.php @@ -6,6 +6,7 @@ interface StorageDriver { public function identifyTenant(string $domain): array; + /** @return array[] */ public function getAllTenants(array $uuids = []): array; public function getTenantById(string $uuid, array $fields = []): array; diff --git a/src/StorageDrivers/DatabaseStorageDriver.php b/src/StorageDrivers/DatabaseStorageDriver.php new file mode 100644 index 00000000..faaa9f16 --- /dev/null +++ b/src/StorageDrivers/DatabaseStorageDriver.php @@ -0,0 +1,84 @@ +getTenantIdByDomain($domain); + if (! $id) { + throw new \Exception("Tenant could not be identified on domain {$domain}"); + } + + return $this->getTenantById($id); + } + + /** + * Get information about the tenant based on his uuid. + * + * @param string $uuid + * @param array $fields + * @return array + */ + public function getTenantById(string $uuid, array $fields = []): array + { + if ($fields) { + return Tenant::decodeData(Tenant::find($uuid)->only($fields)); + } else { + return Tenant::find($uuid)->decoded(); + } + } + + public function getTenantIdByDomain(string $domain): ?string + { + return Tenant::where('domain', $domain)->first()->uuid ?? null; + } + + public function createTenant(string $domain, string $uuid): array + { + return Tenant::create(['uuid' => $uuid, 'domain' => $domain])->toArray(); + } + + public function deleteTenant(string $id): bool + { + return Tenant::find($id)->delete(); + } + + public function getAllTenants(array $uuids = []): array + { + return Tenant::getAllTenants($uuids)->toArray(); + } + + public function get(string $uuid, string $key) + { + return Tenant::find($uuid)->get($key); + } + + public function getMany(string $uuid, array $keys): array + { + return Tenant::find($uuid)->getMany($keys); + } + + public function put(string $uuid, string $key, $value) + { + return Tenant::find($uuid)->put($key, $value); + } + + public function putMany(string $uuid, array $values): array + { + foreach ($values as $key => $value) { + Tenant::find($uuid)->put($key, $value); + } + + return $values; + } +} diff --git a/src/StorageDrivers/RedisStorageDriver.php b/src/StorageDrivers/RedisStorageDriver.php index 82faebdb..63c087af 100644 --- a/src/StorageDrivers/RedisStorageDriver.php +++ b/src/StorageDrivers/RedisStorageDriver.php @@ -11,7 +11,7 @@ class RedisStorageDriver implements StorageDriver public function __construct() { - $this->redis = Redis::connection('tenancy'); + $this->redis = Redis::connection(config('tenancy.redis.connection', 'tenancy')); } public function identifyTenant(string $domain): array @@ -33,8 +33,6 @@ class RedisStorageDriver implements StorageDriver */ public function getTenantById(string $uuid, array $fields = []): array { - $fields = (array) $fields; - if (! $fields) { return $this->redis->hgetall("tenants:$uuid"); } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 76218665..09ffeaaf 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -33,9 +33,13 @@ class TenancyServiceProvider extends ServiceProvider ]); $this->publishes([ - __DIR__ . '/config/tenancy.php' => config_path('tenancy.php'), + __DIR__ . '/../assets/config.php' => config_path('tenancy.php'), ], 'config'); + $this->publishes([ + __DIR__ . '/../assets/migrations/' => database_path('migrations'), + ], 'migrations'); + $this->loadRoutesFrom(__DIR__ . '/routes.php'); Route::middlewareGroup('tenancy', [ @@ -52,7 +56,7 @@ class TenancyServiceProvider extends ServiceProvider */ public function register() { - $this->mergeConfigFrom(__DIR__ . '/config/tenancy.php', 'tenancy'); + $this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy'); $this->app->bind(StorageDriver::class, $this->app['config']['tenancy.storage_driver']); $this->app->bind(ServerConfigManager::class, $this->app['config']['tenancy.server.manager']); diff --git a/src/Tenant.php b/src/Tenant.php new file mode 100644 index 00000000..2eb8039f --- /dev/null +++ b/src/Tenant.php @@ -0,0 +1,102 @@ +map([__CLASS__, 'decodeData'])->toBase(); + } + + public function decoded() + { + return static::decodeData($this); + } + + /** + * Return a tenant array with data decoded into separate keys. + * + * @param Tenant|array $tenant + * @return array + */ + public static function decodeData($tenant) + { + $tenant = $tenant instanceof self ? (array) $tenant->attributes : $tenant; + $decoded = json_decode($tenant[$dataColumn = static::dataColumn()], true); + + foreach ($decoded as $key => $value) { + $tenant[$key] = $value; + } + + // If $tenant[$dataColumn] has been overriden by a value, don't delete the key. + if (! array_key_exists($dataColumn, $decoded)) { + unset($tenant[$dataColumn]); + } + + return $tenant; + } + + public function getFromData(string $key) + { + $this->dataArray = $this->dataArray ?? json_decode($this->{$this->dataColumn()}, true); + + return $this->dataArray[$key] ?? null; + } + + public function get(string $key) + { + return $this->$key ?? $this->getFromData($key) ?? null; + } + + /** @todo In v2, this should return an associative array. */ + public function getMany(array $keys): array + { + return array_map([$this, 'get'], $keys); + } + + public function put(string $key, $value) + { + if (array_key_exists($key, $this->customColumns())) { + $this->update([$key => $value]); + } else { + $obj = json_decode($this->{$this->dataColumn()}); + $obj->$key = $value; + + $this->update([$this->dataColumn() => json_encode($obj)]); + } + + return $value; + } +} diff --git a/src/TenantManager.php b/src/TenantManager.php index 9d5531bb..37c9e716 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -23,7 +23,7 @@ final class TenantManager * * @var StorageDriver */ - protected $storage; + public $storage; /** * Database manager. @@ -86,7 +86,10 @@ final class TenantManager throw new \Exception("Domain $domain is already occupied by tenant $id."); } - $tenant = $this->jsonDecodeArrayValues($this->storage->createTenant($domain, (string) \Webpatser\Uuid\Uuid::generate(1, $domain))); + $tenant = $this->storage->createTenant($domain, (string) \Webpatser\Uuid\Uuid::generate(1, $domain)); + if ($this->useJson()) { + $tenant = $this->jsonDecodeArrayValues($tenant); + } if ($data) { $this->put($data, null, $tenant['uuid']); @@ -115,7 +118,12 @@ final class TenantManager { $fields = (array) $fields; - return $this->jsonDecodeArrayValues($this->storage->getTenantById($uuid, $fields)); + $tenant = $this->storage->getTenantById($uuid, $fields); + if ($this->useJson()) { + $tenant = $this->jsonDecodeArrayValues($tenant); + } + + return $tenant; } /** @@ -200,7 +208,9 @@ final class TenantManager */ public function setTenant(array $tenant): array { - $tenant = $this->jsonDecodeArrayValues($tenant); + if ($this->useJson()) { + $tenant = $this->jsonDecodeArrayValues($tenant); + } $this->tenant = $tenant; @@ -227,10 +237,15 @@ final class TenantManager public function all($uuids = []) { $uuids = (array) $uuids; + $tenants = $this->storage->getAllTenants($uuids); - return collect(array_map(function ($tenant_array) { - return $this->jsonDecodeArrayValues($tenant_array); - }, $this->storage->getAllTenants($uuids))); + if ($this->useJson()) { + $tenants = array_map(function ($tenant_array) { + return $this->jsonDecodeArrayValues($tenant_array); + }, $tenants); + } + + return collect($tenants); } /** @@ -336,6 +351,15 @@ final class TenantManager return $array; } + public function useJson() + { + if (property_exists($this->storage, 'useJson') && $this->storage->useJson === false) { + return false; + } + + return true; + } + /** * Return the identified tenant's attribute(s). * diff --git a/test b/test index ff55e60b..822b6d4b 100755 --- a/test +++ b/test @@ -4,7 +4,9 @@ set -e # for development docker-compose up -d printf "Variant 1\n\n" -TENANCY_TEST_REDIS_TENANCY=1 TENANCY_TEST_REDIS_CLIENT=phpredis docker-compose exec test vendor/bin/phpunit --coverage-php coverage/1.cov "$@" +docker-compose exec test env TENANCY_TEST_REDIS_TENANCY=1 TENANCY_TEST_REDIS_CLIENT=phpredis TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/1.cov "$@" printf "Variant 2\n\n" -TENANCY_TEST_REDIS_TENANCY=0 TENANCY_TEST_REDIS_CLIENT=predis docker-compose exec test vendor/bin/phpunit --coverage-php coverage/2.cov "$@" +docker-compose exec test env TENANCY_TEST_REDIS_TENANCY=0 TENANCY_TEST_REDIS_CLIENT=predis TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/2.cov "$@" +printf "Variant 3\n\n" +docker-compose exec test env TENANCY_TEST_REDIS_TENANCY=1 TENANCY_TEST_REDIS_CLIENT=phpredis TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/3.cov "$@" docker-compose exec test vendor/bin/phpcov merge --clover clover.xml coverage/ diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index df90a21b..6d2a8e41 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -210,6 +210,7 @@ class Kernel extends HttpKernel ->expectsQuestion('Do you want to publish the default database migration?', 'yes'); $this->assertFileExists(base_path('routes/tenant.php')); $this->assertFileExists(base_path('config/tenancy.php')); + $this->assertFileExists(database_path('migrations/2019_08_08_000000_create_tenants_table.php')); $this->assertSame("assertSame($value, tenancy()->put($value)); } + + /** @test */ + public function correct_storage_driver_is_used() + { + if (config('tenancy.storage_driver') == DatabaseStorageDriver::class) { + $this->assertSame('DatabaseStorageDriver', class_basename(tenancy()->storage)); + } elseif (config('tenancy.storage_driver') == RedisStorageDriver::class) { + $this->assertSame('RedisStorageDriver', class_basename(tenancy()->storage)); + } + } + + /** @test */ + public function data_is_stored_with_correct_data_types() + { + tenancy()->put('someBool', false); + $this->assertSame('boolean', gettype(tenancy()->get('someBool'))); + + tenancy()->put('someInt', 5); + $this->assertSame('integer', gettype(tenancy()->get('someInt'))); + + tenancy()->put('someDouble', 11.40); + $this->assertSame('double', gettype(tenancy()->get('someDouble'))); + + tenancy()->put('string', 'foo'); + $this->assertSame('string', gettype(tenancy()->get('string'))); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 0183cc5a..9cafb202 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,19 +3,14 @@ namespace Stancl\Tenancy\Tests; use Illuminate\Support\Facades\Redis; +use Stancl\Tenancy\StorageDrivers\RedisStorageDriver; +use Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver; abstract class TestCase extends \Orchestra\Testbench\TestCase { public $autoCreateTenant = true; public $autoInitTenancy = true; - private function checkRequirements(): void - { - parent::checkRequirements(); - - dd($this->getAnnotations()); - } - /** * Setup the test environment. * @@ -28,6 +23,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase Redis::connection('tenancy')->flushdb(); Redis::connection('cache')->flushdb(); + $this->loadMigrationsFrom([ + '--path' => realpath(__DIR__ . '/../assets/migrations'), + '--database' => 'central', + ]); + config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom + if ($this->autoCreateTenant) { $this->createTenant(); } @@ -37,6 +38,13 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase } } + protected function tearDown(): void + { + // config(['database.default' => 'central']); + + parent::tearDown(); + } + public function createTenant($domain = 'localhost') { tenant()->create($domain); @@ -59,6 +67,8 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); } + fclose(fopen(database_path('central.sqlite'), 'w')); + $app['config']->set([ 'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), 'database.redis.default.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), @@ -72,6 +82,10 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'database' => env('TENANCY_TEST_REDIS_DB', 14), 'prefix' => 'abc', // todo unrelated to tenancy, but this doesn't seem to have an effect? try to replicate in a fresh laravel installation ], + 'database.connections.central' => [ + 'driver' => 'sqlite', + 'database' => database_path('central.sqlite'), + ], 'tenancy.database' => [ 'based_on' => 'sqlite', 'prefix' => 'tenant', @@ -90,11 +104,27 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'tenancy.redis.prefixed_connections' => ['default'], 'tenancy.migrations_directory' => database_path('../migrations'), ]); + + if (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'redis') { + $app['config']->set([ + 'tenancy.storage_driver' => RedisStorageDriver::class, + ]); + + tenancy()->storage = $app->make(RedisStorageDriver::class); + } elseif (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'db') { + $app['config']->set([ + 'tenancy.storage_driver' => DatabaseStorageDriver::class, + ]); + + tenancy()->storage = $app->make(DatabaseStorageDriver::class); + } } protected function getPackageProviders($app) { - return [\Stancl\Tenancy\TenancyServiceProvider::class]; + return [ + \Stancl\Tenancy\TenancyServiceProvider::class, + ]; } protected function getPackageAliases($app) From d63e15ec2ea37e5a67c1772278565a95b6daf7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 15:36:31 +0200 Subject: [PATCH 02/32] Add issue templates --- .github/ISSUE_TEMPLATE/---bug-report.md | 22 +++++++++++++++++++ .../ISSUE_TEMPLATE/---feature-suggestion.md | 14 ++++++++++++ .github/ISSUE_TEMPLATE/---support-question.md | 10 +++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 22 +++++++++++++++++++ .github/ISSUE_TEMPLATE/feature-suggestion.md | 14 ++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++++ 6 files changed, 102 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/---bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/---feature-suggestion.md create mode 100644 .github/ISSUE_TEMPLATE/---support-question.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature-suggestion.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 00000000..ce301e06 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,22 @@ +--- +name: "\U0001F41B Bug Report" +about: Report unexpected behavior with stancl/tenancy. +title: '' +labels: bug +assignees: stancl + +--- + +#### Describe the bug + + +#### Steps to reproduce + + +#### Expected behavior +A clear and concise description of what you expected to happen. + +#### Your setup + - Laravel version [e.g. 5.8] + - stancl/tenancy version [e.g. 22] + - Storage driver [e.g. Redis] diff --git a/.github/ISSUE_TEMPLATE/---feature-suggestion.md b/.github/ISSUE_TEMPLATE/---feature-suggestion.md new file mode 100644 index 00000000..537b20f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---feature-suggestion.md @@ -0,0 +1,14 @@ +--- +name: "\U0001F4A1 Feature Suggestion" +about: Suggest an idea for stancl/tenancy. +title: '' +labels: feature +assignees: stancl + +--- + +#### Description + + +#### Why this should be added + diff --git a/.github/ISSUE_TEMPLATE/---support-question.md b/.github/ISSUE_TEMPLATE/---support-question.md new file mode 100644 index 00000000..76723c61 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---support-question.md @@ -0,0 +1,10 @@ +--- +name: "❤️ Support Question" +about: Ask for help with implementing stancl/tenancy. +title: '' +labels: support +assignees: stancl + +--- + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..34a0ed84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Report unexpected behavior with stancl/tenancy. +title: '' +labels: bug +assignees: stancl + +--- + +#### Describe the bug + + +#### Steps to reproduce + + +#### Expected behavior +A clear and concise description of what you expected to happen. + +#### Your setup + - Laravel version [e.g. 5.8] + - stancl/tenancy version [e.g. 22] + - Storage driver [e.g. Redis] diff --git a/.github/ISSUE_TEMPLATE/feature-suggestion.md b/.github/ISSUE_TEMPLATE/feature-suggestion.md new file mode 100644 index 00000000..233e06a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-suggestion.md @@ -0,0 +1,14 @@ +--- +name: Feature suggestion +about: Suggest an idea for stancl/tenancy. +title: '' +labels: feature +assignees: stancl + +--- + +#### Description + + +#### Why this should be added + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..7d9fa972 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: stancl + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 0f1da9e15b1dd76e3fa4b3bc6f26ac50357d59d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 15:39:07 +0200 Subject: [PATCH 03/32] update issue templates --- .../ISSUE_TEMPLATE/---feature-suggestion.md | 14 ------------ .../{---bug-report.md => bug-report.md} | 0 .github/ISSUE_TEMPLATE/bug_report.md | 22 ------------------- .github/ISSUE_TEMPLATE/feature-suggestion.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 20 ----------------- ...upport-question.md => support-question.md} | 0 6 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/---feature-suggestion.md rename .github/ISSUE_TEMPLATE/{---bug-report.md => bug-report.md} (100%) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md rename .github/ISSUE_TEMPLATE/{---support-question.md => support-question.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/---feature-suggestion.md b/.github/ISSUE_TEMPLATE/---feature-suggestion.md deleted file mode 100644 index 537b20f6..00000000 --- a/.github/ISSUE_TEMPLATE/---feature-suggestion.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: "\U0001F4A1 Feature Suggestion" -about: Suggest an idea for stancl/tenancy. -title: '' -labels: feature -assignees: stancl - ---- - -#### Description - - -#### Why this should be added - diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/---bug-report.md rename to .github/ISSUE_TEMPLATE/bug-report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 34a0ed84..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Bug report -about: Report unexpected behavior with stancl/tenancy. -title: '' -labels: bug -assignees: stancl - ---- - -#### Describe the bug - - -#### Steps to reproduce - - -#### Expected behavior -A clear and concise description of what you expected to happen. - -#### Your setup - - Laravel version [e.g. 5.8] - - stancl/tenancy version [e.g. 22] - - Storage driver [e.g. Redis] diff --git a/.github/ISSUE_TEMPLATE/feature-suggestion.md b/.github/ISSUE_TEMPLATE/feature-suggestion.md index 233e06a2..537b20f6 100644 --- a/.github/ISSUE_TEMPLATE/feature-suggestion.md +++ b/.github/ISSUE_TEMPLATE/feature-suggestion.md @@ -1,5 +1,5 @@ --- -name: Feature suggestion +name: "\U0001F4A1 Feature Suggestion" about: Suggest an idea for stancl/tenancy. title: '' labels: feature diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 7d9fa972..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: feature -assignees: stancl - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/---support-question.md b/.github/ISSUE_TEMPLATE/support-question.md similarity index 100% rename from .github/ISSUE_TEMPLATE/---support-question.md rename to .github/ISSUE_TEMPLATE/support-question.md From 5ba4eec417f06e3cd48133ae9aba22d082ea00cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 15:41:02 +0200 Subject: [PATCH 04/32] Update issue templates --- .github/ISSUE_TEMPLATE/---documentation-issue.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/---documentation-issue.md diff --git a/.github/ISSUE_TEMPLATE/---documentation-issue.md b/.github/ISSUE_TEMPLATE/---documentation-issue.md new file mode 100644 index 00000000..7fc7ec35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---documentation-issue.md @@ -0,0 +1,10 @@ +--- +name: "\U0001F4DA Documentation Issue" +about: Suggest how the documentation could be improved. +title: '' +labels: documentation +assignees: stancl + +--- + + From e7716f94a0e6a31c4384725354615dceb25994d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 15:41:40 +0200 Subject: [PATCH 05/32] Rename ---documentation-issue.md to documentation-issue.md --- .../{---documentation-issue.md => documentation-issue.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{---documentation-issue.md => documentation-issue.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/---documentation-issue.md b/.github/ISSUE_TEMPLATE/documentation-issue.md similarity index 100% rename from .github/ISSUE_TEMPLATE/---documentation-issue.md rename to .github/ISSUE_TEMPLATE/documentation-issue.md From 2fe464a1f9e8fc0006d79e9b3f3c96f7f21ffb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 16:01:39 +0200 Subject: [PATCH 06/32] Use only --- src/Tenant.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tenant.php b/src/Tenant.php index 2eb8039f..03fbeb71 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -77,7 +77,7 @@ class Tenant extends Model public function get(string $key) { - return $this->$key ?? $this->getFromData($key) ?? null; + return $this->attributes[$key] ?? $this->getFromData($key) ?? null; } /** @todo In v2, this should return an associative array. */ From 1216addd1fc06a67ce3cc64234cd06cbcbbf5dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 17:55:40 +0200 Subject: [PATCH 07/32] Improve get() --- src/TenantManager.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/TenantManager.php b/src/TenantManager.php index 37c9e716..1409a36f 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -273,6 +273,10 @@ final class TenantManager { $uuid = $uuid ?: $this->tenant['uuid']; + if (isset($this->tenant['uuid']) && $uuid === $this->tenant['uuid'] && isset($this->tenant[$key])) { + return $this->tenant[$key]; + } + if (\is_array($key)) { return $this->jsonDecodeArrayValues($this->storage->getMany($uuid, $key)); } @@ -372,6 +376,6 @@ final class TenantManager return $this->tenant; } - return $this->tenant[(string) $attribute]; + return $this->get((string) $attribute); } } From fd79e0aec32892551c0da4c9a5c06c6bd448a104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 18:04:54 +0200 Subject: [PATCH 08/32] Fix illegal offset --- src/TenantManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TenantManager.php b/src/TenantManager.php index 1409a36f..5c84bd89 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -273,7 +273,7 @@ final class TenantManager { $uuid = $uuid ?: $this->tenant['uuid']; - if (isset($this->tenant['uuid']) && $uuid === $this->tenant['uuid'] && isset($this->tenant[$key])) { + if (array_key_exists('uuid', $this->tenant) && $uuid === $this->tenant['uuid'] && array_key_exists($key, $this->tenant)) { return $this->tenant[$key]; } From 3c9ead6aacf628233a5d456db422c3c62a5f43ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 18:10:34 +0200 Subject: [PATCH 09/32] default value --- src/TenantManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TenantManager.php b/src/TenantManager.php index 5c84bd89..b07c93b1 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -37,7 +37,7 @@ final class TenantManager * * @var array */ - public $tenant; + public $tenant = []; public function __construct(Application $app, StorageDriver $storage, DatabaseManager $database) { From bbafc9d8e2a6656b13b35a9a0834f5e1eea0640f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 18:20:34 +0200 Subject: [PATCH 10/32] is_array check --- src/TenantManager.php | 3 ++- tests/TenantStorageTest.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TenantManager.php b/src/TenantManager.php index b07c93b1..8da49762 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -273,7 +273,8 @@ final class TenantManager { $uuid = $uuid ?: $this->tenant['uuid']; - if (array_key_exists('uuid', $this->tenant) && $uuid === $this->tenant['uuid'] && array_key_exists($key, $this->tenant)) { + if (array_key_exists('uuid', $this->tenant) && $uuid === $this->tenant['uuid'] && + array_key_exists($key, $this->tenant) && ! is_array($key)) { return $this->tenant[$key]; } diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php index 8904ed18..7999f37c 100644 --- a/tests/TenantStorageTest.php +++ b/tests/TenantStorageTest.php @@ -43,6 +43,7 @@ class TenantStorageTest extends TestCase $data = array_combine($keys, $vals); tenancy()->put($data); + dd(tenant()->get($keys)); $this->assertSame($vals, tenant()->get($keys)); } From 417330decd246e80462b0f4e4e9bead7ea1f608f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 18:21:53 +0200 Subject: [PATCH 11/32] Add native_function_invocation --- .styleci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.styleci.yml b/.styleci.yml index 2a6c92f2..a1b99be9 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,4 +1,6 @@ preset: laravel +enabled: +- native_function_invocation disabled: - concat_without_spaces - ternary_operator_spaces From 84890cdd1e239394567d3c609023c26b4dce497b Mon Sep 17 00:00:00 2001 From: stancl Date: Fri, 16 Aug 2019 16:21:59 +0000 Subject: [PATCH 12/32] Apply fixes from StyleCI --- src/CacheManager.php | 2 +- src/Commands/Install.php | 6 ++-- src/Commands/Run.php | 8 ++--- src/DatabaseManager.php | 4 +-- src/StorageDrivers/RedisStorageDriver.php | 14 ++++---- src/Tenant.php | 14 ++++---- .../SQLiteDatabaseManager.php | 4 +-- src/TenantManager.php | 32 +++++++++---------- src/TenantRouteServiceProvider.php | 2 +- src/Traits/BootstrapsTenancy.php | 2 +- src/Traits/HasATenantsOption.php | 2 +- src/Traits/TenantManagerEvents.php | 2 +- src/helpers.php | 6 ++-- tests/BootstrapsTenancyTest.php | 2 +- tests/CommandsTest.php | 10 +++--- tests/ReidentificationTest.php | 2 +- tests/TenantAssetTest.php | 6 ++-- tests/TenantDatabaseManagerTest.php | 4 +-- tests/TenantStorageTest.php | 14 ++++---- tests/TestCase.php | 10 +++--- 20 files changed, 73 insertions(+), 73 deletions(-) diff --git a/src/CacheManager.php b/src/CacheManager.php index d3b31ea2..52952fe7 100644 --- a/src/CacheManager.php +++ b/src/CacheManager.php @@ -18,7 +18,7 @@ class CacheManager extends BaseCacheManager $names = $parameters[0]; $names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/5.7/cache#removing-tagged-cache-items - return $this->store()->tags(array_merge($tags, $names)); + return $this->store()->tags(\array_merge($tags, $names)); } return $this->store()->tags($tags)->$method(...$parameters); diff --git a/src/Commands/Install.php b/src/Commands/Install.php index de1a81b2..a2f162e5 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -34,14 +34,14 @@ class Install extends Command ]); $this->info('✔️ Created config/tenancy.php'); - file_put_contents(app_path('Http/Kernel.php'), str_replace( + \file_put_contents(app_path('Http/Kernel.php'), \str_replace( 'protected $middlewarePriority = [', "protected \$middlewarePriority = [\n \Stancl\Tenancy\Middleware\InitializeTenancy::class,", - file_get_contents(app_path('Http/Kernel.php')) + \file_get_contents(app_path('Http/Kernel.php')) )); $this->info('✔️ Set middleware priority'); - file_put_contents(base_path('routes/tenant.php'), + \file_put_contents(base_path('routes/tenant.php'), " 'bar', 'abc' => 'xyz=zzz'] - $arguments = array_reduce($this->option('argument'), $callback(), []); + $arguments = \array_reduce($this->option('argument'), $callback(), []); // Turns ['foo=bar', 'abc=xyz=zzz'] into ['--foo' => 'bar', '--abc' => 'xyz=zzz'] - $options = array_reduce($this->option('option'), $callback('--'), []); + $options = \array_reduce($this->option('option'), $callback('--'), []); // Run command - $this->call($this->argument('commandname'), array_merge($arguments, $options)); + $this->call($this->argument('commandname'), \array_merge($arguments, $options)); tenancy()->end(); }); diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 68e0cde0..b8152194 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -50,7 +50,7 @@ final class DatabaseManager $databaseManagers = config('tenancy.database_managers'); - if (! array_key_exists($driver, $databaseManagers)) { + if (! \array_key_exists($driver, $databaseManagers)) { throw new \Exception("Database could not be created: no database manager for driver $driver is registered."); } @@ -76,7 +76,7 @@ final class DatabaseManager $databaseManagers = config('tenancy.database_managers'); - if (! array_key_exists($driver, $databaseManagers)) { + if (! \array_key_exists($driver, $databaseManagers)) { throw new \Exception("Database could not be deleted: no database manager for driver $driver is registered."); } diff --git a/src/StorageDrivers/RedisStorageDriver.php b/src/StorageDrivers/RedisStorageDriver.php index 63c087af..9866018c 100644 --- a/src/StorageDrivers/RedisStorageDriver.php +++ b/src/StorageDrivers/RedisStorageDriver.php @@ -37,7 +37,7 @@ class RedisStorageDriver implements StorageDriver return $this->redis->hgetall("tenants:$uuid"); } - return array_combine($fields, $this->redis->hmget("tenants:$uuid", $fields)); + return \array_combine($fields, $this->redis->hmget("tenants:$uuid", $fields)); } public function getTenantIdByDomain(string $domain): ?string @@ -48,7 +48,7 @@ class RedisStorageDriver implements StorageDriver public function createTenant(string $domain, string $uuid): array { $this->redis->hmset("domains:$domain", 'tenant_id', $uuid); - $this->redis->hmset("tenants:$uuid", 'uuid', json_encode($uuid), 'domain', json_encode($domain)); + $this->redis->hmset("tenants:$uuid", 'uuid', \json_encode($uuid), 'domain', \json_encode($domain)); return $this->redis->hgetall("tenants:$uuid"); } @@ -63,7 +63,7 @@ class RedisStorageDriver implements StorageDriver public function deleteTenant(string $id): bool { try { - $domain = json_decode($this->getTenantById($id)['domain']); + $domain = \json_decode($this->getTenantById($id)['domain']); } catch (\Throwable $th) { throw new \Exception("No tenant with UUID $id exists."); } @@ -75,7 +75,7 @@ class RedisStorageDriver implements StorageDriver public function getAllTenants(array $uuids = []): array { - $hashes = array_map(function ($hash) { + $hashes = \array_map(function ($hash) { return "tenants:{$hash}"; }, $uuids); @@ -91,13 +91,13 @@ class RedisStorageDriver implements StorageDriver $all_keys = $this->redis->scan(null, 'MATCH', $redis_prefix . 'tenants:*')[1]; } - $hashes = array_map(function ($key) use ($redis_prefix) { + $hashes = \array_map(function ($key) use ($redis_prefix) { // Left strip $redis_prefix from $key - return substr($key, strlen($redis_prefix)); + return \substr($key, \strlen($redis_prefix)); }, $all_keys); } - return array_map(function ($tenant) { + return \array_map(function ($tenant) { return $this->redis->hgetall($tenant); }, $hashes); } diff --git a/src/Tenant.php b/src/Tenant.php index 03fbeb71..db9997d0 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -54,14 +54,14 @@ class Tenant extends Model public static function decodeData($tenant) { $tenant = $tenant instanceof self ? (array) $tenant->attributes : $tenant; - $decoded = json_decode($tenant[$dataColumn = static::dataColumn()], true); + $decoded = \json_decode($tenant[$dataColumn = static::dataColumn()], true); foreach ($decoded as $key => $value) { $tenant[$key] = $value; } // If $tenant[$dataColumn] has been overriden by a value, don't delete the key. - if (! array_key_exists($dataColumn, $decoded)) { + if (! \array_key_exists($dataColumn, $decoded)) { unset($tenant[$dataColumn]); } @@ -70,7 +70,7 @@ class Tenant extends Model public function getFromData(string $key) { - $this->dataArray = $this->dataArray ?? json_decode($this->{$this->dataColumn()}, true); + $this->dataArray = $this->dataArray ?? \json_decode($this->{$this->dataColumn()}, true); return $this->dataArray[$key] ?? null; } @@ -83,18 +83,18 @@ class Tenant extends Model /** @todo In v2, this should return an associative array. */ public function getMany(array $keys): array { - return array_map([$this, 'get'], $keys); + return \array_map([$this, 'get'], $keys); } public function put(string $key, $value) { - if (array_key_exists($key, $this->customColumns())) { + if (\array_key_exists($key, $this->customColumns())) { $this->update([$key => $value]); } else { - $obj = json_decode($this->{$this->dataColumn()}); + $obj = \json_decode($this->{$this->dataColumn()}); $obj->$key = $value; - $this->update([$this->dataColumn() => json_encode($obj)]); + $this->update([$this->dataColumn() => \json_encode($obj)]); } return $value; diff --git a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php index ca6d027a..cdfc094b 100644 --- a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php +++ b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php @@ -9,7 +9,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager public function createDatabase(string $name): bool { try { - return fclose(fopen(database_path($name), 'w')); + return \fclose(\fopen(database_path($name), 'w')); } catch (\Throwable $th) { return false; } @@ -18,7 +18,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager public function deleteDatabase(string $name): bool { try { - return unlink(database_path($name)); + return \unlink(database_path($name)); } catch (\Throwable $th) { return false; } diff --git a/src/TenantManager.php b/src/TenantManager.php index 8da49762..6e27ee1a 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -64,7 +64,7 @@ final class TenantManager $tenant = $this->storage->identifyTenant($domain); - if (! $tenant || ! array_key_exists('uuid', $tenant) || ! $tenant['uuid']) { + if (! $tenant || ! \array_key_exists('uuid', $tenant) || ! $tenant['uuid']) { throw new \Exception("Tenant could not be identified on domain {$domain}."); } @@ -94,7 +94,7 @@ final class TenantManager if ($data) { $this->put($data, null, $tenant['uuid']); - $tenant = array_merge($tenant, $data); + $tenant = \array_merge($tenant, $data); } $this->database->create($this->getDatabaseName($tenant)); @@ -175,7 +175,7 @@ final class TenantManager $uuid = $this->getIdByDomain($domain); - if (is_null($uuid)) { + if (\is_null($uuid)) { throw new \Exception("Tenant with domain $domain could not be identified."); } @@ -240,7 +240,7 @@ final class TenantManager $tenants = $this->storage->getAllTenants($uuids); if ($this->useJson()) { - $tenants = array_map(function ($tenant_array) { + $tenants = \array_map(function ($tenant_array) { return $this->jsonDecodeArrayValues($tenant_array); }, $tenants); } @@ -273,8 +273,8 @@ final class TenantManager { $uuid = $uuid ?: $this->tenant['uuid']; - if (array_key_exists('uuid', $this->tenant) && $uuid === $this->tenant['uuid'] && - array_key_exists($key, $this->tenant) && ! is_array($key)) { + if (\array_key_exists('uuid', $this->tenant) && $uuid === $this->tenant['uuid'] && + \array_key_exists($key, $this->tenant) && ! \is_array($key)) { return $this->tenant[$key]; } @@ -282,7 +282,7 @@ final class TenantManager return $this->jsonDecodeArrayValues($this->storage->getMany($uuid, $key)); } - return json_decode($this->storage->get($uuid, $key), true); + return \json_decode($this->storage->get($uuid, $key), true); } /** @@ -295,10 +295,10 @@ final class TenantManager */ public function put($key, $value = null, string $uuid = null) { - if (in_array($key, ['uuid', 'domain'], true) || ( - is_array($key) && ( - in_array('uuid', array_keys($key), true) || - in_array('domain', array_keys($key), true) + if (\in_array($key, ['uuid', 'domain'], true) || ( + \is_array($key) && ( + \in_array('uuid', \array_keys($key), true) || + \in_array('domain', \array_keys($key), true) ) )) { throw new CannotChangeUuidOrDomainException; @@ -319,7 +319,7 @@ final class TenantManager } if (! \is_null($value)) { - return $target[$key] = json_decode($this->storage->put($uuid, $key, json_encode($value)), true); + return $target[$key] = \json_decode($this->storage->put($uuid, $key, \json_encode($value)), true); } if (! \is_array($key)) { @@ -328,7 +328,7 @@ final class TenantManager foreach ($key as $k => $v) { $target[$k] = $v; - $key[$k] = json_encode($v); + $key[$k] = \json_encode($v); } return $this->jsonDecodeArrayValues($this->storage->putMany($uuid, $key)); @@ -349,8 +349,8 @@ final class TenantManager protected function jsonDecodeArrayValues(array $array) { - array_walk($array, function (&$value, $key) { - $value = json_decode($value, true); + \array_walk($array, function (&$value, $key) { + $value = \json_decode($value, true); }); return $array; @@ -358,7 +358,7 @@ final class TenantManager public function useJson() { - if (property_exists($this->storage, 'useJson') && $this->storage->useJson === false) { + if (\property_exists($this->storage, 'useJson') && $this->storage->useJson === false) { return false; } diff --git a/src/TenantRouteServiceProvider.php b/src/TenantRouteServiceProvider.php index 4ea77402..e6551756 100644 --- a/src/TenantRouteServiceProvider.php +++ b/src/TenantRouteServiceProvider.php @@ -10,7 +10,7 @@ class TenantRouteServiceProvider extends RouteServiceProvider public function map() { if (! \in_array(request()->getHost(), $this->app['config']['tenancy.exempt_domains'] ?? []) - && file_exists(base_path('routes/tenant.php'))) { + && \file_exists(base_path('routes/tenant.php'))) { Route::middleware(['web', 'tenancy']) ->namespace($this->app['config']['tenant_route_namespace'] ?? 'App\Http\Controllers') ->group(base_path('routes/tenant.php')); diff --git a/src/Traits/BootstrapsTenancy.php b/src/Traits/BootstrapsTenancy.php index b7188968..5e45c3bc 100644 --- a/src/Traits/BootstrapsTenancy.php +++ b/src/Traits/BootstrapsTenancy.php @@ -138,7 +138,7 @@ trait BootstrapsTenancy foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { $old['disks'][$disk] = Storage::disk($disk)->getAdapter()->getPathPrefix(); - if ($root = str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) { + if ($root = \str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) { Storage::disk($disk)->getAdapter()->setPathPrefix($root); } else { $root = $this->app['config']["filesystems.disks.{$disk}.root"]; diff --git a/src/Traits/HasATenantsOption.php b/src/Traits/HasATenantsOption.php index 1dc055af..1c37cfe1 100644 --- a/src/Traits/HasATenantsOption.php +++ b/src/Traits/HasATenantsOption.php @@ -8,7 +8,7 @@ trait HasATenantsOption { protected function getOptions() { - return array_merge([ + return \array_merge([ ['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null], ], parent::getOptions()); } diff --git a/src/Traits/TenantManagerEvents.php b/src/Traits/TenantManagerEvents.php index 416ebd72..38e303a0 100644 --- a/src/Traits/TenantManagerEvents.php +++ b/src/Traits/TenantManagerEvents.php @@ -78,7 +78,7 @@ trait TenantManagerEvents */ public function event(string $name): Collection { - return array_reduce($this->listeners[$name], function ($prevents, $listener) { + return \array_reduce($this->listeners[$name], function ($prevents, $listener) { return $prevents->merge($listener($this) ?? []); }, collect([])); } diff --git a/src/helpers.php b/src/helpers.php index 0041f131..29cb3411 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -2,7 +2,7 @@ use Stancl\Tenancy\TenantManager; -if (! function_exists('tenancy')) { +if (! \function_exists('tenancy')) { function tenancy($key = null) { if ($key) { @@ -13,14 +13,14 @@ if (! function_exists('tenancy')) { } } -if (! function_exists('tenant')) { +if (! \function_exists('tenant')) { function tenant($key = null) { return tenancy($key); } } -if (! function_exists('tenant_asset')) { +if (! \function_exists('tenant_asset')) { function tenant_asset($asset) { return route('stancl.tenancy.asset', ['asset' => $asset]); diff --git a/tests/BootstrapsTenancyTest.php b/tests/BootstrapsTenancyTest.php index fc0d761f..1f21b858 100644 --- a/tests/BootstrapsTenancyTest.php +++ b/tests/BootstrapsTenancyTest.php @@ -86,7 +86,7 @@ class BootstrapsTenancyTest extends TestCase $current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix(); if ($override = config("tenancy.filesystem.root_override.{$disk}")) { - $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override); + $correct_path_prefix = \str_replace('%storage_path%', storage_path(), $override); } else { if ($base = $old_storage_facade_roots[$disk]) { $correct_path_prefix = $base . "/$suffix/"; diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 6d2a8e41..6b161738 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -117,14 +117,14 @@ class CommandsTest extends TestCase /** @test */ public function install_command_works() { - if (! is_dir($dir = app_path('Http'))) { - mkdir($dir, 0777, true); + if (! \is_dir($dir = app_path('Http'))) { + \mkdir($dir, 0777, true); } - if (! is_dir($dir = base_path('routes'))) { - mkdir($dir, 0777, true); + if (! \is_dir($dir = base_path('routes'))) { + \mkdir($dir, 0777, true); } - file_put_contents(app_path('Http/Kernel.php'), "getAdapter()->getPathPrefix(); if ($override = config("tenancy.filesystem.root_override.{$disk}")) { - $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override); + $correct_path_prefix = \str_replace('%storage_path%', storage_path(), $override); } else { if ($base = $originals[$disk]) { $correct_path_prefix = $base . "/$suffix/"; diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 01f1fd28..b43d43c6 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -16,9 +16,9 @@ class TenantAssetTest extends TestCase $this->get(tenant_asset($filename))->assertSuccessful(); $this->assertFileExists($path); - $f = fopen($path, 'r'); - $content = fread($f, filesize($path)); - fclose($f); + $f = \fopen($path, 'r'); + $content = \fread($f, \filesize($path)); + \fclose($f); $this->assertSame('bar', $content); } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 9f6e911b..533c6eee 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -87,7 +87,7 @@ class TenantDatabaseManagerTest extends TestCase config()->set('database.default', 'pgsql'); - $db_name = strtolower('testdatabase' . $this->randomString(10)); + $db_name = \strtolower('testdatabase' . $this->randomString(10)); $this->assertTrue(app(DatabaseManager::class)->create($db_name, 'pgsql')); $this->assertNotEmpty(DB::select("SELECT datname FROM pg_database WHERE datname = '$db_name'")); @@ -104,7 +104,7 @@ class TenantDatabaseManagerTest extends TestCase config()->set('database.default', 'pgsql'); - $db_name = strtolower('testdatabase' . $this->randomString(10)); + $db_name = \strtolower('testdatabase' . $this->randomString(10)); $databaseManagers = config('tenancy.database_managers'); $job = new QueuedTenantDatabaseCreator(app($databaseManagers['pgsql']), $db_name); diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php index 7999f37c..79001bdd 100644 --- a/tests/TenantStorageTest.php +++ b/tests/TenantStorageTest.php @@ -40,7 +40,7 @@ class TenantStorageTest extends TestCase { $keys = ['foo', 'abc']; $vals = ['bar', 'xyz']; - $data = array_combine($keys, $vals); + $data = \array_combine($keys, $vals); tenancy()->put($data); dd(tenant()->get($keys)); @@ -76,13 +76,13 @@ class TenantStorageTest extends TestCase $keys = ['foo', 'abc']; $vals = ['bar', 'xyz']; - $data = array_combine($keys, $vals); + $data = \array_combine($keys, $vals); tenancy()->put($data, null, $uuid); $this->assertSame($vals, tenancy()->get($keys, $uuid)); $this->assertNotSame($vals, tenancy()->get($keys)); - $this->assertFalse(array_intersect($data, tenant()->tenant) == $data); // assert array not subset + $this->assertFalse(\array_intersect($data, tenant()->tenant) == $data); // assert array not subset } /** @test */ @@ -130,15 +130,15 @@ class TenantStorageTest extends TestCase public function data_is_stored_with_correct_data_types() { tenancy()->put('someBool', false); - $this->assertSame('boolean', gettype(tenancy()->get('someBool'))); + $this->assertSame('boolean', \gettype(tenancy()->get('someBool'))); tenancy()->put('someInt', 5); - $this->assertSame('integer', gettype(tenancy()->get('someInt'))); + $this->assertSame('integer', \gettype(tenancy()->get('someInt'))); tenancy()->put('someDouble', 11.40); - $this->assertSame('double', gettype(tenancy()->get('someDouble'))); + $this->assertSame('double', \gettype(tenancy()->get('someDouble'))); tenancy()->put('string', 'foo'); - $this->assertSame('string', gettype(tenancy()->get('string'))); + $this->assertSame('string', \gettype(tenancy()->get('string'))); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 9cafb202..b4f39982 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,7 +24,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase Redis::connection('cache')->flushdb(); $this->loadMigrationsFrom([ - '--path' => realpath(__DIR__ . '/../assets/migrations'), + '--path' => \realpath(__DIR__ . '/../assets/migrations'), '--database' => 'central', ]); config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom @@ -63,11 +63,11 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase */ protected function getEnvironmentSetUp($app) { - if (file_exists(__DIR__ . '/../.env')) { + if (\file_exists(__DIR__ . '/../.env')) { \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); } - fclose(fopen(database_path('central.sqlite'), 'w')); + \fclose(\fopen(database_path('central.sqlite'), 'w')); $app['config']->set([ 'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), @@ -160,7 +160,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase public function randomString(int $length = 10) { - return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length); + return \substr(\str_shuffle(\str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', \ceil($length / \strlen($x)))), 1, $length); } public function isContainerized() @@ -170,6 +170,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase public function assertArrayIsSubset($subset, $array, string $message = ''): void { - parent::assertTrue(array_intersect($subset, $array) == $subset, $message); + parent::assertTrue(\array_intersect($subset, $array) == $subset, $message); } } From 89c7289b66f29fc8bbc63457133896f372d380a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 18:26:18 +0200 Subject: [PATCH 13/32] Switch order of conditions --- src/TenantManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TenantManager.php b/src/TenantManager.php index 8da49762..fdd077f8 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -274,7 +274,7 @@ final class TenantManager $uuid = $uuid ?: $this->tenant['uuid']; if (array_key_exists('uuid', $this->tenant) && $uuid === $this->tenant['uuid'] && - array_key_exists($key, $this->tenant) && ! is_array($key)) { + ! is_array($key) && array_key_exists($key, $this->tenant)) { return $this->tenant[$key]; } From 6944a46d4c25ddc4f9b1d0a6dad181097f4d77c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 18:41:49 +0200 Subject: [PATCH 14/32] remove dd --- tests/TenantStorageTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php index 79001bdd..806e19b0 100644 --- a/tests/TenantStorageTest.php +++ b/tests/TenantStorageTest.php @@ -43,7 +43,6 @@ class TenantStorageTest extends TestCase $data = \array_combine($keys, $vals); tenancy()->put($data); - dd(tenant()->get($keys)); $this->assertSame($vals, tenant()->get($keys)); } From 609569635525782fb642ba533f195fbf5438f3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 16 Aug 2019 18:54:25 +0200 Subject: [PATCH 15/32] Fix invoke test --- src/TenantManager.php | 1 + tests/TenantManagerTest.php | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/TenantManager.php b/src/TenantManager.php index beeb641d..5c10d80c 100644 --- a/src/TenantManager.php +++ b/src/TenantManager.php @@ -370,6 +370,7 @@ final class TenantManager * * @param string $attribute * @return mixed + * @todo Deprecate this in v2. */ public function __invoke($attribute) { diff --git a/tests/TenantManagerTest.php b/tests/TenantManagerTest.php index 453730ea..112e66b5 100644 --- a/tests/TenantManagerTest.php +++ b/tests/TenantManagerTest.php @@ -24,6 +24,9 @@ class TenantManagerTest extends TestCase /** @test */ public function invoke_works() { + tenant()->create('foo.localhost'); + tenancy()->init('foo.localhost'); + $this->assertSame(tenant('uuid'), tenant()('uuid')); } From 6028e70b98a810533a81227dd0bfdfd62ba56f29 Mon Sep 17 00:00:00 2001 From: stancl Date: Fri, 16 Aug 2019 16:54:34 +0000 Subject: [PATCH 16/32] Apply fixes from StyleCI --- tests/TenantManagerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TenantManagerTest.php b/tests/TenantManagerTest.php index 112e66b5..73bcf0fe 100644 --- a/tests/TenantManagerTest.php +++ b/tests/TenantManagerTest.php @@ -26,7 +26,7 @@ class TenantManagerTest extends TestCase { tenant()->create('foo.localhost'); tenancy()->init('foo.localhost'); - + $this->assertSame(tenant('uuid'), tenant()('uuid')); } From 7bcad4718664944c974c1cbfbdb323f95b9af1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 10:03:41 +0200 Subject: [PATCH 17/32] Updated comment --- src/DatabaseManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index b8152194..df5806ed 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -94,7 +94,7 @@ final class DatabaseManager public function createTenantConnection(string $database_name) { - // Create the `tenancy` database connection. + // Create the `tenant` database connection. $based_on = config('tenancy.database.based_on') ?: config('database.default'); config()->set([ 'database.connections.tenant' => config('database.connections.' . $based_on), From 40fa69932b9487325325c8af9650b656d5ac8998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 14:11:49 +0200 Subject: [PATCH 18/32] [1.7.0] Fix Redis scan (#97) * Fix Redis scan * Fix keys issue --- src/StorageDrivers/RedisStorageDriver.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/StorageDrivers/RedisStorageDriver.php b/src/StorageDrivers/RedisStorageDriver.php index 9866018c..b2e4ab44 100644 --- a/src/StorageDrivers/RedisStorageDriver.php +++ b/src/StorageDrivers/RedisStorageDriver.php @@ -86,11 +86,10 @@ class RedisStorageDriver implements StorageDriver if (config('database.redis.client') === 'phpredis') { $redis_prefix = $this->redis->getOption($this->redis->client()::OPT_PREFIX) ?? $redis_prefix; - $all_keys = $this->redis->scan(null, $redis_prefix . 'tenants:*'); - } else { - $all_keys = $this->redis->scan(null, 'MATCH', $redis_prefix . 'tenants:*')[1]; } + $all_keys = $this->redis->keys('tenants:*'); + $hashes = \array_map(function ($key) use ($redis_prefix) { // Left strip $redis_prefix from $key return \substr($key, \strlen($redis_prefix)); From 8d92d7481ee696260c136ef510b173788bde226b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 14:37:39 +0200 Subject: [PATCH 19/32] Remove default JSON value Fails on MySQL --- assets/migrations/2019_08_08_000000_create_tenants_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/migrations/2019_08_08_000000_create_tenants_table.php b/assets/migrations/2019_08_08_000000_create_tenants_table.php index fc07702e..35158a88 100644 --- a/assets/migrations/2019_08_08_000000_create_tenants_table.php +++ b/assets/migrations/2019_08_08_000000_create_tenants_table.php @@ -19,7 +19,7 @@ class CreateTenantsTable extends Migration // your indexed columns go here - $table->json('data')->default('{}'); + $table->json('data'); }); } From 542fc888bca6eb047bcf7e5b107786c4bb0f8926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 14:41:33 +0200 Subject: [PATCH 20/32] Set 'data' to '{}' --- src/StorageDrivers/DatabaseStorageDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StorageDrivers/DatabaseStorageDriver.php b/src/StorageDrivers/DatabaseStorageDriver.php index faaa9f16..377a7046 100644 --- a/src/StorageDrivers/DatabaseStorageDriver.php +++ b/src/StorageDrivers/DatabaseStorageDriver.php @@ -45,7 +45,7 @@ class DatabaseStorageDriver implements StorageDriver public function createTenant(string $domain, string $uuid): array { - return Tenant::create(['uuid' => $uuid, 'domain' => $domain])->toArray(); + return Tenant::create(['uuid' => $uuid, 'domain' => $domain, 'data' => '{}'])->toArray(); } public function deleteTenant(string $id): bool From 86fd919adf4427e1cf2d23152e6efe5c5bce1551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 14:46:15 +0200 Subject: [PATCH 21/32] Except data --- src/StorageDrivers/DatabaseStorageDriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StorageDrivers/DatabaseStorageDriver.php b/src/StorageDrivers/DatabaseStorageDriver.php index 377a7046..3c6db731 100644 --- a/src/StorageDrivers/DatabaseStorageDriver.php +++ b/src/StorageDrivers/DatabaseStorageDriver.php @@ -45,7 +45,7 @@ class DatabaseStorageDriver implements StorageDriver public function createTenant(string $domain, string $uuid): array { - return Tenant::create(['uuid' => $uuid, 'domain' => $domain, 'data' => '{}'])->toArray(); + return Tenant::create(['uuid' => $uuid, 'domain' => $domain, 'data' => '{}'])->except('data')->toArray(); } public function deleteTenant(string $id): bool From 824d299dcd5fc4d14b865630fbb11e64bdfdd549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 14:51:03 +0200 Subject: [PATCH 22/32] wip --- src/StorageDrivers/DatabaseStorageDriver.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/StorageDrivers/DatabaseStorageDriver.php b/src/StorageDrivers/DatabaseStorageDriver.php index 3c6db731..fec1197a 100644 --- a/src/StorageDrivers/DatabaseStorageDriver.php +++ b/src/StorageDrivers/DatabaseStorageDriver.php @@ -45,7 +45,10 @@ class DatabaseStorageDriver implements StorageDriver public function createTenant(string $domain, string $uuid): array { - return Tenant::create(['uuid' => $uuid, 'domain' => $domain, 'data' => '{}'])->except('data')->toArray(); + $tenant = Tenant::create(['uuid' => $uuid, 'domain' => $domain, 'data' => '{}'])->toArray(); + unset($tenant['data']); + + return $tenant; } public function deleteTenant(string $id): bool From be077ae73c3be668e5ce07d462d1e9336e57dc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 15:25:39 +0200 Subject: [PATCH 23/32] Add the option to specify connection in DatabaseManager --- src/Commands/Migrate.php | 9 ++++++--- src/DatabaseManager.php | 28 +++++++++++++++++----------- tests/CommandsTest.php | 2 ++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 472c189e..bf8b0fd3 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -47,11 +47,14 @@ class Migrate extends MigrateCommand return; } - $this->input->setOption('database', 'tenant'); - tenant()->all($this->option('tenants'))->each(function ($tenant) { $this->line("Tenant: {$tenant['uuid']} ({$tenant['domain']})"); - $this->database->connectToTenant($tenant); + + // See Illuminate\Database\Migrations\DatabaseMigrationRepository::getConnection. + // Database connections are cached by Illuminate\Database\ConnectionResolver. + $connectionName = "tenant{$tenant['uuid']}"; + $this->input->setOption('database', $connectionName); + $this->database->connectToTenant($tenant, $connectionName); // Migrate parent::handle(); diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index df5806ed..d5ac7f57 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -9,6 +9,8 @@ use Illuminate\Database\DatabaseManager as BaseDatabaseManager; final class DatabaseManager { public $originalDefaultConnection; + + protected $defaultTenantConnectionName = 'tenant'; public function __construct(BaseDatabaseManager $database) { @@ -16,15 +18,15 @@ final class DatabaseManager $this->database = $database; } - public function connect(string $database) + public function connect(string $database, string $connectionName = null) { - $this->createTenantConnection($database); - $this->useConnection('tenant'); + $this->createTenantConnection($database, $connectionName); + $this->useConnection($connectionName); } - public function connectToTenant($tenant) + public function connectToTenant($tenant, string $connectionName = null) { - $this->connect(tenant()->getDatabaseName($tenant)); + $this->connect(tenant()->getDatabaseName($tenant), $connectionName); } public function disconnect() @@ -92,21 +94,25 @@ final class DatabaseManager return config('database.connections.tenant.driver'); } - public function createTenantConnection(string $database_name) + public function createTenantConnection(string $databaseName, string $connectionName = null) { - // Create the `tenant` database connection. + $connectionName = $connectionName ?: $this->defaultTenantConnectionName; + + // Create the database connection. $based_on = config('tenancy.database.based_on') ?: config('database.default'); config()->set([ - 'database.connections.tenant' => config('database.connections.' . $based_on), + "database.connections.$connectionName" => config('database.connections.' . $based_on), ]); // Change DB name - $database_name = $this->getDriver() === 'sqlite' ? database_path($database_name) : $database_name; - config()->set(['database.connections.tenant.database' => $database_name]); + $databaseName = $this->getDriver() === 'sqlite' ? database_path($databaseName) : $databaseName; + config()->set(["database.connections.$connectionName.database" => $databaseName]); } - public function useConnection(string $connection) + public function useConnection(string $connection = null) { + $connection = $connection ?: $this->defaultTenantConnectionName; + $this->database->setDefaultConnection($connection); $this->database->reconnect($connection); } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 6b161738..8f2825e7 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -114,6 +114,8 @@ class CommandsTest extends TestCase ->expectsOutput('xyz'); } + // todo check that multiple tenants can be migrated at once using all database engines + /** @test */ public function install_command_works() { From fd66537e7f759f25894a3903a87b8aa0141a7072 Mon Sep 17 00:00:00 2001 From: stancl Date: Sat, 17 Aug 2019 13:25:48 +0000 Subject: [PATCH 24/32] Apply fixes from StyleCI --- src/Commands/Migrate.php | 2 +- src/DatabaseManager.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index bf8b0fd3..11d885c3 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -49,7 +49,7 @@ class Migrate extends MigrateCommand tenant()->all($this->option('tenants'))->each(function ($tenant) { $this->line("Tenant: {$tenant['uuid']} ({$tenant['domain']})"); - + // See Illuminate\Database\Migrations\DatabaseMigrationRepository::getConnection. // Database connections are cached by Illuminate\Database\ConnectionResolver. $connectionName = "tenant{$tenant['uuid']}"; diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index d5ac7f57..c8514c9d 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -9,7 +9,7 @@ use Illuminate\Database\DatabaseManager as BaseDatabaseManager; final class DatabaseManager { public $originalDefaultConnection; - + protected $defaultTenantConnectionName = 'tenant'; public function __construct(BaseDatabaseManager $database) From 1fe722c4568a4d935e586b0c1b8645064e0b1f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 15:36:26 +0200 Subject: [PATCH 25/32] Move documentation to netlify --- README.md | 636 +----------------------------------------------------- 1 file changed, 3 insertions(+), 633 deletions(-) diff --git a/README.md b/README.md index 092d3210..aa5c0cf3 100644 --- a/README.md +++ b/README.md @@ -13,638 +13,8 @@ You won't have to change a thing in your application's code.\* - :heavy_check_mark: No replacing of Laravel classes (`Cache`, `Storage`, ...) with tenancy-aware classes - :heavy_check_mark: Built-in tenant identification based on hostname (including second level domains) -\* depending on how you use the filesystem. Be sure to read [that section](#filesystemstorage). Everything else will work out of the box. +\* depending on how you use the filesystem. Everything else will work out of the box. -## Table Of Contents +### [Documentation](https://stancl-tenancy.netlify.com/docs/) -
-Click to expand/collapse - -- [stancl/tenancy](#stancltenancy) - + [*A Laravel multi-database tenancy package that respects your code.*](#-a-laravel-multi-database-tenancy-package-that-respects-your-code-) -- [Installation](#installation) - + [Requirements](#requirements) - + [Installing the package](#installing-the-package) - + [Configuring the `InitializeTenancy` middleware](#configuring-the--initializetenancy--middleware) - + [Creating tenant routes](#creating-tenant-routes) - + [Publishing the configuration file](#publishing-the-configuration-file) - - [`exempt_domains`](#-exempt-domains-) - - [`database`](#-database-) - - [`redis`](#-redis-) - - [`cache`](#-cache-) - - [`filesystem`](#-filesystem-) -- [Usage](#usage) - * [Creating a Redis connection for storing tenancy-related data](#creating-a-redis-connection-for-storing-tenancy-related-data) - * [Obtaining a `TenantManager` instance](#obtaining-a--tenantmanager--instance) - + [Creating a new tenant](#creating-a-new-tenant) - + [Starting a session as a tenant](#starting-a-session-as-a-tenant) - + [Getting tenant information based on his UUID](#getting-tenant-information-based-on-his-uuid) - + [Getting tenant UUID based on his domain](#getting-tenant-uuid-based-on-his-domain) - + [Getting tenant information based on his domain](#getting-tenant-information-based-on-his-domain) - + [Getting current tenant information](#getting-current-tenant-information) - + [Listing all tenants](#listing-all-tenants) - + [Deleting a tenant](#deleting-a-tenant) - * [Storage driver](#storage-driver) - + [Storing custom data](#storing-custom-data) - * [Database](#database) - * [Redis](#redis) - * [Cache](#cache) - * [Filesystem/Storage](#filesystem-storage) - * [Artisan commands](#artisan-commands) - - [`tenants:list`](#-tenants-list-) - - [`tenants:migrate`, `tenants:rollback`, `tenants:seed`](#-tenants-migrate----tenants-rollback----tenants-seed-) - - [Running your commands for tenants](#running-your-commands-for-tenants) - + [Tenant migrations](#tenant-migrations) - * [Testing](#testing) -- [Tips](#tips) - * [HTTPS certificates](#https-certificates) - + [1. Use nginx with the lua module](#1-use-nginx-with-the-lua-module) - + [2. Add a simple server block for each tenant](#2-add-a-simple-server-block-for-each-tenant) - + [Generating certificates](#generating-certificates) -- [Development](#development) - * [Running tests](#running-tests) - + [With Docker](#with-docker) - + [Without Docker](#without-docker) - -
- - -## Stay updated - -If you'd like to be notified about new versions and related stuff, [sign up for e-mail notifications](http://eepurl.com/gyCnbf) or join our [Telegram channel](https://t.me/joinchat/AAAAAFjdrbSJg0ZCHTzxLA). - -# Installation - -> If you're installing this package for the first time, **there's also a [tutorial](https://stancl.github.io/blog/how-to-make-any-laravel-app-multi-tenant-in-5-minutes/).** - -### Requirements - -- Laravel 5.8 - -### Installing the package - -``` -composer require stancl/tenancy -``` - -This package follows [semantic versioning 2.0.0](https://semver.org). Each major release will have its own branch, so that bug fixes can be provided for older versions as well. - -### Configuring the `InitializeTenancy` middleware - -The `TenancyServiceProvider` automatically adds the `tenancy` middleware group which can be assigned to routes. You only need to make sure the middleware is top priority. - -Open `app/Http/Kernel.php` and make the middleware top priority, so that it gets executed before anything else, making sure things like the database switch connections soon enough. - -```php -protected $middlewarePriority = [ - \Stancl\Tenancy\Middleware\InitializeTenancy::class, - // ... -]; -``` - -When a tenant route is visited, but the tenant can't be identified, an exception is thrown. If you want to change this behavior, to a redirect for example, add this to your `app/Providers/AppServiceProvider.php`'s `boot()` method. - -```php -// use Stancl\Tenancy\Middleware\InitializeTenancy; - -$this->app->bind(InitializeTenancy::class, function ($app) { - return new InitializeTenancy(function ($exception) { - // redirect - }); -}); -``` - -### Creating tenant routes - -`Stancl\Tenancy\TenantRouteServiceProvider` maps tenant routes only if the current domain is not [exempt from tenancy](#exempt_domains). Tenant routes are loaded from `routes/tenant.php`. - -Rename the `routes/web.php` file to `routes/tenant.php`. This file will contain routes accessible only with tenancy. - -Create an empty `routes/web.php` file. This file will contain routes accessible without tenancy (such as the routes specific to the part of your app which creates tenants - landing page, sign up page, etc). - -### Publishing the configuration file - -```php -php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=config -``` - -You should see something along the lines of `Copied File [...] to [/config/tenancy.php]`. - -#### `exempt_domains` - -Domains listed in this array won't have tenant routes. - -For example, you can put the domain on which you have your landing page here. - -#### `database` - -Databases will be named like this: - -```php -config('tenancy.database.prefix') . $uuid . config('tenancy.database.suffix') -``` - -They will use a connection based on the connection specified using the `based_on` setting. Using `mysql` or `sqlite` is fine, but if you need to change more things than just the database name, you can create a new `tenant` connection and set `tenancy.database.based_on` to `tenant`. - -#### `redis` - -Keys will be prefixed with: - -```php -config('tenancy.redis.prefix_base') . $uuid -``` - -These changes will only apply for connections listed in `prefixed_connections`. - -You can enable Redis tenancy by changing the `tenancy.redis.tenancy` config to `true`. - -**Note: If you want Redis to be multi-tenant, you *must* use phpredis. Predis does not support prefixes.** - -If you're using Laravel 5.7, predis is not supported even if Redis tenancy is disabled. - -#### `cache` - -Cache keys will be tagged with a tag: - -```php -config('tenancy.cache.tag_base') . $uuid -``` - -#### `filesystem` - -Filesystem paths will be suffixed with: - -```php -config('tenancy.filesystem.suffix_base') . $uuid -``` - -These changes will only apply for disks listed in `disks`. - -You can see an example in the [Filesystem](#filesystemstorage) section of the documentation The `filesystem.root_override` section is explained there as well. - -# Usage - -## Creating a Redis connection for storing tenancy-related data - -Add an array like this to `database.redis` config: - -```php -'tenancy' => [ - 'host' => env('TENANCY_REDIS_HOST', '127.0.0.1'), - 'password' => env('TENANCY_REDIS_PASSWORD', null), - 'port' => env('TENANCY_REDIS_PORT', 6380), - 'database' => env('TENANCY_REDIS_DB', 3), -], -``` - -Note the different `database` number and the different port. - -A different port is used in this example, because if you use Redis for caching, you may want to run one instance with no persistence and another instance with persistence for tenancy-related data. If you want to run only one Redis instance, just make sure you use a different database number to avoid collisions. - -Read the [Storage driver](#storage-driver) section for more information. - -## Obtaining a `TenantManager` instance - -You can use the `tenancy()` and `tenant()` helpers to resolve `Stancl\Tenancy\TenantManager` out of the service container. These two helpers are exactly the same, the only reason there are two is nice syntax. `tenancy()->init()` sounds better than `tenant()->init()` and `tenant()->create()` sounds better than `tenancy()->create()`. **You may also use the `Tenancy` facade.** - -### Creating a new tenant - -```php ->>> tenant()->create('dev.localhost') -=> [ - "uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957", - "domain" => "dev.localhost", - ] -``` - -You can also put data into the storage during the tenant creation process: - -```php ->>> tenant()->create('dev.localhost', [ - 'plan' => 'basic' -]) -=> [ - "uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957", - "domain" => "dev.localhost", - "plan" => "basic", - ] -``` - -If you want to specify the tenant's database name, set the `tenancy.database_name_key` configuration key to the name of the key that is used to specify the database name in the tenant storage. You must use a name that you won't use for storing other data, so it's recommended to avoid names like `database` and use names like `_stancl_tenancy_database_name` instead. Then just give the key a value during the tenant creation process: - -```php ->>> tenant()->create('example.com', [ - '_stancl_tenancy_database_name' => 'example_com' -]) -=> [ - "uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957", - "domain" => "example.com", - "_stancl_tenancy_database_name" => "example_com", - ] -``` - -When you create a new tenant, you can [migrate](#tenant-migrations) their database like this: - -```php -\Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant['uuid']] -]); -``` - -You can also seed the database in the same way. The only difference is the command name (`tenants:seed`). - -### Starting a session as a tenant - -This switches the DB connection, prefixes Redis, changes filesystem root paths and tags cache. - -```php -tenancy()->init(); -// The domain will be autodetected unless specified as an argument -tenancy()->init('dev.localhost'); -``` - -### Getting tenant information based on his UUID - -You can use `find()`, which is an alias for `getTenantById()`. -You may use the second argument to specify the key(s) as a string/array. - -```php ->>> tenant()->getTenantById('dbe0b330-1a6e-11e9-b4c3-354da4b4f339'); -=> [ - "uuid" => "dbe0b330-1a6e-11e9-b4c3-354da4b4f339", - "domain" => "localhost", - "foo" => "bar", - ] ->>> tenant()->getTenantById('dbe0b330-1a6e-11e9-b4c3-354da4b4f339', 'foo'); -=> [ - "foo" => "bar", - ] ->>> tenant()->getTenantById('dbe0b330-1a6e-11e9-b4c3-354da4b4f339', ['foo', 'domain']); -=> [ - "foo" => "bar", - "domain" => "localhost", - ] -``` - -### Getting tenant UUID based on his domain - -```php ->>> tenant()->getTenantIdByDomain('localhost'); -=> "b3ce3f90-1a88-11e9-a6b0-038c6337ae50" ->>> tenant()->getIdByDomain('localhost'); -=> "b3ce3f90-1a88-11e9-a6b0-038c6337ae50" -``` - -### Getting tenant information based on his domain - -You may use the second argument to specify the key(s) as a string/array. - -```php ->>> tenant()->findByDomain('localhost'); -=> [ - "uuid" => "b3ce3f90-1a88-11e9-a6b0-038c6337ae50", - "domain" => "localhost", - ] -``` - -### Getting current tenant information - -You can access the public array `tenant` of `TenantManager` like this: - -```php -tenancy()->tenant -``` - -which returns an array. If you want to get the value of a specific key from the array, you can use one of the helpers with an argument --- the key on the `tenant` array. - -```php -tenant('uuid'); // Does the same thing as tenant()->tenant['uuid'] -``` - -### Listing all tenants - -```php ->>> tenant()->all(); -=> Illuminate\Support\Collection {#2980 - all: [ - [ - "uuid" => "32e20780-1a88-11e9-a051-4b6489a7edac", - "domain" => "localhost", - ], - [ - "uuid" => "49670df0-1a87-11e9-b7ba-cf5353777957", - "domain" => "dev.localhost", - ], - ], - } ->>> tenant()->all()->pluck('domain'); -=> Illuminate\Support\Collection {#2983 - all: [ - "localhost", - "dev.localhost", - ], - } -``` - -### Deleting a tenant - -```php ->>> tenant()->delete('dbe0b330-1a6e-11e9-b4c3-354da4b4f339'); -=> true ->>> tenant()->delete(tenant()->getTenantIdByDomain('dev.localhost')); -=> true ->>> tenant()->delete(tenant()->findByDomain('localhost')['uuid']); -=> true -``` - -Note that deleting a tenant doesn't delete his database. You can do this manually, though. To get the database name of a tenant, you can do use the `TenantManager::getDatabaseName()` method. - -```php ->>> tenant()->getDatabaseName(tenant()->findByDomain('laravel.localhost')) -=> "tenant67412a60-1c01-11e9-a9e9-f799baa56fd9" -``` - -## Storage driver - -### Database - -The database storage driver lets you store information about tenants in a relational database like MySQL, PostgreSQL and SQLite. - -To use this storage driver, publish the `create_tenants_table` migration: - -``` -php artisan vendor:publish --provider='Stancl\Tenancy\TenancyServiceProvider' --tag=migrations -``` - -By default, the table contains only `uuid`, `domain` and `data` columns. - -The `data` column is used to store information about a tenant, such as their selected plan, in JSON form. This package does not store anything in the column by default. - -You can store specific keys in your own columns. This is useful if you want to use RDBMS features like indexes. - -If you don't need any custom columns, you can skip the next section and run: - -``` -php artisan migrate -``` - -#### Adding your own columns - -To add your own columns, TODO. - -### Redis - -Using Redis as your storage driver is recommended due to its low overhead compared to a relational database like MySQL. - -**Note that you need to configure persistence on your Redis instance** if you don't want to lose all information about tenants. - -Read the [Redis documentation page on persistence](https://redis.io/topics/persistence). You should definitely use AOF and if you want to be even more protected from data loss, you can use RDB **in conjunction with AOF**. - -If your cache driver is Redis and you don't want to use AOF with it, run two Redis instances. Otherwise, just make sure you use a different database (number) for tenancy and for anything else. - -### Storing custom data - -Along with the tenant and database info, you can store your own data in the storage. This is useful, for example, when you want to store tenant-specific config. You can use: - -```php -get (string|array $key, string $uuid = null) // $uuid defaults to the current tenant's UUID -put (string|array $key, mixed $value = null, string $uuid = null) // if $key is array, make sure $value is null -``` - -```php -tenancy()->get($key); -tenancy()->get($key, $uuid); -tenancy()->get(['key1', 'key2']); -tenancy()->put($key, $value); -tenancy()->set($key, $value); // alias for put() -tenancy()->put($key, $value, $uuid); -tenancy()->put(['key1' => 'value1', 'key2' => 'value2']); -tenancy()->put(['key1' => 'value1', 'key2' => 'value2'], null, $uuid); -``` - -Note that `$key` has to be a string or an array with string keys. The value(s) can be of any data type. Example with arrays: - -```php ->>> tenant()->put('foo', ['a' => 'b', 'c' => 'd']); -=> [ // put() returns the supplied value(s) - "a" => "b", - "c" => "d", - ] ->>> tenant()->get('foo'); -=> [ - "a" => "b", - "c" => "d", - ] -``` - -## Database - -The entire application will use a new database connection. The connection will be based on the connection specified in `tenancy.database.based_on`. A database name of `tenancy.database.prefix` + tenant UUID + `tenancy.database.suffix` will be used. You can set the suffix to `.sqlite` if you're using sqlite and want the files to be in the sqlite format and you can leave the suffix empty if you're using MySQL (for example). - -## Redis - -Connections listed in the `tenancy.redis.prefixed_connections` config array use a prefix based on the `tenancy.redis.prefix_base` and the tenant UUID. This separates tenant data. **However note that `Redis::scan()` does not respect the prefix.** - -**Note: You *must* use phpredis if you want mutli-tenant Redis. Predis doesn't support prefixes.** - -## Cache - -Both the `cache()` helper and the `Cache` facade will use `Stancl\Tenancy\CacheManager`, which adds a tag (`prefix_base` + tenant UUID) to all methods called on it. - -If you need to store something in global, non-tenant cache, you can use the `GlobalCache` facade the same way you'd use the `Cache` facade. - -## Filesystem/Storage - -Assuming the following tenancy config: - -```php -'filesystem' => [ - 'suffix_base' => 'tenant', - // Disks which should be suffixed with the suffix_base + tenant UUID. - 'disks' => [ - 'local', - // 'public', - // 's3', - ], - 'root_override' => [ - // Disks whose roots should be overriden after storage_path() is suffixed. - 'local' => '%storage_path%/app/', - 'public' => '%storage_path%/app/public/', - ], -], -``` - -1. The `storage_path()` will be suffixed with a directory named `tenant` + the tenant UUID. -2. The `local` disk's root will be `storage_path('app')` (which is equivalen to `storage_path() . '/app/'`). - By default, all disks' roots are suffixed with `tenant` + the tenant UUID. This works for s3 and similar disks. But for local disks, this results in unwanted behavior. The default root for this disk is `storage_path('app')`: - - ```php - 'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), - ], - ``` - - However, this configration file was loaded *before* tenancy was initialized. This means that if we simply suffix this disk's root, we get `/path_to_your_application/storage/app/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/`. That's not what we want. We want `/path_to_your_application/storage/tenant1e22e620-1cb8-11e9-93b6-8d1b78ac0bcd/app/`. - - This is what the override section of the config is for. `%storage_path%` gets replaced by `storage_path()` *after* tenancy is initialized. **The roots of disks listed in the `root_override` section of the config will be replaced according it. All other disks will be simply suffixed with `tenant` + the tenant UUID.** - -```php ->>> Storage::disk('local')->getAdapter()->getPathPrefix() -=> "/var/www/laravel/multitenancy/storage/app/" ->>> tenancy()->init() -=> [ - "uuid" => "dbe0b330-1a6e-11e9-b4c3-354da4b4f339", - "domain" => "localhost", - ] ->>> Storage::disk('local')->getAdapter()->getPathPrefix() -=> "/var/www/laravel/multitenancy/storage/tenantdbe0b330-1a6e-11e9-b4c3-354da4b4f339/app/" -``` - -`storage_path()` will also be suffixed in the same way. Note that this means that each tenant will have their own storage directory. - -![The folder structure](https://i.imgur.com/GAXQOnN.png) - -If you write to these directories, you will need to create them after you create the tenant. See the docs for [PHP's mkdir](http://php.net/function.mkdir). - -Logs will be saved to `storage/logs` regardless of any changes to `storage_path()`. - -One thing that you **will** have to change if you use storage similarly to the example on the image is your use of the helper function `asset()` (that is, if you use it). - -You need to make this change to your code: - -```diff -- asset("storage/images/products/$product_id.png"); -+ tenant_asset("images/products/$product_id.png"); -``` - -Note that all (public) tenant assets have to be in the `app/public/` subdirectory of the tenant's storage directory, as shown in the image above. - -This is what the backend of `tenant_asset()` returns: -```php -// TenantAssetsController -return response()->file(storage_path('app/public/' . $path)); -``` - -With default filesystem configuration, these two commands are equivalent: - -```php -Storage::disk('public')->put($filename, $data); -Storage::disk('local')->put("public/$filename", $data); -``` - -If you want to store something globally, simply create a new disk and *don't* add it to the `tenancy.filesystem.disks` config. - -## Artisan commands - -``` -Available commands for the "tenants" namespace: - tenants:list List tenants. - tenants:migrate Run migrations for tenant(s) - tenants:rollback Rollback migrations for tenant(s). - tenants:run Run a command for tenant(s). - tenants:seed Seed tenant database(s). -``` - -#### `tenants:list` - -``` -$ artisan tenants:list -Listing all tenants. -[Tenant] uuid: dbe0b330-1a6e-11e9-b4c3-354da4b4f339 @ localhost -[Tenant] uuid: 49670df0-1a87-11e9-b7ba-cf5353777957 @ dev.localhost -``` - -#### `tenants:migrate`, `tenants:rollback`, `tenants:seed` - -- You may specify the tenant UUID(s) using the `--tenants` option. - -``` -$ artisan tenants:seed --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 -Tenant: 8075a580-1cb8-11e9-8822-49c5d8f8ff23 (laravel.localhost) -Database seeding completed successfully. -``` - -### Running your commands for tenants - -You can use the `tenants:run` command to run your own commands for tenants. - -If your command's signature were `email:send {user} {--queue} {--subject} {body}`, you would run this command like this: - -``` -$ artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..." -``` - -The `=` separates the argument/option name from its value, but you can still use `=` in the argument's value. - -### Tenant migrations - -Tenant migrations are located in `database/migrations/tenant`, so you should move your tenant migrations there. - -## Testing - -To test your multi-tenant application, simply run the following at the beginning of every test: - -```php -tenant()->create('test.localhost') -tenancy()->init('test.localhost') -``` - -To do this automatically, you can make this part of your `TestCase::setUp()` method. [Here](https://github.com/stancl/tenancy/blob/13048002ef687c3c85207df1fbf8b09ce89fb430/tests/TestCase.php#L31-L37)'s how this package handles it. - -# Tips - -- If you create a tenant using the interactive console (`artisan tinker`) and use sqlite, you might need to change the database's permissions and/or ownership (`chmod`/`chown`) so that the web application can access it. - -## HTTPS certificates - -
-Click to expand/collapse - -HTTPS certificates are very easy to deal with if you use the `yourclient1.yourapp.com`, `yourclient2.yourapp.com` model. You can use a wildcard HTTPS certificate. - -If you use the model where second level domains are used, there are multiple ways you can solve this. - -This guide focuses on nginx. - -### 1. Use nginx with the lua module - -Specifically, you're interested in the [`ssl_certificate_by_lua_block`](https://github.com/openresty/lua-nginx-module#ssl_certificate_by_lua_block) directive. Nginx doesn't support using variables such as the hostname in the `ssl_certificate` directive, which is why the lua module is needed. - -This approach lets you use one server block for all tenants. - -### 2. Add a simple server block for each tenant - -You can store most of your config in a file, such as `/etc/nginx/includes/tenant`, and include this file into tenant server blocks. - -```nginx -server { - include includes/tenant; - server_name foo.bar; - # ssl_certificate /etc/foo/...; -} -``` - -### Generating certificates - -You can generate a certificate using certbot. If you use the `--nginx` flag, you will need to run certbot as root. If you use the `--webroot` flag, you only need the user that runs it to have write access to the webroot directory (or perhaps webroot/.well-known is enough) and some certbot files (you can specify these using --work-dir, --config-dir and --logs-dir). - -Creating this config dynamically from PHP is not easy, but is probably feasible. Giving `www-data` write access to `/etc/nginx/sites-available/tenants.conf` should work. - -However, you still need to reload nginx configuration to apply the changes to configuration. This is problematic and I'm not sure if there is a simple and secure way to do this from PHP. - -
- -# Development - -## Running tests - -### With Docker - -If you have Docker installed, simply run `./test`. When you're done testing, run `docker-compose down` to shut down the containers. - -### Without Docker - -If you run the tests of this package, please make sure you don't store anything in Redis @ 127.0.0.1:6379 db#14. The contents of this database are flushed everytime the tests are run. - -Some tests are run only if the `CI`, `TRAVIS` and `CONTINUOUS_INTEGRATION` environment variables are set to `true`. This is to avoid things like bloating your MySQL instance with test databases. +Documentation has been moved to a custom website: https://stancl-tenancy.netlify.com/docs/ From 7f02ccf2959dffb7a4d941d200dbb7da164b8713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 15:40:59 +0200 Subject: [PATCH 26/32] v1.7.0 --- CHANGELOG-1.x.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG-1.x.md b/CHANGELOG-1.x.md index fe5caaca..fd4391e7 100644 --- a/CHANGELOG-1.x.md +++ b/CHANGELOG-1.x.md @@ -1,5 +1,22 @@ # Release Notes for 1.x +## [v1.7.0 (2019-08-17)](https://github.com/stancl/tenancy/compare/v1.6.1...v1.7.0) + +### Added: + +- DB storage driver - you don't have to use Redis to store tenants anymore. Relational databases are now supported as well. [more info](https://stancl-tenancy.netlify.com/docs/storage-drivers/#database) +- `tenancy:install` will do everything except DB/Redis connection creation for you. It will make changes to Http/Kernel.php, create `routes/tenant.php`, publish config, and (optionally) publish the migration. [more info](https://stancl-tenancy.netlify.com/docs/installation/) +- `tenants:run` [more info](https://stancl-tenancy.netlify.com/docs/console-commands/#run) +- New documentation: https://stancl-tenancy.netlify.com +- Custom tenant DB names [more info](https://stancl-tenancy.netlify.com/docs/custom-database-names/) +- stancl/tenancy events [more info](https://stancl-tenancy.netlify.com/docs/event-system/) + +### Fixed: + +- #89 *Command "tenants:migrate" cannot be found when used in app code* +- #87 *Unable to migrate multiple tenants at once when using MySQL* +- #96 *Issue w/ redis->scan() in getAllTenants logic.* + ## [v1.6.1 (2019-08-04)](https://github.com/stancl/tenancy/compare/v1.6.0...v1.6.1) Multiple phpunit.xml configs are now generated to run the tests with different configurations, such as different Redis drivers. From 47fb158b59be504d1091cfd6b7940dc89b7ca640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 16:33:22 +0200 Subject: [PATCH 27/32] Use MySQL by default; create migrations/tenant in tenancy:install --- assets/config.php | 2 +- src/Commands/Install.php | 13 ++++++++++--- tests/CommandsTest.php | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/assets/config.php b/assets/config.php index b8f27925..1904f1dd 100644 --- a/assets/config.php +++ b/assets/config.php @@ -19,7 +19,7 @@ return [ // 'localhost', ], 'database' => [ - 'based_on' => 'sqlite', + 'based_on' => 'mysql', 'prefix' => 'tenant', 'suffix' => '', ], diff --git a/src/Commands/Install.php b/src/Commands/Install.php index a2f162e5..141f2bc8 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -41,8 +41,9 @@ class Install extends Command )); $this->info('✔️ Set middleware priority'); - \file_put_contents(base_path('routes/tenant.php'), -"info('✔️ Created routes/tenant.php'); $this->line(''); @@ -71,6 +73,11 @@ Route::get('/your/application/homepage', function () { $this->info('✔️ Created migration.'); } + if (! \is_dir(database_path('migrations/tenant'))) { + mkdir(database_path('migrations/tenant')); + $this->info('✔️ Created database/migrations/tenant folder.'); + } + $this->comment('✨️ stancl/tenancy installed successfully.'); } } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 8f2825e7..f1e6e4d5 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -213,6 +213,7 @@ class Kernel extends HttpKernel $this->assertFileExists(base_path('routes/tenant.php')); $this->assertFileExists(base_path('config/tenancy.php')); $this->assertFileExists(database_path('migrations/2019_08_08_000000_create_tenants_table.php')); + $this->assertDirectoryExists(database_path('migrations/tenant')); $this->assertSame(" Date: Sat, 17 Aug 2019 14:33:59 +0000 Subject: [PATCH 28/32] Apply fixes from StyleCI --- src/Commands/Install.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 141f2bc8..d05495b3 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -74,7 +74,7 @@ Route::get('/your/application/homepage', function () { } if (! \is_dir(database_path('migrations/tenant'))) { - mkdir(database_path('migrations/tenant')); + \mkdir(database_path('migrations/tenant')); $this->info('✔️ Created database/migrations/tenant folder.'); } From f87c2b69ec8365ac6d075338c5da87c894d7dfdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 21:25:53 +0200 Subject: [PATCH 29/32] Default to default DB --- src/Tenant.php | 2 +- tests/TenantStorageTest.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Tenant.php b/src/Tenant.php index db9997d0..418b78fa 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -30,7 +30,7 @@ class Tenant extends Model public function getConnectionName() { - return config('tenancy.storage.db.connection', 'central'); + return config('tenancy.storage.db.connection') ?: config('database.default'); } public static function getAllTenants(array $uuids) diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php index 806e19b0..471dedaf 100644 --- a/tests/TenantStorageTest.php +++ b/tests/TenantStorageTest.php @@ -4,6 +4,7 @@ namespace Stancl\Tenancy\Tests; use Stancl\Tenancy\StorageDrivers\RedisStorageDriver; use Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver; +use Stancl\Tenancy\Tenant; class TenantStorageTest extends TestCase { @@ -140,4 +141,15 @@ class TenantStorageTest extends TestCase tenancy()->put('string', 'foo'); $this->assertSame('string', \gettype(tenancy()->get('string'))); } + + /** @test */ + public function tenant_model_uses_correct_connection() + { + config(['tenancy.storage.db.connection' => 'foo']); + $this->assertSame('foo', (new Tenant)->getConnectionName()); + + config(['tenancy.storage.db.connection' => null]); + config(['database.default' => 'foobar']); + $this->assertSame('foobar', (new Tenant)->getConnectionName()); + } } From f10f2508afbc5f01ee0cb0669fdc2438bd460f43 Mon Sep 17 00:00:00 2001 From: stancl Date: Sat, 17 Aug 2019 19:26:14 +0000 Subject: [PATCH 30/32] Apply fixes from StyleCI --- tests/TenantStorageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php index 471dedaf..da32f228 100644 --- a/tests/TenantStorageTest.php +++ b/tests/TenantStorageTest.php @@ -2,9 +2,9 @@ namespace Stancl\Tenancy\Tests; +use Stancl\Tenancy\Tenant; use Stancl\Tenancy\StorageDrivers\RedisStorageDriver; use Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver; -use Stancl\Tenancy\Tenant; class TenantStorageTest extends TestCase { From 5edf5822695a5bd8061507a08ef4dbe6be525a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 21:56:30 +0200 Subject: [PATCH 31/32] Make DB storage driver prefix sqlite DBs with database_path() --- src/DatabaseManager.php | 8 +++++--- tests/DatabaseManagerTest.php | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index c8514c9d..81864b46 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -89,9 +89,11 @@ final class DatabaseManager } } - public function getDriver(): ?string + public function getDriver($connectionName = null): ?string { - return config('database.connections.tenant.driver'); + $connectionName = $connectionName ?: $this->defaultTenantConnectionName; + + return config("database.connections.$connectionName.driver"); } public function createTenantConnection(string $databaseName, string $connectionName = null) @@ -105,7 +107,7 @@ final class DatabaseManager ]); // Change DB name - $databaseName = $this->getDriver() === 'sqlite' ? database_path($databaseName) : $databaseName; + $databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName; config()->set(["database.connections.$connectionName.database" => $databaseName]); } diff --git a/tests/DatabaseManagerTest.php b/tests/DatabaseManagerTest.php index e6d7c2cf..9fca4b4e 100644 --- a/tests/DatabaseManagerTest.php +++ b/tests/DatabaseManagerTest.php @@ -2,6 +2,8 @@ namespace Stancl\Tenancy\Tests; +use Stancl\Tenancy\DatabaseManager; + class DatabaseManagerTest extends TestCase { public $autoInitTenancy = false; @@ -17,4 +19,14 @@ class DatabaseManagerTest extends TestCase $this->assertSame($old_connection_name, $new_connection_name); $this->assertNotEquals('tenant', $new_connection_name); } + + /** @test */ + public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() + { + config(['database.connections.tenant.driver' => 'mysql']); + app(DatabaseManager::class)->createTenantConnection('foodb', 'fooconn'); + + // make tenant not sqlite so that it has to detect sqlite from fooconn + $this->assertSame(config('database.connections.fooconn.database'), database_path('foodb')); + } } From 6cee5d36ade8af3e35e51ed9e213056e8df96336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 17 Aug 2019 21:57:13 +0200 Subject: [PATCH 32/32] wip --- tests/DatabaseManagerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/DatabaseManagerTest.php b/tests/DatabaseManagerTest.php index 9fca4b4e..ec8b8df2 100644 --- a/tests/DatabaseManagerTest.php +++ b/tests/DatabaseManagerTest.php @@ -23,10 +23,10 @@ class DatabaseManagerTest extends TestCase /** @test */ public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() { + // make `tenant` not sqlite so that it has to detect sqlite from fooconn config(['database.connections.tenant.driver' => 'mysql']); app(DatabaseManager::class)->createTenantConnection('foodb', 'fooconn'); - // make tenant not sqlite so that it has to detect sqlite from fooconn $this->assertSame(config('database.connections.fooconn.database'), database_path('foodb')); } }