1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 20:14: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"
- cat vendor/laravel/framework/src/Illuminate/Foundation/Application.php| grep 'const VERSION'
script: ./test
script: ./fulltest
after_success:
- 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)
[![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)
[![Travis CI build](https://travis-ci.com/stancl/tenancy.svg?branch=1.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)
[![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/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 replacing of Laravel classes (`Cache`, `Storage`, ...) with tenancy-aware classes
- :heavy_check_mark: Built-in tenant identification based on hostname (including second level domains)
\* depending on how you use the filesystem. 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
Documentation can be found here: https://tenancy.samuelstancl.me/docs/v2/
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);
return [
'storage_driver' => 'Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver',
'storage' => [
'db' => [ // Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver
'storage_driver' => 'db',
'storage_drivers' => [
'db' => [
'driver' => Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver::class,
'data_column' => 'data',
'custom_columns' => [
// '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',
],
],
@ -21,7 +27,7 @@ return [
// 'localhost',
],
'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',
'suffix' => '',
],
@ -35,7 +41,7 @@ return [
'cache' => [
'tag_base' => 'tenant',
],
'filesystem' => [ // https://stancl-tenancy.netlify.com/docs/filesystem-tenancy/
'filesystem' => [ // https://tenancy.samuelstancl.me/docs/v2/filesystem-tenancy/
'suffix_base' => 'tenant',
// Disks which should be suffixed with the suffix_base + tenant id.
'disks' => [
@ -51,29 +57,43 @@ return [
],
'database_managers' => [
// Tenant database managers handle the creation & deletion of tenant databases.
'sqlite' => 'Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager',
'mysql' => 'Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager',
'pgsql' => 'Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager',
'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
'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' => [
// Tenancy bootstrappers are executed when tenancy is initialized.
// Their responsibility is making Laravel features tenant-aware.
'database' => 'Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper',
'cache' => 'Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper',
'filesystem' => 'Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper',
'redis' => 'Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper',
'queue' => 'Stancl\Tenancy\TenancyBootstrappers\QueueTenancyBootstrapper',
'database' => Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper::class,
'cache' => Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper::class,
'filesystem' => Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper::class,
'queue' => Stancl\Tenancy\TenancyBootstrappers\QueueTenancyBootstrapper::class,
// 'redis' => Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
],
'features' => [
// Features are classes that provide additional functionality
// not needed for tenancy to be bootstrapped. They are run
// 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
'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_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
*/
public function up()
public function up(): void
{
Schema::create('tenants', function (Blueprint $table) {
$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');
});
@ -28,8 +29,8 @@ class CreateTenantsTable extends Migration
*
* @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
*/
public function up()
public function up(): void
{
Schema::create('domains', function (Blueprint $table) {
$table->string('tenant_id', 36)->primary(); // 36 characters is the default uuid length
$table->string('domain', 255)->index(); // don't change this
$table->string('domain', 255)->primary();
$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
*/
public function down()
public function down(): void
{
Schema::drop('domains');
Schema::dropIfExists('domains');
}
}

View file

@ -11,7 +11,8 @@
],
"require": {
"illuminate/support": "^6.0",
"webpatser/laravel-uuid": "^3.0"
"facade/ignition-contracts": "^1.0",
"ramsey/uuid": "^3.7"
},
"require-dev": {
"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
{
/**
* Add tags and forward the call to the inner cache store.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
$tags = [config('tenancy.cache.tag_base') . tenant('id')];
if ($method === 'tags') {
if (\count($parameters) !== 1) {
if (count($parameters) !== 1) {
throw new \Exception("Method tags() takes exactly 1 argument. {count($parameters)} passed.");
}
$names = $parameters[0];
$names = (array) $names; // cache()->tags('foo') https://laravel.com/docs/5.7/cache#removing-tagged-cache-items
return $this->store()->tags(\array_merge($tags, $names));
return $this->store()->tags(array_merge($tags, $names));
}
return $this->store()->tags($tags)->$method(...$parameters);

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');
$newKernel = \str_replace(
$newKernel = str_replace(
'protected $middlewarePriority = [',
"protected \$middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::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);
\file_put_contents(app_path('Http/Kernel.php'), $newKernel);
file_put_contents(app_path('Http/Kernel.php'), $newKernel);
$this->info('✔️ Set middleware priority');
\file_put_contents(
file_put_contents(
base_path('routes/tenant.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');
});
"
@ -74,16 +74,16 @@ Route::get('/', function () {
$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.");
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', [
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
'--tag' => 'migrations',
]);
$this->info('✔️ Created migration.');
$this->info('✔️ Created migrations.');
}
if (! \is_dir(database_path('migrations/tenant'))) {
\mkdir(database_path('migrations/tenant'));
if (! is_dir(database_path('migrations/tenant'))) {
mkdir(database_path('migrations/tenant'));
$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) {
$this->line("Tenant: {$tenant['id']}");
// See Illuminate\Database\Migrations\DatabaseMigrationRepository::getConnection.
// Database connections are cached by Illuminate\Database\ConnectionResolver.
$this->input->setOption('database', 'tenant');
tenancy()->initialize($tenant); // todo2 test that this works with multiple tenants with MySQL
$this->input->setOption('database', $tenant->getConnectionName());
tenancy()->initialize($tenant);
// Migrate
parent::handle();
tenancy()->endTenancy();
});
if ($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;
}
$this->input->setOption('database', 'tenant');
$originalTenant = tenancy()->getTenant();
tenancy()->all($this->option('tenants'))->each(function ($tenant) {
$this->line("Tenant: {$tenant['id']}");
$this->input->setOption('database', $tenant->getConnectionName());
tenancy()->initialize($tenant);
// Migrate
parent::handle();
tenancy()->endTenancy();
});
if ($originalTenant) {
tenancy()->initialize($originalTenant);
} else {
tenancy()->endTenancy();
}
}
}

View file

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

View file

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

View file

@ -31,7 +31,7 @@ class TenantList extends Command
{
$this->info('Listing all tenants.');
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;
/**
* TenancyBootstrappers are classes that make existing code tenant-aware.
*/
interface TenancyBootstrapper
{
public function start(Tenant $tenant); // todo2 TenantManager instead of Tenant
public function start(Tenant $tenant);
public function end();
}

View file

@ -62,7 +62,7 @@ class DatabaseManager
public function createTenantConnection($databaseName, $connectionName)
{
// 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];
// 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
* @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"];
}
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->database->purge();
@ -103,6 +122,12 @@ class DatabaseManager
}
}
/**
* Create a database for a tenant.
*
* @param Tenant $tenant
* @return void
*/
public function createDatabase(Tenant $tenant)
{
$database = $tenant->getDatabaseName();
@ -111,10 +136,16 @@ class DatabaseManager
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
QueuedTenantDatabaseCreator::dispatch($manager, $database);
} else {
return $manager->createDatabase($database);
$manager->createDatabase($database);
}
}
/**
* Delete a tenant's database.
*
* @param Tenant $tenant
* @return void
*/
public function deleteDatabase(Tenant $tenant)
{
$database = $tenant->getDatabaseName();
@ -123,15 +154,19 @@ class DatabaseManager
if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) {
QueuedTenantDatabaseDeleter::dispatch($manager, $database);
} 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
{
// todo2 this shouldn't have to create a connection
$this->createTenantConnection($tenant->getDatabaseName(), $tenant->getConnectionName());
$driver = $this->getDriver($tenant->getConnectionName());
$driver = $this->getDriver($this->getBaseConnection($tenant->getConnectionName()));
$databaseManagers = $this->app['config']['tenancy.database_managers'];

View file

@ -8,6 +8,6 @@ class DatabaseManagerNotRegisteredException extends \Exception
{
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;
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)
{
$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;
use Illuminate\Support\Facades\Facade;
use Stancl\Tenancy\Tenant as Tenant;
use Stancl\Tenancy\Tenant;
// todo2 rename to CurrentTenant?
class TenantFacade extends Facade
{
protected static function getFacadeAccessor()
{
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. */
protected $callback;
public function __construct()
{
$this->callback = function ($entry) {
return [];
};
}
public function bootstrap(TenantManager $tenantManager): void
{
if (! class_exists(Telescope::class)) {
@ -26,7 +33,6 @@ class TelescopeTags implements Feature
if (in_array('tenancy', optional(request()->route())->middleware() ?? [])) {
$tags = array_merge($tags, [
'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;
/** @var TenantDatabaseManager */
protected $databaseManager;
/** @var string */
protected $databaseName;
/**

View file

@ -15,7 +15,10 @@ class QueuedTenantDatabaseDeleter implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var TenantDatabaseManager */
protected $databaseManager;
/** @var string */
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;
use Closure;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
class InitializeTenancy
{
public function __construct(Closure $onFail = null)
/** @var callable */
protected $onFail;
public function __construct(callable $onFail = null)
{
$this->onFail = $onFail ?? function ($e) {
throw $e;
@ -25,8 +29,8 @@ class InitializeTenancy
public function handle($request, Closure $next)
{
try {
tenancy()->init();
} catch (\Exception $e) {
tenancy()->init($request->getHost());
} catch (TenantCouldNotBeIdentifiedException $e) {
($this->onFail)($e);
}

View file

@ -6,6 +6,10 @@ namespace Stancl\Tenancy\Middleware;
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
{
/**
@ -19,8 +23,16 @@ class PreventAccessFromTenantDomains
{
// If the domain is not in exempt domains, it's a tenant domain.
// Tenant domains can't have routes without tenancy middleware.
if (! \in_array(request()->getHost(), config('tenancy.exempt_domains')) &&
! \in_array('tenancy', request()->route()->middleware())) {
$isExemptDomain = in_array($request->getHost(), config('tenancy.exempt_domains'));
$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);
}

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\Support\Facades\DB;
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\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains;
@ -16,17 +17,34 @@ use Stancl\Tenancy\Tenant;
class DatabaseStorageDriver implements StorageDriver
{
// todo2 write tests verifying that data is decoded and added to the array
/** @var Application */
protected $app;
/** @var \Illuminate\Database\Connection */
protected $centralDatabase;
/** @var Tenant The default tenant. */
protected $tenant;
public function __construct(Application $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
@ -54,13 +72,12 @@ class DatabaseStorageDriver implements StorageDriver
public function ensureTenantCanBeCreated(Tenant $tenant): void
{
// todo2 test this
if (Tenants::find($tenant->id)) {
throw new TenantWithThisIdAlreadyExistsException($tenant->id);
}
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
{
DB::transaction(function () use ($tenant) {
Tenants::create(['id' => $tenant->id, 'data' => '{}'])->toArray();
$this->centralDatabase->transaction(function () use ($tenant) {
Tenants::create(['id' => $tenant->id, 'data' => json_encode($tenant->data)])->toArray();
$domainData = [];
foreach ($tenant->domains as $domain) {
$domainData[] = ['domain' => $domain, 'tenant_id' => $tenant->id];
}
Domains::create($domainData);
Domains::insert($domainData);
});
}
public function updateTenant(Tenant $tenant): void
{
$this->centralDatabase->transaction(function () use ($tenant) {
Tenants::find($tenant->id)->putMany($tenant->data);
Domains::firstOrCreate(array_map(function ($domain) use ($tenant) {
return [
$original_domains = Domains::where('tenant_id', $tenant->id)->get()->map(function ($model) {
return $model->domain;
})->toArray();
$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,
];
}, $tenant->domains));
]);
}
});
}
public function deleteTenant(Tenant $tenant): void
{
$this->centralDatabase->transaction(function () use ($tenant) {
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
{
use CentralConnection;
protected $guarded = [];
protected $primaryKey = 'id';
protected $primaryKey = 'domain';
public $incrementing = 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
{
use CentralConnection;
protected $guarded = [];
protected $primaryKey = 'id';
public $incrementing = false;
public $timestamps = false;
public $table = 'tenants';
public function getTable()
{
return config('tenancy.storage_drivers.db.table_names.TenantModel', 'tenants');
}
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()
{
return config('tenancy.storage.db.custom_columns', []);
}
public function getConnectionName()
{
return config('tenancy.storage.db.connection') ?? app(DatabaseManager::class)->originalDefaultConnectionName;
return config('tenancy.storage_drivers.db.custom_columns', []);
}
public static function getAllTenants(array $ids)
@ -81,7 +82,7 @@ class TenantModel extends Model
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);
return $result;

View file

@ -7,14 +7,13 @@ namespace Stancl\Tenancy\StorageDrivers;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\Tenant;
// todo2 transactions instead of pipelines?
class RedisStorageDriver implements StorageDriver
{
// todo2 json encoding?
/** @var Application */
protected $app;
@ -27,7 +26,7 @@ class RedisStorageDriver implements StorageDriver
public function __construct(Application $app, Redis $redis)
{
$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
{
// 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
@ -59,26 +68,12 @@ class RedisStorageDriver implements StorageDriver
throw new TenantCouldNotBeIdentifiedException($domain);
}
return $this->find($id);
return $this->findById($id);
}
public function findById(string $id): Tenant
{
$data = $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);
return $this->makeTenant($this->redis->hgetall("tenants:$id"));
}
public function getTenantIdByDomain(string $domain): ?string
@ -88,32 +83,49 @@ class RedisStorageDriver implements StorageDriver
public function createTenant(Tenant $tenant): void
{
$this->redis->pipeline(function ($pipe) use ($tenant) {
$id = $tenant->id;
$this->redis->transaction(function ($pipe) use ($tenant) {
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
{
$this->redis->pipeline(function ($pipe) use ($tenant) {
$pipe->hmset("tenants:{$tenant->id}", $tenant->data);
$id = $tenant->id;
foreach ($tenant->domains as $domain) {
$pipe->hmset("domains:$domain", 'tenant_id', $tenant->id);
$old_domains = json_decode($this->redis->hget("tenants:$id", '_tenancy_domains'), true);
$deleted_domains = array_diff($old_domains, $tenant->domains);
$domains = $tenant->domains;
$data = [];
foreach ($tenant->data as $key => $value) {
$data[$key] = json_encode($value);
}
// todo2 deleted domains
$this->redis->transaction(function ($pipe) use ($id, $data, $deleted_domains, $domains) {
foreach ($deleted_domains as $deleted_domain) {
$pipe->del("domains:$deleted_domain");
}
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
{
$this->redis->pipeline(function ($pipe) use ($tenant) {
$this->redis->transaction(function ($pipe) use ($tenant) {
foreach ($tenant->domains as $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
{
// todo2 $this->redis->pipeline()
$hashes = array_map(function ($hash) {
return "tenants:{$hash}";
}, $ids);
@ -143,15 +160,38 @@ class RedisStorageDriver implements StorageDriver
}
return array_map(function ($tenant) {
return $this->redis->hgetall($tenant);
return $this->makeTenant($this->redis->hgetall($tenant));
}, $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)
{
$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
@ -161,7 +201,7 @@ class RedisStorageDriver implements StorageDriver
$result = [];
$values = $this->redis->hmget("tenants:{$tenant->id}", $keys);
foreach ($keys as $i => $key) {
$result[$key] = $values[$i];
$result[$key] = json_decode($values[$i], true);
}
return $result;

View file

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

View file

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

View file

@ -9,37 +9,50 @@ use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant;
// todo better solution than tenant_asset?
class FilesystemTenancyBootstrapper implements TenancyBootstrapper
{
protected $originalPaths = [];
/** @var Application */
protected $app;
/** @var array */
public $originalPaths = [];
public function __construct(Application $app)
{
$this->app = $app;
$this->originalPaths = [
'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)
{
// todo2 revisit this
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->id;
// 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
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
$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);
} else {
$root = $this->app['config']["filesystems.disks.{$disk}.root"];
@ -52,7 +65,11 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
public function end()
{
// 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
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {

View file

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

View file

@ -4,28 +4,28 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenancyBootstrappers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Config\Repository;
use Illuminate\Support\Facades\Redis;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant;
class RedisTenancyBootstrapper implements TenancyBootstrapper
{
/** @var string[string] Original prefixes of connections */
protected $originalPrefixes = [];
/** @var array<string, string> Original prefixes of connections */
public $originalPrefixes = [];
/** @var Application */
protected $app;
/** @var Repository */
protected $config;
public function __construct(Application $app)
public function __construct(Repository $config)
{
$this->app = $app;
$this->config = $config;
}
public function start(Tenant $tenant)
{
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();
$this->originalPrefixes[$connection] = $client->getOption($client::OPT_PREFIX);
@ -40,10 +40,12 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
$client->setOption($client::OPT_PREFIX, $this->originalPrefixes[$connection]);
}
$this->originalPrefixes = [];
}
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\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper;
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.
*
* @return void
*/
public function register()
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
$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->singleton(DatabaseManager::class);
@ -79,5 +47,54 @@ class TenancyServiceProvider extends ServiceProvider
$this->app->bind('globalCache', function ($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 Illuminate\Foundation\Application;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ForwardsCalls;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
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.
*/
class Tenant implements ArrayAccess
{
use Traits\HasArrayAccess;
use Traits\HasArrayAccess,
ForwardsCalls;
/**
* Tenant data. A "cache" of tenant storage.
@ -51,8 +51,16 @@ class Tenant implements ArrayAccess
*
* @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)
{
$this->app = $app;
@ -61,6 +69,12 @@ class Tenant implements ArrayAccess
$this->idGenerator = $idGenerator;
}
/**
* Public constructor.
*
* @param Application $app
* @return self
*/
public static function new(Application $app = null): self
{
$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
{
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
{
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;
return $this;
}
return $this;
}
/**
* Does this model exist in the tenant storage.
*
* @return bool
*/
public function isPersisted(): bool
{
return $this->persisted;
@ -127,6 +163,11 @@ class Tenant implements ArrayAccess
return $this;
}
/**
* Unassign all domains from the tenant.
*
* @return self
*/
public function clearDomains(): self
{
$this->domains = [];
@ -134,6 +175,12 @@ class Tenant implements ArrayAccess
return $this;
}
/**
* Set (overwrite) the tenant's domains.
*
* @param string|string[] $domains
* @return self
*/
public function withDomains($domains): self
{
$domains = (array) $domains;
@ -143,6 +190,12 @@ class Tenant implements ArrayAccess
return $this;
}
/**
* Set (overwrite) tenant data.
*
* @param array $data
* @return self
*/
public function withData(array $data): self
{
$this->data = $data;
@ -150,11 +203,21 @@ class Tenant implements ArrayAccess
return $this;
}
/**
* Generate a random ID.
*
* @return void
*/
public function generateId()
{
$this->id = $this->idGenerator->generate($this->domains, $this->data);
}
/**
* Write the tenant's state to storage.
*
* @return self
*/
public function save(): self
{
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
*/
@ -201,12 +264,22 @@ class Tenant implements ArrayAccess
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']);
}
public function getConnectionName()
/**
* Get the tenant's database connection's name.
*
* @return string
*/
public function getConnectionName(): string
{
return $this->data['_tenancy_db_connection'] ?? 'tenant';
}
@ -243,6 +316,13 @@ class Tenant implements ArrayAccess
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
{
if ($key === 'id') {
@ -268,6 +348,20 @@ class Tenant implements ArrayAccess
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)
{
return $this->get($key);
@ -278,6 +372,16 @@ class Tenant implements ArrayAccess
if ($key === 'id' && isset($this->data['id'])) {
throw new TenantStorageException("Tenant ids can't be changed.");
}
$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;
use Illuminate\Support\Facades\DB;
use Illuminate\Config\Repository;
use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager;
use Stancl\Tenancy\Contracts\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
{
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
{
return DB::statement("DROP DATABASE `$name`");
return $this->database->statement("DROP DATABASE `$name`");
}
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;
use Illuminate\Support\Facades\DB;
use Illuminate\Config\Repository;
use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager;
use Stancl\Tenancy\Contracts\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
{
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
{
return DB::statement("DROP DATABASE \"$name\"");
return $this->database->statement("DROP DATABASE \"$name\"");
}
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 Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
/**
* @internal Class is subject to breaking changes in minor and patch versions.
@ -50,6 +51,12 @@ class TenantManager
$this->bootstrapFeatures();
}
/**
* Write a new tenant to storage.
*
* @param Tenant $tenant
* @return self
*/
public function createTenant(Tenant $tenant): self
{
$this->ensureTenantCanBeCreated($tenant);
@ -58,14 +65,24 @@ class TenantManager
$this->database->createDatabase($tenant);
if ($this->shouldMigrateAfterCreation()) {
if ($this->shouldQueueMigration()) {
QueuedTenantDatabaseMigrator::dispatch($tenant);
} else {
$this->artisan->call('tenants:migrate', [
'--tenants' => [$tenant['id']],
]);
}
}
return $this;
}
/**
* Delete a tenant from storage.
*
* @param Tenant $tenant
* @return self
*/
public function deleteTenant(Tenant $tenant): self
{
$this->storage->deleteTenant($tenant);
@ -77,6 +94,13 @@ class TenantManager
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
{
return Tenant::create($domains, $data);
@ -95,6 +119,12 @@ class TenantManager
$this->database->ensureTenantCanBeCreated($tenant);
}
/**
* Update an existing tenant in storage.
*
* @param Tenant $tenant
* @return self
*/
public function updateTenant(Tenant $tenant): self
{
$this->storage->updateTenant($tenant);
@ -102,6 +132,12 @@ class TenantManager
return $this;
}
/**
* Find tenant by domain & initialize tenancy.
*
* @param string|null $domain
* @return self
*/
public function init(string $domain = null): self
{
$domain = $domain ?? request()->getHost();
@ -110,6 +146,12 @@ class TenantManager
return $this;
}
/**
* Find tenant by ID & initialize tenancy.
*
* @param string $id
* @return self
*/
public function initById(string $id): self
{
$this->initializeTenancy($this->find($id));
@ -156,6 +198,12 @@ class TenantManager
return collect($this->storage->all($only));
}
/**
* Initialize tenancy.
*
* @param Tenant $tenant
* @return self
*/
public function initializeTenancy(Tenant $tenant): self
{
$this->setTenant($tenant);
@ -171,6 +219,12 @@ class TenantManager
return $this->initializeTenancy($tenant);
}
/**
* Execute TenancyBootstrappers.
*
* @param Tenant $tenant
* @return self
*/
public function bootstrapTenancy(Tenant $tenant): self
{
$prevented = $this->event('bootstrapping');
@ -257,6 +311,11 @@ class TenantManager
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
{
return $this->app['config']['tenancy.delete_database_after_tenant_deletion'] ?? false;
@ -277,6 +336,19 @@ class TenantManager
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.
*

View file

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

View file

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

View file

@ -2,14 +2,15 @@
declare(strict_types=1);
namespace Stancl\Tenancy;
namespace Stancl\Tenancy\UniqueIDGenerators;
use Ramsey\Uuid\Uuid;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
class UUIDGenerator implements UniqueIdentifierGenerator
{
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)
{
if (! is_null($key)) {
return app(Tenant::class)->get($key);
return optional(app(Tenant::class))->get($key) ?? null;
}
return app(Tenant::class);
@ -30,6 +30,20 @@ if (! \function_exists('tenant')) {
if (! \function_exists('tenant_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
set -e
# for development
docker-compose up -d
printf "Variant 1\n\n"
docker-compose exec test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/2.cov "$@"
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 vendor/bin/phpcov merge --clover clover.xml coverage/

View file

@ -117,190 +117,55 @@ class CommandsTest extends TestCase
->expectsOutput('xyz');
}
// todo2 check that multiple tenants can be migrated at once using all database engines
/** @test */
public function install_command_works()
{
if (! \is_dir($dir = app_path('Http'))) {
\mkdir($dir, 0777, true);
if (! is_dir($dir = app_path('Http'))) {
mkdir($dir, 0777, true);
}
if (! \is_dir($dir = base_path('routes'))) {
\mkdir($dir, 0777, true);
if (! is_dir($dir = base_path('routes'))) {
mkdir($dir, 0777, true);
}
// todo2 move this to a file
\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,
];
}
");
file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernel.stub'));
$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('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->assertSame("<?php
$this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernel.stub'), file_get_contents(app_path('Http/Kernel.php')));
}
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
/** @test */
public function migrate_fresh_command_works()
{
/**
* 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,
];
$this->assertFalse(Schema::hasTable('users'));
Artisan::call('tenants:migrate-fresh');
$this->assertFalse(Schema::hasTable('users'));
tenancy()->init('test.localhost');
$this->assertTrue(Schema::hasTable('users'));
/**
* 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,
],
$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());
'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,
];
// test that db is wiped
Artisan::call('tenants:migrate-fresh');
$this->assertFalse(DB::table('users')->exists());
}
", \file_get_contents(app_path('Http/Kernel.php')));
/** @test */
public function create_command_works()
{
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
$data = $tenant->data;
unset($data['id']);
$this->assertSame(['plan' => 'free', 'email' => 'foo@test.local'], $data);
$this->assertSame(['aaa.localhost', 'bbb.localhost'], $tenant->domains);
}
}

View file

@ -25,10 +25,24 @@ class DatabaseManagerTest extends TestCase
/** @test */
public function db_name_is_prefixed_with_db_path_when_sqlite_is_used()
{
// make `tenant` not sqlite so that it has to detect sqlite from fooconn
config(['database.connections.tenant.driver' => 'mysql']);
config(['database.connections.fooconn.driver' => 'sqlite']);
app(DatabaseManager::class)->createTenantConnection('foodb', 'fooconn');
$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;
use Tenancy;
use Tenant;
class FacadeTest extends TestCase
{
/** @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('abc', 'xyz');
@ -17,4 +24,10 @@ class FacadeTest extends TestCase
$this->assertSame('bar', Tenant::get('foo'));
$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 */
public function queues_use_non_tenant_db_connection()
{
// todo2 finish this test. requires using the db driver
// requires using the db driver
$this->markTestIncomplete();
}
@ -56,6 +56,6 @@ class TestJob implements ShouldQueue
*/
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();
if ($override = config("tenancy.filesystem.root_override.{$disk}")) {
$correct_path_prefix = \str_replace('%storage_path%', storage_path(), $override);
$correct_path_prefix = str_replace('%storage_path%', storage_path(), $override);
} else {
if ($base = $originals[$disk]) {
$correct_path_prefix = $base . "/$suffix/";

View file

@ -6,8 +6,7 @@ namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Redis;
// todo2 rename
class BootstrapsTenancyTest extends TestCase
class TenancyBootstrappersTest extends TestCase
{
public $autoInitTenancy = false;
@ -56,7 +55,7 @@ class BootstrapsTenancyTest extends TestCase
$current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($override = config("tenancy.filesystem.root_override.{$disk}")) {
$correct_path_prefix = \str_replace('%storage_path%', storage_path(), $override);
$correct_path_prefix = str_replace('%storage_path%', storage_path(), $override);
} else {
if ($base = $old_storage_facade_roots[$disk]) {
$correct_path_prefix = $base . "/$suffix/";
@ -78,4 +77,20 @@ class BootstrapsTenancyTest extends TestCase
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
$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;
use Stancl\Tenancy\Tenant;
class TenantAssetTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */
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);
\Storage::disk('public')->put($filename, 'bar');
$path = storage_path("app/public/$filename");
// response()->file() returns BinaryFileResponse whose content is
// inaccessible via getContent, so ->assertSee() can't be used
// $this->get(tenant_asset($filename))->assertSuccessful(); // TODO2 COMMENTED ASSERTIONS
// $this->assertFileExists($path); // TODO2 COMMENTED ASSERTIONS
$this->assertFileExists($path);
$response = $this->get(tenant_asset($filename));
$f = \fopen($path, 'r');
$content = \fread($f, \filesize($path));
\fclose($f);
$response->assertSuccessful();
// $this->assertSame('bar', $content); // TODO2 COMMENTED ASSERTIONS
$this->assertTrue(true); // TODO2 COMMENTED ASSERTIONS
$f = fopen($path, 'r');
$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
{
public $autoInitTenancy = false;
/**
* @test
* @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.');
}
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();
$this->assertFalse(app($databaseManager)->databaseExists($name));
app($databaseManager)->createDatabase($name);
@ -34,6 +34,20 @@ class TenantDatabaseManagerTest extends TestCase
$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
* @dataProvider database_manager_provider

View file

@ -5,8 +5,13 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
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\TenantManager;
class TenantManagerTest extends TestCase
{
@ -147,7 +152,7 @@ class TenantManagerTest extends TestCase
{
$tenant1 = Tenant::new()->withDomains(['foo.localhost'])->save();
$tenant2 = Tenant::new()->withDomains(['bar.localhost'])->save();
$this->assertEquals([$tenant1, $tenant2], tenancy()->all()->toArray());
$this->assertEqualsCanonicalizing([$tenant1, $tenant2], tenancy()->all()->toArray());
}
/** @test */
@ -187,4 +192,78 @@ class TenantManagerTest extends TestCase
$this->expectException(\Stancl\Tenancy\Exceptions\TenantStorageException::class);
$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;
use Route;
use Stancl\Tenancy\Tenant;
class TenantRedirectMacroTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */
public function tenant_redirect_macro_replaces_only_the_hostname()
{
config([
'tenancy.features' => ['Stancl\Tenancy\Features\TenantRedirect'],
]);
Route::get('/foobar', function () {
return 'Foo';
})->name('home');
@ -19,6 +27,9 @@ class TenantRedirectMacroTest extends TestCase
return redirect()->route('home')->tenant('abcd');
});
Tenant::create('foo.localhost');
tenancy()->init('foo.localhost');
$this->get('/redirect')
->assertRedirect('http://abcd/foobar');
}

View file

@ -16,9 +16,9 @@ class TenantStorageTest extends TestCase
{
$abc = Tenant::new()->withDomains(['abc.localhost'])->save();
$exists = function () use ($abc) {
return tenancy()->all()->reduce(function ($result, $tenant) use ($abc) {
return $result ?: $tenant->id === $abc->id;
}, false);
return tenancy()->all()->contains(function ($tenant) use ($abc) {
return $tenant->id === $abc->id;
});
};
$this->assertTrue($exists());
@ -95,26 +95,26 @@ class TenantStorageTest extends TestCase
public function data_is_stored_with_correct_data_types()
{
tenant()->put('someBool', false);
$this->assertSame('boolean', \gettype(tenant()->get('someBool')));
$this->assertSame('boolean', \gettype(tenant()->get(['someBool'])['someBool']));
$this->assertSame('boolean', gettype(tenant()->get('someBool')));
$this->assertSame('boolean', gettype(tenant()->get(['someBool'])['someBool']));
tenant()->put('someInt', 5);
$this->assertSame('integer', \gettype(tenant()->get('someInt')));
$this->assertSame('integer', \gettype(tenant()->get(['someInt'])['someInt']));
$this->assertSame('integer', gettype(tenant()->get('someInt')));
$this->assertSame('integer', gettype(tenant()->get(['someInt'])['someInt']));
tenant()->put('someDouble', 11.40);
$this->assertSame('double', \gettype(tenant()->get('someDouble')));
$this->assertSame('double', \gettype(tenant()->get(['someDouble'])['someDouble']));
$this->assertSame('double', gettype(tenant()->get('someDouble')));
$this->assertSame('double', gettype(tenant()->get(['someDouble'])['someDouble']));
tenant()->put('string', 'foo');
$this->assertSame('string', \gettype(tenant()->get('string')));
$this->assertSame('string', \gettype(tenant()->get(['string'])['string']));
$this->assertSame('string', gettype(tenant()->get('string')));
$this->assertSame('string', gettype(tenant()->get(['string'])['string']));
}
/** @test */
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());
}
@ -149,7 +149,7 @@ class TenantStorageTest extends TestCase
]);
config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
config(['tenancy.storage.db.custom_columns' => [
config(['tenancy.storage_drivers.db.custom_columns' => [
'foo',
]]);
@ -159,6 +159,6 @@ class TenantStorageTest extends TestCase
tenant()->put(['foo' => 'bar', 'abc' => 'xyz']);
$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;
use Illuminate\Support\Facades\Redis;
use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
use Stancl\Tenancy\StorageDrivers\RedisStorageDriver;
use Stancl\Tenancy\Tenant;
abstract class TestCase extends \Orchestra\Testbench\TestCase
@ -27,7 +25,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
Redis::connection('cache')->flushdb();
$this->loadMigrationsFrom([
'--path' => \realpath(__DIR__ . '/../assets/migrations'),
'--path' => realpath(__DIR__ . '/../assets/migrations'),
'--database' => 'central',
]);
config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
@ -59,11 +57,11 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
*/
protected function getEnvironmentSetUp($app)
{
if (\file_exists(__DIR__ . '/../.env')) {
if (file_exists(__DIR__ . '/../.env')) {
\Dotenv\Dotenv::create(__DIR__ . '/..')->load();
}
\fclose(\fopen(database_path('central.sqlite'), 'w'));
fclose(fopen(database_path('central.sqlite'), 'w'));
$app['config']->set([
'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'),
@ -99,17 +97,13 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'),
'tenancy.redis.prefixed_connections' => ['default'],
'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['config']->set([
'tenancy.storage_driver' => RedisStorageDriver::class,
]);
} elseif (env('TENANCY_TEST_STORAGE_DRIVER', 'redis') === 'db') {
$app['config']->set([
'tenancy.storage_driver' => DatabaseStorageDriver::class,
]);
}
$app->singleton(\Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class);
$app['config']->set(['tenancy.storage_driver' => env('TENANCY_TEST_STORAGE_DRIVER', 'redis')]);
}
protected function getPackageProviders($app)
@ -152,7 +146,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
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()
@ -162,6 +156,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
public function assertArrayIsSubset($subset, $array, string $message = ''): void
{
parent::assertTrue(\array_intersect($subset, $array) == $subset, $message);
parent::assertTrue(array_intersect($subset, $array) == $subset, $message);
}
}