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

[4.x] Storage::url() support (modified #689) (#909)

* This adds support for tenancy aware  Storage::url() method

* Trigger CI build

* Fixed Link command for Laravel v6, added StorageLink Events, more StorageLink tests, added RemoveStorageSymlinks Job, added Storage Jobs to TenancyServiceProvider stub, renamed misleading config example.

* Fix typo

* Fix code style (php-cs-fixer)

* Update config comments

* Format code in Link command, make writing more concise

* Change "symLinks" to "symlinks"

* Refactor Link command

* Fix test name typo

* Test fetching files using the public URL

* Extract Link command logic into actions

* Fix code style (php-cs-fixer)

* Check if closure is null in CreateStorageSymlinksAction

* Stop using command terminology in CreateStorageSymlinksAction

* Separate the Storage::url() test cases

* Update url_override comments

* Remove afterLink closures, add types, move actions, add usage explanation to the symlink trait

* Fix code style (php-cs-fixer)

* Update public storage URL test

* Fix issue with using str()

* Improve url_override comment, add todos

* add todo comment

* fix docblock style

* Add link command tests back

* Add types to $tenants in the action handle() methods

* Fix typo, update variable name formatting

* Add tests for the symlink actions

* Change possibleTenantSymlinks not to prefix the paths twice while tenancy is initialized

* Fix code style (php-cs-fixer)

* Stop testing storage directory existence in symlink test

* Don't specify full namespace for Tenant model annotation

* Don't specify full namespace in ActionTest

* Remove "change to DI" todo

* Remove possibleTenantSymlinks return annotation

* Remove symlink-related jobs, instantiate and use actions

* Revert "Remove symlink-related jobs, instantiate and use actions"

This reverts commit 547440c887.

* Add a comment line about the possible tenant symlinks

* Correct storagePath and publicPath variables

* Revert "Correct storagePath and publicPath variables"

This reverts commit e3aa8e2086.

* add a todo

Co-authored-by: Martin Vlcek <martin@dontfreakout.eu>
Co-authored-by: lukinovec <lukinovec@gmail.com>
Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
This commit is contained in:
Samuel Štancl 2022-09-28 05:09:45 +02:00 committed by GitHub
parent b78320b882
commit 7bacc50b27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 622 additions and 14 deletions

View file

@ -28,6 +28,7 @@ class TenancyServiceProvider extends ServiceProvider
Jobs\CreateDatabase::class, Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class, Jobs\MigrateDatabase::class,
// Jobs\SeedDatabase::class, // Jobs\SeedDatabase::class,
Jobs\CreateStorageSymlinks::class,
// Your own jobs to prepare the tenant. // Your own jobs to prepare the tenant.
// Provision API keys, create S3 buckets, anything you want! // Provision API keys, create S3 buckets, anything you want!
@ -52,6 +53,7 @@ class TenancyServiceProvider extends ServiceProvider
Events\TenantDeleted::class => [ Events\TenantDeleted::class => [
JobPipeline::make([ JobPipeline::make([
Jobs\DeleteDatabase::class, Jobs\DeleteDatabase::class,
Jobs\RemoveStorageSymlinks::class,
])->send(function (Events\TenantDeleted $event) { ])->send(function (Events\TenantDeleted $event) {
return $event->tenant; return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
@ -95,6 +97,12 @@ class TenancyServiceProvider extends ServiceProvider
Listeners\UpdateSyncedResource::class, Listeners\UpdateSyncedResource::class,
], ],
// Storage symlinks
Events\CreatingStorageSymlink::class => [],
Events\StorageSymlinkCreated::class => [],
Events\RemovingStorageSymlink::class => [],
Events\StorageSymlinkRemoved::class => [],
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops) // Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
Events\SyncedResourceChangedInForeignDatabase::class => [], Events\SyncedResourceChangedInForeignDatabase::class => [],
]; ];

View file

@ -119,6 +119,24 @@ return [
'public' => '%storage_path%/app/public/', 'public' => '%storage_path%/app/public/',
], ],
/*
* Tenant-aware Storage::disk()->url() can be enabled for specific local disks here
* by mapping the disk's name to a name with '%tenant_id%' (this will be used as the public name of the disk).
* Doing that will override the disk's default URL with a URL containing the current tenant's key.
*
* For example, Storage::disk('public')->url('') will return https://your-app.test/storage/ by default.
* After adding 'public' => 'public-%tenant_id%' to 'url_override',
* the returned URL will be https://your-app.test/public-1/ (%tenant_id% gets substitued by the current tenant's ID).
*
* Use `php artisan tenants:link` to create a symbolic link from the tenant's storage to its public directory.
*/
'url_override' => [
// Note that the local disk you add must exist in the tenancy.filesystem.root_override config
// todo@v4 Rename %tenant_id% to %tenant_key%
// todo@v4 Rename url_override to something that describes the config key better
'public' => 'public-%tenant_id%',
],
/** /**
* Should storage_path() be suffixed. * Should storage_path() be suffixed.
* *

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Actions;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\CreatingStorageSymlink;
use Stancl\Tenancy\Events\StorageSymlinkCreated;
class CreateStorageSymlinksAction
{
use DealsWithTenantSymlinks;
public static function handle(Tenant|Collection|LazyCollection $tenants, bool $relativeLink = false, bool $force = false): void
{
$tenants = $tenants instanceof Tenant ? collect([$tenants]) : $tenants;
/** @var Tenant $tenant */
foreach ($tenants as $tenant) {
foreach (static::possibleTenantSymlinks($tenant) as $publicPath => $storagePath) {
static::createLink($publicPath, $storagePath, $tenant, $relativeLink, $force);
}
}
}
protected static function createLink(string $publicPath, string $storagePath, Tenant $tenant, bool $relativeLink, bool $force): void
{
event(new CreatingStorageSymlink($tenant));
if (static::symlinkExists($publicPath)) {
// If $force isn't passed, don't overwrite the existing symlink
throw_if(! $force, new Exception("The [$publicPath] link already exists."));
app()->make('files')->delete($publicPath);
}
// Make sure the storage path exists before we create a symlink
if (! is_dir($storagePath)) {
mkdir($storagePath, 0777, true);
}
if ($relativeLink) {
app()->make('files')->relativeLink($storagePath, $publicPath);
} else {
app()->make('files')->link($storagePath, $publicPath);
}
event((new StorageSymlinkCreated($tenant)));
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Actions;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\RemovingStorageSymlink;
use Stancl\Tenancy\Events\StorageSymlinkRemoved;
class RemoveStorageSymlinksAction
{
use DealsWithTenantSymlinks;
public static function handle(Tenant|Collection|LazyCollection $tenants): void
{
$tenants = $tenants instanceof Tenant ? collect([$tenants]) : $tenants;
/** @var Tenant $tenant */
foreach ($tenants as $tenant) {
foreach (static::possibleTenantSymlinks($tenant) as $publicPath => $storagePath) {
static::removeLink($publicPath, $tenant);
}
}
}
protected static function removeLink(string $publicPath, Tenant $tenant): void
{
if (static::symlinkExists($publicPath)) {
event(new RemovingStorageSymlink($tenant));
app()->make('files')->delete($publicPath);
event(new StorageSymlinkRemoved($tenant));
}
}
}

View file

@ -57,9 +57,10 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
// todo@v4 \League\Flysystem\PathPrefixer is making this a lot more painful in flysystem v2 // todo@v4 \League\Flysystem\PathPrefixer is making this a lot more painful in flysystem v2
$diskConfig = $this->app['config']["filesystems.disks.{$disk}"];
$originalRoot = $diskConfig['root'] ?? null;
$originalRoot = $this->app['config']["filesystems.disks.{$disk}.root"]; $this->originalPaths['disks']['path'][$disk] = $originalRoot;
$this->originalPaths['disks'][$disk] = $originalRoot;
$finalPrefix = str_replace( $finalPrefix = str_replace(
['%storage_path%', '%tenant%'], ['%storage_path%', '%tenant%'],
@ -74,6 +75,19 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
} }
$this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix; $this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix;
// Storage Url
if ($diskConfig['driver'] === 'local') {
$this->originalPaths['disks']['url'][$disk] = $diskConfig['url'] ?? null;
if ($url = str_replace(
'%tenant_id%',
$tenant->getTenantKey(),
$this->app['config']["tenancy.filesystem.url_override.{$disk}"] ?? ''
)) {
$this->app['config']["filesystems.disks.{$disk}.url"] = url($url);
}
}
} }
} }
@ -88,8 +102,16 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
// Storage facade // Storage facade
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']); Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { foreach ($this->app['config']['tenancy.filesystem.disks'] as $diskName) {
$this->app['config']["filesystems.disks.{$disk}.root"] = $this->originalPaths['disks'][$disk]; $this->app['config']["filesystems.disks.$diskName.root"] = $this->originalPaths['disks']['path'][$diskName];
$diskConfig = $this->app['config']['filesystems.disks.' . $diskName];
// Storage Url
$url = $this->originalPaths['disks.url.' . $diskName] ?? null;
if ($diskConfig['driver'] === 'local' && ! is_null($url)) {
$$this->app['config']["filesystems.disks.$diskName.url"] = $url;
}
} }
} }
} }

