diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8bcabcc --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 https://github.com/fabioassuncao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2625209 --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# Laravel Modules With Livewire + +Using [Laravel Livewire](https://github.com/livewire/livewire) in [Laravel Modules](https://github.com/nWidart/laravel-modules) package with automatically registered livewire components for every modules. + + + +### Installation: + +Install through composer: + +``` +composer require codions/laravel-modules-livewire +``` + +Publish the package's configuration file: + +``` +php artisan vendor:publish --tag=modules-livewire-config +``` + +### Making Components: + +**Command Signature:** + +`php artisan module:make-livewire --view= --force --inline --stub= --custom` + +**Example:** + +``` +php artisan module:make-livewire Pages/AboutPage Core +``` + +``` +php artisan module:make-livewire Pages\\AboutPage Core +``` + +``` +php artisan module:make-livewire pages.about-page Core +``` + +**Force create component if the class already exists:** + +``` +php artisan module:make-livewire Pages/AboutPage Core --force +``` + +**Output:** + +``` +COMPONENT CREATED + +CLASS: Modules/Core/Http/Livewire/Pages/AboutPage.php +VIEW: Modules/Core/Resources/views/livewire/pages/about-page.blade.php +TAG: +``` + +**Inline Component:** + +``` +php artisan module:make-livewire Core Pages/AboutPage --inline +``` + +**Output:** + +``` +COMPONENT CREATED + +CLASS: Modules/Core/Http/Livewire/Pages/AboutPage.php +TAG: +``` + +**Modifying Stubs:** + +Publish the package's stubs: + +``` +php artisan vendor:publish --tag=modules-livewire-stub +``` + +After publishing the stubs, will create these files. And when running the make command, will use these stub files by default. + +``` +stubs/modules-livewire/livewire.inline.stub +stubs/modules-livewire/livewire.stub +stubs/modules-livewire/livewire.view.stub +``` + +**You're able to set a custom stub directory for component with (--stub) option.** + +``` +php artisan module:make-livewire Core Pages/AboutPage --stub=about +``` + +``` +php artisan module:make-livewire Core Pages/AboutPage --stub=modules-livewire/core +``` + +``` +php artisan module:make-livewire Core Pages/AboutPage --stub=./ +``` + +**Extra Option (--view):** + +**You're able to set a custom view path for component with (--view) option.** + +**Example:** + +``` +php artisan module:make-livewire Pages/AboutPage Core --view=pages/about +``` + +``` +php artisan module:make-livewire Pages/AboutPage Core --view=pages.about +``` + +**Output:** + +``` +COMPONENT CREATED + +CLASS: Modules/Core/Http/Livewire/Pages/AboutPage.php +VIEW: Modules/Core/Resources/views/livewire/pages/about.blade.php +TAG: +``` +### Rendering Components: + +`` + +**Example:** + +``` + +``` +### Custom Module: + +**To create components for the custom module, should be add custom modules in the config file.** + +The config file is located at `config/modules-livewire.php` after publishing the config file. + +Remove comment for these lines & add your custom modules. + +``` + /* + |-------------------------------------------------------------------------- + | Custom modules setup + |-------------------------------------------------------------------------- + | + */ + + // 'custom_modules' => [ + // 'Chat' => [ + // 'path' => base_path('libraries/Chat'), + // 'module_namespace' => 'Libraries\\Chat', + // // 'namespace' => 'Http\\Livewire', + // // 'view' => 'Resources/views/livewire', + // // 'name_lower' => 'chat', + // ], + // ], +``` + +**Custom module config details** + +> **path:** Add module full path (required). +> +> **module_namespace:** Add module namespace (required). +> +> **namespace:** By default using `config('modules-livewire.namespace')` value. You can set a different value for the specific module. +> +> **view:** By default using `config('modules-livewire.view')` value. You can set a different value for the specific module. +> +> **name_lower:** By default using module name to lowercase. If you set a custom name, module components will be register by custom name. +> + + +## Credits +- This project is a modified version of [mhmiton/laravel-modules-livewire](https://github.com/mhmiton/laravel-modules-livewire), created as a fork with additional changes. + +## License +Laravel Themes Manager is open-sourced software licensed under the [MIT license](LICENSE). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ab3f414 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "codions/laravel-modules-livewire", + "description": "Using Laravel Livewire in Laravel Modules package with automatically registered livewire components for every modules.", + "keywords": [ + "laravel", + "modules", + "module", + "livewire", + "nwidart", + "mhmiton" + ], + "license": "MIT", + "authors": [ + { + "name": "Fábio Assunção", + "email": "fabio23gt@gmail.com" + } + ], + "type": "library", + "require": { + "php": ">=7.3" + }, + "require-dev": { + "laravel/framework": "^7.0|^8.0|^9.0|^10.0" + }, + "extra": { + "laravel": { + "providers": [ + "Codions\\LaravelModulesLivewire\\LaravelModulesLivewireServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Codions\\LaravelModulesLivewire\\": "src/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/modules-livewire.php b/config/modules-livewire.php new file mode 100644 index 0000000..0994bbc --- /dev/null +++ b/config/modules-livewire.php @@ -0,0 +1,40 @@ + 'Http\\Livewire', + + /* + |-------------------------------------------------------------------------- + | View Path + |-------------------------------------------------------------------------- + | + */ + + 'view' => 'Resources/views/livewire', + + /* + |-------------------------------------------------------------------------- + | Custom modules setup + |-------------------------------------------------------------------------- + | + */ + + // 'custom_modules' => [ + // 'Chat' => [ + // 'path' => base_path('libraries/Chat'), + // 'module_namespace' => 'Libraries\\Chat', + // // 'namespace' => 'Http\\Livewire', + // // 'view' => 'Resources/views/livewire', + // // 'name_lower' => 'chat', + // ], + // ], + +]; diff --git a/src/Commands/LivewireMakeCommand.php b/src/Commands/LivewireMakeCommand.php new file mode 100644 index 0000000..ee852d0 --- /dev/null +++ b/src/Commands/LivewireMakeCommand.php @@ -0,0 +1,96 @@ +parser()) { + return false; + } + + if (! $this->checkClassNameValid()) { + return false; + } + + if (! $this->checkReservedClassName()) { + return false; + } + + $class = $this->createClass(); + + $view = $this->createView(); + + if ($class || $view) { + $this->line(" COMPONENT CREATED 🤙\n"); + + $class && $this->line("CLASS: {$this->getClassSourcePath()}"); + + $view && $this->line("VIEW: {$this->getViewSourcePath()}"); + + $class && $this->line("TAG: {$class->tag}"); + } + + return false; + } + + protected function createClass() + { + $classFile = $this->component->class->file; + + if (File::exists($classFile) && ! $this->isForce()) { + $this->line(" WHOOPS-IE-TOOTLES 😳 \n"); + $this->line("Class already exists: {$this->getClassSourcePath()}"); + + return false; + } + + $this->ensureDirectoryExists($classFile); + + File::put($classFile, $this->getClassContents()); + + return $this->component->class; + } + + protected function createView() + { + if ($this->isInline()) { + return false; + } + + $viewFile = $this->component->view->file; + + if (File::exists($viewFile) && ! $this->isForce()) { + $this->line("View already exists: {$this->getViewSourcePath()}"); + + return false; + } + + $this->ensureDirectoryExists($viewFile); + + File::put($viewFile, $this->getViewContents()); + + return $this->component->view; + } +} diff --git a/src/Commands/stubs/livewire.inline.stub b/src/Commands/stubs/livewire.inline.stub new file mode 100644 index 0000000..3f0260a --- /dev/null +++ b/src/Commands/stubs/livewire.inline.stub @@ -0,0 +1,17 @@ + +

