mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 22:14:03 +00:00
Merge branch '2.x'
This commit is contained in:
commit
d900929264
87 changed files with 2952 additions and 570 deletions
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
|
|
@ -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']
|
||||
6
.github/ISSUE_TEMPLATE/bug-report.md
vendored
6
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -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]
|
||||
|
|
|
|||
31
.github/workflows/ci.yml
vendored
Normal file
31
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -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)
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
preset: laravel
|
||||
enabled:
|
||||
- declare_strict_types
|
||||
- alpha_ordered_imports
|
||||
disabled:
|
||||
- concat_without_spaces
|
||||
- ternary_operator_spaces
|
||||
- length_ordered_imports
|
||||
|
|
|
|||
26
.travis.yml
26
.travis.yml
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
39
DONATIONS.md
Normal file
39
DONATIONS.md
Normal file
|
|
@ -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.
|
||||
33
Dockerfile
33
Dockerfile
|
|
@ -4,27 +4,32 @@ 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 \
|
||||
|
|
|
|||
29
README.md
29
README.md
|
|
@ -1,10 +1,16 @@
|
|||
# [stancl/tenancy](https://tenancy.samuelstancl.me)
|
||||
<p align="center">
|
||||
<a href="https://tenancyforlaravel.com"><img width="800" src="/art/logo.png" alt="Tenancy for Laravel logo" /></a>
|
||||
</p>
|
||||
|
||||
[](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)
|
||||
<p align="center">
|
||||
<a href="https://laravel.com"><img alt="Laravel 6.x" src="https://img.shields.io/badge/laravel-6.x-red.svg"></a>
|
||||
<a href="https://packagist.org/packages/stancl/tenancy"><img alt="Latest Stable Version" src="https://poser.pugx.org/stancl/tenancy/version"></a>
|
||||
<a href="https://github.com/stancl/tenancy/actions"><img alt="GitHub Actions CI status" src="https://github.com/stancl/tenancy/workflows/CI/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/stancl/tenancy"><img alt="codecov" src="https://codecov.io/gh/stancl/tenancy/branch/2.x/graph/badge.svg"></a>
|
||||
<a href="https://github.com/stancl/tenancy/blob/2.x/DONATIONS.md"><img alt="Donate" src="https://img.shields.io/badge/Donate-%3C3-red"></a>
|
||||
</p>
|
||||
|
||||
<h1><a href="https://tenancyforlaravel.com">Tenancy for Laravel — stancl/tenancy</a></h1>
|
||||
|
||||
### *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)
|
||||
|
|
|
|||
9
SUPPORT.md
Normal file
9
SUPPORT.md
Normal file
|
|
@ -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)
|
||||
BIN
art/logo.png
Normal file
BIN
art/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 495 KiB |
BIN
art/old_logo.png
Normal file
BIN
art/old_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
16
assets/tenant_routes.php.stub
Normal file
16
assets/tenant_routes.php.stub
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Tenant Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can register the tenant routes for your application.
|
||||
| These routes are loaded by the TenantRouteServiceProvider
|
||||
| with the tenancy and web middleware groups. Good luck!
|
||||
|
|
||||
*/
|
||||
|
||||
Route::get('/app', function () {
|
||||
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
|
||||
});
|
||||
|
|
@ -10,17 +10,17 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"illuminate/support": "^6.0",
|
||||
"ext-json": "*",
|
||||
"illuminate/support": "^6.0|^7.0",
|
||||
"facade/ignition-contracts": "^1.0",
|
||||
"ramsey/uuid": "^3.7"
|
||||
"ramsey/uuid": "^3.7|^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"vlucas/phpdotenv": "^3.3",
|
||||
"laravel/telescope": "^2.0",
|
||||
"laravel/framework": "^6.0",
|
||||
"orchestra/testbench-browser-kit": "^4.0",
|
||||
"vlucas/phpdotenv": "^3.3|^4.0",
|
||||
"laravel/framework": "^6.0|^7.0",
|
||||
"orchestra/testbench-browser-kit": "^4.0|^5.0",
|
||||
"league/flysystem-aws-s3-v3": "~1.0",
|
||||
"phpunit/phpcov": "^6.0"
|
||||
"phpunit/phpcov": "^6.0|^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
2
fulltest
2
fulltest
|
|
@ -4,4 +4,4 @@ set -e
|
|||
# for development
|
||||
docker-compose up -d
|
||||
./test "$@"
|
||||
docker-compose exec test vendor/bin/phpcov merge --clover clover.xml coverage/
|
||||
docker-compose exec -T test vendor/bin/phpcov merge --clover clover.xml coverage/
|
||||
|
|
|
|||
|
|
@ -33,53 +33,35 @@ class Install extends Command
|
|||
$this->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'),
|
||||
"<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Tenant Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register tenant routes for your application. These
|
||||
| routes are loaded by the TenantRouteServiceProvider within a group
|
||||
| which contains the \"InitializeTenancy\" middleware. Good luck!
|
||||
|
|
||||
*/
|
||||
|
||||
Route::get('/app', function () {
|
||||
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
|
||||
});
|
||||
"
|
||||
);
|
||||
$this->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'))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/Contracts/Future/CanDeleteKeys.php
Normal file
21
src/Contracts/Future/CanDeleteKeys.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Contracts\Future;
|
||||
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
/**
|
||||
* This interface will be part of the StorageDriver interface in 3.x.
|
||||
*/
|
||||
interface CanDeleteKeys
|
||||
{
|
||||
/**
|
||||
* Delete keys from the storage.
|
||||
*
|
||||
* @param string[] $keys
|
||||
* @return void
|
||||
*/
|
||||
public function deleteMany(array $keys, Tenant $tenant = null): void;
|
||||
}
|
||||
24
src/Contracts/Future/CanFindByAnyKey.php
Normal file
24
src/Contracts/Future/CanFindByAnyKey.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Contracts\Future;
|
||||
|
||||
use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
/**
|
||||
* This interface *might* be part of the StorageDriver interface in 3.x.
|
||||
*/
|
||||
interface CanFindByAnyKey
|
||||
{
|
||||
/**
|
||||
* Find a tenant using an arbitrary key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return Tenant
|
||||
* @throws TenantDoesNotExistException
|
||||
*/
|
||||
public function findBy(string $key, $value): Tenant;
|
||||
}
|
||||
13
src/Contracts/Future/CanSetConnection.php
Normal file
13
src/Contracts/Future/CanSetConnection.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Contracts\Future;
|
||||
|
||||
/**
|
||||
* This interface *might* be part of the TenantDatabaseManager interface in 3.x.
|
||||
*/
|
||||
interface CanSetConnection
|
||||
{
|
||||
public function setConnection(string $connection): void;
|
||||
}
|
||||
|
|
@ -4,8 +4,12 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Stancl\Tenancy\Contracts\Future\CanSetConnection;
|
||||
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
|
||||
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
|
||||
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
|
||||
use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException;
|
||||
|
|
@ -23,6 +27,9 @@ class DatabaseManager
|
|||
/** @var BaseDatabaseManager */
|
||||
protected $database;
|
||||
|
||||
/** @var TenantManager */
|
||||
protected $tenancy;
|
||||
|
||||
public function __construct(Application $app, BaseDatabaseManager $database)
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/Exceptions/NotImplementedException.php
Normal file
15
src/Exceptions/NotImplementedException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class NotImplementedException extends Exception
|
||||
{
|
||||
public function __construct($class, $method, $extra)
|
||||
{
|
||||
parent::__construct("The $class class does not implement the $method method. $extra");
|
||||
}
|
||||
}
|
||||
15
src/Exceptions/TenantDatabaseDoesNotExistException.php
Normal file
15
src/Exceptions/TenantDatabaseDoesNotExistException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class TenantDatabaseDoesNotExistException extends Exception
|
||||
{
|
||||
public function __construct($database)
|
||||
{
|
||||
parent::__construct("Database $database does not exist.");
|
||||
}
|
||||
}
|
||||
15
src/Exceptions/TenantDoesNotExistException.php
Normal file
15
src/Exceptions/TenantDoesNotExistException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class TenantDoesNotExistException extends Exception
|
||||
{
|
||||
public function __construct(string $id, string $key = 'id')
|
||||
{
|
||||
$this->message = "Tenant with this $key does not exist: $id";
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
46
src/Features/Timestamps.php
Normal file
46
src/Features/Timestamps.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Features;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Stancl\Tenancy\Contracts\Feature;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Stancl\Tenancy\TenantManager;
|
||||
|
||||
class Timestamps implements Feature
|
||||
{
|
||||
/** @var Repository */
|
||||
protected $config;
|
||||
|
||||
public function __construct(Repository $config)
|
||||
{
|
||||
$this->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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
src/Jobs/QueuedTenantDatabaseSeeder.php
Normal file
38
src/Jobs/QueuedTenantDatabaseSeeder.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class QueuedTenantDatabaseSeeder implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** @var string */
|
||||
protected $tenantId;
|
||||
|
||||
public function __construct(Tenant $tenant)
|
||||
{
|
||||
$this->tenantId = $tenant->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Artisan::call('tenants:seed', [
|
||||
'--tenants' => [$this->tenantId],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
70
src/Middleware/InitializeTenancyByRequestData.php
Normal file
70
src/Middleware/InitializeTenancyByRequestData.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
|
||||
|
||||
class InitializeTenancyByRequestData
|
||||
{
|
||||
/** @var string|null */
|
||||
protected $header;
|
||||
|
||||
/** @var string|null */
|
||||
protected $queryParameter;
|
||||
|
||||
/** @var callable */
|
||||
protected $onFail;
|
||||
|
||||
public function __construct(?string $header = 'X-Tenant', ?string $queryParameter = 'tenant', callable $onFail = null)
|
||||
{
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
src/StorageDrivers/Database/CachedTenantResolver.php
Normal file
68
src/StorageDrivers/Database/CachedTenantResolver.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Cache\CacheManager;
|
||||
use Illuminate\Cache\Repository as CacheRepository;
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
|
||||
class CachedTenantResolver
|
||||
{
|
||||
/** @var CacheRepository */
|
||||
protected $cache;
|
||||
|
||||
/** @var ConfigRepository */
|
||||
protected $config;
|
||||
|
||||
public function __construct(CacheManager $cacheManager, ConfigRepository $config)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @internal Class is subject to breaking changes in minor and patch versions.
|
||||
*/
|
||||
class DomainModel extends Model
|
||||
{
|
||||
use CentralConnection;
|
||||
|
||||
protected $guarded = [];
|
||||
protected $primaryKey = 'domain';
|
||||
public $incrementing = false;
|
||||
public $timestamps = false;
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return config('tenancy.storage_drivers.db.table_names.DomainModel', 'domains');
|
||||
}
|
||||
}
|
||||
57
src/StorageDrivers/Database/DomainRepository.php
Normal file
57
src/StorageDrivers/Database/DomainRepository.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class DomainRepository extends Repository
|
||||
{
|
||||
public function getTenantIdByDomain(string $domain): ?string
|
||||
{
|
||||
return $this->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';
|
||||
}
|
||||
}
|
||||
41
src/StorageDrivers/Database/Repository.php
Normal file
41
src/StorageDrivers/Database/Repository.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
/** @mixin Builder */
|
||||
abstract class Repository
|
||||
{
|
||||
/** @var Connection */
|
||||
public $database;
|
||||
|
||||
/** @var string */
|
||||
protected $tableName;
|
||||
|
||||
/** @var Builder */
|
||||
private $table;
|
||||
|
||||
public function __construct(ConfigRepository $config)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @internal Class is subject to breaking changes in minor and patch versions.
|
||||
*/
|
||||
class TenantModel extends Model
|
||||
{
|
||||
use CentralConnection;
|
||||
|
||||
protected $guarded = [];
|
||||
protected $primaryKey = 'id';
|
||||
public $incrementing = false;
|
||||
public $timestamps = false;
|
||||
|
||||
public function getTable()
|
||||
{
|
||||
return config('tenancy.storage_drivers.db.table_names.TenantModel', 'tenants');
|
||||
}
|
||||
|
||||
public static function dataColumn()
|
||||
{
|
||||
return config('tenancy.storage_drivers.db.data_column', 'data');
|
||||
}
|
||||
|
||||
public static function customColumns()
|
||||
{
|
||||
return config('tenancy.storage_drivers.db.custom_columns', []);
|
||||
}
|
||||
|
||||
public static function getAllTenants(array $ids)
|
||||
{
|
||||
$tenants = $ids ? static::findMany($ids) : static::all();
|
||||
|
||||
return $tenants->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),
|
||||
]));
|
||||
}
|
||||
}
|
||||
186
src/StorageDrivers/Database/TenantRepository.php
Normal file
186
src/StorageDrivers/Database/TenantRepository.php
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\StorageDrivers\Database;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use stdClass;
|
||||
|
||||
class TenantRepository extends Repository
|
||||
{
|
||||
public function all($ids = [])
|
||||
{
|
||||
if ($ids) {
|
||||
$data = $this->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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
103
src/Tenant.php
103
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);
|
||||
|
|
|
|||
|
|
@ -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'");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
Normal file
47
src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\TenantDatabaseManagers;
|
||||
|
||||
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 PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
|
||||
{
|
||||
/** @var string */
|
||||
protected $connection;
|
||||
|
||||
public function __construct(Repository $config)
|
||||
{
|
||||
$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 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'");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/Traits/HasATenantArgument.php
Normal file
29
src/Traits/HasATenantArgument.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Traits;
|
||||
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
trait HasATenantArgument
|
||||
{
|
||||
protected function getArguments()
|
||||
{
|
||||
return array_merge([
|
||||
['tenant', InputArgument::REQUIRED, 'Tenant id', null],
|
||||
], parent::getArguments());
|
||||
}
|
||||
|
||||
protected function getTenants(): array
|
||||
{
|
||||
return [tenancy()->find($this->argument('tenant'))];
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->specifyParameters();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
src/Traits/TenantAwareCommand.php
Normal file
38
src/Traits/TenantAwareCommand.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Traits;
|
||||
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
trait TenantAwareCommand
|
||||
{
|
||||
/** @return int */
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$tenants = $this->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;
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
8
test
8
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 "$@"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
164
tests/CachedResolverTest.php
Normal file
164
tests/CachedResolverTest.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class CachedResolverTest extends TestCase
|
||||
{
|
||||
public $autoCreateTenant = false;
|
||||
public $autoInitTenancy = false;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
if (config('tenancy.storage_driver') !== 'db') {
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
143
tests/DatabaseSchemaManagerTest.php
Normal file
143
tests/DatabaseSchemaManagerTest.php
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class DatabaseSchemaManagerTest extends TestCase
|
||||
{
|
||||
public $autoInitTenancy = false;
|
||||
|
||||
protected function getEnvironmentSetUp($app)
|
||||
{
|
||||
parent::getEnvironmentSetUp($app);
|
||||
|
||||
$app['config']->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);
|
||||
}
|
||||
}
|
||||
38
tests/Etc/AddUserCommand.php
Normal file
38
tests/Etc/AddUserCommand.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Traits\HasATenantsOption;
|
||||
use Stancl\Tenancy\Traits\TenantAwareCommand;
|
||||
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
use TenantAwareCommand, HasATenantsOption;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:add';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
User::create([
|
||||
'name' => 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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,5 +15,6 @@ class ConsoleKernel extends Kernel
|
|||
*/
|
||||
protected $commands = [
|
||||
ExampleCommand::class,
|
||||
AddUserCommand::class,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
66
tests/Etc/defaultHttpKernelv7.stub
Normal file
66
tests/Etc/defaultHttpKernelv7.stub
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Fruitcake\Cors\HandleCors::class,
|
||||
\App\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
\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,
|
||||
];
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ class Kernel extends HttpKernel
|
|||
],
|
||||
|
||||
'api' => [
|
||||
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
],
|
||||
80
tests/Etc/modifiedHttpKernelv7.stub
Normal file
80
tests/Etc/modifiedHttpKernelv7.stub
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Fruitcake\Cors\HandleCors::class,
|
||||
\App\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
\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,
|
||||
];
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
56
tests/Features/TimestampTest.php
Normal file
56
tests/Features/TimestampTest.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Features;
|
||||
|
||||
use Stancl\Tenancy\Features\Timestamps;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
class TimestampTest extends TestCase
|
||||
{
|
||||
public $autoCreateTenant = false;
|
||||
public $autoInitTenancy = false;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['tenancy.features' => [
|
||||
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);
|
||||
}
|
||||
}
|
||||
45
tests/FutureTest.php
Normal file
45
tests/FutureTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class FutureTest extends TestCase
|
||||
{
|
||||
public $autoCreateTenant = false;
|
||||
public $autoInitTenancy = false;
|
||||
|
||||
/** @test */
|
||||
public function keys_can_be_deleted_from_tenant_storage()
|
||||
{
|
||||
$tenant = Tenant::new()->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']));
|
||||
}
|
||||
}
|
||||
62
tests/RequestDataIdentificationTest.php
Normal file
62
tests/RequestDataIdentificationTest.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||
use Stancl\Tenancy\Tenant;
|
||||
|
||||
class RequestDataIdentificationTest extends TestCase
|
||||
{
|
||||
public $autoCreateTenant = false;
|
||||
public $autoInitTenancy = false;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'tenancy.exempt_domains' => [
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
35
tests/Traits/TenantAwareCommandTest.php
Normal file
35
tests/Traits/TenantAwareCommandTest.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Traits;
|
||||
|
||||
use Stancl\Tenancy\Tenant;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
|
||||
class TenantAwareCommandTest extends TestCase
|
||||
{
|
||||
public $autoCreateTenant = false;
|
||||
public $autoInitTenancy = false;
|
||||
|
||||
/** @test */
|
||||
public function commands_run_globally_are_tenant_aware_and_return_valid_exit_code()
|
||||
{
|
||||
$tenant1 = Tenant::new()->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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue