Overriding Service Provider Methods (Fortify)

For the past few years, I've managed to override the boot method in the Fortify service provider located at App\Providers\FortifyServiceProvider so that I can define the Fortify Views from my custom service provider that's loaded dynamically from a package.

This all worked perfectly until Laravel 11.9.0, where there was an update to how the service providers were loaded, I'm guessing that's where the issue may lie.

https://github.com/laravel/framework/blob/11.x/CHANGELOG.md

Do you have any ideas on how this might now be achieved without touching the main service provider in the App\Providers folder so that the custom logic remains inside my custom package?

derekbuntin Member
derekbuntin
0
12
1086
alex Member
alex
Moderator

Is it this change directly that's affecting this (just so I'm clear)?

https://github.com/laravel/framework/pull/51343

derekbuntin Member
derekbuntin

I am not 100% sure but it seems like it might be.

I used the AliasLoader to alias the App\Providers\FortifyServiceProvider, which when called used my own one within my own package which is loaded dynamically.

It's a little strange as I do the same with Jetstream which does seem to work so I'm unsure what has changed.

I set the login view (Fortify::loginView) and other views and features in the boot method.

alex Member
alex
Moderator

Really strange! I’ll do some experimenting and get back to you if I find something.

Do you have a snippet of code for how you’re currently doing this inside your package?

derekbuntin Member
derekbuntin

Yes here is the service provider:

<?php

namespace Adonis\Cms\Providers;

use Adonis\Cms\Facades\Setting;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        AliasLoader::getInstance()->alias('App\Providers\FortifyServiceProvider', 'Adonis\Cms\Providers\FortifyServiceProvider');
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        if (Schema::hasTable('settings')) {
            $this->overrideFortifyFeatures();
            $this->overrideFortifyViews();
            $this->overrideFortifyActions();
        }

        Fortify::ignoreRoutes();
    }

    /**
     * Override Fortify features based on settings.
     */
    public function overrideFortifyFeatures(): void
    {
        $features = Config::get('fortify.features');

        foreach ($features as $key => $feature) {
            if ($feature === 'registration' && Setting::get('features_registration_enabled', 0) === false) {
                Arr::forget($features, $key);
            } elseif ($feature === 'registration' && Setting::get('features_registration_enabled', 0) === true && Setting::get('features_registration_method', 'none') != 'fortify') {
                Arr::forget($features, $key);
            }
        }
        Config::set('fortify.features', $features);
    }

    /**
     * Override Fortify views based on settings.
     */
    public function overrideFortifyViews(): void
    {
        Fortify::loginView(function () {
            return view('cms::public.auth.'.Setting::get('layout_login', 'login-default'));
        });

        Fortify::registerView(function () {
            return view('cms::public.auth.register');
        });

        Fortify::resetPasswordView(function () {
            return view('cms::public.auth.forgot-password');
        });

        Fortify::twoFactorChallengeView(function () {
            return view('cms::public.auth.two-factor-challenge');
        });

        Fortify::resetPasswordView(function () {
            return view('cms::public.auth.reset-password');
        });

        Fortify::requestPasswordResetLinkView(function () {
            return view('cms::public.auth.forgot-password');
        });

        Fortify::verifyEmailView(function () {
            return view('cms::public.auth.verify-email');
        });

        Fortify::confirmPasswordView(function () {
            return view('cms::public.auth.confirm-password');
        });
    }

    /**
     * Override Fortify actions.
     */
    public function overrideFortifyActions(): void
    {
        Fortify::createUsersUsing(\Adonis\Cms\Actions\Fortify\CreateNewUser::class);
        Fortify::updateUserProfileInformationUsing(\Adonis\Cms\Actions\Fortify\UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(\Adonis\Cms\Actions\Fortify\UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(\Adonis\Cms\Actions\Fortify\ResetUserPassword::class);
    }
}
alex Member
alex
Moderator

I've been playing around with this and understand the issue better now. One thing, can you show me the code where you're registering the service provider in your package?

derekbuntin Member
derekbuntin

Yeah sure mate, I have the main package service provider with this method, which is registered in the register method:

private function RegisterProviderClasses(): void
    {
        $providers = FileHelper::getClassNames(base_path('packages/Adonis/Cms/src/Providers'), 'Adonis\\Cms\\Providers');

        foreach ($providers as $providerClass) {
            if (class_exists($providerClass)) {
                $this->app->register($providerClass);
            } else {
                echo "Class {$providerClass} does not exist.";
            }
        }
    }

this is what's in the boot method:

$this->registerProviderClasses();

The fileHelper loads all the class names then register them dynamically:

public static function getClassNames(string $folder, string $namespaceBase): array
    {
        // Get all the files in the directory
        $files = File::files($folder);
        $classNames = [];

        foreach ($files as $file) {
            // Get filename without extension
            $filename = $file->getBasename('.php');

            // Append the namespace base
            $fileNamespace = $namespaceBase.'\\'.$filename;

            $classNames[] = $fileNamespace;
        }

        return $classNames;
    }

I hope this makes sense ;-)

