app['config']->set('view.compiled', storage_path('framework/views')); $this->app['config']->set('view.cache', true); $this->app['config']->set('view.compiled_extension', 'php'); $this->registerFactory(); $this->registerViewFinder(); $this->registerBladeCompiler(); $this->registerEngineResolver(); $this->app->terminating(static function () { \Illuminate\View\Component::flushCache(); }); } /** * Register the view environment. * * @return void */ public function registerFactory() { $this->app->singleton('view', function ($app) { // Next we need to grab the engine resolver instance that will be used by the // environment. The resolver will be used by an environment to get each of // the various engine implementations such as plain PHP or Blade engine. $resolver = $app['view.engine.resolver']; $finder = $app['view.finder']; $factory = $this->createFactory($resolver, $finder, $app['events']); // Legacy template extensions — still needed for plugins with .tpl.php/.sub.php/.inc.php array_map(fn ($ext) => $factory->addExtension($ext, 'php'), ['inc.php', 'sub.php', 'tpl.php']); // Use blade engine for all things blade (registered last to take priority) $factory->addExtension('blade.php', 'blade'); // We will also set the container instance on this view environment since the // view composers may be classes registered in the container, which allows // for great testable, flexible composers for the application developer. $factory->setContainer(app()); $factory->share('app', $app); // Find and set composers $composers = $this->getComposerPaths(); foreach ($composers as $key => $composerClass) { if ( (is_subclass_of($composerClass, \Leantime\Core\UI\Composer::class) || is_subclass_of($composerClass, \Leantime\Core\Controller\Composer::class)) && ! (new \ReflectionClass($composerClass))->isAbstract() ) { $factory->composer($composerClass::$views, $composerClass); } } $app->terminating(static function () { \Illuminate\View\Component::forgetFactory(); }); return $factory; }); } /** * Create a new Factory Instance. * * @param \Illuminate\View\Engines\EngineResolver $resolver * @param \Illuminate\View\ViewFinderInterface $finder * @param \Illuminate\Contracts\Events\Dispatcher $events * @return \Illuminate\View\Factory */ protected function createFactory($resolver, $finder, $events) { return new Factory($resolver, $finder, $events); } /** * Register the view finder implementation. * * @return void */ public function registerViewFinder() { $this->app->bind('view.finder', function ($app) { $fileViewFinder = new FileViewFinder($app['files'], []); $this->viewPaths = $this->getViewPaths(); array_map([$fileViewFinder, 'addNamespace'], array_keys($this->viewPaths), array_values($this->viewPaths)); return $fileViewFinder; }); } /** * Register the Blade compiler implementation. * * @return void */ public function registerBladeCompiler() { $this->app->singleton('blade.compiler', function ($app) { $compiler = new BladeCompiler( $app['files'], $app['config']['view.compiled'], $app->basePath(), $app['config']->get('view.cache', true), $app['config']->get('view.compiled_extension', 'php'), ); if (! $this->viewPaths) { $this->viewPaths = $this->getViewPaths(); } $namespaces = array_keys($this->viewPaths); array_map( [$compiler, 'anonymousComponentNamespace'], array_map(fn ($namespace) => "$namespace::components", $namespaces), $namespaces ); // Class-based components (Type 3): map to a PHP component class when one // exists, transparently falling back to the anonymous blade file otherwise. Registration // is lazy, so a namespace whose class root does not exist (or whose plugin folder casing // differs) is harmless — the anonymous component is still used. foreach ($namespaces as $namespace) { if ($classNamespace = $this->componentClassNamespace($namespace)) { $compiler->componentNamespace($classNamespace, $namespace); } } return tap($compiler, function ($blade) { $blade->component('dynamic-component', DynamicComponent::class); }); }); } /** * Resolve the PHP class namespace that backs class-based (Type 3) Blade components for a given * view namespace, or null when the namespace has no class root. * * Maps: global/globals → Leantime\Views\Components; a domain → Leantime\Domain\{Studly}\View\Components; * a plugin → Leantime\Plugins\{Studly}\View\Components. */ private function componentClassNamespace(string $namespace): ?string { if (in_array($namespace, ['global', 'globals', '__components'], true)) { return 'Leantime\\Views\\Components'; } $studly = Str::studly($namespace); if (is_dir(APP_ROOT.'/app/Domain/'.$studly)) { return 'Leantime\\Domain\\'.$studly.'\\View\\Components'; } if (is_dir(APP_ROOT.'/app/Plugins/'.$studly)) { return 'Leantime\\Plugins\\'.$studly.'\\View\\Components'; } return null; } /** * Register the engine resolver instance. * * @return void */ public function registerEngineResolver() { $this->app->singleton('view.engine.resolver', function () { $resolver = new EngineResolver; // Next, we will register the various view engines with the resolver so that the // environment will resolve the engines needed for various views based on the // extension of view file. We call a method for each of the view's engines. foreach (['file', 'php', 'blade'] as $engine) { $this->{'register'.ucfirst($engine).'Engine'}($resolver); } return $resolver; }); } /** * Register the file engine implementation. * * @param \Illuminate\View\Engines\EngineResolver $resolver * @return void */ public function registerFileEngine($resolver) { $resolver->register('file', function () { return new FileEngine(Container::getInstance()->make('files')); }); } /** * Register the PHP engine implementation. * * @param \Illuminate\View\Engines\EngineResolver $resolver * @return void */ public function registerPhpEngine($resolver) { $resolver->register('php', function () { return new PhpEngine(Container::getInstance()->make('files')); }); } /** * Register the Blade engine implementation. * * @param \Illuminate\View\Engines\EngineResolver $resolver * @return void */ public function registerBladeEngine($resolver) { $resolver->register('blade', function () { $app = Container::getInstance(); $compiler = new CompilerEngine( $app->make('blade.compiler'), $app->make('files'), ); $app->terminating(static function () use ($compiler) { $compiler->forgetCompiledOrNotExpired(); }); return $compiler; }); } /** * @throws BindingResolutionException */ public function getComposerPaths() { $pathRepo = app()->make(PathManifestRepository::class); if ($viewPaths = $pathRepo->loadManifest('composerPaths')) { return $viewPaths; } $storePaths = $this->discoverComposerPaths(); $viewPaths = $pathRepo->writeManifest('composerPaths', $storePaths); return $viewPaths; } private function discoverComposerPaths() { $appComposerClasses = collect(glob(APP_ROOT.'/app/Views/Composers/*.php')) ->concat(glob(APP_ROOT.'/app/Domain/*/Composers/*.php')); try { $pluginService = $this->app->make(\Leantime\Core\Plugins\Plugins::class); $enabledPluginPaths = $pluginService->getEnabledPluginPaths(); $pluginComposerClasses = collect($enabledPluginPaths) ->map(function ($pluginInfo) { $composersPath = $pluginInfo['path'].'/Composers/'; if ($pluginInfo['format'] === 'phar') { if (! file_exists($pluginInfo['path'])) { return []; } try { $composers = []; $p = new \Phar($composersPath, 0); $paths = collect(new \RecursiveIteratorIterator($p)); foreach ($paths as $path) { $something = $path; $composers[] = 'Plugins/'.$pluginInfo['foldername'].'/Composers/'.$path->getFileName(); } return $composers; } catch (\Exception $e) { return []; } } // Use glob which works for both folder and phar paths return glob($composersPath.'*.php') ?: []; }) ->flatten(); } catch (\Exception $e) { Log::error($e); $pluginComposerClasses = collect(); } $composerList = $appComposerClasses ->concat($pluginComposerClasses) ->map(fn ($filepath) => Str::of($filepath) ->replace([APP_ROOT.'/app/', '.php'], ['', '', '']) ->replace('/', '\\') ->start($this->app->getNamespace()) ->toString()) ->all(); return $composerList; } public function boot() { app('blade.compiler')->directive( 'dispatchEvent', fn ($args) => "dispatchTplEvent($args); ?>", ); app('blade.compiler')->directive( 'dispatchFilter', fn ($args) => "dispatchTplFilter($args); ?>", ); app('blade.compiler')->directive( 'spaceless', fn ($args) => '', ); app('blade.compiler')->directive( 'endspaceless', fn ($args) => "\\s+<', ob_get_clean()); ?>", ); app('blade.compiler')->directive( 'formatDate', fn ($args) => "date(); ?>", ); app('blade.compiler')->directive( 'formatTime', fn ($args) => "time(); ?>", ); app('blade.compiler')->directive( 'displayNotification', fn ($args) => 'displayNotification(); ?>', ); } public function getViewPaths() { $pathRepo = app()->make(PathManifestRepository::class); // Check if we're in a test environment if (app()->environment('testing')) { // Force regeneration of view paths in test environment return $this->discoverViewPaths(); } if ($viewPaths = $pathRepo->loadManifest('viewPaths')) { return $viewPaths; } $storePaths = $this->discoverViewPaths(); $viewPaths = $pathRepo->writeManifest('viewPaths', $storePaths); return $viewPaths; } private function discoverViewPaths() { $domainPaths = collect(glob($this->app->basePath().'/app/Domain/*')) ->mapWithKeys(function ($path) { if (is_dir($path.'/Templates')) { return [ $basename = strtolower(basename($path)) => [ "$path/Templates", ], ]; } return []; }); // Get all plugin paths (both enabled and disabled) since views are discovered at service provider registration // For view paths we need to include all plugins, not just enabled ones, to avoid database calls during registration $pluginPaths = collect(glob($this->app->basePath().'/app/Plugins/*')) ->mapWithKeys(function ($path) { $pluginFolder = basename($path); // Check phar templates first $pharTemplatesPath = 'phar://'.APP_ROOT.'/app/Plugins/'.$pluginFolder.'/'.$pluginFolder.'.phar/Templates'; if (is_dir($pharTemplatesPath)) { return [strtolower($pluginFolder) => [$pharTemplatesPath]]; } // Check folder templates $folderTemplatesPath = APP_ROOT.'/app/Plugins/'.$pluginFolder.'/Templates'; if (is_dir($folderTemplatesPath)) { return [strtolower($pluginFolder) => [$folderTemplatesPath]]; } return []; }); $storePaths = $domainPaths ->merge($pluginPaths) ->merge(['globals' => APP_ROOT.'/app/Views/Templates']) ->merge(['global' => APP_ROOT.'/app/Views/Templates']) ->merge(['__components' => $this->app['config']->get('view.compiled')]) ->all(); return $storePaths; } }