[quote]

+ + blade; + } +} diff --git a/src/Commands/stubs/livewire.stub b/src/Commands/stubs/livewire.stub new file mode 100644 index 0000000..7539885 --- /dev/null +++ b/src/Commands/stubs/livewire.stub @@ -0,0 +1,13 @@ + +

[quote]

+ diff --git a/src/LaravelModulesLivewireServiceProvider.php b/src/LaravelModulesLivewireServiceProvider.php new file mode 100644 index 0000000..4b58d47 --- /dev/null +++ b/src/LaravelModulesLivewireServiceProvider.php @@ -0,0 +1,66 @@ +registerProviders(); + + $this->registerCommands(); + + $this->registerPublishables(); + + $this->mergeConfigFrom( + __DIR__ . '/../config/modules-livewire.php', + 'modules-livewire' + ); + } + + protected function registerProviders() + { + $this->app->register(LivewireComponentServiceProvider::class); + } + + protected function registerCommands() + { + if (! $this->app->runningInConsole()) { + return; + } + + $this->commands([ + LivewireMakeCommand::class, + ]); + } + + protected function registerPublishables() + { + $this->publishes([ + __DIR__ . '/../config/modules-livewire.php' => base_path('config/modules-livewire.php'), + ], ['modules-livewire-config']); + + $this->publishes([ + __DIR__ . '/Commands/stubs/' => base_path('stubs/modules-livewire'), + ], ['modules-livewire-stub']); + } +} diff --git a/src/Providers/LivewireComponentServiceProvider.php b/src/Providers/LivewireComponentServiceProvider.php new file mode 100644 index 0000000..6fc860f --- /dev/null +++ b/src/Providers/LivewireComponentServiceProvider.php @@ -0,0 +1,115 @@ +registerModuleComponents(); + + $this->registerCustomModuleComponents(); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return []; + } + + protected function registerModuleComponents() + { + if (Decomposer::checkDependencies()->type == 'error') { + return false; + } + + $modules = Module::toCollection(); + + $modulesLivewireNamespace = config('modules-livewire.namespace', 'Http\\Livewire'); + + $modules->each(function ($module) use ($modulesLivewireNamespace) { + $directory = (string) Str::of($module->getPath()) + ->append('/' . $modulesLivewireNamespace) + ->replace(['\\'], '/'); + + $namespace = config('modules.namespace', 'Modules') . '\\' . $module->getName() . '\\' . $modulesLivewireNamespace; + + $this->registerComponentDirectory($directory, $namespace, $module->getLowerName() . '::'); + }); + } + + protected function registerCustomModuleComponents() + { + if (Decomposer::checkDependencies(['livewire/livewire'])->type == 'error') { + return false; + } + + $modules = collect(config('modules-livewire.custom_modules', [])); + + $modules->each(function ($module, $moduleName) { + $moduleLivewireNamespace = $module['namespace'] ?? config('modules-livewire.namespace', 'Http\\Livewire'); + + $directory = (string) Str::of($module['path'] ?? '') + ->append('/' . $moduleLivewireNamespace) + ->replace(['\\'], '/'); + + $namespace = ($module['module_namespace'] ?? $moduleName) . '\\' . $moduleLivewireNamespace; + + $lowerName = $module['name_lower'] ?? strtolower($moduleName); + + $this->registerComponentDirectory($directory, $namespace, $lowerName . '::'); + }); + } + + protected function registerComponentDirectory($directory, $namespace, $aliasPrefix = '') + { + $filesystem = new Filesystem(); + + if (! $filesystem->isDirectory($directory)) { + return false; + } + + collect($filesystem->allFiles($directory)) + ->map(function (SplFileInfo $file) use ($namespace) { + return (string) Str::of($namespace) + ->append('\\', $file->getRelativePathname()) + ->replace(['/', '.php'], ['\\', '']); + }) + ->filter(function ($class) { + return is_subclass_of($class, Component::class) && ! (new ReflectionClass($class))->isAbstract(); + }) + ->each(function ($class) use ($namespace, $aliasPrefix) { + $alias = $aliasPrefix . Str::of($class) + ->after($namespace . '\\') + ->replace(['/', '\\'], '.') + ->explode('.') + ->map([Str::class, 'kebab']) + ->implode('.'); + + if (Str::endsWith($class, ['\Index', '\index'])) { + Livewire::component(Str::beforeLast($alias, '.index'), $class); + } + + Livewire::component($alias, $class); + }); + } +} diff --git a/src/Support/Decomposer.php b/src/Support/Decomposer.php new file mode 100644 index 0000000..40f7005 --- /dev/null +++ b/src/Support/Decomposer.php @@ -0,0 +1,92 @@ +get(base_path('composer.json')); + + return collect(json_decode($composer, true)); + } catch (\Exception $e) { + return collect([]); + } + } + + public static function getPackage($packageName) + { + $packages = self::getComposerData(); + + $packageFile = base_path("/vendor/{$packageName}/composer.json"); + + if (! file_exists($packageFile)) { + return null; + } + + $version = $packages->get('require')[$packageName] + ?? $packages->get('require-dev')[$packageName] + ?? null; + + return $version + ? (object) ['name' => $packageName, 'version' => $version] + : null; + } + + public static function hasPackage($packageName) + { + if (is_array($packageName)) { + return self::hasPackages($packageName); + } + + return self::getPackage($packageName) ? true : false; + } + + public static function hasPackages($packageNames = []) + { + $packages = $packageNames ?? (new static())->dependencies; + + foreach ($packages as $v) { + if (! self::getPackage($v)) { + return false; + + break; + } + } + + return true; + } + + public static function checkDependencies($packageNames = null) + { + $packages = $packageNames ?? (new static())->dependencies; + + $type = 'success'; + + $output = ''; + + if (! self::hasPackages($packages)) { + $type = 'error'; + + $output .= "\n WHOOPS! 😳 \n"; + + foreach ($packages as $package) { + if (! self::hasPackage($package)) { + $name = Str::of($package)->after('/')->studly(); + + $output .= "\n{$name} not found! \n"; + + $output .= "Install the {$name} package - composer require {$package} \n"; + } + } + } + + return (object) ['type' => $type, 'message' => $output]; + } +} diff --git a/src/Traits/CommandHelper.php b/src/Traits/CommandHelper.php new file mode 100644 index 0000000..98637de --- /dev/null +++ b/src/Traits/CommandHelper.php @@ -0,0 +1,163 @@ +option('custom') === true; + } + + protected function isForce() + { + return $this->option('force') === true; + } + + protected function isInline() + { + return $this->option('inline') === true; + } + + protected function ensureDirectoryExists($path) + { + if (! File::isDirectory(dirname($path))) { + File::makeDirectory(dirname($path), 0777, $recursive = true, $force = true); + } + } + + protected function getModule() + { + $moduleName = $this->argument('module'); + + if ($this->isCustomModule()) { + $module = config("modules-livewire.custom_modules.{$moduleName}"); + + $path = $module['path'] ?? ''; + + if (! $module || ! File::isDirectory($path)) { + $this->line(" WHOOPS! 😳 \n"); + + $path && $this->line("The custom {$moduleName} module not found in this path - {$path}."); + + ! $path && $this->line("The custom {$moduleName} module not found."); + + return null; + } + + return $moduleName; + } + + if (! $module = $this->laravel['modules']->find($moduleName)) { + $this->line(" WHOOPS! 😳 \n"); + $this->line("The {$moduleName} module not found."); + + return null; + } + + return $module; + } + + protected function getModuleName() + { + return $this->isCustomModule() + ? $this->module + : $this->module->getName(); + } + + protected function getModuleLowerName() + { + return $this->isCustomModule() + ? config("modules-livewire.custom_modules.{$this->module}.name_lower", strtolower($this->module)) + : $this->module->getLowerName(); + } + + protected function getModulePath() + { + $path = $this->isCustomModule() + ? config("modules-livewire.custom_modules.{$this->module}.path") + : $this->module->getPath(); + + return strtr($path, ['\\' => '/']); + } + + protected function getModuleNamespace() + { + return $this->isCustomModule() + ? config("modules-livewire.custom_modules.{$this->module}.module_namespace", $this->module) + : config('modules.namespace', 'Modules'); + } + + protected function getModuleLivewireNamespace() + { + $moduleLivewireNamespace = config('modules-livewire.namespace', 'Http\\Livewire'); + + if ($this->isCustomModule()) { + return config("modules-livewire.custom_modules.{$this->module}.namespace", $moduleLivewireNamespace); + } + + return $moduleLivewireNamespace; + } + + protected function getNamespace($classPath) + { + $classPath = Str::contains($classPath, '/') ? '/' . $classPath : ''; + + $prefix = $this->isCustomModule() + ? $this->getModuleNamespace() . '\\' . $this->getModuleLivewireNamespace() + : $this->getModuleNamespace() . '\\' . $this->module->getName() . '\\' . $this->getModuleLivewireNamespace(); + + return (string) Str::of($classPath) + ->beforeLast('/') + ->prepend($prefix) + ->replace(['/'], ['\\']); + } + + protected function getModuleLivewireViewDir() + { + $moduleLivewireViewDir = config('modules-livewire.view', 'Resources/views/livewire'); + + if ($this->isCustomModule()) { + $moduleLivewireViewDir = config("modules-livewire.custom_modules.{$this->module}.view", $moduleLivewireViewDir); + } + + return $this->getModulePath() . '/' . $moduleLivewireViewDir; + } + + protected function checkClassNameValid() + { + if (! $this->isClassNameValid($name = $this->component->class->name)) { + $this->line(" WHOOPS! 😳 \n"); + $this->line("Class is invalid: {$name}"); + + return false; + } + + return true; + } + + protected function checkReservedClassName() + { + if ($this->isReservedClassName($name = $this->component->class->name)) { + $this->line(" WHOOPS! 😳 \n"); + $this->line("Class is reserved: {$name}"); + + return false; + } + + return true; + } + + protected function isClassNameValid($name) + { + return (new \Livewire\Commands\MakeCommand())->isClassNameValid($name); + } + + protected function isReservedClassName($name) + { + return (new \Livewire\Commands\MakeCommand())->isReservedClassName($name); + } +} diff --git a/src/Traits/ComponentParser.php b/src/Traits/ComponentParser.php new file mode 100644 index 0000000..5de5257 --- /dev/null +++ b/src/Traits/ComponentParser.php @@ -0,0 +1,214 @@ +isCustomModule() ? ['livewire/livewire'] : null + ); + + if ($checkDependencies->type == 'error') { + $this->line($checkDependencies->message); + + return false; + } + + if (! $module = $this->getModule()) { + return false; + } + + $this->module = $module; + + $this->directories = collect( + preg_split('/[.\/(\\\\)]+/', $this->argument('component')) + )->map([Str::class, 'studly']); + + $this->component = $this->getComponent(); + + return $this; + } + + protected function getComponent() + { + $classInfo = $this->getClassInfo(); + + $viewInfo = $this->getViewInfo(); + + $stubInfo = $this->getStubInfo(); + + return (object) [ + 'class' => $classInfo, + 'view' => $viewInfo, + 'stub' => $stubInfo, + ]; + } + + protected function getClassInfo() + { + $modulePath = $this->getModulePath(); + + $moduleLivewireNamespace = $this->getModuleLivewireNamespace(); + + $classDir = (string) Str::of($modulePath) + ->append('/' . $moduleLivewireNamespace) + ->replace(['\\'], '/'); + + $classPath = $this->directories->implode('/'); + + $namespace = $this->getNamespace($classPath); + + $className = $this->directories->last(); + + $componentTag = $this->getComponentTag(); + + return (object) [ + 'dir' => $classDir, + 'path' => $classPath, + 'file' => $classDir . '/' . $classPath . '.php', + 'namespace' => $namespace, + 'name' => $className, + 'tag' => $componentTag, + ]; + } + + protected function getViewInfo() + { + $moduleLivewireViewDir = $this->getModuleLivewireViewDir(); + + $path = $this->directories + ->map([Str::class, 'kebab']) + ->implode('/'); + + if ($this->option('view')) { + $path = strtr($this->option('view'), ['.' => '/']); + } + + return (object) [ + 'dir' => $moduleLivewireViewDir, + 'path' => $path, + 'folder' => Str::after($moduleLivewireViewDir, 'views/'), + 'file' => $moduleLivewireViewDir . '/' . $path . '.blade.php', + 'name' => strtr($path, ['/' => '.']), + ]; + } + + protected function getStubInfo() + { + $defaultStubDir = __DIR__ . '/../Commands/stubs/'; + + $stubDir = File::isDirectory($publishedStubDir = base_path('stubs/modules-livewire/')) + ? $publishedStubDir + : $defaultStubDir; + + if ($this->option('stub')) { + $customStubDir = Str::of(base_path('stubs/')) + ->append($this->option('stub') . '/') + ->replace(['../', './'], ''); + + $stubDir = File::isDirectory($customStubDir) ? $customStubDir : $stubDir; + } + + $classStubName = $this->isInline() ? 'livewire.inline.stub' : 'livewire.stub'; + + $classStub = File::exists($stubDir . $classStubName) + ? $stubDir . $classStubName + : $defaultStubDir . $classStubName; + + $viewStub = File::exists($stubDir . 'livewire.view.stub') + ? $stubDir . 'livewire.view.stub' + : $defaultStubDir . 'livewire.view.stub'; + + return (object) [ + 'dir' => $stubDir, + 'class' => $classStub, + 'view' => $viewStub, + ]; + } + + protected function getClassContents() + { + $template = file_get_contents($this->component->stub->class); + + if ($this->isInline()) { + $template = preg_replace('/\[quote\]/', $this->getComponentQuote(), $template); + } + + return preg_replace( + ['/\[namespace\]/', '/\[class\]/', '/\[view\]/'], + [$this->getClassNamespace(), $this->getClassName(), $this->getViewName()], + $template, + ); + } + + protected function getViewContents() + { + return preg_replace( + '/\[quote\]/', + $this->getComponentQuote(), + file_get_contents($this->component->stub->view), + ); + } + + protected function getClassSourcePath() + { + return Str::after($this->component->class->file, $this->getBasePath() . '/'); + } + + protected function getClassNamespace() + { + return $this->component->class->namespace; + } + + protected function getClassName() + { + return $this->component->class->name; + } + + protected function getViewName() + { + return $this->getModuleLowerName() . '::' . $this->component->view->folder . '.' . $this->component->view->name; + } + + protected function getViewSourcePath() + { + return Str::after($this->component->view->file, $this->getBasePath() . '/'); + } + + protected function getComponentTag() + { + $directoryAsView = $this->directories + ->map([Str::class, 'kebab']) + ->implode('.'); + + $tag = "getModuleLowerName()}::{$directoryAsView} />"; + + $tagWithOutIndex = Str::replaceLast('.index', '', $tag); + + return $tagWithOutIndex; + } + + protected function getComponentQuote() + { + return "The {$this->getClassName()} livewire component is loaded from the " . ($this->isCustomModule() ? 'custom ' : '') . "{$this->getModuleName()} module."; + } + + protected function getBasePath($path = null) + { + return strtr(base_path($path), ['\\' => '/']); + } +}