alex Member
alex
Moderator

Makes perfect sense and super helpful. Working on this right now for you :)

alex Member
alex
Moderator

Playing around with this, I've noticed that using

AliasLoader::getInstance()->alias('App\Providers\LocalServiceProvider', 'App\Providers\PackageServiceProvider');

much earlier on in the lifecycle of the app works (e.g. if it's put in bootstrap/app.php). Have you tried adding the alias to your package service provider boot method?

I don't have a local package up and running to test this, so if you've not already tried, give that a go and we'll take it from there.

derekbuntin Member
derekbuntin

Yeah I'm sure I did try that, I'll give it a go again tomorrow and see how it goes, I'll keep you posted Alex, thanks mate.

alex Member
alex
Moderator

No worries, I'll wait for you to try and then continue experimenting if not.

derekbuntin Member
derekbuntin
Solution

Ok, so after getting back into this, I have resolved the issue.

I created a new ServiceProviderHelper.php and added the following code:

<?php

declare(strict_types=1);

namespace Adonis\Cms\Helpers;

use Illuminate\Container\Container;

class ServiceProviderHelper
{
    /**
     * Check if a given service provider is registered.
     *
     * @param string $providerClass
     * @return bool
     */
    public static function isRegistered(string $providerClass): bool
    {
        if (!class_exists($providerClass)) {
            return false;
        }

        $app = app();
        $providers = $app->getLoadedProviders();

        return isset($providers[$providerClass]);
    }

    public static function registerProviderClasses(Container $app): void
    {
        $allProviders = FileHelper::getClassNames(base_path('packages/Adonis/Cms/src/Providers'), 'Adonis\\Cms\\Providers');
        $fortifyProvider = '';
        $jetstreamProvider = '';

        foreach ($allProviders as $providerClass) {
            if (class_exists($providerClass)) {
                if (str_contains($providerClass, 'FortifyServiceProvider')) {
                    $fortifyProvider = $providerClass;
                    continue;
                }

                if (str_contains($providerClass, 'JetstreamServiceProvider')) {
                    $jetstreamProvider = $providerClass;
                    continue;
                }

                $app->register($providerClass);
            } else {
                echo "Class {$providerClass} does not exist.\n";
            }
        }

        $app->booted(function () use ($app, $fortifyProvider, $jetstreamProvider) {
            if ($fortifyProvider) {
                $app->register($fortifyProvider);
            }

            if ($jetstreamProvider) {
                $app->register($jetstreamProvider);
            }
        });
    }

    public static function overrideProviders(Container $app): void
    {
        $providers = [
            \App\Providers\FortifyServiceProvider::class,
            \App\Providers\JetstreamServiceProvider::class,
        ];

        foreach ($providers as $default) {
            self::unregisterServiceProvider($app, $default);
        }
    }

    public static function unregisterServiceProvider(Container $app, string $provider): void
    {
        if (isset($app->getLoadedProviders()[$provider])) {
            unset($app->getLoadedProviders()[$provider]);
            $app->forgetInstance($provider);
        }
    }
}

Then in my main package service provider, I referenced the methods in the helper within the 'register' method like so:

ServiceProviderHelper::registerProviderClasses($this->app);
ServiceProviderHelper::overrideProviders($this->app);

did a composer dump-autoload along with an artisan optimize:clear and it loaded perfectly.

The methods in the helper file check the loaded providers for the jetstream and fortify service providers, and if they exist, then unregister them from the service container.

I then register our custom providers, which provide the required overrides.

Previously I hadn't unregistered the providers in the app/Providers folder, and sometimes you'd get a quick glimpse of the original before refreshing to see the overrides, this no longer happens and seems to work well.

Persistence always pays off :-)

I hope this might help someone else needing the same functionality.

alex Member
alex
Moderator

Ah brilliant — I'm so glad you've sorted this.

Really appreciate you posting the solution here for anyone that needs it in the future!