73
src/Commands/Link.php Normal file
View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\LazyCollection;
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
use Stancl\Tenancy\Concerns\HasATenantsOption;
class Link extends Command
{
use HasATenantsOption;
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'tenants:link
{--tenants=* : The tenant(s) to run the command for. Default: all}
{--relative : Create the symbolic link using relative paths}
{--force : Recreate existing symbolic links}
{--remove : Remove symbolic links}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create or remove tenant symbolic links.';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$tenants = $this->getTenants();
try {
if ($this->option('remove')) {
$this->removeLinks($tenants);
} else {
$this->createLinks($tenants);
}
} catch (Exception $exception) {
$this->error($exception->getMessage());
}
}
protected function removeLinks(LazyCollection $tenants): void
{
RemoveStorageSymlinksAction::handle($tenants);
$this->info('The links have been removed.');
}
protected function createLinks(LazyCollection $tenants): void
{
CreateStorageSymlinksAction::handle(
$tenants,
$this->option('relative') ?? false,
$this->option('force') ?? false,
);
$this->info('The links have been created.');
}
}

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Concerns;
use Illuminate\Support\Collection;
use Stancl\Tenancy\Database\Models\Tenant;
trait DealsWithTenantSymlinks
{
/**
* Get all possible tenant symlinks, existing or not (array of ['public path' => 'storage path']).
*
* Tenants can have a symlink for each disk registered in the tenancy.filesystem.url_override config.
*
* This is used for creating all possible tenant symlinks and removing all existing tenant symlinks.
*/
protected static function possibleTenantSymlinks(Tenant $tenant): Collection
{
$diskUrls = config('tenancy.filesystem.url_override');
$disks = config('tenancy.filesystem.root_override');
$suffixBase = config('tenancy.filesystem.suffix_base');
$symlinks = collect();
$tenantKey = $tenant->getTenantKey();
foreach ($diskUrls as $disk => $publicPath) {
$storagePath = str_replace('%storage_path%', $suffixBase . $tenantKey, $disks[$disk]);
$publicPath = str_replace('%tenant_id%', $tenantKey, $publicPath);
tenancy()->central(function () use ($symlinks, $publicPath, $storagePath) {
$symlinks->push([public_path($publicPath) => storage_path($storagePath)]);
});
}
return $symlinks->mapWithKeys(fn ($item) => $item);
}
/** Determine if the provided path is an existing symlink. */
protected static function symlinkExists(string $link): bool
{
return file_exists($link) && is_link($link);
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class CreatingStorageSymlink extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class RemovingStorageSymlink extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class StorageSymlinkCreated extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Events;
class StorageSymlinkRemoved extends Contracts\TenantEvent
{
}

View file

@ -0,0 +1,40 @@
<?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 Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
use Stancl\Tenancy\Contracts\Tenant;
class CreateStorageSymlinks implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Tenant $tenant;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
CreateStorageSymlinksAction::handle($this->tenant);
}
}

