Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.x] This adds support for tenancy aware Storage::url() method #689

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions assets/TenancyServiceProvider.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function events()
Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class,
// Jobs\SeedDatabase::class,
Jobs\CreateStorageSymlinks::class,

// Your own jobs to prepare the tenant.
// Provision API keys, create S3 buckets, anything you want!
Expand All @@ -44,6 +45,7 @@ public function events()
Events\TenantDeleted::class => [
JobPipeline::make([
Jobs\DeleteDatabase::class,
Jobs\RemoveStorageSymlinks::class,
])->send(function (Events\TenantDeleted $event) {
return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
Expand Down Expand Up @@ -87,6 +89,12 @@ public function events()
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)
Events\SyncedResourceChangedInForeignDatabase::class => [],
];
Expand Down
10 changes: 10 additions & 0 deletions assets/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
'public' => '%storage_path%/app/public/',
],

/*
* Use this to support Storage url method on local driver disks.
* You should create a symbolic link which points to the public directory using command: artisan tenants:link
* Then you can use tenant aware Storage url: Storage::disk('public')->url('file.jpg')
*/
'url_override' => [
// The array key is local disk (must exist in root_override) and value is public directory (%tenant_id% will be replaced with actual tenant id).
'public' => 'public-%tenant_id%',
],

/**
* Should storage_path() be suffixed.
*
Expand Down
45 changes: 38 additions & 7 deletions src/Bootstrappers/FilesystemTenancyBootstrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

namespace Stancl\Tenancy\Bootstrappers;

use Illuminate\Contracts\Foundation\Application;
use Stancl\Tenancy\Contracts\Tenant;
use Illuminate\Support\Facades\Storage;
use Illuminate\Contracts\Foundation\Application;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;

class FilesystemTenancyBootstrapper implements TenancyBootstrapper
{
Expand Down Expand Up @@ -56,10 +56,10 @@ public function bootstrap(Tenant $tenant)
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);

foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
// todo@v4 \League\Flysystem\PathPrefixer is making this a lot more painful in flysystem v2
/** @var string|null $originalRoot */
$originalRoot = $this->getDiskConfig($disk)['root'] ?? null;

$originalRoot = $this->app['config']["filesystems.disks.{$disk}.root"];
$this->originalPaths['disks'][$disk] = $originalRoot;
$this->originalPaths['disks']['path'][$disk] = $originalRoot;

$finalPrefix = str_replace(
['%storage_path%', '%tenant%'],
Expand All @@ -73,7 +73,20 @@ public function bootstrap(Tenant $tenant)
: $suffix;
}

$this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix;
$this->updateDiskConfig($disk, 'root', $finalPrefix);

// Storage Url
if ($this->getDiskConfig($disk)['driver'] === 'local') {
$this->originalPaths['disks']['url'][$disk] = $this->getDiskConfig($disk)['url'] ?? null;

if ($url = str_replace(
'%tenant_id%',
$tenant->getTenantKey(),
$this->app['config']["tenancy.filesystem.url_override.{$disk}"] ?? ''
)) {
$this->updateDiskConfig($disk, 'url', url($url));
}
}
}
}

Expand All @@ -89,7 +102,25 @@ public function revert()
// Storage facade
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
$this->app['config']["filesystems.disks.{$disk}.root"] = $this->originalPaths['disks'][$disk];
$this->updateDiskConfig($disk, 'root', $this->originalPaths['disks']['path'][$disk]);

// Storage Url
$url = $this->originalPaths['disks.url.' . $disk] ?? null;

if ($this->getDiskConfig($disk)['driver'] === 'local' && ! is_null($url)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getDiskConfig() method doesn't make much sense. If you're returning null, but using ['driver'] on the return value, that will return in exceptions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, there should be better error handling (would the exception be different from the ones that pieces of the original code would throw?). But if there's no disk with the passed name, the method should return null, that makes sense to me.

$this->updateDiskConfig($disk, 'url', $url);
}
}
}

/** @return array|null */
protected function getDiskConfig(string $diskName)
{
return $this->app['config']['filesystems.disks.' . $diskName] ?? null;
}

