Search code examples
laravellaravel-livewire

Livewire 3 Package Development


In the Livewire v2 there was a doc for using Livewire within a package. I could not find any related doc for v3. I tried the approach mentioned in the v2 doc but it does not work for v3.

I need to register livewire components from multiple custom paths/ namespaces. I want to register those manually.


Solution

  • TLDR below.

    Yea my comment was kinda lame. So no, I have never done this and have no experience what you can do about it. But there are so many packages using Livewire and registering components, so let's get to the bottom of this.

    Let me take laravel/jetstream as an example, using livewire 3.0 since its release from beta.

    In the JetstreamServiceProvider they are registering the livewire components quite differently, different from your example above. Using register() with the callAfterResolving callback instead of boot().

    public function register()
    {
        $this->mergeConfigFrom(__DIR__.'/../config/jetstream.php', 'jetstream');
    
        $this->callAfterResolving(BladeCompiler::class, function () {
            if (config('jetstream.stack') === 'livewire' && class_exists(Livewire::class)) {
                Livewire::component('navigation-menu', NavigationMenu::class);
                // .. other Livewire components
            }
        });
    }
    

    Now if we go down the road and look how Livewire registers its components, it's quite obvious why they made this choice. It comes down to these 3 lines.

    app()->make('view.engine.resolver')->register('blade', function () {
      return new ExtendedCompilerEngine(app('blade.compiler'));
    });
    

    So to put it plainly, it would only make sense to make any component available in a package using the register method of the service provider, making sure the BladeCompiler is resolved. Why the Livewire v2 documentation says something completely else (and Livewire v3 documentation not even mentioning it) is bogus to me, maybe they wrote it with the idea of some standards never implemented in Livewire, because Livewire has changed so much internally, I'll give all the blame to just a blip in documentation, which sadly happens.

    This is how registering Livewire components in a service provider should look like.

    namespace SomePackage;
    
    use SomePackage\Livewire\SomeComponent;
    use Illuminate\View\Compilers\BladeCompiler;
    use Illuminate\Support\ServiceProvider;
    use Livewire\Livewire;
    
    class SomePackageServiceProvider extends ServiceProvider {
    
        public function register() {
            $this->callAfterResolving(BladeCompiler::class, function () {
                // 1. 
                // Check the existence of the Livewire class ..
                if(class_exists(Livewire::class)) {
                    Livewire::component('some-component', SomeComponent::class);
                }
    
                // 2.
                // where 1. might be redundant because Livewire obviously set as a dependency in your composer.json, just skip Livewire existence check.
                Livewire::component('some-component', SomeComponent::class);
    
            });
        }
    
    }
    

    With or without the if(class_exists(Livewire::class)), the use of use Livewire\Livewire would already fatally crash if that particular class doesn't exist. So checking the existence of class_exists(Livewire::class) is likely to be stupid anyway.

    TLDR;

    So this would be my final clean take on how to register Livewire components in a service provider:

    namespace SomePackage;
    
    use SomePackage\Livewire\SomeComponent;
    use Illuminate\View\Compilers\BladeCompiler;
    use Illuminate\Support\ServiceProvider;
    use Livewire\Livewire;
    
    class SomePackageServiceProvider extends ServiceProvider {
    
        public function register() {
            // .. merge config, etc.
            $this->registerLivewireComponents();
        }
    
        /**
         * Register Livewire components
         */
        protected function registerLivewireComponents() {
             $this->callAfterResolving(BladeCompiler::class, function () 
                Livewire::component('some-component', SomeComponent::class);
             });
    
        }
    
    }
    

    Does this work? I have no idea, but I guess you'll tell me soon enough.

    UPDATE v1 spatie/laravel-dashboard

    Though looking at the package of spatie/laravel-dashboard, they are registering the components in the boot method of its service provider.

    class DashboardServiceProvider extends ServiceProvider
    {
        public function boot()
        {
            // ... code skip
       
            $this
                ->registerPublishables()
                ->registerBladeComponents()
                ->registerLivewireComponents();        
        }  
    
        
        protected function registerLivewireComponents(): self
        {
            Livewire::component('dashboard-update-mode', UpdateModeComponent::class);
    
            return $this;
        }
    }
    

    Acting on the knowledge that the register method of a service provider is solely used to register (inject) services to a container (bootstrap) and the boot method to be used after all services have been bootstrapped (registered or injected), the register method would be the logical place to register any Livewire components, so there must be a reason why Spatie choose a different path. It's very interesting indeed ;)

    So to come to terms, both methods explained in the answer should work.

    UPDATE v2 test

    So I did some testing and could not find any problems running Livewire components in my test package.

    This simple package exists out of a route called test, a view called counter and a Livewire component class called Counter.

    The service provider set the view namespace and directory to be included and registers the component.

    namespace TheCore\Test;
    
    // .. skipping all the includes
    
    class TestServiceProvider extends ServiceProvider {
    
        public function boot() {
          // namespace called `test`!
          $this->loadViewsFrom(__DIR__.'/../resources/views', 'test');
        }
    
        public function register() {
           Livewire::component('counter', Counter::class);
        }
    
    }
    

    The route

    Route::get('/test', function() {
        return Test::testView();
    })->name('thecore-test')->middleware(['web']);
    

    Where Test::testView() returns

    public function testView() {
        // hence the namespace scope is set to 'test', 
        // else it will land in the app `views` directory 
        // and not in this package `views` directory
        return view('test::testing');
    }
    

    The counter example from Livewires quickstart page, where render() is only relevant:

    public function render() {
        // hence the scoping of 'test' again. 
        return view('test::livewire.counter');
    }
    

    and the Livewire component being called in a blade

    <livewire:counter />
    

    Works as expected. I used all types of registration, in boot(), in register(), with and without the callAfterResolving callback, didn't change much.

    So much for this satellite being found in space ;)