From fe0a322b87969f42031c411a48479046bd54fb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 25 Oct 2022 12:53:31 +0200 Subject: [PATCH 01/37] add public connection() method to the Tenant DB manager interface --- .../StatefulTenantDatabaseManager.php | 22 +++++++++++++++++++ .../Contracts/TenantDatabaseManager.php | 9 -------- src/Database/DatabaseConfig.php | 4 +++- .../TenantDatabaseManager.php | 6 ++--- tests/DatabasePreparationTest.php | 4 +--- tests/TenantDatabaseManagerTest.php | 6 ++++- 6 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 src/Database/Contracts/StatefulTenantDatabaseManager.php diff --git a/src/Database/Contracts/StatefulTenantDatabaseManager.php b/src/Database/Contracts/StatefulTenantDatabaseManager.php new file mode 100644 index 00000000..36a08db2 --- /dev/null +++ b/src/Database/Contracts/StatefulTenantDatabaseManager.php @@ -0,0 +1,22 @@ +setConnection($this->getTemplateConnectionName()); + if ($databaseManager instanceof Contracts\StatefulTenantDatabaseManager) { + $databaseManager->setConnection($this->getTemplateConnectionName()); + } return $databaseManager; } diff --git a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php index b7dd15fa..87916088 100644 --- a/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php +++ b/src/Database/TenantDatabaseManagers/TenantDatabaseManager.php @@ -6,15 +6,15 @@ namespace Stancl\Tenancy\Database\TenantDatabaseManagers; use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; -use Stancl\Tenancy\Database\Contracts\TenantDatabaseManager as Contract; +use Stancl\Tenancy\Database\Contracts\StatefulTenantDatabaseManager; use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; -abstract class TenantDatabaseManager implements Contract // todo better naming? +abstract class TenantDatabaseManager implements StatefulTenantDatabaseManager { /** The database connection to the server. */ protected string $connection; - protected function database(): Connection + public function database(): Connection { if (! isset($this->connection)) { throw new NoConnectionSetException(static::class); diff --git a/tests/DatabasePreparationTest.php b/tests/DatabasePreparationTest.php index e31fac9b..d5641af4 100644 --- a/tests/DatabasePreparationTest.php +++ b/tests/DatabasePreparationTest.php @@ -22,9 +22,7 @@ test('database can be created after tenant creation', function () { })->toListener()); $tenant = Tenant::create(); - - $manager = app(MySQLDatabaseManager::class); - $manager->setConnection('mysql'); + $manager = $tenant->database()->manager(); expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue(); }); diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index b16c06b6..33a3158f 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Database\Contracts\StatefulTenantDatabaseManager; use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; @@ -36,7 +37,10 @@ test('databases can be created and deleted', function ($driver, $databaseManager $name = 'db' . pest()->randomString(); $manager = app($databaseManager); - $manager->setConnection($driver); + + if ($manager instanceof StatefulTenantDatabaseManager) { + $manager->setConnection($driver); + } expect($manager->databaseExists($name))->toBeFalse(); From 8c346409483029368b9e711c8f0190b5bfdc98bb Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Tue, 25 Oct 2022 10:54:04 +0000 Subject: [PATCH 02/37] Fix code style (php-cs-fixer) --- src/Database/Contracts/StatefulTenantDatabaseManager.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Database/Contracts/StatefulTenantDatabaseManager.php b/src/Database/Contracts/StatefulTenantDatabaseManager.php index 36a08db2..1a2e928d 100644 --- a/src/Database/Contracts/StatefulTenantDatabaseManager.php +++ b/src/Database/Contracts/StatefulTenantDatabaseManager.php @@ -1,5 +1,7 @@ Date: Tue, 25 Oct 2022 18:03:04 +0200 Subject: [PATCH 03/37] [4.x] Make `tenants:migrate` default to configured schema path (#985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add --schema-path to migration parameters config * Set TenantDump's path to configured schema-path if the path doesn't get passed * Test schema dump file creation and usage * Fix code style (php-cs-fixer) * hardcode default instead of reading from a config key that doesn't have to exist Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- assets/config.php | 1 + src/Commands/TenantDump.php | 4 ++++ tests/CommandsTest.php | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/assets/config.php b/assets/config.php index eb68d9b0..0e035953 100644 --- a/assets/config.php +++ b/assets/config.php @@ -258,6 +258,7 @@ return [ 'migration_parameters' => [ '--force' => true, // This needs to be true to run migrations in production. '--path' => [database_path('migrations/tenant')], + '--schema-path' => database_path('schema/tenant-schema.dump'), '--realpath' => true, ], diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 6edae6b0..3f957bdd 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -22,6 +22,10 @@ class TenantDump extends DumpCommand public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int { + if (is_null($this->option('path'))) { + $this->input->setOption('path', database_path('schema/tenant-schema.dump')); + } + $tenant = $this->option('tenant') ?? tenant() ?? $this->ask('What tenant do you want to dump the schema for?') diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 793dca30..9a9f0bc5 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -120,6 +120,39 @@ test('dump command works', function () { expect('tests/Etc/tenant-schema-test.dump')->toBeFile(); }); +test('tenant dump file gets created as tenant-schema.dump in the database schema folder by default', function() { + config(['tenancy.migration_parameters.--schema-path' => $schemaPath = database_path('schema/tenant-schema.dump')]); + + $tenant = Tenant::create(); + Artisan::call('tenants:migrate'); + + tenancy()->initialize($tenant); + + Artisan::call('tenants:dump'); + + expect($schemaPath)->toBeFile(); + unlink($schemaPath); +}); + +test('migrate command uses the correct schema path by default', function () { + config(['tenancy.migration_parameters.--schema-path' => 'tests/Etc/tenant-schema.dump']); + $tenant = Tenant::create(); + + expect(Schema::hasTable('schema_users'))->toBeFalse(); + expect(Schema::hasTable('users'))->toBeFalse(); + + Artisan::call('tenants:migrate'); + + expect(Schema::hasTable('schema_users'))->toBeFalse(); + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant); + + // Check for both tables to see if missing migrations also get executed + expect(Schema::hasTable('schema_users'))->toBeTrue(); + expect(Schema::hasTable('users'))->toBeTrue(); +}); + test('rollback command works', function () { $tenant = Tenant::create(); Artisan::call('tenants:migrate'); From 648acc48c72ec8793f30379cacf514decb9d14f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 25 Oct 2022 18:04:13 +0200 Subject: [PATCH 04/37] remove HasDataColumn trait --- src/Database/Concerns/HasDataColumn.php | 15 --------------- src/Database/Models/Tenant.php | 14 +++++++++----- 2 files changed, 9 insertions(+), 20 deletions(-) delete mode 100644 src/Database/Concerns/HasDataColumn.php diff --git a/src/Database/Concerns/HasDataColumn.php b/src/Database/Concerns/HasDataColumn.php deleted file mode 100644 index cf67b832..00000000 --- a/src/Database/Concerns/HasDataColumn.php +++ /dev/null @@ -1,15 +0,0 @@ -getAttribute($this->getTenantKeyName()); } + /** Get the current tenant. */ public static function current(): static|null { return tenant(); } - /** @throws TenancyNotInitializedException */ + /** + * Get the current tenant or throw an exception if tenancy is not initialized. + * + * @throws TenancyNotInitializedException + */ public static function currentOrFail(): static { return static::current() ?? throw new TenancyNotInitializedException; From 15dc40839b6d8dcfb54c32a4f998784fb687e710 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 26 Oct 2022 12:08:14 +0200 Subject: [PATCH 05/37] Unlink tenant schema path before each test in CommandsTest (#986) * Add tenantSchemaPath method * Unlink tenant schema path before each test in CommandsTest * Remove the tenantSchemaPath helper --- tests/CommandsTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 9a9f0bc5..ea973070 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -26,6 +26,10 @@ use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; beforeEach(function () { + if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) { + unlink($schemaPath); + } + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { return $event->tenant; })->toListener()); @@ -131,7 +135,6 @@ test('tenant dump file gets created as tenant-schema.dump in the database schema Artisan::call('tenants:dump'); expect($schemaPath)->toBeFile(); - unlink($schemaPath); }); test('migrate command uses the correct schema path by default', function () { From ae8b01153ab6662b6a93ff05e813f5ea7d606931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 26 Oct 2022 12:20:21 +0200 Subject: [PATCH 06/37] phpstan pro --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b30ea94c..bbca1e14 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,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", + "phpstan": "vendor/bin/phpstan --pro", "cs": "php-cs-fixer fix --config=.php-cs-fixer.php", "test": "PHP_VERSION=8.1 ./test --no-coverage", "test-full": "PHP_VERSION=8.1 ./test" From bf504f4c795dfc5c59aee64f48d7395c0cf60acc Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Mon, 31 Oct 2022 16:13:54 +0500 Subject: [PATCH 07/37] [4.x] Use a dedicated DB connection for creating/deleting tenant databases (#946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create host connection for creating, deleting tenants * purge connection and add more tests * remove unused method * Improvements * test named * remove host connection name config key * Revert "remove host connection name config key" This reverts commit 42acb823e8f437bd0d6560b4cf567ef9769aa5b8. * Update DatabaseConfig.php * Update assets/config.php Co-authored-by: Samuel Štancl * Update DatabaseConfig.php * todo and comments * remove debug code * Update DatabaseConfig.php * strict assertions * Update TenantDatabaseManagerTest.php * Update src/Database/DatabaseConfig.php Co-authored-by: Samuel Štancl * purge connection improvements * Update DatabaseConfig.php * Update DatabaseConfig.php * Update DatabaseConfig.php * improve comments * remove "ensuring connection exists" check * remove test because it's duplicate * removing test because other two tests are using the same logic, so this test kinda already covered * Update TenantDatabaseManagerTest.php * Update DatabaseConfig.php * Revert "Update TenantDatabaseManagerTest.php" This reverts commit b8e0a1c982a4cf95bbc3bd646fa571eee510ba7b. * add default * Update src/Database/DatabaseConfig.php Co-authored-by: Samuel Štancl * update comment * remove unness mysql config and add a comment * tenancy_db_connection tenant config test * Update TenantDatabaseManagerTest.php * update test name and improve assertions * typo * change inline variable name * Update TenantDatabaseManagerTest.php * Update TenantDatabaseManagerTest.php * add DB::purge() calls * add new assertions [ci skip] * Fix code style (php-cs-fixer) * replace hostManager with manager * fix test * method rename Co-authored-by: Samuel Štancl Co-authored-by: Samuel Štancl Co-authored-by: PHP CS Fixer --- assets/config.php | 5 + src/Database/DatabaseConfig.php | 85 +++++++++++++-- tests/TenantDatabaseManagerTest.php | 158 ++++++++++++++++++++++++++-- 3 files changed, 229 insertions(+), 19 deletions(-) diff --git a/assets/config.php b/assets/config.php index 0e035953..82a4c722 100644 --- a/assets/config.php +++ b/assets/config.php @@ -98,6 +98,11 @@ return [ */ 'template_tenant_connection' => null, + /** + * The name of the temporary connection used for creating and deleting tenant databases. + */ + 'tenant_host_connection_name' => 'tenant_host_connection', + /** * Tenant database names are created like this: * prefix + tenant_id + suffix. diff --git a/src/Database/DatabaseConfig.php b/src/Database/DatabaseConfig.php index 6c4df0d8..309d828f 100644 --- a/src/Database/DatabaseConfig.php +++ b/src/Database/DatabaseConfig.php @@ -5,10 +5,14 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database; use Closure; +use Illuminate\Database; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase as Tenant; +use Stancl\Tenancy\Database\Exceptions\DatabaseManagerNotRegisteredException; +use Stancl\Tenancy\Database\Exceptions\NoConnectionSetException; class DatabaseConfig { @@ -83,7 +87,7 @@ class DatabaseConfig { $this->tenant->setInternal('db_name', $this->getName()); - if ($this->manager() instanceof Contracts\ManagesDatabaseUsers) { + if ($this->connectionDriverManager($this->getTemplateConnectionName()) 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)); } @@ -100,6 +104,11 @@ class DatabaseConfig ?? config('tenancy.database.central_connection'); } + public function getTenantHostConnectionName(): string + { + return config('tenancy.database.tenant_host_connection_name', 'tenant_host_connection'); + } + /** * Tenant's own database connection config. */ @@ -114,6 +123,40 @@ class DatabaseConfig ); } + /** + * Tenant's host database connection config. + */ + public function hostConnection(): array + { + $config = $this->tenantConfig(); + $template = $this->getTemplateConnectionName(); + $templateConnection = config("database.connections.{$template}"); + + if ($this->connectionDriverManager($template) instanceof Contracts\ManagesDatabaseUsers) { + // We're removing the username and password because user with these credentials is not created yet + // If you need to provide username and password when using PermissionControlledMySQLDatabaseManager, + // consider creating a new connection and use it as `tenancy_db_connection` tenant config key + unset($config['username'], $config['password']); + } + + if (! $config) { + return $templateConnection; + } + + return array_replace($templateConnection, $config); + } + + /** + * Purge host database connection. + * + * It's possible database has previous tenant connection. + * This will clean up the previous connection before creating it for the current tenant. + */ + public function purgeHostConnection(): void + { + DB::purge($this->getTenantHostConnectionName()); + } + /** * Additional config for the database connection, specific to this tenant. */ @@ -140,10 +183,37 @@ class DatabaseConfig }, []); } - /** Get the TenantDatabaseManager for this tenant's connection. */ + /** Get the TenantDatabaseManager for this tenant's connection. + * + * @throws NoConnectionSetException|DatabaseManagerNotRegisteredException + */ public function manager(): Contracts\TenantDatabaseManager { - $driver = config("database.connections.{$this->getTemplateConnectionName()}.driver"); + // Laravel caches the previous PDO connection, so we purge it to be able to change the connection details + $this->purgeHostConnection(); // todo come up with a better name + + // Create the tenant host connection config + $tenantHostConnectionName = $this->getTenantHostConnectionName(); + config(["database.connections.{$tenantHostConnectionName}" => $this->hostConnection()]); + + $manager = $this->connectionDriverManager($tenantHostConnectionName); + + if ($manager instanceof Contracts\StatefulTenantDatabaseManager) { + $manager->setConnection($tenantHostConnectionName); + } + + return $manager; + } + + /** + * todo come up with a better name + * Get database manager class from the given connection config's driver. + * + * @throws DatabaseManagerNotRegisteredException + */ + protected function connectionDriverManager(string $connectionName): Contracts\TenantDatabaseManager + { + $driver = config("database.connections.{$connectionName}.driver"); $databaseManagers = config('tenancy.database.managers'); @@ -151,13 +221,6 @@ class DatabaseConfig throw new Exceptions\DatabaseManagerNotRegisteredException($driver); } - /** @var Contracts\TenantDatabaseManager $databaseManager */ - $databaseManager = app($databaseManagers[$driver]); - - if ($databaseManager instanceof Contracts\StatefulTenantDatabaseManager) { - $databaseManager->setConnection($this->getTemplateConnectionName()); - } - - return $databaseManager; + return app($databaseManagers[$driver]); } } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 33a3158f..19b74e21 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; @@ -52,7 +53,7 @@ test('databases can be created and deleted', function ($driver, $databaseManager expect($manager->databaseExists($name))->toBeTrue(); $manager->deleteDatabase($tenant); expect($manager->databaseExists($name))->toBeFalse(); -})->with('database_manager_provider'); +})->with('database_managers'); test('dbs can be created when another driver is used for the central db', function () { expect(config('database.default'))->toBe('central'); @@ -104,7 +105,7 @@ test('the tenant connection is fully removed', function () { $tenant = Tenant::create(); - expect(array_keys(app('db')->getConnections()))->toBe(['central']); + expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant_host_connection']); pest()->assertArrayNotHasKey('tenant', config('database.connections')); tenancy()->initialize($tenant); @@ -183,7 +184,7 @@ test('a tenants database cannot be created when the database already exists', fu ]); }); -test('tenant database can be created on a foreign server', function () { +test('tenant database can be created and deleted on a foreign server', function () { config([ 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, 'database.connections.mysql2' => [ @@ -219,10 +220,151 @@ test('tenant database can be created on a foreign server', function () { /** @var PermissionControlledMySQLDatabaseManager $manager */ $manager = $tenant->database()->manager(); - $manager->setConnection('mysql'); - expect($manager->databaseExists($name))->toBeFalse(); + expect($manager->databaseExists($name))->toBeTrue(); // mysql2 - $manager->setConnection('mysql2'); + $manager->setConnection('mysql'); + expect($manager->databaseExists($name))->toBeFalse(); // check that the DB doesn't exist in 'mysql' + + $manager->setConnection('mysql2'); // set the connection back + $manager->deleteDatabase($tenant); + + expect($manager->databaseExists($name))->toBeFalse(); +}); + +test('tenant database can be created on a foreign server by using the host from tenant config', function () { + config([ + 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class, + 'tenancy.database.template_tenant_connection' => 'mysql', // This will be overridden by tenancy_db_host + 'database.connections.mysql2' => [ + 'driver' => 'mysql', + 'host' => 'mysql2', + 'port' => 3306, + 'database' => 'main', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $name = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_host' => 'mysql2', + ]); + + /** @var MySQLDatabaseManager $manager */ + $manager = $tenant->database()->manager(); + + expect($manager->databaseExists($name))->toBeTrue(); +}); + +test('database credentials can be provided to PermissionControlledMySQLDatabaseManager by specifying a connection', function () { + config([ + 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, + 'tenancy.database.template_tenant_connection' => 'mysql', + 'database.connections.mysql2' => [ + 'driver' => 'mysql', + 'host' => 'mysql2', + 'port' => 3306, + 'database' => 'main', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ]); + + // Create a new random database user with privileges to use with mysql2 connection + $username = 'dbuser' . Str::random(4); + $password = Str::random('8'); + $mysql2DB = DB::connection('mysql2'); + $mysql2DB->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}';"); + $mysql2DB->statement("GRANT ALL PRIVILEGES ON *.* TO `{$username}`@`%` identified by '{$password}' WITH GRANT OPTION;"); + $mysql2DB->statement("FLUSH PRIVILEGES;"); + + DB::purge('mysql2'); // forget the mysql2 connection so that it uses the new credentials the next time + + config(['database.connections.mysql2.username' => $username]); + config(['database.connections.mysql2.password' => $password]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $name = 'foo' . Str::random(8); + $usernameForNewDB = 'user_for_new_db' . Str::random(4); + $passwordForNewDB = Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_connection' => 'mysql2', + 'tenancy_db_username' => $usernameForNewDB, + 'tenancy_db_password' => $passwordForNewDB, + ]); + + /** @var PermissionControlledMySQLDatabaseManager $manager */ + $manager = $tenant->database()->manager(); + + expect($manager->database()->getConfig('username'))->toBe($username); // user created for the HOST connection + expect($manager->userExists($usernameForNewDB))->toBeTrue(); + expect($manager->databaseExists($name))->toBeTrue(); +}); + +test('tenant database can be created by using the username and password from tenant config', function () { + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + config([ + 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class, + 'tenancy.database.template_tenant_connection' => 'mysql', + ]); + + // Create a new random database user with privileges to use with `mysql` connection + $username = 'dbuser' . Str::random(4); + $password = Str::random('8'); + $mysqlDB = DB::connection('mysql'); + $mysqlDB->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}';"); + $mysqlDB->statement("GRANT ALL PRIVILEGES ON *.* TO `{$username}`@`%` identified by '{$password}' WITH GRANT OPTION;"); + $mysqlDB->statement("FLUSH PRIVILEGES;"); + + DB::purge('mysql2'); // forget the mysql2 connection so that it uses the new credentials the next time + + // Remove `mysql` credentials to make sure we will be using the credentials from the tenant config + config(['database.connections.mysql.username' => null]); + config(['database.connections.mysql.password' => null]); + + $name = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_username' => $username, + 'tenancy_db_password' => $password, + ]); + + /** @var MySQLDatabaseManager $manager */ + $manager = $tenant->database()->manager(); + + expect($manager->database()->getConfig('username'))->toBe($username); // user created for the HOST connection expect($manager->databaseExists($name))->toBeTrue(); }); @@ -245,11 +387,11 @@ test('path used by sqlite manager can be customized', function () { 'tenancy_db_connection' => 'sqlite', ]); - expect(file_exists( $customPath . '/' . $name))->toBeTrue(); + expect(file_exists($customPath . '/' . $name))->toBeTrue(); }); // Datasets -dataset('database_manager_provider', [ +dataset('database_managers', [ ['mysql', MySQLDatabaseManager::class], ['mysql', PermissionControlledMySQLDatabaseManager::class], ['sqlite', SQLiteDatabaseManager::class], From 198f34f5e1524232d661d5a24e64f2ba2566721b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Mon, 31 Oct 2022 12:14:44 +0100 Subject: [PATCH 08/37] [4.x] Add pending tenants (modified #782) (#869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add readied tenants Add config for readied tenants Add `create` and `clear` command Add Readied scope and static functions Add tests * Fix initialize function name * Add readied events * Fix readied column cast * Laravel 6 compatible * Add readied scope tests * Rename config from include_in_scope to include_in_queries * Change terminology to pending * Update CreatePendingTenants.php * Laravel 6 compatible * Update CreatePendingTenants.php * runForMultiple can scope pending tenants * Fix issues * Code and comment style improvements * Change 'tenant' to 'tenants' in command signature * Fix code style (php-cs-fixer) * Rename variables in CreatePendingTenants * Remove withPending from runForMultiple * Update tenants option trait * Update command that use tenants * Fix code style (php-cs-fixer) * Improve getTenants condition * Update config comments * Minor config comment corrections * Grammar fix * Update comments and naming * Correct comments * Improve writing * Remove pending tenant clearing time constraints * Allow using only one time constraint for clearing the pending tenants * phpunit to pest * Fix code style (php-cs-fixer) * Fix code style (php-cs-fixer) * [4.x] Optionally delete storage after tenant deletion (#938) * Add test for deleting storage after tenant deletion * Save `storage_path()` in a variable after initializing tenant in test Co-authored-by: Samuel Štancl * Add DeleteTenantStorage listener * Update test name * Remove storage deletion config key * Remove tenant storage deletion events * Move tenant storage deletion to the DeletingTenant event Co-authored-by: Samuel Štancl * [4.x] Finish incomplete and missing tests (#947) * complete test sqlite manager customize path * complete test seed command works * complete uniqe exists test * Update SingleDatabaseTenancyTest.php * refactor the ternary into if condition * custom path * simplify if condition * random dir name * Update SingleDatabaseTenancyTest.php * Update CommandsTest.php * prefix random DB name with custom_ Co-authored-by: Samuel Štancl * [4.x] Add batch tenancy queue bootstrapper (#874) * exclude master from CI * Add batch tenancy queue bootstrapper * add test case * skip tests for old versions * variable docblocks * use Laravel's connection getter and setter * convert test to pest * bottom space * singleton regis in TestCase * Update src/Bootstrappers/BatchTenancyBootstrapper.php Co-authored-by: Samuel Štancl * convert batch class resolution to property level * enabled BatchTenancyBootstrapper by default * typehint DatabaseBatchRepository * refactore name * DI DB manager * typehint * Update config.php * use initialize() twice without end()ing tenancy to assert that previousConnection logic works correctly Co-authored-by: Samuel Štancl Co-authored-by: Abrar Ahmad Co-authored-by: Samuel Štancl * [4.x] Storage::url() support (modified #689) (#909) * This adds support for tenancy aware Storage::url() method * Trigger CI build * Fixed Link command for Laravel v6, added StorageLink Events, more StorageLink tests, added RemoveStorageSymlinks Job, added Storage Jobs to TenancyServiceProvider stub, renamed misleading config example. * Fix typo * Fix code style (php-cs-fixer) * Update config comments * Format code in Link command, make writing more concise * Change "symLinks" to "symlinks" * Refactor Link command * Fix test name typo * Test fetching files using the public URL * Extract Link command logic into actions * Fix code style (php-cs-fixer) * Check if closure is null in CreateStorageSymlinksAction * Stop using command terminology in CreateStorageSymlinksAction * Separate the Storage::url() test cases * Update url_override comments * Remove afterLink closures, add types, move actions, add usage explanation to the symlink trait * Fix code style (php-cs-fixer) * Update public storage URL test * Fix issue with using str() * Improve url_override comment, add todos * add todo comment * fix docblock style * Add link command tests back * Add types to $tenants in the action handle() methods * Fix typo, update variable name formatting * Add tests for the symlink actions * Change possibleTenantSymlinks not to prefix the paths twice while tenancy is initialized * Fix code style (php-cs-fixer) * Stop testing storage directory existence in symlink test * Don't specify full namespace for Tenant model annotation * Don't specify full namespace in ActionTest * Remove "change to DI" todo * Remove possibleTenantSymlinks return annotation * Remove symlink-related jobs, instantiate and use actions * Revert "Remove symlink-related jobs, instantiate and use actions" This reverts commit 547440c887dd86d75c7a5543fec576e233487eff. * Add a comment line about the possible tenant symlinks * Correct storagePath and publicPath variables * Revert "Correct storagePath and publicPath variables" This reverts commit e3aa8e208686e5fdf8e15a3bdb88d6f9853316fe. * add a todo Co-authored-by: Martin Vlcek Co-authored-by: lukinovec Co-authored-by: PHP CS Fixer * Use HasTenantOptions in Link * Correct the tenant order in Run command * Fix code style (php-cs-fixer) * Fix formatting issue * Add missing imports * Fix code style (php-cs-fixer) * Use HasTenantOptions instead of the old trait name in Up/Down commands * Fix test name typo * Remove redundant passing of $withPending to runForMultiple in TenantCollection's runForEach * Make `with-pending` default to `config('tenancy.pending.include_in_queries')` in HasTenantOptions * Make `createPending()` return the created tenant * Fix code style (php-cs-fixer) * Remove tenant ordering * Fix code style (php-cs-fixer) * Remove duplicate tenancy bootstrappers config setting * Add and use getWithPendingOption method * Fix code style (php-cs-fixer) * Add optionNotPassedValue property * Test using --with-pending and the include_in_queries config value * Make with-pending VALUE_NONE * use plural in test names * fix test names * add pullPendingTenantFromPool * Add docblock type * Import commands * Fix code style (php-cs-fixer) * Move pending tenant tests to a more appropriate file * Delete queuetest from gitignore * Delete queuetest file * Add queuetest to gitignore * Rename pullPendingTenant to pullPending and don't pass bool to that method * Add a test that checks if pulling a pending tenant removes it from the pool * bump stancl/virtualcolumn to ^1.3 * Update pending tenant pulling test * Dynamically get columns for pending queries * Dynamically get virtual column name in ClearPendingTenants * Fix ClearPendingTenants bug * Make test name more accurate * Update test name * add a todo * Update include in queries test name * Remove `Tenant::query()->delete()` from pending tenant check test * Rename the pending tenant check test name * Update HasPending.php * fix all() call * code style * all() -> get() * Remove redundant `Tenant::all()` call Co-authored-by: j.stein Co-authored-by: lukinovec Co-authored-by: PHP CS Fixer Co-authored-by: Abrar Ahmad Co-authored-by: Riley19280 Co-authored-by: Martin Vlcek --- .gitignore | 1 + assets/TenancyServiceProvider.stub.php | 6 + assets/config.php | 19 ++ composer.json | 2 +- src/Commands/ClearPendingTenants.php | 74 +++++++ src/Commands/CreatePendingTenants.php | 62 ++++++ src/Commands/Down.php | 4 +- src/Commands/Link.php | 4 +- src/Commands/Migrate.php | 5 +- src/Commands/MigrateFresh.php | 5 +- src/Commands/Rollback.php | 5 +- src/Commands/Run.php | 4 +- src/Commands/Seed.php | 4 +- src/Commands/Up.php | 4 +- ...TenantsOption.php => HasTenantOptions.php} | 10 +- src/Database/Concerns/HasPending.php | 103 +++++++++ src/Database/Concerns/PendingScope.php | 88 ++++++++ src/Database/Models/Tenant.php | 1 + src/Events/CreatingPendingTenant.php | 9 + src/Events/PendingTenantCreated.php | 9 + src/Events/PendingTenantPulled.php | 9 + src/Events/PullingPendingTenant.php | 9 + src/Jobs/ClearPendingTenants.php | 28 +++ src/Jobs/CreatePendingTenants.php | 28 +++ src/Tenancy.php | 2 +- src/TenancyServiceProvider.php | 6 +- tests/CommandsTest.php | 5 - tests/Etc/Console/AddUserCommand.php | 4 +- tests/Etc/Tenant.php | 3 +- tests/PendingTenantsTest.php | 209 ++++++++++++++++++ 30 files changed, 693 insertions(+), 29 deletions(-) create mode 100644 src/Commands/ClearPendingTenants.php create mode 100644 src/Commands/CreatePendingTenants.php rename src/Concerns/{HasATenantsOption.php => HasTenantOptions.php} (63%) create mode 100644 src/Database/Concerns/HasPending.php create mode 100644 src/Database/Concerns/PendingScope.php create mode 100644 src/Events/CreatingPendingTenant.php create mode 100644 src/Events/PendingTenantCreated.php create mode 100644 src/Events/PendingTenantPulled.php create mode 100644 src/Events/PullingPendingTenant.php create mode 100644 src/Jobs/ClearPendingTenants.php create mode 100644 src/Jobs/CreatePendingTenants.php create mode 100644 tests/PendingTenantsTest.php diff --git a/.gitignore b/.gitignore index 64d9dc21..5a5960b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +.DS_Store composer.lock vendor/ .vscode/ diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 7c52e295..a38aee42 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -61,6 +61,12 @@ class TenancyServiceProvider extends ServiceProvider Events\TenantMaintenanceModeEnabled::class => [], Events\TenantMaintenanceModeDisabled::class => [], + // Pending tenant events + Events\CreatingPendingTenant::class => [], + Events\PendingTenantCreated::class => [], + Events\PullingPendingTenant::class => [], + Events\PendingTenantPulled::class => [], + // Domain events Events\CreatingDomain::class => [], Events\DomainCreated::class => [], diff --git a/assets/config.php b/assets/config.php index 82a4c722..20826d7d 100644 --- a/assets/config.php +++ b/assets/config.php @@ -86,6 +86,25 @@ return [ // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed ], + + /** + * Pending tenants config. + * This is useful if you're looking for a way to always have a tenant ready to be used. + */ + 'pending' => [ + /** + * If disabled, pending tenants will be excluded from all tenant queries. + * You can still use ::withPending(), ::withoutPending() and ::onlyPending() to include or exclude the pending tenants regardless of this setting. + * Note: when disabled, this will also ignore pending tenants when running the tenant commands (migration, seed, etc.) + */ + 'include_in_queries' => true, + /** + * Defines how many pending tenants you want to have ready in the pending tenant pool. + * This depends on the volume of tenants you're creating. + */ + 'count' => env('TENANCY_PENDING_COUNT', 5), + ], + /** * Database tenancy config. Used by DatabaseTenancyBootstrapper. */ diff --git a/composer.json b/composer.json index b30ea94c..49657912 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "spatie/ignition": "^1.4", "ramsey/uuid": "^4.0", "stancl/jobpipeline": "^1.0", - "stancl/virtualcolumn": "^1.0" + "stancl/virtualcolumn": "^1.3" }, "require-dev": { "laravel/framework": "^9.0", diff --git a/src/Commands/ClearPendingTenants.php b/src/Commands/ClearPendingTenants.php new file mode 100644 index 00000000..18d9fa42 --- /dev/null +++ b/src/Commands/ClearPendingTenants.php @@ -0,0 +1,74 @@ +info('Removing pending tenants.'); + + $expirationDate = now(); + // We compare the original expiration date to the new one to check if the new one is different later + $originalExpirationDate = $expirationDate->copy()->toImmutable(); + + // Skip the time constraints if the 'all' option is given + if (! $this->option('all')) { + $olderThanDays = $this->option('older-than-days'); + $olderThanHours = $this->option('older-than-hours'); + + if ($olderThanDays && $olderThanHours) { + $this->line(" Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components + $this->line('Please, choose only one of these options.'); + + return 1; // Exit code for failure + } + + if ($olderThanDays) { + $expirationDate->subDays($olderThanDays); + } + + if ($olderThanHours) { + $expirationDate->subHours($olderThanHours); + } + } + + $deletedTenantCount = tenancy() + ->query() + ->onlyPending() + ->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) { + $query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp); + }) + ->get() + ->each // Trigger the model events by deleting the tenants one by one + ->delete() + ->count(); + + $this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.'); + } +} diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php new file mode 100644 index 00000000..88202093 --- /dev/null +++ b/src/Commands/CreatePendingTenants.php @@ -0,0 +1,62 @@ +info('Creating pending tenants.'); + + $maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count')); + $pendingTenantCount = $this->getPendingTenantCount(); + $createdCount = 0; + + while ($pendingTenantCount < $maxPendingTenantCount) { + tenancy()->model()::createPending(); + + // Fetching the pending tenant count in each iteration prevents creating too many tenants + // If pending tenants are being created somewhere else while running this command + $pendingTenantCount = $this->getPendingTenantCount(); + + $createdCount++; + } + + $this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.'); + $this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); + + return 1; + } + + /** + * Calculate the number of currently available pending tenants. + */ + private function getPendingTenantCount(): int + { + return tenancy() + ->query() + ->onlyPending() + ->count(); + } +} diff --git a/src/Commands/Down.php b/src/Commands/Down.php index e7341d7f..3b68bcb2 100644 --- a/src/Commands/Down.php +++ b/src/Commands/Down.php @@ -5,11 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Foundation\Console\DownCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; class Down extends DownCommand { - use HasATenantsOption; + use HasTenantOptions; protected $signature = 'tenants:down {--redirect= : The path that users should be redirected to} diff --git a/src/Commands/Link.php b/src/Commands/Link.php index 0a587122..a6dd6c5f 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -9,11 +9,11 @@ use Illuminate\Console\Command; use Illuminate\Support\LazyCollection; use Stancl\Tenancy\Actions\CreateStorageSymlinksAction; use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; class Link extends Command { - use HasATenantsOption; + use HasTenantOptions; protected $signature = 'tenants:link {--tenants=* : The tenant(s) to run the command for. Default: all} diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 82395fcc..0d2fceaa 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -7,14 +7,15 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Console\Migrations\MigrateCommand; use Illuminate\Database\Migrations\Migrator; +use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Events\DatabaseMigrated; use Stancl\Tenancy\Events\MigratingDatabase; class Migrate extends MigrateCommand { - use HasATenantsOption, ExtendsLaravelCommand; + use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand; protected $description = 'Run migrations for tenant(s)'; diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 657c4990..45a93115 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\DealsWithMigrations; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Symfony\Component\Console\Input\InputOption; class MigrateFresh extends Command { - use HasATenantsOption; + use HasTenantOptions, DealsWithMigrations; protected $description = 'Drop all tables and re-run all migrations for tenant(s)'; diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index 1e84ab12..f9d9dac0 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -6,14 +6,15 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Migrations\Migrator; +use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Events\DatabaseRolledBack; use Stancl\Tenancy\Events\RollingBackDatabase; class Rollback extends RollbackCommand { - use HasATenantsOption, ExtendsLaravelCommand; + use HasTenantOptions, DealsWithMigrations, ExtendsLaravelCommand; protected $description = 'Rollback migrations for tenant(s).'; diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 5ecc7c77..afc9871a 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; class Run extends Command { - use HasATenantsOption; + use HasTenantOptions; protected $description = 'Run a command for tenant(s)'; diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 8ed0b6d9..5cf468e9 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\Seeds\SeedCommand; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Events\DatabaseSeeded; use Stancl\Tenancy\Events\SeedingDatabase; class Seed extends SeedCommand { - use HasATenantsOption; + use HasTenantOptions; protected $description = 'Seed tenant database(s).'; diff --git a/src/Commands/Up.php b/src/Commands/Up.php index 08c935c3..cf005251 100644 --- a/src/Commands/Up.php +++ b/src/Commands/Up.php @@ -5,11 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; class Up extends Command { - use HasATenantsOption; + use HasTenantOptions; protected $signature = 'tenants:up'; diff --git a/src/Concerns/HasATenantsOption.php b/src/Concerns/HasTenantOptions.php similarity index 63% rename from src/Concerns/HasATenantsOption.php rename to src/Concerns/HasTenantOptions.php index 32d508ec..f8a763a7 100644 --- a/src/Concerns/HasATenantsOption.php +++ b/src/Concerns/HasTenantOptions.php @@ -5,14 +5,19 @@ declare(strict_types=1); namespace Stancl\Tenancy\Concerns; use Illuminate\Support\LazyCollection; +use Stancl\Tenancy\Database\Concerns\PendingScope; use Symfony\Component\Console\Input\InputOption; -trait HasATenantsOption +/** + * Adds 'tenants' and 'with-pending' options. + */ +trait HasTenantOptions { protected function getOptions() { return array_merge([ ['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null], + ['with-pending', null, InputOption::VALUE_NONE, 'include pending tenants in query'], ], parent::getOptions()); } @@ -23,6 +28,9 @@ trait HasATenantsOption ->when($this->option('tenants'), function ($query) { $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); }) + ->when(tenancy()->model()::hasGlobalScope(PendingScope::class), function ($query) { + $query->withPending(config('tenancy.pending.include_in_queries') ?: $this->option('with-pending')); + }) ->cursor(); } diff --git a/src/Database/Concerns/HasPending.php b/src/Database/Concerns/HasPending.php new file mode 100644 index 00000000..3fa9399d --- /dev/null +++ b/src/Database/Concerns/HasPending.php @@ -0,0 +1,103 @@ +casts['pending_since'] = 'timestamp'; + } + + /** + * Determine if the model instance is in a pending state. + * + * @return bool + */ + public function pending() + { + return ! is_null($this->pending_since); + } + + /** Create a pending tenant. */ + public static function createPending($attributes = []): Tenant + { + $tenant = static::create($attributes); + + event(new CreatingPendingTenant($tenant)); + + // Update the pending_since value only after the tenant is created so it's + // Not marked as pending until finishing running the migrations, seeders, etc. + $tenant->update([ + 'pending_since' => now()->timestamp, + ]); + + event(new PendingTenantCreated($tenant)); + + return $tenant; + } + + /** Pull a pending tenant. */ + public static function pullPending(): Tenant + { + return static::pullPendingFromPool(true); + } + + /** Try to pull a tenant from the pool of pending tenants. */ + public static function pullPendingFromPool(bool $firstOrCreate = false): ?Tenant + { + if (! static::onlyPending()->exists()) { + if (! $firstOrCreate) { + return null; + } + + static::createPending(); + } + + // A pending tenant is surely available at this point + $tenant = static::onlyPending()->first(); + + event(new PullingPendingTenant($tenant)); + + $tenant->update([ + 'pending_since' => null, + ]); + + event(new PendingTenantPulled($tenant)); + + return $tenant; + } +} diff --git a/src/Database/Concerns/PendingScope.php b/src/Database/Concerns/PendingScope.php new file mode 100644 index 00000000..8a6ad913 --- /dev/null +++ b/src/Database/Concerns/PendingScope.php @@ -0,0 +1,88 @@ +when(! config('tenancy.pending.include_in_queries'), function (Builder $builder) { + $builder->whereNull($builder->getModel()->getColumnForQuery('pending_since')); + }); + } + + /** + * Extend the query builder with the needed functions. + * + * @return void + */ + public function extend(Builder $builder) + { + foreach ($this->extensions as $extension) { + $this->{"add{$extension}"}($builder); + } + } + /** + * Add the with-pending extension to the builder. + * + * @return void + */ + protected function addWithPending(Builder $builder) + { + $builder->macro('withPending', function (Builder $builder, $withPending = true) { + if (! $withPending) { + return $builder->withoutPending(); + } + + return $builder->withoutGlobalScope($this); + }); + } + + /** + * Add the without-pending extension to the builder. + * + * @return void + */ + protected function addWithoutPending(Builder $builder) + { + $builder->macro('withoutPending', function (Builder $builder) { + $builder->withoutGlobalScope($this) + ->whereNull($builder->getModel()->getColumnForQuery('pending_since')) + ->orWhereNull($builder->getModel()->getDataColumn()); + + return $builder; + }); + } + + /** + * Add the only-pending extension to the builder. + * + * @return void + */ + protected function addOnlyPending(Builder $builder) + { + $builder->macro('onlyPending', function (Builder $builder) { + $builder->withoutGlobalScope($this)->whereNotNull($builder->getModel()->getColumnForQuery('pending_since')); + + return $builder; + }); + } +} diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index 9cb5f5f3..37c2af2d 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -28,6 +28,7 @@ class Tenant extends Model implements Contracts\Tenant Concerns\GeneratesIds, Concerns\HasInternalKeys, Concerns\TenantRun, + Concerns\HasPending, Concerns\InitializationHelpers, Concerns\InvalidatesResolverCache; diff --git a/src/Events/CreatingPendingTenant.php b/src/Events/CreatingPendingTenant.php new file mode 100644 index 00000000..dfbe6c70 --- /dev/null +++ b/src/Events/CreatingPendingTenant.php @@ -0,0 +1,9 @@ +model()->cursor(); // todo1 phpstan thinks this isn't needed, but tests fail without it $originalTenant = $this->tenant; diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 63a22a11..01770cda 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -78,7 +78,9 @@ class TenancyServiceProvider extends ServiceProvider public function boot(): void { $this->commands([ + Commands\Up::class, Commands\Run::class, + Commands\Down::class, Commands\Link::class, Commands\Seed::class, Commands\Install::class, @@ -87,8 +89,8 @@ class TenancyServiceProvider extends ServiceProvider Commands\TenantList::class, Commands\TenantDump::class, Commands\MigrateFresh::class, - Commands\Down::class, - Commands\Up::class, + Commands\ClearPendingTenants::class, + Commands\CreatePendingTenants::class, ]); $this->app->extend(FreshCommand::class, function () { diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index ea973070..95672753 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -24,7 +24,6 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; - beforeEach(function () { if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) { unlink($schemaPath); @@ -34,10 +33,6 @@ beforeEach(function () { return $event->tenant; })->toListener()); - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); - config([ 'tenancy.bootstrappers' => [ DatabaseTenancyBootstrapper::class, diff --git a/tests/Etc/Console/AddUserCommand.php b/tests/Etc/Console/AddUserCommand.php index f102bae6..9b421f95 100644 --- a/tests/Etc/Console/AddUserCommand.php +++ b/tests/Etc/Console/AddUserCommand.php @@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Tests\Etc\Console; use Illuminate\Console\Command; use Illuminate\Support\Str; -use Stancl\Tenancy\Concerns\HasATenantsOption; +use Stancl\Tenancy\Concerns\HasTenantOptions; use Stancl\Tenancy\Concerns\TenantAwareCommand; use Stancl\Tenancy\Tests\Etc\User; class AddUserCommand extends Command { - use TenantAwareCommand, HasATenantsOption; + use TenantAwareCommand, HasTenantOptions; /** * The name and signature of the console command. diff --git a/tests/Etc/Tenant.php b/tests/Etc/Tenant.php index 9b59dedb..f9a11d95 100644 --- a/tests/Etc/Tenant.php +++ b/tests/Etc/Tenant.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Tests\Etc; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; +use Stancl\Tenancy\Database\Concerns\HasPending; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Database\Models; @@ -15,5 +16,5 @@ use Stancl\Tenancy\Database\Models; */ class Tenant extends Models\Tenant implements TenantWithDatabase { - use HasDatabase, HasDomains, MaintenanceMode; + use HasDatabase, HasDomains, HasPending, MaintenanceMode; } diff --git a/tests/PendingTenantsTest.php b/tests/PendingTenantsTest.php new file mode 100644 index 00000000..8dbda9ee --- /dev/null +++ b/tests/PendingTenantsTest.php @@ -0,0 +1,209 @@ +count())->toBe(1); + + Tenant::onlyPending()->first()->update([ + 'pending_since' => null + ]); + + expect(Tenant::onlyPending()->count())->toBe(0); +}); + +test('pending trait adds query scopes', function () { + Tenant::createPending(); + Tenant::create(); + Tenant::create(); + + expect(Tenant::onlyPending()->count())->toBe(1) + ->and(Tenant::withPending(true)->count())->toBe(3) + ->and(Tenant::withPending(false)->count())->toBe(2) + ->and(Tenant::withoutPending()->count())->toBe(2); + +}); + +test('pending tenants can be created and deleted using commands', function () { + config(['tenancy.pending.count' => 4]); + + Artisan::call(CreatePendingTenants::class); + + expect(Tenant::onlyPending()->count())->toBe(4); + + Artisan::call(ClearPendingTenants::class); + + expect(Tenant::onlyPending()->count())->toBe(0); +}); + +test('CreatePendingTenants command can have an older than constraint', function () { + config(['tenancy.pending.count' => 2]); + + Artisan::call(CreatePendingTenants::class); + + tenancy()->model()->query()->onlyPending()->first()->update([ + 'pending_since' => now()->subDays(5)->timestamp + ]); + + Artisan::call('tenants:pending-clear --older-than-days=2'); + + expect(Tenant::onlyPending()->count())->toBe(1); +}); + +test('CreatePendingTenants command cannot run with both time constraints', function () { + pest()->artisan('tenants:pending-clear --older-than-days=2 --older-than-hours=2') + ->assertFailed(); +}); + +test('CreatePendingTenants commands all option overrides any config constraints', function () { + Tenant::createPending(); + Tenant::createPending(); + + tenancy()->model()->query()->onlyPending()->first()->update([ + 'pending_since' => now()->subDays(10) + ]); + + config(['tenancy.pending.older_than_days' => 4]); + + Artisan::call(ClearPendingTenants::class, [ + '--all' => true + ]); + + expect(Tenant::onlyPending()->count())->toBe(0); +}); + +test('tenancy can check if there are any pending tenants', function () { + expect(Tenant::onlyPending()->exists())->toBeFalse(); + + Tenant::createPending(); + + expect(Tenant::onlyPending()->exists())->toBeTrue(); +}); + +test('tenancy can pull a pending tenant', function () { + Tenant::createPending(); + + expect(Tenant::pullPendingFromPool())->toBeInstanceOf(Tenant::class); +}); + +test('pulling a tenant from the pending tenant pool removes it from the pool', function () { + Tenant::createPending(); + + expect(Tenant::onlyPending()->count())->toEqual(1); + + Tenant::pullPendingFromPool(); + + expect(Tenant::onlyPending()->count())->toEqual(0); +}); + +test('a new tenant gets created while pulling a pending tenant if the pending pool is empty', function () { + expect(Tenant::withPending()->get()->count())->toBe(0); // All tenants + + Tenant::pullPending(); + + expect(Tenant::withPending()->get()->count())->toBe(1); // All tenants +}); + +test('pending tenants are included in all queries based on the include_in_queries config', function () { + Tenant::createPending(); + + config(['tenancy.pending.include_in_queries' => false]); + + expect(Tenant::all()->count())->toBe(0); + + config(['tenancy.pending.include_in_queries' => true]); + + expect(Tenant::all()->count())->toBe(1); +}); + +test('pending events are dispatched', function () { + Event::fake([ + CreatingPendingTenant::class, + PendingTenantCreated::class, + PullingPendingTenant::class, + PendingTenantPulled::class, + ]); + + Tenant::createPending(); + + Event::assertDispatched(CreatingPendingTenant::class); + Event::assertDispatched(PendingTenantCreated::class); + + Tenant::pullPending(); + + Event::assertDispatched(PullingPendingTenant::class); + Event::assertDispatched(PendingTenantPulled::class); +}); + +test('commands do not run for pending tenants if tenancy.pending.include_in_queries is false and the with pending option does not get passed', function() { + config(['tenancy.pending.include_in_queries' => false]); + + $tenants = collect([ + Tenant::create(), + Tenant::create(), + Tenant::createPending(), + Tenant::createPending(), + ]); + + pest()->artisan('tenants:migrate --with-pending'); + + $artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'"); + + $pendingTenants = $tenants->filter->pending(); + $readyTenants = $tenants->reject->pending(); + + $pendingTenants->each(fn ($tenant) => $artisan->doesntExpectOutputToContain("Tenant: {$tenant->getTenantKey()}")); + $readyTenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}")); + + $artisan->assertExitCode(0); +}); + +test('commands run for pending tenants too if tenancy.pending.include_in_queries is true', function() { + config(['tenancy.pending.include_in_queries' => true]); + + $tenants = collect([ + Tenant::create(), + Tenant::create(), + Tenant::createPending(), + Tenant::createPending(), + ]); + + pest()->artisan('tenants:migrate --with-pending'); + + $artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'"); + + $tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}")); + + $artisan->assertExitCode(0); +}); + +test('commands run for pending tenants too if the with pending option is passed', function() { + config(['tenancy.pending.include_in_queries' => false]); + + $tenants = collect([ + Tenant::create(), + Tenant::create(), + Tenant::createPending(), + Tenant::createPending(), + ]); + + pest()->artisan('tenants:migrate --with-pending'); + + $artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz' --with-pending"); + + $tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}")); + + $artisan->assertExitCode(0); +}); From aa536529dfd11fbbca96ba51a937331bc098fea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 1 Nov 2022 17:48:56 +0100 Subject: [PATCH 09/37] update DatabaseSeeder namespace --- assets/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/config.php b/assets/config.php index 20826d7d..cfde54ac 100644 --- a/assets/config.php +++ b/assets/config.php @@ -290,7 +290,7 @@ return [ * Parameters used by the tenants:seed command. */ 'seeder_parameters' => [ - '--class' => 'DatabaseSeeder', // root seeder class + '--class' => 'Database\Seeders\DatabaseSeeder', // root seeder class // '--force' => true, ], From 77c5ae1f32e8c54510a37206f8d96786fd766031 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Thu, 3 Nov 2022 21:51:29 +0500 Subject: [PATCH 10/37] [4.x] Configure attributes for synced resources when creating models (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * configure attributes for creating resource * Update ResourceSyncingTest.php * Update ci.yml * Update ResourceSyncingTest.php * Update ci.yml * cs * comments * Update tests/ResourceSyncingTest.php Co-authored-by: Samuel Štancl * improve comments, move method to `SyncMaster` interface * Revert "improve comments, move method to `SyncMaster` interface" This reverts commit 5ddd50deb9f5aa2ad0ebadec5bd5afcbf7e1257e. * Update ResourceSyncingTest.php * Update ResourceSyncingTest.php * update comment * Update ResourceSyncingTest.php * Update ResourceSyncingTest.php * wip * wip * wip * add a todo * assert that creation attributes returns null * classes at the end * rename method to `getAttributesForCreation` * Update ResourceSyncingTest.php * update comments * Fix little grammer * merge default values with sync attributes and tests * Update ResourceSyncingTest.php * method rename * method rename * Update ResourceSyncingTest.php * comments * Update ResourceSyncingTest.php * allow defining a mix of attribute names and default values * add test * code improvements * Fix code style (php-cs-fixer) * remove unused import * fix all phpstan issues in resource syncing code * Fix code style (php-cs-fixer) * wip * improve tests * Update ResourceSyncingTest.php * better names * Update UpdateSyncedResource.php * code style * Update UpdateSyncedResource.php * add comments above new tests * methods dockblocks and correct names * Update ResourceSyncingTest.php * update comments * remove different schema setup * delete custom migrations * self review * grammar, code style * refactor helpers for creating tenants Co-authored-by: Samuel Štancl Co-authored-by: Samuel Štancl Co-authored-by: PHP CS Fixer --- src/Contracts/Syncable.php | 3 + src/Database/Concerns/ResourceSyncing.php | 5 + src/Events/SyncedResourceSaved.php | 13 +- src/Listeners/UpdateSyncedResource.php | 86 +++- ...dd_extra_column_to_central_users_table.php | 26 ++ tests/ResourceSyncingTest.php | 373 +++++++++++++++++- 6 files changed, 469 insertions(+), 37 deletions(-) create mode 100644 tests/Etc/synced_resource_migrations/users_extra/2019_08_08_000009_add_extra_column_to_central_users_table.php diff --git a/src/Contracts/Syncable.php b/src/Contracts/Syncable.php index e09f4f7e..a481f318 100644 --- a/src/Contracts/Syncable.php +++ b/src/Contracts/Syncable.php @@ -15,4 +15,7 @@ interface Syncable public function getSyncedAttributeNames(): array; public function triggerSyncEvent(): void; + + /** Get the attributes used for creating the *other* model (i.e. tenant if this is the central one, and central if this is the tenant one). */ + public function getSyncedCreationAttributes(): array|null; // todo come up with a better name } diff --git a/src/Database/Concerns/ResourceSyncing.php b/src/Database/Concerns/ResourceSyncing.php index df5b0766..fd63738d 100644 --- a/src/Database/Concerns/ResourceSyncing.php +++ b/src/Database/Concerns/ResourceSyncing.php @@ -32,4 +32,9 @@ trait ResourceSyncing /** @var Syncable $this */ event(new SyncedResourceSaved($this, tenant())); } + + public function getSyncedCreationAttributes(): array|null + { + return null; + } } diff --git a/src/Events/SyncedResourceSaved.php b/src/Events/SyncedResourceSaved.php index 72d34d16..5c3b1334 100644 --- a/src/Events/SyncedResourceSaved.php +++ b/src/Events/SyncedResourceSaved.php @@ -10,14 +10,9 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; class SyncedResourceSaved { - public Syncable&Model $model; - - /** @var (TenantWithDatabase&Model)|null */ - public TenantWithDatabase|null $tenant; - - public function __construct(Syncable $model, TenantWithDatabase|null $tenant) - { - $this->model = $model; - $this->tenant = $tenant; + public function __construct( + public Syncable&Model $model, + public TenantWithDatabase|null $tenant, + ) { } } diff --git a/src/Listeners/UpdateSyncedResource.php b/src/Listeners/UpdateSyncedResource.php index 45f73516..39391eac 100644 --- a/src/Listeners/UpdateSyncedResource.php +++ b/src/Listeners/UpdateSyncedResource.php @@ -4,14 +4,19 @@ 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 Illuminate\Support\Arr; +use Stancl\Tenancy\Contracts\Syncable; use Stancl\Tenancy\Contracts\SyncMaster; +use Stancl\Tenancy\Contracts\Tenant; +use Stancl\Tenancy\Database\TenantCollection; use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase; use Stancl\Tenancy\Events\SyncedResourceSaved; use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException; +// todo@v4 review all code related to resource syncing + class UpdateSyncedResource extends QueueableListener { public static bool $shouldQueue = false; @@ -30,25 +35,28 @@ class UpdateSyncedResource extends QueueableListener $this->updateResourceInTenantDatabases($tenants, $event, $syncedAttributes); } - protected function getTenantsForCentralModel($centralModel): EloquentCollection + protected function getTenantsForCentralModel(Syncable $centralModel): TenantCollection { if (! $centralModel instanceof SyncMaster) { // If we're trying to use a tenant User model instead of the central User model, for example. throw new ModelNotSyncMasterException(get_class($centralModel)); } - /** @var SyncMaster|Model $centralModel */ + /** @var Tenant&Model&SyncMaster $centralModel */ // Since this model is "dirty" (taken by reference from the event), it might have the tenants // relationship already loaded and cached. For this reason, we refresh the relationship. $centralModel->load('tenants'); - return $centralModel->tenants; + /** @var TenantCollection $tenants */ + $tenants = $centralModel->tenants; + + return $tenants; } - protected function updateResourceInCentralDatabaseAndGetTenants($event, $syncedAttributes): EloquentCollection + protected function updateResourceInCentralDatabaseAndGetTenants(SyncedResourceSaved $event, array $syncedAttributes): TenantCollection { - /** @var Model|SyncMaster $centralModel */ + /** @var (Model&SyncMaster)|null $centralModel */ $centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey()) ->first(); @@ -59,15 +67,17 @@ class UpdateSyncedResource extends QueueableListener event(new SyncedResourceChangedInForeignDatabase($event->model, null)); } else { // If the resource doesn't exist at all in the central DB,we create - // the record with all attributes, not just the synced ones. - $centralModel = $event->model->getCentralModelName()::create($event->model->getAttributes()); + $centralModel = $event->model->getCentralModelName()::create($this->getAttributesForCreation($event->model)); event(new SyncedResourceChangedInForeignDatabase($event->model, null)); } }); // If the model was just created, the mapping of the tenant to the user likely doesn't exist, so we create it. $currentTenantMapping = function ($model) use ($event) { - return ((string) $model->pivot->tenant_id) === ((string) $event->tenant->getTenantKey()); + /** @var Tenant */ + $tenant = $event->tenant; + + return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey()); }; $mappingExists = $centralModel->tenants->contains($currentTenantMapping); @@ -76,22 +86,29 @@ class UpdateSyncedResource extends QueueableListener // Here we should call TenantPivot, but we call general Pivot, so that this works // even if people use their own pivot model that is not based on our TenantPivot Pivot::withoutEvents(function () use ($centralModel, $event) { - $centralModel->tenants()->attach($event->tenant->getTenantKey()); + /** @var Tenant */ + $tenant = $event->tenant; + + $centralModel->tenants()->attach($tenant->getTenantKey()); }); } - return $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) { + /** @var TenantCollection $tenants */ + $tenants = $centralModel->tenants->filter(function ($model) use ($currentTenantMapping) { // Remove the mapping for the current tenant. return ! $currentTenantMapping($model); }); + + return $tenants; } - protected function updateResourceInTenantDatabases($tenants, $event, $syncedAttributes): void + protected function updateResourceInTenantDatabases(TenantCollection $tenants, SyncedResourceSaved $event, array $syncedAttributes): void { tenancy()->runForMultiple($tenants, function ($tenant) use ($event, $syncedAttributes) { // Forget instance state and find the model, // again in the current tenant's context. + /** @var Model&Syncable $eventModel */ $eventModel = $event->model; if ($eventModel instanceof SyncMaster) { @@ -112,12 +129,53 @@ class UpdateSyncedResource extends QueueableListener if ($localModel) { $localModel->update($syncedAttributes); } else { - // When creating, we use all columns, not just the synced ones. - $localModel = $localModelClass::create($eventModel->getAttributes()); + $localModel = $localModelClass::create($this->getAttributesForCreation($eventModel)); } event(new SyncedResourceChangedInForeignDatabase($localModel, $tenant)); }); }); } + + protected function getAttributesForCreation(Model&Syncable $model): array + { + if (! $model->getSyncedCreationAttributes()) { + // Creation attributes are not specified so create the model as 1:1 copy + // exclude the "primary key" because we want primary key to handle by the target model to avoid duplication errors + $attributes = $model->getAttributes(); + unset($attributes[$model->getKeyName()]); + + return $attributes; + } + + if (Arr::isAssoc($model->getSyncedCreationAttributes())) { + // Developer provided the default values (key => value) or mix of default values and attribute names (values only) + // We will merge the default values with provided attributes and sync attributes + [$attributeNames, $defaultValues] = $this->getAttributeNamesAndDefaultValues($model); + $attributes = $model->only(array_merge($model->getSyncedAttributeNames(), $attributeNames)); + + return array_merge($attributes, $defaultValues); + } + + // Developer provided the attribute names, so we'll use them to pick model attributes + return $model->only($model->getSyncedCreationAttributes()); + } + + /** + * Split the attribute names (sequential index items) and default values (key => values). + */ + protected function getAttributeNamesAndDefaultValues(Model&Syncable $model): array + { + $syncedCreationAttributes = $model->getSyncedCreationAttributes() ?? []; + + $attributes = Arr::where($syncedCreationAttributes, function ($value, $key) { + return is_numeric($key); + }); + + $defaultValues = Arr::where($syncedCreationAttributes, function ($value, $key) { + return is_string($key); + }); + + return [$attributes, $defaultValues]; + } } diff --git a/tests/Etc/synced_resource_migrations/users_extra/2019_08_08_000009_add_extra_column_to_central_users_table.php b/tests/Etc/synced_resource_migrations/users_extra/2019_08_08_000009_add_extra_column_to_central_users_table.php new file mode 100644 index 00000000..bfa13cc1 --- /dev/null +++ b/tests/Etc/synced_resource_migrations/users_extra/2019_08_08_000009_add_extra_column_to_central_users_table.php @@ -0,0 +1,26 @@ +string('foo'); + }); + } + + public function down() + { + } +} diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 214a9f47..430c52ef 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -44,9 +44,10 @@ beforeEach(function () { Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); - UpdateSyncedResource::$shouldQueue = false; // global state cleanup + UpdateSyncedResource::$shouldQueue = false; // Global state cleanup Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class); + // Run migrations on central connection pest()->artisan('migrate', [ '--path' => [ __DIR__ . '/Etc/synced_resource_migrations', @@ -83,7 +84,7 @@ test('only the synced columns are updated in the central db', function () { ]); $tenant = ResourceTenant::create(); - migrateTenantsResource(); + migrateUsersTableForTenants(); tenancy()->initialize($tenant); @@ -126,6 +127,231 @@ test('only the synced columns are updated in the central db', function () { ], ResourceUser::first()->getAttributes()); }); +// This tests attribute list on the central side, and default values on the tenant side +// Those two don't depend on each other, we're just testing having each option on each side +// using tests that combine the two, to avoid having an excessively long and complex test suite +test('sync resource creation works when central model provides attributes and resource model provides default values', function () { + [$tenant1, $tenant2] = createTenantsAndRunMigrations(); + + addExtraColumnToCentralDB(); + + $centralUser = CentralUserProvidingAttributeNames::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', + 'foo' => 'bar', // foo does not exist in resource model + ]); + + $tenant1->run(function () { + expect(ResourceUserProvidingDefaultValues::all())->toHaveCount(0); + }); + + // When central model provides the list of attributes, resource model will be created from the provided list of attributes' values + $centralUser->tenants()->attach('t1'); + + $tenant1->run(function () { + $resourceUser = ResourceUserProvidingDefaultValues::all(); + expect($resourceUser)->toHaveCount(1); + expect($resourceUser->first()->global_id)->toBe('acme'); + expect($resourceUser->first()->email)->toBe('john@localhost'); + // 'foo' attribute is not provided by central model + expect($resourceUser->first()->foo)->toBeNull(); + }); + + tenancy()->initialize($tenant2); + + // When resource model provides the list of default values, central model will be created from the provided list of default values + ResourceUserProvidingDefaultValues::create([ + 'global_id' => 'asdf', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', + ]); + + tenancy()->end(); + + // Assert central user was created using the list of default values + $centralUser = CentralUserProvidingAttributeNames::whereGlobalId('asdf')->first(); + expect($centralUser)->not()->toBeNull(); + expect($centralUser->name)->toBe('Default Name'); + expect($centralUser->email)->toBe('default@localhost'); + expect($centralUser->password)->toBe('password'); + expect($centralUser->role)->toBe('admin'); + expect($centralUser->foo)->toBe('bar'); +}); + +// This tests default values on the central side, and attribute list on the tenant side +// Those two don't depend on each other, we're just testing having each option on each side +// using tests that combine the two, to avoid having an excessively long and complex test suite +test('sync resource creation works when central model provides default values and resource model provides attributes', function () { + [$tenant1, $tenant2] = createTenantsAndRunMigrations(); + + addExtraColumnToCentralDB(); + + $centralUser = CentralUserProvidingDefaultValues::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', + 'foo' => 'bar', // foo does not exist in resource model + ]); + + $tenant1->run(function () { + expect(ResourceUserProvidingDefaultValues::all())->toHaveCount(0); + }); + + // When central model provides the list of default values, resource model will be created from the provided list of default values + $centralUser->tenants()->attach('t1'); + + $tenant1->run(function () { + // Assert resource user was created using the list of default values + $resourceUser = ResourceUserProvidingDefaultValues::first(); + expect($resourceUser)->not()->toBeNull(); + expect($resourceUser->global_id)->toBe('acme'); + expect($resourceUser->email)->toBe('default@localhost'); + expect($resourceUser->password)->toBe('password'); + expect($resourceUser->role)->toBe('admin'); + }); + + tenancy()->initialize($tenant2); + + // When resource model provides the list of attributes, central model will be created from the provided list of attributes' values + ResourceUserProvidingAttributeNames::create([ + 'global_id' => 'asdf', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', + ]); + + tenancy()->end(); + + // Assert central user was created using the list of provided attributes + $centralUser = CentralUserProvidingAttributeNames::whereGlobalId('asdf')->first(); + expect($centralUser)->not()->toBeNull(); + expect($centralUser->email)->toBe('john@localhost'); + expect($centralUser->password)->toBe('secret'); + expect($centralUser->role)->toBe('commenter'); +}); + +// This tests mixed attribute list/defaults on the central side, and no specified attributes on the tenant side +// Those two don't depend on each other, we're just testing having each option on each side +// using tests that combine the two, to avoid having an excessively long and complex test suite +test('sync resource creation works when central model provides mixture and resource model provides nothing', function () { + [$tenant1, $tenant2] = createTenantsAndRunMigrations(); + + $centralUser = CentralUserProvidingMixture::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commentator' + ]); + + $tenant1->run(function () { + expect(ResourceUser::all())->toHaveCount(0); + }); + + // When central model provides the list of a mixture (attributes and default values), resource model will be created from the provided list of mixture (attributes and default values) + $centralUser->tenants()->attach('t1'); + + $tenant1->run(function () { + $resourceUser = ResourceUser::first(); + + // Assert resource user was created using the provided attributes and default values + expect($resourceUser->global_id)->toBe('acme'); + expect($resourceUser->name)->toBe('John Doe'); + expect($resourceUser->email)->toBe('john@localhost'); + // default values + expect($resourceUser->role)->toBe('admin'); + expect($resourceUser->password)->toBe('secret'); + }); + + tenancy()->initialize($tenant2); + + // When resource model provides nothing/null, the central model will be created as a 1:1 copy of resource model + $resourceUser = ResourceUser::create([ + 'global_id' => 'acmey', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commentator' + ]); + + tenancy()->end(); + + $centralUser = CentralUserProvidingMixture::whereGlobalId('acmey')->first(); + expect($resourceUser->getSyncedCreationAttributes())->toBeNull(); + + $centralUser = $centralUser->toArray(); + $resourceUser = $resourceUser->toArray(); + unset($centralUser['id']); + unset($resourceUser['id']); + + // Assert central user created as 1:1 copy of resource model except "id" + expect($centralUser)->toBe($resourceUser); +}); + +// This tests no specified attributes on the central side, and mixed attribute list/defaults on the tenant side +// Those two don't depend on each other, we're just testing having each option on each side +// using tests that combine the two, to avoid having an excessively long and complex test suite +test('sync resource creation works when central model provides nothing and resource model provides mixture', function () { + [$tenant1, $tenant2] = createTenantsAndRunMigrations(); + + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + $tenant1->run(function () { + expect(ResourceUserProvidingMixture::all())->toHaveCount(0); + }); + + // When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model + $centralUser->tenants()->attach('t1'); + + expect($centralUser->getSyncedCreationAttributes())->toBeNull(); + $tenant1->run(function () use ($centralUser) { + $resourceUser = ResourceUserProvidingMixture::first(); + expect($resourceUser)->not()->toBeNull(); + $resourceUser = $resourceUser->toArray(); + $centralUser = $centralUser->withoutRelations()->toArray(); + unset($resourceUser['id']); + unset($centralUser['id']); + + expect($resourceUser)->toBe($centralUser); + }); + + tenancy()->initialize($tenant2); + + // When resource model provides the list of a mixture (attributes and default values), central model will be created from the provided list of mixture (attributes and default values) + ResourceUserProvidingMixture::create([ + 'global_id' => 'absd', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + tenancy()->end(); + + $centralUser = CentralUser::whereGlobalId('absd')->first(); + + // Assert central user was created using the provided list of attributes and default values + expect($centralUser->name)->toBe('John Doe'); + expect($centralUser->email)->toBe('john@localhost'); + // default values + expect($centralUser->role)->toBe('admin'); + expect($centralUser->password)->toBe('secret'); +}); + test('creating the resource in tenant database creates it in central database and creates the mapping', function () { creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase(); }); @@ -152,7 +378,7 @@ test('attaching a tenant to the central resource triggers a pull from the tenant $tenant = ResourceTenant::create([ 'id' => 't1', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); $tenant->run(function () { expect(ResourceUser::all())->toHaveCount(0); @@ -177,7 +403,7 @@ test('attaching users to tenants does not do anything', function () { $tenant = ResourceTenant::create([ 'id' => 't1', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); $tenant->run(function () { expect(ResourceUser::all())->toHaveCount(0); @@ -212,7 +438,7 @@ test('resources are synced only to workspaces that have the resource', function $t3 = ResourceTenant::create([ 'id' => 't3', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); $centralUser->tenants()->attach('t1'); $centralUser->tenants()->attach('t2'); @@ -250,7 +476,7 @@ test('when a resource exists in other tenant dbs but is created in a tenant db t $t2 = ResourceTenant::create([ 'id' => 't2', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); // Copy (cascade) user to t1 DB $centralUser->tenants()->attach('t1'); @@ -298,7 +524,7 @@ test('the synced columns are updated in other tenant dbs where the resource exis $t3 = ResourceTenant::create([ 'id' => 't3', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); // Copy (cascade) user to t1 DB $centralUser->tenants()->attach('t1'); @@ -353,7 +579,7 @@ test('when the resource doesnt exist in the tenant db non synced columns will ca 'id' => 't1', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); $centralUser->tenants()->attach('t1'); @@ -367,7 +593,7 @@ test('when the resource doesnt exist in the central db non synced columns will b 'id' => 't1', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); $t1->run(function () { ResourceUser::create([ @@ -389,7 +615,7 @@ test('the listener can be queued', function () { 'id' => 't1', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); Queue::assertNothingPushed(); @@ -428,7 +654,7 @@ test('an event is fired for all touched resources', function () { $t3 = ResourceTenant::create([ 'id' => 't3', ]); - migrateTenantsResource(); + migrateUsersTableForTenants(); // Copy (cascade) user to t1 DB $centralUser->tenants()->attach('t1'); @@ -509,7 +735,7 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase() expect(ResourceUser::all())->toHaveCount(0); $tenant = ResourceTenant::create(); - migrateTenantsResource(); + migrateUsersTableForTenants(); tenancy()->initialize($tenant); @@ -524,7 +750,7 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase() tenancy()->end(); - // Asset user was created + // Assert user was created expect(CentralUser::first()->global_id)->toBe('acme'); expect(CentralUser::first()->role)->toBe('commenter'); @@ -537,7 +763,28 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase() expect(ResourceUser::first()->role)->toBe('commenter'); } -function migrateTenantsResource() +/** + * Create two tenants and run migrations for those tenants. + */ +function createTenantsAndRunMigrations(): array +{ + [$tenant1, $tenant2] = [ResourceTenant::create(['id' => 't1']), ResourceTenant::create(['id' => 't2'])]; + + migrateUsersTableForTenants(); + + return [$tenant1, $tenant2]; +} + +function addExtraColumnToCentralDB(): void +{ + // migrate extra column "foo" in central DB + pest()->artisan('migrate', [ + '--path' => __DIR__ . '/Etc/synced_resource_migrations/users_extra', + '--realpath' => true, + ])->assertExitCode(0); +} + +function migrateUsersTableForTenants(): void { pest()->artisan('tenants:migrate', [ '--path' => __DIR__ . '/Etc/synced_resource_migrations/users', @@ -593,6 +840,7 @@ class CentralUser extends Model implements SyncMaster public function getSyncedAttributeNames(): array { return [ + 'global_id', 'name', 'password', 'email', @@ -628,9 +876,106 @@ class ResourceUser extends Model implements Syncable public function getSyncedAttributeNames(): array { return [ + 'global_id', 'name', 'password', 'email', ]; } } + +// override method in ResourceUser class to return default attribute values +class ResourceUserProvidingDefaultValues extends ResourceUser +{ + public function getSyncedCreationAttributes(): array + { + // Default values when creating resources from tenant to central DB + return + [ + 'name' => 'Default Name', + 'email' => 'default@localhost', + 'password' => 'password', + 'role' => 'admin', + 'foo' => 'bar' + ]; + } +} + +// override method in ResourceUser class to return attribute names +class ResourceUserProvidingAttributeNames extends ResourceUser +{ + public function getSyncedCreationAttributes(): array + { + // Attributes used when creating resources from tenant to central DB + // Notice here we are not adding "code" filed because it doesn't + // exist in central model + return + [ + 'name', + 'password', + 'email', + 'role', + 'foo' => 'bar' + ]; + } + +} + +// override method in CentralUser class to return attribute default values +class CentralUserProvidingDefaultValues extends CentralUser +{ + public function getSyncedCreationAttributes(): array + { + // Attributes default values when creating resources from central to tenant model + return + [ + 'name' => 'Default User', + 'email' => 'default@localhost', + 'password' => 'password', + 'role' => 'admin', + ]; + } +} + +// override method in CentralUser class to return attribute names +class CentralUserProvidingAttributeNames extends CentralUser +{ + public function getSyncedCreationAttributes(): array + { + // Attributes used when creating resources from central to tenant DB + return + [ + 'global_id', + 'name', + 'password', + 'email', + 'role', + ]; + } +} + +class CentralUserProvidingMixture extends CentralUser +{ + public function getSyncedCreationAttributes(): array + { + return [ + 'name', + 'email', + 'role' => 'admin', + 'password' => 'secret', + ]; + } +} + +class ResourceUserProvidingMixture extends ResourceUser +{ + public function getSyncedCreationAttributes(): array + { + return [ + 'name', + 'email', + 'role' => 'admin', + 'password' => 'secret', + ]; + } +} From 22d1b2065bdd882899e5121a44a0b96bb614dc4c Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Fri, 4 Nov 2022 19:04:29 +0500 Subject: [PATCH 11/37] [4.x] Add feature to ignore the resource synchronization based on provided condition. (#993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * add test * readability * remove group * DisabledSync -> ConditionalSync; test both cases with dataset Co-authored-by: Samuel Štancl --- src/Contracts/Syncable.php | 2 + src/Database/Concerns/ResourceSyncing.php | 10 ++++- src/Database/Models/TenantPivot.php | 2 +- t | 3 ++ tests/ResourceSyncingTest.php | 53 +++++++++++++++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100755 t diff --git a/src/Contracts/Syncable.php b/src/Contracts/Syncable.php index a481f318..f8e7fd84 100644 --- a/src/Contracts/Syncable.php +++ b/src/Contracts/Syncable.php @@ -18,4 +18,6 @@ interface Syncable /** Get the attributes used for creating the *other* model (i.e. tenant if this is the central one, and central if this is the tenant one). */ public function getSyncedCreationAttributes(): array|null; // todo come up with a better name + + public function shouldSync(): bool; } diff --git a/src/Database/Concerns/ResourceSyncing.php b/src/Database/Concerns/ResourceSyncing.php index fd63738d..ea9f83b4 100644 --- a/src/Database/Concerns/ResourceSyncing.php +++ b/src/Database/Concerns/ResourceSyncing.php @@ -13,8 +13,9 @@ trait ResourceSyncing public static function bootResourceSyncing(): void { static::saved(function (Syncable $model) { - /** @var ResourceSyncing $model */ - $model->triggerSyncEvent(); + if ($model->shouldSync()) { + $model->triggerSyncEvent(); + } }); static::creating(function (self $model) { @@ -37,4 +38,9 @@ trait ResourceSyncing { return null; } + + public function shouldSync(): bool + { + return true; + } } diff --git a/src/Database/Models/TenantPivot.php b/src/Database/Models/TenantPivot.php index 2c7583c1..3cc614a9 100644 --- a/src/Database/Models/TenantPivot.php +++ b/src/Database/Models/TenantPivot.php @@ -14,7 +14,7 @@ class TenantPivot extends Pivot static::saved(function (self $pivot) { $parent = $pivot->pivotParent; - if ($parent instanceof Syncable) { + if ($parent instanceof Syncable && $parent->shouldSync()) { $parent->triggerSyncEvent(); } }); diff --git a/t b/t new file mode 100755 index 00000000..3c74f2e8 --- /dev/null +++ b/t @@ -0,0 +1,3 @@ +#!/bin/bash + +docker-compose exec -T test vendor/bin/pest --no-coverage --filter "$@" diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 430c52ef..e1586bc1 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -763,6 +763,43 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase() expect(ResourceUser::first()->role)->toBe('commenter'); } +test('resources are synced only when sync is enabled', function (bool $enabled) { + app()->instance('_tenancy_test_shouldSync', $enabled); + + [$tenant1, $tenant2] = createTenantsAndRunMigrations(); + migrateUsersTableForTenants(); + + tenancy()->initialize($tenant1); + + TenantUserWithConditionalSync::create([ + 'global_id' => 'absd', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + tenancy()->end(); + + expect(CentralUserWithConditionalSync::all())->toHaveCount($enabled ? 1 : 0); + expect(CentralUserWithConditionalSync::whereGlobalId('absd')->exists())->toBe($enabled); + + $centralUser = CentralUserWithConditionalSync::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'password', + 'role' => 'commenter', + ]); + + $centralUser->tenants()->attach('t2'); + + $tenant2->run(function () use ($enabled) { + expect(TenantUserWithConditionalSync::all())->toHaveCount($enabled ? 1 : 0); + expect(TenantUserWithConditionalSync::whereGlobalId('acme')->exists())->toBe($enabled); + }); +})->with([[true], [false]]); + /** * Create two tenants and run migrations for those tenants. */ @@ -979,3 +1016,19 @@ class ResourceUserProvidingMixture extends ResourceUser ]; } } + +class CentralUserWithConditionalSync extends CentralUser +{ + public function shouldSync(): bool + { + return app('_tenancy_test_shouldSync'); + } +} + +class TenantUserWithConditionalSync extends ResourceUser +{ + public function shouldSync(): bool + { + return app('_tenancy_test_shouldSync'); + } +} From b7a6953231146fa92d970edd9dbb16ec74dd2a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 4 Nov 2022 15:17:54 +0100 Subject: [PATCH 12/37] mention ./t in CONTRIBUTING.md --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d76a686a..03aa4ee8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,8 @@ Run `composer docker-up` to start the containers. Then run `composer test` to ru 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. +If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'`. + When you're done testing, run `composer docker-down` to shut down the containers. ### Docker on M1 From 942d79cbd7da38337f4cd03577a738eb29cd2d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 8 Nov 2022 13:34:04 +0100 Subject: [PATCH 13/37] resolve all phpstan issues --- phpstan.neon | 12 ++++-- src/Commands/ClearPendingTenants.php | 20 +++------ src/Commands/CreatePendingTenants.php | 23 ++-------- src/Commands/Install.php | 1 + src/Commands/MigrateFresh.php | 4 +- src/Concerns/DealsWithMigrations.php | 3 ++ src/Database/Concerns/HasPending.php | 43 +++++++++---------- src/Jobs/ClearPendingTenants.php | 7 +-- src/Jobs/CreatePendingTenants.php | 7 +-- src/Middleware/InitializeTenancyByPath.php | 2 - .../InitializeTenancyByRequestData.php | 19 ++++---- src/Tenancy.php | 4 +- 12 files changed, 57 insertions(+), 88 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 0567d5ff..a6bce96d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,14 +16,17 @@ parameters: ignoreErrors: - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Foundation\\Application#' - '#Cannot access offset (.*?) on Illuminate\\Contracts\\Config\\Repository#' + - + message: '#Call to an undefined (method|static method) Illuminate\\Database\\Eloquent\\(Model|Builder)#' + paths: + - src/Commands/CreatePendingTenants.php + - src/Commands/ClearPendingTenants.php + - src/Database/Concerns/PendingScope.php + - src/Database/ParentModelScope.php - message: '#invalid type Laravel\\Telescope\\IncomingEntry#' paths: - src/Features/TelescopeTags.php - - - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getRelationshipToPrimaryModel\(\)#' - paths: - - src/Database/ParentModelScope.php - message: '#Parameter \#1 \$key of method Illuminate\\Contracts\\Cache\\Repository::put\(\) expects string#' paths: @@ -44,6 +47,7 @@ parameters: message: '#Trying to invoke Closure\|null but it might not be a callable#' paths: - src/Database/DatabaseConfig.php + - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false diff --git a/src/Commands/ClearPendingTenants.php b/src/Commands/ClearPendingTenants.php index 18d9fa42..19d31195 100644 --- a/src/Commands/ClearPendingTenants.php +++ b/src/Commands/ClearPendingTenants.php @@ -9,27 +9,14 @@ use Illuminate\Database\Eloquent\Builder; class ClearPendingTenants extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ protected $signature = 'tenants:pending-clear {--all : Override the default settings and deletes all pending tenants} {--older-than-days= : Deletes all pending tenants older than the amount of days} {--older-than-hours= : Deletes all pending tenants older than the amount of hours}'; - /** - * The console command description. - * - * @var string - */ protected $description = 'Remove pending tenants.'; - /** - * Execute the console command. - */ - public function handle() + public function handle(): int { $this->info('Removing pending tenants.'); @@ -39,7 +26,10 @@ class ClearPendingTenants extends Command // Skip the time constraints if the 'all' option is given if (! $this->option('all')) { + /** @var ?int $olderThanDays */ $olderThanDays = $this->option('older-than-days'); + + /** @var ?int $olderThanHours */ $olderThanHours = $this->option('older-than-hours'); if ($olderThanDays && $olderThanHours) { @@ -70,5 +60,7 @@ class ClearPendingTenants extends Command ->count(); $this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.'); + + return 0; } } diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php index 88202093..7b2c7934 100644 --- a/src/Commands/CreatePendingTenants.php +++ b/src/Commands/CreatePendingTenants.php @@ -8,24 +8,11 @@ use Illuminate\Console\Command; class CreatePendingTenants extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ protected $signature = 'tenants:pending-create {--count= : The number of pending tenants to be created}'; - /** - * The console command description. - * - * @var string - */ protected $description = 'Create pending tenants.'; - /** - * Execute the console command. - */ - public function handle() + public function handle(): int { $this->info('Creating pending tenants.'); @@ -46,13 +33,11 @@ class CreatePendingTenants extends Command $this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.'); $this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); - return 1; + return 0; } - /** - * Calculate the number of currently available pending tenants. - */ - private function getPendingTenantCount(): int + /** Calculate the number of currently available pending tenants. */ + protected function getPendingTenantCount(): int { return tenancy() ->query() diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 77c96588..c7041a72 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -117,6 +117,7 @@ class Install extends Command $this->newLine(); } } else { + /** @var string $warning */ $this->components->warn($warning); } } diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 45a93115..7df75fb0 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; -use Illuminate\Console\Command; +use Illuminate\Database\Console\Migrations\BaseCommand; use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\HasTenantOptions; use Symfony\Component\Console\Input\InputOption; -class MigrateFresh extends Command +class MigrateFresh extends BaseCommand { use HasTenantOptions, DealsWithMigrations; diff --git a/src/Concerns/DealsWithMigrations.php b/src/Concerns/DealsWithMigrations.php index 3129c68d..3a757271 100644 --- a/src/Concerns/DealsWithMigrations.php +++ b/src/Concerns/DealsWithMigrations.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace Stancl\Tenancy\Concerns; +/** + * @mixin \Illuminate\Database\Console\Migrations\BaseCommand + */ trait DealsWithMigrations { protected function getMigrationPaths(): array diff --git a/src/Database/Concerns/HasPending.php b/src/Database/Concerns/HasPending.php index 3fa9399d..4d72486f 100644 --- a/src/Database/Concerns/HasPending.php +++ b/src/Database/Concerns/HasPending.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Events\CreatingPendingTenant; use Stancl\Tenancy\Events\PendingTenantCreated; @@ -14,7 +15,7 @@ use Stancl\Tenancy\Events\PullingPendingTenant; // todo consider adding a method that sets pending_since to null — to flag tenants as not-pending /** - * @property Carbon $pending_since + * @property ?Carbon $pending_since * * @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder withPending(bool $withPending = true) * @method static static|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder onlyPending() @@ -22,38 +23,30 @@ use Stancl\Tenancy\Events\PullingPendingTenant; */ trait HasPending { - /** - * Boot the has pending trait for a model. - * - * @return void - */ - public static function bootHasPending() + /** Boot the trait. */ + public static function bootHasPending(): void { static::addGlobalScope(new PendingScope()); } - /** - * Initialize the has pending trait for an instance. - * - * @return void - */ - public function initializeHasPending() + /** Initialize the trait. */ + public function initializeHasPending(): void { $this->casts['pending_since'] = 'timestamp'; } - /** - * Determine if the model instance is in a pending state. - * - * @return bool - */ - public function pending() + /** Determine if the model instance is in a pending state. */ + public function pending(): bool { return ! is_null($this->pending_since); } - /** Create a pending tenant. */ - public static function createPending($attributes = []): Tenant + /** + * Create a pending tenant. + * + * @param array $attributes + */ + public static function createPending(array $attributes = []): Model&Tenant { $tenant = static::create($attributes); @@ -71,9 +64,12 @@ trait HasPending } /** Pull a pending tenant. */ - public static function pullPending(): Tenant + public static function pullPending(): Model&Tenant { - return static::pullPendingFromPool(true); + /** @var Model&Tenant $pendingTenant */ + $pendingTenant = static::pullPendingFromPool(true); + + return $pendingTenant; } /** Try to pull a tenant from the pool of pending tenants. */ @@ -88,6 +84,7 @@ trait HasPending } // A pending tenant is surely available at this point + /** @var Model&Tenant $tenant */ $tenant = static::onlyPending()->first(); event(new PullingPendingTenant($tenant)); diff --git a/src/Jobs/ClearPendingTenants.php b/src/Jobs/ClearPendingTenants.php index 7cd78495..773e3e93 100644 --- a/src/Jobs/ClearPendingTenants.php +++ b/src/Jobs/ClearPendingTenants.php @@ -16,12 +16,7 @@ class ClearPendingTenants implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Execute the job. - * - * @return void - */ - public function handle() + public function handle(): void { Artisan::call(ClearPendingTenantsCommand::class); } diff --git a/src/Jobs/CreatePendingTenants.php b/src/Jobs/CreatePendingTenants.php index 8f3da218..81199761 100644 --- a/src/Jobs/CreatePendingTenants.php +++ b/src/Jobs/CreatePendingTenants.php @@ -16,12 +16,7 @@ class CreatePendingTenants implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Execute the job. - * - * @return void - */ - public function handle() + public function handle(): void { Artisan::call(CreatePendingTenantsCommand::class); } diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index 3e484f87..e73605e3 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -45,8 +45,6 @@ class InitializeTenancyByPath extends IdentificationMiddleware } else { throw new RouteIsMissingTenantParameterException; } - - return $next($request); } protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php index ca29f3d7..925907f0 100644 --- a/src/Middleware/InitializeTenancyByRequestData.php +++ b/src/Middleware/InitializeTenancyByRequestData.php @@ -34,18 +34,17 @@ class InitializeTenancyByRequestData extends IdentificationMiddleware protected function getPayload(Request $request): ?string { + $payload = null; + if (static::$header && $request->hasHeader(static::$header)) { - return $request->header(static::$header); + $payload = $request->header(static::$header); + } elseif (static::$queryParameter && $request->has(static::$queryParameter)) { + $payload = $request->get(static::$queryParameter); + } elseif (static::$cookie && $request->hasCookie(static::$cookie)) { + $payload = $request->cookie(static::$cookie); } - if (static::$queryParameter && $request->has(static::$queryParameter)) { - return $request->get(static::$queryParameter); - } - - if (static::$cookie && $request->hasCookie(static::$cookie)) { - return $request->cookie(static::$cookie); - } - - return null; + /** @var ?string $payload */ + return $payload; } } diff --git a/src/Tenancy.php b/src/Tenancy.php index e95e0059..5b30e3e0 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -42,8 +42,7 @@ class Tenancy } } - // todo1 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()) { + if ($this->initialized && $this->tenant?->getTenantKey() === $tenant->getTenantKey()) { return; } @@ -52,6 +51,7 @@ class Tenancy $this->end(); } + /** @var Tenant&Model $tenant */ $this->tenant = $tenant; event(new Events\InitializingTenancy($this)); From 99dd862b20bf643b70fb97388f7d9f309d3832a6 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Tue, 8 Nov 2022 17:47:24 +0500 Subject: [PATCH 14/37] [4.x] [WIP] Add phpstan to CI (#928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add phpstan * resolve phpstan issue from CI Co-authored-by: Samuel Štancl --- .github/workflows/ci.yml | 9 +++++++++ composer.json | 3 ++- src/Resolvers/PathTenantResolver.php | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26de6a18..724aed35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,3 +103,12 @@ jobs: author_email: "phpcsfixer@example.com" message: Fix code style (php-cs-fixer) + phpstan: + name: Static analysis (PHPStan) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install composer dependencies + run: composer install + - name: Run phpstan + run: vendor/bin/phpstan analyse diff --git a/composer.json b/composer.json index 587bbb06..68f16f25 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,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", - "phpstan": "vendor/bin/phpstan --pro", + "phpstan": "vendor/bin/phpstan", + "phpstan-pro": "vendor/bin/phpstan --pro", "cs": "php-cs-fixer fix --config=.php-cs-fixer.php", "test": "PHP_VERSION=8.1 ./test --no-coverage", "test-full": "PHP_VERSION=8.1 ./test" diff --git a/src/Resolvers/PathTenantResolver.php b/src/Resolvers/PathTenantResolver.php index 1359e9c1..090ea365 100644 --- a/src/Resolvers/PathTenantResolver.php +++ b/src/Resolvers/PathTenantResolver.php @@ -15,7 +15,10 @@ class PathTenantResolver extends Contracts\CachedTenantResolver /** @var Route $route */ $route = $args[0]; - if ($id = (string) $route->parameter(static::tenantParameterName())) { + /** @var string $id */ + $id = $route->parameter(static::tenantParameterName()); + + if ($id) { $route->forgetParameter(static::tenantParameterName()); if ($tenant = tenancy()->find($id)) { From ea3e44576fc2ca8939708da73d58302203036a80 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Wed, 9 Nov 2022 17:00:54 +0500 Subject: [PATCH 15/37] [4.x] Resource syncing improvements (#992) * Update TenantSyncingTest.php * Update ResourceSyncingTest.php * rename UserTenant to ResourceTenant * Revert "rename UserTenant to ResourceTenant" This reverts commit f9ba778e1b6da7f19a191e68b07248b4dee1ddc3. * rename TenantUser class * return style * Update ResourceSyncingTest.php * revert return style * Update ResourceSyncingTest.php --- tests/ResourceSyncingTest.php | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index e1586bc1..811b8d1a 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -130,7 +130,7 @@ test('only the synced columns are updated in the central db', function () { // This tests attribute list on the central side, and default values on the tenant side // Those two don't depend on each other, we're just testing having each option on each side // using tests that combine the two, to avoid having an excessively long and complex test suite -test('sync resource creation works when central model provides attributes and resource model provides default values', function () { +test('sync resource creation works when central model provides attributes and tenant model provides default values', function () { [$tenant1, $tenant2] = createTenantsAndRunMigrations(); addExtraColumnToCentralDB(); @@ -145,14 +145,14 @@ test('sync resource creation works when central model provides attributes and re ]); $tenant1->run(function () { - expect(ResourceUserProvidingDefaultValues::all())->toHaveCount(0); + expect(TenantUserProvidingDefaultValues::all())->toHaveCount(0); }); // When central model provides the list of attributes, resource model will be created from the provided list of attributes' values $centralUser->tenants()->attach('t1'); $tenant1->run(function () { - $resourceUser = ResourceUserProvidingDefaultValues::all(); + $resourceUser = TenantUserProvidingDefaultValues::all(); expect($resourceUser)->toHaveCount(1); expect($resourceUser->first()->global_id)->toBe('acme'); expect($resourceUser->first()->email)->toBe('john@localhost'); @@ -163,7 +163,7 @@ test('sync resource creation works when central model provides attributes and re tenancy()->initialize($tenant2); // When resource model provides the list of default values, central model will be created from the provided list of default values - ResourceUserProvidingDefaultValues::create([ + TenantUserProvidingDefaultValues::create([ 'global_id' => 'asdf', 'name' => 'John Doe', 'email' => 'john@localhost', @@ -186,7 +186,7 @@ test('sync resource creation works when central model provides attributes and re // This tests default values on the central side, and attribute list on the tenant side // Those two don't depend on each other, we're just testing having each option on each side // using tests that combine the two, to avoid having an excessively long and complex test suite -test('sync resource creation works when central model provides default values and resource model provides attributes', function () { +test('sync resource creation works when central model provides default values and tenant model provides attributes', function () { [$tenant1, $tenant2] = createTenantsAndRunMigrations(); addExtraColumnToCentralDB(); @@ -201,7 +201,7 @@ test('sync resource creation works when central model provides default values an ]); $tenant1->run(function () { - expect(ResourceUserProvidingDefaultValues::all())->toHaveCount(0); + expect(TenantUserProvidingDefaultValues::all())->toHaveCount(0); }); // When central model provides the list of default values, resource model will be created from the provided list of default values @@ -209,7 +209,7 @@ test('sync resource creation works when central model provides default values an $tenant1->run(function () { // Assert resource user was created using the list of default values - $resourceUser = ResourceUserProvidingDefaultValues::first(); + $resourceUser = TenantUserProvidingDefaultValues::first(); expect($resourceUser)->not()->toBeNull(); expect($resourceUser->global_id)->toBe('acme'); expect($resourceUser->email)->toBe('default@localhost'); @@ -220,7 +220,7 @@ test('sync resource creation works when central model provides default values an tenancy()->initialize($tenant2); // When resource model provides the list of attributes, central model will be created from the provided list of attributes' values - ResourceUserProvidingAttributeNames::create([ + TenantUserProvidingAttributeNames::create([ 'global_id' => 'asdf', 'name' => 'John Doe', 'email' => 'john@localhost', @@ -241,7 +241,7 @@ test('sync resource creation works when central model provides default values an // This tests mixed attribute list/defaults on the central side, and no specified attributes on the tenant side // Those two don't depend on each other, we're just testing having each option on each side // using tests that combine the two, to avoid having an excessively long and complex test suite -test('sync resource creation works when central model provides mixture and resource model provides nothing', function () { +test('sync resource creation works when central model provides mixture and tenant model provides nothing', function () { [$tenant1, $tenant2] = createTenantsAndRunMigrations(); $centralUser = CentralUserProvidingMixture::create([ @@ -299,7 +299,7 @@ test('sync resource creation works when central model provides mixture and resou // This tests no specified attributes on the central side, and mixed attribute list/defaults on the tenant side // Those two don't depend on each other, we're just testing having each option on each side // using tests that combine the two, to avoid having an excessively long and complex test suite -test('sync resource creation works when central model provides nothing and resource model provides mixture', function () { +test('sync resource creation works when central model provides nothing and tenant model provides mixture', function () { [$tenant1, $tenant2] = createTenantsAndRunMigrations(); $centralUser = CentralUser::create([ @@ -311,7 +311,7 @@ test('sync resource creation works when central model provides nothing and resou ]); $tenant1->run(function () { - expect(ResourceUserProvidingMixture::all())->toHaveCount(0); + expect(TenantUserProvidingMixture::all())->toHaveCount(0); }); // When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model @@ -319,7 +319,7 @@ test('sync resource creation works when central model provides nothing and resou expect($centralUser->getSyncedCreationAttributes())->toBeNull(); $tenant1->run(function () use ($centralUser) { - $resourceUser = ResourceUserProvidingMixture::first(); + $resourceUser = TenantUserProvidingMixture::first(); expect($resourceUser)->not()->toBeNull(); $resourceUser = $resourceUser->toArray(); $centralUser = $centralUser->withoutRelations()->toArray(); @@ -332,7 +332,7 @@ test('sync resource creation works when central model provides nothing and resou tenancy()->initialize($tenant2); // When resource model provides the list of a mixture (attributes and default values), central model will be created from the provided list of mixture (attributes and default values) - ResourceUserProvidingMixture::create([ + TenantUserProvidingMixture::create([ 'global_id' => 'absd', 'name' => 'John Doe', 'email' => 'john@localhost', @@ -829,6 +829,7 @@ function migrateUsersTableForTenants(): void ])->assertExitCode(0); } +// Tenant model used for resource syncing setup class ResourceTenant extends Tenant { public function users() @@ -885,6 +886,7 @@ class CentralUser extends Model implements SyncMaster } } +// Tenant users class ResourceUser extends Model implements Syncable { use ResourceSyncing; @@ -922,7 +924,7 @@ class ResourceUser extends Model implements Syncable } // override method in ResourceUser class to return default attribute values -class ResourceUserProvidingDefaultValues extends ResourceUser +class TenantUserProvidingDefaultValues extends ResourceUser { public function getSyncedCreationAttributes(): array { @@ -939,7 +941,7 @@ class ResourceUserProvidingDefaultValues extends ResourceUser } // override method in ResourceUser class to return attribute names -class ResourceUserProvidingAttributeNames extends ResourceUser +class TenantUserProvidingAttributeNames extends ResourceUser { public function getSyncedCreationAttributes(): array { @@ -1004,7 +1006,7 @@ class CentralUserProvidingMixture extends CentralUser } } -class ResourceUserProvidingMixture extends ResourceUser +class TenantUserProvidingMixture extends ResourceUser { public function getSyncedCreationAttributes(): array { From dd0f03f74274a96b0723fd5c1c65a78e66a8cf6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 10 Nov 2022 16:03:13 +0100 Subject: [PATCH 16/37] Fix #998, centralize config used by BelongsToTenant and HasDomains --- assets/config.php | 33 ++++++++++++------- src/Database/Concerns/BelongsToTenant.php | 4 +-- src/Database/Concerns/HasDomains.php | 6 ++-- src/Database/Models/Domain.php | 2 +- src/Resolvers/DomainTenantResolver.php | 2 +- src/Tenancy.php | 2 +- src/TenancyServiceProvider.php | 6 ++-- ...edDomainAndSubdomainIdentificationTest.php | 2 +- tests/DeleteDomainsJobTest.php | 4 +-- tests/DomainTest.php | 2 +- tests/SingleDatabaseTenancyTest.php | 4 +-- tests/SubdomainTest.php | 2 +- tests/TenantModelTest.php | 2 +- tests/TestCase.php | 2 +- 14 files changed, 41 insertions(+), 32 deletions(-) diff --git a/assets/config.php b/assets/config.php index cfde54ac..ce276f3d 100644 --- a/assets/config.php +++ b/assets/config.php @@ -6,10 +6,29 @@ use Stancl\Tenancy\Middleware; use Stancl\Tenancy\Resolvers; return [ - 'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class, - 'domain_model' => Stancl\Tenancy\Database\Models\Domain::class, + /** + * Configuration for the models used by Tenancy. + */ + 'models' => [ + 'tenant' => Stancl\Tenancy\Database\Models\Tenant::class, + 'domain' => Stancl\Tenancy\Database\Models\Domain::class, - 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, + /** + * Name of the column used to for ->tenant() relationships. + * + * This is used by the HasDomains trait, and models that use the BelongsToTenant trait (used in single-database tenancy). + */ + 'tenant_key_column' => 'tenant_id', + + /** + * Used for generating tenant IDs. + * + * - Feel free to override this with a custom class that implements the UniqueIdentifierGenerator interface. + * - To use autoincrement IDs, set this to null and update the `tenants` table migration to use an autoincrement column. + * SECURITY NOTE: Keep in mind that autoincrement IDs come with *potential* enumeration issues (such as tenant storage URLs). + */ + 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, + ], /** * The list of domains hosting your central app. @@ -293,12 +312,4 @@ return [ '--class' => 'Database\Seeders\DatabaseSeeder', // root seeder class // '--force' => true, ], - - /** - * Single-database tenancy config. - */ - 'single_db' => [ - /** The name of the column used by models with the BelongsToTenant trait. */ - 'tenant_id_column' => 'tenant_id', - ], ]; diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index 07048a1f..1be3c4cf 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -14,12 +14,12 @@ trait BelongsToTenant { public function tenant() { - return $this->belongsTo(config('tenancy.tenant_model'), static::tenantIdColumn()); + return $this->belongsTo(config('tenancy.models.tenant'), static::tenantIdColumn()); } public static function tenantIdColumn(): string { - return config('tenancy.single_db.tenant_id_column'); + return config('tenancy.models.tenant_key_column'); } public static function bootBelongsToTenant(): void diff --git a/src/Database/Concerns/HasDomains.php b/src/Database/Concerns/HasDomains.php index bd512e23..aa1e49d0 100644 --- a/src/Database/Concerns/HasDomains.php +++ b/src/Database/Concerns/HasDomains.php @@ -2,8 +2,6 @@ declare(strict_types=1); -// todo not sure if this should be in Database\ - namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Domain; @@ -17,12 +15,12 @@ trait HasDomains { public function domains() { - return $this->hasMany(config('tenancy.domain_model'), 'tenant_id'); + return $this->hasMany(config('tenancy.models.domain'), 'tenant_id'); } public function createDomain($data): Domain { - $class = config('tenancy.domain_model'); + $class = config('tenancy.models.domain'); if (! is_array($data)) { $data = ['domain' => $data]; diff --git a/src/Database/Models/Domain.php b/src/Database/Models/Domain.php index 16695711..e5c49bcf 100644 --- a/src/Database/Models/Domain.php +++ b/src/Database/Models/Domain.php @@ -28,7 +28,7 @@ class Domain extends Model implements Contracts\Domain public function tenant(): BelongsTo { - return $this->belongsTo(config('tenancy.tenant_model')); + return $this->belongsTo(config('tenancy.models.tenant')); } protected $dispatchesEvents = [ diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index cf88f579..2163febe 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -18,7 +18,7 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver { $domain = $args[0]; - $tenant = config('tenancy.tenant_model')::query() + $tenant = config('tenancy.models.tenant')::query() ->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain)) ->with('domains') ->first(); diff --git a/src/Tenancy.php b/src/Tenancy.php index 5b30e3e0..8788ec4c 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -97,7 +97,7 @@ class Tenancy public static function model(): Tenant&Model { - $class = config('tenancy.tenant_model'); + $class = config('tenancy.models.tenant'); /** @var Tenant&Model $model */ $model = new $class; diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 01770cda..ded96f35 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -54,9 +54,9 @@ class TenancyServiceProvider extends ServiceProvider $this->app->singleton($bootstrapper); } - // Bind the class in the tenancy.id_generator config to the UniqueIdentifierGenerator abstract. - if (! is_null($this->app['config']['tenancy.id_generator'])) { - $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.id_generator']); + // Bind the class in the tenancy.models.id_generator config to the UniqueIdentifierGenerator abstract. + if (! is_null($this->app['config']['tenancy.models.id_generator'])) { + $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.models.id_generator']); } $this->app->singleton(Commands\Migrate::class, function ($app) { diff --git a/tests/CombinedDomainAndSubdomainIdentificationTest.php b/tests/CombinedDomainAndSubdomainIdentificationTest.php index 4e3c190b..8d613875 100644 --- a/tests/CombinedDomainAndSubdomainIdentificationTest.php +++ b/tests/CombinedDomainAndSubdomainIdentificationTest.php @@ -16,7 +16,7 @@ beforeEach(function () { }); }); - config(['tenancy.tenant_model' => CombinedTenant::class]); + config(['tenancy.models.tenant' => CombinedTenant::class]); }); test('tenant can be identified by subdomain', function () { diff --git a/tests/DeleteDomainsJobTest.php b/tests/DeleteDomainsJobTest.php index bdee14dd..bd825b71 100644 --- a/tests/DeleteDomainsJobTest.php +++ b/tests/DeleteDomainsJobTest.php @@ -6,7 +6,7 @@ use Stancl\Tenancy\Database\Concerns\HasDomains; use Stancl\Tenancy\Jobs\DeleteDomains; beforeEach(function () { - config(['tenancy.tenant_model' => DatabaseAndDomainTenant::class]); + config(['tenancy.models.tenant' => DatabaseAndDomainTenant::class]); }); test('job delete domains successfully', function (){ @@ -29,4 +29,4 @@ test('job delete domains successfully', function (){ class DatabaseAndDomainTenant extends \Stancl\Tenancy\Tests\Etc\Tenant { use HasDomains; -} \ No newline at end of file +} diff --git a/tests/DomainTest.php b/tests/DomainTest.php index 6995da24..2fc04b76 100644 --- a/tests/DomainTest.php +++ b/tests/DomainTest.php @@ -21,7 +21,7 @@ beforeEach(function () { }); }); - config(['tenancy.tenant_model' => DomainTenant::class]); + config(['tenancy.models.tenant' => DomainTenant::class]); }); test('tenant can be identified using hostname', function () { diff --git a/tests/SingleDatabaseTenancyTest.php b/tests/SingleDatabaseTenancyTest.php index ec0a0edf..d9f10fc0 100644 --- a/tests/SingleDatabaseTenancyTest.php +++ b/tests/SingleDatabaseTenancyTest.php @@ -31,7 +31,7 @@ beforeEach(function () { $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade'); }); - config(['tenancy.tenant_model' => Tenant::class]); + config(['tenancy.models.tenant' => Tenant::class]); }); test('primary models are scoped to the current tenant', function () { @@ -142,7 +142,7 @@ test('tenant id is not auto added when creating primary resources in central con }); test('tenant id column name can be customized', function () { - config(['tenancy.single_db.tenant_id_column' => 'team_id']); + config(['tenancy.models.tenant_key_column' => 'team_id']); Schema::drop('comments'); Schema::drop('posts'); diff --git a/tests/SubdomainTest.php b/tests/SubdomainTest.php index 0ff52bc0..365ecc47 100644 --- a/tests/SubdomainTest.php +++ b/tests/SubdomainTest.php @@ -20,7 +20,7 @@ beforeEach(function () { }); }); - config(['tenancy.tenant_model' => SubdomainTenant::class]); + config(['tenancy.models.tenant' => SubdomainTenant::class]); }); test('tenant can be identified by subdomain', function () { diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index fb62260c..1c5a7700 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -43,7 +43,7 @@ test('current tenant can be resolved from service container using typehint', fun }); test('id is generated when no id is supplied', function () { - config(['tenancy.id_generator' => UUIDGenerator::class]); + config(['tenancy.models.id_generator' => UUIDGenerator::class]); $this->mock(UUIDGenerator::class, function ($mock) { return $mock->shouldReceive('generate')->once(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 1c0ceb83..7b9deea0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -109,7 +109,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'central' => true, ], 'tenancy.seeder_parameters' => [], - 'tenancy.tenant_model' => Tenant::class, // Use test tenant w/ DBs & domains + 'tenancy.models.tenant' => Tenant::class, // Use test tenant w/ DBs & domains ]); $app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration From 2a39b0526a1fb122d6965e7932ec62399b1d5b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 10 Nov 2022 16:44:52 +0100 Subject: [PATCH 17/37] Fix #998, properly replace ALL tenant_id literals --- CONTRIBUTING.md | 8 +++++++- assets/config.php | 2 +- ...create_tenant_user_impersonation_tokens_table.php | 5 +++-- .../2019_09_15_000020_create_domains_table.php | 5 +++-- src/Database/Concerns/BelongsToTenant.php | 12 ++++-------- src/Database/Concerns/HasDomains.php | 3 ++- src/Database/Concerns/HasScopedValidationRules.php | 5 +++-- src/Database/TenantScope.php | 4 ++-- src/Features/UserImpersonation.php | 4 ++-- src/Listeners/UpdateSyncedResource.php | 3 ++- src/Tenancy.php | 6 ++++++ 11 files changed, 35 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03aa4ee8..0095be7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,10 +10,16 @@ Run `composer docker-up` to start the containers. Then run `composer test` to ru 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. -If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'`. +If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'` (`--no-coverage` speeds up the execution time). When you're done testing, run `composer docker-down` to shut down the containers. +### Debugging tests + +If you're developing some feature and you encounter `SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry` errors, it's likely that some PHP errors were thrown in past test runs and prevented the test cleanup from running properly. + +To fix this, simply delete the database memory by shutting down containers and starting them again: `composer docker-down && composer docker-up`. + ### 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. diff --git a/assets/config.php b/assets/config.php index ce276f3d..3778e107 100644 --- a/assets/config.php +++ b/assets/config.php @@ -14,7 +14,7 @@ return [ 'domain' => Stancl\Tenancy\Database\Models\Domain::class, /** - * Name of the column used to for ->tenant() relationships. + * Name of the column used to relate models to tenants. * * This is used by the HasDomains trait, and models that use the BelongsToTenant trait (used in single-database tenancy). */ diff --git a/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php b/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php index 32597f38..7bcc3e75 100644 --- a/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php +++ b/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php @@ -5,6 +5,7 @@ declare(strict_types=1); use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use Stancl\Tenancy\Tenancy; class CreateTenantUserImpersonationTokensTable extends Migration { @@ -17,13 +18,13 @@ class CreateTenantUserImpersonationTokensTable extends Migration { Schema::create('tenant_user_impersonation_tokens', function (Blueprint $table) { $table->string('token', 128)->primary(); - $table->string('tenant_id'); + $table->string(Tenancy::tenantKeyColumn()); $table->string('user_id'); $table->string('auth_guard'); $table->string('redirect_url'); $table->timestamp('created_at'); - $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + $table->foreign(Tenancy::tenantKeyColumn())->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); }); } diff --git a/assets/migrations/2019_09_15_000020_create_domains_table.php b/assets/migrations/2019_09_15_000020_create_domains_table.php index 17f706c2..511e6cc9 100644 --- a/assets/migrations/2019_09_15_000020_create_domains_table.php +++ b/assets/migrations/2019_09_15_000020_create_domains_table.php @@ -5,6 +5,7 @@ declare(strict_types=1); use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use Stancl\Tenancy\Tenancy; class CreateDomainsTable extends Migration { @@ -18,10 +19,10 @@ class CreateDomainsTable extends Migration Schema::create('domains', function (Blueprint $table) { $table->increments('id'); $table->string('domain', 255)->unique(); - $table->string('tenant_id'); + $table->string(Tenancy::tenantKeyColumn()); $table->timestamps(); - $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade'); + $table->foreign(Tenancy::tenantKeyColumn())->references('id')->on('tenants')->onUpdate('cascade'); }); } diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index 1be3c4cf..ccf87c81 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Database\TenantScope; +use Stancl\Tenancy\Tenancy; /** * @property-read Tenant $tenant @@ -14,12 +15,7 @@ trait BelongsToTenant { public function tenant() { - return $this->belongsTo(config('tenancy.models.tenant'), static::tenantIdColumn()); - } - - public static function tenantIdColumn(): string - { - return config('tenancy.models.tenant_key_column'); + return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn()); } public static function bootBelongsToTenant(): void @@ -27,9 +23,9 @@ trait BelongsToTenant static::addGlobalScope(new TenantScope); static::creating(function ($model) { - if (! $model->getAttribute(static::tenantIdColumn()) && ! $model->relationLoaded('tenant')) { + if (! $model->getAttribute(Tenancy::tenantKeyColumn()) && ! $model->relationLoaded('tenant')) { if (tenancy()->initialized) { - $model->setAttribute(static::tenantIdColumn(), tenant()->getTenantKey()); + $model->setAttribute(Tenancy::tenantKeyColumn(), tenant()->getTenantKey()); $model->setRelation('tenant', tenant()); } } diff --git a/src/Database/Concerns/HasDomains.php b/src/Database/Concerns/HasDomains.php index aa1e49d0..ae3aed42 100644 --- a/src/Database/Concerns/HasDomains.php +++ b/src/Database/Concerns/HasDomains.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Domain; +use Stancl\Tenancy\Tenancy; /** * @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains @@ -15,7 +16,7 @@ trait HasDomains { public function domains() { - return $this->hasMany(config('tenancy.models.domain'), 'tenant_id'); + return $this->hasMany(config('tenancy.models.domain'), Tenancy::tenantKeyColumn()); } public function createDomain($data): Domain diff --git a/src/Database/Concerns/HasScopedValidationRules.php b/src/Database/Concerns/HasScopedValidationRules.php index 7913a215..979a3ecc 100644 --- a/src/Database/Concerns/HasScopedValidationRules.php +++ b/src/Database/Concerns/HasScopedValidationRules.php @@ -6,16 +6,17 @@ namespace Stancl\Tenancy\Database\Concerns; use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Unique; +use Stancl\Tenancy\Tenancy; trait HasScopedValidationRules { public function unique($table, $column = 'NULL') { - return (new Unique($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey()); + return (new Unique($table, $column))->where(Tenancy::tenantKeyColumn(), $this->getTenantKey()); } public function exists($table, $column = 'NULL') { - return (new Exists($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey()); + return (new Exists($table, $column))->where(Tenancy::tenantKeyColumn(), $this->getTenantKey()); } } diff --git a/src/Database/TenantScope.php b/src/Database/TenantScope.php index fdab9d70..e3b1db69 100644 --- a/src/Database/TenantScope.php +++ b/src/Database/TenantScope.php @@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Database; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; -use Stancl\Tenancy\Database\Concerns\BelongsToTenant; +use Stancl\Tenancy\Tenancy; class TenantScope implements Scope { @@ -17,7 +17,7 @@ class TenantScope implements Scope return; } - $builder->where($model->qualifyColumn(BelongsToTenant::tenantIdColumn()), tenant()->getTenantKey()); + $builder->where($model->qualifyColumn(Tenancy::tenantKeyColumn()), tenant()->getTenantKey()); } public function extend(Builder $builder): void diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 41bf774b..4c9bb104 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -20,7 +20,7 @@ class UserImpersonation implements Feature { $tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string $authGuard = null): ImpersonationToken { return ImpersonationToken::create([ - 'tenant_id' => $tenant->getTenantKey(), + Tenancy::tenantKeyColumn() => $tenant->getTenantKey(), 'user_id' => $userId, 'redirect_url' => $redirectUrl, 'auth_guard' => $authGuard, @@ -39,7 +39,7 @@ class UserImpersonation implements Feature abort_if($tokenExpired, 403); - $tokenTenantId = (string) $token->tenant_id; + $tokenTenantId = (string) $token->getAttribute(Tenancy::tenantKeyColumn()); $currentTenantId = (string) tenant()->getTenantKey(); abort_unless($tokenTenantId === $currentTenantId, 403); diff --git a/src/Listeners/UpdateSyncedResource.php b/src/Listeners/UpdateSyncedResource.php index 39391eac..38245a80 100644 --- a/src/Listeners/UpdateSyncedResource.php +++ b/src/Listeners/UpdateSyncedResource.php @@ -14,6 +14,7 @@ use Stancl\Tenancy\Database\TenantCollection; use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase; use Stancl\Tenancy\Events\SyncedResourceSaved; use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException; +use Stancl\Tenancy\Tenancy; // todo@v4 review all code related to resource syncing @@ -77,7 +78,7 @@ class UpdateSyncedResource extends QueueableListener /** @var Tenant */ $tenant = $event->tenant; - return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey()); + return ((string) $model->pivot->getAttribute(Tenancy::tenantKeyColumn())) === ((string) $tenant->getTenantKey()); }; $mappingExists = $centralModel->tenants->contains($currentTenantMapping); diff --git a/src/Tenancy.php b/src/Tenancy.php index 8788ec4c..e8187dd8 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -105,6 +105,12 @@ class Tenancy return $model; } + /** Name of the column used to relate models to tenants. */ + public static function tenantKeyColumn(): string + { + return config('tenancy.models.tenant_key_column') ?? 'tenant_id'; + } + /** * Try to find a tenant using an ID. * From fb2369dc1179cc31d0b7a3201805fe9de15a512b Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Fri, 11 Nov 2022 17:02:13 +0500 Subject: [PATCH 18/37] anonymous publishable migrations (#1001) --- ...5_000010_create_tenant_user_impersonation_tokens_table.php | 4 ++-- assets/migrations/2019_09_15_000010_create_tenants_table.php | 4 ++-- assets/migrations/2019_09_15_000020_create_domains_table.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php b/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php index 7bcc3e75..c720160a 100644 --- a/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php +++ b/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php @@ -7,7 +7,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use Stancl\Tenancy\Tenancy; -class CreateTenantUserImpersonationTokensTable extends Migration +return new class extends Migration { /** * Run the migrations. @@ -37,4 +37,4 @@ class CreateTenantUserImpersonationTokensTable extends Migration { Schema::dropIfExists('tenant_user_impersonation_tokens'); } -} +}; diff --git a/assets/migrations/2019_09_15_000010_create_tenants_table.php b/assets/migrations/2019_09_15_000010_create_tenants_table.php index ec730651..a923f2c8 100644 --- a/assets/migrations/2019_09_15_000010_create_tenants_table.php +++ b/assets/migrations/2019_09_15_000010_create_tenants_table.php @@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateTenantsTable extends Migration +return new class extends Migration { /** * Run the migrations. @@ -34,4 +34,4 @@ class CreateTenantsTable extends Migration { Schema::dropIfExists('tenants'); } -} +}; diff --git a/assets/migrations/2019_09_15_000020_create_domains_table.php b/assets/migrations/2019_09_15_000020_create_domains_table.php index 511e6cc9..ac238830 100644 --- a/assets/migrations/2019_09_15_000020_create_domains_table.php +++ b/assets/migrations/2019_09_15_000020_create_domains_table.php @@ -7,7 +7,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use Stancl\Tenancy\Tenancy; -class CreateDomainsTable extends Migration +return new class extends Migration { /** * Run the migrations. @@ -35,4 +35,4 @@ class CreateDomainsTable extends Migration { Schema::dropIfExists('domains'); } -} +}; From 1db2f0913f00897ee48a07c58962f0060f36d70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 17 Nov 2022 03:39:57 +0100 Subject: [PATCH 19/37] add todo --- assets/routes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/routes.php b/assets/routes.php index a27f782d..a9c09797 100644 --- a/assets/routes.php +++ b/assets/routes.php @@ -5,6 +5,7 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Controllers\TenantAssetController; +// todo make this work with path identification Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset']) ->where('path', '(.*)') ->name('stancl.tenancy.asset'); From 9520cbc811e3859a6aa30526b0e52e0b5ac80bf7 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Fri, 18 Nov 2022 15:58:04 +0100 Subject: [PATCH 20/37] Only delete tenants in MigrateFreshOverride if the tenants table exists (#1007) * Don't drop tenant databases on `migrate:fresh` if the tenants table doesn't exist * Fix code style (php-cs-fixer) Co-authored-by: PHP CS Fixer --- src/Commands/MigrateFreshOverride.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Commands/MigrateFreshOverride.php b/src/Commands/MigrateFreshOverride.php index 88e9e21e..f2fd70b0 100644 --- a/src/Commands/MigrateFreshOverride.php +++ b/src/Commands/MigrateFreshOverride.php @@ -5,13 +5,18 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Database\Console\Migrations\FreshCommand; +use Illuminate\Support\Facades\Schema; class MigrateFreshOverride extends FreshCommand { public function handle() { if (config('tenancy.database.drop_tenant_databases_on_migrate_fresh')) { - tenancy()->model()::cursor()->each->delete(); + $tenantModel = tenancy()->model(); + + if (Schema::hasTable($tenantModel->getTable())) { + $tenantModel::cursor()->each->delete(); + } } return parent::handle(); From cb7567a88ae3554d7801600e2e9fe16e26c07fd9 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 23 Nov 2022 08:38:20 +0100 Subject: [PATCH 21/37] [4.x] Make `TenantDump` work when called outside the tenant context (#1015) * Run TenantDump as passed tenant * Fix code style (php-cs-fixer) * Fix `tenants:dump` tests * Update dump command test * Remove redundant `tenant-schema.dump` unlinking * Delete duplicate test * Update test name Co-authored-by: PHP CS Fixer --- src/Commands/TenantDump.php | 2 +- tests/CommandsTest.php | 35 +++++++++++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 3f957bdd..b02af71f 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -41,7 +41,7 @@ class TenantDump extends DumpCommand return 1; } - parent::handle($connections, $dispatcher); + $tenant->run(fn () => parent::handle($connections, $dispatcher)); return 0; } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 95672753..355fb429 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -25,7 +25,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; beforeEach(function () { - if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) { + if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) { unlink($schemaPath); } @@ -111,28 +111,30 @@ test('migrate command loads schema state', function () { test('dump command works', function () { $tenant = Tenant::create(); + $schemaPath = 'tests/Etc/tenant-schema-test.dump'; + Artisan::call('tenants:migrate'); - tenancy()->initialize($tenant); + expect($schemaPath)->not()->toBeFile(); - Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"'); - expect('tests/Etc/tenant-schema-test.dump')->toBeFile(); -}); - -test('tenant dump file gets created as tenant-schema.dump in the database schema folder by default', function() { - config(['tenancy.migration_parameters.--schema-path' => $schemaPath = database_path('schema/tenant-schema.dump')]); - - $tenant = Tenant::create(); - Artisan::call('tenants:migrate'); - - tenancy()->initialize($tenant); - - Artisan::call('tenants:dump'); + Artisan::call('tenants:dump ' . "--tenant='$tenant->id' --path='$schemaPath'"); expect($schemaPath)->toBeFile(); }); -test('migrate command uses the correct schema path by default', function () { +test('dump command generates dump at the passed path', function() { + $tenant = Tenant::create(); + + Artisan::call('tenants:migrate'); + + expect($schemaPath = 'tests/Etc/tenant-schema-test.dump')->not()->toBeFile(); + + Artisan::call("tenants:dump --tenant='$tenant->id' --path='$schemaPath'"); + + expect($schemaPath)->toBeFile(); +}); + +test('migrate command correctly uses the schema dump located at the configured schema path by default', function () { config(['tenancy.migration_parameters.--schema-path' => 'tests/Etc/tenant-schema.dump']); $tenant = Tenant::create(); @@ -146,6 +148,7 @@ test('migrate command uses the correct schema path by default', function () { tenancy()->initialize($tenant); + // schema_users is a table included in the tests/Etc/tenant-schema dump // Check for both tables to see if missing migrations also get executed expect(Schema::hasTable('schema_users'))->toBeTrue(); expect(Schema::hasTable('users'))->toBeTrue(); From ea19117870a4f0bc091621d23aae70888cb5a799 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 23 Nov 2022 13:12:29 +0100 Subject: [PATCH 22/37] Use the hardcoded default path in `TenantDump` only if the path isn't configured (#1019) * Use the hardcoded tenant dump path only if the path isn't configured * Test generating tenant dump at the configured path --- src/Commands/TenantDump.php | 2 +- tests/CommandsTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index b02af71f..c7bd9b99 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -23,7 +23,7 @@ class TenantDump extends DumpCommand public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int { if (is_null($this->option('path'))) { - $this->input->setOption('path', database_path('schema/tenant-schema.dump')); + $this->input->setOption('path', config('tenancy.migration_parameters.--schema-path') ?? database_path('schema/tenant-schema.dump')); } $tenant = $this->option('tenant') diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 355fb429..d8484253 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -134,6 +134,20 @@ test('dump command generates dump at the passed path', function() { expect($schemaPath)->toBeFile(); }); +test('dump command generates dump at the path specified in the tenancy migration parameters config', function() { + config(['tenancy.migration_parameters.--schema-path' => $schemaPath = 'tests/Etc/tenant-schema-test.dump']); + + $tenant = Tenant::create(); + + Artisan::call('tenants:migrate'); + + expect($schemaPath)->not()->toBeFile(); + + Artisan::call("tenants:dump --tenant='$tenant->id'"); + + expect($schemaPath)->toBeFile(); +}); + test('migrate command correctly uses the schema dump located at the configured schema path by default', function () { config(['tenancy.migration_parameters.--schema-path' => 'tests/Etc/tenant-schema.dump']); $tenant = Tenant::create(); From 73c5655bc886f46ae4689b29bcd19657ea86a406 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Fri, 25 Nov 2022 07:09:31 +0500 Subject: [PATCH 23/37] Manual mode improvements (use correct event type, add new listeners) (#1013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix issue and add test * Update CreateTenantConnection.php * add purge call * Update ManualModeTest.php * use tenant connection and central connection listener * Update ManualModeTest.php * fix test * improvements * Update ManualModeTest.php * add comment * simplify comment Co-authored-by: Samuel Štancl --- src/Listeners/CreateTenantConnection.php | 9 ++--- src/Listeners/UseCentralConnection.php | 21 +++++++++++ src/Listeners/UseTenantConnection.php | 21 +++++++++++ tests/ManualModeTest.php | 45 ++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 src/Listeners/UseCentralConnection.php create mode 100644 src/Listeners/UseTenantConnection.php create mode 100644 tests/ManualModeTest.php diff --git a/src/Listeners/CreateTenantConnection.php b/src/Listeners/CreateTenantConnection.php index b4983d32..6af18a10 100644 --- a/src/Listeners/CreateTenantConnection.php +++ b/src/Listeners/CreateTenantConnection.php @@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Listeners; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; -use Stancl\Tenancy\Events\Contracts\TenantEvent; +use Stancl\Tenancy\Events\Contracts\TenancyEvent; class CreateTenantConnection { @@ -15,11 +15,12 @@ class CreateTenantConnection ) { } - public function handle(TenantEvent $event): void + public function handle(TenancyEvent $event): void { - /** @var TenantWithDatabase */ - $tenant = $event->tenant; + /** @var TenantWithDatabase $tenant */ + $tenant = $event->tenancy->tenant; + $this->database->purgeTenantConnection(); $this->database->createTenantConnection($tenant); } } diff --git a/src/Listeners/UseCentralConnection.php b/src/Listeners/UseCentralConnection.php new file mode 100644 index 00000000..716a5148 --- /dev/null +++ b/src/Listeners/UseCentralConnection.php @@ -0,0 +1,21 @@ +database->reconnectToCentral(); + } +} diff --git a/src/Listeners/UseTenantConnection.php b/src/Listeners/UseTenantConnection.php new file mode 100644 index 00000000..a4c12108 --- /dev/null +++ b/src/Listeners/UseTenantConnection.php @@ -0,0 +1,21 @@ +database->setDefaultConnection('tenant'); + } +} diff --git a/tests/ManualModeTest.php b/tests/ManualModeTest.php new file mode 100644 index 00000000..fe1ba9a6 --- /dev/null +++ b/tests/ManualModeTest.php @@ -0,0 +1,45 @@ +send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + Event::listen(TenancyInitialized::class, CreateTenantConnection::class); + Event::listen(TenancyInitialized::class, UseTenantConnection::class); + Event::listen(TenancyEnded::class, UseCentralConnection::class); + + $tenant = Tenant::create(); + + expect(app('db')->getDefaultConnection())->toBe('central'); + expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant_host_connection']); + pest()->assertArrayNotHasKey('tenant', config('database.connections')); + + tenancy()->initialize($tenant); + + // Trigger creation of the tenant connection + createUsersTable(); + + expect(app('db')->getDefaultConnection())->toBe('tenant'); + expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant']); + pest()->assertArrayHasKey('tenant', config('database.connections')); + + tenancy()->end(); + + expect(array_keys(app('db')->getConnections()))->toBe(['central']); + expect(config('database.connections.tenant'))->toBeNull(); + expect(app('db')->getDefaultConnection())->toBe(config('tenancy.database.central_connection')); +}); From 7d3298c6bb40d3977770675b008ae3f138364f26 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 29 Nov 2022 09:31:07 +0100 Subject: [PATCH 24/37] Improve code of pending tenants (#1025) * Remove `--all` option from ClearPendingTenants * Improve query formatting * Remove redundant test * Convert time constrait options to int * Improve CreatePendingTenants success message --- src/Commands/ClearPendingTenants.php | 38 +++++++++++---------------- src/Commands/CreatePendingTenants.php | 7 +++-- src/Concerns/HasTenantOptions.php | 3 +-- tests/PendingTenantsTest.php | 17 ------------ 4 files changed, 19 insertions(+), 46 deletions(-) diff --git a/src/Commands/ClearPendingTenants.php b/src/Commands/ClearPendingTenants.php index 19d31195..82b01cd0 100644 --- a/src/Commands/ClearPendingTenants.php +++ b/src/Commands/ClearPendingTenants.php @@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Builder; class ClearPendingTenants extends Command { protected $signature = 'tenants:pending-clear - {--all : Override the default settings and deletes all pending tenants} {--older-than-days= : Deletes all pending tenants older than the amount of days} {--older-than-hours= : Deletes all pending tenants older than the amount of hours}'; @@ -24,32 +23,25 @@ class ClearPendingTenants extends Command // We compare the original expiration date to the new one to check if the new one is different later $originalExpirationDate = $expirationDate->copy()->toImmutable(); - // Skip the time constraints if the 'all' option is given - if (! $this->option('all')) { - /** @var ?int $olderThanDays */ - $olderThanDays = $this->option('older-than-days'); + $olderThanDays = (int) $this->option('older-than-days'); + $olderThanHours = (int) $this->option('older-than-hours'); - /** @var ?int $olderThanHours */ - $olderThanHours = $this->option('older-than-hours'); + if ($olderThanDays && $olderThanHours) { + $this->line(" Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components + $this->line('Please, choose only one of these options.'); - if ($olderThanDays && $olderThanHours) { - $this->line(" Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components - $this->line('Please, choose only one of these options.'); - - return 1; // Exit code for failure - } - - if ($olderThanDays) { - $expirationDate->subDays($olderThanDays); - } - - if ($olderThanHours) { - $expirationDate->subHours($olderThanHours); - } + return 1; // Exit code for failure } - $deletedTenantCount = tenancy() - ->query() + if ($olderThanDays) { + $expirationDate->subDays($olderThanDays); + } + + if ($olderThanHours) { + $expirationDate->subHours($olderThanHours); + } + + $deletedTenantCount = tenancy()->query() ->onlyPending() ->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) { $query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp); diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php index 7b2c7934..5c255664 100644 --- a/src/Commands/CreatePendingTenants.php +++ b/src/Commands/CreatePendingTenants.php @@ -30,8 +30,8 @@ class CreatePendingTenants extends Command $createdCount++; } - $this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.'); - $this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); + $this->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.'); + $this->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); return 0; } @@ -39,8 +39,7 @@ class CreatePendingTenants extends Command /** Calculate the number of currently available pending tenants. */ protected function getPendingTenantCount(): int { - return tenancy() - ->query() + return tenancy()->query() ->onlyPending() ->count(); } diff --git a/src/Concerns/HasTenantOptions.php b/src/Concerns/HasTenantOptions.php index f8a763a7..b558da64 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -23,8 +23,7 @@ trait HasTenantOptions protected function getTenants(): LazyCollection { - return tenancy() - ->query() + return tenancy()->query() ->when($this->option('tenants'), function ($query) { $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); }) diff --git a/tests/PendingTenantsTest.php b/tests/PendingTenantsTest.php index 8dbda9ee..26fd5c34 100644 --- a/tests/PendingTenantsTest.php +++ b/tests/PendingTenantsTest.php @@ -67,23 +67,6 @@ test('CreatePendingTenants command cannot run with both time constraints', funct ->assertFailed(); }); -test('CreatePendingTenants commands all option overrides any config constraints', function () { - Tenant::createPending(); - Tenant::createPending(); - - tenancy()->model()->query()->onlyPending()->first()->update([ - 'pending_since' => now()->subDays(10) - ]); - - config(['tenancy.pending.older_than_days' => 4]); - - Artisan::call(ClearPendingTenants::class, [ - '--all' => true - ]); - - expect(Tenant::onlyPending()->count())->toBe(0); -}); - test('tenancy can check if there are any pending tenants', function () { expect(Tenant::onlyPending()->exists())->toBeFalse(); From 68de3600bd6418eae3ea23d460c527e82d45608a Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Wed, 14 Dec 2022 19:08:00 +0500 Subject: [PATCH 25/37] Improve commands CLI output (#1030) * use component info/error methods * Update src/Commands/ClearPendingTenants.php Co-authored-by: lukinovec Co-authored-by: lukinovec --- src/Commands/ClearPendingTenants.php | 7 +++---- src/Commands/CreatePendingTenants.php | 6 +++--- src/Commands/Link.php | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Commands/ClearPendingTenants.php b/src/Commands/ClearPendingTenants.php index 82b01cd0..0e27a209 100644 --- a/src/Commands/ClearPendingTenants.php +++ b/src/Commands/ClearPendingTenants.php @@ -17,7 +17,7 @@ class ClearPendingTenants extends Command public function handle(): int { - $this->info('Removing pending tenants.'); + $this->components->info('Removing pending tenants.'); $expirationDate = now(); // We compare the original expiration date to the new one to check if the new one is different later @@ -27,8 +27,7 @@ class ClearPendingTenants extends Command $olderThanHours = (int) $this->option('older-than-hours'); if ($olderThanDays && $olderThanHours) { - $this->line(" Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components - $this->line('Please, choose only one of these options.'); + $this->components->error("Cannot use '--older-than-days' and '--older-than-hours' together. Please, choose only one of these options."); return 1; // Exit code for failure } @@ -51,7 +50,7 @@ class ClearPendingTenants extends Command ->delete() ->count(); - $this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.'); + $this->components->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.'); return 0; } diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php index 5c255664..c37b8bd7 100644 --- a/src/Commands/CreatePendingTenants.php +++ b/src/Commands/CreatePendingTenants.php @@ -14,7 +14,7 @@ class CreatePendingTenants extends Command public function handle(): int { - $this->info('Creating pending tenants.'); + $this->components->info('Creating pending tenants.'); $maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count')); $pendingTenantCount = $this->getPendingTenantCount(); @@ -30,8 +30,8 @@ class CreatePendingTenants extends Command $createdCount++; } - $this->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.'); - $this->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); + $this->components->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.'); + $this->components->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); return 0; } diff --git a/src/Commands/Link.php b/src/Commands/Link.php index a6dd6c5f..d49cc7f2 100644 --- a/src/Commands/Link.php +++ b/src/Commands/Link.php @@ -34,7 +34,7 @@ class Link extends Command $this->createLinks($tenants); } } catch (Exception $exception) { - $this->error($exception->getMessage()); + $this->components->error($exception->getMessage()); return 1; } From 21dc69e35830f66571a5f195d283098f3500a882 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Thu, 15 Dec 2022 15:03:03 +0100 Subject: [PATCH 26/37] Use invade in BootstrapperTest (#1033) --- tests/BootstrapperTest.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index ba4ea41a..da51cbde 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -331,15 +331,7 @@ function getDiskPrefix(string $disk): string /** @var FilesystemAdapter $disk */ $disk = Storage::disk($disk); $adapter = $disk->getAdapter(); + $prefix = invade(invade($adapter)->prefixer)->prefix; - $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); - $prefixer->setAccessible(true); - - // reflection -> instance - $prefixer = $prefixer->getValue($adapter); - - $prefix = (new ReflectionProperty($prefixer, 'prefix')); - $prefix->setAccessible(true); - - return $prefix->getValue($prefixer); + return $prefix; } From f42f08cb874001b8626d193022cfb994069d013e Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Sat, 17 Dec 2022 06:08:03 +0500 Subject: [PATCH 27/37] Add session state when impersonating tenant (#1029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Fix code style (php-cs-fixer) * Update TenantUserImpersonationTest.php * renamed method * update method name in test * rename session key * fix test * Update src/Features/UserImpersonation.php Co-authored-by: Samuel Štancl * Update UserImpersonation.php Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- src/Features/UserImpersonation.php | 17 +++++++++++++++++ tests/TenantUserImpersonationTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 4c9bb104..608bed07 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -48,6 +48,23 @@ class UserImpersonation implements Feature $token->delete(); + session()->put('tenancy_impersonating', true); + return redirect($token->redirect_url); } + + public static function isImpersonating(): bool + { + return session()->has('tenancy_impersonating'); + } + + /** + * Logout from the current domain and forget impersonation session. + */ + public static function leave(): void // todo possibly rename + { + auth()->logout(); + + session()->forget('tenancy_impersonating'); + } } diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index 0fcb9022..1e72c604 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -83,6 +83,19 @@ test('tenant user can be impersonated on a tenant domain', function () { pest()->get('http://foo.localhost/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); + + expect(UserImpersonation::isImpersonating())->toBeTrue(); + expect(session('tenancy_impersonating'))->toBeTrue(); + + // Leave impersonation + UserImpersonation::leave(); + + expect(UserImpersonation::isImpersonating())->toBeFalse(); + expect(session('tenancy_impersonating'))->toBeNull(); + + // Assert can't access the tenant dashboard + pest()->get('http://foo.localhost/dashboard') + ->assertRedirect('http://foo.localhost/login'); }); test('tenant user can be impersonated on a tenant path', function () { @@ -116,6 +129,19 @@ test('tenant user can be impersonated on a tenant path', function () { pest()->get('/acme/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); + + expect(UserImpersonation::isImpersonating())->toBeTrue(); + expect(session('tenancy_impersonating'))->toBeTrue(); + + // Leave impersonation + UserImpersonation::leave(); + + expect(UserImpersonation::isImpersonating())->toBeFalse(); + expect(session('tenancy_impersonating'))->toBeNull(); + + // Assert can't access the tenant dashboard + pest()->get('/acme/dashboard') + ->assertRedirect('/login'); }); test('tokens have a limited ttl', function () { From 0f892f1585cd79dc48f9e4e50c14c5131c1f5995 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 4 Jan 2023 02:12:25 +0100 Subject: [PATCH 28/37] Make tenants able to have custom mail credentials (#989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace MailManager singleton with an instance of a custom mail manager which always resolves the mailers instead of getting the cached ones * Fix code style (php-cs-fixer) * Add MailTenancyBootstrapper * Add MailTenancyBootstrapper to tenancy.bootstrappers config (commented out) * Fix code style (php-cs-fixer) * Make credentials map a public static property * Always resolve only the mailers specified in the mailersToNotCache public static property * Fix typo in comment * Update TenancyServiceProvider comment * add todo * Add comments to TenancyMailManager, rename property * Remove the configKey array check * Simplify bootstrap method * Change $credentialsMap so that config keys are the keys, and the tenant property names are the values * Rename $mailersToAlwaysResolve to $tenantMailers * Update comment * Update comment * Rename variable in TenancyServiceProvider comment * Scaffold tests * Update comments after review * Uncomment MailTenancyBootstrapper in config * Use array_key_exists instead of null check * Split config logic into methods * Update mapping credentials * Add tests for the added logic * Fix code style (php-cs-fixer) * Delete default 'smtp' mailer in $tenantMailers * Add separate method to pick the appropriate mail credentials map preset * Specify test name * Move mail bootstrapper tests to BootstrapperTest * Depend less on the default mailer by adding a static `$mailer` property * Use static property for map presets * Comment out MailTenancyBootstrapper from config * Add return types to MailTenancyBootstrapper methods * Update test name * Move MailManager extension to MailTenancyBootstrapper * Fix code style (php-cs-fixer) * Update config reverting test * Use `invade()` instead of ReflectionClass * Fix constructor parameter formatting * Delete TenancyMailManager, update tests * Add return type * Update comment * Update MailTest * Delete `group('mailer')` * Delete bindNewMailManagerInstance() * Delete remaining `group('mailer')` * Fix comment * Fix comment Co-authored-by: PHP CS Fixer Co-authored-by: Samuel Štancl --- assets/config.php | 1 + src/Bootstrappers/MailTenancyBootstrapper.php | 79 +++++++++++++++++++ tests/BootstrapperTest.php | 45 +++++++++++ tests/MailTest.php | 72 +++++++++++++++++ tests/TestCase.php | 3 + 5 files changed, 200 insertions(+) create mode 100644 src/Bootstrappers/MailTenancyBootstrapper.php create mode 100644 tests/MailTest.php diff --git a/assets/config.php b/assets/config.php index 3778e107..fab224db 100644 --- a/assets/config.php +++ b/assets/config.php @@ -102,6 +102,7 @@ return [ Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class, + // Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper::class, // Queueing mail requires using QueueTenancyBootstrapper with $forceRefresh set to true // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed ], diff --git a/src/Bootstrappers/MailTenancyBootstrapper.php b/src/Bootstrappers/MailTenancyBootstrapper.php new file mode 100644 index 00000000..7f15f547 --- /dev/null +++ b/src/Bootstrappers/MailTenancyBootstrapper.php @@ -0,0 +1,79 @@ + 'tenant_property', + * ] + */ + public static array $credentialsMap = []; + + public static string|null $mailer = null; + + protected array $originalConfig = []; + + public static array $mapPresets = [ + 'smtp' => [ + 'mail.mailers.smtp.host' => 'smtp_host', + 'mail.mailers.smtp.port' => 'smtp_port', + 'mail.mailers.smtp.username' => 'smtp_username', + 'mail.mailers.smtp.password' => 'smtp_password', + ], + ]; + + public function __construct( + protected Repository $config, + protected Application $app + ) { + static::$mailer ??= $config->get('mail.default'); + static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$mailer] ?? []); + } + + public function bootstrap(Tenant $tenant): void + { + // Forget the mail manager instance to clear the cached mailers + $this->app->forgetInstance('mail.manager'); + + $this->setConfig($tenant); + } + + public function revert(): void + { + $this->unsetConfig(); + + $this->app->forgetInstance('mail.manager'); + } + + protected function setConfig(Tenant $tenant): void + { + foreach (static::$credentialsMap as $configKey => $storageKey) { + $override = $tenant->$storageKey; + + if (array_key_exists($storageKey, $tenant->getAttributes())) { + $this->originalConfig[$configKey] ??= $this->config->get($configKey); + + $this->config->set($configKey, $override); + } + } + } + + protected function unsetConfig(): void + { + foreach ($this->originalConfig as $key => $value) { + $this->config->set($key, $value); + } + } +} diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index da51cbde..3cc50b58 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Illuminate\Support\Str; +use Illuminate\Mail\MailManager; use Illuminate\Support\Facades\DB; use Stancl\JobPipeline\JobPipeline; use Illuminate\Support\Facades\File; @@ -23,6 +24,7 @@ use Stancl\Tenancy\Jobs\RemoveStorageSymlinks; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\DeleteTenantStorage; use Stancl\Tenancy\Listeners\RevertToCentralContext; +use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; @@ -326,6 +328,49 @@ test('local storage public urls are generated correctly', function() { expect(File::isDirectory($tenantStoragePath))->toBeFalse(); }); +test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() { + MailTenancyBootstrapper::$credentialsMap = [ + 'mail.mailers.smtp.username' => 'smtp_username', + 'mail.mailers.smtp.password' => 'smtp_password' + ]; + + config([ + 'mail.default' => 'smtp', + 'mail.mailers.smtp.username' => $defaultUsername = 'default username', + 'mail.mailers.smtp.password' => 'no password' + ]); + + $tenant = Tenant::create(['smtp_password' => $password = 'testing password']); + + tenancy()->initialize($tenant); + + expect(array_key_exists('smtp_password', tenant()->getAttributes()))->toBeTrue(); + expect(array_key_exists('smtp_host', tenant()->getAttributes()))->toBeFalse(); + expect(config('mail.mailers.smtp.username'))->toBe($defaultUsername); + expect(config('mail.mailers.smtp.password'))->toBe(tenant()->smtp_password); + + // Assert that the current mailer uses tenant's smtp_password + assertMailerTransportUsesPassword($password); +}); + +test('MailTenancyBootstrapper reverts the config and mailer credentials to default when tenancy ends', function() { + MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password']; + config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'no password']); + + tenancy()->initialize(Tenant::create(['smtp_password' => $tenantPassword = 'testing password'])); + + expect(config('mail.mailers.smtp.password'))->toBe($tenantPassword); + + assertMailerTransportUsesPassword($tenantPassword); + + tenancy()->end(); + + expect(config('mail.mailers.smtp.password'))->toBe($defaultPassword); + + // Assert that the current mailer uses the default SMTP password + assertMailerTransportUsesPassword($defaultPassword); +}); + function getDiskPrefix(string $disk): string { /** @var FilesystemAdapter $disk */ diff --git a/tests/MailTest.php b/tests/MailTest.php new file mode 100644 index 00000000..544fda1b --- /dev/null +++ b/tests/MailTest.php @@ -0,0 +1,72 @@ + 'smtp']); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); +}); + +// Initialize tenancy as $tenant and assert that the smtp mailer's transport has the correct password +function assertMailerTransportUsesPassword(string|null $password) { + $manager = app(MailManager::class); + $mailer = invade($manager)->get('smtp'); + $mailerPassword = invade($mailer->getSymfonyTransport())->password; + + expect($mailerPassword)->toBe((string) $password); +}; + +test('mailer transport uses the correct credentials', function() { + config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'DEFAULT']); + MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password']; + + tenancy()->initialize($tenant = Tenant::create()); + assertMailerTransportUsesPassword($defaultPassword); // $tenant->smtp_password is not set, so the default password should be used + tenancy()->end(); + + // Assert mailer uses the updated password + $tenant->update(['smtp_password' => $newPassword = 'changed']); + + tenancy()->initialize($tenant); + assertMailerTransportUsesPassword($newPassword); + tenancy()->end(); + + // Assert mailer uses the correct password after switching to a different tenant + tenancy()->initialize(Tenant::create(['smtp_password' => $newTenantPassword = 'updated'])); + assertMailerTransportUsesPassword($newTenantPassword); + tenancy()->end(); + + // Assert mailer uses the default password after tenancy ends + assertMailerTransportUsesPassword($defaultPassword); +}); + + +test('initializing and ending tenancy binds a fresh MailManager instance without cached mailers', function() { + $mailers = fn() => invade(app(MailManager::class))->mailers; + + app(MailManager::class)->mailer('smtp'); + + expect($mailers())->toHaveCount(1); + + tenancy()->initialize(Tenant::create()); + + expect($mailers())->toHaveCount(0); + + app(MailManager::class)->mailer('smtp'); + + expect($mailers())->toHaveCount(1); + + tenancy()->end(); + + expect($mailers())->toHaveCount(0); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 7b9deea0..07af199f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -14,6 +14,7 @@ use Stancl\Tenancy\Facades\GlobalCache; use Stancl\Tenancy\Facades\Tenancy; use Stancl\Tenancy\TenancyServiceProvider; use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper; abstract class TestCase extends \Orchestra\Testbench\TestCase { @@ -104,6 +105,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase '--force' => true, ], 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that + 'tenancy.bootstrappers.mail' => MailTenancyBootstrapper::class, 'queue.connections.central' => [ 'driver' => 'sync', 'central' => true, @@ -113,6 +115,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase ]); $app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration + $app->singleton(MailTenancyBootstrapper::class); } protected function getPackageProviders($app) From 03ac1ef12768c03d119aa6296413709a80062dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 4 Jan 2023 02:23:48 +0100 Subject: [PATCH 29/37] fix phpstan errors (seems like it started ignoring @property annotations on interfaces and abstract classes) --- phpstan.neon | 4 +++- src/Contracts/Domain.php | 2 +- src/Resolvers/DomainTenantResolver.php | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index a6bce96d..7ae06b44 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -28,7 +28,7 @@ parameters: paths: - src/Features/TelescopeTags.php - - message: '#Parameter \#1 \$key of method Illuminate\\Contracts\\Cache\\Repository::put\(\) expects string#' + message: '#Parameter \#1 \$key of method Illuminate\\Cache\\Repository::put\(\) expects#' paths: - src/helpers.php - @@ -48,6 +48,8 @@ parameters: paths: - src/Database/DatabaseConfig.php - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' + - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#' + - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#' checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false diff --git a/src/Contracts/Domain.php b/src/Contracts/Domain.php index a9a19a50..cfe89f43 100644 --- a/src/Contracts/Domain.php +++ b/src/Contracts/Domain.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * * @see \Stancl\Tenancy\Database\Models\Domain * - * @method __call(string $method, array $parameters) IDE support. This will be a model. + * @method __call(string $method, array $parameters) IDE support. This will be a model. // todo check if we can remove these now * @method static __callStatic(string $method, array $parameters) IDE support. This will be a model. * @mixin \Illuminate\Database\Eloquent\Model */ diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index 2163febe..ba4e66a0 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Resolvers; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; use Stancl\Tenancy\Contracts\Domain; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; @@ -39,14 +40,17 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver protected function setCurrentDomain(Tenant $tenant, string $domain): void { + /** @var Tenant&Model $tenant */ static::$currentDomain = $tenant->domains->where('domain', $domain)->first(); } public function getArgsForTenant(Tenant $tenant): array { + /** @var Tenant&Model $tenant */ + $tenant->unsetRelation('domains'); - return $tenant->domains->map(function (Domain $domain) { + return $tenant->domains->map(function (Domain&Model $domain) { return [$domain->domain]; })->toArray(); } From d4c6c34e7c3e320fafd7eb6630e0b71fac4de83b Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Wed, 4 Jan 2023 01:24:21 +0000 Subject: [PATCH 30/37] Fix code style (php-cs-fixer) --- src/Resolvers/DomainTenantResolver.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index ba4e66a0..ceecd0b6 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -47,7 +47,6 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver public function getArgsForTenant(Tenant $tenant): array { /** @var Tenant&Model $tenant */ - $tenant->unsetRelation('domains'); return $tenant->domains->map(function (Domain&Model $domain) { From d0dd87ab07868f7cccc4a6412eb466319156a7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 4 Jan 2023 02:43:10 +0100 Subject: [PATCH 31/37] bump PHP to 8.2, minor ci fixes --- Dockerfile | 2 +- composer.json | 12 ++++++------ docker-compose.yml | 2 +- tests/CommandsTest.php | 2 +- tests/Etc/Console/ExampleCommand.php | 12 ++++++------ 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0ced8009..5dfe442c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # add amd64 platform to support Mac M1 FROM --platform=linux/amd64 shivammathur/node:latest-amd64 -ARG PHP_VERSION=8.1 +ARG PHP_VERSION=8.2 WORKDIR /var/www/html diff --git a/composer.json b/composer.json index 68f16f25..0cfe3984 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^8.1", + "php": "^8.2", "ext-json": "*", "illuminate/support": "^9.0", "spatie/ignition": "^1.4", @@ -58,16 +58,16 @@ } }, "scripts": { - "docker-up": "PHP_VERSION=8.1 docker-compose up -d", - "docker-down": "PHP_VERSION=8.1 docker-compose down", - "docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build", + "docker-up": "PHP_VERSION=8.2 docker-compose up -d", + "docker-down": "PHP_VERSION=8.2 docker-compose down", + "docker-rebuild": "PHP_VERSION=8.2 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", "phpstan-pro": "vendor/bin/phpstan --pro", "cs": "php-cs-fixer fix --config=.php-cs-fixer.php", - "test": "PHP_VERSION=8.1 ./test --no-coverage", - "test-full": "PHP_VERSION=8.1 ./test" + "test": "PHP_VERSION=8.2 ./test --no-coverage", + "test-full": "PHP_VERSION=8.2 ./test" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/docker-compose.yml b/docker-compose.yml index 116b48f1..465b36cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: build: context: . args: - PHP_VERSION: ${PHP_VERSION:-8.1} + PHP_VERSION: ${PHP_VERSION:-8.2} depends_on: mysql: condition: service_healthy diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index d8484253..7d6f0884 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -372,7 +372,7 @@ function runCommandWorks(): void Artisan::call('tenants:migrate', ['--tenants' => [$id]]); pest()->artisan("tenants:run --tenants=$id 'foo foo --b=bar --c=xyz' ") - ->expectsOutput("User's name is Test command") + ->expectsOutput("User's name is Test user") ->expectsOutput('foo') ->expectsOutput('xyz'); } diff --git a/tests/Etc/Console/ExampleCommand.php b/tests/Etc/Console/ExampleCommand.php index 72263b37..cdd7b551 100644 --- a/tests/Etc/Console/ExampleCommand.php +++ b/tests/Etc/Console/ExampleCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests\Etc\Console; +use Illuminate\Support\Str; use Illuminate\Console\Command; class ExampleCommand extends Command @@ -22,14 +23,13 @@ class ExampleCommand extends Command */ public function handle() { - User::create([ - 'id' => 999, - 'name' => 'Test command', - 'email' => 'test@command.com', + $id = User::create([ + 'name' => 'Test user', + 'email' => Str::random(8) . '@example.com', 'password' => bcrypt('password'), - ]); + ])->id; - $this->line("User's name is " . User::find(999)->name); + $this->line("User's name is " . User::find($id)->name); $this->line($this->argument('a')); $this->line($this->option('c')); } From 9078280a44c3c5d5d4e37ad3c7fc356461603e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 4 Jan 2023 03:04:00 +0100 Subject: [PATCH 32/37] revert to 8.1 in CI for now --- Dockerfile | 3 ++- INTERNAL.md | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 INTERNAL.md diff --git a/Dockerfile b/Dockerfile index 5dfe442c..73a052d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ # add amd64 platform to support Mac M1 FROM --platform=linux/amd64 shivammathur/node:latest-amd64 -ARG PHP_VERSION=8.2 +# todo update this to 8.2 once shivammathur/node supports that +ARG PHP_VERSION=8.1 WORKDIR /var/www/html diff --git a/INTERNAL.md b/INTERNAL.md new file mode 100644 index 00000000..4b3297dd --- /dev/null +++ b/INTERNAL.md @@ -0,0 +1,8 @@ +# Internal development notes + +## Updating the docker image used by the GH action + +1. Login in to Docker Hub: `docker login -u archtechx -p` +2. Build the image (probably shut down docker-compose containers first): `docker-compose build --no-cache` +3. Tag a new image: `docker tag tenancy_test archtechx/tenancy:latest` +4. Push the image: `docker push archtechx/tenancy:latest` From db1dc334a63058eb3ad976bdc1bc90606d958175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 4 Jan 2023 03:04:56 +0100 Subject: [PATCH 33/37] add todo --- src/Tenancy.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tenancy.php b/src/Tenancy.php index e8187dd8..991f9234 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -118,6 +118,7 @@ class Tenancy */ public static function find(int|string $id): Tenant|null { + // todo update all syntax like this once we're fully on PHP 8.2 /** @var (Tenant&Model)|null */ $tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first(); From 32a128d65750b178e3ed32c70d42ab57dca860c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 4 Jan 2023 03:08:56 +0100 Subject: [PATCH 34/37] lower required php version back to 8.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0cfe3984..098b1cc4 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^8.2", + "php": "^8.1", "ext-json": "*", "illuminate/support": "^9.0", "spatie/ignition": "^1.4", From 24d71230e8231d6086f5eaf316ae34129a1e02d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 4 Jan 2023 03:18:52 +0100 Subject: [PATCH 35/37] comment out php 8.2 phpstan ignores --- phpstan.neon | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 7ae06b44..6a864833 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -48,8 +48,10 @@ parameters: paths: - src/Database/DatabaseConfig.php - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' - - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#' - - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#' + + # php 8.2 + # - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#' + # - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#' checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false From 48fc63fe02363af3ac3baa5c8fcf1a21d84a5336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 6 Jan 2023 02:38:31 +0100 Subject: [PATCH 36/37] switch to PHP 8.2 --- Dockerfile | 3 +-- composer.json | 2 +- phpstan.neon | 6 ++---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 73a052d5..5dfe442c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ # add amd64 platform to support Mac M1 FROM --platform=linux/amd64 shivammathur/node:latest-amd64 -# todo update this to 8.2 once shivammathur/node supports that -ARG PHP_VERSION=8.1 +ARG PHP_VERSION=8.2 WORKDIR /var/www/html diff --git a/composer.json b/composer.json index 098b1cc4..0cfe3984 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^8.1", + "php": "^8.2", "ext-json": "*", "illuminate/support": "^9.0", "spatie/ignition": "^1.4", diff --git a/phpstan.neon b/phpstan.neon index 6a864833..7ae06b44 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -48,10 +48,8 @@ parameters: paths: - src/Database/DatabaseConfig.php - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' - - # php 8.2 - # - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#' - # - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#' + - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#' + - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#' checkMissingIterableValueType: false treatPhpDocTypesAsCertain: false From 21d55ef4726cca70179842c3d26266602f8bbdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 6 Jan 2023 02:44:37 +0100 Subject: [PATCH 37/37] add setup-php to phpstan job --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 724aed35..dc61273d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,11 @@ jobs: name: Static analysis (PHPStan) runs-on: ubuntu-latest steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: imagick, swoole - uses: actions/checkout@v2 - name: Install composer dependencies run: composer install