View file

@ -0,0 +1,40 @@
<?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 Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
use Stancl\Tenancy\Contracts\Tenant;
class RemoveStorageSymlinks implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Tenant $tenant;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
RemoveStorageSymlinksAction::handle($this->tenant);
}
}

View file

@ -78,6 +78,7 @@ class TenancyServiceProvider extends ServiceProvider
{ {
$this->commands([ $this->commands([
Commands\Run::class, Commands\Run::class,
Commands\Link::class,
Commands\Seed::class, Commands\Seed::class,
Commands\Install::class, Commands\Install::class,
Commands\Migrate::class, Commands\Migrate::class,

69
tests/ActionTest.php Normal file
View file

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
});
// todo move these to be in the same file as the other tests from this PR (#909) rather than generic "action tests"
test('create storage symlinks action works', function() {
config([
'tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class,
],
'tenancy.filesystem.suffix_base' => 'tenant-',
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
]);
/** @var Tenant $tenant */
$tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
tenancy()->initialize($tenant);
$this->assertDirectoryDoesNotExist($publicPath = public_path("public-$tenantKey"));
CreateStorageSymlinksAction::handle($tenant);
$this->assertDirectoryExists($publicPath);
$this->assertEquals(storage_path("app/public/"), readlink($publicPath));
});
test('remove storage symlinks action works', function() {
config([
'tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class,
],
'tenancy.filesystem.suffix_base' => 'tenant-',
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
]);
/** @var Tenant $tenant */
$tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
tenancy()->initialize($tenant);
CreateStorageSymlinksAction::handle($tenant);
$this->assertDirectoryExists($publicPath = public_path("public-$tenantKey"));
RemoveStorageSymlinksAction::handle($tenant);
$this->assertDirectoryDoesNotExist($publicPath);
});

View file

@ -14,16 +14,19 @@ use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Events\TenantDeleted;
use Stancl\Tenancy\Events\DeletingTenant;
use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemAdapter;
use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Events\DeletingTenant;
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
beforeEach(function () { beforeEach(function () {
$this->mockConsoleOutput = false; $this->mockConsoleOutput = false;
@ -192,8 +195,121 @@ test('tenant storage can get deleted after the tenant when DeletingTenant listen
'tenancy.bootstrappers' => [ 'tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class, FilesystemTenancyBootstrapper::class,
], ],
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
]); ]);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
$tenant1StorageUrl = 'http://localhost/public-' . $tenant1->getKey().'/';
$tenant2StorageUrl = 'http://localhost/public-' . $tenant2->getKey().'/';
tenancy()->initialize($tenant1);
$this->assertEquals(
$tenant1StorageUrl,
Storage::disk('public')->url('')
);
Storage::disk('public')->put($tenant1FileName = 'tenant1.txt', 'text');
$this->assertEquals(
$tenant1StorageUrl . $tenant1FileName,
Storage::disk('public')->url($tenant1FileName)
);
tenancy()->initialize($tenant2);
$this->assertEquals(
$tenant2StorageUrl,
Storage::disk('public')->url('')
);
Storage::disk('public')->put($tenant2FileName = 'tenant2.txt', 'text');
$this->assertEquals(
$tenant2StorageUrl . $tenant2FileName,
Storage::disk('public')->url($tenant2FileName)
);
});
test('files can get fetched using the storage url', function() {
config([
'tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class,
],
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
]);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
pest()->artisan('tenants:link');
// First tenant
tenancy()->initialize($tenant1);
Storage::disk('public')->put($tenantFileName = 'tenant1.txt', $tenantKey = $tenant1->getTenantKey());
$url = Storage::disk('public')->url($tenantFileName);
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant_id%', $tenantKey);
$hostname = Str::of($url)->before($tenantDiskName);
$parsedUrl = Str::of($url)->after($hostname);
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
// Second tenant
tenancy()->initialize($tenant2);
Storage::disk('public')->put($tenantFileName = 'tenant2.txt', $tenantKey = $tenant2->getTenantKey());
$url = Storage::disk('public')->url($tenantFileName);
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant_id%', $tenantKey);
$hostname = Str::of($url)->before($tenantDiskName);
$parsedUrl = Str::of($url)->after($hostname);
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
});
test('create and delete storage symlinks jobs work', function() {
Event::listen(
TenantCreated::class,
JobPipeline::make([CreateStorageSymlinks::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener()
);
Event::listen(
TenantDeleted::class,
JobPipeline::make([RemoveStorageSymlinks::class])->send(function (TenantDeleted $event) {
return $event->tenant;
})->toListener()
);
config([
'tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class,
],
'tenancy.filesystem.suffix_base' => 'tenant-',
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
]);
/** @var Tenant $tenant */
$tenant = Tenant::create();
tenancy()->initialize($tenant);
$tenantKey = $tenant->getTenantKey();
$this->assertDirectoryExists(storage_path("app/public"));
$this->assertEquals(storage_path("app/public/"), readlink(public_path("public-$tenantKey")));
$tenant->delete();
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
});
test('local storage public urls are generated correctly', function() {
Event::listen(DeletingTenant::class, DeleteTenantStorage::class); Event::listen(DeletingTenant::class, DeleteTenantStorage::class);
tenancy()->initialize(Tenant::create()); tenancy()->initialize(Tenant::create());

View file

@ -29,6 +29,15 @@ beforeEach(function () {
DatabaseTenancyBootstrapper::class, DatabaseTenancyBootstrapper::class,
]]); ]]);
config([
'tenancy.bootstrappers' => [
DatabaseTenancyBootstrapper::class,
],
'tenancy.filesystem.suffix_base' => 'tenant-',
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class);
}); });
@ -196,6 +205,43 @@ test('run command with array of tenants works', function () {
->expectsOutput('Tenant: ' . $tenantId2); ->expectsOutput('Tenant: ' . $tenantId2);
}); });
test('link command works', function() {
$tenantId1 = Tenant::create()->getTenantKey();
$tenantId2 = Tenant::create()->getTenantKey();
pest()->artisan('tenants:link');
$this->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public"));
$this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1")));
$this->assertDirectoryExists(storage_path("tenant-$tenantId2/app/public"));
$this->assertEquals(storage_path("tenant-$tenantId2/app/public/"), readlink(public_path("public-$tenantId2")));
pest()->artisan('tenants:link', [
'--remove' => true,
]);
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId1"));
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId2"));
});
test('link command works with a specified tenant', function() {
$tenantKey = Tenant::create()->getTenantKey();
pest()->artisan('tenants:link', [
'--tenants' => [$tenantKey],
]);
$this->assertDirectoryExists(storage_path("tenant-$tenantKey/app/public"));
$this->assertEquals(storage_path("tenant-$tenantKey/app/public/"), readlink(public_path("public-$tenantKey")));
pest()->artisan('tenants:link', [
'--remove' => true,
'--tenants' => [$tenantKey],
]);
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
});
test('run command works when sub command asks questions and accepts arguments', function () { test('run command works when sub command asks questions and accepts arguments', function () {
$tenant = Tenant::create(); $tenant = Tenant::create();
$id = $tenant->getTenantKey(); $id = $tenant->getTenantKey();

View file

@ -61,7 +61,7 @@ test('secondary models are not scoped to the current tenant when accessed direct
expect(Comment::count())->toBe(2); expect(Comment::count())->toBe(2);
}); });
test('secondary models a r e scoped to the current tenant when accessed directly and parent relationship traitis used', function () { test('secondary models ARE scoped to the current tenant when accessed directly and parent relationship trait is used', function () {
$acme = Tenant::create([ $acme = Tenant::create([
'id' => 'acme', 'id' => 'acme',
]); ]);