From be1316013374c4ea7c655cbdb340a35d8942e52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 28 Jul 2022 15:14:29 +0200 Subject: [PATCH 01/33] wip Debuggable trait --- src/Concerns/Debuggable.php | 68 ++++++++++++++++++++++++++++++++++ src/Enums/LogMode.php | 10 +++++ src/Tenancy.php | 3 +- src/TenancyServiceProvider.php | 15 ++++++++ tests/DebuggableTest.php | 68 ++++++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/Concerns/Debuggable.php create mode 100644 src/Enums/LogMode.php create mode 100644 tests/DebuggableTest.php diff --git a/src/Concerns/Debuggable.php b/src/Concerns/Debuggable.php new file mode 100644 index 00000000..c8bbce86 --- /dev/null +++ b/src/Concerns/Debuggable.php @@ -0,0 +1,68 @@ +eventLog = []; + $this->logMode = $mode; + + return $this; + } + + public function logMode(): LogMode + { + return $this->logMode; + } + + public function getLog(): array + { + return $this->eventLog; + } + + public function logEvent(TenancyEvent $event): static + { + $this->eventLog[] = ['time' => now(), 'event' => $event::class, 'tenant' => $this->tenant]; + + return $this; + } + + public function dump(Closure $dump = null): static + { + $dump ??= dd(...); + + // Dump the log if we were already logging in silent mode + // Otherwise start logging in instant mode + match ($this->logMode) { + LogMode::NONE => $this->log(LogMode::INSTANT), + LogMode::SILENT => $dump($this->eventLog), + LogMode::INSTANT => null, + }; + + return $this; + } + + public function dd(Closure $dump = null): void + { + $dump ??= dd(...); + + if ($this->logMode === LogMode::SILENT) { + $dump($this->eventLog); + } else { + $dump($this); + } + } +} diff --git a/src/Enums/LogMode.php b/src/Enums/LogMode.php new file mode 100644 index 00000000..11ca0c28 --- /dev/null +++ b/src/Enums/LogMode.php @@ -0,0 +1,10 @@ +loadRoutesFrom(__DIR__ . '/../assets/routes.php'); } + Event::listen('Stancl\\Tenancy\\Events\\*', function (string $name, array $data) { + $event = $data[0]; + + if ($event instanceof TenancyEvent) { + match (tenancy()->logMode()) { + LogMode::SILENT => tenancy()->logEvent($event), + LogMode::INSTANT => dump($event), // todo0 perhaps still log + default => null, + }; + } + }); + $this->app->singleton('globalUrl', function ($app) { if ($app->bound(FilesystemTenancyBootstrapper::class)) { $instance = clone $app['url']; diff --git a/tests/DebuggableTest.php b/tests/DebuggableTest.php new file mode 100644 index 00000000..49e180d7 --- /dev/null +++ b/tests/DebuggableTest.php @@ -0,0 +1,68 @@ +log(LogMode::SILENT); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + tenancy()->end(); + + assertTenancyInitializedAndEnded(tenancy()->getLog(), $tenant); +}); + +test('tenancy logs event silently by default', function () { + tenancy()->log(); + + expect(tenancy()->logMode())->toBe(LogMode::SILENT); +}); + +test('the log can be dumped', function (string $method) { + tenancy()->log(); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + tenancy()->end(); + + $output = []; + tenancy()->$method(function ($data) use (&$output) { + $output = $data; + }); + + assertTenancyInitializedAndEnded($output, $tenant); +})->with([ + 'dump', + 'dd', +]); + +test('tenancy can log events immediately', function () { + // todo implement + pest()->markTestIncomplete(); +}); + +// todo test the different behavior of the methods in different contexts, or get rid of the logic and simplify it + +function assertTenancyInitializedAndEnded(array $log, Tenant $tenant): void +{ + expect($log)->toHaveCount(4); + + expect($log[0]['event'])->toBe(InitializingTenancy::class); + expect($log[0]['tenant'])->toBe($tenant); + expect($log[1]['event'])->toBe(TenancyInitialized::class); + expect($log[1]['tenant'])->toBe($tenant); + + expect($log[2]['event'])->toBe(EndingTenancy::class); + expect($log[2]['tenant'])->toBe($tenant); + expect($log[3]['event'])->toBe(TenancyEnded::class); + expect($log[3]['tenant'])->toBe($tenant); +} From 3d8d670447b8ad5eb54c2903a18b39031c51fdc0 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Thu, 28 Jul 2022 13:30:51 +0000 Subject: [PATCH 02/33] Fix code style (php-cs-fixer) --- src/Concerns/Debuggable.php | 2 ++ src/Enums/LogMode.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Concerns/Debuggable.php b/src/Concerns/Debuggable.php index c8bbce86..98697f1d 100644 --- a/src/Concerns/Debuggable.php +++ b/src/Concerns/Debuggable.php @@ -1,5 +1,7 @@ Date: Tue, 2 Aug 2022 04:21:03 +0200 Subject: [PATCH 03/33] minor code updates --- src/CacheManager.php | 2 +- src/Tenancy.php | 36 +++++++++++++++------------------- src/TenancyServiceProvider.php | 8 ++------ 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/CacheManager.php b/src/CacheManager.php index 09581201..eb6bac4c 100644 --- a/src/CacheManager.php +++ b/src/CacheManager.php @@ -26,7 +26,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 + $names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/9.x/cache#removing-tagged-cache-items return $this->store()->tags(array_merge($tags, $names)); } diff --git a/src/Tenancy.php b/src/Tenancy.php index 439c34cb..1359fcab 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy; +use Closure; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Traits\Macroable; @@ -16,20 +17,17 @@ class Tenancy { use Macroable, Debuggable; - /** @var Tenant|Model|null */ - public $tenant; + /** The current tenant. */ + public Tenant&Model $tenant; - /** @var callable|null */ - public $getBootstrappersUsing = null; + // todo docblock + public ?Closure $getBootstrappersUsing = null; - /** @var bool */ - public $initialized = false; + /** Is tenancy fully initialized? */ + public $initialized = false; // todo document the difference between $tenant being set and $initialized being true (e.g. end of initialize() method) - /** - * Initializes the tenant. - * @param Tenant|int|string $tenant - */ - public function initialize($tenant): void + /** Initialize tenancy for the passed tenant. */ + public function initialize(Tenant|int|string $tenant): void { if (! is_object($tenant)) { $tenantId = $tenant; @@ -85,29 +83,28 @@ class Tenancy return array_map('app', $resolve($this->tenant)); } - public function query(): Builder + public static function query(): Builder { - return $this->model()->query(); + return static::model()->query(); } - /** @return Tenant|Model */ - public function model() + public static function model(): Tenant&Model { $class = config('tenancy.tenant_model'); return new $class; } - public function find($id): ?Tenant + public static function find(int|string $id): Tenant|null { - return $this->model()->where($this->model()->getTenantKeyName(), $id)->first(); + return static::model()->where(static::model()->getTenantKeyName(), $id)->first(); } /** * Run a callback in the central context. * Atomic, safely reverts to previous context. */ - public function central(callable $callback) + public function central(Closure $callback) { $previousTenant = $this->tenant; @@ -129,9 +126,8 @@ class Tenancy * More performant than running $tenant->run() one by one. * * @param Tenant[]|\Traversable|string[]|null $tenants - * @return void */ - public function runForMultiple($tenants, callable $callback) + public function runForMultiple($tenants, callable $callback): void { // Convert null to all tenants $tenants = is_null($tenants) ? $this->model()->cursor() : $tenants; diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index debe6b17..3850720c 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -16,9 +16,7 @@ use Stancl\Tenancy\Resolvers\DomainTenantResolver; class TenancyServiceProvider extends ServiceProvider { - /** - * Register services. - */ + /* Register services. */ public function register(): void { $this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy'); @@ -75,9 +73,7 @@ class TenancyServiceProvider extends ServiceProvider }); } - /** - * Bootstrap services. - */ + /* Bootstrap services. */ public function boot(): void { $this->commands([ From 9c584b8394b41435eabe77b3360a890d48e82957 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Tue, 2 Aug 2022 02:21:33 +0000 Subject: [PATCH 04/33] Fix code style (php-cs-fixer) --- src/Concerns/Debuggable.php | 2 ++ src/Enums/LogMode.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Concerns/Debuggable.php b/src/Concerns/Debuggable.php index c8bbce86..98697f1d 100644 --- a/src/Concerns/Debuggable.php +++ b/src/Concerns/Debuggable.php @@ -1,5 +1,7 @@ Date: Tue, 2 Aug 2022 15:01:03 +0200 Subject: [PATCH 05/33] Fix $tenant type --- src/Tenancy.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Tenancy.php b/src/Tenancy.php index 1359fcab..6fb225e6 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -17,8 +17,12 @@ class Tenancy { use Macroable, Debuggable; - /** The current tenant. */ - public Tenant&Model $tenant; + /** + * The current tenant. + * + * @var (Tenant&Model)|null + */ + public ?Tenant $tenant; // todo docblock public ?Closure $getBootstrappersUsing = null; From e4a47f3e6e0f5c0aecb7973f9a85b60290ec2a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 2 Aug 2022 15:21:02 +0200 Subject: [PATCH 06/33] Set default $tenant value --- src/Tenancy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tenancy.php b/src/Tenancy.php index 6fb225e6..ba151f22 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -22,7 +22,7 @@ class Tenancy * * @var (Tenant&Model)|null */ - public ?Tenant $tenant; + public ?Tenant $tenant = null; // todo docblock public ?Closure $getBootstrappersUsing = null; From a45aa8e274029ce9ec5995c645b81a40819f7a3b Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Thu, 4 Aug 2022 23:29:39 +0500 Subject: [PATCH 07/33] Improve `tenants:run` command (#912) * improve `tenants:run` command * Update Run.php --- src/Commands/Run.php | 27 ++++++--------------------- tests/CommandsTest.php | 4 ++-- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 2b20d9c3..6542f1ca 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Artisan; class Run extends Command { @@ -20,10 +21,8 @@ class Run extends Command * * @var string */ - protected $signature = "tenants:run {commandname : The command's name.} - {--tenants=* : The tenant(s) to run the command for. Default: all} - {--argument=* : The arguments to pass to the command. Default: none} - {--option=* : The options to pass to the command. Default: none}"; + protected $signature = "tenants:run {commandname : The artisan command.} + {--tenants=* : The tenant(s) to run the command for. Default: all}"; /** * Execute the console command. @@ -33,23 +32,9 @@ class Run extends Command tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { $this->line("Tenant: {$tenant->getTenantKey()}"); - $callback = function ($prefix = '') { - return function ($arguments, $argument) use ($prefix) { - [$key, $value] = explode('=', $argument, 2); - $arguments[$prefix . $key] = $value; - - return $arguments; - }; - }; - - // Turns ['foo=bar', 'abc=xyz=zzz'] into ['foo' => 'bar', 'abc' => 'xyz=zzz'] - $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('--'), []); - - // Run command - $this->call($this->argument('commandname'), array_merge($arguments, $options)); + Artisan::call($this->argument('commandname')); + $this->comment('Command output:'); + $this->info(Artisan::output()); }); } } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 5172d752..7415b74f 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -174,7 +174,7 @@ test('run command with array of tenants works', function () { $tenantId2 = Tenant::create()->getTenantKey(); Artisan::call('tenants:migrate-fresh'); - pest()->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'") + pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'") ->expectsOutput('Tenant: ' . $tenantId1) ->expectsOutput('Tenant: ' . $tenantId2); }); @@ -186,7 +186,7 @@ function runCommandWorks(): void Artisan::call('tenants:migrate', ['--tenants' => [$id]]); - pest()->artisan("tenants:run foo --tenants=$id --argument='a=foo' --option='b=bar' --option='c=xyz'") + pest()->artisan("tenants:run --tenants=$id 'foo foo --b=bar --c=xyz' ") ->expectsOutput("User's name is Test command") ->expectsOutput('foo') ->expectsOutput('xyz'); From db4a795c3e0d7b3c6344ad4ffca9838e4c704342 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Thu, 4 Aug 2022 18:30:06 +0000 Subject: [PATCH 08/33] Fix code style (php-cs-fixer) --- src/Commands/Run.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 6542f1ca..075f9116 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -21,8 +21,8 @@ class Run extends Command * * @var string */ - protected $signature = "tenants:run {commandname : The artisan command.} - {--tenants=* : The tenant(s) to run the command for. Default: all}"; + protected $signature = 'tenants:run {commandname : The artisan command.} + {--tenants=* : The tenant(s) to run the command for. Default: all}'; /** * Execute the console command. From 931c76d697f40f4ab0c7e96957ddc083da3557bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Mon, 22 Aug 2022 17:59:43 +0200 Subject: [PATCH 09/33] Pull 3.x changes into master (#922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * exclude master from CI * Add space after 'up' in 'docker-compose up-d' (#900) * Fix ArgumentCountError on the TenantAssetsController (#894) * Fix ArgumentCount exception on the TenantAssetsController when no `$path` is provided * CS * CS * Handle null case explicitly * code style Co-authored-by: Bram Wubs Co-authored-by: Samuel Štancl * Add support for nested tenant config override (#920) * feat: add support for nested tenant config override * test: ensure nested tenant values are mapped * Update TenantConfigTest.php Co-authored-by: lukinovec Co-authored-by: Bram Wubs Co-authored-by: Bram Wubs Co-authored-by: George Bishop Co-authored-by: Abrar Ahmad --- src/Controllers/TenantAssetsController.php | 7 +++++-- src/Features/TenantConfig.php | 3 ++- tests/Features/TenantConfigTest.php | 22 ++++++++++++++++++++++ tests/TenantAssetTest.php | 12 ++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetsController.php index 67478cf9..5549da0d 100644 --- a/src/Controllers/TenantAssetsController.php +++ b/src/Controllers/TenantAssetsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Controllers; use Illuminate\Routing\Controller; +use Throwable; class TenantAssetsController extends Controller { @@ -15,11 +16,13 @@ class TenantAssetsController extends Controller $this->middleware(static::$tenancyMiddleware); } - public function asset($path) + public function asset($path = null) { + abort_if($path === null, 404); + try { return response()->file(storage_path("app/public/$path")); - } catch (\Throwable $th) { + } catch (Throwable $th) { abort(404); } } diff --git a/src/Features/TenantConfig.php b/src/Features/TenantConfig.php index fb09a871..95691f0d 100644 --- a/src/Features/TenantConfig.php +++ b/src/Features/TenantConfig.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Features; use Illuminate\Contracts\Config\Repository; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Event; use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Contracts\Tenant; @@ -45,7 +46,7 @@ class TenantConfig implements Feature { /** @var Tenant|Model $tenant */ foreach (static::$storageToConfigMap as $storageKey => $configKey) { - $override = $tenant->getAttribute($storageKey); + $override = Arr::get($tenant, $storageKey); if (! is_null($override)) { if (is_array($configKey)) { diff --git a/tests/Features/TenantConfigTest.php b/tests/Features/TenantConfigTest.php index 35df35ed..5c12c5f0 100644 --- a/tests/Features/TenantConfigTest.php +++ b/tests/Features/TenantConfigTest.php @@ -14,6 +14,28 @@ afterEach(function () { TenantConfig::$storageToConfigMap = []; }); +test('nested tenant values are merged', function () { + expect(config('whitelabel.theme'))->toBeNull(); + config([ + 'tenancy.features' => [TenantConfig::class], + 'tenancy.bootstrappers' => [], + ]); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + TenantConfig::$storageToConfigMap = [ + 'whitelabel.config.theme' => 'whitelabel.theme', + ]; + + $tenant = Tenant::create([ + 'whitelabel' => ['config' => ['theme' => 'dark']], + ]); + + tenancy()->initialize($tenant); + expect(config('whitelabel.theme'))->toBe('dark'); + tenancy()->end(); +}); + test('config is merged and removed', function () { expect(config('services.paypal'))->toBe(null); config([ diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 2c5000f1..d43b7989 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -94,6 +94,18 @@ test('asset helper tenancy can be disabled', function () { expect(asset('foo'))->toBe($original); }); +test('test asset controller returns a 404 when no path is provided', function () { + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + pest()->get(tenant_asset(null), [ + 'X-Tenant' => $tenant->id, + ])->assertNotFound(); +}); + function getEnvironmentSetUp($app) { $app->booted(function () { From ca2eefa30a40dc9f500545b6203b2d61a9848d9d Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Thu, 25 Aug 2022 19:52:46 +0200 Subject: [PATCH 10/33] [WIP] Use a lighter Docker image, use it in CI (#793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use action services and setup-php in workflow * Fix codecov * exclude master from CI * Add space after 'up' in 'docker-compose up-d' (#900) * Fix ArgumentCountError on the TenantAssetsController (#894) * Fix ArgumentCount exception on the TenantAssetsController when no `$path` is provided * CS * CS * Handle null case explicitly * code style Co-authored-by: Bram Wubs Co-authored-by: Samuel Štancl * Improve Dockerfile and use it in CI * Update Dockerfile * mssql CI health check * cache key * Update ci.yml * Update ci.yml * Update composer.json * register dumcommand when L8 * Update ci.yml * Update composer.json * Update composer.json * Update composer.json * wip * removed extensions config and php version from matrix * introduce php-cs-fixer issue for testing * Fix code style (php-cs-fixer) * install composer in Docker and remove setup-php step * added pcov for coverage * on master branch * composer test command * tests above services * Update ci.yml * Revert "register dumcommand when L8" This reverts commit f165fc58ba9b62b76798fdf0f9330319bceb7afd. * removed composer cache dependencies Co-authored-by: Samuel Štancl Co-authored-by: lukinovec Co-authored-by: Bram Wubs Co-authored-by: Bram Wubs Co-authored-by: Samuel Štancl Co-authored-by: Abrar Ahmad Co-authored-by: PHP CS Fixer --- .github/workflows/ci.yml | 81 ++++++++++++++++++++++++++++++++++------ Dockerfile | 52 +++++++++----------------- composer.json | 12 +++--- 3 files changed, 93 insertions(+), 52 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0fe927a..56f90c64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,32 +1,91 @@ name: CI env: - COMPOSE_INTERACTIVE_NO_CLI: 1 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} on: push: - branches: [ master ] pull_request: branches: [ master ] jobs: tests: runs-on: ubuntu-latest + container: abrardev/tenancy:latest strategy: matrix: - php: ["8.1"] - laravel: ["^9.0"] + laravel: ['^8.0', '^9.0'] steps: - - uses: actions/checkout@v2 - - name: Start docker containers - run: PHP_VERSION=${{ matrix.php }} docker-compose up -d - - name: Install dependencies - run: docker-compose exec -T test composer require --no-interaction "laravel/framework:${{ matrix.laravel }}" - - name: Run tests - run: ./test + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Composer dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update + composer update --prefer-dist --no-interaction + - name: Run tests + run: ./vendor/bin/pest + env: + DB_PASSWORD: password + DB_USERNAME: root + DB_DATABASE: main + TENANCY_TEST_MYSQL_HOST: mysql + TENANCY_TEST_PGSQL_HOST: postgres + TENANCY_TEST_REDIS_HOST: redis + TENANCY_TEST_SQLSRV_HOST: mssql + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + token: 24382d15-84e7-4a55-bea4-c4df96a24a9b + + services: + postgres: + image: postgres:latest + env: + POSTGRES_PASSWORD: password + POSTGRES_USER: root + POSTGRES_DB: main + ports: + - 5432/tcp + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: false + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: main + ports: + - 3306/tcp + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + mysql2: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: false + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: main + ports: + - 3306/tcp + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + ports: + - 1433/tcp + env: + ACCEPT_EULA: Y + SA_PASSWORD: P@ssword + options: --health-cmd "echo quit | /opt/mssql-tools/bin/sqlcmd -S 127.0.0.1 -l 1 -U sa -P P@ssword" + + redis: + image: redis + ports: + - 6379/tcp + options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 php-cs-fixer: name: Code style (php-cs-fixer) diff --git a/Dockerfile b/Dockerfile index fb63afe3..9b60589c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,7 @@ -ARG PHP_VERSION=7.4 -ARG PHP_TARGET=php:${PHP_VERSION}-cli +# add amd64 platform to support Mac M1 +FROM --platform=linux/amd64 shivammathur/node:latest-amd64 -FROM --platform=linux/amd64 ${PHP_TARGET} - -ARG COMPOSER_TARGET=2.0.3 +ARG PHP_VERSION=8.1 WORKDIR /var/www/html @@ -17,43 +15,27 @@ LABEL org.opencontainers.image.source=https://github.com/stancl/tenancy \ ENV TZ=Europe/London ENV LANG=en_GB.UTF-8 -# Note: we only install reliable/core 1st-party php extensions here. -# If your app needs custom ones install them in the apps own -# Dockerfile _and pin the versions_! Eg: -# RUN pecl install memcached-2.2.0 && docker-php-ext-enable memcached - - +# install MYSSQL ODBC Driver RUN apt-get update \ && apt-get install -y gnupg2 \ && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ - && curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list \ + && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list \ && apt-get update \ && ACCEPT_EULA=Y apt-get install -y unixodbc-dev msodbcsql17 -RUN apt-get install -y --no-install-recommends locales apt-transport-https libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl mariadb-client sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git +RUN apt-get update \ + && apt-get install -y --no-install-recommends libhiredis0.14 libjemalloc2 liblua5.1-0 lua-bitop lua-cjson redis redis-server redis-tools + +RUN pecl install redis-5.3.7 sqlsrv pdo_sqlsrv pcov \ + && printf "; priority=20\nextension=redis.so\n" > /etc/php/$PHP_VERSION/mods-available/redis.ini \ + && printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/$PHP_VERSION/mods-available/sqlsrv.ini \ + && printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/$PHP_VERSION/mods-available/pdo_sqlsrv.ini \ + && printf "; priority=40\nextension=pcov.so\n" > /etc/php/$PHP_VERSION/mods-available/pcov.ini \ + && phpenmod -v $PHP_VERSION redis sqlsrv pdo_sqlsrv pcov + +# install composer +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer -RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ - # && if [ "${PHP_VERSION}" = "7.4" ]; then docker-php-ext-configure gd --with-freetype --with-jpeg; else docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; fi \ - && docker-php-ext-install -j$(nproc) gd pdo pdo_mysql pdo_pgsql pdo_sqlite pgsql zip gmp bcmath pcntl ldap sysvmsg exif \ - # install the redis php extension - && pecl install redis-5.3.7 \ - && docker-php-ext-enable redis \ - # install the pcov extention - && pecl install pcov \ - && docker-php-ext-enable pcov \ - && echo "pcov.enabled = 1" > /usr/local/etc/php/conf.d/pcov.ini \ - # install sqlsrv - && pecl install sqlsrv pdo_sqlsrv \ - && docker-php-ext-enable sqlsrv pdo_sqlsrv -# clear the apt cache -RUN rm -rf /var/lib/apt/lists/* \ - && rm -rf /var/lib/apt/lists/* \ - # install composer - && curl -o /tmp/composer-setup.php https://getcomposer.org/installer \ - && curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \ - && php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \ - && php /tmp/composer-setup.php --version=${COMPOSER_TARGET} --no-ansi --install-dir=/usr/local/bin --filename=composer --snapshot \ - && rm -f /tmp/composer-setup.* # set the system timezone RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ && echo $TZ > /etc/timezone diff --git a/composer.json b/composer.json index 8aca9ded..03f08244 100644 --- a/composer.json +++ b/composer.json @@ -17,16 +17,16 @@ "require": { "php": "^8.1", "ext-json": "*", - "illuminate/support": "^9.0", + "illuminate/support": "^8.0|^9.0", "facade/ignition-contracts": "^1.0", "ramsey/uuid": "^4.0", - "stancl/jobpipeline": "^1.6", - "stancl/virtualcolumn": "^1.2" + "stancl/jobpipeline": "^1.0", + "stancl/virtualcolumn": "^1.0" }, "require-dev": { - "laravel/framework": "^9.0", - "orchestra/testbench": "^7.0", - "league/flysystem-aws-s3-v3": "^3.0", + "laravel/framework": "^8.0|^9.0", + "orchestra/testbench": "^6.0|^7.0", + "league/flysystem-aws-s3-v3": "^1.0|^3.0", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5", "pestphp/pest": "^1.21" From ddc7cf49c3206172af316d9c2e93b1a15b4dbb16 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Fri, 26 Aug 2022 19:42:57 +0500 Subject: [PATCH 11/33] switch php version based on ARG (#924) --- Dockerfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Dockerfile b/Dockerfile index 9b60589c..b1d76ea8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,13 @@ RUN apt-get update \ && apt-get update \ && ACCEPT_EULA=Y apt-get install -y unixodbc-dev msodbcsql17 +# set PHP version +RUN update-alternatives --set php /usr/bin/php$PHP_VERSION \ + && update-alternatives --set phar /usr/bin/phar$PHP_VERSION \ + && update-alternatives --set phar.phar /usr/bin/phar.phar$PHP_VERSION \ + && update-alternatives --set phpize /usr/bin/phpize$PHP_VERSION \ + && update-alternatives --set php-config /usr/bin/php-config$PHP_VERSION + RUN apt-get update \ && apt-get install -y --no-install-recommends libhiredis0.14 libjemalloc2 liblua5.1-0 lua-bitop lua-cjson redis redis-server redis-tools From 55d0a9ab87a762fcc22f473712524c78a2d2be1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 26 Aug 2022 21:35:17 +0200 Subject: [PATCH 12/33] misc improvements - stronger types, exception refactor --- Dockerfile | 6 --- src/Contracts/ManagesDatabaseUsers.php | 5 +++ src/Contracts/SyncMaster.php | 2 + src/Contracts/Tenant.php | 12 ++--- .../TenantCouldNotBeIdentifiedException.php | 45 ++++++++++++++++++- src/Contracts/TenantDatabaseManager.php | 16 ++----- src/Contracts/TenantWithDatabase.php | 2 +- src/Database/Concerns/HasInternalKeys.php | 16 +++---- src/Database/Concerns/TenantRun.php | 6 ++- src/Database/DatabaseManager.php | 2 + src/Database/Models/ImpersonationToken.php | 4 +- src/Database/Models/Tenant.php | 2 +- src/Database/Models/TenantPivot.php | 4 +- src/Database/ParentModelScope.php | 2 +- src/Database/TenantCollection.php | 3 +- src/DatabaseConfig.php | 7 +-- .../TenantCouldNotBeIdentifiedById.php | 28 ------------ ...enantCouldNotBeIdentifiedByIdException.php | 18 ++++++++ ...antCouldNotBeIdentifiedByPathException.php | 21 +++------ ...dNotBeIdentifiedByRequestDataException.php | 21 +++------ ...tCouldNotBeIdentifiedOnDomainException.php | 21 +++------ .../TenantDatabaseAlreadyExistsException.php | 14 +++--- .../TenantDatabaseDoesNotExistException.php | 2 +- ...nantDatabaseUserAlreadyExistsException.php | 14 +++--- src/Features/CrossDomainRedirect.php | 2 +- src/Features/UniversalRoutes.php | 7 +-- src/Features/UserImpersonation.php | 27 +++++------ .../Contracts/CachedTenantResolver.php | 9 ++-- src/Resolvers/DomainTenantResolver.php | 17 +++---- src/Resolvers/PathTenantResolver.php | 11 ++--- src/Resolvers/RequestDataTenantResolver.php | 9 ++-- src/Tenancy.php | 14 +++--- .../MicrosoftSQLDatabaseManager.php | 9 ++-- tests/TenantModelTest.php | 10 +++-- 34 files changed, 179 insertions(+), 209 deletions(-) delete mode 100644 src/Exceptions/TenantCouldNotBeIdentifiedById.php create mode 100644 src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php diff --git a/Dockerfile b/Dockerfile index b1d76ea8..0ced8009 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,12 +5,6 @@ ARG PHP_VERSION=8.1 WORKDIR /var/www/html -LABEL org.opencontainers.image.source=https://github.com/stancl/tenancy \ - org.opencontainers.image.vendor="Samuel Štancl" \ - org.opencontainers.image.licenses="MIT" \ - org.opencontainers.image.title="PHP ${PHP_VERSION} with modules for laravel support" \ - org.opencontainers.image.description="PHP ${PHP_VERSION} with a set of php/os packages suitable for running Laravel apps" - # our default timezone and langauge ENV TZ=Europe/London ENV LANG=en_GB.UTF-8 diff --git a/src/Contracts/ManagesDatabaseUsers.php b/src/Contracts/ManagesDatabaseUsers.php index 6de28a80..d4da1c3c 100644 --- a/src/Contracts/ManagesDatabaseUsers.php +++ b/src/Contracts/ManagesDatabaseUsers.php @@ -6,11 +6,16 @@ namespace Stancl\Tenancy\Contracts; use Stancl\Tenancy\DatabaseConfig; +// todo possibly move to Database namespace, along with other classes + interface ManagesDatabaseUsers extends TenantDatabaseManager { + /** Create a database user. */ public function createUser(DatabaseConfig $databaseConfig): bool; + /** Delete a database user. */ public function deleteUser(DatabaseConfig $databaseConfig): bool; + /** Does a database user exist? */ public function userExists(string $username): bool; } diff --git a/src/Contracts/SyncMaster.php b/src/Contracts/SyncMaster.php index e1b34149..28fafa91 100644 --- a/src/Contracts/SyncMaster.php +++ b/src/Contracts/SyncMaster.php @@ -7,6 +7,8 @@ namespace Stancl\Tenancy\Contracts; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\BelongsToMany; +// todo move all resource syncing-related things to a separate namespace? + /** * @property-read Tenant[]|Collection $tenants */ diff --git a/src/Contracts/Tenant.php b/src/Contracts/Tenant.php index 770e862e..c01cd7d5 100644 --- a/src/Contracts/Tenant.php +++ b/src/Contracts/Tenant.php @@ -4,11 +4,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Contracts; +use Closure; + /** * @see \Stancl\Tenancy\Database\Models\Tenant * - * @method __call(string $method, array $parameters) IDE support. This will be a model. - * @method static __callStatic(string $method, array $parameters) IDE support. This will be a model. * @mixin \Illuminate\Database\Eloquent\Model */ interface Tenant @@ -17,14 +17,14 @@ interface Tenant public function getTenantKeyName(): string; /** Get the value of the key used for identifying the tenant. */ - public function getTenantKey(); + public function getTenantKey(): int|string; /** Get the value of an internal key. */ - public function getInternal(string $key); + public function getInternal(string $key): mixed; /** Set the value of an internal key. */ - public function setInternal(string $key, $value); + public function setInternal(string $key, mixed $value): static; /** Run a callback in this tenant's environment. */ - public function run(callable $callback); + public function run(Closure $callback): mixed; } diff --git a/src/Contracts/TenantCouldNotBeIdentifiedException.php b/src/Contracts/TenantCouldNotBeIdentifiedException.php index ac767e5e..2f301c37 100644 --- a/src/Contracts/TenantCouldNotBeIdentifiedException.php +++ b/src/Contracts/TenantCouldNotBeIdentifiedException.php @@ -5,7 +5,50 @@ declare(strict_types=1); namespace Stancl\Tenancy\Contracts; use Exception; +use Facade\IgnitionContracts\BaseSolution; +use Facade\IgnitionContracts\ProvidesSolution; +use Facade\IgnitionContracts\Solution; -abstract class TenantCouldNotBeIdentifiedException extends Exception +abstract class TenantCouldNotBeIdentifiedException extends Exception implements ProvidesSolution { + /** Default solution title. */ + protected string $solutionTitle = 'Tenant could not be identified'; + + /** Default solution description. */ + protected string $solutionDescription = 'Are you sure this tenant exists?'; + + /** Set the message. */ + protected function tenantCouldNotBeIdentified(string $how): static + { + $this->message = "Tenant could not be identified " . $how; + + return $this; + } + + /** Set the solution title. */ + protected function title(string $solutionTitle): static + { + $this->solutionTitle = $solutionTitle; + + return $this; + } + + /** Set the solution description. */ + protected function description(string $solutionDescription): static + { + $this->solutionDescription = $solutionDescription; + + return $this; + } + + /** Get the Ignition description. */ + public function getSolution(): Solution + { + return BaseSolution::create($this->solutionTitle) + ->setSolutionDescription($this->solutionDescription) + ->setDocumentationLinks([ + 'Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants', + 'Tenant Identification' => 'https://tenancyforlaravel.com/docs/v3/tenant-identification', + ]); + } } diff --git a/src/Contracts/TenantDatabaseManager.php b/src/Contracts/TenantDatabaseManager.php index 92801d75..deaecb33 100644 --- a/src/Contracts/TenantDatabaseManager.php +++ b/src/Contracts/TenantDatabaseManager.php @@ -8,24 +8,16 @@ use Stancl\Tenancy\Exceptions\NoConnectionSetException; interface TenantDatabaseManager { - /** - * Create a database. - */ + /** Create a database. */ public function createDatabase(TenantWithDatabase $tenant): bool; - /** - * Delete a database. - */ + /** Delete a database. */ public function deleteDatabase(TenantWithDatabase $tenant): bool; - /** - * Does a database exist. - */ + /** Does a database exist? */ public function databaseExists(string $name): bool; - /** - * Make a DB connection config array. - */ + /** Construct a DB connection config array. */ public function makeConnectionConfig(array $baseConfig, string $databaseName): array; /** diff --git a/src/Contracts/TenantWithDatabase.php b/src/Contracts/TenantWithDatabase.php index c3f51628..98268502 100644 --- a/src/Contracts/TenantWithDatabase.php +++ b/src/Contracts/TenantWithDatabase.php @@ -11,5 +11,5 @@ interface TenantWithDatabase extends Tenant public function database(): DatabaseConfig; /** Get an internal key. */ - public function getInternal(string $key); + public function getInternal(string $key): mixed; } diff --git a/src/Database/Concerns/HasInternalKeys.php b/src/Database/Concerns/HasInternalKeys.php index b4e175c3..ea70d6ed 100644 --- a/src/Database/Concerns/HasInternalKeys.php +++ b/src/Database/Concerns/HasInternalKeys.php @@ -6,26 +6,20 @@ namespace Stancl\Tenancy\Database\Concerns; trait HasInternalKeys { - /** - * Get the internal prefix. - */ + /** Get the internal prefix. */ public static function internalPrefix(): string { return 'tenancy_'; } - /** - * Get an internal key. - */ - public function getInternal(string $key) + /** Get an internal key. */ + public function getInternal(string $key): mixed { return $this->getAttribute(static::internalPrefix() . $key); } - /** - * Set internal key. - */ - public function setInternal(string $key, $value) + /** Set internal key. */ + public function setInternal(string $key, mixed $value): static { $this->setAttribute(static::internalPrefix() . $key, $value); diff --git a/src/Database/Concerns/TenantRun.php b/src/Database/Concerns/TenantRun.php index 29bbedac..01d5b9d8 100644 --- a/src/Database/Concerns/TenantRun.php +++ b/src/Database/Concerns/TenantRun.php @@ -4,15 +4,17 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; +use Closure; use Stancl\Tenancy\Contracts\Tenant; trait TenantRun { /** * Run a callback in this tenant's context. - * Atomic, safely reverts to previous context. + * + * This method is atomic and safely reverts to the previous context. */ - public function run(callable $callback) + public function run(Closure $callback): mixed { /** @var Tenant $this */ $originalTenant = tenant(); diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index 6242ffa9..eb807696 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -14,6 +14,8 @@ use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; +// todo move to Database namespace + /** * @internal Class is subject to breaking changes in minor and patch versions. */ diff --git a/src/Database/Models/ImpersonationToken.php b/src/Database/Models/ImpersonationToken.php index 43c536fb..d73b436b 100644 --- a/src/Database/Models/ImpersonationToken.php +++ b/src/Database/Models/ImpersonationToken.php @@ -35,10 +35,8 @@ class ImpersonationToken extends Model 'created_at', ]; - public static function boot() + public static function booted() { - parent::boot(); - static::creating(function ($model) { $model->created_at = $model->created_at ?? $model->freshTimestamp(); $model->token = $model->token ?? Str::random(128); diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index f88297be..4518e7b7 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -39,7 +39,7 @@ class Tenant extends Model implements Contracts\Tenant return 'id'; } - public function getTenantKey() + public function getTenantKey(): int|string { return $this->getAttribute($this->getTenantKeyName()); } diff --git a/src/Database/Models/TenantPivot.php b/src/Database/Models/TenantPivot.php index f745b45a..5c0d6a37 100644 --- a/src/Database/Models/TenantPivot.php +++ b/src/Database/Models/TenantPivot.php @@ -9,10 +9,8 @@ use Stancl\Tenancy\Contracts\Syncable; class TenantPivot extends Pivot { - public static function boot() + public static function booted() { - parent::boot(); - static::saved(function (self $pivot) { $parent = $pivot->pivotParent; diff --git a/src/Database/ParentModelScope.php b/src/Database/ParentModelScope.php index 387b1d19..78f5de20 100644 --- a/src/Database/ParentModelScope.php +++ b/src/Database/ParentModelScope.php @@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Scope; class ParentModelScope implements Scope { - public function apply(Builder $builder, Model $model) + public function apply(Builder $builder, Model $model): void { if (! tenancy()->initialized) { return; diff --git a/src/Database/TenantCollection.php b/src/Database/TenantCollection.php index ba3a8fab..45a137fe 100644 --- a/src/Database/TenantCollection.php +++ b/src/Database/TenantCollection.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database; +use Closure; use Illuminate\Database\Eloquent\Collection; use Stancl\Tenancy\Contracts\Tenant; @@ -16,7 +17,7 @@ use Stancl\Tenancy\Contracts\Tenant; */ class TenantCollection extends Collection { - public function runForEach(callable $callable): self + public function runForEach(Closure $callable): self { tenancy()->runForMultiple($this->items, $callable); diff --git a/src/DatabaseConfig.php b/src/DatabaseConfig.php index b3195960..60e70ce5 100644 --- a/src/DatabaseConfig.php +++ b/src/DatabaseConfig.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy; +use Closure; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; @@ -48,17 +49,17 @@ class DatabaseConfig $this->tenant = $tenant; } - public static function generateDatabaseNamesUsing(callable $databaseNameGenerator): void + public static function generateDatabaseNamesUsing(Closure $databaseNameGenerator): void { static::$databaseNameGenerator = $databaseNameGenerator; } - public static function generateUsernamesUsing(callable $usernameGenerator): void + public static function generateUsernamesUsing(Closure $usernameGenerator): void { static::$usernameGenerator = $usernameGenerator; } - public static function generatePasswordsUsing(callable $passwordGenerator): void + public static function generatePasswordsUsing(Closure $passwordGenerator): void { static::$passwordGenerator = $passwordGenerator; } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedById.php b/src/Exceptions/TenantCouldNotBeIdentifiedById.php deleted file mode 100644 index 5c2e562c..00000000 --- a/src/Exceptions/TenantCouldNotBeIdentifiedById.php +++ /dev/null @@ -1,28 +0,0 @@ -setSolutionDescription('Are you sure the ID is correct and the tenant exists?') - ->setDocumentationLinks([ - 'Initializing Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants', - ]); - } -} diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php b/src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php new file mode 100644 index 00000000..13b83daf --- /dev/null +++ b/src/Exceptions/TenantCouldNotBeIdentifiedByIdException.php @@ -0,0 +1,18 @@ +tenantCouldNotBeIdentified("by tenant id: $tenant_id") + ->title('Tenant could not be identified with that ID') + ->description('Are you sure the ID is correct and the tenant exists?'); + } +} diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php b/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php index 896c9323..5a494d90 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedByPathException.php @@ -4,24 +4,15 @@ declare(strict_types=1); namespace Stancl\Tenancy\Exceptions; -use Facade\IgnitionContracts\BaseSolution; -use Facade\IgnitionContracts\ProvidesSolution; -use Facade\IgnitionContracts\Solution; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; -class TenantCouldNotBeIdentifiedByPathException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution +class TenantCouldNotBeIdentifiedByPathException extends TenantCouldNotBeIdentifiedException { - public function __construct($tenant_id) + public function __construct(int|string $tenant_id) { - parent::__construct("Tenant could not be identified on path with tenant_id: $tenant_id"); - } - - public function getSolution(): Solution - { - return BaseSolution::create('Tenant could not be identified on this path') - ->setSolutionDescription('Did you forget to create a tenant for this path?') - ->setDocumentationLinks([ - 'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants/', - ]); + $this + ->tenantCouldNotBeIdentified("on path with tenant id: $tenant_id") + ->title('Tenant could not be identified on this path') + ->description('Did you forget to create a tenant for this path?'); } } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php b/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php index f0447d96..1f1c98a1 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php @@ -4,24 +4,15 @@ declare(strict_types=1); namespace Stancl\Tenancy\Exceptions; -use Facade\IgnitionContracts\BaseSolution; -use Facade\IgnitionContracts\ProvidesSolution; -use Facade\IgnitionContracts\Solution; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; -class TenantCouldNotBeIdentifiedByRequestDataException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution +class TenantCouldNotBeIdentifiedByRequestDataException extends TenantCouldNotBeIdentifiedException { - public function __construct($tenant_id) + public function __construct(mixed $payload) { - parent::__construct("Tenant could not be identified by request data with payload: $tenant_id"); - } - - public function getSolution(): Solution - { - return BaseSolution::create('Tenant could not be identified with this request data') - ->setSolutionDescription('Did you forget to create a tenant with this id?') - ->setDocumentationLinks([ - 'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants/', - ]); + $this + ->tenantCouldNotBeIdentified("by request data with payload: $payload") + ->title('Tenant could not be identified using this request data') + ->description('Did you forget to create a tenant with this id?'); } } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php b/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php index 66bc1db8..0421fe1b 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedOnDomainException.php @@ -4,24 +4,15 @@ declare(strict_types=1); namespace Stancl\Tenancy\Exceptions; -use Facade\IgnitionContracts\BaseSolution; -use Facade\IgnitionContracts\ProvidesSolution; -use Facade\IgnitionContracts\Solution; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; -class TenantCouldNotBeIdentifiedOnDomainException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution +class TenantCouldNotBeIdentifiedOnDomainException extends TenantCouldNotBeIdentifiedException { - public function __construct($domain) + public function __construct(string $domain) { - parent::__construct("Tenant could not be identified on domain $domain"); - } - - public function getSolution(): Solution - { - return BaseSolution::create('Tenant could not be identified on this domain') - ->setSolutionDescription('Did you forget to create a tenant for this domain?') - ->setDocumentationLinks([ - 'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v3/tenants/', - ]); + $this + ->tenantCouldNotBeIdentified("on domain $domain") + ->title('Tenant could not be identified on this domain') + ->description('Did you forget to create a tenant for this domain?'); } } diff --git a/src/Exceptions/TenantDatabaseAlreadyExistsException.php b/src/Exceptions/TenantDatabaseAlreadyExistsException.php index 4c08b66e..ce648625 100644 --- a/src/Exceptions/TenantDatabaseAlreadyExistsException.php +++ b/src/Exceptions/TenantDatabaseAlreadyExistsException.php @@ -8,18 +8,14 @@ use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; class TenantDatabaseAlreadyExistsException extends TenantCannotBeCreatedException { - /** @var string */ - protected $database; + public function __construct( + protected string $database, + ) { + parent::__construct(); + } public function reason(): string { return "Database {$this->database} already exists."; } - - public function __construct(string $database) - { - $this->database = $database; - - parent::__construct(); - } } diff --git a/src/Exceptions/TenantDatabaseDoesNotExistException.php b/src/Exceptions/TenantDatabaseDoesNotExistException.php index 2f6df0f9..9aa5c3c9 100644 --- a/src/Exceptions/TenantDatabaseDoesNotExistException.php +++ b/src/Exceptions/TenantDatabaseDoesNotExistException.php @@ -8,7 +8,7 @@ use Exception; class TenantDatabaseDoesNotExistException extends Exception { - public function __construct($database) + public function __construct(string $database) { parent::__construct("Database $database does not exist."); } diff --git a/src/Exceptions/TenantDatabaseUserAlreadyExistsException.php b/src/Exceptions/TenantDatabaseUserAlreadyExistsException.php index f84e39ec..00efe2b2 100644 --- a/src/Exceptions/TenantDatabaseUserAlreadyExistsException.php +++ b/src/Exceptions/TenantDatabaseUserAlreadyExistsException.php @@ -8,18 +8,14 @@ use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; class TenantDatabaseUserAlreadyExistsException extends TenantCannotBeCreatedException { - /** @var string */ - protected $user; + public function __construct( + protected string $user, + ) { + parent::__construct(); + } public function reason(): string { return "Database user {$this->user} already exists."; } - - public function __construct(string $user) - { - parent::__construct(); - - $this->user = $user; - } } diff --git a/src/Features/CrossDomainRedirect.php b/src/Features/CrossDomainRedirect.php index 6ebd15fc..0b6d7682 100644 --- a/src/Features/CrossDomainRedirect.php +++ b/src/Features/CrossDomainRedirect.php @@ -15,7 +15,7 @@ class CrossDomainRedirect implements Feature RedirectResponse::macro('domain', function (string $domain) { /** @var RedirectResponse $this */ - // replace first occurance of hostname fragment with $domain + // Replace first occurrence of the hostname fragment with $domain $url = $this->getTargetUrl(); $hostname = parse_url($url, PHP_URL_HOST); $position = strpos($url, $hostname); diff --git a/src/Features/UniversalRoutes.php b/src/Features/UniversalRoutes.php index c73a5304..e327b5d3 100644 --- a/src/Features/UniversalRoutes.php +++ b/src/Features/UniversalRoutes.php @@ -13,9 +13,10 @@ use Stancl\Tenancy\Tenancy; class UniversalRoutes implements Feature { - public static $middlewareGroup = 'universal'; + public static string $middlewareGroup = 'universal'; - public static $identificationMiddlewares = [ + // todo docblock + public static array $identificationMiddlewares = [ Middleware\InitializeTenancyByDomain::class, Middleware\InitializeTenancyBySubdomain::class, ]; @@ -39,7 +40,7 @@ class UniversalRoutes implements Feature } } - public static function routeHasMiddleware(Route $route, $middleware): bool + public static function routeHasMiddleware(Route $route, string $middleware): bool { if (in_array($middleware, $route->middleware(), true)) { return true; diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index f96465ff..021b44fc 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Features; -use Carbon\Carbon; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; use Stancl\Tenancy\Contracts\Feature; @@ -14,7 +13,8 @@ use Stancl\Tenancy\Tenancy; class UserImpersonation implements Feature { - public static $ttl = 60; // seconds + /** The lifespan of impersonation tokens (in seconds). */ + public static int $ttl = 60; public function bootstrap(Tenancy $tenancy): void { @@ -28,25 +28,20 @@ class UserImpersonation implements Feature }); } - /** - * Impersonate a user and get an HTTP redirect response. - * - * @param string|ImpersonationToken $token - * @param int $ttl - */ - public static function makeResponse($token, int $ttl = null): RedirectResponse + /** Impersonate a user and get an HTTP redirect response. */ + public static function makeResponse(string|ImpersonationToken $token, int $ttl = null): RedirectResponse { $token = $token instanceof ImpersonationToken ? $token : ImpersonationToken::findOrFail($token); + $ttl ??= static::$ttl; - if (((string) $token->tenant_id) !== ((string) tenant()->getTenantKey())) { - abort(403); - } + $tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl; - $ttl = $ttl ?? static::$ttl; + abort_if($tokenExpired, 403); - if ($token->created_at->diffInSeconds(Carbon::now()) > $ttl) { - abort(403); - } + $tokenTenantId = (string) $token->tenant_id; + $currentTenantId = (string) tenant()->getTenantKey(); + + abort_unless($tokenTenantId === $currentTenantId, 403); Auth::guard($token->auth_guard)->loginUsingId($token->user_id); diff --git a/src/Resolvers/Contracts/CachedTenantResolver.php b/src/Resolvers/Contracts/CachedTenantResolver.php index e84f1fb1..a3e3daeb 100644 --- a/src/Resolvers/Contracts/CachedTenantResolver.php +++ b/src/Resolvers/Contracts/CachedTenantResolver.php @@ -11,14 +11,11 @@ use Stancl\Tenancy\Contracts\TenantResolver; abstract class CachedTenantResolver implements TenantResolver { - /** @var bool */ - public static $shouldCache = false; + public static bool $shouldCache = false; // todo docblocks for these - /** @var int */ - public static $cacheTTL = 3600; // seconds + public static int $cacheTTL = 3600; // seconds - /** @var string|null */ - public static $cacheStore = null; // default + public static string|null $cacheStore = null; // default /** @var Repository */ protected $cache; diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index 13eeb448..44ec4fdc 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -11,21 +11,14 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; class DomainTenantResolver extends Contracts\CachedTenantResolver { - /** - * The model representing the domain that the tenant was identified on. - * - * @var Domain - */ - public static $currentDomain; + /** The model representing the domain that the tenant was identified on. */ + public static Domain $currentDomain; // todo |null? - /** @var bool */ - public static $shouldCache = false; + public static bool $shouldCache = false; - /** @var int */ - public static $cacheTTL = 3600; // seconds + public static int $cacheTTL = 3600; // seconds - /** @var string|null */ - public static $cacheStore = null; // default + public static string|null $cacheStore = null; // default public function resolveWithoutCache(...$args): Tenant { diff --git a/src/Resolvers/PathTenantResolver.php b/src/Resolvers/PathTenantResolver.php index 0b79626f..36a05a83 100644 --- a/src/Resolvers/PathTenantResolver.php +++ b/src/Resolvers/PathTenantResolver.php @@ -10,16 +10,13 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException; class PathTenantResolver extends Contracts\CachedTenantResolver { - public static $tenantParameterName = 'tenant'; + public static string $tenantParameterName = 'tenant'; - /** @var bool */ - public static $shouldCache = false; + public static bool $shouldCache = false; - /** @var int */ - public static $cacheTTL = 3600; // seconds + public static int $cacheTTL = 3600; // seconds - /** @var string|null */ - public static $cacheStore = null; // default + public static string|null $cacheStore = null; // default public function resolveWithoutCache(...$args): Tenant { diff --git a/src/Resolvers/RequestDataTenantResolver.php b/src/Resolvers/RequestDataTenantResolver.php index d9f2ebac..65d4ce38 100644 --- a/src/Resolvers/RequestDataTenantResolver.php +++ b/src/Resolvers/RequestDataTenantResolver.php @@ -9,14 +9,11 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException; class RequestDataTenantResolver extends Contracts\CachedTenantResolver { - /** @var bool */ - public static $shouldCache = false; + public static bool $shouldCache = false; - /** @var int */ - public static $cacheTTL = 3600; // seconds + public static int $cacheTTL = 3600; // seconds - /** @var string|null */ - public static $cacheStore = null; // default + public static string|null $cacheStore = null; // default public function resolveWithoutCache(...$args): Tenant { diff --git a/src/Tenancy.php b/src/Tenancy.php index ba151f22..c0f655c2 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -11,7 +11,7 @@ use Illuminate\Support\Traits\Macroable; use Stancl\Tenancy\Concerns\Debuggable; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedById; +use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException; class Tenancy { @@ -38,7 +38,7 @@ class Tenancy $tenant = $this->find($tenantId); if (! $tenant) { - throw new TenantCouldNotBeIdentifiedById($tenantId); + throw new TenantCouldNotBeIdentifiedByIdException($tenantId); } } @@ -62,17 +62,17 @@ class Tenancy public function end(): void { - event(new Events\EndingTenancy($this)); - if (! $this->initialized) { return; } - event(new Events\TenancyEnded($this)); + event(new Events\EndingTenancy($this)); + + $this->tenant = null; $this->initialized = false; - $this->tenant = null; + event(new Events\TenancyEnded($this)); } /** @return TenancyBootstrapper[] */ @@ -131,7 +131,7 @@ class Tenancy * * @param Tenant[]|\Traversable|string[]|null $tenants */ - public function runForMultiple($tenants, callable $callback): void + public function runForMultiple($tenants, Closure $callback): void { // Convert null to all tenants $tenants = is_null($tenants) ? $this->model()->cursor() : $tenants; diff --git a/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php b/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php index 0bc34623..044c35b6 100644 --- a/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php @@ -2,6 +2,8 @@ declare(strict_types=1); +// todo likely move all of these classes to Database\ + namespace Stancl\Tenancy\TenantDatabaseManagers; use Illuminate\Database\Connection; @@ -12,10 +14,9 @@ use Stancl\Tenancy\Exceptions\NoConnectionSetException; class MicrosoftSQLDatabaseManager implements TenantDatabaseManager { - /** @var string */ - protected $connection; + protected string $connection; // todo docblock, in all of these classes - protected function database(): Connection + protected function database(): Connection // todo consider abstracting this method & setConnection() into a base class { if ($this->connection === null) { throw new NoConnectionSetException(static::class); @@ -33,7 +34,7 @@ class MicrosoftSQLDatabaseManager implements TenantDatabaseManager { $database = $tenant->database()->getName(); $charset = $this->database()->getConfig('charset'); - $collation = $this->database()->getConfig('collation'); + $collation = $this->database()->getConfig('collation'); // todo check why these are not used return $this->database()->statement("CREATE DATABASE [{$database}]"); } diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index d50c9b6b..b4fd38f6 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -157,23 +157,25 @@ class AnotherTenant extends Model implements Contracts\Tenant return 'id'; } - public function getTenantKey() + public function getTenantKey(): int|string { return $this->getAttribute('id'); } - public function run(callable $callback) + public function run(Closure $callback): mixed { $callback(); } - public function getInternal(string $key) + public function getInternal(string $key): mixed { return $this->$key; } - public function setInternal(string $key, $value) + public function setInternal(string $key, mixed $value): static { $this->$key = $value; + + return $this; } } From 06cc7bf68623e4f91be5020d666847951ad148b6 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Fri, 26 Aug 2022 19:35:44 +0000 Subject: [PATCH 13/33] Fix code style (php-cs-fixer) --- src/Contracts/TenantCouldNotBeIdentifiedException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Contracts/TenantCouldNotBeIdentifiedException.php b/src/Contracts/TenantCouldNotBeIdentifiedException.php index 2f301c37..0066291f 100644 --- a/src/Contracts/TenantCouldNotBeIdentifiedException.php +++ b/src/Contracts/TenantCouldNotBeIdentifiedException.php @@ -20,7 +20,7 @@ abstract class TenantCouldNotBeIdentifiedException extends Exception implements /** Set the message. */ protected function tenantCouldNotBeIdentified(string $how): static { - $this->message = "Tenant could not be identified " . $how; + $this->message = 'Tenant could not be identified ' . $how; return $this; } From 02faf2407f52865c452278f7fc38e4a42640246f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 26 Aug 2022 21:39:05 +0200 Subject: [PATCH 14/33] Laravel 9 only --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56f90c64..a9da7221 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - laravel: ['^8.0', '^9.0'] + laravel: ['^9.0'] steps: - name: Checkout From 5f41647a6b349f103962c4ba62bb74f7698b7fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 26 Aug 2022 21:49:09 +0200 Subject: [PATCH 15/33] revert Tenancy changes, fix failing tests --- composer.json | 8 ++++---- src/Tenancy.php | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 03f08244..0df504b7 100644 --- a/composer.json +++ b/composer.json @@ -17,16 +17,16 @@ "require": { "php": "^8.1", "ext-json": "*", - "illuminate/support": "^8.0|^9.0", + "illuminate/support": "^9.0", "facade/ignition-contracts": "^1.0", "ramsey/uuid": "^4.0", "stancl/jobpipeline": "^1.0", "stancl/virtualcolumn": "^1.0" }, "require-dev": { - "laravel/framework": "^8.0|^9.0", - "orchestra/testbench": "^6.0|^7.0", - "league/flysystem-aws-s3-v3": "^1.0|^3.0", + "laravel/framework": "^9.0", + "orchestra/testbench": "^7.0", + "league/flysystem-aws-s3-v3": "^3.0", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5", "pestphp/pest": "^1.21" diff --git a/src/Tenancy.php b/src/Tenancy.php index c0f655c2..d08e34ce 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -68,11 +68,13 @@ class Tenancy event(new Events\EndingTenancy($this)); + // todo find a way to refactor these two methods + + event(new Events\TenancyEnded($this)); + $this->tenant = null; $this->initialized = false; - - event(new Events\TenancyEnded($this)); } /** @return TenancyBootstrapper[] */ From 2d7e9dfd89387d348b31424668ff7e883801a0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 26 Aug 2022 22:29:40 +0200 Subject: [PATCH 16/33] Add 'composer coverage' --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 0df504b7..c155743e 100644 --- a/composer.json +++ b/composer.json @@ -60,6 +60,7 @@ "docker-down": "PHP_VERSION=8.1 docker-compose down", "docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build", "docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml", + "coverage": "open coverage/phpunit/html/index.html", "test": "PHP_VERSION=8.1 ./test" }, "minimum-stability": "dev", From 40bf28c7d0b7abd252f34af00876ada226d398c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 27 Aug 2022 03:17:16 +0200 Subject: [PATCH 17/33] general refactor, move more classes under Database namespace --- assets/config.php | 12 +++---- composer.json | 3 +- .../DatabaseTenancyBootstrapper.php | 2 +- src/CacheManager.php | 2 ++ src/Contracts/TenantWithDatabase.php | 15 --------- src/Controllers/TenantAssetsController.php | 5 +-- .../Concerns/CreatesDatabaseUsers.php | 4 +-- src/Database/Concerns/HasDatabase.php | 4 +-- src/Database/Concerns/HasDomains.php | 1 + .../Contracts/ManagesDatabaseUsers.php | 6 ++-- .../Contracts/TenantDatabaseManager.php | 4 +-- src/Database/Contracts/TenantWithDatabase.php | 13 ++++++++ src/{ => Database}/DatabaseConfig.php | 32 +++++++------------ src/Database/DatabaseManager.php | 14 +++----- .../DatabaseManagerNotRegisteredException.php | 15 +++++++++ .../Exceptions/NoConnectionSetException.php | 2 +- .../TenantDatabaseAlreadyExistsException.php | 2 +- .../TenantDatabaseDoesNotExistException.php | 2 +- ...nantDatabaseUserAlreadyExistsException.php | 2 +- .../MicrosoftSQLDatabaseManager.php | 8 ++--- .../MySQLDatabaseManager.php | 8 ++--- ...rmissionControlledMySQLDatabaseManager.php | 8 ++--- .../PostgreSQLDatabaseManager.php | 8 ++--- .../PostgreSQLSchemaManager.php | 8 ++--- .../SQLiteDatabaseManager.php | 10 +++--- ...SyncedResourceChangedInForeignDatabase.php | 2 +- src/Events/SyncedResourceSaved.php | 2 +- .../DatabaseManagerNotRegisteredException.php | 13 -------- ...enantCouldNotBeIdentifiedByIdException.php | 2 ++ src/Jobs/CreateDatabase.php | 2 +- src/Jobs/DeleteDatabase.php | 2 +- src/Jobs/DeleteDomains.php | 2 +- src/Jobs/MigrateDatabase.php | 2 +- src/Jobs/SeedDatabase.php | 2 +- src/UUIDGenerator.php | 2 ++ tests/DatabasePreparationTest.php | 2 +- tests/DatabaseUsersTest.php | 6 ++-- tests/Etc/Tenant.php | 2 +- tests/ResourceSyncingTest.php | 2 +- tests/TenantDatabaseManagerTest.php | 16 +++++----- tests/TestCase.php | 6 +--- 41 files changed, 122 insertions(+), 133 deletions(-) delete mode 100644 src/Contracts/TenantWithDatabase.php rename src/{ => Database}/Concerns/CreatesDatabaseUsers.php (80%) rename src/{ => Database}/Contracts/ManagesDatabaseUsers.php (74%) rename src/{ => Database}/Contracts/TenantDatabaseManager.php (86%) create mode 100644 src/Database/Contracts/TenantWithDatabase.php rename src/{ => Database}/DatabaseConfig.php (84%) create mode 100644 src/Database/Exceptions/DatabaseManagerNotRegisteredException.php rename src/{ => Database}/Exceptions/NoConnectionSetException.php (83%) rename src/{ => Database}/Exceptions/TenantDatabaseAlreadyExistsException.php (89%) rename src/{ => Database}/Exceptions/TenantDatabaseDoesNotExistException.php (84%) rename src/{ => Database}/Exceptions/TenantDatabaseUserAlreadyExistsException.php (89%) rename src/{ => Database}/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php (87%) rename src/{ => Database}/TenantDatabaseManagers/MySQLDatabaseManager.php (86%) rename src/{ => Database}/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php (90%) rename src/{ => Database}/TenantDatabaseManagers/PostgreSQLDatabaseManager.php (84%) rename src/{ => Database}/TenantDatabaseManagers/PostgreSQLSchemaManager.php (85%) rename src/{ => Database}/TenantDatabaseManagers/SQLiteDatabaseManager.php (79%) delete mode 100644 src/Exceptions/DatabaseManagerNotRegisteredException.php diff --git a/assets/config.php b/assets/config.php index e1c82e6b..2a54e0b9 100644 --- a/assets/config.php +++ b/assets/config.php @@ -58,22 +58,22 @@ return [ * TenantDatabaseManagers are classes that handle the creation & deletion of tenant databases. */ 'managers' => [ - 'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class, - 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class, - 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, - 'sqlsrv' => Stancl\Tenancy\TenantDatabaseManagers\MicrosoftSQLDatabaseManager::class, + 'sqlite' => Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager::class, + 'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\MySQLDatabaseManager::class, + 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, + 'sqlsrv' => Stancl\Tenancy\Database\TenantDatabaseManagers\MicrosoftSQLDatabaseManager::class, /** * Use this database manager for MySQL to have a DB user created for each tenant database. * You can customize the grants given to these users by changing the $grants property. */ - // 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class, + // 'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class, /** * Disable the pgsql manager above, and enable the one below if you * want to separate tenant DBs by schemas rather than databases. */ - // 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database + // 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database ], ], diff --git a/composer.json b/composer.json index c155743e..36e1c1b3 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,8 @@ "docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build", "docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml", "coverage": "open coverage/phpunit/html/index.html", - "test": "PHP_VERSION=8.1 ./test" + "test": "PHP_VERSION=8.1 ./test --no-coverage", + "test-full": "PHP_VERSION=8.1 ./test" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Bootstrappers/DatabaseTenancyBootstrapper.php b/src/Bootstrappers/DatabaseTenancyBootstrapper.php index 59ee0aec..dd94bfd4 100644 --- a/src/Bootstrappers/DatabaseTenancyBootstrapper.php +++ b/src/Bootstrappers/DatabaseTenancyBootstrapper.php @@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Bootstrappers; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; -use Stancl\Tenancy\Contracts\TenantWithDatabase; +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException; diff --git a/src/CacheManager.php b/src/CacheManager.php index eb6bac4c..f28134b8 100644 --- a/src/CacheManager.php +++ b/src/CacheManager.php @@ -6,6 +6,8 @@ namespace Stancl\Tenancy; use Illuminate\Cache\CacheManager as BaseCacheManager; +// todo move to Cache namespace? + class CacheManager extends BaseCacheManager { /** diff --git a/src/Contracts/TenantWithDatabase.php b/src/Contracts/TenantWithDatabase.php deleted file mode 100644 index 98268502..00000000 --- a/src/Contracts/TenantWithDatabase.php +++ /dev/null @@ -1,15 +0,0 @@ -file(storage_path("app/public/$path")); - } catch (Throwable $th) { + } catch (Throwable) { abort(404); } } diff --git a/src/Concerns/CreatesDatabaseUsers.php b/src/Database/Concerns/CreatesDatabaseUsers.php similarity index 80% rename from src/Concerns/CreatesDatabaseUsers.php rename to src/Database/Concerns/CreatesDatabaseUsers.php index 863dd4a7..f329f071 100644 --- a/src/Concerns/CreatesDatabaseUsers.php +++ b/src/Database/Concerns/CreatesDatabaseUsers.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Concerns; +namespace Stancl\Tenancy\Database\Concerns; -use Stancl\Tenancy\Contracts\TenantWithDatabase; +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; trait CreatesDatabaseUsers { diff --git a/src/Database/Concerns/HasDatabase.php b/src/Database/Concerns/HasDatabase.php index aab381bd..42e6555a 100644 --- a/src/Database/Concerns/HasDatabase.php +++ b/src/Database/Concerns/HasDatabase.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; -use Stancl\Tenancy\Contracts\TenantWithDatabase; -use Stancl\Tenancy\DatabaseConfig; +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; +use Stancl\Tenancy\Database\DatabaseConfig; trait HasDatabase { diff --git a/src/Database/Concerns/HasDomains.php b/src/Database/Concerns/HasDomains.php index 39411fea..a510fc62 100644 --- a/src/Database/Concerns/HasDomains.php +++ b/src/Database/Concerns/HasDomains.php @@ -2,6 +2,7 @@ declare(strict_types=1); +// todo not sure if this should be in Database\ namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Domain; diff --git a/src/Contracts/ManagesDatabaseUsers.php b/src/Database/Contracts/ManagesDatabaseUsers.php similarity index 74% rename from src/Contracts/ManagesDatabaseUsers.php rename to src/Database/Contracts/ManagesDatabaseUsers.php index d4da1c3c..83b9fad5 100644 --- a/src/Contracts/ManagesDatabaseUsers.php +++ b/src/Database/Contracts/ManagesDatabaseUsers.php @@ -2,11 +2,9 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Contracts; +namespace Stancl\Tenancy\Database\Contracts; -use Stancl\Tenancy\DatabaseConfig; - -// todo possibly move to Database namespace, along with other classes +use Stancl\Tenancy\Database\DatabaseConfig; interface ManagesDatabaseUsers extends TenantDatabaseManager { diff --git a/src/Contracts/TenantDatabaseManager.php b/src/Database/Contracts/TenantDatabaseManager.php similarity index 86% rename from src/Contracts/TenantDatabaseManager.php rename to src/Database/Contracts/TenantDatabaseManager.php index deaecb33..474ee016 100644 --- a/src/Contracts/TenantDatabaseManager.php +++ b/src/Database/Contracts/TenantDatabaseManager.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Contracts; +namespace Stancl\Tenancy\Database\Contracts; -use Stancl\Tenancy\Exceptions\NoConnectionSetException; +use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; interface TenantDatabaseManager { diff --git a/src/Database/Contracts/TenantWithDatabase.php b/src/Database/Contracts/TenantWithDatabase.php new file mode 100644 index 00000000..c9247d94 --- /dev/null +++ b/src/Database/Contracts/TenantWithDatabase.php @@ -0,0 +1,13 @@ +tenant->setInternal('db_name', $this->getName() ?? (static::$databaseNameGenerator)($this->tenant)); - if ($this->manager() instanceof ManagesDatabaseUsers) { + if ($this->manager() instanceof Contracts\ManagesDatabaseUsers) { $this->tenant->setInternal('db_username', $this->getUsername() ?? (static::$usernameGenerator)($this->tenant)); $this->tenant->setInternal('db_password', $this->getPassword() ?? (static::$passwordGenerator)($this->tenant)); } @@ -143,20 +137,18 @@ class DatabaseConfig }, []); } - /** - * Get the TenantDatabaseManager for this tenant's connection. - */ - public function manager(): TenantDatabaseManager + /** Get the TenantDatabaseManager for this tenant's connection. */ + public function manager(): Contracts\TenantDatabaseManager { $driver = config("database.connections.{$this->getTemplateConnectionName()}.driver"); $databaseManagers = config('tenancy.database.managers'); if (! array_key_exists($driver, $databaseManagers)) { - throw new DatabaseManagerNotRegisteredException($driver); + throw new Exceptions\DatabaseManagerNotRegisteredException($driver); } - /** @var TenantDatabaseManager $databaseManager */ + /** @var Contracts\TenantDatabaseManager $databaseManager */ $databaseManager = app($databaseManagers[$driver]); $databaseManager->setConnection($this->getTemplateConnectionName()); diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index eb807696..00afe938 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -7,14 +7,8 @@ namespace Stancl\Tenancy\Database; use Illuminate\Config\Repository; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; -use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; -use Stancl\Tenancy\Contracts\TenantWithDatabase; -use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; -use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; -use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; - -// todo move to Database namespace +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; /** * @internal Class is subject to breaking changes in minor and patch versions. @@ -97,11 +91,11 @@ class DatabaseManager $manager = $tenant->database()->manager(); if ($manager->databaseExists($database = $tenant->database()->getName())) { - throw new TenantDatabaseAlreadyExistsException($database); + throw new Exceptions\TenantDatabaseAlreadyExistsException($database); } - if ($manager instanceof ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) { - throw new TenantDatabaseUserAlreadyExistsException($username); + if ($manager instanceof Contracts\ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) { + throw new Exceptions\TenantDatabaseUserAlreadyExistsException($username); } } } diff --git a/src/Database/Exceptions/DatabaseManagerNotRegisteredException.php b/src/Database/Exceptions/DatabaseManagerNotRegisteredException.php new file mode 100644 index 00000000..d499df5d --- /dev/null +++ b/src/Database/Exceptions/DatabaseManagerNotRegisteredException.php @@ -0,0 +1,15 @@ +database()->getName()), ''); - } catch (\Throwable $th) { + } catch (\Throwable) { return false; } } @@ -22,7 +22,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager { try { return unlink(database_path($tenant->database()->getName())); - } catch (\Throwable $th) { + } catch (\Throwable) { return false; } } diff --git a/src/Events/SyncedResourceChangedInForeignDatabase.php b/src/Events/SyncedResourceChangedInForeignDatabase.php index 1830a5c1..e54ac1b7 100644 --- a/src/Events/SyncedResourceChangedInForeignDatabase.php +++ b/src/Events/SyncedResourceChangedInForeignDatabase.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Events; use Stancl\Tenancy\Contracts\Syncable; -use Stancl\Tenancy\Contracts\TenantWithDatabase; +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; class SyncedResourceChangedInForeignDatabase { diff --git a/src/Events/SyncedResourceSaved.php b/src/Events/SyncedResourceSaved.php index 898bac48..23e56e21 100644 --- a/src/Events/SyncedResourceSaved.php +++ b/src/Events/SyncedResourceSaved.php @@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Events; use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Syncable; -use Stancl\Tenancy\Contracts\TenantWithDatabase; +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; class SyncedResourceSaved { diff --git a/src/Exceptions/DatabaseManagerNotRegisteredException.php b/src/Exceptions/DatabaseManagerNotRegisteredException.php deleted file mode 100644 index e93c31a1..00000000 --- a/src/Exceptions/DatabaseManagerNotRegisteredException.php +++ /dev/null @@ -1,13 +0,0 @@ - \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, + 'tenancy.database.managers.pgsql' => \Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLSchemaManager::class, 'tenancy.boostrappers' => [ DatabaseTenancyBootstrapper::class, ], diff --git a/tests/TestCase.php b/tests/TestCase.php index 554aeb8d..1c6c6d8a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -48,11 +48,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase protected function getEnvironmentSetUp($app) { if (file_exists(__DIR__ . '/../.env')) { - if (method_exists(\Dotenv\Dotenv::class, 'createImmutable')) { - \Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->load(); - } else { - \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); - } + \Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->load(); } $app['config']->set([ From 824292e6df0cb2518f6e962981e837caade8f0ce Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Sat, 27 Aug 2022 01:17:45 +0000 Subject: [PATCH 18/33] Fix code style (php-cs-fixer) --- src/Database/Concerns/HasDomains.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/Concerns/HasDomains.php b/src/Database/Concerns/HasDomains.php index a510fc62..594e9a81 100644 --- a/src/Database/Concerns/HasDomains.php +++ b/src/Database/Concerns/HasDomains.php @@ -3,6 +3,7 @@ declare(strict_types=1); // todo not sure if this should be in Database\ + namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Domain; From d2e1ce0a1eaf21aa167f527e7181fc350ffdcbf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 27 Aug 2022 22:29:08 +0200 Subject: [PATCH 19/33] refactor TenantDatabaseManagers --- src/Concerns/Debuggable.php | 2 ++ src/Database/DatabaseConfig.php | 11 +++--- .../MicrosoftSQLDatabaseManager.php | 31 +--------------- .../MySQLDatabaseManager.php | 30 +--------------- ...rmissionControlledMySQLDatabaseManager.php | 7 ---- .../PostgreSQLDatabaseManager.php | 30 +--------------- .../PostgreSQLSchemaManager.php | 23 +----------- .../SQLiteDatabaseManager.php | 5 +-- .../TenantDatabaseManager.php | 35 +++++++++++++++++++ src/Resolvers/DomainTenantResolver.php | 4 +-- 10 files changed, 52 insertions(+), 126 deletions(-) create mode 100644 src/Database/TenantDatabaseManagers/TenantDatabaseManager.php diff --git a/src/Concerns/Debuggable.php b/src/Concerns/Debuggable.php index 98697f1d..ff781f89 100644 --- a/src/Concerns/Debuggable.php +++ b/src/Concerns/Debuggable.php @@ -9,6 +9,8 @@ use Stancl\Tenancy\Enums\LogMode; use Stancl\Tenancy\Events\Contracts\TenancyEvent; use Stancl\Tenancy\Tenancy; +// todo finish this feature + /** * @mixin Tenancy */ diff --git a/src/Database/DatabaseConfig.php b/src/Database/DatabaseConfig.php index 4c33348d..a4c79582 100644 --- a/src/Database/DatabaseConfig.php +++ b/src/Database/DatabaseConfig.php @@ -12,14 +12,17 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase as Tenant; class DatabaseConfig { - // todo docblocks + /** The tenant whose database we're dealing with. */ public Tenant&Model $tenant; - public static Closure $usernameGenerator; + /** Database username generator (can be set by the developer.) */ + public static Closure|null $usernameGenerator = null; - public static Closure $passwordGenerator; + /** Database password generator (can be set by the developer.) */ + public static Closure|null $passwordGenerator = null; - public static Closure $databaseNameGenerator; + /** Database name generator (can be set by the developer.) */ + public static Closure|null $databaseNameGenerator = null; public static function __constructStatic(): void { diff --git a/src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php b/src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php index 241a879a..65950baa 100644 --- a/src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php @@ -2,34 +2,12 @@ declare(strict_types=1); -// todo likely move all of these classes to Database\ - namespace Stancl\Tenancy\Database\TenantDatabaseManagers; -use Illuminate\Database\Connection; -use Illuminate\Support\Facades\DB; -use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; -use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; -class MicrosoftSQLDatabaseManager implements TenantDatabaseManager +class MicrosoftSQLDatabaseManager extends TenantDatabaseManager { - protected string $connection; // todo docblock, in all of these classes - - protected function database(): Connection // todo consider abstracting this method & setConnection() into a base class - { - if ($this->connection === null) { - throw new NoConnectionSetException(static::class); - } - - return DB::connection($this->connection); - } - - public function setConnection(string $connection): void - { - $this->connection = $connection; - } - public function createDatabase(TenantWithDatabase $tenant): bool { $database = $tenant->database()->getName(); @@ -48,11 +26,4 @@ class MicrosoftSQLDatabaseManager implements TenantDatabaseManager { return (bool) $this->database()->select("SELECT name FROM master.sys.databases WHERE name = '$name'"); } - - public function makeConnectionConfig(array $baseConfig, string $databaseName): array - { - $baseConfig['database'] = $databaseName; - - return $baseConfig; - } } diff --git a/src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php b/src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php index 92a434c2..c96c162b 100644 --- a/src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/MySQLDatabaseManager.php @@ -4,31 +4,10 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\TenantDatabaseManagers; -use Illuminate\Database\Connection; -use Illuminate\Support\Facades\DB; -use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; -use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; -class MySQLDatabaseManager implements TenantDatabaseManager +class MySQLDatabaseManager extends TenantDatabaseManager { - /** @var string */ - protected $connection; - - protected function database(): Connection - { - if ($this->connection === null) { - throw new NoConnectionSetException(static::class); - } - - return DB::connection($this->connection); - } - - public function setConnection(string $connection): void - { - $this->connection = $connection; - } - public function createDatabase(TenantWithDatabase $tenant): bool { $database = $tenant->database()->getName(); @@ -47,11 +26,4 @@ class MySQLDatabaseManager implements TenantDatabaseManager { return (bool) $this->database()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); } - - public function makeConnectionConfig(array $baseConfig, string $databaseName): array - { - $baseConfig['database'] = $databaseName; - - return $baseConfig; - } } diff --git a/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php index 30913f59..337864dc 100644 --- a/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php @@ -54,11 +54,4 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl { return (bool) $this->database()->select("SELECT count(*) FROM mysql.user WHERE user = '$username'")[0]->{'count(*)'}; } - - public function makeConnectionConfig(array $baseConfig, string $databaseName): array - { - $baseConfig['database'] = $databaseName; - - return $baseConfig; - } } diff --git a/src/Database/TenantDatabaseManagers/PostgreSQLDatabaseManager.php b/src/Database/TenantDatabaseManagers/PostgreSQLDatabaseManager.php index d342b7b0..6a3aee2e 100644 --- a/src/Database/TenantDatabaseManagers/PostgreSQLDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/PostgreSQLDatabaseManager.php @@ -4,31 +4,10 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\TenantDatabaseManagers; -use Illuminate\Database\Connection; -use Illuminate\Support\Facades\DB; -use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; -use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; -class PostgreSQLDatabaseManager implements TenantDatabaseManager +class PostgreSQLDatabaseManager extends TenantDatabaseManager { - /** @var string */ - protected $connection; - - protected function database(): Connection - { - if ($this->connection === null) { - throw new NoConnectionSetException(static::class); - } - - return DB::connection($this->connection); - } - - public function setConnection(string $connection): void - { - $this->connection = $connection; - } - public function createDatabase(TenantWithDatabase $tenant): bool { return $this->database()->statement("CREATE DATABASE \"{$tenant->database()->getName()}\" WITH TEMPLATE=template0"); @@ -43,11 +22,4 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager { return (bool) $this->database()->select("SELECT datname FROM pg_database WHERE datname = '$name'"); } - - public function makeConnectionConfig(array $baseConfig, string $databaseName): array - { - $baseConfig['database'] = $databaseName; - - return $baseConfig; - } } diff --git a/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php index 1a3dcc77..fa5aa593 100644 --- a/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/Database/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -4,31 +4,10 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\TenantDatabaseManagers; -use Illuminate\Database\Connection; -use Illuminate\Support\Facades\DB; -use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; -use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; -class PostgreSQLSchemaManager implements TenantDatabaseManager +class PostgreSQLSchemaManager extends TenantDatabaseManager { - /** @var string */ - protected $connection; - - protected function database(): Connection - { - if ($this->connection === null) { - throw new NoConnectionSetException(static::class); - } - - return DB::connection($this->connection); - } - - public function setConnection(string $connection): void - { - $this->connection = $connection; - } - public function createDatabase(TenantWithDatabase $tenant): bool { return $this->database()->statement("CREATE SCHEMA \"{$tenant->database()->getName()}\""); diff --git a/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php b/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php index 6f3a8f62..59c373a9 100644 --- a/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/SQLiteDatabaseManager.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Database\TenantDatabaseManagers; use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; +use Throwable; class SQLiteDatabaseManager implements TenantDatabaseManager { @@ -13,7 +14,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager { try { return file_put_contents(database_path($tenant->database()->getName()), ''); - } catch (\Throwable) { + } catch (Throwable) { return false; } } @@ -22,7 +23,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager { try { return unlink(database_path($tenant->database()->getName())); - } catch (\Throwable) { + } catch (Throwable) { return false; } } diff --git a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php new file mode 100644 index 00000000..f0751f0d --- /dev/null +++ b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php @@ -0,0 +1,35 @@ +connection)) { + throw new NoConnectionSetException(static::class); + } + + return DB::connection($this->connection); + } + + public function setConnection(string $connection): void + { + $this->connection = $connection; + } + + public function makeConnectionConfig(array $baseConfig, string $databaseName): array + { + $baseConfig['database'] = $databaseName; + + return $baseConfig; + } +} diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index 44ec4fdc..eb25ad26 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -26,9 +26,7 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver /** @var Tenant|null $tenant */ $tenant = config('tenancy.tenant_model')::query() - ->whereHas('domains', function (Builder $query) use ($domain) { - $query->where('domain', $domain); - }) + ->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain)) ->with('domains') ->first(); From eade69c3f469832c42caabc9af1f2731ec7c6cf1 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Sat, 27 Aug 2022 20:29:46 +0000 Subject: [PATCH 20/33] Fix code style (php-cs-fixer) --- src/Database/TenantDatabaseManagers/TenantDatabaseManager.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php index f0751f0d..b7dd15fa 100644 --- a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php @@ -1,5 +1,7 @@ Date: Mon, 29 Aug 2022 23:55:33 +0500 Subject: [PATCH 21/33] add phpstan (#926) --- composer.json | 3 ++- phpstan.neon | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 36e1c1b3..16822ac3 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "league/flysystem-aws-s3-v3": "^3.0", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5", - "pestphp/pest": "^1.21" + "pestphp/pest": "^1.21", + "nunomaduro/larastan": "^1.0" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..c84dfecb --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,18 @@ +includes: + - ./vendor/nunomaduro/larastan/extension.neon + +parameters: + paths: + - src + # - tests + + level: 8 + + universalObjectCratesClasses: + - Illuminate\Routing\Route + + ignoreErrors: + + checkMissingIterableValueType: false + treatPhpDocTypesAsCertain: false + From 8af354c20e9678abb8ef7a25d8363c11dfc45807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 30 Aug 2022 05:44:23 +0200 Subject: [PATCH 22/33] get phpstan errors down from 252 to 189 --- CONTRIBUTING.md | 16 ++++++++++++++++ composer.json | 1 + phpstan.neon | 9 ++++++++- src/Contracts/Syncable.php | 4 ++-- src/Contracts/TenantResolver.php | 2 +- src/Controllers/TenantAssetsController.php | 2 +- .../Concerns/BelongsToPrimaryModel.php | 2 +- src/Database/Concerns/BelongsToTenant.php | 2 +- .../Concerns/ConvertsDomainsToLowercase.php | 2 +- .../Concerns/EnsuresDomainIsNotOccupied.php | 2 +- src/Database/Concerns/GeneratesIds.php | 2 +- .../Concerns/InvalidatesResolverCache.php | 2 +- .../InvalidatesTenantsResolverCache.php | 2 +- src/Database/Concerns/ResourceSyncing.php | 4 ++-- src/Database/DatabaseManager.php | 10 +++++----- .../Exceptions/NoConnectionSetException.php | 2 +- src/Database/Models/Domain.php | 3 ++- src/Database/Models/ImpersonationToken.php | 14 +++++++------- src/Database/Models/TenantPivot.php | 2 +- src/Events/SyncedResourceSaved.php | 9 ++++----- src/Features/TenantConfig.php | 13 ++++++------- src/Features/UserImpersonation.php | 1 + src/Jobs/CreateDatabase.php | 10 +++------- src/Jobs/DeleteDatabase.php | 13 +++++-------- src/Jobs/DeleteDomains.php | 10 ++++++---- src/Jobs/MigrateDatabase.php | 18 +++++------------- src/Jobs/SeedDatabase.php | 18 +++++------------- src/Listeners/BootstrapTenancy.php | 2 +- src/Listeners/CreateTenantConnection.php | 2 +- src/Listeners/QueueableListener.php | 4 ++-- src/Listeners/RevertToCentralContext.php | 2 +- src/Listeners/UpdateSyncedResource.php | 11 ++++++----- .../Contracts/CachedTenantResolver.php | 6 +++--- src/Resolvers/DomainTenantResolver.php | 2 +- src/Resolvers/PathTenantResolver.php | 2 +- src/Resolvers/RequestDataTenantResolver.php | 2 +- src/Tenancy.php | 3 ++- src/helpers.php | 17 ++++++++--------- tests/EventListenerTest.php | 2 +- tests/ResourceSyncingTest.php | 4 ++-- 40 files changed, 119 insertions(+), 115 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12e5e55b..d76a686a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,24 @@ php-cs-fixer will fix code style violations in your pull requests. Run `composer docker-up` to start the containers. Then run `composer test` to run the tests. +If you need to pass additional flags to phpunit, use `./test --foo` instead of `composer test --foo`. Composer scripts unfortunately don't pass CLI arguments. + When you're done testing, run `composer docker-down` to shut down the containers. ### Docker on M1 Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1. + +### Coverage reports + +To run tests and generate coverage reports, use `composer test-full`. + +To view the coverage reports in your browser, use `composer coverage` (works on macOS; on other operating systems you can manually open `coverage/phpunit/html/index.html` in your browser). + +### Rebuilding containers + +If you need to rebuild the container for any reason (e.g. a change in `Dockerfile`), you can use `composer docker-rebuild`. + +## PHPStan + +Use `composer phpstan` to run our phpstan suite. diff --git a/composer.json b/composer.json index 16822ac3..ff5befd9 100644 --- a/composer.json +++ b/composer.json @@ -62,6 +62,7 @@ "docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build", "docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml", "coverage": "open coverage/phpunit/html/index.html", + "phpstan": "vendor/bin/phpstan", "test": "PHP_VERSION=8.1 ./test --no-coverage", "test-full": "PHP_VERSION=8.1 ./test" }, diff --git a/phpstan.neon b/phpstan.neon index c84dfecb..9ff082dd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -12,7 +12,14 @@ parameters: - Illuminate\Routing\Route ignoreErrors: + - + message: '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#' + paths: + - src/TenancyServiceProvider.php + - + message: '#invalid type Laravel\\Telescope\\IncomingEntry#' + paths: + - src/Features/TelescopeTags.php checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false - diff --git a/src/Contracts/Syncable.php b/src/Contracts/Syncable.php index 1f6e36e7..e09f4f7e 100644 --- a/src/Contracts/Syncable.php +++ b/src/Contracts/Syncable.php @@ -8,11 +8,11 @@ interface Syncable { public function getGlobalIdentifierKeyName(): string; - public function getGlobalIdentifierKey(); + public function getGlobalIdentifierKey(): string|int; public function getCentralModelName(): string; public function getSyncedAttributeNames(): array; - public function triggerSyncEvent(); + public function triggerSyncEvent(): void; } diff --git a/src/Contracts/TenantResolver.php b/src/Contracts/TenantResolver.php index bba72f34..b4b45f4b 100644 --- a/src/Contracts/TenantResolver.php +++ b/src/Contracts/TenantResolver.php @@ -11,5 +11,5 @@ interface TenantResolver * * @throws TenantCouldNotBeIdentifiedException */ - public function resolve(...$args): Tenant; + public function resolve(mixed ...$args): Tenant; } diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetsController.php index 893f5662..03d600d0 100644 --- a/src/Controllers/TenantAssetsController.php +++ b/src/Controllers/TenantAssetsController.php @@ -17,7 +17,7 @@ class TenantAssetsController extends Controller $this->middleware(static::$tenancyMiddleware); } - public function asset($path = null) + public function asset(string $path = null) { abort_if($path === null, 404); diff --git a/src/Database/Concerns/BelongsToPrimaryModel.php b/src/Database/Concerns/BelongsToPrimaryModel.php index 72101d6a..a50f9d9e 100644 --- a/src/Database/Concerns/BelongsToPrimaryModel.php +++ b/src/Database/Concerns/BelongsToPrimaryModel.php @@ -10,7 +10,7 @@ trait BelongsToPrimaryModel { abstract public function getRelationshipToPrimaryModel(): string; - public static function bootBelongsToPrimaryModel() + public static function bootBelongsToPrimaryModel(): void { static::addGlobalScope(new ParentModelScope); } diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index 5410758d..ade966a8 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -19,7 +19,7 @@ trait BelongsToTenant return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn); } - public static function bootBelongsToTenant() + public static function bootBelongsToTenant(): void { static::addGlobalScope(new TenantScope); diff --git a/src/Database/Concerns/ConvertsDomainsToLowercase.php b/src/Database/Concerns/ConvertsDomainsToLowercase.php index e3f0c6ac..8068902d 100644 --- a/src/Database/Concerns/ConvertsDomainsToLowercase.php +++ b/src/Database/Concerns/ConvertsDomainsToLowercase.php @@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Database\Concerns; trait ConvertsDomainsToLowercase { - public static function bootConvertsDomainsToLowercase() + public static function bootConvertsDomainsToLowercase(): void { static::saving(function ($model) { $model->domain = strtolower($model->domain); diff --git a/src/Database/Concerns/EnsuresDomainIsNotOccupied.php b/src/Database/Concerns/EnsuresDomainIsNotOccupied.php index 2b1c56ee..a853c437 100644 --- a/src/Database/Concerns/EnsuresDomainIsNotOccupied.php +++ b/src/Database/Concerns/EnsuresDomainIsNotOccupied.php @@ -8,7 +8,7 @@ use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException; trait EnsuresDomainIsNotOccupied { - public static function bootEnsuresDomainIsNotOccupied() + public static function bootEnsuresDomainIsNotOccupied(): void { static::saving(function ($self) { if ($domain = $self->newQuery()->where('domain', $self->domain)->first()) { diff --git a/src/Database/Concerns/GeneratesIds.php b/src/Database/Concerns/GeneratesIds.php index d2796ba4..301ea385 100644 --- a/src/Database/Concerns/GeneratesIds.php +++ b/src/Database/Concerns/GeneratesIds.php @@ -8,7 +8,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; trait GeneratesIds { - public static function bootGeneratesIds() + public static function bootGeneratesIds(): void { static::creating(function (self $model) { if (! $model->getKey() && $model->shouldGenerateId()) { diff --git a/src/Database/Concerns/InvalidatesResolverCache.php b/src/Database/Concerns/InvalidatesResolverCache.php index 7dff35ad..3b864789 100644 --- a/src/Database/Concerns/InvalidatesResolverCache.php +++ b/src/Database/Concerns/InvalidatesResolverCache.php @@ -16,7 +16,7 @@ trait InvalidatesResolverCache Resolvers\RequestDataTenantResolver::class, ]; - public static function bootInvalidatesResolverCache() + public static function bootInvalidatesResolverCache(): void { static::saved(function (Tenant $tenant) { foreach (static::$resolvers as $resolver) { diff --git a/src/Database/Concerns/InvalidatesTenantsResolverCache.php b/src/Database/Concerns/InvalidatesTenantsResolverCache.php index 555aceeb..8d7c2845 100644 --- a/src/Database/Concerns/InvalidatesTenantsResolverCache.php +++ b/src/Database/Concerns/InvalidatesTenantsResolverCache.php @@ -19,7 +19,7 @@ trait InvalidatesTenantsResolverCache Resolvers\RequestDataTenantResolver::class, ]; - public static function bootInvalidatesTenantsResolverCache() + public static function bootInvalidatesTenantsResolverCache(): void { static::saved(function (Model $model) { foreach (static::$resolvers as $resolver) { diff --git a/src/Database/Concerns/ResourceSyncing.php b/src/Database/Concerns/ResourceSyncing.php index f1026f7a..df5b0766 100644 --- a/src/Database/Concerns/ResourceSyncing.php +++ b/src/Database/Concerns/ResourceSyncing.php @@ -10,7 +10,7 @@ use Stancl\Tenancy\Events\SyncedResourceSaved; trait ResourceSyncing { - public static function bootResourceSyncing() + public static function bootResourceSyncing(): void { static::saved(function (Syncable $model) { /** @var ResourceSyncing $model */ @@ -27,7 +27,7 @@ trait ResourceSyncing }); } - public function triggerSyncEvent() + public function triggerSyncEvent(): void { /** @var Syncable $this */ event(new SyncedResourceSaved($this, tenant())); diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index 00afe938..a92ccb7b 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -34,7 +34,7 @@ class DatabaseManager /** * Connect to a tenant's database. */ - public function connectToTenant(TenantWithDatabase $tenant) + public function connectToTenant(TenantWithDatabase $tenant): void { $this->purgeTenantConnection(); $this->createTenantConnection($tenant); @@ -44,7 +44,7 @@ class DatabaseManager /** * Reconnect to the default non-tenant connection. */ - public function reconnectToCentral() + public function reconnectToCentral(): void { $this->purgeTenantConnection(); $this->setDefaultConnection($this->config->get('tenancy.database.central_connection')); @@ -53,7 +53,7 @@ class DatabaseManager /** * Change the default database connection config. */ - public function setDefaultConnection(string $connection) + public function setDefaultConnection(string $connection): void { $this->config['database.default'] = $connection; $this->database->setDefaultConnection($connection); @@ -62,7 +62,7 @@ class DatabaseManager /** * Create the tenant database connection. */ - public function createTenantConnection(TenantWithDatabase $tenant) + public function createTenantConnection(TenantWithDatabase $tenant): void { $this->config['database.connections.tenant'] = $tenant->database()->connection(); } @@ -70,7 +70,7 @@ class DatabaseManager /** * Purge the tenant database connection. */ - public function purgeTenantConnection() + public function purgeTenantConnection(): void { if (array_key_exists('tenant', $this->database->getConnections())) { $this->database->purge('tenant'); diff --git a/src/Database/Exceptions/NoConnectionSetException.php b/src/Database/Exceptions/NoConnectionSetException.php index 1db73d04..75c5c77d 100644 --- a/src/Database/Exceptions/NoConnectionSetException.php +++ b/src/Database/Exceptions/NoConnectionSetException.php @@ -8,7 +8,7 @@ use Exception; class NoConnectionSetException extends Exception { - public function __construct($manager) + public function __construct(string $manager) { parent::__construct("No connection was set on this $manager instance."); } diff --git a/src/Database/Models/Domain.php b/src/Database/Models/Domain.php index 261918c4..16695711 100644 --- a/src/Database/Models/Domain.php +++ b/src/Database/Models/Domain.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Stancl\Tenancy\Contracts; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Database\Concerns; @@ -25,7 +26,7 @@ class Domain extends Model implements Contracts\Domain protected $guarded = []; - public function tenant() + public function tenant(): BelongsTo { return $this->belongsTo(config('tenancy.tenant_model')); } diff --git a/src/Database/Models/ImpersonationToken.php b/src/Database/Models/ImpersonationToken.php index d73b436b..8161aca7 100644 --- a/src/Database/Models/ImpersonationToken.php +++ b/src/Database/Models/ImpersonationToken.php @@ -10,12 +10,12 @@ use Illuminate\Support\Str; use Stancl\Tenancy\Database\Concerns\CentralConnection; /** - * @param string $token - * @param string $tenant_id - * @param string $user_id - * @param string $auth_guard - * @param string $redirect_url - * @param Carbon $created_at + * @property string $token + * @property string $tenant_id + * @property string $user_id + * @property string $auth_guard + * @property string $redirect_url + * @property Carbon $created_at */ class ImpersonationToken extends Model { @@ -35,7 +35,7 @@ class ImpersonationToken extends Model 'created_at', ]; - public static function booted() + public static function booted(): void { static::creating(function ($model) { $model->created_at = $model->created_at ?? $model->freshTimestamp(); diff --git a/src/Database/Models/TenantPivot.php b/src/Database/Models/TenantPivot.php index 5c0d6a37..2c7583c1 100644 --- a/src/Database/Models/TenantPivot.php +++ b/src/Database/Models/TenantPivot.php @@ -9,7 +9,7 @@ use Stancl\Tenancy\Contracts\Syncable; class TenantPivot extends Pivot { - public static function booted() + public static function booted(): void { static::saved(function (self $pivot) { $parent = $pivot->pivotParent; diff --git a/src/Events/SyncedResourceSaved.php b/src/Events/SyncedResourceSaved.php index 23e56e21..72d34d16 100644 --- a/src/Events/SyncedResourceSaved.php +++ b/src/Events/SyncedResourceSaved.php @@ -10,13 +10,12 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; class SyncedResourceSaved { - /** @var Syncable|Model */ - public $model; + public Syncable&Model $model; - /** @var TenantWithDatabase|Model|null */ - public $tenant; + /** @var (TenantWithDatabase&Model)|null */ + public TenantWithDatabase|null $tenant; - public function __construct(Syncable $model, ?TenantWithDatabase $tenant) + public function __construct(Syncable $model, TenantWithDatabase|null $tenant) { $this->model = $model; $this->tenant = $tenant; diff --git a/src/Features/TenantConfig.php b/src/Features/TenantConfig.php index 95691f0d..50756b2c 100644 --- a/src/Features/TenantConfig.php +++ b/src/Features/TenantConfig.php @@ -19,8 +19,7 @@ class TenantConfig implements Feature /** @var Repository */ protected $config; - /** @var array */ - public $originalConfig = []; + public array $originalConfig = []; public static $storageToConfigMap = [ // 'paypal_api_key' => 'services.paypal.api_key', @@ -51,14 +50,14 @@ class TenantConfig implements Feature if (! is_null($override)) { if (is_array($configKey)) { foreach ($configKey as $key) { - $this->originalConfig[$key] = $this->originalConfig[$key] ?? $this->config[$key]; + $this->originalConfig[$key] = $this->originalConfig[$key] ?? $this->config->get($key); - $this->config[$key] = $override; + $this->config->set($key, $override); } } else { - $this->originalConfig[$configKey] = $this->originalConfig[$configKey] ?? $this->config[$configKey]; + $this->originalConfig[$configKey] = $this->originalConfig[$configKey] ?? $this->config->get($configKey); - $this->config[$configKey] = $override; + $this->config->set($configKey, $override); } } } @@ -67,7 +66,7 @@ class TenantConfig implements Feature public function unsetTenantConfig(): void { foreach ($this->originalConfig as $key => $value) { - $this->config[$key] = $value; + $this->config->set($key, $value); } } } diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 021b44fc..41bf774b 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -31,6 +31,7 @@ class UserImpersonation implements Feature /** Impersonate a user and get an HTTP redirect response. */ public static function makeResponse(string|ImpersonationToken $token, int $ttl = null): RedirectResponse { + /** @var ImpersonationToken $token */ $token = $token instanceof ImpersonationToken ? $token : ImpersonationToken::findOrFail($token); $ttl ??= static::$ttl; diff --git a/src/Jobs/CreateDatabase.php b/src/Jobs/CreateDatabase.php index b4c0d400..cb807c95 100644 --- a/src/Jobs/CreateDatabase.php +++ b/src/Jobs/CreateDatabase.php @@ -19,13 +19,9 @@ class CreateDatabase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var TenantWithDatabase|Model */ - protected $tenant; - - public function __construct(TenantWithDatabase $tenant) - { - $this->tenant = $tenant; - } + public function __construct( + protected TenantWithDatabase&Model $tenant, + ) {} public function handle(DatabaseManager $databaseManager) { diff --git a/src/Jobs/DeleteDatabase.php b/src/Jobs/DeleteDatabase.php index 87f78d32..b59a1c05 100644 --- a/src/Jobs/DeleteDatabase.php +++ b/src/Jobs/DeleteDatabase.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; @@ -17,15 +18,11 @@ class DeleteDatabase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var TenantWithDatabase */ - protected $tenant; + public function __construct( + protected TenantWithDatabase&Model $tenant, + ) {} - public function __construct(TenantWithDatabase $tenant) - { - $this->tenant = $tenant; - } - - public function handle() + public function handle(): void { event(new DeletingDatabase($this->tenant)); diff --git a/src/Jobs/DeleteDomains.php b/src/Jobs/DeleteDomains.php index c720d4f7..f8d7f764 100644 --- a/src/Jobs/DeleteDomains.php +++ b/src/Jobs/DeleteDomains.php @@ -5,24 +5,26 @@ declare(strict_types=1); namespace Stancl\Tenancy\Jobs; use Illuminate\Bus\Queueable; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; +use Stancl\Tenancy\Database\Concerns\HasDomains; class DeleteDomains { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var TenantWithDatabase */ - protected $tenant; + /** @var TenantWithDatabase&Model&HasDomains */ // todo unresolvable type for phpstan + protected TenantWithDatabase&Model $tenant; - public function __construct(TenantWithDatabase $tenant) + public function __construct(TenantWithDatabase&Model $tenant) { $this->tenant = $tenant; } - public function handle() + public function handle(): void { $this->tenant->domains->each->delete(); } diff --git a/src/Jobs/MigrateDatabase.php b/src/Jobs/MigrateDatabase.php index d34b1ee4..424dacc9 100644 --- a/src/Jobs/MigrateDatabase.php +++ b/src/Jobs/MigrateDatabase.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; @@ -16,20 +17,11 @@ class MigrateDatabase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var TenantWithDatabase */ - protected $tenant; + public function __construct( + protected TenantWithDatabase&Model $tenant, + ) {} - public function __construct(TenantWithDatabase $tenant) - { - $this->tenant = $tenant; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle() + public function handle(): void { Artisan::call('tenants:migrate', [ '--tenants' => [$this->tenant->getTenantKey()], diff --git a/src/Jobs/SeedDatabase.php b/src/Jobs/SeedDatabase.php index 4400c21a..9958695e 100644 --- a/src/Jobs/SeedDatabase.php +++ b/src/Jobs/SeedDatabase.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; @@ -16,20 +17,11 @@ class SeedDatabase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** @var TenantWithDatabase */ - protected $tenant; + public function __construct( + protected TenantWithDatabase&Model $tenant, + ) {} - public function __construct(TenantWithDatabase $tenant) - { - $this->tenant = $tenant; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle() + public function handle(): void { Artisan::call('tenants:seed', [ '--tenants' => [$this->tenant->getTenantKey()], diff --git a/src/Listeners/BootstrapTenancy.php b/src/Listeners/BootstrapTenancy.php index d03da698..205efc5f 100644 --- a/src/Listeners/BootstrapTenancy.php +++ b/src/Listeners/BootstrapTenancy.php @@ -10,7 +10,7 @@ use Stancl\Tenancy\Events\TenancyInitialized; class BootstrapTenancy { - public function handle(TenancyInitialized $event) + public function handle(TenancyInitialized $event): void { event(new BootstrappingTenancy($event->tenancy)); diff --git a/src/Listeners/CreateTenantConnection.php b/src/Listeners/CreateTenantConnection.php index 987a8804..01351c08 100644 --- a/src/Listeners/CreateTenantConnection.php +++ b/src/Listeners/CreateTenantConnection.php @@ -17,7 +17,7 @@ class CreateTenantConnection $this->database = $database; } - public function handle(TenantEvent $event) + public function handle(TenantEvent $event): void { $this->database->createTenantConnection($event->tenant); } diff --git a/src/Listeners/QueueableListener.php b/src/Listeners/QueueableListener.php index b3afba0b..e10c1e7a 100644 --- a/src/Listeners/QueueableListener.php +++ b/src/Listeners/QueueableListener.php @@ -11,9 +11,9 @@ use Illuminate\Contracts\Queue\ShouldQueue; */ abstract class QueueableListener implements ShouldQueue { - public static $shouldQueue = false; + public static bool $shouldQueue = false; - public function shouldQueue($event) + public function shouldQueue($event): bool { if (static::$shouldQueue) { return true; diff --git a/src/Listeners/RevertToCentralContext.php b/src/Listeners/RevertToCentralContext.php index 788e3876..ac746ed4 100644 --- a/src/Listeners/RevertToCentralContext.php +++ b/src/Listeners/RevertToCentralContext.php @@ -10,7 +10,7 @@ use Stancl\Tenancy\Events\TenancyEnded; class RevertToCentralContext { - public function handle(TenancyEnded $event) + public function handle(TenancyEnded $event): void { event(new RevertingToCentralContext($event->tenancy)); diff --git a/src/Listeners/UpdateSyncedResource.php b/src/Listeners/UpdateSyncedResource.php index 9be290f0..45f73516 100644 --- a/src/Listeners/UpdateSyncedResource.php +++ b/src/Listeners/UpdateSyncedResource.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Listeners; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; use Stancl\Tenancy\Contracts\SyncMaster; @@ -13,9 +14,9 @@ use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException; class UpdateSyncedResource extends QueueableListener { - public static $shouldQueue = false; + public static bool $shouldQueue = false; - public function handle(SyncedResourceSaved $event) + public function handle(SyncedResourceSaved $event): void { $syncedAttributes = $event->model->only($event->model->getSyncedAttributeNames()); @@ -29,7 +30,7 @@ class UpdateSyncedResource extends QueueableListener $this->updateResourceInTenantDatabases($tenants, $event, $syncedAttributes); } - protected function getTenantsForCentralModel($centralModel) + protected function getTenantsForCentralModel($centralModel): EloquentCollection { if (! $centralModel instanceof SyncMaster) { // If we're trying to use a tenant User model instead of the central User model, for example. @@ -45,7 +46,7 @@ class UpdateSyncedResource extends QueueableListener return $centralModel->tenants; } - protected function updateResourceInCentralDatabaseAndGetTenants($event, $syncedAttributes) + protected function updateResourceInCentralDatabaseAndGetTenants($event, $syncedAttributes): EloquentCollection { /** @var Model|SyncMaster $centralModel */ $centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey()) @@ -85,7 +86,7 @@ class UpdateSyncedResource extends QueueableListener }); } - protected function updateResourceInTenantDatabases($tenants, $event, $syncedAttributes) + protected function updateResourceInTenantDatabases($tenants, $event, $syncedAttributes): void { tenancy()->runForMultiple($tenants, function ($tenant) use ($event, $syncedAttributes) { // Forget instance state and find the model, diff --git a/src/Resolvers/Contracts/CachedTenantResolver.php b/src/Resolvers/Contracts/CachedTenantResolver.php index a3e3daeb..f93d7bb5 100644 --- a/src/Resolvers/Contracts/CachedTenantResolver.php +++ b/src/Resolvers/Contracts/CachedTenantResolver.php @@ -25,7 +25,7 @@ abstract class CachedTenantResolver implements TenantResolver $this->cache = $cache->store(static::$cacheStore); } - public function resolve(...$args): Tenant + public function resolve(mixed ...$args): Tenant { if (! static::$shouldCache) { return $this->resolveWithoutCache(...$args); @@ -58,12 +58,12 @@ abstract class CachedTenantResolver implements TenantResolver } } - public function getCacheKey(...$args): string + public function getCacheKey(mixed ...$args): string { return '_tenancy_resolver:' . static::class . ':' . json_encode($args); } - abstract public function resolveWithoutCache(...$args): Tenant; + abstract public function resolveWithoutCache(mixed ...$args): Tenant; public function resolved(Tenant $tenant, ...$args): void { diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index eb25ad26..926c02c0 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -20,7 +20,7 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver public static string|null $cacheStore = null; // default - public function resolveWithoutCache(...$args): Tenant + public function resolveWithoutCache(mixed ...$args): Tenant { $domain = $args[0]; diff --git a/src/Resolvers/PathTenantResolver.php b/src/Resolvers/PathTenantResolver.php index 36a05a83..2ac2a59f 100644 --- a/src/Resolvers/PathTenantResolver.php +++ b/src/Resolvers/PathTenantResolver.php @@ -18,7 +18,7 @@ class PathTenantResolver extends Contracts\CachedTenantResolver public static string|null $cacheStore = null; // default - public function resolveWithoutCache(...$args): Tenant + public function resolveWithoutCache(mixed ...$args): Tenant { /** @var Route $route */ $route = $args[0]; diff --git a/src/Resolvers/RequestDataTenantResolver.php b/src/Resolvers/RequestDataTenantResolver.php index 65d4ce38..5ed65495 100644 --- a/src/Resolvers/RequestDataTenantResolver.php +++ b/src/Resolvers/RequestDataTenantResolver.php @@ -15,7 +15,7 @@ class RequestDataTenantResolver extends Contracts\CachedTenantResolver public static string|null $cacheStore = null; // default - public function resolveWithoutCache(...$args): Tenant + public function resolveWithoutCache(mixed ...$args): Tenant { $payload = $args[0]; diff --git a/src/Tenancy.php b/src/Tenancy.php index d08e34ce..012881ae 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -28,7 +28,7 @@ class Tenancy public ?Closure $getBootstrappersUsing = null; /** Is tenancy fully initialized? */ - public $initialized = false; // todo document the difference between $tenant being set and $initialized being true (e.g. end of initialize() method) + public bool $initialized = false; // todo document the difference between $tenant being set and $initialized being true (e.g. end of initialize() method) /** Initialize tenancy for the passed tenant. */ public function initialize(Tenant|int|string $tenant): void @@ -42,6 +42,7 @@ class Tenancy } } + // todo0 for phpstan this should be $this->tenant?, but first I want to clean up the $initialized logic and explore removing the property if ($this->initialized && $this->tenant->getTenantKey() === $tenant->getTenantKey()) { return; } diff --git a/src/helpers.php b/src/helpers.php index 9c3b75cd..23b5a627 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -15,15 +15,14 @@ if (! function_exists('tenancy')) { if (! function_exists('tenant')) { /** - * Get a key from the current tenant's storage. + * Get the current tenant or a key from the current tenant's properties. * - * @param string|null $key * @return Tenant|null|mixed */ - function tenant($key = null) + function tenant(string $key = null): mixed { if (! app()->bound(Tenant::class)) { - return; + return null; } if (is_null($key)) { @@ -35,15 +34,15 @@ if (! function_exists('tenant')) { } if (! function_exists('tenant_asset')) { - /** @return string */ - function tenant_asset($asset) + // todo docblock + function tenant_asset(string|null $asset): string { return route('stancl.tenancy.asset', ['path' => $asset]); } } if (! function_exists('global_asset')) { - function global_asset($asset) + function global_asset(string $asset) // todo types, also inside the globalUrl implementation { return app('globalUrl')->asset($asset); } @@ -57,9 +56,9 @@ if (! function_exists('global_cache')) { } if (! function_exists('tenant_route')) { - function tenant_route(string $domain, $route, $parameters = [], $absolute = true) + function tenant_route(string $domain, string $route, array $parameters = [], bool $absolute = true): string { - // replace first occurance of hostname fragment with $domain + // replace the first occurrence of the hostname fragment with $domain $url = route($route, $parameters, $absolute); $hostname = parse_url($url, PHP_URL_HOST); $position = strpos($url, $hostname); diff --git a/tests/EventListenerTest.php b/tests/EventListenerTest.php index 85203f07..454c86ed 100644 --- a/tests/EventListenerTest.php +++ b/tests/EventListenerTest.php @@ -181,7 +181,7 @@ test('database is not migrated if creation is disabled', function () { class FooListener extends QueueableListener { - public static $shouldQueue = false; + public static bool $shouldQueue = false; public function handle() { diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index fad84aef..214a9f47 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -575,7 +575,7 @@ class CentralUser extends Model implements SyncMaster return ResourceUser::class; } - public function getGlobalIdentifierKey() + public function getGlobalIdentifierKey(): string|int { return $this->getAttribute($this->getGlobalIdentifierKeyName()); } @@ -610,7 +610,7 @@ class ResourceUser extends Model implements Syncable public $timestamps = false; - public function getGlobalIdentifierKey() + public function getGlobalIdentifierKey(): string|int { return $this->getAttribute($this->getGlobalIdentifierKeyName()); } From 3605252d8531634407b28d32773179b6ce9eb6a5 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Tue, 30 Aug 2022 03:46:17 +0000 Subject: [PATCH 23/33] Fix code style (php-cs-fixer) --- src/Jobs/CreateDatabase.php | 3 ++- src/Jobs/DeleteDatabase.php | 3 ++- src/Jobs/DeleteDomains.php | 2 +- src/Jobs/MigrateDatabase.php | 3 ++- src/Jobs/SeedDatabase.php | 3 ++- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Jobs/CreateDatabase.php b/src/Jobs/CreateDatabase.php index cb807c95..f143f399 100644 --- a/src/Jobs/CreateDatabase.php +++ b/src/Jobs/CreateDatabase.php @@ -21,7 +21,8 @@ class CreateDatabase implements ShouldQueue public function __construct( protected TenantWithDatabase&Model $tenant, - ) {} + ) { + } public function handle(DatabaseManager $databaseManager) { diff --git a/src/Jobs/DeleteDatabase.php b/src/Jobs/DeleteDatabase.php index b59a1c05..71358f74 100644 --- a/src/Jobs/DeleteDatabase.php +++ b/src/Jobs/DeleteDatabase.php @@ -20,7 +20,8 @@ class DeleteDatabase implements ShouldQueue public function __construct( protected TenantWithDatabase&Model $tenant, - ) {} + ) { + } public function handle(): void { diff --git a/src/Jobs/DeleteDomains.php b/src/Jobs/DeleteDomains.php index f8d7f764..8d89ce9e 100644 --- a/src/Jobs/DeleteDomains.php +++ b/src/Jobs/DeleteDomains.php @@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; +use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; class DeleteDomains { diff --git a/src/Jobs/MigrateDatabase.php b/src/Jobs/MigrateDatabase.php index 424dacc9..4624f212 100644 --- a/src/Jobs/MigrateDatabase.php +++ b/src/Jobs/MigrateDatabase.php @@ -19,7 +19,8 @@ class MigrateDatabase implements ShouldQueue public function __construct( protected TenantWithDatabase&Model $tenant, - ) {} + ) { + } public function handle(): void { diff --git a/src/Jobs/SeedDatabase.php b/src/Jobs/SeedDatabase.php index 9958695e..e1bae0c7 100644 --- a/src/Jobs/SeedDatabase.php +++ b/src/Jobs/SeedDatabase.php @@ -19,7 +19,8 @@ class SeedDatabase implements ShouldQueue public function __construct( protected TenantWithDatabase&Model $tenant, - ) {} + ) { + } public function handle(): void { From 3542b3f028fa8f233cdfd3c0b141a2e38cee087d Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Tue, 30 Aug 2022 18:21:19 +0500 Subject: [PATCH 24/33] update spatie/ignition to support L9 (#930) --- composer.json | 2 +- src/Contracts/TenantCouldNotBeIdentifiedException.php | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index ff5befd9..cc213add 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "php": "^8.1", "ext-json": "*", "illuminate/support": "^9.0", - "facade/ignition-contracts": "^1.0", + "spatie/ignition": "^1.4", "ramsey/uuid": "^4.0", "stancl/jobpipeline": "^1.0", "stancl/virtualcolumn": "^1.0" diff --git a/src/Contracts/TenantCouldNotBeIdentifiedException.php b/src/Contracts/TenantCouldNotBeIdentifiedException.php index 0066291f..011d974b 100644 --- a/src/Contracts/TenantCouldNotBeIdentifiedException.php +++ b/src/Contracts/TenantCouldNotBeIdentifiedException.php @@ -5,9 +5,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Contracts; use Exception; -use Facade\IgnitionContracts\BaseSolution; -use Facade\IgnitionContracts\ProvidesSolution; -use Facade\IgnitionContracts\Solution; +use Spatie\Ignition\Contracts\BaseSolution; +use Spatie\Ignition\Contracts\ProvidesSolution; abstract class TenantCouldNotBeIdentifiedException extends Exception implements ProvidesSolution { @@ -42,7 +41,7 @@ abstract class TenantCouldNotBeIdentifiedException extends Exception implements } /** Get the Ignition description. */ - public function getSolution(): Solution + public function getSolution(): BaseSolution { return BaseSolution::create($this->solutionTitle) ->setSolutionDescription($this->solutionDescription) From 2b02198dda8ea365456f0f71bc5ea837a051547f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 30 Aug 2022 16:23:50 +0200 Subject: [PATCH 25/33] Move to archtechx/tenancy:latest --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9da7221..db2b0ffc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: jobs: tests: runs-on: ubuntu-latest - container: abrardev/tenancy:latest + container: archtechx/tenancy:latest strategy: matrix: From 62d19c5f5d5f46b28ec95c7bc0c4e97393c58244 Mon Sep 17 00:00:00 2001 From: emargareten <46111162+emargareten@users.noreply.github.com> Date: Tue, 30 Aug 2022 17:24:50 +0300 Subject: [PATCH 26/33] Reverse bootstrappers when reverting to central (#932) Some of my bootstrappers are depending on previous bootstrappers but when reverting it needs to run in reverse order. Submitting to v4 since this might be a breaking change (currently I am overriding this file). --- src/Listeners/RevertToCentralContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Listeners/RevertToCentralContext.php b/src/Listeners/RevertToCentralContext.php index ac746ed4..0a680532 100644 --- a/src/Listeners/RevertToCentralContext.php +++ b/src/Listeners/RevertToCentralContext.php @@ -14,7 +14,7 @@ class RevertToCentralContext { event(new RevertingToCentralContext($event->tenancy)); - foreach ($event->tenancy->getBootstrappers() as $bootstrapper) { + foreach (array_reverse($event->tenancy->getBootstrappers()) as $bootstrapper) { $bootstrapper->revert(); } From f941df3a82e6856a1dc299ff89e063c2baaed4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 1 Sep 2022 19:06:54 +0200 Subject: [PATCH 27/33] minor improvements for phpstan --- src/Features/CrossDomainRedirect.php | 11 ++++++++--- src/Tenancy.php | 2 +- src/helpers.php | 12 ++++++++---- tests/TestCase.php | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Features/CrossDomainRedirect.php b/src/Features/CrossDomainRedirect.php index 0b6d7682..4efc767b 100644 --- a/src/Features/CrossDomainRedirect.php +++ b/src/Features/CrossDomainRedirect.php @@ -15,11 +15,16 @@ class CrossDomainRedirect implements Feature RedirectResponse::macro('domain', function (string $domain) { /** @var RedirectResponse $this */ - // Replace first occurrence of the hostname fragment with $domain $url = $this->getTargetUrl(); + + /** + * The original hostname in the redirect response. + * + * @var string $hostname + */ $hostname = parse_url($url, PHP_URL_HOST); - $position = strpos($url, $hostname); - $this->setTargetUrl(substr_replace($url, $domain, $position, strlen($hostname))); + + $this->setTargetUrl((string) str($url)->replace($hostname, $domain)); return $this; }); diff --git a/src/Tenancy.php b/src/Tenancy.php index 012881ae..0a8d4542 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -146,7 +146,7 @@ class Tenancy $tenants = is_string($tenants) ? [$tenants] : $tenants; // Use all tenants if $tenants is falsey - $tenants = $tenants ?: $this->model()->cursor(); + $tenants = $tenants ?: $this->model()->cursor(); // todo0 phpstan thinks this isn't needed, but tests fail without it $originalTenant = $this->tenant; diff --git a/src/helpers.php b/src/helpers.php index 23b5a627..ac805aa5 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -58,11 +58,15 @@ if (! function_exists('global_cache')) { if (! function_exists('tenant_route')) { function tenant_route(string $domain, string $route, array $parameters = [], bool $absolute = true): string { - // replace the first occurrence of the hostname fragment with $domain $url = route($route, $parameters, $absolute); - $hostname = parse_url($url, PHP_URL_HOST); - $position = strpos($url, $hostname); - return substr_replace($url, $domain, $position, strlen($hostname)); + /** + * The original hostname in the generated route. + * + * @var string $hostname + */ + $hostname = parse_url($url, PHP_URL_HOST); + + return (string) str($url)->replace($hostname, $domain); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 1c6c6d8a..67029422 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -96,7 +96,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '--realpath' => true, '--force' => true, ], - 'tenancy.bootstrappers.redis' => \Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, + 'tenancy.bootstrappers.redis' => \Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // todo0 change this to []? two tests in TenantDatabaseManagerTest are failing with that 'queue.connections.central' => [ 'driver' => 'sync', 'central' => true, From 020039bf89f6836c3c948ecc913ac9808cfee0f6 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Thu, 1 Sep 2022 17:07:17 +0000 Subject: [PATCH 28/33] Fix code style (php-cs-fixer) --- src/Features/CrossDomainRedirect.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Features/CrossDomainRedirect.php b/src/Features/CrossDomainRedirect.php index 4efc767b..a48be6ea 100644 --- a/src/Features/CrossDomainRedirect.php +++ b/src/Features/CrossDomainRedirect.php @@ -14,7 +14,6 @@ class CrossDomainRedirect implements Feature { RedirectResponse::macro('domain', function (string $domain) { /** @var RedirectResponse $this */ - $url = $this->getTargetUrl(); /** From f83504ac6f61e260d5382a5feaac0fa98ef98664 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Fri, 2 Sep 2022 17:24:37 +0200 Subject: [PATCH 29/33] [4.x] Add ScoutTenancyBootstrapper (#936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add ScoutTenancyBootstrapper * Fix code style (php-cs-fixer) * extract getTenantPrefix method * Fix code style (php-cs-fixer) Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- .../Integrations/ScoutTenancyBootstrapper.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php diff --git a/src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php b/src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php new file mode 100644 index 00000000..49869bb5 --- /dev/null +++ b/src/Bootstrappers/Integrations/ScoutTenancyBootstrapper.php @@ -0,0 +1,42 @@ +config = $config; + } + + public function bootstrap(Tenant $tenant) + { + if (! isset($this->originalScoutPrefix)) { + $this->originalScoutPrefix = $this->config->get('scout.prefix'); + } + + $this->config->set('scout.prefix', $this->getTenantPrefix($tenant)); + } + + public function revert() + { + $this->config->set('scout.prefix', $this->originalScoutPrefix); + } + + protected function getTenantPrefix(Tenant $tenant): string + { + return (string) $tenant->getTenantKey(); + } +} From 3bf2c39e1a38672a2a9345ab46a9076609deb901 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Fri, 2 Sep 2022 17:46:27 +0200 Subject: [PATCH 30/33] [4.x] Make impersonation tokens require stateful guards (#935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Throw an exception on attempt to create impersonation token with a non-stateful guard * Test that impersonation tokens can only be created with a stateful guard * Fix code style (php-cs-fixer) * Escape backslashes in the exception's message Co-authored-by: Samuel Štancl * Make the exception only about requiring a stateful guard Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- src/Database/Models/ImpersonationToken.php | 11 +++- .../StatefulGuardRequiredException.php | 15 +++++ tests/TenantUserImpersonationTest.php | 62 ++++++++++++++++--- 3 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/Exceptions/StatefulGuardRequiredException.php diff --git a/src/Database/Models/ImpersonationToken.php b/src/Database/Models/ImpersonationToken.php index 8161aca7..05d17ad4 100644 --- a/src/Database/Models/ImpersonationToken.php +++ b/src/Database/Models/ImpersonationToken.php @@ -5,9 +5,12 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Models; use Carbon\Carbon; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; use Stancl\Tenancy\Database\Concerns\CentralConnection; +use Stancl\Tenancy\Exceptions\StatefulGuardRequiredException; /** * @property string $token @@ -38,9 +41,15 @@ class ImpersonationToken extends Model public static function booted(): void { static::creating(function ($model) { + $authGuard = $model->auth_guard ?? config('auth.defaults.guard'); + + if (! Auth::guard($authGuard) instanceof StatefulGuard) { + throw new StatefulGuardRequiredException($authGuard); + } + $model->created_at = $model->created_at ?? $model->freshTimestamp(); $model->token = $model->token ?? Str::random(128); - $model->auth_guard = $model->auth_guard ?? config('auth.defaults.guard'); + $model->auth_guard = $authGuard; }); } } diff --git a/src/Exceptions/StatefulGuardRequiredException.php b/src/Exceptions/StatefulGuardRequiredException.php new file mode 100644 index 00000000..fe8bef6e --- /dev/null +++ b/src/Exceptions/StatefulGuardRequiredException.php @@ -0,0 +1,15 @@ +artisan('migrate', [ @@ -223,6 +225,46 @@ test('impersonation works with multiple models and guards', function () { }); }); +test('impersonation tokens can be created only with stateful guards', function () { + config([ + 'auth.guards' => [ + 'nonstateful' => [ + 'driver' => 'nonstateful', + 'provider' => 'provider', + ], + 'stateful' => [ + 'driver' => 'session', + 'provider' => 'provider', + ], + ], + 'auth.providers.provider' => [ + 'driver' => 'eloquent', + 'model' => ImpersonationUser::class, + ], + ]); + + $tenant = Tenant::create(); + migrateTenants(); + + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + Auth::extend('nonstateful', fn($app, $name, array $config) => new TokenGuard(Auth::createUserProvider($config['provider']), request())); + + expect(fn() => tenancy()->impersonate($tenant, $user->id, '/dashboard', 'nonstateful')) + ->toThrow(StatefulGuardRequiredException::class); + + Auth::extend('stateful', fn ($app, $name, array $config) => new SessionGuard($name, Auth::createUserProvider($config['provider']), session())); + + expect(tenancy()->impersonate($tenant, $user->id, '/dashboard', 'stateful')) + ->toBeInstanceOf(ImpersonationToken::class); +}); + function migrateTenants() { pest()->artisan('tenants:migrate')->assertExitCode(0); From 409190fae1584d17ee6ed86318c0055d9f239ce6 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Fri, 2 Sep 2022 21:46:13 +0500 Subject: [PATCH 31/33] [4.x] Improve `tenants:run` command to execute Input\Output commands (#923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * run command useable for questions asking commands * move console classes to Console directory * fix styling * Update src/Commands/Run.php Co-authored-by: Samuel Štancl * remove tenant migration line * assert command executed in tenant context * improve test * cleanup code * Update CommandsTest.php * remove irrelevant assertions Co-authored-by: Samuel Štancl --- src/Commands/Run.php | 27 +++++++++--- tests/CommandsTest.php | 24 ++++++++++ tests/Etc/{ => Console}/AddUserCommand.php | 3 +- tests/Etc/{ => Console}/ConsoleKernel.php | 3 +- tests/Etc/{ => Console}/ExampleCommand.php | 2 +- tests/Etc/Console/ExampleQuestionCommand.php | 46 ++++++++++++++++++++ tests/TestCase.php | 2 +- 7 files changed, 98 insertions(+), 9 deletions(-) rename tests/Etc/{ => Console}/AddUserCommand.php (91%) rename tests/Etc/{ => Console}/ConsoleKernel.php (72%) rename tests/Etc/{ => Console}/ExampleCommand.php (94%) create mode 100644 tests/Etc/Console/ExampleQuestionCommand.php diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 075f9116..a24fb9c7 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -5,7 +5,9 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Artisan; +use Illuminate\Contracts\Console\Kernel; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\ConsoleOutput; class Run extends Command { @@ -29,12 +31,27 @@ class Run extends Command */ public function handle() { - tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { + $argvInput = $this->ArgvInput(); + tenancy()->runForMultiple($this->option('tenants'), function ($tenant) use ($argvInput) { $this->line("Tenant: {$tenant->getTenantKey()}"); - Artisan::call($this->argument('commandname')); - $this->comment('Command output:'); - $this->info(Artisan::output()); + $this->getLaravel() + ->make(Kernel::class) + ->handle($argvInput, new ConsoleOutput); }); } + + /** + * Get command as ArgvInput instance. + */ + protected function ArgvInput(): ArgvInput + { + // Convert string command to array + $subCommand = explode(' ', $this->argument('commandname')); + + // Add "artisan" as first parameter because ArgvInput expects "artisan" as first parameter and later removes it + array_unshift($subCommand, 'artisan'); + + return new ArgvInput($subCommand); + } } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 7415b74f..19018c9a 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -16,6 +16,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\ExampleSeeder; use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Tests\Etc\User; beforeEach(function () { Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { @@ -179,6 +180,29 @@ test('run command with array of tenants works', function () { ->expectsOutput('Tenant: ' . $tenantId2); }); +test('run command works when sub command asks questions and accepts arguments', function () { + $tenant = Tenant::create(); + $id = $tenant->getTenantKey(); + + Artisan::call('tenants:migrate'); + + pest()->artisan("tenants:run --tenants=$id 'user:addwithname Abrar' ") + ->expectsQuestion('What is your email?', 'email@localhost') + ->expectsOutput("Tenant: $id") + ->expectsOutput("User created: Abrar(email@localhost)"); + + // Assert we are in central context + expect(tenancy()->initialized)->toBeFalse(); + + // Assert user was created in tenant context + tenancy()->initialize($tenant); + $user = User::first(); + + // Assert user is same as provided using the command + expect($user->name)->toBe('Abrar'); + expect($user->email)->toBe('email@localhost'); +}); + // todo@tests function runCommandWorks(): void { diff --git a/tests/Etc/AddUserCommand.php b/tests/Etc/Console/AddUserCommand.php similarity index 91% rename from tests/Etc/AddUserCommand.php rename to tests/Etc/Console/AddUserCommand.php index 46e1fcbb..f102bae6 100644 --- a/tests/Etc/AddUserCommand.php +++ b/tests/Etc/Console/AddUserCommand.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests\Etc; +namespace Stancl\Tenancy\Tests\Etc\Console; use Illuminate\Console\Command; use Illuminate\Support\Str; use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Concerns\TenantAwareCommand; +use Stancl\Tenancy\Tests\Etc\User; class AddUserCommand extends Command { diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/Console/ConsoleKernel.php similarity index 72% rename from tests/Etc/ConsoleKernel.php rename to tests/Etc/Console/ConsoleKernel.php index a548f113..c5e5ee85 100644 --- a/tests/Etc/ConsoleKernel.php +++ b/tests/Etc/Console/ConsoleKernel.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests\Etc; +namespace Stancl\Tenancy\Tests\Etc\Console; use Orchestra\Testbench\Foundation\Console\Kernel; @@ -10,6 +10,7 @@ class ConsoleKernel extends Kernel { protected $commands = [ ExampleCommand::class, + ExampleQuestionCommand::class, AddUserCommand::class, ]; } diff --git a/tests/Etc/ExampleCommand.php b/tests/Etc/Console/ExampleCommand.php similarity index 94% rename from tests/Etc/ExampleCommand.php rename to tests/Etc/Console/ExampleCommand.php index 49e7189b..72263b37 100644 --- a/tests/Etc/ExampleCommand.php +++ b/tests/Etc/Console/ExampleCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests\Etc; +namespace Stancl\Tenancy\Tests\Etc\Console; use Illuminate\Console\Command; diff --git a/tests/Etc/Console/ExampleQuestionCommand.php b/tests/Etc/Console/ExampleQuestionCommand.php new file mode 100644 index 00000000..9a967054 --- /dev/null +++ b/tests/Etc/Console/ExampleQuestionCommand.php @@ -0,0 +1,46 @@ +ask('What is your email?'); + + User::create([ + 'name' => $this->argument('name'), + 'email' => $email, + 'email_verified_at' => now(), + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'remember_token' => Str::random(10), + ]); + + $this->line("User created: ". $this->argument('name') . "($email)"); + + return 0; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 67029422..ed567497 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -142,7 +142,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase */ protected function resolveApplicationConsoleKernel($app) { - $app->singleton('Illuminate\Contracts\Console\Kernel', Etc\ConsoleKernel::class); + $app->singleton('Illuminate\Contracts\Console\Kernel', Etc\Console\ConsoleKernel::class); } public function randomString(int $length = 10) From f2c64088ed950f85787e7489b7d56cf40f02c96c Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Fri, 2 Sep 2022 22:04:00 +0500 Subject: [PATCH 32/33] [4.x] Set tenant as a default parameter for the URLs when using Path identification (#925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * set tenant as default url parameter * Update PathIdentificationTest.php * assertion * test rename * fix tests * fix string Co-authored-by: Samuel Štancl --- src/Middleware/InitializeTenancyByPath.php | 8 +++++++ tests/PathIdentificationTest.php | 26 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index e66400c5..ae15323c 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -7,6 +7,9 @@ namespace Stancl\Tenancy\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Routing\Route; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\URL; +use Stancl\Tenancy\Events\InitializingTenancy; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Tenancy; @@ -37,6 +40,11 @@ class InitializeTenancyByPath extends IdentificationMiddleware // We don't want to initialize tenancy if the tenant is // simply injected into some route controller action. if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) { + // Set tenant as a default parameter for the URLs in the current request + Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) { + URL::defaults([PathTenantResolver::$tenantParameterName => $event->tenancy->tenant->getTenantKey()]); + }); + return $this->initializeTenancy( $request, $next, diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index bda0cfcb..bfa8f8ad 100644 --- a/tests/PathIdentificationTest.php +++ b/tests/PathIdentificationTest.php @@ -18,7 +18,11 @@ beforeEach(function () { ], function () { Route::get('/foo/{a}/{b}', function ($a, $b) { return "$a + $b"; - }); + })->name('foo'); + + Route::get('/baz/{a}/{b}', function ($a, $b) { + return "$a - $b"; + })->name('baz'); }); }); @@ -123,3 +127,23 @@ test('tenant parameter name can be customized', function () { ->withoutExceptionHandling() ->get('/acme/foo/abc/xyz'); }); + +test('tenant parameter is set for all routes as the default parameter once the tenancy initialized', function () { + Tenant::create([ + 'id' => 'acme', + ]); + + expect(tenancy()->initialized)->toBeFalse(); + + // make a request that will initialize tenancy + pest()->get(route('foo', ['tenant' => 'acme', 'a' => 1, 'b' => 2])); + + expect(tenancy()->initialized)->toBeTrue(); + expect(tenant('id'))->toBe('acme'); + + // assert that the route WITHOUT the tenant parameter matches the route WITH the tenant parameter + expect(route('baz', ['a' => 1, 'b' => 2]))->toBe(route('baz', ['tenant' => 'acme', 'a' => 1, 'b' => 2])); + + expect(route('baz', ['a' => 1, 'b' => 2]))->toBe('http://localhost/acme/baz/1/2'); // assert the full route string + pest()->get(route('baz', ['a' => 1, 'b' => 2]))->assertOk(); // Assert route don't need tenant parameter +}); From abd17f83a18070d70e08567d10dcaa1983f6806e Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Thu, 8 Sep 2022 21:18:59 +0500 Subject: [PATCH 33/33] add mssql health checks (#939) --- docker-compose.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7b635637..116b48f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: condition: service_healthy redis: condition: service_healthy + # mssql: + # condition: service_healthy volumes: - .:/var/www/html:delegated environment: @@ -74,4 +76,8 @@ services: environment: - ACCEPT_EULA=Y - SA_PASSWORD=P@ssword # todo reuse values from env above - # todo missing health check + healthcheck: + test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ssword -Q "SELECT 1" -b -o /dev/null + interval: 10s + timeout: 10s + retries: 10