mirror of
https://github.com/archtechx/tenancy.git
synced 2026-03-22 05:14:04 +00:00
Merge branch 'tenantkey-resolver' into 3.x
This commit is contained in:
commit
cea7ee9c9d
45 changed files with 515 additions and 158 deletions
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support Questions & Other
|
||||
url: https://github.com/stancl/tenancy/blob/3.x/SUPPORT.md
|
||||
url: https://archte.ch/discord
|
||||
about: 'If you have a question or need help using the package.'
|
||||
- name: Documentation Issue
|
||||
url: https://github.com/stancl/tenancy-docs/issues
|
||||
|
|
|
|||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
|
|
@ -5,9 +5,9 @@ env:
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ 3.x, 2.x ]
|
||||
branches: [ 3.x ]
|
||||
pull_request:
|
||||
branches: [ 3.x, 2.x ]
|
||||
branches: [ 3.x ]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
|
@ -15,17 +15,19 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ["7.3", "8.0"]
|
||||
laravel: ["^6.0", "^8.0", "^9.0"]
|
||||
exclude:
|
||||
- laravel: "^9.0"
|
||||
php: "7.3"
|
||||
include:
|
||||
- laravel: 9
|
||||
php: "8.0"
|
||||
- laravel: 10
|
||||
php: "8.1"
|
||||
- laravel: 11
|
||||
php: "8.3"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Start docker containers
|
||||
run: PHP_VERSION=${{ matrix.php }} docker-compose up -d
|
||||
- name: Install dependencies
|
||||
run: docker-compose exec -T test composer require --no-interaction "laravel/framework:${{ matrix.laravel }}"
|
||||
run: docker-compose exec -T test composer require --no-interaction "laravel/framework:^${{ matrix.laravel }}.0"
|
||||
- name: Run tests
|
||||
run: ./test
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,8 +5,10 @@ vendor/
|
|||
.idea
|
||||
psysh
|
||||
.phpunit.result.cache
|
||||
.phpunit.cache
|
||||
phpunit_var_*.xml
|
||||
coverage/
|
||||
clover.xml
|
||||
tests/Etc/tmp/queuetest.json
|
||||
docker-compose.override.yml
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -21,4 +21,4 @@ services:
|
|||
platform: linux/amd64
|
||||
```
|
||||
|
||||
to `docker-compose.override.yml` to make `docker-compose up-d` work on M1.
|
||||
to `docker-compose.override.yml` to make `docker-compose up -d` work on M1.
|
||||
|
|
|
|||
68
Dockerfile
68
Dockerfile
|
|
@ -1,50 +1,42 @@
|
|||
ARG PHP_VERSION=7.4
|
||||
ARG PHP_TARGET=php:${PHP_VERSION}-cli
|
||||
# add amd64 platform to support Mac M1
|
||||
FROM --platform=linux/amd64 shivammathur/node:jammy-amd64
|
||||
|
||||
FROM ${PHP_TARGET}
|
||||
|
||||
ARG COMPOSER_TARGET=2.0.3
|
||||
ARG PHP_VERSION=8.1
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/stancl/tenancy \
|
||||
org.opencontainers.image.vendor="Samuel Štancl" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="PHP ${PHP_VERSION} with modules for laravel support" \
|
||||
org.opencontainers.image.description="PHP ${PHP_VERSION} with a set of php/os packages suitable for running Laravel apps"
|
||||
|
||||
# our default timezone and langauge
|
||||
ENV TZ=Europe/London
|
||||
ENV LANG=en_GB.UTF-8
|
||||
|
||||
# Note: we only install reliable/core 1st-party php extensions here.
|
||||
# If your app needs custom ones install them in the apps own
|
||||
# Dockerfile _and pin the versions_! Eg:
|
||||
# RUN pecl install memcached-2.2.0 && docker-php-ext-enable memcached
|
||||
# install MYSSQL ODBC Driver
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y gnupg2 \
|
||||
&& curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \
|
||||
&& curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list \
|
||||
&& apt-get update \
|
||||
&& ACCEPT_EULA=Y apt-get install -y unixodbc-dev=2.3.7 unixodbc=2.3.7 odbcinst1debian2=2.3.7 odbcinst=2.3.7 msodbcsql17
|
||||
|
||||
# set PHP version
|
||||
RUN update-alternatives --set php /usr/bin/php$PHP_VERSION \
|
||||
&& update-alternatives --set phar /usr/bin/phar$PHP_VERSION \
|
||||
&& update-alternatives --set phar.phar /usr/bin/phar.phar$PHP_VERSION \
|
||||
&& update-alternatives --set phpize /usr/bin/phpize$PHP_VERSION \
|
||||
&& update-alternatives --set php-config /usr/bin/php-config$PHP_VERSION
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libhiredis0.14 libjemalloc2 liblua5.1-0 lua-bitop lua-cjson redis redis-server redis-tools
|
||||
|
||||
RUN pecl install redis-5.3.7 sqlsrv pdo_sqlsrv pcov \
|
||||
&& printf "; priority=20\nextension=redis.so\n" > /etc/php/$PHP_VERSION/mods-available/redis.ini \
|
||||
&& printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/$PHP_VERSION/mods-available/sqlsrv.ini \
|
||||
&& printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/$PHP_VERSION/mods-available/pdo_sqlsrv.ini \
|
||||
&& printf "; priority=40\nextension=pcov.so\n" > /etc/php/$PHP_VERSION/mods-available/pcov.ini \
|
||||
&& phpenmod -v $PHP_VERSION redis sqlsrv pdo_sqlsrv pcov
|
||||
|
||||
# install composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# install some OS packages we need
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git
|
||||
# install php extensions
|
||||
RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \
|
||||
# && if [ "${PHP_VERSION}" = "7.4" ]; then docker-php-ext-configure gd --with-freetype --with-jpeg; else docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; fi \
|
||||
&& docker-php-ext-install -j$(nproc) gd pdo pdo_mysql pdo_pgsql pdo_sqlite pgsql zip gmp bcmath pcntl ldap sysvmsg exif \
|
||||
# install the redis php extension
|
||||
&& pecl install redis-5.3.7 \
|
||||
&& docker-php-ext-enable redis \
|
||||
# install the pcov extention
|
||||
&& pecl install pcov \
|
||||
&& docker-php-ext-enable pcov \
|
||||
&& echo "pcov.enabled = 1" > /usr/local/etc/php/conf.d/pcov.ini
|
||||
# clear the apt cache
|
||||
RUN rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
# install composer
|
||||
&& curl -o /tmp/composer-setup.php https://getcomposer.org/installer \
|
||||
&& curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \
|
||||
&& php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \
|
||||
&& php /tmp/composer-setup.php --version=${COMPOSER_TARGET} --no-ansi --install-dir=/usr/local/bin --filename=composer --snapshot \
|
||||
&& rm -f /tmp/composer-setup.*
|
||||
# set the system timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||
&& echo $TZ > /etc/timezone
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://laravel.com"><img alt="Laravel 9.x" src="https://img.shields.io/badge/laravel-9.x-red.svg"></a>
|
||||
<a href="https://laravel.com"><img alt="Laravel 10.x" src="https://img.shields.io/badge/laravel-10.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://github.com/stancl/tenancy/blob/3.x/DONATIONS.md"><img alt="Donate" src="https://img.shields.io/badge/Donate-%3C3-red"></a>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Get Support
|
||||
|
||||
If you need help with implementing the package, you can join our community [Discord server](https://discord.gg/7cpgPxv) and ask in `#help`.
|
||||
If you need help with implementing the package, you can join our community [Discord server](https://archte.ch/discord) and ask in `#tenancy-help`.
|
||||
|
||||
If you're interested in paid consulting from the maintainer, see the [contact page](https://tenancyforlaravel.com/contact/) on our website.
|
||||
|
|
|
|||
|
|
@ -120,10 +120,12 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
|
||||
protected function mapRoutes()
|
||||
{
|
||||
if (file_exists(base_path('routes/tenant.php'))) {
|
||||
Route::namespace(static::$controllerNamespace)
|
||||
->group(base_path('routes/tenant.php'));
|
||||
}
|
||||
$this->app->booted(function () {
|
||||
if (file_exists(base_path('routes/tenant.php'))) {
|
||||
Route::namespace(static::$controllerNamespace)
|
||||
->group(base_path('routes/tenant.php'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function makeTenancyMiddlewareHighestPriority()
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ return [
|
|||
* See https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper
|
||||
*/
|
||||
'root_override' => [
|
||||
// Disks whose roots should be overriden after storage_path() is suffixed.
|
||||
// Disks whose roots should be overridden after storage_path() is suffixed.
|
||||
'local' => '%storage_path%/app/',
|
||||
'public' => '%storage_path%/app/public/',
|
||||
],
|
||||
|
|
@ -139,7 +139,7 @@ return [
|
|||
],
|
||||
|
||||
/**
|
||||
* Redis tenancy config. Used by RedisTenancyBoostrapper.
|
||||
* Redis tenancy config. Used by RedisTenancyBootstrapper.
|
||||
*
|
||||
* Note: You need phpredis to use Redis tenancy.
|
||||
*
|
||||
|
|
@ -168,6 +168,7 @@ return [
|
|||
// Stancl\Tenancy\Features\UniversalRoutes::class,
|
||||
// Stancl\Tenancy\Features\TenantConfig::class, // https://tenancyforlaravel.com/docs/v3/features/tenant-config
|
||||
// Stancl\Tenancy\Features\CrossDomainRedirect::class, // https://tenancyforlaravel.com/docs/v3/features/cross-domain-redirect
|
||||
// Stancl\Tenancy\Features\ViteBundler::class,
|
||||
],
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,19 +10,20 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"ext-json": "*",
|
||||
"illuminate/support": "^6.0|^7.0|^8.0|^9.0",
|
||||
"facade/ignition-contracts": "^1.0",
|
||||
"ramsey/uuid": "^3.7|^4.0",
|
||||
"stancl/jobpipeline": "^1.0",
|
||||
"stancl/virtualcolumn": "^1.0"
|
||||
"illuminate/support": "^9.0|^10.0|^11.0",
|
||||
"facade/ignition-contracts": "^1.0.2",
|
||||
"ramsey/uuid": "^4.7.3",
|
||||
"stancl/jobpipeline": "^1.6.2",
|
||||
"stancl/virtualcolumn": "^1.3.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "^6.0|^7.0|^8.0|^9.0",
|
||||
"orchestra/testbench": "^4.0|^5.0|^6.0|^7.0",
|
||||
"league/flysystem-aws-s3-v3": "^1.0|^3.0",
|
||||
"doctrine/dbal": "^2.10",
|
||||
"spatie/valuestore": "^1.2.5"
|
||||
"laravel/framework": "^9.0|^10.0|^11.0",
|
||||
"orchestra/testbench": "^7.0|^8.0|^9.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.12.2",
|
||||
"doctrine/dbal": "^3.6.0",
|
||||
"spatie/valuestore": "^1.3.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
@ -49,5 +50,10 @@
|
|||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
docker-compose-m1.override.yml
Normal file
7
docker-compose-m1.override.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
services:
|
||||
mysql:
|
||||
platform: linux/amd64
|
||||
mysql2:
|
||||
platform: linux/amd64
|
||||
mssql:
|
||||
image: mcr.microsoft.com/azure-sql-edge
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
version: '2.4'
|
||||
services:
|
||||
test:
|
||||
build:
|
||||
|
|
|
|||
70
phpunit.xml
70
phpunit.xml
|
|
@ -1,38 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">./src</directory>
|
||||
<exclude>
|
||||
<file>./src/routes.php</file>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="redis"/>
|
||||
<env name="MAIL_DRIVER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="DB_CONNECTION" value="central"/>
|
||||
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
|
||||
</php>
|
||||
<logging>
|
||||
<log type="coverage-clover" target="coverage/phpunit/clover.xml" showUncoveredFiles="true"/>
|
||||
<log type="coverage-html" target="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/>
|
||||
</logging>
|
||||
</phpunit>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
|
||||
<coverage>
|
||||
<report>
|
||||
<clover outputFile="coverage/phpunit/clover.xml"/>
|
||||
<html outputDirectory="coverage/phpunit/html" lowUpperBound="35" highLowerBound="70"/>
|
||||
</report>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="redis"/>
|
||||
<env name="MAIL_DRIVER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="DB_CONNECTION" value="central"/>
|
||||
<env name="AWS_DEFAULT_REGION" value="us-west-2"/>
|
||||
</php>
|
||||
<logging/>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<file>./src/routes.php</file>
|
||||
<file>./src/Vite.php</file>
|
||||
</exclude>
|
||||
</source>
|
||||
</phpunit>
|
||||
|
|
|
|||
|
|
@ -63,14 +63,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
|||
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
|
||||
if (version_compare(app()->version(), '8.64', '>=')) {
|
||||
// JobRetryRequested only exists since Laravel 8.64
|
||||
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
||||
$previousTenant = tenant();
|
||||
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
|
||||
$previousTenant = tenant();
|
||||
|
||||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
}
|
||||
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
|
||||
});
|
||||
|
||||
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
|
||||
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
|
|||
$prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey();
|
||||
$client = Redis::connection($connection)->client();
|
||||
|
||||
$this->originalPrefixes[$connection] = $client->getOption($client::OPT_PREFIX);
|
||||
$client->setOption($client::OPT_PREFIX, $prefix);
|
||||
$this->originalPrefixes[$connection] = $client->getOption(\Redis::OPT_PREFIX);
|
||||
$client->setOption(\Redis::OPT_PREFIX, $prefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
|
|||
foreach ($this->prefixedConnections() as $connection) {
|
||||
$client = Redis::connection($connection)->client();
|
||||
|
||||
$client->setOption($client::OPT_PREFIX, $this->originalPrefixes[$connection]);
|
||||
$client->setOption(\Redis::OPT_PREFIX, $this->originalPrefixes[$connection]);
|
||||
}
|
||||
|
||||
$this->originalPrefixes = [];
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ final class MigrateFresh extends Command
|
|||
parent::__construct();
|
||||
|
||||
$this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null);
|
||||
$this->addOption('--step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually.');
|
||||
|
||||
$this->setName('tenants:migrate-fresh');
|
||||
}
|
||||
|
|
@ -47,6 +48,7 @@ final class MigrateFresh extends Command
|
|||
$this->info('Migrating.');
|
||||
$this->callSilent('tenants:migrate', [
|
||||
'--tenants' => [$tenant->getTenantKey()],
|
||||
'--step' => $this->option('step'),
|
||||
'--force' => true,
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||
|
||||
trait TenantAwareCommand
|
||||
{
|
||||
/** @return int */
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$tenants = $this->getTenants();
|
||||
$exitCode = 0;
|
||||
|
|
|
|||
|
|
@ -15,4 +15,6 @@ interface Syncable
|
|||
public function getSyncedAttributeNames(): array;
|
||||
|
||||
public function triggerSyncEvent();
|
||||
|
||||
public function shouldSync(): bool;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Controllers;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Throwable;
|
||||
|
||||
class TenantAssetsController extends Controller
|
||||
{
|
||||
|
|
@ -15,12 +17,53 @@ class TenantAssetsController extends Controller
|
|||
$this->middleware(static::$tenancyMiddleware);
|
||||
}
|
||||
|
||||
public function asset($path)
|
||||
public function asset($path = null)
|
||||
{
|
||||
$this->validatePath($path);
|
||||
|
||||
try {
|
||||
return response()->file(storage_path("app/public/$path"));
|
||||
} catch (\Throwable $th) {
|
||||
} catch (Throwable $th) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent path traversal attacks. This is generally a non-issue on modern
|
||||
* webservers but it's still worth handling on the application level as well.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
protected function validatePath(string|null $path): void
|
||||
{
|
||||
$this->abortIf($path === null, 'Empty path');
|
||||
|
||||
$allowedRoot = realpath(storage_path('app/public'));
|
||||
|
||||
// `storage_path('app/public')` doesn't exist, so it cannot contain files
|
||||
$this->abortIf($allowedRoot === false, "Storage root doesn't exist");
|
||||
|
||||
$attemptedPath = realpath("{$allowedRoot}/{$path}");
|
||||
|
||||
// User is attempting to access a nonexistent file
|
||||
$this->abortIf($attemptedPath === false, 'Accessing a nonexistent file');
|
||||
|
||||
// User is attempting to access a file outside the $allowedRoot folder
|
||||
$this->abortIf(! str($attemptedPath)->startsWith($allowedRoot), 'Accessing a file outside the storage root');
|
||||
}
|
||||
|
||||
protected function abortIf($condition, $exceptionMessage): void
|
||||
{
|
||||
if ($condition) {
|
||||
if (app()->runningUnitTests()) {
|
||||
// Makes testing the cause of the failure in validatePath() easier
|
||||
throw new Exception($exceptionMessage);
|
||||
} else {
|
||||
// We always use 404 to avoid leaking information about the cause of the error
|
||||
// e.g. when someone is trying to access a nonexistent file outside of the allowed
|
||||
// root folder, we don't want to let the user know whether such a file exists or not.
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,16 @@ trait ResourceSyncing
|
|||
{
|
||||
static::saved(function (Syncable $model) {
|
||||
/** @var ResourceSyncing $model */
|
||||
$model->triggerSyncEvent();
|
||||
if ($model->shouldSync()) {
|
||||
$model->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
|
||||
static::creating(function (self $model) {
|
||||
if (! $model->getAttribute($model->getGlobalIdentifierKeyName()) && app()->bound(UniqueIdentifierGenerator::class)) {
|
||||
$model->setAttribute(
|
||||
$model->getGlobalIdentifierKeyName(),
|
||||
app(UniqueIdentifierGenerator::class)->generate($model)
|
||||
);
|
||||
$keyName = $model->getGlobalIdentifierKeyName();
|
||||
|
||||
if (! $model->getAttribute($keyName) && app()->bound(UniqueIdentifierGenerator::class)) {
|
||||
$model->setAttribute($keyName, app(UniqueIdentifierGenerator::class)->generate($model));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -32,4 +33,9 @@ trait ResourceSyncing
|
|||
/** @var Syncable $this */
|
||||
event(new SyncedResourceSaved($this, tenant()));
|
||||
}
|
||||
|
||||
public function shouldSync(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ class ImpersonationToken extends Model
|
|||
protected $primaryKey = 'token';
|
||||
public $incrementing = false;
|
||||
protected $table = 'tenant_user_impersonation_tokens';
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ class Tenant extends Model implements Contracts\Tenant
|
|||
Concerns\TenantRun,
|
||||
Concerns\InvalidatesResolverCache;
|
||||
|
||||
protected static $modelsShouldPreventAccessingMissingAttributes = false;
|
||||
|
||||
protected $table = 'tenants';
|
||||
protected $primaryKey = 'id';
|
||||
protected $guarded = [];
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class TenantPivot extends Pivot
|
|||
static::saved(function (self $pivot) {
|
||||
$parent = $pivot->pivotParent;
|
||||
|
||||
if ($parent instanceof Syncable) {
|
||||
if ($parent instanceof Syncable && $parent->shouldSync()) {
|
||||
$parent->triggerSyncEvent();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Features;
|
|||
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Contracts\Feature;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
|
|
@ -45,7 +46,7 @@ class TenantConfig implements Feature
|
|||
{
|
||||
/** @var Tenant|Model $tenant */
|
||||
foreach (static::$storageToConfigMap as $storageKey => $configKey) {
|
||||
$override = $tenant->getAttribute($storageKey);
|
||||
$override = Arr::get($tenant, $storageKey);
|
||||
|
||||
if (! is_null($override)) {
|
||||
if (is_array($configKey)) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class UniversalRoutes implements Feature
|
|||
|
||||
public static function routeHasMiddleware(Route $route, $middleware): bool
|
||||
{
|
||||
if (in_array($middleware, $route->middleware(), true)) {
|
||||
if (in_array($middleware, $route->computedMiddleware ?? $route->middleware(), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
26
src/Features/ViteBundler.php
Normal file
26
src/Features/ViteBundler.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Features;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Stancl\Tenancy\Contracts\Feature;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
use Stancl\Tenancy\Vite;
|
||||
|
||||
class ViteBundler implements Feature
|
||||
{
|
||||
/** @var Application */
|
||||
protected $app;
|
||||
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
public function bootstrap(Tenancy $tenancy): void
|
||||
{
|
||||
$this->app->singleton(\Illuminate\Foundation\Vite::class, Vite::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Middleware;
|
|||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Stancl\Tenancy\Contracts\TenantResolver;
|
||||
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,7 @@ abstract class CachedTenantResolver implements TenantResolver
|
|||
|
||||
$key = $this->getCacheKey(...$args);
|
||||
|
||||
if ($this->cache->has($key)) {
|
||||
$tenant = $this->cache->get($key);
|
||||
|
||||
if ($tenant = $this->cache->get($key)) {
|
||||
$this->resolved($tenant, ...$args);
|
||||
|
||||
return $tenant;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
|||
throw new TenantCouldNotBeIdentifiedByPathException($id);
|
||||
}
|
||||
|
||||
public function resolved(Tenant $tenant, ...$args): void
|
||||
{
|
||||
/** @var Route $route */
|
||||
$route = $args[0];
|
||||
|
||||
$route->forgetParameter(static::$tenantParameterName);
|
||||
}
|
||||
|
||||
public function getArgsForTenant(Tenant $tenant): array
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class TenancyServiceProvider extends ServiceProvider
|
|||
$this->app->singleton(Commands\Rollback::class, function ($app) {
|
||||
return new Commands\Rollback($app['migrator']);
|
||||
});
|
||||
|
||||
$this->app->singleton(Commands\Seed::class, function ($app) {
|
||||
return new Commands\Seed($app['db']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
|
|||
|
||||
protected function isVersion8(): bool
|
||||
{
|
||||
$version = $this->database()->select($this->database()->raw('select version()'))[0]->{'version()'};
|
||||
$versionSelect = $this->database()->raw('select version()')->getValue($this->database()->getQueryGrammar());
|
||||
$version = $this->database()->select($versionSelect)[0]->{'version()'};
|
||||
|
||||
return version_compare($version, '8.0.0') >= 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,11 +46,7 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager
|
|||
|
||||
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
|
||||
{
|
||||
if (version_compare(app()->version(), '9.0', '>=')) {
|
||||
$baseConfig['search_path'] = $databaseName;
|
||||
} else {
|
||||
$baseConfig['schema'] = $databaseName;
|
||||
}
|
||||
$baseConfig['search_path'] = $databaseName;
|
||||
|
||||
return $baseConfig;
|
||||
}
|
||||
|
|
|
|||
23
src/Vite.php
Normal file
23
src/Vite.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy;
|
||||
|
||||
use Illuminate\Foundation\Vite as BaseVite;
|
||||
|
||||
class Vite extends BaseVite // todo move to a different directory in v4
|
||||
{
|
||||
/**
|
||||
* Generate an asset path for the application.
|
||||
*
|
||||
* @param string $path
|
||||
* @param bool|null $secure
|
||||
* @return string
|
||||
*/
|
||||
protected function assetPath($path, $secure = null)
|
||||
{
|
||||
// In Laravel 9, the property doesn't exist.
|
||||
return (isset($this->assetPathResolver) && $this->assetPathResolver)
|
||||
? ($this->assetPathResolver)($path, $secure)
|
||||
: global_asset($path);
|
||||
}
|
||||
}
|
||||
1
test
1
test
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
cat vendor/laravel/framework/src/Illuminate/Foundation/Application.php | grep 'const VERSION'
|
||||
docker-compose exec -T test vendor/bin/phpunit "$@"
|
||||
|
|
|
|||
|
|
@ -207,10 +207,6 @@ class BootstrapperTest extends TestCase
|
|||
$disk = Storage::disk($disk);
|
||||
$adapter = $disk->getAdapter();
|
||||
|
||||
if (! Str::startsWith(app()->version(), '9.')) {
|
||||
return $adapter->getPathPrefix();
|
||||
}
|
||||
|
||||
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
|
||||
$prefixer->setAccessible(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -196,10 +196,12 @@ class CommandsTest extends TestCase
|
|||
{
|
||||
$tenantId1 = Tenant::create()->getTenantKey();
|
||||
$tenantId2 = Tenant::create()->getTenantKey();
|
||||
$tenantId3 = Tenant::create()->getTenantKey();
|
||||
Artisan::call('tenants:migrate-fresh');
|
||||
|
||||
$this->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'")
|
||||
->expectsOutput('Tenant: ' . $tenantId1)
|
||||
->expectsOutput('Tenant: ' . $tenantId2);
|
||||
->expectsOutput('Tenant: ' . $tenantId2)
|
||||
->doesntExpectOutput('Tenant: ' . $tenantId3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class EventListenerTest extends TestCase
|
|||
});
|
||||
|
||||
$tenant = Tenant::create();
|
||||
dispatch_now(new CreateDatabase($tenant));
|
||||
dispatch_sync(new CreateDatabase($tenant));
|
||||
|
||||
$this->assertFalse($tenant->database()->manager()->databaseExists(
|
||||
$tenant->database()->getName()
|
||||
|
|
@ -192,12 +192,13 @@ class EventListenerTest extends TestCase
|
|||
})->toListener()
|
||||
);
|
||||
|
||||
Tenant::create([
|
||||
$tenant = Tenant::create([
|
||||
'tenancy_create_database' => false,
|
||||
'tenancy_db_name' => 'already_created',
|
||||
]);
|
||||
|
||||
$this->assertFalse($this->hasFailed());
|
||||
// assert test didn't fail
|
||||
$this->assertTrue($tenant->exists());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,30 @@ class TenantConfigTest extends TestCase
|
|||
parent::tearDown();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function nested_tenant_values_are_merged()
|
||||
{
|
||||
$this->assertSame(null, config('whitelabel.theme'));
|
||||
config([
|
||||
'tenancy.features' => [TenantConfig::class],
|
||||
'tenancy.bootstrappers' => [],
|
||||
]);
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
TenantConfig::$storageToConfigMap = [
|
||||
'whitelabel.config.theme' => 'whitelabel.theme',
|
||||
];
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'whitelabel' => ['config' => ['theme' => 'dark']],
|
||||
]);
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
$this->assertSame('dark', config('whitelabel.theme'));
|
||||
tenancy()->end();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function config_is_merged_and_removed()
|
||||
{
|
||||
|
|
|
|||
34
tests/Features/ViteBundlerTest.php
Normal file
34
tests/Features/ViteBundlerTest.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Stancl\Tenancy\Features\ViteBundler;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\Tests\TestCase;
|
||||
use Stancl\Tenancy\Vite as StanclVite;
|
||||
|
||||
class ViteBundlerTest extends TestCase
|
||||
{
|
||||
/** @test */
|
||||
public function the_vite_helper_uses_our_custom_class()
|
||||
{
|
||||
$vite = app(\Illuminate\Foundation\Vite::class);
|
||||
|
||||
$this->assertInstanceOf(\Illuminate\Foundation\Vite::class, $vite);
|
||||
$this->assertNotInstanceOf(StanclVite::class, $vite);
|
||||
|
||||
config([
|
||||
'tenancy.features' => [ViteBundler::class],
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
app()->forgetInstance(\Illuminate\Foundation\Vite::class);
|
||||
|
||||
$vite = app(\Illuminate\Foundation\Vite::class);
|
||||
|
||||
$this->assertInstanceOf(StanclVite::class, $vite);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Spatie\Valuestore\Valuestore;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -25,7 +23,6 @@ use Illuminate\Queue\Events\JobProcessed;
|
|||
use Illuminate\Queue\Events\JobProcessing;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use PDO;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
|
|
@ -59,6 +56,7 @@ class QueueTest extends TestCase
|
|||
|
||||
public function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
$this->valuestore->flush();
|
||||
}
|
||||
|
||||
|
|
@ -197,10 +195,6 @@ class QueueTest extends TestCase
|
|||
*/
|
||||
public function tenancy_is_initialized_when_retrying_jobs(bool $shouldEndTenancy)
|
||||
{
|
||||
if (! Str::startsWith(app()->version(), '8')) {
|
||||
$this->markTestSkipped('queue:retry tenancy is only supported in Laravel 8');
|
||||
}
|
||||
|
||||
$this->withFailedJobs();
|
||||
$this->withTenantDatabases();
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,61 @@ class ResourceSyncingTest extends TestCase
|
|||
});
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function resources_are_synced_only_sync_is_enabled()
|
||||
{
|
||||
CentralUser::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'secret',
|
||||
'role' => 'commenter', // synced
|
||||
]);
|
||||
|
||||
$tenant = ResourceTenant::create();
|
||||
$this->migrateTenants();
|
||||
|
||||
$tenant->run(function() {
|
||||
ResourceUser::create([
|
||||
'name' => 'Foo',
|
||||
'email' => 'foo@email.com',
|
||||
'password' => 'secret',
|
||||
'global_id' => 'acme',
|
||||
'role' => 'not_sync',
|
||||
]);
|
||||
});
|
||||
|
||||
$centralUser = CentralUser::first();
|
||||
$this->assertSame('John Doe', $centralUser->name); // not sync
|
||||
$this->assertSame('john@localhost', $centralUser->email); // not sync
|
||||
$this->assertSame('secret', $centralUser->password); // not sync
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function central_users_are_synced_only_sync_is_enabled()
|
||||
{
|
||||
$centralUser = CentralUser::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'secret',
|
||||
'role' => 'not_sync', // unsynced
|
||||
]);
|
||||
|
||||
$t1 = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
$this->migrateTenants();
|
||||
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
$t1->run(function () {
|
||||
// assert user not exsits
|
||||
$this->assertCount(0, ResourceUser::all());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function only_the_synced_columns_are_updated_in_the_central_db()
|
||||
{
|
||||
|
|
@ -626,6 +681,11 @@ class CentralUser extends Model implements SyncMaster
|
|||
'email',
|
||||
];
|
||||
}
|
||||
|
||||
public function shouldSync()
|
||||
{
|
||||
return $this->role !== 'not_sync';
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceUser extends Model implements Syncable
|
||||
|
|
@ -659,4 +719,9 @@ class ResourceUser extends Model implements Syncable
|
|||
'email',
|
||||
];
|
||||
}
|
||||
|
||||
public function shouldSync()
|
||||
{
|
||||
return $this->role !== 'not_sync';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,4 +126,87 @@ class TenantAssetTest extends TestCase
|
|||
|
||||
$this->assertSame($original, asset('foo'));
|
||||
}
|
||||
|
||||
public function test_asset_controller_returns_a_404_when_no_path_is_provided()
|
||||
{
|
||||
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->expectExceptionMessage('Empty path'); // outside tests this is a 404
|
||||
|
||||
$this->get(tenant_asset(null), [
|
||||
'X-Tenant' => $tenant->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_controller_returns_a_404_when_the_storage_root_doesnt_exist()
|
||||
{
|
||||
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$storageRoot = storage_path("app/public");
|
||||
|
||||
if (is_dir($storageRoot)) {
|
||||
rmdir(storage_path("app/public"));
|
||||
}
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->expectExceptionMessage("Storage root doesn't exist"); // outside tests this is a 404
|
||||
|
||||
$this->get(tenant_asset('foo.txt'), [
|
||||
'X-Tenant' => $tenant->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_controller_returns_a_404_when_accessing_a_nonexistent_file()
|
||||
{
|
||||
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$storageRoot = storage_path("app/public");
|
||||
|
||||
if (! is_dir($storageRoot)) {
|
||||
mkdir(storage_path("app/public"), recursive: true);
|
||||
}
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->expectExceptionMessage("Accessing a nonexistent file"); // outside tests this is a 404
|
||||
|
||||
$this->get(tenant_asset('foo.txt'), [
|
||||
'X-Tenant' => $tenant->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_controller_returns_a_404_when_accessing_a_file_outside_the_storage_root()
|
||||
{
|
||||
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$storageRoot = storage_path("app/public");
|
||||
|
||||
if (! is_dir($storageRoot)) {
|
||||
mkdir(storage_path("app/public"), recursive: true);
|
||||
file_put_contents(storage_path('app/foo.txt'), 'bar');
|
||||
}
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->expectExceptionMessage('Accessing a file outside the storage root'); // outside tests this is a 404
|
||||
|
||||
$this->get(tenant_asset('../foo.txt'), [
|
||||
'X-Tenant' => $tenant->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class TenantDatabaseManagerTest extends TestCase
|
|||
$this->assertTrue($postgresManager->databaseExists($database));
|
||||
}
|
||||
|
||||
public function database_manager_provider()
|
||||
public static function database_manager_provider()
|
||||
{
|
||||
return [
|
||||
['mysql', MySQLDatabaseManager::class],
|
||||
|
|
@ -194,9 +194,7 @@ class TenantDatabaseManagerTest extends TestCase
|
|||
]);
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$schemaConfig = version_compare(app()->version(), '9.0', '>=') ?
|
||||
config('database.connections.' . config('database.default') . '.search_path') :
|
||||
config('database.connections.' . config('database.default') . '.schema');
|
||||
$schemaConfig = config('database.connections.' . config('database.default') . '.search_path');
|
||||
|
||||
$this->assertSame($tenant->database()->getName(), $schemaConfig);
|
||||
$this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database']));
|
||||
|
|
|
|||
|
|
@ -57,9 +57,11 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
|
||||
$app['config']->set([
|
||||
'database.default' => 'central',
|
||||
'cache.default' => 'redis',
|
||||
'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'),
|
||||
'database.redis.default.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'),
|
||||
'database.redis.options.prefix' => 'foo',
|
||||
'database.redis.client' => 'predis',
|
||||
'database.connections.central' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
|
|
@ -80,6 +82,8 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
]) : [],
|
||||
],
|
||||
'database.connections.sqlite.database' => ':memory:',
|
||||
'database.connections.mysql.charset' => 'utf8mb4',
|
||||
'database.connections.mysql.collation' => 'utf8mb4_unicode_ci',
|
||||
'database.connections.mysql.host' => env('TENANCY_TEST_MYSQL_HOST', '127.0.0.1'),
|
||||
'database.connections.pgsql.host' => env('TENANCY_TEST_PGSQL_HOST', '127.0.0.1'),
|
||||
'tenancy.filesystem.disks' => [
|
||||
|
|
|
|||
|
|
@ -63,4 +63,46 @@ class UniversalRouteTest extends TestCase
|
|||
->assertSuccessful()
|
||||
->assertSee('acme');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function universal_route_works_when_middleware_is_inserted_via_controller_middleware()
|
||||
{
|
||||
Route::middlewareGroup('universal', []);
|
||||
config(['tenancy.features' => [UniversalRoutes::class]]);
|
||||
|
||||
Route::get('/foo', [UniversalRouteController::class, 'show']);
|
||||
|
||||
$this->get('http://localhost/foo')
|
||||
->assertSuccessful()
|
||||
->assertSee('Tenancy is not initialized.');
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
$tenant->domains()->create([
|
||||
'domain' => 'acme.localhost',
|
||||
]);
|
||||
|
||||
$this->get('http://acme.localhost/foo')
|
||||
->assertSuccessful()
|
||||
->assertSee('Tenancy is initialized.');
|
||||
}
|
||||
}
|
||||
|
||||
class UniversalRouteController
|
||||
{
|
||||
public function getMiddleware()
|
||||
{
|
||||
return array_map(fn($middleware) => [
|
||||
'middleware' => $middleware,
|
||||
'options' => [],
|
||||
], ['universal', InitializeTenancyByDomain::class]);
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
return tenancy()->initialized
|
||||
? 'Tenancy is initialized.'
|
||||
: 'Tenancy is not initialized.';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue