sprout icon indicating copy to clipboard operation
sprout copied to clipboard

Support for third-party packages

Open ollieread opened this issue 1 year ago • 8 comments

Some third-party packages will require some additional work to support. While supporting every package is unfeasible, it's perhaps worth looking into some additional addons to at least support Laravel's first-party packages.

This is a non-exhaustive list of packages to look into supporting.

  • [x] Livewire
  • [x] Filament
  • [ ] Telescope
  • [ ] Nova
  • [ ] Cashier
  • [x] Fortify
  • [ ] Horizon
  • [ ] Inertia
  • [ ] Jetstream
  • [ ] Octane
  • [ ] Pulse
  • [ ] Sanctum
  • [ ] Scout
  • [ ] Spark
  • [ ] Ziggy (#102)
  • [ ] Reverb (#115)
  • [ ] Nightwatch

[!NOTE] Some of these packages won't require any work, or may require so much work that they cannot be supported.

Discussion: https://github.com/orgs/sprout-laravel/discussions/72

ollieread avatar Feb 02 '25 12:02 ollieread

Livewire

I've looked into this briefly and found the following:

  • The livewire.update route needs to be registered using a custom route, which is wrapped in the tenanted route group.
  • If the generated route requires parameters, they should have a default value set.
  • It's possible that using this will prevent you from having Livewire function in multiple tenancies

I need to find out:

  • [x] Exactly how the Livewire route is used - Added on a <script> tag
  • [x] Whether it's possible to set the route during runtime - It is
  • [x] If not, how difficult would it be to override the functionality that does? - Not needed

ollieread avatar Feb 02 '25 12:02 ollieread

Filament

I've also looked briefly into Filament and found the following:

  • It would require a solution for Livewire, as it's built on it
  • Filaments multitenancy functionality may work for a cross tenant panel
  • To create a portal per tenant, it is most likely that the Panel class needs to be extended to add Sprout-specific functionality

Things to do:

  • [ ] Read about Filament multitenancy more
  • [ ] Look into what would need to be added to have a Panel function

ollieread avatar Feb 02 '25 12:02 ollieread

Telescope

Looking into Laravel Telescope, I've discovered the following:

  • It's possible to tag requests with the 'tenancy' and 'tenant' for a simple less-intrusive solution
  • It may be possible to override Telescopes request watcher to have specific sections for tenants

I need to:

  • [ ] Look further into customising the Telescope request watcher
  • [ ] Look further into creating a custom watcher/panel/section for Sprout, and whether it's even required

ollieread avatar Feb 02 '25 12:02 ollieread

Livewire Cont...

Update Route

Since it is possible to dynamically set the Livewire update route, a route needs to be created during the tenancy setup phase, which adds a route parameter. By default, this should be the path resolver.

An example implementation of a Livewire service override is as follows.

namespace App;

use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Route;
use Livewire\Mechanisms\HandleRequests\HandleRequests;
use Sprout\Contracts\IdentityResolver;
use Sprout\Contracts\Tenancy;
use Sprout\Contracts\Tenant;
use Sprout\Overrides\BaseOverride;

class LivewireOverride extends BaseOverride
{
    public function setup(Tenancy $tenancy, Tenant $tenant): void
    {
        $resolver = $this->getResolverForLivewire();

        // We need to call setup so that the URL generator can be updated
        // with default parameters for the tenant.
        $resolver->setup($tenancy, $tenant);

        $this->getApp()
             ->make(HandleRequests::class)
             ->setUpdateRoute(function (array $handler) use ($tenancy, $resolver) {
                 $route = null;

                 // We won't use the helper here, and we'll use the resolver
                 // directly to ensure that the route is created with the
                 // correct URL parameters.
                 $resolver->routes(
                     $this->getApp()->make(Router::class),
                     static function () use ($handler, &$route) {
                         $route = Route::name('sprout.livewire.update')
                                       ->post('/livewire/update/', $handler);
                     },
                     $tenancy
                 );

                 return $route;
             });
    }

    protected function getResolverForLivewire(): IdentityResolver
    {
        return $this->getSprout()
                    ->resolvers()
                    ->get($this->config['resolver'] ?? 'path');
    }
}

File Upload

The disk to use for upload needs to be set via the config option livewire.temporary_file_upload.disk. Unfortunately, the upload controller does something different depending on whether it's an S3 disk, GCS disk or other disk. The logic for determining this is less than ideal for Sprouts filesystem override.

The class that sets all this up is Livewire\Features\SupportFileUploads\SupportFileUploads, which is a component hook, which is entirely static so can't be overridden via DI.

It looks like the best solution is to redefine the already existing routes when defining the update route for the path resolver.

// Create the upload and preview routes
Route::name('livewire.upload-file')
     ->post('/livewire/upload-file', [FileUploadController::class, 'handle']);

Route::name('livewire.preview-file')
     ->get('/livewire/preview-file/{filename}', [FilePreviewController::class, 'handle']);

As for the filesystem handling, it looks like the only solution to that is a hacky workaround using composers autoload classmap, to map to a fake Sprout controlled version of FileUploadConfiguration.

ollieread avatar Feb 08 '25 21:02 ollieread

Filament Cont...

To override filament, there are two options.

  1. Add filament/filament to dont-discover in composer.json and register a service provider which overrides the default filament one, providing a custom set of routes.
public function configurePackage(Package $package): void
{
    $package
        ->name('filament-panels')
        ->hasCommands($this->getCommands())
        ->hasRoutes('web')
        ->hasTranslations()
        ->hasViews();
}
  1. Extend the Panel class and others that are used to generate routes, to be tenant-aware.

ollieread avatar Feb 10 '25 14:02 ollieread

Fortify

To make Fortify multitenanted, the automatic routing needs to be disabled by setting Fortify::$registersRoutes to false, and then manually registering the routes in a tenanted route group like so:

Route::tenanted(function () {
    Route::group([
        'namespace' => 'Laravel\Fortify\Http\Controllers',
        'domain' => config('fortify.domain', null),
        'prefix' => config('fortify.prefix'),
    ], function () {
        $this->loadRoutesFrom(base_path('vendor/laravel/fortify/routes/routes.php'));
    });
});

ollieread avatar Feb 11 '25 22:02 ollieread

Fortify Cont...

Fortify also uses a few config values to know the routes that it links and redirects to.

See: https://github.com/laravel/fortify/blob/f1ad2dd08646fb7d78e609f38e2b1071c71c4e94/config/fortify.php#L20-L60

ollieread avatar Feb 26 '25 18:02 ollieread

Horizon

Need to use wildcard queues for name patterns, so each tenant can have their own queue.

ollieread avatar Jul 08 '25 17:07 ollieread