protected function updateDiskConfig(string $diskName, string $updateProperty, $value)
{
$this->app['config']["filesystems.disks.$diskName.$updateProperty"] = $value;
}
}
131 changes: 131 additions & 0 deletions src/Commands/Link.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace Stancl\Tenancy\Commands;

use Illuminate\Console\Command;
use Stancl\Tenancy\Concerns\HasATenantsOption;
use Stancl\Tenancy\Contracts\Tenant;

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 the symbolic links configured for the tenancy applications';

/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$relative = $this->option('relative');

if ($this->option('remove')) {
foreach ($this->links() as $link => $target) {
if (is_link($link)) {
$this->laravel->make('files')->delete($link);

$this->info("The [$link] link has been removed.");
}
}

$this->info('The links have been removed.');

return;
}

foreach ($this->links() as $link => $target) {
if (file_exists($link) && ! $this->isRemovableSymlink($link, $this->option('force'))) {
$this->error("The [$link] link already exists.");
continue;
}

if (is_link($link)) {
$this->laravel->make('files')->delete($link);
}

if ($relative) {
$this->laravel->make('files')->relativeLink($target, $link);
} else {
$this->laravel->make('files')->link($target, $link);
}

$this->info("The [$link] link has been connected to [$target].");
}

$this->info('The links have been created.');
}

/**
* Get the symbolic links that are configured for the application.
*
* @return array
*/
protected function links()
{
$disk_urls = config('tenancy.filesystem.url_override');
$disks = config('tenancy.filesystem.root_override');
$suffix_base = config('tenancy.filesystem.suffix_base');

$tenants = $this->option('remove') && filled($this->option('tenants'))
? collect($this->option('tenants'))
: $this->getTenants()->map(function(Tenant $tenant) { return $tenant->getTenantKey(); });

return $tenants
->map(function ($tenant_key) use ($suffix_base, $disk_urls, $disks) {

$map = [];

foreach ($disk_urls as $disk => $public_path) {
$storage_path = str_replace('%storage_path%', $suffix_base . $tenant_key, $disks[$disk]);
$storage_path = storage_path($storage_path);

$public_path = str_replace('%tenant_id%', $tenant_key, $public_path);
$public_path = public_path($public_path);

// make sure storage path exist before we create symlink
if (! is_dir($storage_path)) {
mkdir($storage_path, 0777, true);
}

$map[] = [$public_path => $storage_path];
}

return $map;

})->flatten(1)
->mapWithKeys(function ($item) {return $item; })
->all();
}

/**
* Determine if the provided path is a symlink that can be removed.
*
* @param string $link
* @param bool $force
* @return bool
*/
protected function isRemovableSymlink(string $link, bool $force): bool
{
return is_link($link) && $force;
}
}
9 changes: 9 additions & 0 deletions src/Events/CreatingStorageSymlink.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Stancl\Tenancy\Events;

class CreatingStorageSymlink extends Contracts\TenantEvent
{
}
9 changes: 9 additions & 0 deletions src/Events/RemovingStorageSymlink.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Stancl\Tenancy\Events;

class RemovingStorageSymlink extends Contracts\TenantEvent
{
}
9 changes: 9 additions & 0 deletions src/Events/StorageSymlinkCreated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Stancl\Tenancy\Events;

class StorageSymlinkCreated extends Contracts\TenantEvent
{
}
9 changes: 9 additions & 0 deletions src/Events/StorageSymlinkRemoved.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Stancl\Tenancy\Events;

class StorageSymlinkRemoved extends Contracts\TenantEvent
{
}
52 changes: 52 additions & 0 deletions src/Jobs/CreateStorageSymlinks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Stancl\Tenancy\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
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\Contracts\Tenant;
use Stancl\Tenancy\Events\CreatingStorageSymlink;
use Stancl\Tenancy\Events\StorageSymlinkCreated;

class CreateStorageSymlinks implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* @var \Stancl\Tenancy\Contracts\Tenant
*/
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()
{
event(new CreatingStorageSymlink($this->tenant));

Artisan::call('tenants:link', [
'--tenants' => [$this->tenant->getTenantKey()],
]);

event(new StorageSymlinkCreated($this->tenant));
}
}
Loading