1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 18:44:03 +00:00

Merge branch '2.x'

This commit is contained in:
Samuel Štancl 2019-10-07 20:43:53 +02:00
commit a266a46c83
66 changed files with 1503 additions and 600 deletions

View file

@ -20,7 +20,7 @@ before_script:
- export DB_USERNAME=root DB_PASSWORD="" DB_DATABASE=tenancy CODECOV_TOKEN="24382d15-84e7-4a55-bea4-c4df96a24a9b" - export DB_USERNAME=root DB_PASSWORD="" DB_DATABASE=tenancy CODECOV_TOKEN="24382d15-84e7-4a55-bea4-c4df96a24a9b"
- cat vendor/laravel/framework/src/Illuminate/Foundation/Application.php| grep 'const VERSION' - cat vendor/laravel/framework/src/Illuminate/Foundation/Application.php| grep 'const VERSION'
script: ./test script: ./fulltest
after_success: after_success:
- bash <(curl -s https://codecov.io/bash) - bash <(curl -s https://codecov.io/bash)

View file

@ -1,119 +0,0 @@
# Release Notes for 1.x
## [v1.8.0 (2019-08-17)](https://github.com/stancl/tenancy/compare/v1.7.0...v1.8.0)
### Added
- **Multi-tenant Jobs:** Jobs are now automatically multi-tenant. The [documentation page](https://stancl-tenancy.netlify.com/docs/jobs-queues/) covers the small tweaks you will have to make to your config to get multi-tenant jobs to work.
- **Telescope Integration**: You can read more about this on the [documentation page](https://stancl-tenancy.netlify.com/docs/telescope/).
- **Horizon Integration**: You can read more about this on the [documentation page](https://stancl-tenancy.netlify.com/docs/horizon/).
- **Tenant Redirect** and **Custom ID schemes**: You can now easily redirect to tenant domains. You can also use a custom tenant ID scheme if you don't like UUIDs. You can read about these features [here](https://stancl-tenancy.netlify.com/docs/misc-tips/).
### Fixed
- #112 *PostgreSQL Database creation error.*
### Code
- Strict types declaration is now used in every file.
## [v1.7.0 (2019-08-17)](https://github.com/stancl/tenancy/compare/v1.6.1...v1.7.0)
### Added:
- DB storage driver - you don't have to use Redis to store tenants anymore. Relational databases are now supported as well. [more info](https://stancl-tenancy.netlify.com/docs/storage-drivers/#database)
- `tenancy:install` will do everything except DB/Redis connection creation for you. It will make changes to Http/Kernel.php, create `routes/tenant.php`, publish config, and (optionally) publish the migration. [more info](https://stancl-tenancy.netlify.com/docs/installation/)
- `tenants:run` [more info](https://stancl-tenancy.netlify.com/docs/console-commands/#run)
- New documentation: https://stancl-tenancy.netlify.com
- Custom tenant DB names [more info](https://stancl-tenancy.netlify.com/docs/custom-database-names/)
- stancl/tenancy events [more info](https://stancl-tenancy.netlify.com/docs/event-system/)
### Fixed:
- #89 *Command "tenants:migrate" cannot be found when used in app code*
- #87 *Unable to migrate multiple tenants at once when using MySQL*
- #96 *Issue w/ redis->scan() in getAllTenants logic.*
## [v1.6.1 (2019-08-04)](https://github.com/stancl/tenancy/compare/v1.6.0...v1.6.1)
Multiple phpunit.xml configs are now generated to run the tests with different configurations, such as different Redis drivers.
### Fixed
- `tenancy()->all()` with predis [`0dc8c80`](https://github.com/stancl/tenancy/commit/0dc8c80a02efbee5676cc72e648e108037ca5268)
### Dropped
- Laravel 5.7 support [`65b3882`](https://github.com/stancl/tenancy/commit/65b38827d5a2fa183838a9dce9fb6a157fd7e859)
## [v1.6.0 (2019-07-30)](https://github.com/stancl/tenancy/compare/v1.5.1...v1.6.0)
### Added
- `GlobalCache` facade [#78](https://github.com/stancl/tenancy/pull/78)
## [v1.5.1 (2019-07-25)](https://github.com/stancl/tenancy/compare/v1.5.0...v1.5.1)
### Fixed
- Database is reconnected after migrating/rolling back/seeding is done [#71](https://github.com/stancl/tenancy/pull/71)
- Fixed tenant()->delete() (it used to delete the record from the `tenants` namespace but not the `domains` namespace) [#73](https://github.com/stancl/tenancy/pull/73)
## [v1.5.0 (2019-07-13)](https://github.com/stancl/tenancy/compare/v1.4.0...v1.5.0)
### Added
- PostgreSQL DB manager [#52](https://github.com/stancl/tenancy/pull/52)
- `tenancy()->end()` [#68](https://github.com/stancl/tenancy/pull/68)
### Fixed
- Return type docblock for `TenantManager::all()` [#63](https://github.com/stancl/tenancy/issue/63)
## [v1.4.0 (2019-07-03)](https://github.com/stancl/tenancy/compare/v1.3.1...v1.4.0)
### Added
- Predis support [#59](https://github.com/stancl/tenancy/pull/59)
## [v1.3.1 (2019-05-06)](https://github.com/stancl/tenancy/compare/v1.3.0...v1.3.1)
### Fixed
- Fix jobs [#38](https://github.com/stancl/tenancy/pull/38)
- Fix tests for 5.8 [#41](https://github.com/stancl/tenancy/issues/41)
## [v1.3.0 (2019-02-27)](https://github.com/stancl/tenancy/compare/v1.2.0...v1.3.0)
### Added
- Add 5.8 support [#33](https://github.com/stancl/tenancy/pull/33)
## [v1.2.0 (2019-02-15)](https://github.com/stancl/tenancy/compare/v1.1.3...v1.2.0)
### Added
- Add `Tenancy` facade [#29](https://github.com/stancl/tenancy/issues/29) [`987c54f`](https://github.com/stancl/tenancy/commit/987c54f04e6ff3bdef068d92da6a9ace847f6c37)
## [v1.1.3 (2019-02-13)](https://github.com/stancl/tenancy/compare/v1.1.2...v1.1.3)
### Fixed
- Fix CacheManager (it merged tags incorrectly), write tests for CacheManager [#31](https://github.com/stancl/tenancy/issues/31) [`a2d68b1`](https://github.com/stancl/tenancy/commit/a2d68b12611350f70befa3eb97fb56c99d006b54)
## [v1.1.2 (2019-02-13)](https://github.com/stancl/tenancy/compare/v1.1.1...v1.1.2)
### Fixed
- Fix small bug in CacheManager [`d4d4119`](https://github.com/stancl/tenancy/commit/d4d411975496272158d7823597427fad8966fff8)
## [v1.1.1 (2019-02-11)](https://github.com/stancl/tenancy/compare/v1.1.0...v1.1.1)
### Fixed
- Fix "Associative arrays are stored as objects" [#28](https://github.com/stancl/tenancy/issues/28)
## [v1.1.0 (2019-02-10)](https://github.com/stancl/tenancy/compare/v1.0.0...v1.1.0)
### Added
- Add array support to the storage [#27](https://github.com/stancl/tenancy/pull/27)

15
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,15 @@
# Contributing
## Code style
StyleCI will automatically fix code style violations in your pull requests.
## Running tests
### With Docker
If you have Docker installed, simply run ./test. When you're done testing, run docker-compose down to shut down the containers.
### Without Docker
If you run the tests of this package, please make sure you don't store anything in Redis @ 127.0.0.1:6379 db#14. The contents of this database are flushed everytime the tests are run.
Some tests are run only if the CI, TRAVIS and CONTINUOUS_INTEGRATION environment variables are set to true. This is to avoid things like bloating your MySQL instance with test databases.

View file

@ -1,22 +1,21 @@
# [stancl/tenancy](https://tenancy.samuelstancl.me) # [stancl/tenancy](https://tenancy.samuelstancl.me)
[![Laravel 5.8](https://img.shields.io/badge/laravel-5.8-red.svg)](https://laravel.com) [![Laravel 6.x](https://img.shields.io/badge/laravel-6.x-red.svg)](https://laravel.com)
[![Latest Stable Version](https://poser.pugx.org/stancl/tenancy/version)](https://packagist.org/packages/stancl/tenancy) [![Latest Stable Version](https://poser.pugx.org/stancl/tenancy/version)](https://packagist.org/packages/stancl/tenancy)
[![Travis CI build](https://travis-ci.com/stancl/tenancy.svg?branch=1.x)](https://travis-ci.com/stancl/tenancy) [![Travis CI build](https://travis-ci.com/stancl/tenancy.svg?branch=2.x)](https://travis-ci.com/stancl/tenancy)
[![codecov](https://codecov.io/gh/stancl/tenancy/branch/1.x/graph/badge.svg)](https://codecov.io/gh/stancl/tenancy) [![codecov](https://codecov.io/gh/stancl/tenancy/branch/2.x/graph/badge.svg)](https://codecov.io/gh/stancl/tenancy)
[![Donate](https://img.shields.io/badge/Donate-%3C3-red)](https://gumroad.com/l/tenancy)
### *A Laravel multi-database tenancy package that respects your code.* ### *Automatic multi-tenancy for your Laravel app.*
You won't have to change a thing in your application's code.\* You won't have to change a thing in your application's code.
- :heavy_check_mark: No model traits to change database connection - :heavy_check_mark: No model traits to change database connection
- :heavy_check_mark: No replacing of Laravel classes (`Cache`, `Storage`, ...) with tenancy-aware classes - :heavy_check_mark: No replacing of Laravel classes (`Cache`, `Storage`, ...) with tenancy-aware classes
- :heavy_check_mark: Built-in tenant identification based on hostname (including second level domains) - :heavy_check_mark: Built-in tenant identification based on hostname (including second level domains)
\* depending on how you use the filesystem. Everything else will work out of the box. ### [Documentation](https://tenancy.samuelstancl.me/docs/v2/)
### [Documentation](https://tenancy.samuelstancl.me/docs) Documentation can be found here: https://tenancy.samuelstancl.me/docs/v2/
Documentation can be found here: https://tenancy.samuelstancl.me/docs
The repository with the documentation source code can be found here: [stancl/tenancy-docs](https://github.com/stancl/tenancy-docs). The repository with the documentation source code can be found here: [stancl/tenancy-docs](https://github.com/stancl/tenancy-docs).

View file

@ -3,16 +3,22 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'storage_driver' => 'Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver', 'storage_driver' => 'db',
'storage' => [ 'storage_drivers' => [
'db' => [ // Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver 'db' => [
'driver' => Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver::class,
'data_column' => 'data', 'data_column' => 'data',
'custom_columns' => [ 'custom_columns' => [
// 'plan', // 'plan',
], ],
'connection' => 'central', 'connection' => null,
'table_names' => [
'TenantModel' => 'tenants',
'DomainModel' => 'domains',
],
], ],
'redis' => [ // Stancl\Tenancy\StorageDrivers\RedisStorageDriver 'redis' => [
'driver' => Stancl\Tenancy\StorageDrivers\RedisStorageDriver::class,
'connection' => 'tenancy', 'connection' => 'tenancy',
], ],
], ],
@ -21,7 +27,7 @@ return [
// 'localhost', // 'localhost',
], ],
'database' => [ 'database' => [
'based_on' => 'mysql', // The connection that will be used as a base for the dynamically created tenant connection. 'based_on' => null, // The connection that will be used as a base for the dynamically created tenant connection.
'prefix' => 'tenant', 'prefix' => 'tenant',
'suffix' => '', 'suffix' => '',
], ],
@ -35,7 +41,7 @@ return [
'cache' => [ 'cache' => [
'tag_base' => 'tenant', 'tag_base' => 'tenant',
], ],
'filesystem' => [ // https://stancl-tenancy.netlify.com/docs/filesystem-tenancy/ 'filesystem' => [ // https://tenancy.samuelstancl.me/docs/v2/filesystem-tenancy/
'suffix_base' => 'tenant', 'suffix_base' => 'tenant',
// Disks which should be suffixed with the suffix_base + tenant id. // Disks which should be suffixed with the suffix_base + tenant id.
'disks' => [ 'disks' => [
@ -51,29 +57,43 @@ return [
], ],
'database_managers' => [ 'database_managers' => [
// Tenant database managers handle the creation & deletion of tenant databases. // Tenant database managers handle the creation & deletion of tenant databases.
'sqlite' => 'Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager', 'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
'mysql' => 'Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager', 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
'pgsql' => 'Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager', 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
],
'database_manager_connections' => [
// Connections used by TenantDatabaseManagers. This tells, for example, the
// MySQLDatabaseManager to use the mysql connection to create databases.
'sqlite' => 'sqlite',
'mysql' => 'mysql',
'pgsql' => 'pgsql',
], ],
'bootstrappers' => [ 'bootstrappers' => [
// Tenancy bootstrappers are executed when tenancy is initialized. // Tenancy bootstrappers are executed when tenancy is initialized.
// Their responsibility is making Laravel features tenant-aware. // Their responsibility is making Laravel features tenant-aware.
'database' => 'Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper', 'database' => Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper::class,
'cache' => 'Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper', 'cache' => Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper::class,
'filesystem' => 'Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper', 'filesystem' => Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper::class,
'redis' => 'Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper', 'queue' => Stancl\Tenancy\TenancyBootstrappers\QueueTenancyBootstrapper::class,
'queue' => 'Stancl\Tenancy\TenancyBootstrappers\QueueTenancyBootstrapper', // 'redis' => Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
], ],
'features' => [ 'features' => [
// Features are classes that provide additional functionality // Features are classes that provide additional functionality
// not needed for tenancy to be bootstrapped. They are run // not needed for tenancy to be bootstrapped. They are run
// regardless of whether tenancy has been initialized. // regardless of whether tenancy has been initialized.
'Stancl\Tenancy\Features\TelescopeTags',
'Stancl\Tenancy\Features\TenantRedirect', // Stancl\Tenancy\Features\TenantConfig::class,
// Stancl\Tenancy\Features\TelescopeTags::class,
// Stancl\Tenancy\Features\TenantRedirect::class,
], ],
'storage_to_config_map' => [ // Used by the TenantConfig feature
// 'paypal_api_key' => 'services.paypal.api_key',
],
'home_url' => '/app',
'migrate_after_creation' => false, // run migrations after creating a tenant 'migrate_after_creation' => false, // run migrations after creating a tenant
'delete_database_after_tenant_deletion' => false, // delete tenant's database after deleting him 'queue_automatic_migration' => false, // queue the automatic post-tenant-creation migrations
'delete_database_after_tenant_deletion' => false, // delete the tenant's database after deleting the tenant
'queue_database_creation' => false, 'queue_database_creation' => false,
'queue_database_deletion' => false, 'queue_database_deletion' => false,
'unique_id_generator' => 'Stancl\Tenancy\UUIDGenerator', 'unique_id_generator' => Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator::class,
]; ];

View file

@ -13,11 +13,12 @@ class CreateTenantsTable extends Migration
* *
* @return void * @return void
*/ */
public function up() public function up(): void
{ {
Schema::create('tenants', function (Blueprint $table) { Schema::create('tenants', function (Blueprint $table) {
$table->string('id', 36)->primary(); // 36 characters is the default uuid length $table->string('id', 36)->primary(); // 36 characters is the default uuid length
// your custom, indexed columns go here
// (optional) your custom, indexed columns may go here
$table->json('data'); $table->json('data');
}); });
@ -28,8 +29,8 @@ class CreateTenantsTable extends Migration
* *
* @return void * @return void
*/ */
public function down() public function down(): void
{ {
Schema::drop('tenants'); Schema::dropIfExists('tenants');
} }
} }

View file

@ -13,11 +13,13 @@ class CreateDomainsTable extends Migration
* *
* @return void * @return void
*/ */
public function up() public function up(): void
{ {
Schema::create('domains', function (Blueprint $table) { Schema::create('domains', function (Blueprint $table) {
$table->string('tenant_id', 36)->primary(); // 36 characters is the default uuid length $table->string('domain', 255)->primary();
$table->string('domain', 255)->index(); // don't change this $table->string('tenant_id', 36);
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
}); });
} }
@ -26,8 +28,8 @@ class CreateDomainsTable extends Migration
* *
* @return void * @return void
*/ */
public function down() public function down(): void
{ {
Schema::drop('domains'); Schema::dropIfExists('domains');
} }
} }

View file

@ -11,7 +11,8 @@
], ],
"require": { "require": {
"illuminate/support": "^6.0", "illuminate/support": "^6.0",
"webpatser/laravel-uuid": "^3.0" "facade/ignition-contracts": "^1.0",
"ramsey/uuid": "^3.7"
}, },
"require-dev": { "require-dev": {
"vlucas/phpdotenv": "^3.3", "vlucas/phpdotenv": "^3.3",

7
fulltest Executable file
View file

@ -0,0 +1,7 @@
#!/bin/bash
set -e
# for development
docker-compose up -d
./test "$@"
docker-compose exec test vendor/bin/phpcov merge --clover clover.xml coverage/

View file

@ -8,19 +8,26 @@ use Illuminate\Cache\CacheManager as BaseCacheManager;
class CacheManager extends BaseCacheManager class CacheManager extends BaseCacheManager
{ {
/**
* Add tags and forward the call to the inner cache store.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters) public function __call($method, $parameters)
{ {
$tags = [config('tenancy.cache.tag_base') . tenant('id')]; $tags = [config('tenancy.cache.tag_base') . tenant('id')];
if ($method === 'tags') { if ($method === 'tags') {
if (\count($parameters) !== 1) { if (count($parameters) !== 1) {
throw new \Exception("Method tags() takes exactly 1 argument. {count($parameters)} passed."); throw new \Exception("Method tags() takes exactly 1 argument. {count($parameters)} passed.");
} }
$names = $parameters[0]; $names = $parameters[0];
$names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/5.7/cache#removing-tagged-cache-items $names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/5.7/cache#removing-tagged-cache-items
return $this->store()->tags(\array_merge($tags, $names)); return $this->store()->tags(array_merge($tags, $names));
} }
return $this->store()->tags($tags)->$method(...$parameters); return $this->store()->tags($tags)->$method(...$parameters);

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Stancl\Tenancy\Tenant;
class CreateTenant extends Command
{
protected $signature = 'tenants:create
{--d|domain=* : The tenant\'s domains.}
{data?* : The tenant\'s data. Separate keys and values by `=`, e.g. `plan=free`.}';
protected $description = 'Create a tenant.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$tenant = Tenant::new()
->withDomains($this->getDomains())
->withData($this->getData())
->save();
$this->info($tenant->id);
}
public function getDomains(): array
{
return $this->option('domain');
}
public function getData(): array
{
return array_reduce($this->argument('data'), function ($data, $pair) {
[$key, $value] = explode('=', $pair, 2);
$data[$key] = $value;
return $data;
}, []);
}
}

View file

@ -36,21 +36,21 @@ class Install extends Command
]); ]);
$this->info('✔️ Created config/tenancy.php'); $this->info('✔️ Created config/tenancy.php');
$newKernel = \str_replace( $newKernel = str_replace(
'protected $middlewarePriority = [', 'protected $middlewarePriority = [',
"protected \$middlewarePriority = [ "protected \$middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class, \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\Stancl\Tenancy\Middleware\InitializeTenancy::class,", \Stancl\Tenancy\Middleware\InitializeTenancy::class,",
\file_get_contents(app_path('Http/Kernel.php')) file_get_contents(app_path('Http/Kernel.php'))
); );
$newKernel = \str_replace("'web' => [", "'web' => [ $newKernel = str_replace("'web' => [", "'web' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel); \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel);
\file_put_contents(app_path('Http/Kernel.php'), $newKernel); file_put_contents(app_path('Http/Kernel.php'), $newKernel);
$this->info('✔️ Set middleware priority'); $this->info('✔️ Set middleware priority');
\file_put_contents( file_put_contents(
base_path('routes/tenant.php'), base_path('routes/tenant.php'),
"<?php "<?php
@ -65,7 +65,7 @@ class Install extends Command
| |
*/ */
Route::get('/', function () { Route::get('/app', function () {
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id'); return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
}); });
" "
@ -74,16 +74,16 @@ Route::get('/', function () {
$this->line(''); $this->line('');
$this->line("This package lets you store data about tenants either in Redis or in a relational database like MySQL. If you're going to use the database storage, you need to create a tenants table."); $this->line("This package lets you store data about tenants either in Redis or in a relational database like MySQL. If you're going to use the database storage, you need to create a tenants table.");
if ($this->confirm('Do you want to publish the default database migration?', true)) { if ($this->confirm('Do you want to publish the default database migrations?', true)) {
$this->callSilent('vendor:publish', [ $this->callSilent('vendor:publish', [
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider', '--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
'--tag' => 'migrations', '--tag' => 'migrations',
]); ]);
$this->info('✔️ Created migration.'); $this->info('✔️ Created migrations.');
} }
if (! \is_dir(database_path('migrations/tenant'))) { if (! is_dir(database_path('migrations/tenant'))) {
\mkdir(database_path('migrations/tenant')); mkdir(database_path('migrations/tenant'));
$this->info('✔️ Created database/migrations/tenant folder.'); $this->info('✔️ Created database/migrations/tenant folder.');
} }

View file

@ -53,19 +53,17 @@ class Migrate extends MigrateCommand
tenancy()->all($this->option('tenants'))->each(function ($tenant) { tenancy()->all($this->option('tenants'))->each(function ($tenant) {
$this->line("Tenant: {$tenant['id']}"); $this->line("Tenant: {$tenant['id']}");
// See Illuminate\Database\Migrations\DatabaseMigrationRepository::getConnection. $this->input->setOption('database', $tenant->getConnectionName());
// Database connections are cached by Illuminate\Database\ConnectionResolver. tenancy()->initialize($tenant);
$this->input->setOption('database', 'tenant');
tenancy()->initialize($tenant); // todo2 test that this works with multiple tenants with MySQL
// Migrate // Migrate
parent::handle(); parent::handle();
tenancy()->endTenancy();
}); });
if ($originalTenant) { if ($originalTenant) {
tenancy()->initialize($originalTenant); tenancy()->initialize($originalTenant);
} else {
tenancy()->endTenancy();
} }
} }
} }

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Stancl\Tenancy\Traits\DealsWithMigrations;
use Stancl\Tenancy\Traits\HasATenantsOption;
final class MigrateFresh extends Command
{
use HasATenantsOption, DealsWithMigrations;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Drop all tables and re-run all migrations for tenant(s)';
public function __construct()
{
parent::__construct();
$this->setName('tenants:migrate-fresh');
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$originalTenant = tenancy()->getTenant();
$this->info('Dropping tables.');
tenancy()->all($this->option('tenants'))->each(function ($tenant) {
$this->line("Tenant: {$tenant->id}");
tenancy()->initialize($tenant);
$this->call('db:wipe', array_filter([
'--database' => $tenant->getConnectionName(),
'--force' => true,
]));
$this->call('tenants:migrate', [
'--tenants' => [$tenant->id],
]);
tenancy()->end();
});
$this->info('Done.');
if ($originalTenant) {
tenancy()->initialize($originalTenant);
}
}
}

View file

@ -49,21 +49,21 @@ class Rollback extends RollbackCommand
return; return;
} }
$this->input->setOption('database', 'tenant');
$originalTenant = tenancy()->getTenant(); $originalTenant = tenancy()->getTenant();
tenancy()->all($this->option('tenants'))->each(function ($tenant) { tenancy()->all($this->option('tenants'))->each(function ($tenant) {
$this->line("Tenant: {$tenant['id']}"); $this->line("Tenant: {$tenant['id']}");
$this->input->setOption('database', $tenant->getConnectionName());
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
// Migrate // Migrate
parent::handle(); parent::handle();
tenancy()->endTenancy();
}); });
if ($originalTenant) { if ($originalTenant) {
tenancy()->initialize($originalTenant); tenancy()->initialize($originalTenant);
} else {
tenancy()->endTenancy();
} }
} }
} }

View file

@ -39,7 +39,7 @@ class Run extends Command
$callback = function ($prefix = '') { $callback = function ($prefix = '') {
return function ($arguments, $argument) use ($prefix) { return function ($arguments, $argument) use ($prefix) {
[$key, $value] = \explode('=', $argument, 2); [$key, $value] = explode('=', $argument, 2);
$arguments[$prefix . $key] = $value; $arguments[$prefix . $key] = $value;
return $arguments; return $arguments;
@ -47,21 +47,19 @@ class Run extends Command
}; };
// Turns ['foo=bar', 'abc=xyz=zzz'] into ['foo' => 'bar', 'abc' => 'xyz=zzz'] // Turns ['foo=bar', 'abc=xyz=zzz'] into ['foo' => 'bar', 'abc' => 'xyz=zzz']
$arguments = \array_reduce($this->option('argument'), $callback(), []); $arguments = array_reduce($this->option('argument'), $callback(), []);
// Turns ['foo=bar', 'abc=xyz=zzz'] into ['--foo' => 'bar', '--abc' => 'xyz=zzz'] // Turns ['foo=bar', 'abc=xyz=zzz'] into ['--foo' => 'bar', '--abc' => 'xyz=zzz']
$options = \array_reduce($this->option('option'), $callback('--'), []); $options = array_reduce($this->option('option'), $callback('--'), []);
// Run command // Run command
$this->call($this->argument('commandname'), \array_merge($arguments, $options)); $this->call($this->argument('commandname'), array_merge($arguments, $options));
tenancy()->endTenancy(); tenancy()->endTenancy();
}); });
if ($originalTenant) { if ($originalTenant) {
tenancy()->initialize($originalTenant); tenancy()->initialize($originalTenant);
} else {
tenancy()->endTenancy();
} }
} }
} }

View file

@ -47,21 +47,21 @@ class Seed extends SeedCommand
return; return;
} }
$this->input->setOption('database', 'tenant');
$originalTenant = tenancy()->getTenant(); $originalTenant = tenancy()->getTenant();
tenancy()->all($this->option('tenants'))->each(function ($tenant) { tenancy()->all($this->option('tenants'))->each(function ($tenant) {
$this->line("Tenant: {$tenant['id']}"); $this->line("Tenant: {$tenant['id']}");
$this->input->setOption('database', $tenant->getConnectionName());
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
// Seed // Seed
parent::handle(); parent::handle();
tenancy()->endTenancy();
}); });
if ($originalTenant) { if ($originalTenant) {
tenancy()->initialize($originalTenant); tenancy()->initialize($originalTenant);
} else {
tenancy()->endTenancy();
} }
} }
} }

View file

@ -31,7 +31,7 @@ class TenantList extends Command
{ {
$this->info('Listing all tenants.'); $this->info('Listing all tenants.');
tenancy()->all()->each(function ($tenant) { tenancy()->all()->each(function ($tenant) {
$this->line("[Tenant] id: {$tenant['id']} @ ", implode('; ', $tenant->domains)); $this->line("[Tenant] id: {$tenant['id']} @ " . implode('; ', $tenant->domains));
}); });
} }
} }

View file

@ -6,9 +6,12 @@ namespace Stancl\Tenancy\Contracts;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
/**
* TenancyBootstrappers are classes that make existing code tenant-aware.
*/
interface TenancyBootstrapper interface TenancyBootstrapper
{ {
public function start(Tenant $tenant); // todo2 TenantManager instead of Tenant public function start(Tenant $tenant);
public function end(); public function end();
} }

View file

@ -62,7 +62,7 @@ class DatabaseManager
public function createTenantConnection($databaseName, $connectionName) public function createTenantConnection($databaseName, $connectionName)
{ {
// Create the database connection. // Create the database connection.
$based_on = $this->app['config']['tenancy.database.based_on'] ?? $this->originalDefaultConnectionName; $based_on = $this->getBaseConnection($connectionName);
$this->app['config']["database.connections.$connectionName"] = $this->app['config']['database.connections.' . $based_on]; $this->app['config']["database.connections.$connectionName"] = $this->app['config']['database.connections.' . $based_on];
// Change database name. // Change database name.
@ -71,17 +71,36 @@ class DatabaseManager
} }
/** /**
* Get the driver of a database connection. * Get the name of the connection that $connectionName should be based on.
* *
* @param string $connectionName * @param string $connectionName
* @return string * @return string
*/ */
protected function getDriver(string $connectionName): string public function getBaseConnection(string $connectionName): string
{
return ($connectionName !== 'tenant' ? $connectionName : null) // 'tenant' is not a specific connection, it's the default
?? $this->app['config']['tenancy.database.based_on']
?? $this->originalDefaultConnectionName; // tenancy.database.based_on === null => use the default connection
}
/**
* Get the driver of a database connection.
*
* @param string $connectionName
* @return string|null
*/
public function getDriver(string $connectionName): ?string
{ {
return $this->app['config']["database.connections.$connectionName.driver"]; return $this->app['config']["database.connections.$connectionName.driver"];
} }
public function switchConnection($connection) /**
* Switch the application's connection.
*
* @param string $connection
* @return void
*/
public function switchConnection(string $connection)
{ {
$this->app['config']['database.default'] = $connection; $this->app['config']['database.default'] = $connection;
$this->database->purge(); $this->database->purge();
@ -103,6 +122,12 @@ class DatabaseManager
} }
} }
/**
* Create a database for a tenant.
*
* @param Tenant $tenant
* @return void
*/
public function createDatabase(Tenant $tenant) public function createDatabase(Tenant $tenant)
{ {
$database = $tenant->getDatabaseName(); $database = $tenant->getDatabaseName();
@ -111,10 +136,16 @@ class DatabaseManager
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) { if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
QueuedTenantDatabaseCreator::dispatch($manager, $database); QueuedTenantDatabaseCreator::dispatch($manager, $database);
} else { } else {
return $manager->createDatabase($database); $manager->createDatabase($database);
} }
} }
/**
* Delete a tenant's database.
*
* @param Tenant $tenant
* @return void
*/
public function deleteDatabase(Tenant $tenant) public function deleteDatabase(Tenant $tenant)
{ {
$database = $tenant->getDatabaseName(); $database = $tenant->getDatabaseName();
@ -123,15 +154,19 @@ class DatabaseManager
if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) { if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) {
QueuedTenantDatabaseDeleter::dispatch($manager, $database); QueuedTenantDatabaseDeleter::dispatch($manager, $database);
} else { } else {
return $manager->deleteDatabase($database); $manager->deleteDatabase($database);
} }
} }
/**
* Get the TenantDatabaseManager for a tenant's database connection.
*
* @param Tenant $tenant
* @return TenantDatabaseManager
*/
protected function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager protected function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
{ {
// todo2 this shouldn't have to create a connection $driver = $this->getDriver($this->getBaseConnection($tenant->getConnectionName()));
$this->createTenantConnection($tenant->getDatabaseName(), $tenant->getConnectionName());
$driver = $this->getDriver($tenant->getConnectionName());
$databaseManagers = $this->app['config']['tenancy.database_managers']; $databaseManagers = $this->app['config']['tenancy.database_managers'];

View file

@ -8,6 +8,6 @@ class DatabaseManagerNotRegisteredException extends \Exception
{ {
public function __construct($driver) public function __construct($driver)
{ {
$this->message = "Database manager for driver $driver is not registered."; parent::__construct("Database manager for driver $driver is not registered.");
} }
} }

View file

@ -4,10 +4,23 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions; namespace Stancl\Tenancy\Exceptions;
class TenantCouldNotBeIdentifiedException extends \Exception use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
class TenantCouldNotBeIdentifiedException extends \Exception implements ProvidesSolution
{ {
public function __construct($domain) public function __construct($domain)
{ {
$this->message = "Tenant could not be identified on domain $domain"; parent::__construct("Tenant could not be identified on domain $domain");
}
public function getSolution(): Solution
{
return BaseSolution::create('Tenant could not be identified on this domain')
->setSolutionDescription('Did you forget to create a tenant for this domain?')
->setDocumentationLinks([
'Creating Tenants' => 'https://tenancy.samuelstancl.me/docs/v2/creating-tenants/',
]);
} }
} }

View file

@ -5,13 +5,17 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Facades; namespace Stancl\Tenancy\Facades;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
use Stancl\Tenancy\Tenant as Tenant; use Stancl\Tenancy\Tenant;
// todo2 rename to CurrentTenant?
class TenantFacade extends Facade class TenantFacade extends Facade
{ {
protected static function getFacadeAccessor() protected static function getFacadeAccessor()
{ {
return Tenant::class; return Tenant::class;
} }
public static function create($domains, array $data = []): Tenant
{
return Tenant::create($domains, $data);
}
} }

View file

@ -14,6 +14,13 @@ class TelescopeTags implements Feature
/** @var callable User-specific callback that returns tags. */ /** @var callable User-specific callback that returns tags. */
protected $callback; protected $callback;
public function __construct()
{
$this->callback = function ($entry) {
return [];
};
}
public function bootstrap(TenantManager $tenantManager): void public function bootstrap(TenantManager $tenantManager): void
{ {
if (! class_exists(Telescope::class)) { if (! class_exists(Telescope::class)) {
@ -26,7 +33,6 @@ class TelescopeTags implements Feature
if (in_array('tenancy', optional(request()->route())->middleware() ?? [])) { if (in_array('tenancy', optional(request()->route())->middleware() ?? [])) {
$tags = array_merge($tags, [ $tags = array_merge($tags, [
'tenant:' . tenant('id'), 'tenant:' . tenant('id'),
// todo2 domain?
]); ]);
} }

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Features;
use Illuminate\Config\Repository;
use Stancl\Tenancy\Contracts\Feature;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager;
class TenantConfig implements Feature
{
/** @var Repository */
protected $config;
/** @var array */
public $originalConfig = [];
public function __construct(Repository $config)
{
$this->config = $config;
foreach ($this->getStorageToConfigMap() as $configKey) {
$this->originalConfig[$configKey] = $this->config[$configKey];
}
}
public function bootstrap(TenantManager $tenantManager): void
{
$tenantManager->eventListener('bootstrapped', function (TenantManager $manager) {
$this->setTenantConfig($manager->getTenant());
});
$tenantManager->eventListener('ended', function () {
$this->unsetTenantConfig();
});
}
public function setTenantConfig(Tenant $tenant): void
{
foreach ($this->getStorageToConfigMap() as $storageKey => $configKey) {
$override = $tenant->data[$storageKey] ?? null;
if (! is_null($override)) {
$this->config[$configKey] = $override;
}
}
}
public function unsetTenantConfig(): void
{
foreach ($this->getStorageToConfigMap() as $configKey) {
$this->config[$configKey] = $this->originalConfig[$configKey];
}
}
public function getStorageToConfigMap(): array
{
return $this->config['tenancy.storage_to_config_map'] ?? [];
}
}

View file

@ -15,7 +15,10 @@ class QueuedTenantDatabaseCreator implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var TenantDatabaseManager */
protected $databaseManager; protected $databaseManager;
/** @var string */
protected $databaseName; protected $databaseName;
/** /**

View file

@ -15,7 +15,10 @@ class QueuedTenantDatabaseDeleter implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var TenantDatabaseManager */
protected $databaseManager; protected $databaseManager;
/** @var string */
protected $databaseName; protected $databaseName;
/** /**

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;
use Stancl\Tenancy\Tenant;
class QueuedTenantDatabaseMigrator implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var Tenant */
protected $tenant;
public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Artisan::call('tenants:migrate', [
'--tenants' => [$this->tenant->id],
]);
}
}

View file

@ -5,10 +5,14 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware; namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
class InitializeTenancy class InitializeTenancy
{ {
public function __construct(Closure $onFail = null) /** @var callable */
protected $onFail;
public function __construct(callable $onFail = null)
{ {
$this->onFail = $onFail ?? function ($e) { $this->onFail = $onFail ?? function ($e) {
throw $e; throw $e;
@ -25,8 +29,8 @@ class InitializeTenancy
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
try { try {
tenancy()->init(); tenancy()->init($request->getHost());
} catch (\Exception $e) { } catch (TenantCouldNotBeIdentifiedException $e) {
($this->onFail)($e); ($this->onFail)($e);
} }

View file

@ -6,6 +6,10 @@ namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
/**
* Prevent access to non-tenant routes from domains that are not exempt from tenancy.
* = allow access to central routes only from routes listed in tenancy.exempt_routes.
*/
class PreventAccessFromTenantDomains class PreventAccessFromTenantDomains
{ {
/** /**
@ -19,8 +23,16 @@ class PreventAccessFromTenantDomains
{ {
// If the domain is not in exempt domains, it's a tenant domain. // If the domain is not in exempt domains, it's a tenant domain.
// Tenant domains can't have routes without tenancy middleware. // Tenant domains can't have routes without tenancy middleware.
if (! \in_array(request()->getHost(), config('tenancy.exempt_domains')) && $isExemptDomain = in_array($request->getHost(), config('tenancy.exempt_domains'));
! \in_array('tenancy', request()->route()->middleware())) { $isTenantDomain = ! $isExemptDomain;
$isTenantRoute = in_array('tenancy', $request->route()->middleware());
if ($isTenantDomain && ! $isTenantRoute) { // accessing web routes from tenant domains
return redirect(config('tenancy.home_url'));
}
if ($isExemptDomain && $isTenantRoute) { // accessing tenant routes on web domains
abort(404); abort(404);
} }

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
trait CentralConnection
{
public function getConnectionName()
{
return DatabaseStorageDriver::getCentralConnectionName();
}
}

View file

@ -7,7 +7,8 @@ namespace Stancl\Tenancy\StorageDrivers\Database;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException; use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException; use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains; use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains;
@ -16,17 +17,34 @@ use Stancl\Tenancy\Tenant;
class DatabaseStorageDriver implements StorageDriver class DatabaseStorageDriver implements StorageDriver
{ {
// todo2 write tests verifying that data is decoded and added to the array
/** @var Application */ /** @var Application */
protected $app; protected $app;
/** @var \Illuminate\Database\Connection */
protected $centralDatabase;
/** @var Tenant The default tenant. */ /** @var Tenant The default tenant. */
protected $tenant; protected $tenant;
public function __construct(Application $app) public function __construct(Application $app)
{ {
$this->app = $app; $this->app = $app;
$this->centralDatabase = $this->getCentralConnection();
}
/**
* Get the central database connection.
*
* @return \Illuminate\Database\Connection
*/
public static function getCentralConnection(): \Illuminate\Database\Connection
{
return DB::connection(static::getCentralConnectionName());
}
public static function getCentralConnectionName(): string
{
return config('tenancy.storage_drivers.db.connection') ?? app(DatabaseManager::class)->originalDefaultConnectionName;
} }
public function findByDomain(string $domain): Tenant public function findByDomain(string $domain): Tenant
@ -54,13 +72,12 @@ class DatabaseStorageDriver implements StorageDriver
public function ensureTenantCanBeCreated(Tenant $tenant): void public function ensureTenantCanBeCreated(Tenant $tenant): void
{ {
// todo2 test this
if (Tenants::find($tenant->id)) { if (Tenants::find($tenant->id)) {
throw new TenantWithThisIdAlreadyExistsException($tenant->id); throw new TenantWithThisIdAlreadyExistsException($tenant->id);
} }
if (Domains::whereIn('domain', $tenant->domains)->exists()) { if (Domains::whereIn('domain', $tenant->domains)->exists()) {
throw new DomainOccupiedByOtherTenantException(); throw new DomainsOccupiedByOtherTenantException;
} }
} }
@ -78,32 +95,45 @@ class DatabaseStorageDriver implements StorageDriver
public function createTenant(Tenant $tenant): void public function createTenant(Tenant $tenant): void
{ {
DB::transaction(function () use ($tenant) { $this->centralDatabase->transaction(function () use ($tenant) {
Tenants::create(['id' => $tenant->id, 'data' => '{}'])->toArray(); Tenants::create(['id' => $tenant->id, 'data' => json_encode($tenant->data)])->toArray();
$domainData = []; $domainData = [];
foreach ($tenant->domains as $domain) { foreach ($tenant->domains as $domain) {
$domainData[] = ['domain' => $domain, 'tenant_id' => $tenant->id]; $domainData[] = ['domain' => $domain, 'tenant_id' => $tenant->id];
} }
Domains::create($domainData);
Domains::insert($domainData);
}); });
} }
public function updateTenant(Tenant $tenant): void public function updateTenant(Tenant $tenant): void
{ {
Tenants::find($tenant->id)->putMany($tenant->data); $this->centralDatabase->transaction(function () use ($tenant) {
Domains::firstOrCreate(array_map(function ($domain) use ($tenant) { Tenants::find($tenant->id)->putMany($tenant->data);
return [
'tenant_id' => $tenant->id, $original_domains = Domains::where('tenant_id', $tenant->id)->get()->map(function ($model) {
'domain' => $domain, return $model->domain;
]; })->toArray();
}, $tenant->domains)); $deleted_domains = array_diff($original_domains, $tenant->domains);
Domains::whereIn('domain', $deleted_domains)->delete();
foreach ($tenant->domains as $domain) {
Domains::firstOrCreate([
'tenant_id' => $tenant->id,
'domain' => $domain,
]);
}
});
} }
public function deleteTenant(Tenant $tenant): void public function deleteTenant(Tenant $tenant): void
{ {
Tenants::find($tenant->id)->delete(); $this->centralDatabase->transaction(function () use ($tenant) {
Domains::where('tenant_id', $tenant->id)->delete(); Tenants::find($tenant->id)->delete();
Domains::where('tenant_id', $tenant->id)->delete();
});
} }
/** /**

View file

@ -11,14 +11,15 @@ use Illuminate\Database\Eloquent\Model;
*/ */
class DomainModel extends Model class DomainModel extends Model
{ {
use CentralConnection;
protected $guarded = []; protected $guarded = [];
protected $primaryKey = 'id'; protected $primaryKey = 'domain';
public $incrementing = false; public $incrementing = false;
public $timestamps = false; public $timestamps = false;
public $table = 'domains';
public function getConnectionName() public function getTable()
{ {
return config('tenancy.storage.db.connection') ?? app(DatabaseManager::class)->originalDefaultConnectionName; return config('tenancy.storage_drivers.db.table_names.DomainModel', 'domains');
} }
} }

View file

@ -11,25 +11,26 @@ use Illuminate\Database\Eloquent\Model;
*/ */
class TenantModel extends Model class TenantModel extends Model
{ {
use CentralConnection;
protected $guarded = []; protected $guarded = [];
protected $primaryKey = 'id'; protected $primaryKey = 'id';
public $incrementing = false; public $incrementing = false;
public $timestamps = false; public $timestamps = false;
public $table = 'tenants';
public function getTable()
{
return config('tenancy.storage_drivers.db.table_names.TenantModel', 'tenants');
}
public static function dataColumn() public static function dataColumn()
{ {
return config('tenancy.storage.db.data_column', 'data'); return config('tenancy.storage_drivers.db.data_column', 'data');
} }
public static function customColumns() public static function customColumns()
{ {
return config('tenancy.storage.db.custom_columns', []); return config('tenancy.storage_drivers.db.custom_columns', []);
}
public function getConnectionName()
{
return config('tenancy.storage.db.connection') ?? app(DatabaseManager::class)->originalDefaultConnectionName;
} }
public static function getAllTenants(array $ids) public static function getAllTenants(array $ids)
@ -81,7 +82,7 @@ class TenantModel extends Model
public function getMany(array $keys): array public function getMany(array $keys): array
{ {
return array_reduce($keys, function ($result, $key) { // todo2 performance return array_reduce($keys, function ($result, $key) {
$result[$key] = $this->get($key); $result[$key] = $this->get($key);
return $result; return $result;

View file

@ -7,14 +7,13 @@ namespace Stancl\Tenancy\StorageDrivers;
use Illuminate\Contracts\Redis\Factory as Redis; use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
// todo2 transactions instead of pipelines?
class RedisStorageDriver implements StorageDriver class RedisStorageDriver implements StorageDriver
{ {
// todo2 json encoding?
/** @var Application */ /** @var Application */
protected $app; protected $app;
@ -27,7 +26,7 @@ class RedisStorageDriver implements StorageDriver
public function __construct(Application $app, Redis $redis) public function __construct(Application $app, Redis $redis)
{ {
$this->app = $app; $this->app = $app;
$this->redis = $redis->connection($app['config']['tenancy.redis.connection'] ?? 'tenancy'); $this->redis = $redis->connection($app['config']['tenancy.storage_drivers.redis.connection'] ?? 'tenancy');
} }
/** /**
@ -49,7 +48,17 @@ class RedisStorageDriver implements StorageDriver
public function ensureTenantCanBeCreated(Tenant $tenant): void public function ensureTenantCanBeCreated(Tenant $tenant): void
{ {
// todo2 // Tenant ID
if ($this->redis->exists("tenants:{$tenant->id}")) {
throw new TenantWithThisIdAlreadyExistsException($tenant->id);
}
// Domains
if ($this->redis->exists(...array_map(function ($domain) {
return "domains:$domain";
}, $tenant->domains))) {
throw new DomainsOccupiedByOtherTenantException;
}
} }
public function findByDomain(string $domain): Tenant public function findByDomain(string $domain): Tenant
@ -59,26 +68,12 @@ class RedisStorageDriver implements StorageDriver
throw new TenantCouldNotBeIdentifiedException($domain); throw new TenantCouldNotBeIdentifiedException($domain);
} }
return $this->find($id); return $this->findById($id);
} }
public function findById(string $id): Tenant public function findById(string $id): Tenant
{ {
$data = $this->redis->hgetall("tenants:$id"); return $this->makeTenant($this->redis->hgetall("tenants:$id"));
$keys = [];
$values = [];
foreach ($data as $i => $value) {
if ($i & 1) { // is odd
$values[] = $value;
} else {
$keys[] = $value;
}
}
$data = array_combine($keys, $values);
$domains = []; // todo2
return Tenant::fromStorage($data)->withDomains($domains);
} }
public function getTenantIdByDomain(string $domain): ?string public function getTenantIdByDomain(string $domain): ?string
@ -88,32 +83,49 @@ class RedisStorageDriver implements StorageDriver
public function createTenant(Tenant $tenant): void public function createTenant(Tenant $tenant): void
{ {
$this->redis->pipeline(function ($pipe) use ($tenant) { $this->redis->transaction(function ($pipe) use ($tenant) {
$id = $tenant->id;
foreach ($tenant->domains as $domain) { foreach ($tenant->domains as $domain) {
$pipe->hmset("domains:$domain", 'tenant_id', $id); $pipe->hmset("domains:$domain", ['tenant_id' => $tenant->id]);
} }
$pipe->hmset("tenants:$id", 'id', json_encode($id), 'domain', json_encode($domain));
$data = [];
foreach ($tenant->data as $key => $value) {
$data[$key] = json_encode($value);
}
$pipe->hmset("tenants:{$tenant->id}", array_merge($data, ['_tenancy_domains' => json_encode($tenant->domains)]));
}); });
} }
public function updateTenant(Tenant $tenant): void public function updateTenant(Tenant $tenant): void
{ {
$this->redis->pipeline(function ($pipe) use ($tenant) { $id = $tenant->id;
$pipe->hmset("tenants:{$tenant->id}", $tenant->data);
foreach ($tenant->domains as $domain) { $old_domains = json_decode($this->redis->hget("tenants:$id", '_tenancy_domains'), true);
$pipe->hmset("domains:$domain", 'tenant_id', $tenant->id); $deleted_domains = array_diff($old_domains, $tenant->domains);
$domains = $tenant->domains;
$data = [];
foreach ($tenant->data as $key => $value) {
$data[$key] = json_encode($value);
}
$this->redis->transaction(function ($pipe) use ($id, $data, $deleted_domains, $domains) {
foreach ($deleted_domains as $deleted_domain) {
$pipe->del("domains:$deleted_domain");
} }
// todo2 deleted domains foreach ($domains as $domain) {
$pipe->hset("domains:$domain", 'tenant_id', $id);
}
$pipe->hmset("tenants:$id", array_merge($data, ['_tenancy_domains' => json_encode($domains)]));
}); });
} }
public function deleteTenant(Tenant $tenant): void public function deleteTenant(Tenant $tenant): void
{ {
$this->redis->pipeline(function ($pipe) use ($tenant) { $this->redis->transaction(function ($pipe) use ($tenant) {
foreach ($tenant->domains as $domain) { foreach ($tenant->domains as $domain) {
$pipe->del("domains:$domain"); $pipe->del("domains:$domain");
} }
@ -122,9 +134,14 @@ class RedisStorageDriver implements StorageDriver
}); });
} }
/**
* Return a list of all tenants.
*
* @param string[] $ids
* @return Tenant[]
*/
public function all(array $ids = []): array public function all(array $ids = []): array
{ {
// todo2 $this->redis->pipeline()
$hashes = array_map(function ($hash) { $hashes = array_map(function ($hash) {
return "tenants:{$hash}"; return "tenants:{$hash}";
}, $ids); }, $ids);
@ -143,15 +160,38 @@ class RedisStorageDriver implements StorageDriver
} }
return array_map(function ($tenant) { return array_map(function ($tenant) {
return $this->redis->hgetall($tenant); return $this->makeTenant($this->redis->hgetall($tenant));
}, $hashes); }, $hashes);
} }
/**
* Make a Tenant instance from low-level array data.
*
* @param array $data
* @return Tenant
*/
protected function makeTenant(array $data): Tenant
{
foreach ($data as $key => $value) {
$data[$key] = json_decode($value, true);
}
$domains = $data['_tenancy_domains'];
unset($data['_tenancy_domains']);
return Tenant::fromStorage($data)->withDomains($domains);
}
public function get(string $key, Tenant $tenant = null) public function get(string $key, Tenant $tenant = null)
{ {
$tenant = $tenant ?? $this->tenant(); $tenant = $tenant ?? $this->tenant();
return json_decode($this->redis->hget("tenants:{$tenant->id}", $key), true); $json_data = $this->redis->hget("tenants:{$tenant->id}", $key);
if ($json_data === false) {
return;
}
return json_decode($json_data, true);
} }
public function getMany(array $keys, Tenant $tenant = null): array public function getMany(array $keys, Tenant $tenant = null): array
@ -161,7 +201,7 @@ class RedisStorageDriver implements StorageDriver
$result = []; $result = [];
$values = $this->redis->hmget("tenants:{$tenant->id}", $keys); $values = $this->redis->hmget("tenants:{$tenant->id}", $keys);
foreach ($keys as $i => $key) { foreach ($keys as $i => $key) {
$result[$key] = $values[$i]; $result[$key] = json_decode($values[$i], true);
} }
return $result; return $result;

View file

@ -36,5 +36,7 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
$this->app->extend('cache', function () { $this->app->extend('cache', function () {
return $this->originalCache; return $this->originalCache;
}); });
$this->originalCache = null;
} }
} }

View file

@ -4,22 +4,17 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenancyBootstrappers; namespace Stancl\Tenancy\TenancyBootstrappers;
use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\DatabaseManager; use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
class DatabaseTenancyBootstrapper implements TenancyBootstrapper class DatabaseTenancyBootstrapper implements TenancyBootstrapper
{ {
/** @var Application */
protected $app;
/** @var DatabaseManager */ /** @var DatabaseManager */
protected $database; protected $database;
public function __construct(Application $app, DatabaseManager $database) public function __construct(DatabaseManager $database)
{ {
$this->app = $app;
$this->database = $database; $this->database = $database;
} }

View file

@ -9,37 +9,50 @@ use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
// todo better solution than tenant_asset?
class FilesystemTenancyBootstrapper implements TenancyBootstrapper class FilesystemTenancyBootstrapper implements TenancyBootstrapper
{ {
protected $originalPaths = [];
/** @var Application */ /** @var Application */
protected $app; protected $app;
/** @var array */
public $originalPaths = [];
public function __construct(Application $app) public function __construct(Application $app)
{ {
$this->app = $app; $this->app = $app;
$this->originalPaths = [ $this->originalPaths = [
'disks' => [], 'disks' => [],
'path' => $this->app->storagePath(), 'storage' => $this->app->storagePath(),
'asset_url' => $this->app['config']['app.asset_url'],
]; ];
$this->app['url']->macro('setAssetRoot', function ($root) {
$this->assetRoot = $root;
return $this;
});
} }
public function start(Tenant $tenant) public function start(Tenant $tenant)
{ {
// todo2 revisit this
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->id; $suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->id;
// storage_path() // storage_path()
$this->app->useStoragePath($this->originalPaths['path'] . "/{$suffix}"); $this->app->useStoragePath($this->originalPaths['storage'] . "/{$suffix}");
// asset()
if ($this->originalPaths['asset_url']) {
$this->app['config']['app.asset_url'] = ($this->originalPaths['asset_url'] ?? $this->app['config']['app.url']) . "/$suffix";
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
} else {
$this->app['url']->setAssetRoot($this->app['url']->route('stancl.tenancy.asset', ['path' => '']));
}
// Storage facade // Storage facade
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
$this->originalPaths['disks'][$disk] = Storage::disk($disk)->getAdapter()->getPathPrefix(); $this->originalPaths['disks'][$disk] = Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($root = \str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) { if ($root = str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) {
Storage::disk($disk)->getAdapter()->setPathPrefix($root); Storage::disk($disk)->getAdapter()->setPathPrefix($root);
} else { } else {
$root = $this->app['config']["filesystems.disks.{$disk}.root"]; $root = $this->app['config']["filesystems.disks.{$disk}.root"];
@ -52,7 +65,11 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
public function end() public function end()
{ {
// storage_path() // storage_path()
$this->app->useStoragePath($this->originalPaths['path']); $this->app->useStoragePath($this->originalPaths['storage']);
// asset()
$this->app['config']['app.asset_url'] = $this->originalPaths['asset_url'];
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
// Storage facade // Storage facade
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {

View file

@ -5,13 +5,14 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenancyBootstrappers; namespace Stancl\Tenancy\TenancyBootstrappers;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Testing\Fakes\QueueFake;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
class QueueTenancyBootstrapper implements TenancyBootstrapper class QueueTenancyBootstrapper implements TenancyBootstrapper
{ {
/** @var bool Has tenancy been started. */ /** @var bool Has tenancy been started. */
protected $started = false; public $started = false;
/** @var Application */ /** @var Application */
protected $app; protected $app;
@ -20,12 +21,14 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
{ {
$this->app = $app; $this->app = $app;
$this->app['queue']->createPayloadUsing([$this, 'createPayload']); $bootstrapper = &$this;
$this->app['events']->listen(\Illuminate\Queue\Events\JobProcessing::class, function ($event) {
if (\array_key_exists('tenant_id', $event->job->payload())) { $queue = $this->app['queue'];
tenancy()->initById($event->job->payload()['tenant_id']); if (! $queue instanceof QueueFake) {
} $queue->createPayloadUsing(function () use (&$bootstrapper) {
}); return $bootstrapper->getPayload();
});
}
} }
public function start(Tenant $tenant) public function start(Tenant $tenant)
@ -38,19 +41,18 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
$this->started = false; $this->started = false;
} }
public function createPayload() public function getPayload()
{ {
if (! $this->started) { if (! $this->started) {
return []; return [];
} }
$id = tenant()->get('id'); $id = tenant('id');
return [ return [
'tenant_id' => $id, 'tenant_id' => $id,
'tags' => [ 'tags' => [
"tenant:$id", "tenant:$id",
// todo2 domain
], ],
]; ];
} }

View file

@ -4,28 +4,28 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenancyBootstrappers; namespace Stancl\Tenancy\TenancyBootstrappers;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Config\Repository;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
class RedisTenancyBootstrapper implements TenancyBootstrapper class RedisTenancyBootstrapper implements TenancyBootstrapper
{ {
/** @var string[string] Original prefixes of connections */ /** @var array<string, string> Original prefixes of connections */
protected $originalPrefixes = []; public $originalPrefixes = [];
/** @var Application */ /** @var Repository */
protected $app; protected $config;
public function __construct(Application $app) public function __construct(Repository $config)
{ {
$this->app = $app; $this->config = $config;
} }
public function start(Tenant $tenant) public function start(Tenant $tenant)
{ {
foreach ($this->prefixedConnections() as $connection) { foreach ($this->prefixedConnections() as $connection) {
$prefix = $this->app['config']['tenancy.redis.prefix_base'] . $tenant['id']; $prefix = $this->config['tenancy.redis.prefix_base'] . $tenant['id'];
$client = Redis::connection($connection)->client(); $client = Redis::connection($connection)->client();
$this->originalPrefixes[$connection] = $client->getOption($client::OPT_PREFIX); $this->originalPrefixes[$connection] = $client->getOption($client::OPT_PREFIX);
@ -40,10 +40,12 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
$client->setOption($client::OPT_PREFIX, $this->originalPrefixes[$connection]); $client->setOption($client::OPT_PREFIX, $this->originalPrefixes[$connection]);
} }
$this->originalPrefixes = [];
} }
protected function prefixedConnections() protected function prefixedConnections()
{ {
return config('tenancy.redis.prefixed_connections'); return $this->config['tenancy.redis.prefixed_connections'];
} }
} }

View file

@ -7,53 +7,21 @@ namespace Stancl\Tenancy;
use Illuminate\Cache\CacheManager; use Illuminate\Cache\CacheManager;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper;
class TenancyServiceProvider extends ServiceProvider class TenancyServiceProvider extends ServiceProvider
{ {
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
$this->commands([
Commands\Run::class,
Commands\Seed::class,
Commands\Install::class,
Commands\Migrate::class,
Commands\Rollback::class,
Commands\TenantList::class,
]);
$this->publishes([
__DIR__ . '/../assets/config.php' => config_path('tenancy.php'),
], 'config');
$this->publishes([
__DIR__ . '/../assets/migrations/' => database_path('migrations'),
], 'migrations');
$this->loadRoutesFrom(__DIR__ . '/routes.php');
Route::middlewareGroup('tenancy', [
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
]);
$this->app->register(TenantRouteServiceProvider::class);
}
/** /**
* Register services. * Register services.
* *
* @return void * @return void
*/ */
public function register() public function register(): void
{ {
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy'); $this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
$this->app->bind(Contracts\StorageDriver::class, function ($app) { $this->app->bind(Contracts\StorageDriver::class, function ($app) {
return $app->make($app['config']['tenancy.storage_driver']); return $app->make($app['config']['tenancy.storage_drivers'][$app['config']['tenancy.storage_driver']]['driver']);
}); });
$this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.unique_id_generator']); $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.unique_id_generator']);
$this->app->singleton(DatabaseManager::class); $this->app->singleton(DatabaseManager::class);
@ -79,5 +47,54 @@ class TenancyServiceProvider extends ServiceProvider
$this->app->bind('globalCache', function ($app) { $this->app->bind('globalCache', function ($app) {
return new CacheManager($app); return new CacheManager($app);
}); });
$this->app->register(TenantRouteServiceProvider::class);
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot(): void
{
$this->commands([
Commands\Run::class,
Commands\Seed::class,
Commands\Install::class,
Commands\Migrate::class,
Commands\Rollback::class,
Commands\TenantList::class,
Commands\CreateTenant::class,
Commands\MigrateFresh::class,
]);
$this->publishes([
__DIR__ . '/../assets/config.php' => config_path('tenancy.php'),
], 'config');
$this->publishes([
__DIR__ . '/../assets/migrations/' => database_path('migrations'),
], 'migrations');
$this->loadRoutesFrom(__DIR__ . '/routes.php');
Route::middlewareGroup('tenancy', [
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
]);
$this->app->singleton('globalUrl', function ($app) {
$instance = clone $app['url'];
$instance->setAssetRoot($app[FilesystemTenancyBootstrapper::class]->originalPaths['asset_url']);
return $instance;
});
// Queue tenancy
$this->app['events']->listen(\Illuminate\Queue\Events\JobProcessing::class, function ($event) {
if (array_key_exists('tenant_id', $event->job->payload())) {
tenancy()->initialize(tenancy()->find($event->job->payload()['tenant_id']));
}
});
} }
} }

View file

@ -6,19 +6,19 @@ namespace Stancl\Tenancy;
use ArrayAccess; use ArrayAccess;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ForwardsCalls;
use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
use Stancl\Tenancy\Exceptions\TenantStorageException; use Stancl\Tenancy\Exceptions\TenantStorageException;
// todo2 write tests for updating the tenant
// todo2 addDomain(), removeDomain()
/** /**
* @internal Class is subject to breaking changes in minor and patch versions. * @internal Class is subject to breaking changes in minor and patch versions.
*/ */
class Tenant implements ArrayAccess class Tenant implements ArrayAccess
{ {
use Traits\HasArrayAccess; use Traits\HasArrayAccess,
ForwardsCalls;
/** /**
* Tenant data. A "cache" of tenant storage. * Tenant data. A "cache" of tenant storage.
@ -51,8 +51,16 @@ class Tenant implements ArrayAccess
* *
* @var bool * @var bool
*/ */
protected $persisted = false; public $persisted = false;
/**
* Use new() if you don't want to swap dependencies.
*
* @param Application $app
* @param StorageDriver $storage
* @param TenantManager $tenantManager
* @param UniqueIdentifierGenerator $idGenerator
*/
public function __construct(Application $app, StorageDriver $storage, TenantManager $tenantManager, UniqueIdentifierGenerator $idGenerator) public function __construct(Application $app, StorageDriver $storage, TenantManager $tenantManager, UniqueIdentifierGenerator $idGenerator)
{ {
$this->app = $app; $this->app = $app;
@ -61,6 +69,12 @@ class Tenant implements ArrayAccess
$this->idGenerator = $idGenerator; $this->idGenerator = $idGenerator;
} }
/**
* Public constructor.
*
* @param Application $app
* @return self
*/
public static function new(Application $app = null): self public static function new(Application $app = null): self
{ {
$app = $app ?? app(); $app = $app ?? app();
@ -73,27 +87,49 @@ class Tenant implements ArrayAccess
); );
} }
/**
* DO NOT CALL THIS METHOD FROM USERLAND. Used by storage
* drivers to create persisted instances of Tenant.
*
* @param array $data
* @return self
*/
public static function fromStorage(array $data): self public static function fromStorage(array $data): self
{ {
return static::new()->withData($data)->persisted(true); return static::new()->withData($data)->persisted(true);
} }
/**
* Create a tenant in a single call.
*
* @param string|string[] $domains
* @param array $data
* @return self
*/
public static function create($domains, array $data = []): self public static function create($domains, array $data = []): self
{ {
return static::new()->withDomains((array) $domains)->withData($data)->save(); return static::new()->withDomains((array) $domains)->withData($data)->save();
} }
protected function persisted($persisted = null) /**
* DO NOT CALL THIS METHOD FROM USERLAND UNLESS YOU KNOW WHAT YOU ARE DOING.
* Set $persisted.
*
* @param bool $persisted
* @return self
*/
public function persisted(bool $persisted): self
{ {
if (gettype($persisted) === 'boolean') { $this->persisted = $persisted;
$this->persisted = $persisted;
return $this;
}
return $this; return $this;
} }
/**
* Does this model exist in the tenant storage.
*
* @return bool
*/
public function isPersisted(): bool public function isPersisted(): bool
{ {
return $this->persisted; return $this->persisted;
@ -127,6 +163,11 @@ class Tenant implements ArrayAccess
return $this; return $this;
} }
/**
* Unassign all domains from the tenant.
*
* @return self
*/
public function clearDomains(): self public function clearDomains(): self
{ {
$this->domains = []; $this->domains = [];
@ -134,6 +175,12 @@ class Tenant implements ArrayAccess
return $this; return $this;
} }
/**
* Set (overwrite) the tenant's domains.
*
* @param string|string[] $domains
* @return self
*/
public function withDomains($domains): self public function withDomains($domains): self
{ {
$domains = (array) $domains; $domains = (array) $domains;
@ -143,6 +190,12 @@ class Tenant implements ArrayAccess
return $this; return $this;
} }
/**
* Set (overwrite) tenant data.
*
* @param array $data
* @return self
*/
public function withData(array $data): self public function withData(array $data): self
{ {
$this->data = $data; $this->data = $data;
@ -150,11 +203,21 @@ class Tenant implements ArrayAccess
return $this; return $this;
} }
/**
* Generate a random ID.
*
* @return void
*/
public function generateId() public function generateId()
{ {
$this->id = $this->idGenerator->generate($this->domains, $this->data); $this->id = $this->idGenerator->generate($this->domains, $this->data);
} }
/**
* Write the tenant's state to storage.
*
* @return self
*/
public function save(): self public function save(): self
{ {
if (! isset($this->data['id'])) { if (! isset($this->data['id'])) {
@ -188,7 +251,7 @@ class Tenant implements ArrayAccess
} }
/** /**
* Unassign all domains from the tenant. * Unassign all domains from the tenant and write to storage.
* *
* @return self * @return self
*/ */
@ -201,12 +264,22 @@ class Tenant implements ArrayAccess
return $this; return $this;
} }
public function getDatabaseName() /**
* Get the tenant's database's name.
*
* @return string
*/
public function getDatabaseName(): string
{ {
return $this->data['_tenancy_db_name'] ?? ($this->app['config']['tenancy.database.prefix'] . $this->id . $this->app['config']['tenancy.database.suffix']); return $this->data['_tenancy_db_name'] ?? ($this->app['config']['tenancy.database.prefix'] . $this->id . $this->app['config']['tenancy.database.suffix']);
} }
public function getConnectionName() /**
* Get the tenant's database connection's name.
*
* @return string
*/
public function getConnectionName(): string
{ {
return $this->data['_tenancy_db_connection'] ?? 'tenant'; return $this->data['_tenancy_db_connection'] ?? 'tenant';
} }
@ -243,6 +316,13 @@ class Tenant implements ArrayAccess
return $this->data[$key]; return $this->data[$key];
} }
/**
* Set a value and write to storage.
*
* @param string|array<string, mixed> $key
* @param mixed $value
* @return self
*/
public function put($key, $value = null): self public function put($key, $value = null): self
{ {
if ($key === 'id') { if ($key === 'id') {
@ -268,6 +348,20 @@ class Tenant implements ArrayAccess
return $this->put($key, $value); return $this->put($key, $value);
} }
/**
* Set a value.
*
* @param string $key
* @param mixed $value
* @return self
*/
public function with(string $key, $value): self
{
$this->data[$key] = $value;
return $this;
}
public function __get($key) public function __get($key)
{ {
return $this->get($key); return $this->get($key);
@ -278,6 +372,16 @@ class Tenant implements ArrayAccess
if ($key === 'id' && isset($this->data['id'])) { if ($key === 'id' && isset($this->data['id'])) {
throw new TenantStorageException("Tenant ids can't be changed."); throw new TenantStorageException("Tenant ids can't be changed.");
} }
$this->data[$key] = $value; $this->data[$key] = $value;
} }
public function __call($method, $parameters)
{
if (Str::startsWith($method, 'with')) {
return $this->with(Str::snake(substr($method, 4)), $parameters[0]);
}
static::throwBadMethodCallException($method);
}
} }

View file

@ -4,23 +4,32 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenantDatabaseManagers; namespace Stancl\Tenancy\TenantDatabaseManagers;
use Illuminate\Support\Facades\DB; use Illuminate\Config\Repository;
use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
class MySQLDatabaseManager implements TenantDatabaseManager class MySQLDatabaseManager implements TenantDatabaseManager
{ {
/** @var \Illuminate\Database\Connection */
protected $database;
public function __construct(Repository $config, IlluminateDatabaseManager $databaseManager)
{
$this->database = $databaseManager->connection($config['tenancy.database_manager_connections.mysql']);
}
public function createDatabase(string $name): bool public function createDatabase(string $name): bool
{ {
return DB::statement("CREATE DATABASE `$name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); return $this->database->statement("CREATE DATABASE `$name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
} }
public function deleteDatabase(string $name): bool public function deleteDatabase(string $name): bool
{ {
return DB::statement("DROP DATABASE `$name`"); return $this->database->statement("DROP DATABASE `$name`");
} }
public function databaseExists(string $name): bool public function databaseExists(string $name): bool
{ {
return (bool) DB::select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); return (bool) $this->database->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'");
} }
} }

View file

@ -4,23 +4,32 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenantDatabaseManagers; namespace Stancl\Tenancy\TenantDatabaseManagers;
use Illuminate\Support\Facades\DB; use Illuminate\Config\Repository;
use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager;
use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantDatabaseManager;
class PostgreSQLDatabaseManager implements TenantDatabaseManager class PostgreSQLDatabaseManager implements TenantDatabaseManager
{ {
/** @var \Illuminate\Database\Connection */
protected $database;
public function __construct(Repository $config, IlluminateDatabaseManager $databaseManager)
{
$this->database = $databaseManager->connection($config['tenancy.database_manager_connections.pgsql']);
}
public function createDatabase(string $name): bool public function createDatabase(string $name): bool
{ {
return DB::statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0"); return $this->database->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0");
} }
public function deleteDatabase(string $name): bool public function deleteDatabase(string $name): bool
{ {
return DB::statement("DROP DATABASE \"$name\""); return $this->database->statement("DROP DATABASE \"$name\"");
} }
public function databaseExists(string $name): bool public function databaseExists(string $name): bool
{ {
return (bool) DB::select("SELECT datname FROM pg_database WHERE datname = '$name'"); return (bool) $this->database->select("SELECT datname FROM pg_database WHERE datname = '$name'");
} }
} }

View file

@ -9,6 +9,7 @@ use Illuminate\Foundation\Application;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
/** /**
* @internal Class is subject to breaking changes in minor and patch versions. * @internal Class is subject to breaking changes in minor and patch versions.
@ -50,6 +51,12 @@ class TenantManager
$this->bootstrapFeatures(); $this->bootstrapFeatures();
} }
/**
* Write a new tenant to storage.
*
* @param Tenant $tenant
* @return self
*/
public function createTenant(Tenant $tenant): self public function createTenant(Tenant $tenant): self
{ {
$this->ensureTenantCanBeCreated($tenant); $this->ensureTenantCanBeCreated($tenant);
@ -58,14 +65,24 @@ class TenantManager
$this->database->createDatabase($tenant); $this->database->createDatabase($tenant);
if ($this->shouldMigrateAfterCreation()) { if ($this->shouldMigrateAfterCreation()) {
$this->artisan->call('tenants:migrate', [ if ($this->shouldQueueMigration()) {
'--tenants' => [$tenant['id']], QueuedTenantDatabaseMigrator::dispatch($tenant);
]); } else {
$this->artisan->call('tenants:migrate', [
'--tenants' => [$tenant['id']],
]);
}
} }
return $this; return $this;
} }
/**
* Delete a tenant from storage.
*
* @param Tenant $tenant
* @return self
*/
public function deleteTenant(Tenant $tenant): self public function deleteTenant(Tenant $tenant): self
{ {
$this->storage->deleteTenant($tenant); $this->storage->deleteTenant($tenant);
@ -77,6 +94,13 @@ class TenantManager
return $this; return $this;
} }
/**
* Alias for Stancl\Tenancy\Tenant::create.
*
* @param string|string[] $domains
* @param array $data
* @return Tenant
*/
public static function create($domains, array $data = []): Tenant public static function create($domains, array $data = []): Tenant
{ {
return Tenant::create($domains, $data); return Tenant::create($domains, $data);
@ -95,6 +119,12 @@ class TenantManager
$this->database->ensureTenantCanBeCreated($tenant); $this->database->ensureTenantCanBeCreated($tenant);
} }
/**
* Update an existing tenant in storage.
*
* @param Tenant $tenant
* @return self
*/
public function updateTenant(Tenant $tenant): self public function updateTenant(Tenant $tenant): self
{ {
$this->storage->updateTenant($tenant); $this->storage->updateTenant($tenant);
@ -102,6 +132,12 @@ class TenantManager
return $this; return $this;
} }
/**
* Find tenant by domain & initialize tenancy.
*
* @param string|null $domain
* @return self
*/
public function init(string $domain = null): self public function init(string $domain = null): self
{ {
$domain = $domain ?? request()->getHost(); $domain = $domain ?? request()->getHost();
@ -110,6 +146,12 @@ class TenantManager
return $this; return $this;
} }
/**
* Find tenant by ID & initialize tenancy.
*
* @param string $id
* @return self
*/
public function initById(string $id): self public function initById(string $id): self
{ {
$this->initializeTenancy($this->find($id)); $this->initializeTenancy($this->find($id));
@ -156,6 +198,12 @@ class TenantManager
return collect($this->storage->all($only)); return collect($this->storage->all($only));
} }
/**
* Initialize tenancy.
*
* @param Tenant $tenant
* @return self
*/
public function initializeTenancy(Tenant $tenant): self public function initializeTenancy(Tenant $tenant): self
{ {
$this->setTenant($tenant); $this->setTenant($tenant);
@ -171,6 +219,12 @@ class TenantManager
return $this->initializeTenancy($tenant); return $this->initializeTenancy($tenant);
} }
/**
* Execute TenancyBootstrappers.
*
* @param Tenant $tenant
* @return self
*/
public function bootstrapTenancy(Tenant $tenant): self public function bootstrapTenancy(Tenant $tenant): self
{ {
$prevented = $this->event('bootstrapping'); $prevented = $this->event('bootstrapping');
@ -257,6 +311,11 @@ class TenantManager
return $this->app['config']['tenancy.migrate_after_creation'] ?? false; return $this->app['config']['tenancy.migrate_after_creation'] ?? false;
} }
public function shouldQueueMigration(): bool
{
return $this->app['config']['tenancy.queue_automatic_migration'] ?? false;
}
public function shouldDeleteDatabase(): bool public function shouldDeleteDatabase(): bool
{ {
return $this->app['config']['tenancy.delete_database_after_tenant_deletion'] ?? false; return $this->app['config']['tenancy.delete_database_after_tenant_deletion'] ?? false;
@ -277,6 +336,19 @@ class TenantManager
return $this; return $this;
} }
/**
* Add an event hook.
* @alias eventListener
*
* @param string $name
* @param callable $listener
* @return self
*/
public function hook(string $name, callable $listener): self
{
return $this->eventListener($name, $listener);
}
/** /**
* Execute event listeners. * Execute event listeners.
* *

View file

@ -11,8 +11,8 @@ class TenantRouteServiceProvider extends RouteServiceProvider
{ {
public function map() public function map()
{ {
if (! \in_array(request()->getHost(), $this->app['config']['tenancy.exempt_domains'] ?? []) if (! in_array(request()->getHost(), $this->app['config']['tenancy.exempt_domains'] ?? [])
&& \file_exists(base_path('routes/tenant.php'))) { && file_exists(base_path('routes/tenant.php'))) {
Route::middleware(['web', 'tenancy']) Route::middleware(['web', 'tenancy'])
->namespace($this->app['config']['tenant_route_namespace'] ?? 'App\Http\Controllers') ->namespace($this->app['config']['tenant_route_namespace'] ?? 'App\Http\Controllers')
->group(base_path('routes/tenant.php')); ->group(base_path('routes/tenant.php'));

View file

@ -10,7 +10,7 @@ trait HasATenantsOption
{ {
protected function getOptions() protected function getOptions()
{ {
return \array_merge([ return array_merge([
['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null], ['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null],
], parent::getOptions()); ], parent::getOptions());
} }

View file

@ -2,14 +2,15 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy; namespace Stancl\Tenancy\UniqueIDGenerators;
use Ramsey\Uuid\Uuid;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
class UUIDGenerator implements UniqueIdentifierGenerator class UUIDGenerator implements UniqueIdentifierGenerator
{ {
public static function generate(array $domains, array $data = []): string public static function generate(array $domains, array $data = []): string
{ {
return (string) \Webpatser\Uuid\Uuid::generate(1, $domains[0] ?? ''); return Uuid::uuid4()->toString();
} }
} }

View file

@ -20,7 +20,7 @@ if (! \function_exists('tenant')) {
function tenant($key = null) function tenant($key = null)
{ {
if (! is_null($key)) { if (! is_null($key)) {
return app(Tenant::class)->get($key); return optional(app(Tenant::class))->get($key) ?? null;
} }
return app(Tenant::class); return app(Tenant::class);
@ -30,6 +30,20 @@ if (! \function_exists('tenant')) {
if (! \function_exists('tenant_asset')) { if (! \function_exists('tenant_asset')) {
function tenant_asset($asset) function tenant_asset($asset)
{ {
return route('stancl.tenancy.asset', ['asset' => $asset]); return route('stancl.tenancy.asset', ['path' => $asset]);
}
}
if (! \function_exists('global_asset')) {
function global_asset($asset)
{
return app('globalUrl')->asset($asset);
}
}
if (! \function_exists('global_cache')) {
function global_cache()
{
return app('globalCache');
} }
} }

3
test
View file

@ -1,10 +1,7 @@
#!/bin/bash #!/bin/bash
set -e set -e
# for development
docker-compose up -d
printf "Variant 1\n\n" printf "Variant 1\n\n"
docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/2.cov "$@" docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/2.cov "$@"
printf "Variant 2\n\n" printf "Variant 2\n\n"
docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/1.cov "$@" docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/1.cov "$@"
docker-compose exec test vendor/bin/phpcov merge --clover clover.xml coverage/

View file

@ -117,190 +117,55 @@ class CommandsTest extends TestCase
->expectsOutput('xyz'); ->expectsOutput('xyz');
} }
// todo2 check that multiple tenants can be migrated at once using all database engines
/** @test */ /** @test */
public function install_command_works() public function install_command_works()
{ {
if (! \is_dir($dir = app_path('Http'))) { if (! is_dir($dir = app_path('Http'))) {
\mkdir($dir, 0777, true); mkdir($dir, 0777, true);
} }
if (! \is_dir($dir = base_path('routes'))) { if (! is_dir($dir = base_path('routes'))) {
\mkdir($dir, 0777, true); mkdir($dir, 0777, true);
} }
// todo2 move this to a file file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernel.stub'));
\file_put_contents(app_path('Http/Kernel.php'), "<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected \$middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected \$middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected \$routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected \$middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}
");
$this->artisan('tenancy:install') $this->artisan('tenancy:install')
->expectsQuestion('Do you want to publish the default database migration?', 'yes'); ->expectsQuestion('Do you want to publish the default database migrations?', 'yes');
$this->assertFileExists(base_path('routes/tenant.php')); $this->assertFileExists(base_path('routes/tenant.php'));
$this->assertFileExists(base_path('config/tenancy.php')); $this->assertFileExists(base_path('config/tenancy.php'));
$this->assertFileExists(database_path('migrations/2019_08_08_000000_create_tenants_table.php')); $this->assertFileExists(database_path('migrations/2019_09_15_000010_create_tenants_table.php'));
$this->assertFileExists(database_path('migrations/2019_09_15_000020_create_domains_table.php'));
$this->assertDirectoryExists(database_path('migrations/tenant')); $this->assertDirectoryExists(database_path('migrations/tenant'));
$this->assertSame("<?php $this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernel.stub'), file_get_contents(app_path('Http/Kernel.php')));
}
namespace App\Http; /** @test */
public function migrate_fresh_command_works()
{
$this->assertFalse(Schema::hasTable('users'));
Artisan::call('tenants:migrate-fresh');
$this->assertFalse(Schema::hasTable('users'));
tenancy()->init('test.localhost');
$this->assertTrue(Schema::hasTable('users'));
use Illuminate\Foundation\Http\Kernel as HttpKernel; $this->assertFalse(DB::table('users')->exists());
DB::table('users')->insert(['name' => 'xxx', 'password' => bcrypt('password'), 'email' => 'foo@bar.xxx']);
$this->assertTrue(DB::table('users')->exists());
class Kernel extends HttpKernel // test that db is wiped
{ Artisan::call('tenants:migrate-fresh');
/** $this->assertFalse(DB::table('users')->exists());
* The application's global HTTP middleware stack. }
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected \$middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/** /** @test */
* The application's route middleware groups. public function create_command_works()
* {
* @var array Artisan::call('tenants:create -d aaa.localhost -d bbb.localhost plan=free email=foo@test.local');
*/ $tenant = tenancy()->all()[1]; // a tenant is autocreated prior to this
protected \$middlewareGroups = [ $data = $tenant->data;
'web' => [ unset($data['id']);
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [ $this->assertSame(['plan' => 'free', 'email' => 'foo@test.local'], $data);
'throttle:60,1', $this->assertSame(['aaa.localhost', 'bbb.localhost'], $tenant->domains);
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected \$routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected \$middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}
", \file_get_contents(app_path('Http/Kernel.php')));
} }
} }

View file

@ -25,10 +25,24 @@ class DatabaseManagerTest extends TestCase
/** @test */ /** @test */
public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() public function db_name_is_prefixed_with_db_path_when_sqlite_is_used()
{ {
// make `tenant` not sqlite so that it has to detect sqlite from fooconn config(['database.connections.fooconn.driver' => 'sqlite']);
config(['database.connections.tenant.driver' => 'mysql']);
app(DatabaseManager::class)->createTenantConnection('foodb', 'fooconn'); app(DatabaseManager::class)->createTenantConnection('foodb', 'fooconn');
$this->assertSame(config('database.connections.fooconn.database'), database_path('foodb')); $this->assertSame(config('database.connections.fooconn.database'), database_path('foodb'));
} }
/** @test */
public function the_default_db_is_used_when_based_on_is_null()
{
$this->assertSame('sqlite', config('database.default'));
config([
'database.connections.sqlite.foo' => 'bar',
'tenancy.database.based_on' => null,
]);
tenancy()->init('test.localhost');
$this->assertSame('tenant', config('database.default'));
$this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo'));
}
} }

View file

@ -0,0 +1,80 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}

View file

@ -0,0 +1,83 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}

View file

@ -4,12 +4,19 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests;
use Tenancy;
use Tenant; use Tenant;
class FacadeTest extends TestCase class FacadeTest extends TestCase
{ {
/** @test */ /** @test */
public function tenant_manager_can_be_accessed_using_the_Tenant_facade() public function tenant_manager_can_be_accessed_using_the_Tenancy_facade()
{
$this->assertSame(tenancy()->getTenant(), Tenancy::getTenant());
}
/** @test */
public function tenant_storage_can_be_accessed_using_the_Tenant_facade()
{ {
tenant()->put('foo', 'bar'); tenant()->put('foo', 'bar');
Tenant::put('abc', 'xyz'); Tenant::put('abc', 'xyz');
@ -17,4 +24,10 @@ class FacadeTest extends TestCase
$this->assertSame('bar', Tenant::get('foo')); $this->assertSame('bar', Tenant::get('foo'));
$this->assertSame('xyz', Tenant::get('abc')); $this->assertSame('xyz', Tenant::get('abc'));
} }
/** @test */
public function tenant_can_be_created_using_the_Tenant_facade()
{
$this->assertSame('bar', Tenant::create(['foo.localhost'], ['foo' => 'bar'])->foo);
}
} }

View file

@ -17,7 +17,7 @@ class QueueTest extends TestCase
/** @test */ /** @test */
public function queues_use_non_tenant_db_connection() public function queues_use_non_tenant_db_connection()
{ {
// todo2 finish this test. requires using the db driver // requires using the db driver
$this->markTestIncomplete(); $this->markTestIncomplete();
} }
@ -56,6 +56,6 @@ class TestJob implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
logger(\json_encode(\DB::table('users')->get())); logger(json_encode(\DB::table('users')->get()));
} }
} }

View file

@ -31,7 +31,7 @@ class ReidentificationTest extends TestCase
$current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix(); $current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($override = config("tenancy.filesystem.root_override.{$disk}")) { if ($override = config("tenancy.filesystem.root_override.{$disk}")) {
$correct_path_prefix = \str_replace('%storage_path%', storage_path(), $override); $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override);
} else { } else {
if ($base = $originals[$disk]) { if ($base = $originals[$disk]) {
$correct_path_prefix = $base . "/$suffix/"; $correct_path_prefix = $base . "/$suffix/";

View file

@ -6,8 +6,7 @@ namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
// todo2 rename class TenancyBootstrappersTest extends TestCase
class BootstrapsTenancyTest extends TestCase
{ {
public $autoInitTenancy = false; public $autoInitTenancy = false;
@ -56,7 +55,7 @@ class BootstrapsTenancyTest extends TestCase
$current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix(); $current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($override = config("tenancy.filesystem.root_override.{$disk}")) { if ($override = config("tenancy.filesystem.root_override.{$disk}")) {
$correct_path_prefix = \str_replace('%storage_path%', storage_path(), $override); $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override);
} else { } else {
if ($base = $old_storage_facade_roots[$disk]) { if ($base = $old_storage_facade_roots[$disk]) {
$correct_path_prefix = $base . "/$suffix/"; $correct_path_prefix = $base . "/$suffix/";
@ -78,4 +77,20 @@ class BootstrapsTenancyTest extends TestCase
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
$this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames());
} }
/** @test */
public function the_default_db_connection_is_used_when_the_config_value_is_null()
{
$original = config('database.default');
tenancy()->create(['foo.localhost']);
tenancy()->init('foo.localhost');
$this->assertSame(null, config("database.connections.$original.foo"));
config(["database.connections.$original.foo" => 'bar']);
tenancy()->create(['bar.localhost']);
tenancy()->init('bar.localhost');
$this->assertSame('bar', config("database.connections.$original.foo"));
}
} }

View file

@ -4,25 +4,68 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests;
use Stancl\Tenancy\Tenant;
class TenantAssetTest extends TestCase class TenantAssetTest extends TestCase
{ {
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */ /** @test */
public function asset_can_be_accessed_using_the_url_returned_by_the_tenant_asset_helper() public function asset_can_be_accessed_using_the_url_returned_by_the_tenant_asset_helper()
{ {
Tenant::create('localhost');
tenancy()->init('localhost');
$filename = 'testfile' . $this->randomString(10); $filename = 'testfile' . $this->randomString(10);
\Storage::disk('public')->put($filename, 'bar'); \Storage::disk('public')->put($filename, 'bar');
$path = storage_path("app/public/$filename"); $path = storage_path("app/public/$filename");
// response()->file() returns BinaryFileResponse whose content is // response()->file() returns BinaryFileResponse whose content is
// inaccessible via getContent, so ->assertSee() can't be used // inaccessible via getContent, so ->assertSee() can't be used
// $this->get(tenant_asset($filename))->assertSuccessful(); // TODO2 COMMENTED ASSERTIONS $this->assertFileExists($path);
// $this->assertFileExists($path); // TODO2 COMMENTED ASSERTIONS $response = $this->get(tenant_asset($filename));
$f = \fopen($path, 'r'); $response->assertSuccessful();
$content = \fread($f, \filesize($path));
\fclose($f);
// $this->assertSame('bar', $content); // TODO2 COMMENTED ASSERTIONS $f = fopen($path, 'r');
$this->assertTrue(true); // TODO2 COMMENTED ASSERTIONS $content = fread($f, filesize($path));
fclose($f);
$this->assertSame('bar', $content);
}
/** @test */
public function asset_helper_returns_a_link_to_TenantAssetController_when_asset_url_is_null()
{
config(['app.asset_url' => null]);
Tenant::create('foo.localhost');
tenancy()->init('foo.localhost');
$this->assertSame(route('stancl.tenancy.asset', ['path' => 'foo']), asset('foo'));
}
/** @test */
public function asset_helper_returns_a_link_to_an_external_url_when_asset_url_is_not_null()
{
config(['app.asset_url' => 'https://an-s3-bucket']);
$tenant = Tenant::create(['foo.localhost']);
tenancy()->init('foo.localhost');
$this->assertSame("https://an-s3-bucket/tenant{$tenant->id}/foo", asset('foo'));
}
/** @test */
public function global_asset_helper_returns_the_same_url_regardless_of_tenancy_initialization()
{
$original = global_asset('foobar');
$this->assertSame(asset('foobar'), global_asset('foobar'));
Tenant::create(['foo.localhost']);
tenancy()->init('foo.localhost');
$this->assertSame($original, global_asset('foobar'));
} }
} }

108
tests/TenantClassTest.php Normal file
View file

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Mockery;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Tenant;
use Tenancy;
class TenantClassTest extends TestCase
{
public $autoInitTenancy = false;
public $autoCreateTenant = false;
/** @test */
public function data_cache_works_properly()
{
// $spy = Mockery::spy(config('tenancy.storage_driver'))->makePartial();
// $this->instance(StorageDriver::class, $spy);
$tenant = Tenant::create(['foo.localhost'], ['foo' => 'bar']);
$this->assertSame('bar', $tenant->data['foo']);
$tenant->put('abc', 'xyz');
$this->assertSame('xyz', $tenant->data['abc']);
$tenant->put(['aaa' => 'bbb', 'ccc' => 'ddd']);
$this->assertSame('bbb', $tenant->data['aaa']);
$this->assertSame('ddd', $tenant->data['ccc']);
// $spy->shouldNotHaveReceived('get');
$this->assertSame(null, $tenant->dfuighdfuigfhdui);
// $spy->shouldHaveReceived('get')->once();
Mockery::close();
}
/** @test */
public function tenant_can_have_multiple_domains()
{
$tenant = Tenant::create(['foo.localhost', 'bar.localhost']);
$this->assertSame(['foo.localhost', 'bar.localhost'], $tenant->domains);
$this->assertSame($tenant->id, Tenancy::findByDomain('foo.localhost')->id);
$this->assertSame($tenant->id, Tenancy::findByDomain('bar.localhost')->id);
}
/** @test */
public function updating_a_tenant_works()
{
$id = 'abc' . $this->randomString();
$tenant = Tenant::create(['foo.localhost'], ['id' => $id]);
$tenant->foo = 'bar';
$tenant->save();
$this->assertEquals(['id' => $id, 'foo' => 'bar'], $tenant->data);
$this->assertEquals(['id' => $id, 'foo' => 'bar'], tenancy()->find($id)->data);
$tenant->addDomains('abc.localhost');
$tenant->save();
$this->assertEqualsCanonicalizing(['foo.localhost', 'abc.localhost'], $tenant->domains);
$this->assertEqualsCanonicalizing(['foo.localhost', 'abc.localhost'], tenancy()->find($id)->domains);
$tenant->removeDomains(['foo.localhost']);
$tenant->save();
$this->assertEqualsCanonicalizing(['abc.localhost'], $tenant->domains);
$this->assertEqualsCanonicalizing(['abc.localhost'], tenancy()->find($id)->domains);
$tenant->withDomains(['completely.localhost', 'different.localhost', 'domains.localhost']);
$tenant->save();
$this->assertEqualsCanonicalizing(['completely.localhost', 'different.localhost', 'domains.localhost'], $tenant->domains);
$this->assertEqualsCanonicalizing(['completely.localhost', 'different.localhost', 'domains.localhost'], tenancy()->find($id)->domains);
}
/** @test */
public function with_methods_work()
{
$id = 'foo' . $this->randomString();
$tenant = Tenant::new()->withDomains(['foo.localhost'])->with('id', $id);
$this->assertSame($id, $tenant->id);
$id2 = 'bar' . $this->randomString();
$tenant2 = Tenant::new()->withDomains(['bar.localhost'])->withId($id2)->withFooBar('xyz');
$this->assertSame($id2, $tenant2->data['id']);
$this->assertSame('xyz', $tenant2->foo_bar);
$this->assertArrayHasKey('foo_bar', $tenant2->data);
}
/** @test */
public function an_exception_is_thrown_when_an_unknown_method_is_called()
{
$tenant = Tenant::new();
$this->expectException(\BadMethodCallException::class);
$tenant->sdjigndfgnjdfgj();
}
/** @test */
public function tenant_data_can_be_set_during_creation()
{
Tenant::new()->withData(['foo' => 'bar'])->save();
$data = tenancy()->all()->first()->data;
unset($data['id']);
$this->assertSame(['foo' => 'bar'], $data);
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
class TenantConfigTest extends TestCase
{
public $autoInitTenancy = false;
public $autoCreateTenant = false;
/** @test */
public function config_is_merged_and_removed()
{
$this->assertSame(null, config('services.paypal'));
config([
'tenancy.storage_to_config_map' => [
'paypal_api_public' => 'services.paypal.public',
'paypal_api_private' => 'services.paypal.private',
],
'tenancy.features' => ['Stancl\Tenancy\Features\TenantConfig'],
]);
tenancy()->create('foo.localhost', [
'paypal_api_public' => 'foo',
'paypal_api_private' => 'bar',
]);
tenancy()->init('foo.localhost');
$this->assertSame(['public' => 'foo', 'private' => 'bar'], config('services.paypal'));
tenancy()->end();
$this->assertSame([
'public' => null,
'private' => null,
], config('services.paypal'));
}
}

View file

@ -14,6 +14,8 @@ use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;
class TenantDatabaseManagerTest extends TestCase class TenantDatabaseManagerTest extends TestCase
{ {
public $autoInitTenancy = false;
/** /**
* @test * @test
* @dataProvider database_manager_provider * @dataProvider database_manager_provider
@ -24,8 +26,6 @@ class TenantDatabaseManagerTest extends TestCase
$this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.'); $this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.');
} }
config()->set('database.default', $driver); // todo the DB creator would not work for MySQL when sqlite is used for the central DB
$name = 'db' . $this->randomString(); $name = 'db' . $this->randomString();
$this->assertFalse(app($databaseManager)->databaseExists($name)); $this->assertFalse(app($databaseManager)->databaseExists($name));
app($databaseManager)->createDatabase($name); app($databaseManager)->createDatabase($name);
@ -34,6 +34,20 @@ class TenantDatabaseManagerTest extends TestCase
$this->assertFalse(app($databaseManager)->databaseExists($name)); $this->assertFalse(app($databaseManager)->databaseExists($name));
} }
/** @test */
public function dbs_can_be_created_when_another_driver_is_used_for_the_central_db()
{
$this->assertSame('sqlite', config('database.default'));
$database = 'db' . $this->randomString();
app(MySQLDatabaseManager::class)->createDatabase($database);
$this->assertTrue(app(MySQLDatabaseManager::class)->databaseExists($database));
$database = 'db2' . $this->randomString();
app(PostgreSQLDatabaseManager::class)->createDatabase($database);
$this->assertTrue(app(PostgreSQLDatabaseManager::class)->databaseExists($database));
}
/** /**
* @test * @test
* @dataProvider database_manager_provider * @dataProvider database_manager_provider

View file

@ -5,8 +5,13 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager;
class TenantManagerTest extends TestCase class TenantManagerTest extends TestCase
{ {
@ -147,7 +152,7 @@ class TenantManagerTest extends TestCase
{ {
$tenant1 = Tenant::new()->withDomains(['foo.localhost'])->save(); $tenant1 = Tenant::new()->withDomains(['foo.localhost'])->save();
$tenant2 = Tenant::new()->withDomains(['bar.localhost'])->save(); $tenant2 = Tenant::new()->withDomains(['bar.localhost'])->save();
$this->assertEquals([$tenant1, $tenant2], tenancy()->all()->toArray()); $this->assertEqualsCanonicalizing([$tenant1, $tenant2], tenancy()->all()->toArray());
} }
/** @test */ /** @test */
@ -187,4 +192,78 @@ class TenantManagerTest extends TestCase
$this->expectException(\Stancl\Tenancy\Exceptions\TenantStorageException::class); $this->expectException(\Stancl\Tenancy\Exceptions\TenantStorageException::class);
$tenant2->put('id', 'foo'); $tenant2->put('id', 'foo');
} }
/** @test */
public function all_returns_a_collection_of_tenant_objects()
{
Tenant::create('foo.localhost');
$this->assertSame('Tenant', class_basename(tenancy()->all()[0]));
}
/** @test */
public function Tenant_is_bound_correctly_to_the_service_container()
{
$this->assertSame(null, app(Tenant::class));
$tenant = Tenant::create(['foo.localhost']);
app(TenantManager::class)->initializeTenancy($tenant);
$this->assertSame($tenant->id, app(Tenant::class)->id);
$this->assertSame(app(Tenant::class), app(TenantManager::class)->getTenant());
app(TenantManager::class)->endTenancy();
$this->assertSame(app(Tenant::class), app(TenantManager::class)->getTenant());
}
/** @test */
public function id_can_be_supplied_during_creation()
{
$id = 'abc' . $this->randomString();
$this->assertSame($id, Tenant::create(['foo.localhost'], ['id' => $id])->id);
$this->assertTrue(tenancy()->all()->contains(function ($tenant) use ($id) {
return $tenant->id === $id;
}));
}
/** @test */
public function automatic_migrations_work()
{
$tenant = Tenant::create(['foo.localhost']);
tenancy()->initialize($tenant);
$this->assertFalse(\Schema::hasTable('users'));
config(['tenancy.migrate_after_creation' => true]);
$tenant2 = Tenant::create(['bar.localhost']);
tenancy()->initialize($tenant2);
$this->assertTrue(\Schema::hasTable('users'));
}
/** @test */
public function ensureTenantCanBeCreated_works()
{
$id = 'foo' . $this->randomString();
Tenant::create(['foo.localhost'], ['id' => $id]);
$this->expectException(DomainsOccupiedByOtherTenantException::class);
Tenant::create(['foo.localhost']);
$this->expectException(TenantWithThisIdAlreadyExistsException::class);
Tenant::create(['bar.localhost'], ['id' => $id]);
}
/** @test */
public function automigration_can_be_queued()
{
Queue::fake();
config([
'tenancy.migrate_after_creation' => true,
'tenancy.queue_automatic_migration' => true,
]);
$tenant = Tenant::new()->save();
tenancy()->initialize($tenant);
Queue::assertPushed(QueuedTenantDatabaseMigrator::class);
$this->assertFalse(\Schema::hasTable('users'));
(new QueuedTenantDatabaseMigrator($tenant))->handle();
$this->assertTrue(\Schema::hasTable('users'));
}
} }

View file

@ -5,12 +5,20 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests;
use Route; use Route;
use Stancl\Tenancy\Tenant;
class TenantRedirectMacroTest extends TestCase class TenantRedirectMacroTest extends TestCase
{ {
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */ /** @test */
public function tenant_redirect_macro_replaces_only_the_hostname() public function tenant_redirect_macro_replaces_only_the_hostname()
{ {
config([
'tenancy.features' => ['Stancl\Tenancy\Features\TenantRedirect'],
]);
Route::get('/foobar', function () { Route::get('/foobar', function () {
return 'Foo'; return 'Foo';
})->name('home'); })->name('home');
@ -19,6 +27,9 @@ class TenantRedirectMacroTest extends TestCase
return redirect()->route('home')->tenant('abcd'); return redirect()->route('home')->tenant('abcd');
}); });
Tenant::create('foo.localhost');
tenancy()->init('foo.localhost');
$this->get('/redirect') $this->get('/redirect')
->assertRedirect('http://abcd/foobar'); ->assertRedirect('http://abcd/foobar');
} }

View file

@ -16,9 +16,9 @@ class TenantStorageTest extends TestCase
{ {
$abc = Tenant::new()->withDomains(['abc.localhost'])->save(); $abc = Tenant::new()->withDomains(['abc.localhost'])->save();
$exists = function () use ($abc) { $exists = function () use ($abc) {
return tenancy()->all()->reduce(function ($result, $tenant) use ($abc) { return tenancy()->all()->contains(function ($tenant) use ($abc) {
return $result ?: $tenant->id === $abc->id; return $tenant->id === $abc->id;
}, false); });
}; };
$this->assertTrue($exists()); $this->assertTrue($exists());
@ -95,26 +95,26 @@ class TenantStorageTest extends TestCase
public function data_is_stored_with_correct_data_types() public function data_is_stored_with_correct_data_types()
{ {
tenant()->put('someBool', false); tenant()->put('someBool', false);
$this->assertSame('boolean', \gettype(tenant()->get('someBool'))); $this->assertSame('boolean', gettype(tenant()->get('someBool')));
$this->assertSame('boolean', \gettype(tenant()->get(['someBool'])['someBool'])); $this->assertSame('boolean', gettype(tenant()->get(['someBool'])['someBool']));
tenant()->put('someInt', 5); tenant()->put('someInt', 5);
$this->assertSame('integer', \gettype(tenant()->get('someInt'))); $this->assertSame('integer', gettype(tenant()->get('someInt')));
$this->assertSame('integer', \gettype(tenant()->get(['someInt'])['someInt'])); $this->assertSame('integer', gettype(tenant()->get(['someInt'])['someInt']));
tenant()->put('someDouble', 11.40); tenant()->put('someDouble', 11.40);
$this->assertSame('double', \gettype(tenant()->get('someDouble'))); $this->assertSame('double', gettype(tenant()->get('someDouble')));
$this->assertSame('double', \gettype(tenant()->get(['someDouble'])['someDouble'])); $this->assertSame('double', gettype(tenant()->get(['someDouble'])['someDouble']));
tenant()->put('string', 'foo'); tenant()->put('string', 'foo');
$this->assertSame('string', \gettype(tenant()->get('string'))); $this->assertSame('string', gettype(tenant()->get('string')));
$this->assertSame('string', \gettype(tenant()->get(['string'])['string'])); $this->assertSame('string', gettype(tenant()->get(['string'])['string']));
} }
/** @test */ /** @test */
public function tenant_model_uses_correct_connection() public function tenant_model_uses_correct_connection()
{ {
config(['tenancy.storage.db.connection' => 'foo']); config(['tenancy.storage_drivers.db.connection' => 'foo']);
$this->assertSame('foo', (new TenantModel)->getConnectionName()); $this->assertSame('foo', (new TenantModel)->getConnectionName());
} }
@ -149,7 +149,7 @@ class TenantStorageTest extends TestCase
]); ]);
config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
config(['tenancy.storage.db.custom_columns' => [ config(['tenancy.storage_drivers.db.custom_columns' => [
'foo', 'foo',
]]); ]]);
@ -159,6 +159,6 @@ class TenantStorageTest extends TestCase
tenant()->put(['foo' => 'bar', 'abc' => 'xyz']); tenant()->put(['foo' => 'bar', 'abc' => 'xyz']);
$this->assertSame(['bar', 'xyz'], tenant()->get(['foo', 'abc'])); $this->assertSame(['bar', 'xyz'], tenant()->get(['foo', 'abc']));
$this->assertSame('bar', \DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo); $this->assertSame('bar', DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo);
} }
} }

View file

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
use Stancl\Tenancy\StorageDrivers\RedisStorageDriver;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
abstract class TestCase extends \Orchestra\Testbench\TestCase abstract class TestCase extends \Orchestra\Testbench\TestCase
@ -27,7 +25,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
Redis::connection('cache')->flushdb(); Redis::connection('cache')->flushdb();
$this->loadMigrationsFrom([ $this->loadMigrationsFrom([
'--path' => \realpath(__DIR__ . '/../assets/migrations'), '--path' => realpath(__DIR__ . '/../assets/migrations'),
'--database' => 'central', '--database' => 'central',
]); ]);
config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
@ -59,11 +57,11 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
*/ */
protected function getEnvironmentSetUp($app) protected function getEnvironmentSetUp($app)
{ {
if (\file_exists(__DIR__ . '/../.env')) { if (file_exists(__DIR__ . '/../.env')) {
\Dotenv\Dotenv::create(__DIR__ . '/..')->load(); \Dotenv\Dotenv::create(__DIR__ . '/..')->load();
} }
\fclose(\fopen(database_path('central.sqlite'), 'w')); fclose(fopen(database_path('central.sqlite'), 'w'));
$app['config']->set([ $app['config']->set([
'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'), 'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'),
@ -99,17 +97,13 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'), 'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'),
'tenancy.redis.prefixed_connections' => ['default'], 'tenancy.redis.prefixed_connections' => ['default'],
'tenancy.migrations_directory' => database_path('../migrations'), 'tenancy.migrations_directory' => database_path('../migrations'),
'tenancy.storage_drivers.db.connection' => 'central',
'tenancy.bootstrappers.redis' => \Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class,
]); ]);
if (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'redis') { $app->singleton(\Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class);
$app['config']->set([
'tenancy.storage_driver' => RedisStorageDriver::class, $app['config']->set(['tenancy.storage_driver' => env('TENANCY_TEST_STORAGE_DRIVER', 'redis')]);
]);
} elseif (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'db') {
$app['config']->set([
'tenancy.storage_driver' => DatabaseStorageDriver::class,
]);
}
} }
protected function getPackageProviders($app) protected function getPackageProviders($app)
@ -152,7 +146,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
public function randomString(int $length = 10) public function randomString(int $length = 10)
{ {
return \substr(\str_shuffle(\str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', (int) (\ceil($length / \strlen($x))))), 1, $length); return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', (int) (ceil($length / strlen($x))))), 1, $length);
} }
public function isContainerized() public function isContainerized()
@ -162,6 +156,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
public function assertArrayIsSubset($subset, $array, string $message = ''): void public function assertArrayIsSubset($subset, $array, string $message = ''): void
{ {
parent::assertTrue(\array_intersect($subset, $array) == $subset, $message); parent::assertTrue(array_intersect($subset, $array) == $subset, $message);
} }
} }