diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..4bea7147
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: stancl
+patreon: samuelstancl
+open_collective: # Replace with a single Open Collective username
+ko_fi: samuelstancl
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: ['https://www.paypal.me/samuelstancl', 'https://gumroad.com/l/tenancy']
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index ce301e06..596c2292 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -17,6 +17,6 @@ assignees: stancl
A clear and concise description of what you expected to happen.
#### Your setup
- - Laravel version [e.g. 5.8]
- - stancl/tenancy version [e.g. 22]
- - Storage driver [e.g. Redis]
+ - Laravel version: [e.g. 6.2.0]
+ - stancl/tenancy version: [e.g. 2.1.0]
+ - Storage driver: [e.g. DB]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..be15dc62
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,31 @@
+name: CI
+
+env:
+ COMPOSE_INTERACTIVE_NO_CLI: 1
+
+on:
+ push:
+ branches: [ 2.x, master ]
+ pull_request:
+ branches: [ 2.x, master ]
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ laravel: ["^6.0", "^7.0"]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Start docker containers
+ run: docker-compose up -d
+ - name: Install dependencies
+ run: docker-compose exec -T test composer require --no-interaction "laravel/framework:${{ matrix.laravel }}"
+ - name: Run tests
+ run: ./fulltest
+ - name: Send code coverage to codecov
+ env:
+ CODECOV_TOKEN: 24382d15-84e7-4a55-bea4-c4df96a24a9b
+ run: bash <(curl -s https://codecov.io/bash)
diff --git a/.styleci.yml b/.styleci.yml
index 258feec5..50c26217 100644
--- a/.styleci.yml
+++ b/.styleci.yml
@@ -1,8 +1,6 @@
preset: laravel
enabled:
- declare_strict_types
-- alpha_ordered_imports
disabled:
- concat_without_spaces
- ternary_operator_spaces
-- length_ordered_imports
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 87f9f300..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# env:
-# - LARAVEL_VERSION="^6.0" TESTBENCH_VERSION="~4.0" REDIS_DRIVER=phpredis
-
-language: php
-php:
- - '7.2'
-
-services:
- - docker
-
-before_install:
- - docker-compose up -d
-
-# install:
-# - travis_retry docker-compose exec test composer require --no-interaction "laravel/framework:$LARAVEL_VERSION" "orchestra/testbench:$TESTBENCH_VERSION"
-install:
-- travis_retry docker-compose exec test composer install --no-interaction
-
-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: ./fulltest
-
-after_success:
- - bash <(curl -s https://codecov.io/bash)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index db8cc9d7..82f7aa5d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,15 +1,14 @@
# Contributing
## Code style
-
-StyleCI will automatically fix code style violations in your pull requests.
+StyleCI will flag 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.
+If you have Docker installed, simply run ./fulltest. 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.
+Some tests are run only if the `CONTINUOUS_INTEGRATION` or `DOCKER` environment variables are set to true. This is to avoid things like bloating your MySQL instance with test databases.
diff --git a/DONATIONS.md b/DONATIONS.md
new file mode 100644
index 00000000..bb48a79a
--- /dev/null
+++ b/DONATIONS.md
@@ -0,0 +1,39 @@
+# Donations
+
+Any donations will be greatly appreciated and help ensure that the package is developed and maintained in the future.
+
+If you're a company and this package is helping you make money, please consider donating.
+
+**If you'd like to donate in your local currency, see the Bank transfer section.**
+
+### Patreon
+
+If you would like to support me on a monthly basis, you can use Patreon: https://patreon.com/samuelstancl
+
+### PayPal
+
+PayPal is the preferable donation method for one-time donations as it comes with the lowest fees.
+
+You can donate here: [https://paypal.me/samuelstancl](https://paypal.me/samuelstancl)
+
+### Other methods
+
+If you can't use PayPal, you may use my Gumroad link. This comes with higher fees but any donations will be greatly appreciated nonetheless.
+
+You can donate here: [https://gumroad.com/l/tenancy](https://gumroad.com/l/tenancy)
+
+### Bank transfer
+
+If you'd like to donate money from your bank account, you can can do that too. I use [TransferWise](https://transferwise.com/invite/u/samuels1719) (affiliate link 🙂), so I can accept bank transfers in virtually any currency.
+
+Contact me on [samuel.stancl@gmail.com](mailto:samuel.stancl@gmail.com?subject=Donation) and I'll give you bank details for your local currency.
+
+### Legal
+
+If you're a business making a donation, you may want an invoice.
+
+Contact me on [samuel.stancl@gmail.com](mailto:samuel.stancl@gmail.com?subject=Donation%20with%20invoice) and let me know what you need to have on the invoice and I will make it happen.
+
+### Thank you!
+
+Again, any donations are greatly appreciated. Thanks to everyone who has donated, you're helping keep this package maintained.
diff --git a/Dockerfile b/Dockerfile
index a6512668..75132249 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,30 +4,35 @@ LABEL maintainer="Samuel Štancl"
ENV DEBIAN_FRONTEND=noninteractive
-RUN apt-get update \
- && apt-get install -y curl zip unzip git sqlite3 \
- php7.2-fpm php7.2-cli \
- php7.2-pgsql php7.2-sqlite3 php7.2-gd \
- php7.2-curl php7.2-memcached \
- php7.2-imap php7.2-mysql php7.2-mbstring \
- php7.2-xml php7.2-zip php7.2-bcmath php7.2-soap \
- php7.2-intl php7.2-readline php7.2-xdebug \
+RUN apt-get update
+RUN apt-get install -y software-properties-common
+RUN add-apt-repository -y ppa:ondrej/php
+
+RUN apt-get install -y curl zip unzip git sqlite3 \
+ php7.4-fpm php7.4-cli \
+ php7.4-pgsql php7.4-sqlite3 php7.4-gd \
+ php7.4-curl php7.4-memcached \
+ php7.4-imap php7.4-mysql php7.4-mbstring \
+ php7.4-xml php7.4-zip php7.4-bcmath php7.4-soap \
+ php7.4-intl php7.4-readline php7.4-xdebug \
php-msgpack php-igbinary \
&& php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \
&& mkdir /run/php
-RUN apt-get install -y php7.2-redis
-
RUN apt-get install -y python3
-RUN apt-get install -y php7.2-dev php-pear
+RUN apt-get install -y php7.4-dev php-pear
+
+RUN pecl install redis-4.3.0
+RUN echo "extension=redis.so" > /etc/php/7.4/mods-available/redis.ini
+RUN ln -sf /etc/php/7.4/mods-available/redis.ini /etc/php/7.4/fpm/conf.d/20-redis.ini
+RUN ln -sf /etc/php/7.4/mods-available/redis.ini /etc/php/7.4/cli/conf.d/20-redis.ini
+
RUN pecl install xdebug
-# RUN echo '' > /etc/php/7.2/cli/conf.d/20-xdebug.ini
-# RUN echo 'zend_extension=/usr/lib/php/20170718/xdebug.so' >> /etc/php/7.2/cli/php.ini
-RUN echo 'zend_extension=/usr/lib/php/20170718/xdebug.so' > /etc/php/7.2/cli/conf.d/20-xdebug.ini
+RUN echo 'zend_extension=/usr/lib/php/20190902/xdebug.so' > /etc/php/7.4/cli/conf.d/20-xdebug.ini
RUN apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-WORKDIR /var/www/html
\ No newline at end of file
+WORKDIR /var/www/html
diff --git a/README.md b/README.md
index 7dc8e32c..a91ce3a4 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,16 @@
-# [stancl/tenancy](https://tenancy.samuelstancl.me)
+
+
+
-[](https://laravel.com)
-[](https://packagist.org/packages/stancl/tenancy)
-[](https://travis-ci.com/stancl/tenancy)
-[](https://codecov.io/gh/stancl/tenancy)
-[](https://gumroad.com/l/tenancy)
+
+
+
+
+
+
+
+
+
### *Automatic multi-tenancy for your Laravel app.*
@@ -14,8 +20,15 @@ You won't have to change a thing in your application's code.
- :heavy_check_mark: No replacing of Laravel classes (`Cache`, `Storage`, ...) with tenancy-aware classes
- :heavy_check_mark: Built-in tenant identification based on hostname (including second level domains)
-### [Documentation](https://tenancy.samuelstancl.me/docs/v2/)
+### [Documentation](https://tenancyforlaravel.com/docs/v2/)
-Documentation can be found here: https://tenancy.samuelstancl.me/docs/v2/
+Documentation can be found here: https://tenancyforlaravel.com/docs/v2/
The repository with the documentation source code can be found here: [stancl/tenancy-docs](https://github.com/stancl/tenancy-docs).
+
+### [Need help?](https://github.com/stancl/tenancy/blob/2.x/SUPPORT.md)
+
+### Credits
+
+- Created by [Samuel Štancl](https://twitter.com/samuelstancl)
+- Logo by [Brian Dillingham](https://twitter.com/dillinghammm)
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644
index 00000000..e35775e1
--- /dev/null
+++ b/SUPPORT.md
@@ -0,0 +1,9 @@
+# Get Support
+
+If you need help with implementing the package, you can:
+- Open an [issue here on GitHub](https://github.com/stancl/tenancy/issues/new?assignees=stancl&labels=support&template=support-question.md&title=)
+- Join our new [Discord server](https://discord.gg/7cpgPxv) and ask in `#help`
+
+The methods above are preferred, but you may also
+- Contact me on Telegram: [@samuelstancl](https://t.me/samuelstancl)
+- Send me an email: [samuel.stancl@gmail.com](mailto:samuel.stancl@gmail.com)
diff --git a/art/logo.png b/art/logo.png
new file mode 100644
index 00000000..9080de1e
Binary files /dev/null and b/art/logo.png differ
diff --git a/art/old_logo.png b/art/old_logo.png
new file mode 100644
index 00000000..365ee375
Binary files /dev/null and b/art/old_logo.png differ
diff --git a/assets/config.php b/assets/config.php
index 64aa211a..844b8d93 100644
--- a/assets/config.php
+++ b/assets/config.php
@@ -3,97 +3,289 @@
declare(strict_types=1);
return [
+ /**
+ * Storage drivers are used to store information about your tenants.
+ * They hold the Tenant Storage data and keeps track of domains.
+ */
'storage_driver' => 'db',
'storage_drivers' => [
+ /**
+ * The majority of applications will want to use this storage driver.
+ * The information about tenants is persisted in a relational DB
+ * like MySQL or PostgreSQL. The only downside is performance.
+ *
+ * A database connection to the central database has to be established on each
+ * request, to identify the tenant based on the domain. This takes three DB
+ * queries. Then, the connection to the tenant database is established.
+ *
+ * Note: From v2.3.0, the performance of the DB storage driver can be improved
+ * by a lot by using Cached Tenant Lookup. Be sure to enable that if you're
+ * using this storage driver. Enabling that feature can completely avoid
+ * querying the central database to identify build the Tenant object.
+ */
'db' => [
'driver' => Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver::class,
'data_column' => 'data',
'custom_columns' => [
// 'plan',
],
+
+ /**
+ * Your central database connection. Set to null to use the default one.
+ *
+ * Note: It's recommended to create a designated central connection,
+ * to let you easily use it in your app, e.g. via the DB facade.
+ */
'connection' => null,
+
'table_names' => [
- 'TenantModel' => 'tenants',
- 'DomainModel' => 'domains',
+ 'tenants' => 'tenants',
+ 'domains' => 'domains',
],
+
+ /**
+ * Here you can enable the Cached Tenant Lookup.
+ *
+ * You can specify what cache store should be used to cache the tenant resolution.
+ * Set to string with a specific cache store name, or to null to disable cache.
+ */
+ 'cache_store' => null,
+ 'cache_ttl' => 3600, // seconds
],
+
+ /**
+ * The Redis storage driver is much more performant than the database driver.
+ * However, by default, Redis is a not a durable data storage. It works well for ephemeral data
+ * like cache, but to hold critical data, it needs to be configured in a way that guarantees
+ * that data will be persisted permanently. Specifically, you want to enable both AOF and
+ * RDB. Read this here: https://tenancy.samuelstancl.me/docs/v2/storage-drivers/#redis.
+ */
'redis' => [
'driver' => Stancl\Tenancy\StorageDrivers\RedisStorageDriver::class,
'connection' => 'tenancy',
],
],
+
+ /**
+ * Controller namespace used by routes in routes/tenant.php.
+ */
'tenant_route_namespace' => 'App\Http\Controllers',
- 'exempt_domains' => [ // e.g. domains which host landing pages, sign up pages, etc
+
+ /**
+ * Central domains (hostnames), e.g. domains which host landing pages, sign up pages, etc.
+ */
+ 'exempt_domains' => [
// 'localhost',
],
- 'database' => [
- 'based_on' => null, // The connection that will be used as a base for the dynamically created tenant connection.
- 'prefix' => 'tenant',
- 'suffix' => '',
- ],
- 'redis' => [
- 'prefix_base' => 'tenant',
- 'prefixed_connections' => [
- // 'default',
- // 'cache',
- ],
- ],
- 'cache' => [
- 'tag_base' => 'tenant',
- ],
- 'filesystem' => [ // https://tenancy.samuelstancl.me/docs/v2/filesystem-tenancy/
- 'suffix_base' => 'tenant',
- // Disks which should be suffixed with the suffix_base + tenant id.
- 'disks' => [
- 'local',
- 'public',
- // 's3',
- ],
- 'root_override' => [
- // Disks whose roots should be overriden after storage_path() is suffixed.
- 'local' => '%storage_path%/app/',
- 'public' => '%storage_path%/app/public/',
- ],
- ],
- 'database_managers' => [
- // Tenant database managers handle the creation & deletion of tenant databases.
- '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',
- ],
+
+ /**
+ * Tenancy bootstrappers are executed when tenancy is initialized.
+ * Their responsibility is making Laravel features tenant-aware.
+ *
+ * To configure their behavior, see the config keys below.
+ */
'bootstrappers' => [
- // Tenancy bootstrappers are executed when tenancy is initialized.
- // Their responsibility is making Laravel features tenant-aware.
'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\TenantConfig::class,
- // Stancl\Tenancy\Features\TelescopeTags::class,
- // Stancl\Tenancy\Features\TenantRedirect::class,
+ /**
+ * Database tenancy config. Used by DatabaseTenancyBootstrapper.
+ */
+ 'database' => [
+ /**
+ * The connection that will be used as a template for the dynamically created tenant connection.
+ * Set to null to use the default connection.
+ */
+ 'based_on' => null,
+
+ /**
+ * Tenant database names are created like this:
+ * prefix + tenant_id + suffix.
+ */
+ 'prefix' => 'tenant',
+ 'suffix' => '',
+
+ 'separate_by' => 'database', // database or schema (only supported by pgsql)
+ ],
+
+ /**
+ * Redis tenancy config. Used by RedisTenancyBoostrapper.
+ *
+ * Note: You need phpredis to use Redis tenancy.
+ *
+ * Note: You don't need to use this if you're using Redis only for cache.
+ * Redis tenancy is only relevant if you're making direct Redis calls,
+ * either using the Redis facade or by injecting it as a dependency.
+ */
+ 'redis' => [
+ 'prefix_base' => 'tenant', // Each key in Redis will be prepended by this prefix_base, followed by the tenant id.
+ 'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another.
+ // 'default',
+ ],
+ ],
+
+ /**
+ * Cache tenancy config. Used by CacheTenancyBootstrapper.
+ *
+ * This works for all Cache facade calls, cache() helper
+ * calls and direct calls to injected cache stores.
+ *
+ * Each key in cache will have a tag applied on it. This tag is used to
+ * scope the cache both when writing to it and when reading from it.
+ */
+ 'cache' => [
+ 'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call.
+ ],
+
+ /**
+ * Filesystem tenancy config. Used by FilesystemTenancyBootstrapper.
+ * https://tenancy.samuelstancl.me/docs/v2/filesystem-tenancy/.
+ */
+ 'filesystem' => [
+ /**
+ * Each disk listed in the 'disks' array will be suffixed by the suffix_base, followed by the tenant_id.
+ */
+ 'suffix_base' => 'tenant',
+ 'disks' => [
+ 'local',
+ 'public',
+ // 's3',
+ ],
+
+ /**
+ * Use this for local disks.
+ *
+ * See https://tenancy.samuelstancl.me/docs/v2/filesystem-tenancy/
+ */
+ 'root_override' => [
+ // Disks whose roots should be overriden after storage_path() is suffixed.
+ 'local' => '%storage_path%/app/',
+ 'public' => '%storage_path%/app/public/',
+ ],
+
+ /**
+ * Should storage_path() be suffixed.
+ *
+ * Note: Disabling this will likely break local disk tenancy. Only disable this if you're using an external file storage service like S3.
+ *
+ * For the vast majority of applications, this feature should be enabled. But in some
+ * edge cases, it can cause issues (like using Passport with Vapor - see #196), so
+ * you may want to disable this if you are experiencing these edge case issues.
+ */
+ 'suffix_storage_path' => true,
+
+ /**
+ * By default, asset() calls are made multi-tenant too. You can use global_asset() and mix()
+ * for global, non-tenant-specific assets. However, you might have some issues when using
+ * packages that use asset() calls inside the tenant app. To avoid such issues, you can
+ * disable asset() helper tenancy and explicitly use tenant_asset() calls in places
+ * where you want to use tenant-specific assets (product images, avatars, etc).
+ */
+ 'asset_helper_tenancy' => true,
+ ],
+
+ /**
+ * TenantDatabaseManagers are classes that handle the creation & deletion of tenant databases.
+ */
+ 'database_managers' => [
+ 'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
+ 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
+ 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
+
+ /**
+ * Disable the pgsql manager above, enable the one below, and set the
+ * tenancy.database.separate_by config key to 'schema' if you would
+ * like to separate tenant DBs by schemas rather than databases.
+ */
+ // 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database
+ ],
+
+ /**
+ * Connections used by TenantDatabaseManagers. This tells, for example, the
+ * MySQLDatabaseManager to use the mysql connection to create databases.
+ */
+ 'database_manager_connections' => [
+ 'sqlite' => 'sqlite',
+ 'mysql' => 'mysql',
+ 'pgsql' => 'pgsql',
+ ],
+
+ /**
+ * Features are classes that provide additional functionality
+ * not needed for tenancy to be bootstrapped. They are run
+ * regardless of whether tenancy has been initialized.
+ *
+ * See the documentation page for each class to
+ * understand which ones you want to enable.
+ */
+ 'features' => [
+ // Stancl\Tenancy\Features\Timestamps::class, // https://tenancy.samuelstancl.me/docs/v2/features/timestamps/
+ // Stancl\Tenancy\Features\TenantConfig::class, // https://tenancy.samuelstancl.me/docs/v2/features/tenant-config/
+ // Stancl\Tenancy\Features\TelescopeTags::class, // https://tenancy.samuelstancl.me/docs/v2/telescope/
+ // Stancl\Tenancy\Features\TenantRedirect::class, // https://tenancy.samuelstancl.me/docs/v2/features/tenant-redirect/
],
'storage_to_config_map' => [ // Used by the TenantConfig feature
// 'paypal_api_key' => 'services.paypal.api_key',
],
+
+ /**
+ * The URL to which users will be redirected when they try to acceess a central route on a tenant domain.
+ */
'home_url' => '/app',
- 'migrate_after_creation' => false, // run migrations after creating a tenant
- '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
+
+ /**
+ * Automatically create a database when creating a tenant.
+ */
+ 'create_database' => true,
+
+ /**
+ * Should tenant databases be created asynchronously in a queued job.
+ */
'queue_database_creation' => false,
+
+ /**
+ * Should tenant migrations be ran after the tenant's database is created.
+ */
+ 'migrate_after_creation' => false,
+ 'migration_parameters' => [
+ // '--force' => true, // Set this to true to be able to run migrations in production
+ // '--path' => [], // If you need to customize paths to tenant migrations
+ ],
+
+ /**
+ * Should tenant databases be automatically seeded after they're created & migrated.
+ */
+ 'seed_after_migration' => false, // should the seeder run after automatic migration
+ 'seeder_parameters' => [
+ '--class' => 'DatabaseSeeder', // root seeder class, e.g.: 'DatabaseSeeder'
+ // '--force' => true,
+ ],
+
+ /**
+ * Automatically delete the tenant's database after the tenant is deleted.
+ *
+ * This will save space but permanently delete data which you might want to keep.
+ */
+ 'delete_database_after_tenant_deletion' => false,
+
+ /**
+ * Should tenant databases be deleted asynchronously in a queued job.
+ */
'queue_database_deletion' => false,
+
+ /**
+ * If you don't supply an id when creating a tenant, this class will be used to generate a random ID.
+ */
'unique_id_generator' => Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator::class,
+
+ /**
+ * Middleware pushed to the global middleware stack.
+ */
+ 'global_middleware' => [
+ Stancl\Tenancy\Middleware\InitializeTenancy::class,
+ ],
];
diff --git a/assets/tenant_routes.php.stub b/assets/tenant_routes.php.stub
new file mode 100644
index 00000000..a51c3f1c
--- /dev/null
+++ b/assets/tenant_routes.php.stub
@@ -0,0 +1,16 @@
+callSilent('vendor:publish', [
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
'--tag' => 'config',
- ]);
+ ]);
$this->info('✔️ Created config/tenancy.php');
- $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'))
- );
+ $newKernel = $this->setMiddlewarePriority();
$newKernel = str_replace("'web' => [", "'web' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel);
+ $newKernel = str_replace("'api' => [", "'api' => [
+ \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel);
+
file_put_contents(app_path('Http/Kernel.php'), $newKernel);
$this->info('✔️ Set middleware priority');
- file_put_contents(
- base_path('routes/tenant.php'),
- "info('✔️ Created routes/tenant.php');
+ if (! file_exists(base_path('routes/tenant.php'))) {
+ file_put_contents(base_path('routes/tenant.php'), file_get_contents(__DIR__ . '/../../assets/tenant_routes.php.stub'));
+ $this->info('✔️ Created routes/tenant.php');
+ } else {
+ $this->info('Found routes/tenant.php.');
+ }
$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 migrations?', true)) {
+ $this->line('This package lets you store data about tenants either in Redis or in a relational database like MySQL. To store data about tenants in a relational database, you need a few database tables.');
+ if ($this->confirm('Do you wish to publish the migrations that create these tables?', true)) {
$this->callSilent('vendor:publish', [
- '--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
- '--tag' => 'migrations',
+ '--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
+ '--tag' => 'migrations',
]);
- $this->info('✔️ Created migrations.');
+ $this->info('✔️ Created migrations. Remember to run [php artisan migrate]!');
}
if (! is_dir(database_path('migrations/tenant'))) {
@@ -89,4 +71,34 @@ Route::get('/app', function () {
$this->comment('✨️ stancl/tenancy installed successfully.');
}
+
+ protected function setMiddlewarePriority(): string
+ {
+ if (app()->version()[0] === '6') {
+ return str_replace(
+ 'protected $middlewarePriority = [',
+ "protected \$middlewarePriority = [
+ \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
+ \Stancl\Tenancy\Middleware\InitializeTenancy::class,",
+ file_get_contents(app_path('Http/Kernel.php'))
+ );
+ } else {
+ return str_replace(
+ "];\n}",
+ "];\n\n protected \$middlewarePriority = [
+ \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
+ \Stancl\Tenancy\Middleware\InitializeTenancy::class,
+ \Illuminate\Session\Middleware\StartSession::class,
+ \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
+ \Illuminate\Routing\Middleware\ThrottleRequests::class,
+ \Illuminate\Session\Middleware\AuthenticateSession::class,
+ \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ \Illuminate\Auth\Middleware\Authorize::class,
+ ];
+}",
+ file_get_contents(app_path('Http/Kernel.php'))
+ );
+ }
+ }
}
diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php
index d9ba4938..279a371b 100644
--- a/src/Commands/Migrate.php
+++ b/src/Commands/Migrate.php
@@ -49,21 +49,15 @@ class Migrate extends MigrateCommand
return;
}
- $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();
+ $tenant->run(function () {
+ // Migrate
+ parent::handle();
+ });
});
-
- if ($originalTenant) {
- tenancy()->initialize($originalTenant);
- }
}
}
diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php
index 6a31e888..bef3a09a 100644
--- a/src/Commands/MigrateFresh.php
+++ b/src/Commands/MigrateFresh.php
@@ -33,30 +33,24 @@ final class MigrateFresh extends Command
*/
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);
+ $tenant->run(function ($tenant) {
+ $this->info('Dropping tables.');
+ $this->call('db:wipe', array_filter([
+ '--database' => $tenant->getConnectionName(),
+ '--force' => true,
+ ]));
- $this->call('db:wipe', array_filter([
- '--database' => $tenant->getConnectionName(),
- '--force' => true,
- ]));
-
- $this->call('tenants:migrate', [
- '--tenants' => [$tenant->id],
- ]);
-
- tenancy()->end();
+ $this->info('Migrating.');
+ $this->callSilent('tenants:migrate', [
+ '--tenants' => [$tenant->id],
+ '--force' => true,
+ ]);
+ });
});
$this->info('Done.');
-
- if ($originalTenant) {
- tenancy()->initialize($originalTenant);
- }
}
}
diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php
index b9168bf7..eee99ddd 100644
--- a/src/Commands/Rollback.php
+++ b/src/Commands/Rollback.php
@@ -49,21 +49,15 @@ class Rollback extends RollbackCommand
return;
}
- $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();
+ $tenant->run(function () {
+ // Rollback
+ parent::handle();
+ });
});
-
- if ($originalTenant) {
- tenancy()->initialize($originalTenant);
- }
}
}
diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php
index 61630408..36f59dd0 100644
--- a/src/Commands/Seed.php
+++ b/src/Commands/Seed.php
@@ -43,25 +43,25 @@ class Seed extends SeedCommand
*/
public function handle()
{
+ foreach (config('tenancy.seeder_parameters') as $parameter => $value) {
+ if (! $this->input->hasParameterOption($parameter)) {
+ $this->input->setOption(ltrim($parameter, '-'), $value);
+ }
+ }
+
if (! $this->confirmToProceed()) {
return;
}
- $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();
+ $tenant->run(function () {
+ // Seed
+ parent::handle();
+ });
});
-
- if ($originalTenant) {
- tenancy()->initialize($originalTenant);
- }
}
}
diff --git a/src/Contracts/Future/CanDeleteKeys.php b/src/Contracts/Future/CanDeleteKeys.php
new file mode 100644
index 00000000..25f2f5f6
--- /dev/null
+++ b/src/Contracts/Future/CanDeleteKeys.php
@@ -0,0 +1,21 @@
+app = $app;
@@ -30,6 +37,19 @@ class DatabaseManager
$this->originalDefaultConnectionName = $app['config']['database.default'];
}
+ /**
+ * Set the TenantManager instance, used to dispatch tenancy events.
+ *
+ * @param TenantManager $tenantManager
+ * @return self
+ */
+ public function withTenantManager(TenantManager $tenantManager): self
+ {
+ $this->tenancy = $tenantManager;
+
+ return $this;
+ }
+
/**
* Connect to a tenant's database.
*
@@ -39,6 +59,7 @@ class DatabaseManager
public function connect(Tenant $tenant)
{
$this->createTenantConnection($tenant->getDatabaseName(), $tenant->getConnectionName());
+ $this->setDefaultConnection($tenant->getConnectionName());
$this->switchConnection($tenant->getConnectionName());
}
@@ -49,7 +70,21 @@ class DatabaseManager
*/
public function reconnect()
{
+ // Opposite order to connect() because we don't
+ // want to ever purge the central connection
$this->switchConnection($this->originalDefaultConnectionName);
+ $this->setDefaultConnection($this->originalDefaultConnectionName);
+ }
+
+ /**
+ * Change the default database connection config.
+ *
+ * @param string $connection
+ * @return void
+ */
+ public function setDefaultConnection(string $connection)
+ {
+ $this->app['config']['database.default'] = $connection;
}
/**
@@ -67,7 +102,9 @@ class DatabaseManager
// Change database name.
$databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName;
- $this->app['config']["database.connections.$connectionName.database"] = $databaseName;
+ $separateBy = $this->separateBy($connectionName);
+
+ $this->app['config']["database.connections.$connectionName.$separateBy"] = $databaseName;
}
/**
@@ -102,7 +139,6 @@ class DatabaseManager
*/
public function switchConnection(string $connection)
{
- $this->app['config']['database.default'] = $connection;
$this->database->purge();
$this->database->reconnect($connection);
$this->database->setDefaultConnection($connection);
@@ -114,6 +150,8 @@ class DatabaseManager
* @param Tenant $tenant
* @return void
* @throws TenantCannotBeCreatedException
+ * @throws DatabaseManagerNotRegisteredException
+ * @throws TenantDatabaseAlreadyExistsException
*/
public function ensureTenantCanBeCreated(Tenant $tenant): void
{
@@ -126,18 +164,43 @@ class DatabaseManager
* Create a database for a tenant.
*
* @param Tenant $tenant
+ * @param ShouldQueue[]|callable[] $afterCreating
* @return void
+ * @throws DatabaseManagerNotRegisteredException
*/
- public function createDatabase(Tenant $tenant)
+ public function createDatabase(Tenant $tenant, array $afterCreating = [])
{
$database = $tenant->getDatabaseName();
$manager = $this->getTenantDatabaseManager($tenant);
+ $afterCreating = array_merge(
+ $afterCreating,
+ $this->tenancy->event('database.creating', $database, $tenant)
+ );
+
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
- QueuedTenantDatabaseCreator::dispatch($manager, $database);
+ $chain = [];
+ foreach ($afterCreating as $item) {
+ if (is_string($item) && class_exists($item)) {
+ $chain[] = new $item($tenant); // Classes are instantiated and given $tenant
+ } elseif ($item instanceof ShouldQueue) {
+ $chain[] = $item;
+ }
+ }
+
+ QueuedTenantDatabaseCreator::withChain($chain)->dispatch($manager, $database);
} else {
$manager->createDatabase($database);
+ foreach ($afterCreating as $item) {
+ if (is_object($item) && ! $item instanceof Closure) {
+ $item->handle($tenant);
+ } else {
+ $item($tenant);
+ }
+ }
}
+
+ $this->tenancy->event('database.created', $database, $tenant);
}
/**
@@ -145,17 +208,22 @@ class DatabaseManager
*
* @param Tenant $tenant
* @return void
+ * @throws DatabaseManagerNotRegisteredException
*/
public function deleteDatabase(Tenant $tenant)
{
$database = $tenant->getDatabaseName();
$manager = $this->getTenantDatabaseManager($tenant);
+ $this->tenancy->event('database.deleting', $database, $tenant);
+
if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) {
QueuedTenantDatabaseDeleter::dispatch($manager, $database);
} else {
$manager->deleteDatabase($database);
}
+
+ $this->tenancy->event('database.deleted', $database, $tenant);
}
/**
@@ -163,10 +231,11 @@ class DatabaseManager
*
* @param Tenant $tenant
* @return TenantDatabaseManager
+ * @throws DatabaseManagerNotRegisteredException
*/
- protected function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
+ public function getTenantDatabaseManager(Tenant $tenant): TenantDatabaseManager
{
- $driver = $this->getDriver($this->getBaseConnection($tenant->getConnectionName()));
+ $driver = $this->getDriver($this->getBaseConnection($connectionName = $tenant->getConnectionName()));
$databaseManagers = $this->app['config']['tenancy.database_managers'];
@@ -174,6 +243,28 @@ class DatabaseManager
throw new DatabaseManagerNotRegisteredException($driver);
}
- return $this->app[$databaseManagers[$driver]];
+ $databaseManager = $this->app[$databaseManagers[$driver]];
+
+ if ($connectionName !== 'tenant' && $databaseManager instanceof CanSetConnection) {
+ $databaseManager->setConnection($connectionName);
+ }
+
+ return $databaseManager;
+ }
+
+ /**
+ * What key on the connection config should be used to separate tenants.
+ *
+ * @param string $connectionName
+ * @return string
+ */
+ public function separateBy(string $connectionName): string
+ {
+ if ($this->getDriver($this->getBaseConnection($connectionName)) === 'pgsql'
+ && $this->app['config']['tenancy.database.separate_by'] === 'schema') {
+ return 'schema';
+ }
+
+ return 'database';
}
}
diff --git a/src/Exceptions/NotImplementedException.php b/src/Exceptions/NotImplementedException.php
new file mode 100644
index 00000000..4ae156f7
--- /dev/null
+++ b/src/Exceptions/NotImplementedException.php
@@ -0,0 +1,15 @@
+message = "Tenant with this $key does not exist: $id";
+ }
+}
diff --git a/src/Features/TelescopeTags.php b/src/Features/TelescopeTags.php
index c1d4450f..7978214b 100644
--- a/src/Features/TelescopeTags.php
+++ b/src/Features/TelescopeTags.php
@@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Features;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Stancl\Tenancy\Contracts\Feature;
+use Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains;
use Stancl\Tenancy\TenantManager;
class TelescopeTags implements Feature
@@ -30,7 +31,15 @@ class TelescopeTags implements Feature
Telescope::tag(function (IncomingEntry $entry) {
$tags = $this->getTags($entry);
- if (in_array('tenancy', optional(request()->route())->middleware() ?? [])) {
+ if (! request()->route()) {
+ return $tags;
+ }
+
+ $tenantRoute = PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'tenancy')
+ || PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'universal');
+
+ // Don't do anything if we're visiting a universal route on a central domain
+ if ($tenantRoute && tenancy()->initialized) {
$tags = array_merge($tags, [
'tenant:' . tenant('id'),
]);
diff --git a/src/Features/TenantConfig.php b/src/Features/TenantConfig.php
index 25c8a57d..9c3d8206 100644
--- a/src/Features/TenantConfig.php
+++ b/src/Features/TenantConfig.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Features;
-use Illuminate\Config\Repository;
+use Illuminate\Contracts\Config\Repository;
use Stancl\Tenancy\Contracts\Feature;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager;
diff --git a/src/Features/Timestamps.php b/src/Features/Timestamps.php
new file mode 100644
index 00000000..309631ed
--- /dev/null
+++ b/src/Features/Timestamps.php
@@ -0,0 +1,46 @@
+config = $config;
+ }
+
+ public function bootstrap(TenantManager $tenantManager): void
+ {
+ $tenantManager->hook('tenant.creating', function ($tm, Tenant $tenant) {
+ $tenant->with('created_at', $this->now());
+ $tenant->with('updated_at', $this->now());
+ });
+
+ $tenantManager->hook('tenant.updating', function ($tm, Tenant $tenant) {
+ $tenant->with('updated_at', $this->now());
+ });
+
+ $tenantManager->hook('tenant.softDeleting', function ($tm, Tenant $tenant) {
+ $tenant->with('deleted_at', $this->now());
+ });
+ }
+
+ public function now(): string
+ {
+ // Add this key to your tenancy.php config if you need to change the format.
+ return Date::now()->format(
+ $this->config->get('tenancy.timestamp_format') ?? 'c' // ISO 8601
+ );
+ }
+}
diff --git a/src/Jobs/QueuedTenantDatabaseMigrator.php b/src/Jobs/QueuedTenantDatabaseMigrator.php
index 84acca66..c71696cc 100644
--- a/src/Jobs/QueuedTenantDatabaseMigrator.php
+++ b/src/Jobs/QueuedTenantDatabaseMigrator.php
@@ -16,12 +16,16 @@ class QueuedTenantDatabaseMigrator implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- /** @var Tenant */
- protected $tenant;
+ /** @var string */
+ protected $tenantId;
- public function __construct(Tenant $tenant)
+ /** @var array */
+ protected $migrationParameters = [];
+
+ public function __construct(Tenant $tenant, $migrationParameters = [])
{
- $this->tenant = $tenant;
+ $this->tenantId = $tenant->id;
+ $this->migrationParameters = $migrationParameters;
}
/**
@@ -32,7 +36,7 @@ class QueuedTenantDatabaseMigrator implements ShouldQueue
public function handle()
{
Artisan::call('tenants:migrate', [
- '--tenants' => [$this->tenant->id],
- ]);
+ '--tenants' => [$this->tenantId],
+ ] + $this->migrationParameters);
}
}
diff --git a/src/Jobs/QueuedTenantDatabaseSeeder.php b/src/Jobs/QueuedTenantDatabaseSeeder.php
new file mode 100644
index 00000000..e1ecea41
--- /dev/null
+++ b/src/Jobs/QueuedTenantDatabaseSeeder.php
@@ -0,0 +1,38 @@
+tenantId = $tenant->id;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ Artisan::call('tenants:seed', [
+ '--tenants' => [$this->tenantId],
+ ]);
+ }
+}
diff --git a/src/Middleware/InitializeTenancy.php b/src/Middleware/InitializeTenancy.php
index 3bc383ba..c8b3bd2f 100644
--- a/src/Middleware/InitializeTenancy.php
+++ b/src/Middleware/InitializeTenancy.php
@@ -28,10 +28,16 @@ class InitializeTenancy
*/
public function handle($request, Closure $next)
{
- try {
- tenancy()->init($request->getHost());
- } catch (TenantCouldNotBeIdentifiedException $e) {
- ($this->onFail)($e);
+ if (tenancy()->initialized) {
+ return $next($request);
+ }
+
+ if (! in_array($request->getHost(), config('tenancy.exempt_domains', []), true)) {
+ try {
+ tenancy()->init($request->getHost());
+ } catch (TenantCouldNotBeIdentifiedException $e) {
+ return ($this->onFail)($e, $request, $next);
+ }
}
return $next($request);
diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php
new file mode 100644
index 00000000..33e17a09
--- /dev/null
+++ b/src/Middleware/InitializeTenancyByRequestData.php
@@ -0,0 +1,70 @@
+header = $header;
+ $this->queryParameter = $queryParameter;
+ $this->onFail = $onFail ?? function ($e) {
+ throw $e;
+ };
+ }
+
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ * @return mixed
+ */
+ public function handle($request, Closure $next)
+ {
+ if ($request->method() !== 'OPTIONS') {
+ try {
+ $this->initializeTenancy($request);
+ } catch (TenantCouldNotBeIdentifiedException $e) {
+ return ($this->onFail)($e, $request, $next);
+ }
+ }
+
+ return $next($request);
+ }
+
+ protected function initializeTenancy(Request $request)
+ {
+ if (tenancy()->initialized) {
+ return;
+ }
+
+ $tenant = null;
+ if ($this->header && $request->hasHeader($this->header)) {
+ $tenant = $request->header($this->header);
+ } elseif ($this->queryParameter && $request->has($this->queryParameter)) {
+ $tenant = $request->get($this->queryParameter);
+ }
+
+ if (! $tenant) {
+ throw new TenantCouldNotBeIdentifiedException($request->getHost());
+ }
+
+ tenancy()->initialize(tenancy()->find($tenant));
+ }
+}
diff --git a/src/Middleware/PreventAccessFromTenantDomains.php b/src/Middleware/PreventAccessFromTenantDomains.php
index c961fada..83afb31f 100644
--- a/src/Middleware/PreventAccessFromTenantDomains.php
+++ b/src/Middleware/PreventAccessFromTenantDomains.php
@@ -5,13 +5,24 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware;
use Closure;
+use Illuminate\Routing\Route;
+use Illuminate\Support\Facades\Route as Router;
/**
- * 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.
+ * Prevent access from tenant domains to central routes and vice versa.
*/
class PreventAccessFromTenantDomains
{
+ /** @var callable */
+ protected $central404;
+
+ public function __construct(callable $central404 = null)
+ {
+ $this->central404 = $central404 ?? function () {
+ return 404;
+ };
+ }
+
/**
* Handle an incoming request.
*
@@ -21,21 +32,44 @@ class PreventAccessFromTenantDomains
*/
public function handle($request, Closure $next)
{
+ // If the route is universal, always let the request pass.
+ if ($this->routeHasMiddleware($request->route(), 'universal')) {
+ return $next($request);
+ }
+
// If the domain is not in exempt domains, it's a tenant domain.
// Tenant domains can't have routes without tenancy middleware.
$isExemptDomain = in_array($request->getHost(), config('tenancy.exempt_domains'));
$isTenantDomain = ! $isExemptDomain;
- $isTenantRoute = in_array('tenancy', $request->route()->middleware());
+ $isTenantRoute = $this->routeHasMiddleware($request->route(), 'tenancy');
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);
+ return ($this->central404)($request, $next);
}
return $next($request);
}
+
+ public static function routeHasMiddleware(Route $route, $middleware): bool
+ {
+ if (in_array($middleware, $route->middleware(), true)) {
+ return true;
+ }
+
+ // Loop one level deep and check if the route's middleware
+ // groups have a `tenancy` middleware group inside them
+ $middlewareGroups = Router::getMiddlewareGroups();
+ foreach ($route->gatherMiddleware() as $inner) {
+ if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/StorageDrivers/Database/CachedTenantResolver.php b/src/StorageDrivers/Database/CachedTenantResolver.php
new file mode 100644
index 00000000..db4d5a46
--- /dev/null
+++ b/src/StorageDrivers/Database/CachedTenantResolver.php
@@ -0,0 +1,68 @@
+cache = $cacheManager->store($config->get('tenancy.storage_drivers.db.cache_store'));
+ $this->config = $config;
+ }
+
+ protected function ttl(): int
+ {
+ return $this->config->get('tenancy.storage_drivers.db.cache_ttl');
+ }
+
+ public function getTenantIdByDomain(string $domain, Closure $query): ?string
+ {
+ return $this->cache->remember('_tenancy_domain_to_id:' . $domain, $this->ttl(), $query);
+ }
+
+ public function getDataById(string $id, Closure $dataQuery): ?array
+ {
+ return $this->cache->remember('_tenancy_id_to_data:' . $id, $this->ttl(), $dataQuery);
+ }
+
+ public function getDomainsById(string $id, Closure $domainsQuery): ?array
+ {
+ return $this->cache->remember('_tenancy_id_to_domains:' . $id, $this->ttl(), $domainsQuery);
+ }
+
+ public function invalidateTenant(string $id): void
+ {
+ $this->invalidateTenantData($id);
+ $this->invalidateTenantDomains($id);
+ }
+
+ public function invalidateTenantData(string $id): void
+ {
+ $this->cache->forget('_tenancy_id_to_data:' . $id);
+ }
+
+ public function invalidateTenantDomains(string $id): void
+ {
+ $this->cache->forget('_tenancy_id_to_domains:' . $id);
+ }
+
+ public function invalidateDomainToIdMapping(array $domains): void
+ {
+ foreach ($domains as $domain) {
+ $this->cache->forget('_tenancy_domain_to_id:' . $domain);
+ }
+ }
+}
diff --git a/src/StorageDrivers/Database/DatabaseStorageDriver.php b/src/StorageDrivers/Database/DatabaseStorageDriver.php
index 4f5c64bb..1303ba38 100644
--- a/src/StorageDrivers/Database/DatabaseStorageDriver.php
+++ b/src/StorageDrivers/Database/DatabaseStorageDriver.php
@@ -4,40 +4,55 @@ declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
+use Illuminate\Config\Repository as ConfigRepository;
+use Illuminate\Database\Connection;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\DB;
+use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
+use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
+use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
-use Stancl\Tenancy\StorageDrivers\Database\DomainModel as Domains;
-use Stancl\Tenancy\StorageDrivers\Database\TenantModel as Tenants;
use Stancl\Tenancy\Tenant;
-class DatabaseStorageDriver implements StorageDriver
+class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAnyKey
{
/** @var Application */
protected $app;
- /** @var \Illuminate\Database\Connection */
+ /** @var Connection */
protected $centralDatabase;
+ /** @var TenantRepository */
+ protected $tenants;
+
+ /** @var DomainRepository */
+ protected $domains;
+
+ /** @var CachedTenantResolver */
+ protected $cache;
+
/** @var Tenant The default tenant. */
protected $tenant;
- public function __construct(Application $app)
+ public function __construct(Application $app, ConfigRepository $config, CachedTenantResolver $cache)
{
$this->app = $app;
+ $this->cache = $cache;
$this->centralDatabase = $this->getCentralConnection();
+ $this->tenants = new TenantRepository($config);
+ $this->domains = new DomainRepository($config);
}
/**
* Get the central database connection.
*
- * @return \Illuminate\Database\Connection
+ * @return Connection
*/
- public static function getCentralConnection(): \Illuminate\Database\Connection
+ public static function getCentralConnection(): Connection
{
return DB::connection(static::getCentralConnectionName());
}
@@ -49,7 +64,16 @@ class DatabaseStorageDriver implements StorageDriver
public function findByDomain(string $domain): Tenant
{
- $id = $this->getTenantIdByDomain($domain);
+ $query = function () use ($domain) {
+ return $this->domains->getTenantIdByDomain($domain);
+ };
+
+ if ($this->usesCache()) {
+ $id = $this->cache->getTenantIdByDomain($domain, $query);
+ } else {
+ $id = $query();
+ }
+
if (! $id) {
throw new TenantCouldNotBeIdentifiedException($domain);
}
@@ -59,24 +83,58 @@ class DatabaseStorageDriver implements StorageDriver
public function findById(string $id): Tenant
{
- return Tenant::fromStorage(Tenants::find($id)->decoded())
- ->withDomains($this->getTenantDomains($id));
+ $dataQuery = function () use ($id) {
+ $data = $this->tenants->find($id);
+
+ return $data ? $this->tenants->decodeData($data) : null;
+ };
+ $domainsQuery = function () use ($id) {
+ return $this->domains->getTenantDomains($id);
+ };
+
+ if ($this->usesCache()) {
+ $data = $this->cache->getDataById($id, $dataQuery);
+ $domains = $this->cache->getDomainsById($id, $domainsQuery);
+ } else {
+ $data = $dataQuery();
+ $domains = $domainsQuery();
+ }
+
+ if (! $data) {
+ throw new TenantDoesNotExistException($id);
+ }
+
+ return Tenant::fromStorage($data)
+ ->withDomains($domains);
}
- protected function getTenantDomains($id)
+ /**
+ * Find a tenant using an arbitrary key.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return Tenant
+ * @throws TenantDoesNotExistException
+ */
+ public function findBy(string $key, $value): Tenant
{
- return Domains::where('tenant_id', $id)->get()->map(function ($model) {
- return $model->domain;
- })->toArray();
+ $tenant = $this->tenants->findBy($key, $value);
+
+ if (! $tenant) {
+ throw new TenantDoesNotExistException($value, $key);
+ }
+
+ return Tenant::fromStorage($this->tenants->decodeData($tenant))
+ ->withDomains($this->domains->getTenantDomains($tenant['id']));
}
public function ensureTenantCanBeCreated(Tenant $tenant): void
{
- if (Tenants::find($tenant->id)) {
+ if ($this->tenants->exists($tenant)) {
throw new TenantWithThisIdAlreadyExistsException($tenant->id);
}
- if (Domains::whereIn('domain', $tenant->domains)->exists()) {
+ if ($this->domains->occupied($tenant->domains)) {
throw new DomainsOccupiedByOtherTenantException;
}
}
@@ -88,52 +146,43 @@ class DatabaseStorageDriver implements StorageDriver
return $this;
}
- public function getTenantIdByDomain(string $domain): ?string
- {
- return Domains::where('domain', $domain)->first()->tenant_id ?? null;
- }
-
public function createTenant(Tenant $tenant): void
{
$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::insert($domainData);
+ $this->tenants->insert($tenant);
+ $this->domains->insertTenantDomains($tenant);
});
}
public function updateTenant(Tenant $tenant): void
{
- $this->centralDatabase->transaction(function () use ($tenant) {
- Tenants::find($tenant->id)->putMany($tenant->data);
+ $originalDomains = $this->domains->getTenantDomains($tenant);
- $original_domains = Domains::where('tenant_id', $tenant->id)->get()->map(function ($model) {
- return $model->domain;
- })->toArray();
- $deleted_domains = array_diff($original_domains, $tenant->domains);
+ $this->centralDatabase->transaction(function () use ($tenant, $originalDomains) {
+ $this->tenants->updateTenant($tenant);
- Domains::whereIn('domain', $deleted_domains)->delete();
-
- foreach ($tenant->domains as $domain) {
- Domains::firstOrCreate([
- 'tenant_id' => $tenant->id,
- 'domain' => $domain,
- ]);
- }
+ $this->domains->updateTenantDomains($tenant, $originalDomains);
});
+
+ if ($this->usesCache()) {
+ $this->cache->invalidateTenant($tenant->id);
+ $this->cache->invalidateDomainToIdMapping($originalDomains);
+ }
}
public function deleteTenant(Tenant $tenant): void
{
+ $originalDomains = $this->domains->getTenantDomains($tenant);
+
$this->centralDatabase->transaction(function () use ($tenant) {
- Tenants::find($tenant->id)->delete();
- Domains::where('tenant_id', $tenant->id)->delete();
+ $this->tenants->where('id', $tenant->id)->delete();
+ $this->domains->where('tenant_id', $tenant->id)->delete();
});
+
+ if ($this->usesCache()) {
+ $this->cache->invalidateTenant($tenant->id);
+ $this->cache->invalidateDomainToIdMapping($originalDomains);
+ }
}
/**
@@ -144,8 +193,9 @@ class DatabaseStorageDriver implements StorageDriver
*/
public function all(array $ids = []): array
{
- return Tenants::getAllTenants($ids)->map(function ($data) {
- return Tenant::fromStorage($data)->withDomains($this->getTenantDomains($data['id']));
+ return $this->tenants->all($ids)->map(function ($data) {
+ return Tenant::fromStorage($data)
+ ->withDomains($this->domains->getTenantDomains($data['id']));
})->toArray();
}
@@ -161,27 +211,46 @@ class DatabaseStorageDriver implements StorageDriver
public function get(string $key, Tenant $tenant = null)
{
- $tenant = $tenant ?? $this->currentTenant();
-
- return Tenants::find($tenant->id)->get($key);
+ return $this->tenants->get($key, $tenant ?? $this->currentTenant());
}
public function getMany(array $keys, Tenant $tenant = null): array
{
- $tenant = $tenant ?? $this->currentTenant();
-
- return Tenants::find($tenant->id)->getMany($keys);
+ return $this->tenants->getMany($keys, $tenant ?? $this->currentTenant());
}
public function put(string $key, $value, Tenant $tenant = null): void
{
$tenant = $tenant ?? $this->currentTenant();
- Tenants::find($tenant->id)->put($key, $value);
+ $this->tenants->put($key, $value, $tenant);
+
+ if ($this->usesCache()) {
+ $this->cache->invalidateTenantData($tenant->id);
+ }
}
public function putMany(array $kvPairs, Tenant $tenant = null): void
{
$tenant = $tenant ?? $this->currentTenant();
- Tenants::find($tenant->id)->putMany($kvPairs);
+ $this->tenants->putMany($kvPairs, $tenant);
+
+ if ($this->usesCache()) {
+ $this->cache->invalidateTenantData($tenant->id);
+ }
+ }
+
+ public function deleteMany(array $keys, Tenant $tenant = null): void
+ {
+ $tenant = $tenant ?? $this->currentTenant();
+ $this->tenants->deleteMany($keys, $tenant);
+
+ if ($this->usesCache()) {
+ $this->cache->invalidateTenantData($tenant->id);
+ }
+ }
+
+ public function usesCache(): bool
+ {
+ return $this->app['config']['tenancy.storage_drivers.db.cache_store'] !== null;
}
}
diff --git a/src/StorageDrivers/Database/DomainModel.php b/src/StorageDrivers/Database/DomainModel.php
deleted file mode 100644
index abddff4b..00000000
--- a/src/StorageDrivers/Database/DomainModel.php
+++ /dev/null
@@ -1,25 +0,0 @@
-where('domain', $domain)->first()->tenant_id ?? null;
+ }
+
+ public function occupied(array $domains): bool
+ {
+ return $this->whereIn('domain', $domains)->exists();
+ }
+
+ public function getTenantDomains($tenant)
+ {
+ $id = $tenant instanceof Tenant ? $tenant->id : $tenant;
+
+ return $this->where('tenant_id', $id)->get('domain')->pluck('domain')->all();
+ }
+
+ public function insertTenantDomains(Tenant $tenant)
+ {
+ $this->insert(array_map(function ($domain) use ($tenant) {
+ return ['domain' => $domain, 'tenant_id' => $tenant->id];
+ }, $tenant->domains));
+ }
+
+ public function updateTenantDomains(Tenant $tenant, array $originalDomains)
+ {
+ $deletedDomains = array_diff($originalDomains, $tenant->domains);
+ $newDomains = array_diff($tenant->domains, $originalDomains);
+
+ $this->whereIn('domain', $deletedDomains)->delete();
+
+ foreach ($newDomains as $domain) {
+ $this->insert([
+ 'tenant_id' => $tenant->id,
+ 'domain' => $domain,
+ ]);
+ }
+ }
+
+ public function getTable(ConfigRepository $config)
+ {
+ return $config->get('tenancy.storage_drivers.db.table_names.DomainModel') // legacy
+ ?? $config->get('tenancy.storage_drivers.db.table_names.domains')
+ ?? 'domains';
+ }
+}
diff --git a/src/StorageDrivers/Database/Repository.php b/src/StorageDrivers/Database/Repository.php
new file mode 100644
index 00000000..e3cce981
--- /dev/null
+++ b/src/StorageDrivers/Database/Repository.php
@@ -0,0 +1,41 @@
+database = DatabaseStorageDriver::getCentralConnection();
+ $this->tableName = $this->getTable($config);
+ $this->table = $this->database->table($this->tableName);
+ }
+
+ public function table()
+ {
+ return $this->table->newQuery()->from($this->tableName);
+ }
+
+ abstract public function getTable(ConfigRepository $config);
+
+ public function __call($method, $parameters)
+ {
+ return $this->table()->$method(...$parameters);
+ }
+}
diff --git a/src/StorageDrivers/Database/TenantModel.php b/src/StorageDrivers/Database/TenantModel.php
deleted file mode 100644
index 782d0308..00000000
--- a/src/StorageDrivers/Database/TenantModel.php
+++ /dev/null
@@ -1,124 +0,0 @@
-map([__CLASS__, 'decodeData'])->toBase();
- }
-
- public function decoded()
- {
- return static::decodeData($this);
- }
-
- /**
- * Return a tenant array with data decoded into separate keys.
- *
- * @param self|array $tenant
- * @return array
- */
- public static function decodeData($tenant)
- {
- $tenant = $tenant instanceof self ? (array) $tenant->attributes : $tenant;
- $decoded = json_decode($tenant[$dataColumn = static::dataColumn()], true);
-
- foreach ($decoded as $key => $value) {
- $tenant[$key] = $value;
- }
-
- // If $tenant[$dataColumn] has been overriden by a value, don't delete the key.
- if (! array_key_exists($dataColumn, $decoded)) {
- unset($tenant[$dataColumn]);
- }
-
- return $tenant;
- }
-
- public function getFromData(string $key)
- {
- $this->dataArray = $this->dataArray ?? json_decode($this->{$this->dataColumn()}, true);
-
- return $this->dataArray[$key] ?? null;
- }
-
- public function get(string $key)
- {
- return $this->attributes[$key] ?? $this->getFromData($key) ?? null;
- }
-
- public function getMany(array $keys): array
- {
- return array_reduce($keys, function ($result, $key) {
- $result[$key] = $this->get($key);
-
- return $result;
- }, []);
- }
-
- public function put(string $key, $value)
- {
- if (in_array($key, $this->customColumns())) {
- $this->update([$key => $value]);
- } else {
- $obj = json_decode($this->{$this->dataColumn()});
- $obj->$key = $value;
-
- $this->update([$this->dataColumn() => json_encode($obj)]);
- }
-
- return $value;
- }
-
- public function putMany(array $kvPairs)
- {
- $customColumns = [];
- $jsonObj = json_decode($this->{$this->dataColumn()});
-
- foreach ($kvPairs as $key => $value) {
- if (in_array($key, $this->customColumns())) {
- $customColumns[$key] = $value;
- continue;
- }
-
- $jsonObj->$key = $value;
- }
-
- $this->update(array_merge($customColumns, [
- $this->dataColumn() => json_encode($jsonObj),
- ]));
- }
-}
diff --git a/src/StorageDrivers/Database/TenantRepository.php b/src/StorageDrivers/Database/TenantRepository.php
new file mode 100644
index 00000000..adaf054a
--- /dev/null
+++ b/src/StorageDrivers/Database/TenantRepository.php
@@ -0,0 +1,186 @@
+whereIn('id', $ids)->get();
+ } else {
+ $data = $this->table()->get();
+ }
+
+ return $data->map(function (stdClass $obj) {
+ return $this->decodeData((array) $obj);
+ });
+ }
+
+ public function find($tenant)
+ {
+ return (array) $this->table()->find(
+ $tenant instanceof Tenant ? $tenant->id : $tenant
+ );
+ }
+
+ public function findBy(string $key, $value)
+ {
+ if (in_array($key, static::customColumns())) {
+ return (array) $this->table()->where($key, $value)->first();
+ }
+
+ return (array) $this->table()->where(
+ static::dataColumn() . '->' . $key,
+ $value
+ )->first();
+ }
+
+ public function updateTenant(Tenant $tenant)
+ {
+ $this->putMany($tenant->data, $tenant);
+ }
+
+ public function exists(Tenant $tenant)
+ {
+ return $this->where('id', $tenant->id)->exists();
+ }
+
+ public function get(string $key, Tenant $tenant)
+ {
+ return $this->decodeFreshDataForTenant($tenant)[$key] ?? null;
+ }
+
+ public function getMany(array $keys, Tenant $tenant)
+ {
+ $decodedData = $this->decodeFreshDataForTenant($tenant);
+
+ $result = [];
+
+ foreach ($keys as $key) {
+ $result[$key] = $decodedData[$key] ?? null;
+ }
+
+ return $result;
+ }
+
+ public function put(string $key, $value, Tenant $tenant)
+ {
+ $record = $this->where('id', $tenant->id);
+
+ if (in_array($key, static::customColumns())) {
+ $record->update([$key => $value]);
+ } else {
+ $data = json_decode($record->first()->{static::dataColumn()}, true);
+ $data[$key] = $value;
+
+ $record->update([static::dataColumn() => $data]);
+ }
+ }
+
+ public function putMany(array $kvPairs, Tenant $tenant)
+ {
+ $record = $this->where('id', $tenant->id);
+
+ $data = [];
+ $jsonData = json_decode($record->first()->{static::dataColumn()}, true);
+ foreach ($kvPairs as $key => $value) {
+ if (in_array($key, static::customColumns())) {
+ $data[$key] = $value;
+ continue;
+ } else {
+ $jsonData[$key] = $value;
+ }
+ }
+
+ $data[static::dataColumn()] = json_encode($jsonData);
+
+ $record->update($data);
+ }
+
+ public function deleteMany(array $keys, Tenant $tenant)
+ {
+ $record = $this->where('id', $tenant->id);
+
+ $data = [];
+ $jsonData = json_decode($record->first(static::dataColumn())->data, true);
+ foreach ($keys as $key) {
+ if (in_array($key, static::customColumns())) {
+ $data[$key] = null;
+
+ continue;
+ } else {
+ unset($jsonData[$key]);
+ }
+ }
+
+ $data[static::dataColumn()] = json_encode($jsonData);
+
+ $record->update($data);
+ }
+
+ public function decodeFreshDataForTenant(Tenant $tenant): array
+ {
+ return $this->decodeData(
+ (array) $this->table()->where('id', $tenant->id)->first()
+ );
+ }
+
+ public static function decodeData(array $columns): array
+ {
+ $dataColumn = static::dataColumn();
+ $decoded = json_decode($columns[$dataColumn], true);
+ $columns = array_merge($columns, $decoded);
+
+ // If $columns[$dataColumn] has been overriden by a value, don't delete the key.
+ if (! array_key_exists($dataColumn, $decoded)) {
+ unset($columns[$dataColumn]);
+ }
+
+ return $columns;
+ }
+
+ public function insert(Tenant $tenant)
+ {
+ $this->table()->insert(array_merge(
+ $this->encodeData($tenant->data),
+ ['id' => $tenant->id]
+ ));
+ }
+
+ public static function encodeData(array $data): array
+ {
+ $result = [];
+ foreach (array_intersect(static::customColumns(), array_keys($data)) as $customColumn) {
+ $result[$customColumn] = $data[$customColumn];
+ unset($data[$customColumn]);
+ }
+
+ $result = array_merge($result, [static::dataColumn() => json_encode($data)]);
+
+ return $result;
+ }
+
+ public static function customColumns(): array
+ {
+ return config('tenancy.storage_drivers.db.custom_columns', []);
+ }
+
+ public static function dataColumn(): string
+ {
+ return config('tenancy.storage_drivers.db.data_column', 'data');
+ }
+
+ public function getTable(ConfigRepository $config)
+ {
+ return $config->get('tenancy.storage_drivers.db.table_names.TenantModel') // legacy
+ ?? $config->get('tenancy.storage_drivers.db.table_names.tenants')
+ ?? 'tenants';
+ }
+}
diff --git a/src/StorageDrivers/RedisStorageDriver.php b/src/StorageDrivers/RedisStorageDriver.php
index 4e35834f..79852f8c 100644
--- a/src/StorageDrivers/RedisStorageDriver.php
+++ b/src/StorageDrivers/RedisStorageDriver.php
@@ -6,13 +6,15 @@ namespace Stancl\Tenancy\StorageDrivers;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Foundation\Application;
+use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
+use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\Tenant;
-class RedisStorageDriver implements StorageDriver
+class RedisStorageDriver implements StorageDriver, CanDeleteKeys
{
/** @var Application */
protected $app;
@@ -73,7 +75,13 @@ class RedisStorageDriver implements StorageDriver
public function findById(string $id): Tenant
{
- return $this->makeTenant($this->redis->hgetall("tenants:$id"));
+ $data = $this->redis->hgetall("tenants:$id");
+
+ if (! $data) {
+ throw new TenantDoesNotExistException($id);
+ }
+
+ return $this->makeTenant($data);
}
public function getTenantIdByDomain(string $domain): ?string
@@ -223,4 +231,11 @@ class RedisStorageDriver implements StorageDriver
$this->redis->hmset("tenants:{$tenant->id}", $kvPairs);
}
+
+ public function deleteMany(array $keys, Tenant $tenant = null): void
+ {
+ $tenant = $tenant ?? $this->tenant();
+
+ $this->redis->hdel("tenants:{$tenant->id}", ...$keys);
+ }
}
diff --git a/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php b/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php
index e93e7301..8920f44a 100644
--- a/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php
+++ b/src/TenancyBootstrappers/DatabaseTenancyBootstrapper.php
@@ -6,6 +6,7 @@ namespace Stancl\Tenancy\TenancyBootstrappers;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\DatabaseManager;
+use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException;
use Stancl\Tenancy\Tenant;
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
@@ -20,6 +21,11 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
public function start(Tenant $tenant)
{
+ $database = $tenant->getDatabaseName();
+ if (! $this->database->getTenantDatabaseManager($tenant)->databaseExists($database)) {
+ throw new TenantDatabaseDoesNotExistException($database);
+ }
+
$this->database->connect($tenant);
}
diff --git a/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php b/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php
index b08a79e2..35ea5b2b 100644
--- a/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php
+++ b/src/TenancyBootstrappers/FilesystemTenancyBootstrapper.php
@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenancyBootstrappers;
-use Illuminate\Contracts\Foundation\Application;
+use Illuminate\Filesystem\FilesystemAdapter;
+use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant;
@@ -38,26 +39,32 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
$suffix = $this->app['config']['tenancy.filesystem.suffix_base'] . $tenant->id;
// storage_path()
- $this->app->useStoragePath($this->originalPaths['storage'] . "/{$suffix}");
+ if ($this->app['config']['tenancy.filesystem.suffix_storage_path'] ?? true) {
+ $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' => '']));
+ if ($this->app['config']['tenancy.filesystem.asset_helper_tenancy'] ?? true) {
+ 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();
+ /** @var FilesystemAdapter $filesystemDisk */
+ $filesystemDisk = Storage::disk($disk);
+ $this->originalPaths['disks'][$disk] = $filesystemDisk->getAdapter()->getPathPrefix();
if ($root = str_replace('%storage_path%', storage_path(), $this->app['config']["tenancy.filesystem.root_override.{$disk}"])) {
- Storage::disk($disk)->getAdapter()->setPathPrefix($root);
+ $filesystemDisk->getAdapter()->setPathPrefix($root);
} else {
$root = $this->app['config']["filesystems.disks.{$disk}.root"];
- Storage::disk($disk)->getAdapter()->setPathPrefix($root . "/{$suffix}");
+ $filesystemDisk->getAdapter()->setPathPrefix($root . "/{$suffix}");
}
}
}
@@ -73,7 +80,10 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
// Storage facade
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
- Storage::disk($disk)->getAdapter()->setPathPrefix($this->originalPaths['disks'][$disk]);
+ /** @var FilesystemAdapter $filesystemDisk */
+ $filesystemDisk = Storage::disk($disk);
+
+ $filesystemDisk->getAdapter()->setPathPrefix($this->originalPaths['disks'][$disk]);
}
}
}
diff --git a/src/TenancyBootstrappers/QueueTenancyBootstrapper.php b/src/TenancyBootstrappers/QueueTenancyBootstrapper.php
index d37f9426..b2a0fbf2 100644
--- a/src/TenancyBootstrappers/QueueTenancyBootstrapper.php
+++ b/src/TenancyBootstrappers/QueueTenancyBootstrapper.php
@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenancyBootstrappers;
-use Illuminate\Contracts\Foundation\Application;
+use Illuminate\Config\Repository;
+use Illuminate\Queue\QueueManager;
use Illuminate\Support\Testing\Fakes\QueueFake;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant;
@@ -14,19 +15,18 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
/** @var bool Has tenancy been started. */
public $started = false;
- /** @var Application */
- protected $app;
+ /** @var Repository */
+ protected $config;
- public function __construct(Application $app)
+ public function __construct(Repository $config, QueueManager $queue)
{
- $this->app = $app;
+ $this->config = $config;
$bootstrapper = &$this;
- $queue = $this->app['queue'];
if (! $queue instanceof QueueFake) {
- $queue->createPayloadUsing(function () use (&$bootstrapper) {
- return $bootstrapper->getPayload();
+ $queue->createPayloadUsing(function ($connection) use (&$bootstrapper) {
+ return $bootstrapper->getPayload($connection);
});
}
}
@@ -41,12 +41,16 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
$this->started = false;
}
- public function getPayload()
+ public function getPayload(string $connection)
{
if (! $this->started) {
return [];
}
+ if ($this->config["queue.connections.$connection.central"]) {
+ return [];
+ }
+
$id = tenant('id');
return [
diff --git a/src/TenancyBootstrappers/RedisTenancyBootstrapper.php b/src/TenancyBootstrappers/RedisTenancyBootstrapper.php
index 53dbd339..ae20e2c4 100644
--- a/src/TenancyBootstrappers/RedisTenancyBootstrapper.php
+++ b/src/TenancyBootstrappers/RedisTenancyBootstrapper.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenancyBootstrappers;
-use Illuminate\Config\Repository;
+use Illuminate\Contracts\Config\Repository;
use Illuminate\Support\Facades\Redis;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Tenant;
diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php
index 7f8e86ad..b5c41c73 100644
--- a/src/TenancyServiceProvider.php
+++ b/src/TenancyServiceProvider.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy;
use Illuminate\Cache\CacheManager;
+use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper;
@@ -77,24 +78,52 @@ class TenancyServiceProvider extends ServiceProvider
__DIR__ . '/../assets/migrations/' => database_path('migrations'),
], 'migrations');
- $this->loadRoutesFrom(__DIR__ . '/routes.php');
+ foreach ($this->app['config']['tenancy.global_middleware'] as $middleware) {
+ $this->app->make(Kernel::class)->prependMiddleware($middleware);
+ }
+ /*
+ * Since tenancy is initialized in the global middleware stack, this
+ * middleware group acts mostly as a 'flag' for the PreventAccess
+ * middleware to decide whether the request should be aborted.
+ */
Route::middlewareGroup('tenancy', [
- \Stancl\Tenancy\Middleware\InitializeTenancy::class,
+ /* Prevent access from tenant domains to central routes and vice versa. */
+ Middleware\PreventAccessFromTenantDomains::class,
]);
+ Route::middlewareGroup('universal', []);
+
+ $this->loadRoutesFrom(__DIR__ . '/routes.php');
+
$this->app->singleton('globalUrl', function ($app) {
- $instance = clone $app['url'];
- $instance->setAssetRoot($app[FilesystemTenancyBootstrapper::class]->originalPaths['asset_url']);
+ if ($app->bound(FilesystemTenancyBootstrapper::class)) {
+ $instance = clone $app['url'];
+ $instance->setAssetRoot($app[FilesystemTenancyBootstrapper::class]->originalPaths['asset_url']);
+ } else {
+ $instance = $app['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']));
+ $tenantId = $event->job->payload()['tenant_id'] ?? null;
+
+ // The job is not tenant-aware
+ if (! $tenantId) {
+ return;
}
+
+ // Tenancy is already initialized for the tenant (e.g. dispatchNow was used)
+ if (tenancy()->initialized && tenant('id') === $tenantId) {
+ return;
+ }
+
+ // Tenancy was either not initialized, or initialized for a different tenant.
+ // Therefore, we initialize it for the correct tenant.
+ tenancy()->initById($tenantId);
});
}
}
diff --git a/src/Tenant.php b/src/Tenant.php
index 836e27b8..38ac7261 100644
--- a/src/Tenant.php
+++ b/src/Tenant.php
@@ -5,11 +5,15 @@ declare(strict_types=1);
namespace Stancl\Tenancy;
use ArrayAccess;
+use Closure;
+use Illuminate\Config\Repository;
use Illuminate\Foundation\Application;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ForwardsCalls;
+use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
+use Stancl\Tenancy\Exceptions\NotImplementedException;
use Stancl\Tenancy\Exceptions\TenantStorageException;
/**
@@ -34,10 +38,10 @@ class Tenant implements ArrayAccess
*/
public $domains = [];
- /** @var Application */
- protected $app;
+ /** @var Repository */
+ protected $config;
- /** @var StorageDriver */
+ /** @var StorageDriver|CanDeleteKeys */
protected $storage;
/** @var TenantManager */
@@ -56,14 +60,13 @@ class Tenant implements ArrayAccess
/**
* Use new() if you don't want to swap dependencies.
*
- * @param Application $app
* @param StorageDriver $storage
* @param TenantManager $tenantManager
* @param UniqueIdentifierGenerator $idGenerator
*/
- public function __construct(Application $app, StorageDriver $storage, TenantManager $tenantManager, UniqueIdentifierGenerator $idGenerator)
+ public function __construct(Repository $config, StorageDriver $storage, TenantManager $tenantManager, UniqueIdentifierGenerator $idGenerator)
{
- $this->app = $app;
+ $this->config = $config;
$this->storage = $storage->withDefaultTenant($this);
$this->manager = $tenantManager;
$this->idGenerator = $idGenerator;
@@ -80,7 +83,7 @@ class Tenant implements ArrayAccess
$app = $app ?? app();
return new static(
- $app,
+ $app[Repository::class],
$app[StorageDriver::class],
$app[TenantManager::class],
$app[UniqueIdentifierGenerator::class]
@@ -230,8 +233,6 @@ class Tenant implements ArrayAccess
$this->manager->createTenant($this);
}
- $this->persisted = true;
-
return $this;
}
@@ -257,10 +258,16 @@ class Tenant implements ArrayAccess
*/
public function softDelete(): self
{
- $this->put('_tenancy_original_domains', $this->domains);
+ $this->manager->event('tenant.softDeleting', $this);
+
+ $this->put([
+ '_tenancy_original_domains' => $this->domains,
+ ]);
$this->clearDomains();
$this->save();
+ $this->manager->event('tenant.softDeleted', $this);
+
return $this;
}
@@ -271,7 +278,7 @@ class Tenant implements ArrayAccess
*/
public function getDatabaseName(): string
{
- return $this->data['_tenancy_db_name'] ?? ($this->app['config']['tenancy.database.prefix'] . $this->id . $this->app['config']['tenancy.database.suffix']);
+ return $this->data['_tenancy_db_name'] ?? ($this->config->get('tenancy.database.prefix') . $this->id . $this->config->get('tenancy.database.suffix'));
}
/**
@@ -325,20 +332,30 @@ class Tenant implements ArrayAccess
*/
public function put($key, $value = null): self
{
+ $this->manager->event('tenant.updating', $this);
+
if ($key === 'id') {
throw new TenantStorageException("Tenant ids can't be changed.");
}
if (is_array($key)) {
- $this->storage->putMany($key);
+ if ($this->persisted) {
+ $this->storage->putMany($key);
+ }
+
foreach ($key as $k => $v) { // Add to cache
$this->data[$k] = $v;
}
} else {
- $this->storage->put($key, $value);
+ if ($this->persisted) {
+ $this->storage->put($key, $value);
+ }
+
$this->data[$key] = $value;
}
+ $this->manager->event('tenant.updated', $this);
+
return $this;
}
@@ -349,7 +366,44 @@ class Tenant implements ArrayAccess
}
/**
- * Set a value.
+ * Delete a key from the tenant's storage.
+ *
+ * @param string $key
+ * @return self
+ */
+ public function deleteKey(string $key): self
+ {
+ return $this->deleteKeys([$key]);
+ }
+
+ /**
+ * Delete keys from the tenant's storage.
+ *
+ * @param string[] $keys
+ * @return self
+ */
+ public function deleteKeys(array $keys): self
+ {
+ $this->manager->event('tenant.updating', $this);
+
+ if (! $this->storage instanceof CanDeleteKeys) {
+ throw new NotImplementedException(get_class($this->storage), 'deleteMany',
+ 'This method was added to storage drivers provided by the package in 2.2.0 and will be part of the StorageDriver contract in 3.0.0.'
+ );
+ } else {
+ $this->storage->deleteMany($keys);
+ foreach ($keys as $key) {
+ unset($this->data[$key]);
+ }
+ }
+
+ $this->manager->event('tenant.updated', $this);
+
+ return $this;
+ }
+
+ /**
+ * Set a value in the data array without saving into storage.
*
* @param string $key
* @param mixed $value
@@ -362,6 +416,27 @@ class Tenant implements ArrayAccess
return $this;
}
+ /**
+ * Run a closure inside the tenant's environment.
+ *
+ * @param Closure $closure
+ * @return mixed
+ */
+ public function run(Closure $closure)
+ {
+ $originalTenant = $this->manager->getTenant();
+
+ $this->manager->initializeTenancy($this);
+ $result = $closure($this);
+ $this->manager->endTenancy($this);
+
+ if ($originalTenant) {
+ $this->manager->initializeTenancy($originalTenant);
+ }
+
+ return $result;
+ }
+
public function __get($key)
{
return $this->get($key);
diff --git a/src/TenantDatabaseManagers/MySQLDatabaseManager.php b/src/TenantDatabaseManagers/MySQLDatabaseManager.php
index 11d8c123..f6c4ef96 100644
--- a/src/TenantDatabaseManagers/MySQLDatabaseManager.php
+++ b/src/TenantDatabaseManagers/MySQLDatabaseManager.php
@@ -4,32 +4,47 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenantDatabaseManagers;
-use Illuminate\Config\Repository;
-use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager;
+use Illuminate\Contracts\Config\Repository;
+use Illuminate\Database\Connection;
+use Illuminate\Support\Facades\DB;
+use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
-class MySQLDatabaseManager implements TenantDatabaseManager
+class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
{
- /** @var \Illuminate\Database\Connection */
- protected $database;
+ /** @var string */
+ protected $connection;
- public function __construct(Repository $config, IlluminateDatabaseManager $databaseManager)
+ public function __construct(Repository $config)
{
- $this->database = $databaseManager->connection($config['tenancy.database_manager_connections.mysql']);
+ $this->connection = $config->get('tenancy.database_manager_connections.mysql');
+ }
+
+ protected function database(): Connection
+ {
+ return DB::connection($this->connection);
+ }
+
+ public function setConnection(string $connection): void
+ {
+ $this->connection = $connection;
}
public function createDatabase(string $name): bool
{
- return $this->database->statement("CREATE DATABASE `$name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+ $charset = $this->database()->getConfig('charset');
+ $collation = $this->database()->getConfig('collation');
+
+ return $this->database()->statement("CREATE DATABASE `$name` CHARACTER SET `$charset` COLLATE `$collation`");
}
public function deleteDatabase(string $name): bool
{
- return $this->database->statement("DROP DATABASE `$name`");
+ return $this->database()->statement("DROP DATABASE `$name`");
}
public function databaseExists(string $name): bool
{
- return (bool) $this->database->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'");
}
}
diff --git a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php
index d6c974e7..fc21668e 100644
--- a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php
+++ b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php
@@ -4,32 +4,44 @@ declare(strict_types=1);
namespace Stancl\Tenancy\TenantDatabaseManagers;
-use Illuminate\Config\Repository;
-use Illuminate\Database\DatabaseManager as IlluminateDatabaseManager;
+use Illuminate\Contracts\Config\Repository;
+use Illuminate\Database\Connection;
+use Illuminate\Support\Facades\DB;
+use Stancl\Tenancy\Contracts\Future\CanSetConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
-class PostgreSQLDatabaseManager implements TenantDatabaseManager
+class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
{
- /** @var \Illuminate\Database\Connection */
- protected $database;
+ /** @var string */
+ protected $connection;
- public function __construct(Repository $config, IlluminateDatabaseManager $databaseManager)
+ public function __construct(Repository $config)
{
- $this->database = $databaseManager->connection($config['tenancy.database_manager_connections.pgsql']);
+ $this->connection = $config->get('tenancy.database_manager_connections.pgsql');
+ }
+
+ protected function database(): Connection
+ {
+ return DB::connection($this->connection);
+ }
+
+ public function setConnection(string $connection): void
+ {
+ $this->connection = $connection;
}
public function createDatabase(string $name): bool
{
- return $this->database->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0");
+ return $this->database()->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0");
}
public function deleteDatabase(string $name): bool
{
- return $this->database->statement("DROP DATABASE \"$name\"");
+ return $this->database()->statement("DROP DATABASE \"$name\"");
}
public function databaseExists(string $name): bool
{
- return (bool) $this->database->select("SELECT datname FROM pg_database WHERE datname = '$name'");
+ return (bool) $this->database()->select("SELECT datname FROM pg_database WHERE datname = '$name'");
}
}
diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
new file mode 100644
index 00000000..a93ed901
--- /dev/null
+++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
@@ -0,0 +1,47 @@
+connection = $config->get('tenancy.database_manager_connections.pgsql');
+ }
+
+ protected function database(): Connection
+ {
+ return DB::connection($this->connection);
+ }
+
+ public function setConnection(string $connection): void
+ {
+ $this->connection = $connection;
+ }
+
+ public function createDatabase(string $name): bool
+ {
+ return $this->database()->statement("CREATE SCHEMA \"$name\"");
+ }
+
+ public function deleteDatabase(string $name): bool
+ {
+ return $this->database()->statement("DROP SCHEMA \"$name\"");
+ }
+
+ public function databaseExists(string $name): bool
+ {
+ return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'");
+ }
+}
diff --git a/src/TenantManager.php b/src/TenantManager.php
index 2fb5f412..103ecc8c 100644
--- a/src/TenantManager.php
+++ b/src/TenantManager.php
@@ -4,23 +4,27 @@ declare(strict_types=1);
namespace Stancl\Tenancy;
+use Exception;
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
use Illuminate\Foundation\Application;
use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
+use Illuminate\Support\Traits\ForwardsCalls;
+use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
+use Stancl\Tenancy\Exceptions\NotImplementedException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
+use Stancl\Tenancy\Jobs\QueuedTenantDatabaseSeeder;
/**
* @internal Class is subject to breaking changes in minor and patch versions.
*/
class TenantManager
{
- /**
- * The current tenant.
- *
- * @var Tenant
- */
+ use ForwardsCalls;
+
+ /** @var Tenant The current tenant. */
protected $tenant;
/** @var Application */
@@ -46,7 +50,7 @@ class TenantManager
$this->app = $app;
$this->storage = $storage;
$this->artisan = $artisan;
- $this->database = $database;
+ $this->database = $database->withTenantManager($this);
$this->bootstrapFeatures();
}
@@ -59,21 +63,43 @@ class TenantManager
*/
public function createTenant(Tenant $tenant): self
{
+ $this->event('tenant.creating', $tenant);
+
$this->ensureTenantCanBeCreated($tenant);
$this->storage->createTenant($tenant);
- $this->database->createDatabase($tenant);
+
+ $tenant->persisted = true;
+
+ /** @var \Illuminate\Contracts\Queue\ShouldQueue[]|callable[] $afterCreating */
+ $afterCreating = [];
if ($this->shouldMigrateAfterCreation()) {
- if ($this->shouldQueueMigration()) {
- QueuedTenantDatabaseMigrator::dispatch($tenant);
- } else {
- $this->artisan->call('tenants:migrate', [
- '--tenants' => [$tenant['id']],
- ]);
- }
+ $afterCreating[] = $this->databaseCreationQueued()
+ ? new QueuedTenantDatabaseMigrator($tenant, $this->getMigrationParameters())
+ : function () use ($tenant) {
+ $this->artisan->call('tenants:migrate', [
+ '--tenants' => [$tenant['id']],
+ ] + $this->getMigrationParameters());
+ };
}
+ if ($this->shouldSeedAfterMigration()) {
+ $afterCreating[] = $this->databaseCreationQueued()
+ ? new QueuedTenantDatabaseSeeder($tenant)
+ : function () use ($tenant) {
+ $this->artisan->call('tenants:seed', [
+ '--tenants' => [$tenant['id']],
+ ]);
+ };
+ }
+
+ if ($this->shouldCreateDatabase($tenant)) {
+ $this->database->createDatabase($tenant, $afterCreating);
+ }
+
+ $this->event('tenant.created', $tenant);
+
return $this;
}
@@ -85,12 +111,16 @@ class TenantManager
*/
public function deleteTenant(Tenant $tenant): self
{
+ $this->event('tenant.deleting', $tenant);
+
$this->storage->deleteTenant($tenant);
if ($this->shouldDeleteDatabase()) {
$this->database->deleteDatabase($tenant);
}
+ $this->event('tenant.deleted', $tenant);
+
return $this;
}
@@ -115,8 +145,11 @@ class TenantManager
*/
public function ensureTenantCanBeCreated(Tenant $tenant): void
{
+ if ($this->shouldCreateDatabase($tenant)) {
+ $this->database->ensureTenantCanBeCreated($tenant);
+ }
+
$this->storage->ensureTenantCanBeCreated($tenant);
- $this->database->ensureTenantCanBeCreated($tenant);
}
/**
@@ -127,8 +160,12 @@ class TenantManager
*/
public function updateTenant(Tenant $tenant): self
{
+ $this->event('tenant.updating', $tenant);
+
$this->storage->updateTenant($tenant);
+ $this->event('tenant.updated', $tenant);
+
return $this;
}
@@ -183,6 +220,34 @@ class TenantManager
return $this->storage->findByDomain($domain);
}
+ /**
+ * Find a tenant using an arbitrary key.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return Tenant
+ * @throws TenantCouldNotBeIdentifiedException
+ * @throws NotImplementedException
+ */
+ public function findBy(string $key, $value): Tenant
+ {
+ if ($key === null) {
+ throw new Exception('No key supplied.');
+ }
+
+ if ($value === null) {
+ throw new Exception('No value supplied.');
+ }
+
+ if (! $this->storage instanceof CanFindByAnyKey) {
+ throw new NotImplementedException(get_class($this->storage), 'findBy',
+ 'This method was added to the DB storage driver provided by the package in 2.2.0 and might be part of the StorageDriver contract in 3.0.0.'
+ );
+ }
+
+ return $this->storage->findBy($key, $value);
+ }
+
/**
* Get all tenants.
*
@@ -206,6 +271,10 @@ class TenantManager
*/
public function initializeTenancy(Tenant $tenant): self
{
+ if ($this->initialized) {
+ $this->endTenancy();
+ }
+
$this->setTenant($tenant);
$this->bootstrapTenancy($tenant);
$this->initialized = true;
@@ -227,20 +296,24 @@ class TenantManager
*/
public function bootstrapTenancy(Tenant $tenant): self
{
- $prevented = $this->event('bootstrapping');
+ $prevented = $this->event('bootstrapping', $tenant);
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
$this->app[$bootstrapper]->start($tenant);
}
- $this->event('bootstrapped');
+ $this->event('bootstrapped', $tenant);
return $this;
}
public function endTenancy(): self
{
- $prevented = $this->event('ending');
+ if (! $this->initialized) {
+ return $this;
+ }
+
+ $prevented = $this->event('ending', $this->tenant);
foreach ($this->tenancyBootstrappers($prevented) as $bootstrapper) {
$this->app[$bootstrapper]->end();
@@ -306,14 +379,28 @@ class TenantManager
return array_diff_key($this->app['config']['tenancy.bootstrappers'], array_flip($except));
}
+ public function shouldCreateDatabase(Tenant $tenant): bool
+ {
+ if (array_key_exists('_tenancy_create_database', $tenant->data)) {
+ return $tenant->data['_tenancy_create_database'];
+ }
+
+ return $this->app['config']['tenancy.create_database'] ?? true;
+ }
+
public function shouldMigrateAfterCreation(): bool
{
return $this->app['config']['tenancy.migrate_after_creation'] ?? false;
}
- public function shouldQueueMigration(): bool
+ public function shouldSeedAfterMigration(): bool
{
- return $this->app['config']['tenancy.queue_automatic_migration'] ?? false;
+ return $this->shouldMigrateAfterCreation() && $this->app['config']['tenancy.seed_after_migration'] ?? false;
+ }
+
+ public function databaseCreationQueued(): bool
+ {
+ return $this->app['config']['tenancy.queue_database_creation'] ?? false;
}
public function shouldDeleteDatabase(): bool
@@ -321,6 +408,16 @@ class TenantManager
return $this->app['config']['tenancy.delete_database_after_tenant_deletion'] ?? false;
}
+ public function getSeederParameters()
+ {
+ return $this->app['config']['tenancy.seeder_parameters'] ?? [];
+ }
+
+ public function getMigrationParameters()
+ {
+ return $this->app['config']['tenancy.migration_parameters'] ?? [];
+ }
+
/**
* Add an event listener.
*
@@ -350,17 +447,25 @@ class TenantManager
}
/**
- * Execute event listeners.
+ * Trigger an event and execute its listeners.
*
* @param string $name
+ * @param mixed ...$args
* @return string[]
*/
- protected function event(string $name): array
+ public function event(string $name, ...$args): array
{
- return array_reduce($this->eventListeners[$name] ?? [], function ($prevented, $listener) {
- $prevented = array_merge($prevented, $listener($this) ?? []);
-
- return $prevented;
+ return array_reduce($this->eventListeners[$name] ?? [], function ($results, $listener) use ($args) {
+ return array_merge($results, $listener($this, ...$args) ?? []);
}, []);
}
+
+ public function __call($method, $parameters)
+ {
+ if (Str::startsWith($method, 'findBy')) {
+ return $this->findBy(Str::snake(substr($method, 6)), $parameters[0]);
+ }
+
+ static::throwBadMethodCallException($method);
+ }
}
diff --git a/src/TenantRouteServiceProvider.php b/src/TenantRouteServiceProvider.php
index e0ab3335..1f36b35b 100644
--- a/src/TenantRouteServiceProvider.php
+++ b/src/TenantRouteServiceProvider.php
@@ -11,11 +11,12 @@ 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'))) {
- Route::middleware(['web', 'tenancy'])
- ->namespace($this->app['config']['tenant_route_namespace'] ?? 'App\Http\Controllers')
- ->group(base_path('routes/tenant.php'));
- }
+ $this->app->booted(function () {
+ if (file_exists(base_path('routes/tenant.php'))) {
+ Route::middleware(['web', 'tenancy'])
+ ->namespace($this->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers')
+ ->group(base_path('routes/tenant.php'));
+ }
+ });
}
}
diff --git a/src/Traits/DealsWithMigrations.php b/src/Traits/DealsWithMigrations.php
index 48531587..f730cf07 100644
--- a/src/Traits/DealsWithMigrations.php
+++ b/src/Traits/DealsWithMigrations.php
@@ -8,6 +8,10 @@ trait DealsWithMigrations
{
protected function getMigrationPaths()
{
- return [config('tenancy.migrations_directory', database_path('migrations/tenant'))];
+ if ($this->input->hasOption('path') && $this->input->getOption('path')) {
+ return parent::getMigrationPaths();
+ }
+
+ return config('tenancy.migration_paths', [config('tenancy.migrations_directory') ?? database_path('migrations/tenant')]);
}
}
diff --git a/src/Traits/HasATenantArgument.php b/src/Traits/HasATenantArgument.php
new file mode 100644
index 00000000..7ad0c846
--- /dev/null
+++ b/src/Traits/HasATenantArgument.php
@@ -0,0 +1,29 @@
+find($this->argument('tenant'))];
+ }
+
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->specifyParameters();
+ }
+}
diff --git a/src/Traits/HasATenantsOption.php b/src/Traits/HasATenantsOption.php
index 4d6b247d..103a3b91 100644
--- a/src/Traits/HasATenantsOption.php
+++ b/src/Traits/HasATenantsOption.php
@@ -14,4 +14,16 @@ trait HasATenantsOption
['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null],
], parent::getOptions());
}
+
+ protected function getTenants(): array
+ {
+ return tenancy()->all($this->option('tenants'))->all();
+ }
+
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->specifyParameters();
+ }
}
diff --git a/src/Traits/TenantAwareCommand.php b/src/Traits/TenantAwareCommand.php
new file mode 100644
index 00000000..fb11df9f
--- /dev/null
+++ b/src/Traits/TenantAwareCommand.php
@@ -0,0 +1,38 @@
+getTenants();
+ $exitCode = 0;
+
+ foreach ($tenants as $tenant) {
+ $result = (int) $tenant->run(function () {
+ return $this->laravel->call([$this, 'handle']);
+ });
+
+ if ($result !== 0) {
+ $exitCode = $result;
+ }
+ }
+
+ return $exitCode;
+ }
+
+ /**
+ * Get an array of tenants for which the command should be executed.
+ *
+ * @return Tenant[]
+ */
+ abstract protected function getTenants(): array;
+}
diff --git a/src/helpers.php b/src/helpers.php
index 56d4cf08..58aa8ccb 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -5,7 +5,8 @@ declare(strict_types=1);
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager;
-if (! \function_exists('tenancy')) {
+if (! function_exists('tenancy')) {
+ /** @return TenantManager|mixed */
function tenancy($key = null)
{
if ($key) {
@@ -16,34 +17,55 @@ if (! \function_exists('tenancy')) {
}
}
-if (! \function_exists('tenant')) {
+if (! function_exists('tenant')) {
+ /**
+ * Get a key from the current tenant's storage.
+ *
+ * @param string|null $key
+ * @return Tenant|mixed
+ */
function tenant($key = null)
{
- if (! is_null($key)) {
- return optional(app(Tenant::class))->get($key) ?? null;
+ if (is_null($key)) {
+ return app(Tenant::class);
}
- return app(Tenant::class);
+ return optional(app(Tenant::class))->get($key) ?? null;
}
}
-if (! \function_exists('tenant_asset')) {
+if (! function_exists('tenant_asset')) {
+ /** @return string */
function tenant_asset($asset)
{
return route('stancl.tenancy.asset', ['path' => $asset]);
}
}
-if (! \function_exists('global_asset')) {
+if (! function_exists('global_asset')) {
function global_asset($asset)
{
return app('globalUrl')->asset($asset);
}
}
-if (! \function_exists('global_cache')) {
+if (! function_exists('global_cache')) {
function global_cache()
{
return app('globalCache');
}
}
+
+if (! function_exists('tenant_route')) {
+ function tenant_route($route, $parameters = [], string $domain = null)
+ {
+ $domain = $domain ?? request()->getHost();
+
+ // replace first occurance of hostname fragment with $domain
+ $url = route($route, $parameters);
+ $hostname = parse_url($url, PHP_URL_HOST);
+ $position = strpos($url, $hostname);
+
+ return substr_replace($url, $domain, $position, strlen($hostname));
+ }
+}
diff --git a/src/routes.php b/src/routes.php
index 69f51e70..093f5de4 100644
--- a/src/routes.php
+++ b/src/routes.php
@@ -2,6 +2,8 @@
declare(strict_types=1);
-Route::get('/tenancy/assets/{path}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset')
- ->where('path', '(.*)')
- ->name('stancl.tenancy.asset');
+Route::middleware(['tenancy'])->group(function () {
+ Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset')
+ ->where('path', '(.*)')
+ ->name('stancl.tenancy.asset');
+});
diff --git a/test b/test
index 3f8244b3..0d5a2556 100755
--- a/test
+++ b/test
@@ -1,7 +1,7 @@
#!/bin/bash
set -e
-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 "$@"
+printf "Variant 1 (DB)\n\n"
+docker-compose exec -T test env TENANCY_TEST_STORAGE_DRIVER=db vendor/bin/phpunit --coverage-php coverage/1.cov "$@"
+printf "Variant 2 (Redis)\n\n"
+docker-compose exec -T test env TENANCY_TEST_STORAGE_DRIVER=redis vendor/bin/phpunit --coverage-php coverage/2.cov "$@"
diff --git a/tests/CacheManagerTest.php b/tests/CacheManagerTest.php
index af2d8882..77e0c357 100644
--- a/tests/CacheManagerTest.php
+++ b/tests/CacheManagerTest.php
@@ -8,15 +8,19 @@ use Stancl\Tenancy\Tenant;
class CacheManagerTest extends TestCase
{
+ public $autoInitTenancy = false;
+
/** @test */
public function default_tag_is_automatically_applied()
{
+ $this->initTenancy();
$this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames());
}
/** @test */
public function tags_are_merged_when_array_is_passed()
{
+ $this->initTenancy();
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
$this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames());
}
@@ -24,6 +28,7 @@ class CacheManagerTest extends TestCase
/** @test */
public function tags_are_merged_when_string_is_passed()
{
+ $this->initTenancy();
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo'];
$this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames());
}
@@ -31,6 +36,7 @@ class CacheManagerTest extends TestCase
/** @test */
public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method()
{
+ $this->initTenancy();
$this->expectException(\Exception::class);
cache()->tags();
}
@@ -38,6 +44,7 @@ class CacheManagerTest extends TestCase
/** @test */
public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method()
{
+ $this->initTenancy();
$this->expectException(\Exception::class);
cache()->tags(1, 2);
}
diff --git a/tests/CachedResolverTest.php b/tests/CachedResolverTest.php
new file mode 100644
index 00000000..0d32915e
--- /dev/null
+++ b/tests/CachedResolverTest.php
@@ -0,0 +1,164 @@
+markTestSkipped('This test is only relevant for the DB storage driver.');
+ }
+
+ config(['tenancy.storage_drivers.db.cache_store' => config('cache.default')]);
+ }
+
+ /** @test */
+ public function a_query_is_not_made_for_tenant_id_once_domain_is_cached()
+ {
+ $tenant = Tenant::new()
+ ->withData(['foo' => 'bar'])
+ ->withDomains(['foo.localhost'])
+ ->save();
+
+ // query is made
+ $queried = tenancy()->findByDomain('foo.localhost');
+ $this->assertEquals($tenant->data, $queried->data);
+ $this->assertSame($tenant->domains, $queried->domains);
+
+ // cache is set
+ $this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost'));
+ $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id));
+ $this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
+
+ // query is not made
+ DatabaseStorageDriver::getCentralConnection()->enableQueryLog();
+ $cached = tenancy()->findByDomain('foo.localhost');
+ $this->assertEquals($tenant->data, $cached->data);
+ $this->assertSame($tenant->domains, $cached->domains);
+ $this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog());
+ }
+
+ /** @test */
+ public function a_query_is_not_made_for_tenant_once_id_is_cached()
+ {
+ $tenant = Tenant::new()
+ ->withData(['foo' => 'bar'])
+ ->withDomains(['foo.localhost'])
+ ->save();
+
+ // query is made
+ $queried = tenancy()->find($tenant->id);
+ $this->assertEquals($tenant->data, $queried->data);
+ $this->assertSame($tenant->domains, $queried->domains);
+
+ // cache is set
+ $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id));
+ $this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
+
+ // query is not made
+ DatabaseStorageDriver::getCentralConnection()->enableQueryLog();
+ $cached = tenancy()->find($tenant->id);
+ $this->assertEquals($tenant->data, $cached->data);
+ $this->assertSame($tenant->domains, $cached->domains);
+ $this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog());
+ }
+
+ /** @test */
+ public function modifying_tenant_domains_invalidates_the_cached_domain_to_id_mapping()
+ {
+ $tenant = Tenant::new()
+ ->withDomains(['foo.localhost', 'bar.localhost'])
+ ->save();
+
+ // queried
+ $this->assertSame($tenant->id, tenancy()->findByDomain('foo.localhost')->id);
+ $this->assertSame($tenant->id, tenancy()->findByDomain('bar.localhost')->id);
+
+ // assert cache set
+ $this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost'));
+ $this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:bar.localhost'));
+
+ $tenant
+ ->removeDomains(['foo.localhost', 'bar.localhost'])
+ ->addDomains(['xyz.localhost'])
+ ->save();
+
+ // assert neither domain is cached
+ $this->assertSame(null, Cache::get('_tenancy_domain_to_id:foo.localhost'));
+ $this->assertSame(null, Cache::get('_tenancy_domain_to_id:bar.localhost'));
+ $this->assertSame(null, Cache::get('_tenancy_domain_to_id:xyz.localhost'));
+ }
+
+ /** @test */
+ public function modifying_tenants_data_invalidates_tenant_data_cache()
+ {
+ $tenant = Tenant::new()->withData(['foo' => 'bar'])->save();
+
+ // cache record is set
+ $this->assertSame('bar', tenancy()->find($tenant->id)->get('foo'));
+ $this->assertSame('bar', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']);
+
+ // cache record is invalidated
+ $tenant->set('foo', 'xyz');
+ $this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id));
+
+ // cache record is set
+ $this->assertSame('xyz', tenancy()->find($tenant->id)->get('foo'));
+ $this->assertSame('xyz', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']);
+
+ // cache record is invalidated
+ $tenant->foo = 'abc';
+ $tenant->save();
+ $this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id));
+ }
+
+ /** @test */
+ public function modifying_tenants_domains_invalidates_tenant_domain_cache()
+ {
+ $tenant = Tenant::new()
+ ->withData(['foo' => 'bar'])
+ ->withDomains(['foo.localhost'])
+ ->save();
+
+ // cache record is set
+ $this->assertSame(['foo.localhost'], tenancy()->find($tenant->id)->domains);
+ $this->assertSame(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id));
+
+ // cache record is invalidated
+ $tenant->addDomains(['bar.localhost'])->save();
+ $this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
+
+ $this->assertEquals(['foo.localhost', 'bar.localhost'], tenancy()->find($tenant->id)->domains);
+ }
+
+ /** @test */
+ public function deleting_a_tenant_invalidates_all_caches()
+ {
+ $tenant = Tenant::new()
+ ->withData(['foo' => 'bar'])
+ ->withDomains(['foo.localhost'])
+ ->save();
+
+ tenancy()->findByDomain('foo.localhost');
+ $this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost'));
+ $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id));
+ $this->assertEquals(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id));
+
+ $tenant->delete();
+ $this->assertEquals(null, Cache::get('_tenancy_domain_to_id:foo.localhost'));
+ $this->assertEquals(null, Cache::get('_tenancy_id_to_data:' . $tenant->id));
+ $this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
+ }
+}
diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php
index 4cf8a92c..39bb46e2 100644
--- a/tests/CommandsTest.php
+++ b/tests/CommandsTest.php
@@ -18,7 +18,7 @@ class CommandsTest extends TestCase
{
parent::setUp();
- config(['tenancy.migrations_directory' => database_path('../migrations')]);
+ config(['tenancy.migration_paths', [database_path('../migrations')]]);
}
/** @test */
@@ -127,16 +127,25 @@ class CommandsTest extends TestCase
mkdir($dir, 0777, true);
}
- file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernel.stub'));
+ if (app()->version()[0] === '6') {
+ file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernelv6.stub'));
+ } else {
+ file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernelv7.stub'));
+ }
$this->artisan('tenancy:install')
- ->expectsQuestion('Do you want to publish the default database migrations?', 'yes');
+ ->expectsQuestion('Do you wish to publish the migrations that create these tables?', 'yes');
$this->assertFileExists(base_path('routes/tenant.php'));
$this->assertFileExists(base_path('config/tenancy.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(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernel.stub'), file_get_contents(app_path('Http/Kernel.php')));
+
+ if (app()->version()[0] === '6') {
+ $this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernelv6.stub'), file_get_contents(app_path('Http/Kernel.php')));
+ } else {
+ $this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernelv7.stub'), file_get_contents(app_path('Http/Kernel.php')));
+ }
}
/** @test */
diff --git a/tests/DatabaseManagerTest.php b/tests/DatabaseManagerTest.php
index cbd976e8..9195ef69 100644
--- a/tests/DatabaseManagerTest.php
+++ b/tests/DatabaseManagerTest.php
@@ -45,4 +45,21 @@ class DatabaseManagerTest extends TestCase
$this->assertSame('tenant', config('database.default'));
$this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo'));
}
+
+ /** @test */
+ public function ending_tenancy_doesnt_purge_the_central_connection()
+ {
+ $this->markTestIncomplete('Seems like this only happens on MySQL?');
+
+ // regression test for https://github.com/stancl/tenancy/pull/189
+ // config(['tenancy.migrate_after_creation' => true]);
+
+ tenancy()->create(['foo.localhost']);
+ tenancy()->init('foo.localhost');
+ tenancy()->end();
+
+ $this->assertNotEmpty(tenancy()->all());
+
+ tenancy()->all()->each->delete();
+ }
}
diff --git a/tests/DatabaseSchemaManagerTest.php b/tests/DatabaseSchemaManagerTest.php
new file mode 100644
index 00000000..5f9589e0
--- /dev/null
+++ b/tests/DatabaseSchemaManagerTest.php
@@ -0,0 +1,143 @@
+set([
+ 'database.default' => 'pgsql',
+ 'database.connections.pgsql.database' => 'main',
+ 'database.connections.pgsql.schema' => 'public',
+ 'tenancy.database.based_on' => null,
+ 'tenancy.database.suffix' => '',
+ 'tenancy.database.separate_by' => 'schema',
+ 'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class,
+ ]);
+ }
+
+ /** @test */
+ public function reconnect_method_works()
+ {
+ $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
+
+ tenancy()->init('test.localhost');
+
+ app(\Stancl\Tenancy\DatabaseManager::class)->reconnect();
+
+ $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
+
+ $this->assertSame($old_connection_name, $new_connection_name);
+ }
+
+ /** @test */
+ public function the_default_db_is_used_when_based_on_is_null()
+ {
+ config(['database.default' => 'pgsql']);
+
+ $this->assertSame('pgsql', config('database.default'));
+ config([
+ 'database.connections.pgsql.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'));
+ }
+
+ /** @test */
+ public function make_sure_using_schema_connection()
+ {
+ $tenant = tenancy()->create(['schema.localhost']);
+ tenancy()->init('schema.localhost');
+
+ $this->assertSame($tenant->getDatabaseName(), config('database.connections.' . config('database.default') . '.schema'));
+ }
+
+ /** @test */
+ public function databases_are_separated_using_schema_and_not_database()
+ {
+ tenancy()->create('foo.localhost');
+ tenancy()->init('foo.localhost');
+ $this->assertSame('tenant', config('database.default'));
+ $this->assertSame('main', config('database.connections.tenant.database'));
+
+ $schema1 = config('database.connections.' . config('database.default') . '.schema');
+ $database1 = config('database.connections.' . config('database.default') . '.database');
+
+ tenancy()->create('bar.localhost');
+ tenancy()->init('bar.localhost');
+ $this->assertSame('tenant', config('database.default'));
+ $this->assertSame('main', config('database.connections.tenant.database'));
+
+ $schema2 = config('database.connections.' . config('database.default') . '.schema');
+ $database2 = config('database.connections.' . config('database.default') . '.database');
+
+ $this->assertSame($database1, $database2);
+ $this->assertNotSame($schema1, $schema2);
+ }
+
+ /** @test */
+ public function schemas_are_separated()
+ {
+ // copied from DataSeparationTest
+
+ $tenant1 = Tenant::create('tenant1.localhost');
+ $tenant2 = Tenant::create('tenant2.localhost');
+ \Artisan::call('tenants:migrate', [
+ '--tenants' => [$tenant1['id'], $tenant2['id']],
+ ]);
+
+ tenancy()->init('tenant1.localhost');
+ User::create([
+ 'name' => 'foo',
+ 'email' => 'foo@bar.com',
+ 'email_verified_at' => now(),
+ 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
+ 'remember_token' => Str::random(10),
+ ]);
+ $this->assertSame('foo', User::first()->name);
+
+ tenancy()->init('tenant2.localhost');
+ $this->assertSame(null, User::first());
+
+ User::create([
+ 'name' => 'xyz',
+ 'email' => 'xyz@bar.com',
+ 'email_verified_at' => now(),
+ 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
+ 'remember_token' => Str::random(10),
+ ]);
+
+ $this->assertSame('xyz', User::first()->name);
+ $this->assertSame('xyz@bar.com', User::first()->email);
+
+ tenancy()->init('tenant1.localhost');
+ $this->assertSame('foo', User::first()->name);
+ $this->assertSame('foo@bar.com', User::first()->email);
+
+ $tenant3 = Tenant::create('tenant3.localhost');
+ \Artisan::call('tenants:migrate', [
+ '--tenants' => [$tenant1['id'], $tenant3['id']],
+ ]);
+
+ tenancy()->init('tenant3.localhost');
+ $this->assertSame(null, User::first());
+
+ tenancy()->init('tenant1.localhost');
+ \DB::table('users')->where('id', 1)->update(['name' => 'xxx']);
+ $this->assertSame('xxx', User::first()->name);
+ }
+}
diff --git a/tests/Etc/AddUserCommand.php b/tests/Etc/AddUserCommand.php
new file mode 100644
index 00000000..a270d158
--- /dev/null
+++ b/tests/Etc/AddUserCommand.php
@@ -0,0 +1,38 @@
+ Str::random(10),
+ 'email' => Str::random(10) . '@gmail.com',
+ 'email_verified_at' => now(),
+ 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
+ 'remember_token' => Str::random(10),
+ ]);
+ }
+}
diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/ConsoleKernel.php
index a801b9dc..1bc66365 100644
--- a/tests/Etc/ConsoleKernel.php
+++ b/tests/Etc/ConsoleKernel.php
@@ -15,5 +15,6 @@ class ConsoleKernel extends Kernel
*/
protected $commands = [
ExampleCommand::class,
+ AddUserCommand::class,
];
}
diff --git a/tests/Etc/defaultHttpKernel.stub b/tests/Etc/defaultHttpKernelv6.stub
similarity index 100%
rename from tests/Etc/defaultHttpKernel.stub
rename to tests/Etc/defaultHttpKernelv6.stub
diff --git a/tests/Etc/defaultHttpKernelv7.stub b/tests/Etc/defaultHttpKernelv7.stub
new file mode 100644
index 00000000..d98a24d5
--- /dev/null
+++ b/tests/Etc/defaultHttpKernelv7.stub
@@ -0,0 +1,66 @@
+ [
+ \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',
+ \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ ],
+ ];
+
+ /**
+ * 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,
+ 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
+ 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
+ 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+ 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+ ];
+}
\ No newline at end of file
diff --git a/tests/Etc/modifiedHttpKernel.stub b/tests/Etc/modifiedHttpKernelv6.stub
similarity index 97%
rename from tests/Etc/modifiedHttpKernel.stub
rename to tests/Etc/modifiedHttpKernelv6.stub
index 86cf535c..0de76bbd 100644
--- a/tests/Etc/modifiedHttpKernel.stub
+++ b/tests/Etc/modifiedHttpKernelv6.stub
@@ -39,6 +39,7 @@ class Kernel extends HttpKernel
],
'api' => [
+ \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
'throttle:60,1',
'bindings',
],
diff --git a/tests/Etc/modifiedHttpKernelv7.stub b/tests/Etc/modifiedHttpKernelv7.stub
new file mode 100644
index 00000000..037a57f4
--- /dev/null
+++ b/tests/Etc/modifiedHttpKernelv7.stub
@@ -0,0 +1,80 @@
+ [
+ \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' => [
+ \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
+ 'throttle:60,1',
+ \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ ],
+ ];
+
+ /**
+ * 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,
+ 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
+ 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
+ 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+ 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+ ];
+
+ protected $middlewarePriority = [
+ \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
+ \Stancl\Tenancy\Middleware\InitializeTenancy::class,
+ \Illuminate\Session\Middleware\StartSession::class,
+ \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
+ \Illuminate\Routing\Middleware\ThrottleRequests::class,
+ \Illuminate\Session\Middleware\AuthenticateSession::class,
+ \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ \Illuminate\Auth\Middleware\Authorize::class,
+ ];
+}
\ No newline at end of file
diff --git a/tests/TenantConfigTest.php b/tests/Features/TenantConfigTest.php
similarity index 93%
rename from tests/TenantConfigTest.php
rename to tests/Features/TenantConfigTest.php
index 68c2cf4a..db8fcaf3 100644
--- a/tests/TenantConfigTest.php
+++ b/tests/Features/TenantConfigTest.php
@@ -2,7 +2,9 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
+namespace Stancl\Tenancy\Tests\Features;
+
+use Stancl\Tenancy\Tests\TestCase;
class TenantConfigTest extends TestCase
{
diff --git a/tests/Features/TimestampTest.php b/tests/Features/TimestampTest.php
new file mode 100644
index 00000000..25cb1017
--- /dev/null
+++ b/tests/Features/TimestampTest.php
@@ -0,0 +1,56 @@
+ [
+ Timestamps::class,
+ ]]);
+ }
+
+ /** @test */
+ public function create_and_update_timestamps_are_added_on_create()
+ {
+ $tenant = Tenant::new()->save();
+ $this->assertArrayHasKey('created_at', $tenant->data);
+ $this->assertArrayHasKey('updated_at', $tenant->data);
+ }
+
+ /** @test */
+ public function update_timestamps_are_added()
+ {
+ $tenant = Tenant::new()->save();
+ $this->assertSame($tenant->created_at, $tenant->updated_at);
+ $this->assertSame('string', gettype($tenant->created_at));
+
+ sleep(1);
+
+ $tenant->put('abc', 'def');
+
+ $this->assertTrue($tenant->updated_at > $tenant->created_at);
+ }
+
+ /** @test */
+ public function softdelete_timestamps_are_added()
+ {
+ $tenant = Tenant::new()->save();
+ $this->assertNull($tenant->deleted_at);
+
+ $tenant->softDelete();
+ $this->assertNotNull($tenant->deleted_at);
+ }
+}
diff --git a/tests/FutureTest.php b/tests/FutureTest.php
new file mode 100644
index 00000000..6a44a80e
--- /dev/null
+++ b/tests/FutureTest.php
@@ -0,0 +1,45 @@
+withData(['email' => 'foo@example.com', 'role' => 'admin'])->save();
+
+ $this->assertArrayHasKey('email', $tenant->data);
+ $tenant->deleteKey('email');
+ $this->assertArrayNotHasKey('email', $tenant->data);
+ $this->assertArrayNotHasKey('email', tenancy()->all()->first()->data);
+
+ $tenant->put(['foo' => 'bar', 'abc' => 'xyz']);
+ $this->assertArrayHasKey('foo', $tenant->data);
+ $this->assertArrayHasKey('abc', $tenant->data);
+
+ $tenant->deleteKeys(['foo', 'abc']);
+ $this->assertArrayNotHasKey('foo', $tenant->data);
+ $this->assertArrayNotHasKey('abc', $tenant->data);
+ }
+
+ /** @test */
+ public function tenant_can_be_identified_using_an_arbitrary_string()
+ {
+ if (! tenancy()->storage instanceof CanFindByAnyKey) {
+ $this->markTestSkipped(get_class(tenancy()->storage) . ' does not implement the CanFindByAnyKey interface.');
+ }
+
+ $tenant = Tenant::new()->withData(['email' => 'foo@example.com'])->save();
+
+ $this->assertSame($tenant->id, tenancy()->findByEmail('foo@example.com')->id);
+ }
+}
diff --git a/tests/QueueTest.php b/tests/QueueTest.php
index 7d24fdc3..d657fc66 100644
--- a/tests/QueueTest.php
+++ b/tests/QueueTest.php
@@ -33,6 +33,19 @@ class QueueTest extends TestCase
return $event->job->payload()['tenant_id'] === tenant('id');
});
}
+
+ /** @test */
+ public function tenancy_is_not_initialized_in_non_tenant_queues()
+ {
+ $this->loadLaravelMigrations(['--database' => 'tenant']);
+ Event::fake();
+
+ dispatch(new TestJob())->onConnection('central');
+
+ Event::assertDispatched(JobProcessing::class, function ($event) {
+ return ! isset($event->job->payload()['tenant_id']);
+ });
+ }
}
class TestJob implements ShouldQueue
diff --git a/tests/TenantRedirectMacroTest.php b/tests/RedirectTest.php
similarity index 56%
rename from tests/TenantRedirectMacroTest.php
rename to tests/RedirectTest.php
index 970579ec..c0bd90a8 100644
--- a/tests/TenantRedirectMacroTest.php
+++ b/tests/RedirectTest.php
@@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Tests;
use Route;
use Stancl\Tenancy\Tenant;
-class TenantRedirectMacroTest extends TestCase
+class RedirectTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
@@ -33,4 +33,17 @@ class TenantRedirectMacroTest extends TestCase
$this->get('/redirect')
->assertRedirect('http://abcd/foobar');
}
+
+ /** @test */
+ public function tenant_route_helper_generates_correct_url()
+ {
+ Route::get('/abcdef/{a?}/{b?}', function () {
+ return 'Foo';
+ })->name('foo');
+
+ $this->assertSame('http://foo.localhost/abcdef/as/df', tenant_route('foo', ['a' => 'as', 'b' => 'df'], 'foo.localhost'));
+ $this->assertSame('http://foo.localhost/abcdef', tenant_route('foo', [], 'foo.localhost'));
+
+ $this->assertSame('http://' . request()->getHost() . '/abcdef/x/y', tenant_route('foo', ['a' => 'x', 'b' => 'y']));
+ }
}
diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php
new file mode 100644
index 00000000..fcbd0997
--- /dev/null
+++ b/tests/RequestDataIdentificationTest.php
@@ -0,0 +1,62 @@
+ [
+ 'localhost',
+ ],
+ ]);
+
+ Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () {
+ return 'Tenant id: ' . tenant('id');
+ });
+ }
+
+ /** @test */
+ public function header_identification_works()
+ {
+ $this->app->bind(InitializeTenancyByRequestData::class, function () {
+ return new InitializeTenancyByRequestData('X-Tenant');
+ });
+ $tenant = Tenant::new()->save();
+ $tenant2 = Tenant::new()->save();
+
+ $this
+ ->withoutExceptionHandling()
+ ->get('test', [
+ 'X-Tenant' => $tenant->id,
+ ])
+ ->assertSee($tenant->id);
+ }
+
+ /** @test */
+ public function query_parameter_identification_works()
+ {
+ $this->app->bind(InitializeTenancyByRequestData::class, function () {
+ return new InitializeTenancyByRequestData(null, 'tenant');
+ });
+ $tenant = Tenant::new()->save();
+ $tenant2 = Tenant::new()->save();
+
+ $this
+ ->withoutExceptionHandling()
+ ->get('test?tenant=' . $tenant->id)
+ ->assertSee($tenant->id);
+ }
+}
diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php
index 248e7703..b9bbabad 100644
--- a/tests/TenantAssetTest.php
+++ b/tests/TenantAssetTest.php
@@ -68,4 +68,20 @@ class TenantAssetTest extends TestCase
$this->assertSame($original, global_asset('foobar'));
}
+
+ /** @test */
+ public function asset_helper_tenancy_can_be_disabled()
+ {
+ $original = asset('foo');
+
+ config([
+ 'app.asset_url' => null,
+ 'tenancy.filesystem.asset_helper_tenancy' => false,
+ ]);
+
+ Tenant::create('foo.localhost');
+ tenancy()->init('foo.localhost');
+
+ $this->assertSame($original, asset('foo'));
+ }
}
diff --git a/tests/TenantClassTest.php b/tests/TenantClassTest.php
index 4a3775f2..38cc27cd 100644
--- a/tests/TenantClassTest.php
+++ b/tests/TenantClassTest.php
@@ -105,4 +105,72 @@ class TenantClassTest extends TestCase
$this->assertSame(['foo' => 'bar'], $data);
}
+
+ /** @test */
+ public function run_method_works()
+ {
+ $this->assertSame(null, tenancy()->getTenant());
+
+ $users_table_empty = function () {
+ return count(\DB::table('users')->get()) === 0;
+ };
+
+ $tenant = Tenant::new()->save();
+ \Artisan::call('tenants:migrate', [
+ '--tenants' => [$tenant->id],
+ ]);
+ tenancy()->initialize($tenant);
+ $this->assertTrue($users_table_empty());
+ tenancy()->end();
+
+ $foo = $tenant->run(function () {
+ \DB::table('users')->insert([
+ 'name' => 'foo',
+ 'email' => 'foo@bar.xy',
+ 'password' => bcrypt('secret'),
+ ]);
+
+ return 'foo';
+ });
+
+ // test return value
+ $this->assertSame('foo', $foo);
+
+ // test that tenancy was ended
+ $this->assertSame(false, tenancy()->initialized);
+ $this->assertSame(null, tenancy()->getTenant());
+
+ // test closure
+ tenancy()->initialize($tenant);
+ $this->assertFalse($users_table_empty());
+
+ // test returning to original tenant
+ $tenant2 = Tenant::new()->save();
+ \Artisan::call('tenants:migrate', [
+ '--tenants' => [$tenant2->id],
+ ]);
+
+ tenancy()->initialize($tenant2);
+ $this->assertSame($tenant2, tenancy()->getTenant());
+ $this->assertTrue($users_table_empty());
+
+ $tenant->run(function () {
+ \DB::table('users')->insert([
+ 'name' => 'bar',
+ 'email' => 'bar@bar.xy',
+ 'password' => bcrypt('secret'),
+ ]);
+ });
+
+ $this->assertSame($tenant2, tenancy()->getTenant());
+
+ $this->assertSame(2, $tenant->run(function () {
+ return \DB::table('users')->count();
+ }));
+
+ // test that the tenant variable can be accessed
+ $this->assertSame($tenant->id, $tenant->run(function ($tenant) {
+ return $tenant->id;
+ }));
+ }
}
diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php
index fc3c34f4..89c0bbd7 100644
--- a/tests/TenantDatabaseManagerTest.php
+++ b/tests/TenantDatabaseManagerTest.php
@@ -10,6 +10,7 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager;
+use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager;
use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;
class TenantDatabaseManagerTest extends TestCase
@@ -78,6 +79,7 @@ class TenantDatabaseManagerTest extends TestCase
['mysql', MySQLDatabaseManager::class],
['sqlite', SQLiteDatabaseManager::class],
['pgsql', PostgreSQLDatabaseManager::class],
+ ['pgsql', PostgreSQLSchemaManager::class],
];
}
diff --git a/tests/TenantManagerEventsTest.php b/tests/TenantManagerEventsTest.php
index eca5077f..4dde735c 100644
--- a/tests/TenantManagerEventsTest.php
+++ b/tests/TenantManagerEventsTest.php
@@ -121,4 +121,18 @@ class TenantManagerEventsTest extends TestCase
tenancy()->init('abc.localhost');
$this->assertSame('tenant', \DB::connection()->getConfig()['name']);
}
+
+ /** @test */
+ public function tenant_is_persisted_before_the_created_hook_is_called()
+ {
+ $was_persisted = false;
+
+ Tenancy::eventListener('tenant.created', function ($tenancy, $tenant) use (&$was_persisted) {
+ $was_persisted = $tenant->persisted;
+ });
+
+ Tenant::new()->save();
+
+ $this->assertTrue($was_persisted);
+ }
}
diff --git a/tests/TenantManagerTest.php b/tests/TenantManagerTest.php
index 7f8aae0d..8e78a607 100644
--- a/tests/TenantManagerTest.php
+++ b/tests/TenantManagerTest.php
@@ -8,10 +8,14 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
+use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
+use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
+use Stancl\Tenancy\Jobs\QueuedTenantDatabaseSeeder;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager;
+use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
class TenantManagerTest extends TestCase
{
@@ -235,6 +239,27 @@ class TenantManagerTest extends TestCase
$this->assertTrue(\Schema::hasTable('users'));
}
+ /** @test */
+ public function automatic_seeding_works()
+ {
+ config(['tenancy.migrate_after_creation' => true]);
+
+ $tenant = Tenant::create(['foo.localhost']);
+ tenancy()->initialize($tenant);
+ $this->assertSame(0, \DB::table('users')->count());
+
+ config([
+ 'tenancy.seed_after_migration' => true,
+ 'tenancy.seeder_parameters' => [
+ '--class' => ExampleSeeder::class,
+ ],
+ ]);
+
+ $tenant2 = Tenant::create(['bar.localhost']);
+ tenancy()->initialize($tenant2);
+ $this->assertSame(1, \DB::table('users')->count());
+ }
+
/** @test */
public function ensureTenantCanBeCreated_works()
{
@@ -248,22 +273,110 @@ class TenantManagerTest extends TestCase
}
/** @test */
- public function automigration_can_be_queued()
+ public function automigration_is_queued_when_db_creation_is_queued()
{
Queue::fake();
config([
+ 'tenancy.queue_database_creation' => true,
'tenancy.migrate_after_creation' => true,
- 'tenancy.queue_automatic_migration' => true,
]);
$tenant = Tenant::new()->save();
- tenancy()->initialize($tenant);
- Queue::assertPushed(QueuedTenantDatabaseMigrator::class);
+ Queue::assertPushedWithChain(QueuedTenantDatabaseCreator::class, [
+ QueuedTenantDatabaseMigrator::class,
+ ]);
- $this->assertFalse(\Schema::hasTable('users'));
- (new QueuedTenantDatabaseMigrator($tenant))->handle();
- $this->assertTrue(\Schema::hasTable('users'));
+ // foreach (Queue::pushedJobs() as $job) {
+ // $job[0]['job']->handle(); // this doesn't execute the chained job
+ // }
+ // tenancy()->initialize($tenant);
+ // $this->assertTrue(\Schema::hasTable('users'));
+ }
+
+ /** @test */
+ public function autoseeding_is_queued_when_db_creation_is_queued()
+ {
+ Queue::fake();
+
+ config([
+ 'tenancy.queue_database_creation' => true,
+ 'tenancy.migrate_after_creation' => true,
+ 'tenancy.seed_after_migration' => true,
+ ]);
+
+ Tenant::new()->save();
+
+ Queue::assertPushedWithChain(QueuedTenantDatabaseCreator::class, [
+ QueuedTenantDatabaseMigrator::class,
+ QueuedTenantDatabaseSeeder::class,
+ ]);
+ }
+
+ /** @test */
+ public function TenantDoesNotExistException_is_thrown_when_find_is_called_on_an_id_that_does_not_belong_to_any_tenant()
+ {
+ $this->expectException(TenantDoesNotExistException::class);
+ tenancy()->find('gjnfdgf');
+ }
+
+ /** @test */
+ public function event_listeners_can_accept_arguments()
+ {
+ tenancy()->hook('tenant.creating', function ($tenantManager, $tenant) {
+ $this->assertSame('bar', $tenant->foo);
+ });
+
+ Tenant::new()->withData(['foo' => 'bar'])->save();
+ }
+
+ /** @test */
+ public function tenant_creating_hook_can_be_used_to_modify_tenants_data()
+ {
+ tenancy()->hook('tenant.creating', function ($tm, Tenant $tenant) {
+ $tenant->put([
+ 'foo' => 'bar',
+ 'abc123' => 'def456',
+ ]);
+ });
+ $tenant = Tenant::new()->save();
+
+ $this->assertArrayHasKey('foo', $tenant->data);
+ $this->assertArrayHasKey('abc123', $tenant->data);
+ }
+
+ /** @test */
+ public function database_creation_can_be_disabled()
+ {
+ config(['tenancy.create_database' => false]);
+
+ tenancy()->hook('database.creating', function () {
+ $this->fail();
+ });
+
+ $tenant = Tenant::new()->save();
+
+ $this->assertTrue(true);
+ }
+
+ /** @test */
+ public function database_creation_can_be_disabled_for_specific_tenants()
+ {
+ config(['tenancy.create_database' => true]);
+
+ tenancy()->hook('database.creating', function () {
+ $this->assertTrue(true);
+ });
+
+ $tenant = Tenant::new()->save();
+
+ tenancy()->hook('database.creating', function () {
+ $this->fail();
+ });
+
+ $tenant2 = Tenant::new()->withData([
+ '_tenancy_create_database' => false,
+ ])->save();
}
}
diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php
index 88bd5823..568746cb 100644
--- a/tests/TenantStorageTest.php
+++ b/tests/TenantStorageTest.php
@@ -4,9 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
-use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
-use Stancl\Tenancy\StorageDrivers\Database\TenantModel;
-use Stancl\Tenancy\StorageDrivers\RedisStorageDriver;
+use Stancl\Tenancy\StorageDrivers\Database\TenantRepository;
use Stancl\Tenancy\Tenant;
class TenantStorageTest extends TestCase
@@ -84,10 +82,12 @@ class TenantStorageTest extends TestCase
/** @test */
public function correct_storage_driver_is_used()
{
- if (config('tenancy.storage_driver') == DatabaseStorageDriver::class) {
+ if (config('tenancy.storage_driver') == 'db') {
$this->assertSame('DatabaseStorageDriver', class_basename(tenancy()->storage));
- } elseif (config('tenancy.storage_driver') == RedisStorageDriver::class) {
+ } elseif (config('tenancy.storage_driver') == 'redis') {
$this->assertSame('RedisStorageDriver', class_basename(tenancy()->storage));
+ } else {
+ dd(class_basename(config('tenancy.storage_driver')));
}
}
@@ -112,10 +112,11 @@ class TenantStorageTest extends TestCase
}
/** @test */
- public function tenant_model_uses_correct_connection()
+ public function tenant_repository_uses_correct_connection()
{
+ config(['database.connections.foo' => config('database.connections.sqlite')]);
config(['tenancy.storage_drivers.db.connection' => 'foo']);
- $this->assertSame('foo', (new TenantModel)->getConnectionName());
+ $this->assertSame('foo', app(TenantRepository::class)->database->getName());
}
/** @test */
@@ -137,7 +138,7 @@ class TenantStorageTest extends TestCase
/** @test */
public function custom_columns_work_with_db_storage_driver()
{
- if (config('tenancy.storage_driver') != 'Stancl\Tenancy\StorageDrivers\DatabaseStorageDriver') {
+ if (config('tenancy.storage_driver') != 'db') {
$this->markTestSkipped();
}
@@ -153,12 +154,41 @@ class TenantStorageTest extends TestCase
'foo',
]]);
- tenant()->create(['foo.localhost']);
+ tenancy()->create(['foo.localhost']);
tenancy()->init('foo.localhost');
- tenant()->put(['foo' => 'bar', 'abc' => 'xyz']);
- $this->assertSame(['bar', 'xyz'], tenant()->get(['foo', 'abc']));
+ tenant()->put('foo', '111');
+ $this->assertSame('111', tenant()->get('foo'));
- $this->assertSame('bar', DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo);
+ tenant()->put(['foo' => 'bar', 'abc' => 'xyz']);
+ $this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc']));
+
+ $this->assertSame('bar', \DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo);
+ }
+
+ /** @test */
+ public function custom_columns_can_be_used_on_tenant_create()
+ {
+ if (config('tenancy.storage_driver') != 'db') {
+ $this->markTestSkipped();
+ }
+
+ tenancy()->endTenancy();
+
+ $this->loadMigrationsFrom([
+ '--path' => __DIR__ . '/Etc',
+ '--database' => 'central',
+ ]);
+ config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
+
+ config(['tenancy.storage_drivers.db.custom_columns' => [
+ 'foo',
+ ]]);
+
+ tenancy()->create(['foo.localhost'], ['foo' => 'bar', 'abc' => 'xyz']);
+ tenancy()->init('foo.localhost');
+
+ $this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc']));
+ $this->assertSame('bar', \DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo);
}
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 19b0a83f..5d865549 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -24,11 +24,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
Redis::connection('tenancy')->flushdb();
Redis::connection('cache')->flushdb();
+ $originalConnection = config('database.default');
$this->loadMigrationsFrom([
'--path' => realpath(__DIR__ . '/../assets/migrations'),
'--database' => 'central',
]);
- config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
+ config(['database.default' => $originalConnection]); // fix issue caused by loadMigrationsFrom
if ($this->autoCreateTenant) {
$this->createTenant();
@@ -96,9 +97,14 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'tenancy.redis.tenancy' => env('TENANCY_TEST_REDIS_TENANCY', true),
'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'),
'tenancy.redis.prefixed_connections' => ['default'],
- 'tenancy.migrations_directory' => database_path('../migrations'),
+ 'tenancy.migration_paths' => [database_path('../migrations')],
'tenancy.storage_drivers.db.connection' => 'central',
'tenancy.bootstrappers.redis' => \Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class,
+ 'queue.connections.central' => [
+ 'driver' => 'sync',
+ 'central' => true,
+ ],
+ 'tenancy.seeder_parameters' => [],
]);
$app->singleton(\Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper::class);
diff --git a/tests/Traits/TenantAwareCommandTest.php b/tests/Traits/TenantAwareCommandTest.php
new file mode 100644
index 00000000..fcc3d0e2
--- /dev/null
+++ b/tests/Traits/TenantAwareCommandTest.php
@@ -0,0 +1,35 @@
+save();
+ $tenant2 = Tenant::new()->save();
+ \Artisan::call('tenants:migrate', [
+ '--tenants' => [$tenant1['id'], $tenant2['id']],
+ ]);
+
+ $this->artisan('user:add')
+ ->assertExitCode(0);
+
+ tenancy()->initializeTenancy($tenant1);
+ $this->assertNotEmpty(\DB::table('users')->get());
+ tenancy()->end();
+
+ tenancy()->initializeTenancy($tenant2);
+ $this->assertNotEmpty(\DB::table('users')->get());
+ tenancy()->end();
+ }
+}