Merge pull request #11258 from filamentphp/feature/cached-components

feature: Allow components to be cached
This commit is contained in:
Dan Harrin
2024-02-06 14:18:25 +00:00
committed by GitHub
8 changed files with 262 additions and 51 deletions
+26 -4
View File
@@ -43,6 +43,32 @@ Not sure where to start? Review the [Getting Started guide](getting-started) to
## Using other Filament packages
The Filament Panel Builder pre-installs the [Form Builder](/docs/forms), [Table Builder](/docs/tables), [Notifications](/docs/notifications), [Actions](/docs/actions), [Infolists](/docs/infolists), and [Widgets](/docs/widgets) packages. No other installation steps are required to use these packages within a panel.
## Improving Filament panel performance
### Caching Blade Icons
You may wish to consider running `php artisan icons:cache` locally, and also in your deployment script. This is because Filament uses the [Blade Icons](https://blade-ui-kit.com/blade-icons) package, which can be much more performant when cached.
### Caching Filament components
You may also wish to consider running `php artisan filament:cache-components` in your deployment script, especially if you have large numbers of components (resources, pages, widgets, relation managers, custom Livewire components, etc.). This will create cache files in the `bootstrap/cache/filament` directory of your application, which contain indexes for each type of component. This can significantly improve the performance of Filament in some apps, as it reduces the number of files that need to be scanned and auto-discovered for components.
However, if you are actively developing your app locally, you should avoid using this command, as it will prevent any new components from being discovered until the cache is cleared or rebuilt.
You can clear the cache at any time without rebuilding it by running `php artisan filament:clear-cached-components`.
### Enabling OPcache on your server
From the [Laravel Forge documentation](https://forge.laravel.com/docs/servers/php.html#opcache):
> Optimizing the PHP OPcache for production will configure OPcache to store your compiled PHP code in memory to greatly improve performance.
Please use a search engine to find the relevant OPcache setup instructions for your environment.
### Optimizing your Laravel app
You should also consider optimizing your Laravel app for production by running `php artisan optimize` in your deployment script. This will cache the configuration files and routes.
## Deploying to production
### Allowing users to access a panel
@@ -73,10 +99,6 @@ class User extends Authenticatable implements FilamentUser
Learn more about [users](users).
### Caching Blade Icons
You may wish to consider using `php artisan icons:cache` in your deployment process, as Filament uses the [Blade Icons](https://blade-ui-kit.com/blade-icons) package, which can be much more performant when cached. This is very hardware-dependant though, and many servers do not require this command for Filament to run smoothly.
## Publishing configuration
You can publish the Filament package configuration (if needed) using the following command:
@@ -0,0 +1,26 @@
<?php
namespace Filament\Commands;
use Filament\Facades\Filament;
use Illuminate\Console\Command;
class CacheComponentsCommand extends Command
{
protected $description = 'Cache all components';
protected $signature = 'filament:cache-components';
public function handle(): int
{
$this->info('Caching registered components...');
foreach (Filament::getPanels() as $panel) {
$panel->cacheComponents();
}
$this->info('All done!');
return static::SUCCESS;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Filament\Commands;
use Filament\Facades\Filament;
use Illuminate\Console\Command;
class ClearCachedComponentsCommand extends Command
{
protected $description = 'Cache all components';
protected $signature = 'filament:clear-cached-components';
public function handle(): int
{
$this->info('Clearing cached components...');
foreach (Filament::getPanels() as $panel) {
$panel->clearCachedComponents();
}
$this->info('All done!');
return static::SUCCESS;
}
}
@@ -96,6 +96,8 @@ class FilamentServiceProvider extends PackageServiceProvider
protected function getCommands(): array
{
$commands = [
Commands\CacheComponentsCommand::class,
Commands\ClearCachedComponentsCommand::class,
Commands\MakeClusterCommand::class,
Commands\MakePageCommand::class,
Commands\MakePanelCommand::class,
+1 -4
View File
@@ -46,10 +46,7 @@ class Panel extends Component
public static function make(): static
{
$static = app(static::class);
$static->configure();
return $static;
return app(static::class);
}
public function default(bool $condition = true): static
@@ -97,11 +97,17 @@ trait HasComponents
protected bool | Closure $hasReadOnlyRelationManagersOnResourceViewPagesByDefault = true;
protected ?bool $hasCachedComponents = null;
/**
* @param array<class-string> $pages
*/
public function pages(array $pages): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->pages = [
...$this->pages,
...$pages,
@@ -120,6 +126,10 @@ trait HasComponents
*/
public function resources(array $resources): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->resources = [
...$this->resources,
...$resources,
@@ -154,6 +164,10 @@ trait HasComponents
*/
public function widgets(array $widgets): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->widgets = [
...$this->widgets,
...$widgets,
@@ -181,6 +195,10 @@ trait HasComponents
public function discoverPages(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->pageDirectories[] = $in;
$this->pageNamespaces[] = $for;
@@ -218,6 +236,10 @@ trait HasComponents
public function discoverClusters(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->clusterDirectories[] = $in;
$this->clusterNamespaces[] = $for;
@@ -261,6 +283,10 @@ trait HasComponents
public function discoverResources(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->resourceDirectories[] = $in;
$this->resourceNamespaces[] = $for;
@@ -298,6 +324,10 @@ trait HasComponents
public function discoverWidgets(string $in, string $for): static
{
if ($this->hasCachedComponents()) {
return $this;
}
$this->widgetDirectories[] = $in;
$this->widgetNamespaces[] = $for;
@@ -329,11 +359,15 @@ trait HasComponents
public function discoverLivewireComponents(string $in, string $for): static
{
$component = [];
if ($this->hasCachedComponents()) {
return $this;
}
$components = [];
$this->discoverComponents(
Component::class,
$component,
$components,
directory: $in,
namespace: $for,
);
@@ -434,6 +468,10 @@ trait HasComponents
*/
public function livewireComponents(array $components): static
{
if ($this->hasCachedComponents()) {
return $this;
}
foreach ($components as $component) {
$this->queueLivewireComponentForRegistration($component);
}
@@ -443,60 +481,60 @@ trait HasComponents
protected function registerLivewireComponents(): void
{
$this->queueLivewireComponentForRegistration(DatabaseNotifications::class);
$this->queueLivewireComponentForRegistration(EditProfile::class);
$this->queueLivewireComponentForRegistration(GlobalSearch::class);
$this->queueLivewireComponentForRegistration(Notifications::class);
if (! $this->hasCachedComponents()) {
$this->queueLivewireComponentForRegistration(DatabaseNotifications::class);
$this->queueLivewireComponentForRegistration(EditProfile::class);
$this->queueLivewireComponentForRegistration(GlobalSearch::class);
$this->queueLivewireComponentForRegistration(Notifications::class);
if ($this->hasEmailVerification() && is_subclass_of($emailVerificationRouteAction = $this->getEmailVerificationPromptRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($emailVerificationRouteAction);
}
if ($this->hasLogin() && is_subclass_of($loginRouteAction = $this->getLoginRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($loginRouteAction);
}
if ($this->hasPasswordReset()) {
if (is_subclass_of($requestPasswordResetRouteAction = $this->getRequestPasswordResetRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($requestPasswordResetRouteAction);
if ($this->hasEmailVerification() && is_subclass_of($emailVerificationRouteAction = $this->getEmailVerificationPromptRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($emailVerificationRouteAction);
}
if (is_subclass_of($resetPasswordRouteAction = $this->getResetPasswordRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($resetPasswordRouteAction);
}
}
if ($this->hasRegistration() && is_subclass_of($registrationRouteAction = $this->getRegistrationRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($registrationRouteAction);
}
foreach ($this->getResources() as $resource) {
foreach ($resource::getPages() as $pageRegistration) {
$this->queueLivewireComponentForRegistration($pageRegistration->getPage());
if ($this->hasLogin() && is_subclass_of($loginRouteAction = $this->getLoginRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($loginRouteAction);
}
foreach ($resource::getRelations() as $relation) {
if ($relation instanceof RelationGroup) {
foreach ($relation->getManagers() as $groupedRelation) {
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($groupedRelation));
}
continue;
if ($this->hasPasswordReset()) {
if (is_subclass_of($requestPasswordResetRouteAction = $this->getRequestPasswordResetRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($requestPasswordResetRouteAction);
}
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($relation));
if (is_subclass_of($resetPasswordRouteAction = $this->getResetPasswordRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($resetPasswordRouteAction);
}
}
foreach ($resource::getWidgets() as $widget) {
$this->queueLivewireComponentForRegistration($this->normalizeWidgetClass($widget));
if ($this->hasRegistration() && is_subclass_of($registrationRouteAction = $this->getRegistrationRouteAction(), Component::class)) {
$this->queueLivewireComponentForRegistration($registrationRouteAction);
}
foreach ($this->getResources() as $resource) {
foreach ($resource::getPages() as $pageRegistration) {
$this->queueLivewireComponentForRegistration($pageRegistration->getPage());
}
foreach ($resource::getRelations() as $relation) {
if ($relation instanceof RelationGroup) {
foreach ($relation->getManagers() as $groupedRelation) {
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($groupedRelation));
}
continue;
}
$this->queueLivewireComponentForRegistration($this->normalizeRelationManagerClass($relation));
}
foreach ($resource::getWidgets() as $widget) {
$this->queueLivewireComponentForRegistration($this->normalizeWidgetClass($widget));
}
}
}
foreach ($this->livewireComponents as $componentName => $componentClass) {
Livewire::component($componentName, $componentClass);
}
$this->livewireComponents = [];
}
/**
@@ -561,4 +599,78 @@ trait HasComponents
return $this->clusteredComponents[$cluster] ?? [];
}
public function hasCachedComponents(): bool
{
return $this->hasCachedComponents ??= ((! app()->runningInConsole()) && app(Filesystem::class)->exists($this->getComponentCachePath()));
}
public function cacheComponents(): void
{
$this->hasCachedComponents = false;
$cachePath = $this->getComponentCachePath();
$filesystem = app(Filesystem::class);
$filesystem->ensureDirectoryExists((string) str($cachePath)->beforeLast(DIRECTORY_SEPARATOR));
$filesystem->put(
$cachePath,
'<?php return ' . var_export([
'livewireComponents' => $this->livewireComponents,
'clusters' => $this->clusters,
'clusteredComponents' => $this->clusteredComponents,
'clusterDirectories' => $this->clusterDirectories,
'clusterNamespaces' => $this->clusterNamespaces,
'pages' => $this->pages,
'pageDirectories' => $this->pageDirectories,
'pageNamespaces' => $this->pageNamespaces,
'resources' => $this->resources,
'resourceDirectories' => $this->resourceDirectories,
'resourceNamespaces' => $this->resourceNamespaces,
'widgets' => $this->widgets,
'widgetDirectories' => $this->widgetDirectories,
'widgetNamespaces' => $this->widgetNamespaces,
], true) . ';',
);
$this->hasCachedComponents = true;
}
public function restoreCachedComponents(): void
{
if (! $this->hasCachedComponents()) {
return;
}
$cache = require $this->getComponentCachePath();
$this->livewireComponents = $cache['livewireComponents'] ?? [];
$this->clusters = $cache['clusters'] ?? [];
$this->clusteredComponents = $cache['clusteredComponents'] ?? [];
$this->clusterDirectories = $cache['clusterDirectories'] ?? [];
$this->clusterNamespaces = $cache['clusterNamespaces'] ?? [];
$this->pages = $cache['pages'] ?? [];
$this->pageDirectories = $cache['pageDirectories'] ?? [];
$this->pageNamespaces = $cache['pageNamespaces'] ?? [];
$this->resources = $cache['resources'] ?? [];
$this->resourceDirectories = $cache['resourceDirectories'] ?? [];
$this->resourceNamespaces = $cache['resourceNamespaces'] ?? [];
$this->widgets = $cache['widgets'] ?? [];
$this->widgetDirectories = $cache['widgetDirectories'] ?? [];
$this->widgetNamespaces = $cache['widgetNamespaces'] ?? [];
}
public function clearCachedComponents(): void
{
app(Filesystem::class)->delete($this->getComponentCachePath());
$this->hasCachedComponents = false;
}
public function getComponentCachePath(): string
{
return (config('filament.cache_path') ?? base_path('bootstrap/cache/filament')) . DIRECTORY_SEPARATOR . "panels/{$this->getId()}.php";
}
}
@@ -2,19 +2,31 @@
namespace Filament\Panel\Concerns;
use Exception;
trait HasId
{
protected string $id;
public function id(string $id): static
{
if (isset($this->id)) {
throw new Exception("The panel has already been registered with the ID [{$this->id}].");
}
$this->id = $id;
$this->configure();
$this->restoreCachedComponents();
return $this;
}
public function getId(): string
{
if (! isset($this->id)) {
throw new Exception('A panel has been registered without an `id()`.');
}
return $this->id;
}
}
+16 -2
View File
@@ -35,8 +35,8 @@ return [
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| This is the storage disk Filament will use to put media. You may use any
| of the disks defined in the `config/filesystems.php`.
| This is the storage disk Filament will use to store files. You may use
| any of the disks defined in the `config/filesystems.php`.
|
*/
@@ -56,6 +56,20 @@ return [
'assets_path' => null,
/*
|--------------------------------------------------------------------------
| Cache Path
|--------------------------------------------------------------------------
|
| This is the directory that Filament will use to store cache files that
| are used to optimize the registration of components.
|
| After changing the path, you should run `php artisan filament:cache-components`.
|
*/
'cache_path' => base_path('bootstrap/cache/filament'),
/*
|--------------------------------------------------------------------------
| Livewire Loading Delay