Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Smart" route URLs caching #119

Open
wants to merge 30 commits into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ea50cf7
Initial draft of the new route caching system
Voltra Dec 2, 2023
7a7fcce
Avoid nested update event triggers
Voltra Dec 2, 2023
91cf440
Fix imports
Voltra Dec 2, 2023
a1d7f1f
Remove all traces of the previous route caching system
Voltra Dec 3, 2023
7b8e444
Use the routing prefix when generating URLs
Voltra Dec 3, 2023
8d925b9
Fix manager DI resolution
Voltra Dec 3, 2023
0852c10
Fix typo in method names
Voltra Dec 6, 2023
809ff7e
Fix cache update to make it more portable across drivers
Voltra Dec 6, 2023
35c01ca
Merge branch '2.x' into feature/SMART_ROUTE_CACHING
Z3d0X Jan 11, 2024
205b1ea
fix: phpstan
Z3d0X Jan 11, 2024
c355b92
fix: codestyle
Z3d0X Jan 11, 2024
4dbb41e
Make the manager constructor keep its previous arity
Voltra Jan 13, 2024
a39138f
Add a command to clear (and refresh) the pages' cache
Voltra Jan 13, 2024
8fa3af0
Add 'deleting' model observer listener
Voltra Jan 13, 2024
2c9a9f8
Merge branch '2.x' of github.com:Z3d0X/filament-fabricator into featu…
Z3d0X Jan 18, 2024
fe7689c
fix: phpstan
Z3d0X Jan 18, 2024
fdb809c
Merge branch '2.x' of github.com:Z3d0X/filament-fabricator into featu…
Z3d0X Feb 9, 2024
2ca3168
Merge branch 'Z3d0X:2.x' into feature/SMART_ROUTE_CACHING
Voltra Feb 9, 2024
ac46951
Add tests and fix the observer implementation issues
Voltra Feb 9, 2024
7914f40
Tweak unit tests setup
Voltra Feb 9, 2024
b5cc4b2
Extend page URLs API to refactor out code
Voltra Feb 10, 2024
128d819
Add tests for the routes cache clear command
Voltra Feb 10, 2024
65ad4e7
Add tests for the routes cache clear command
Voltra Feb 10, 2024
095ba9f
Make the default cache args invariant explicit
Voltra Feb 10, 2024
2ad06f2
Hook the routes cache command to core artisan commands
Voltra Feb 15, 2024
334704f
Fix corner-case bug of changing a page's parent, add laravel-style co…
Voltra Feb 27, 2024
cd6f9c4
Fix invalid syntax for nullable intersection
Voltra Feb 27, 2024
1052a49
Test for internal consistency, format, typos
Voltra Feb 27, 2024
af0224d
Merge 2.x
Voltra Jun 7, 2024
5b4c30d
Allow string-based IDs in the new route caching system
Voltra Jun 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions config/filament-fabricator.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@

'enable-view-page' => false,

/**
* Whether to hook into artisan's core commands to clear and refresh page route caches along with the rest.
* Disable for manual control over cache.
*
* This is the list of commands that will be hooked into:
* - cache:clear -> clear routes cache
* - config:cache -> refresh routes cache
* - config:clear -> clear routes cache
* - optimize -> refresh routes cache
* - optimize:clear -> clear routes cache
* - route:clear -> clear routes cache
*/
'hook-to-commands' => true,

/*
* This is the name of the table that will be created by the migration and
* used by the above page-model shipped with this package.
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
>
<testsuites>
<testsuite name="Z3d0X Test Suite">
<directory>tests</directory>
<directory suffix=".test.php">tests/</directory>
</testsuite>
</testsuites>
<coverage>
Expand Down
68 changes: 68 additions & 0 deletions src/Commands/ClearRoutesCacheCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace Z3d0X\FilamentFabricator\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Z3d0X\FilamentFabricator\Facades\FilamentFabricator;
use Z3d0X\FilamentFabricator\Models\Contracts\Page as PageContract;
use Z3d0X\FilamentFabricator\Services\PageRoutesService;

class ClearRoutesCacheCommand extends Command
{
protected $signature = 'filament-fabricator:clear-routes-cache {--R|refresh}';

protected $description = 'Clear the routes\' cache';

public function __construct(protected PageRoutesService $pageRoutesService)
{
parent::__construct();
}

public function handle(): int
{
$shouldRefresh = (bool) $this->option('refresh');

/**
* @var PageContract[] $pages
*/
$pages = FilamentFabricator::getPageModel()::query()
->whereNull('parent_id')
->with('allChildren')
->get();

foreach ($pages as $page) {
$this->clearPageCache($page, $shouldRefresh);

if ($shouldRefresh) {
$this->pageRoutesService->updateUrlsOf($page);
}
}

return static::SUCCESS;
}

protected function clearPageCache(PageContract $page, bool $shouldRefresh = false)
{
$this->pageRoutesService->removeUrlsOf($page);
$argSets = $page->getAllUrlCacheKeysArgs();

foreach ($argSets as $args) {
$key = $page->getUrlCacheKey($args);
Cache::forget($key);

if ($shouldRefresh) {
// Caches the URL before returning it
/* $noop = */ $page->getUrl($args);
}
}

$childPages = $page->allChildren;

if (filled($childPages)) {
foreach ($childPages as $childPage) {
$this->clearPageCache($childPage, $shouldRefresh);
}
}
}
}
5 changes: 3 additions & 2 deletions src/Facades/FilamentFabricator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Z3d0X\FilamentFabricator\Facades;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Facade;
use Z3d0X\FilamentFabricator\Models\Contracts\Page as PageContract;

Expand All @@ -25,8 +26,8 @@
* @method static array getScripts()
* @method static array getStyles()
* @method static ?string getFavicon()
* @method static class-string<PageContract> getPageModel()
* @method static string getRoutingPrefix()
* @method static class-string<PageContract&Model> getPageModel()
* @method static ?string getRoutingPrefix()
* @method static array getPageUrls()
* @method static ?string getPageUrlFromId(int $id, bool $prefixSlash = false)
*
Expand Down
52 changes: 21 additions & 31 deletions src/FilamentFabricatorManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Z3d0X\FilamentFabricator\Models\Contracts\Page as PageContract;
use Z3d0X\FilamentFabricator\Models\Page;
use Z3d0X\FilamentFabricator\PageBlocks\PageBlock;
use Z3d0X\FilamentFabricator\Services\PageRoutesService;

class FilamentFabricatorManager
{
Expand All @@ -33,8 +34,16 @@ class FilamentFabricatorManager

protected array $pageUrls = [];

public function __construct()
/**
* @note It's only separated to not cause a major version change.
* In the next major release, feel free to make it a constructor promoted property
*/
protected PageRoutesService $routesService;

public function __construct(?PageRoutesService $routesService = null)
{
$this->routesService = $routesService ?? resolve(PageRoutesService::class);

/** @var Collection<string,string> */
$pageBlocks = collect([]);

Expand Down Expand Up @@ -172,44 +181,25 @@ public function getRoutingPrefix(): ?string
return null;
}

return Str::start(config('filament-fabricator.routing.prefix'), '/');
}
$prefix = Str::start($prefix, '/');

public function getPageUrls(): array
{
return Cache::rememberForever('filament-fabricator::page-urls', function () {
$this->getPageModel()::query()
->select('id', 'slug', 'title')
->whereNull('parent_id')
->with(['allChildren'])
->get()
->each(fn (PageContract $page) => $this->setPageUrl($page)); // @phpstan-ignore-line
if ($prefix === '/') {
return $prefix;
}

return $this->pageUrls;
});
return rtrim($prefix, '/');
}

public function getPageUrlFromId(int|string $id, bool $prefixSlash = false): ?string
public function getPageUrls(): array
{
$url = $this->getPageUrls()[$id];

if ($routingPrefix = $this->getRoutingPrefix()) {
$url = Str::start($url, $routingPrefix);
}

return $url;
return $this->routesService->getAllUrls();
}

protected function setPageUrl(PageContract $page, ?string $parentUrl = null): string
public function getPageUrlFromId(int|string $id, bool $prefixSlash = false, array $args = []): ?string
{
$pageUrl = $parentUrl ? $parentUrl . '/' . trim($page->slug, " \n\r\t\v\x00/") : trim($page->slug);

if (filled($page->allChildren)) {
foreach ($page->allChildren as $child) {
$this->setPageUrl($child, $pageUrl);
}
}
/** @var ?PageContract $page */
$page = $this->getPageModel()::query()->find($id);

return $this->pageUrls[$page->id] = Str::start($pageUrl, '/');
return $page?->getUrl($args);
}
}
34 changes: 23 additions & 11 deletions src/FilamentFabricatorServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Z3d0X\FilamentFabricator;

use Illuminate\Console\Events\CommandFinished;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use ReflectionClass;
Expand All @@ -12,7 +14,10 @@
use Symfony\Component\Finder\SplFileInfo;
use Z3d0X\FilamentFabricator\Facades\FilamentFabricator;
use Z3d0X\FilamentFabricator\Layouts\Layout;
use Z3d0X\FilamentFabricator\Listeners\OptimizeWithLaravel;
use Z3d0X\FilamentFabricator\Observers\PageRoutesObserver;
use Z3d0X\FilamentFabricator\PageBlocks\PageBlock;
use Z3d0X\FilamentFabricator\Services\PageRoutesService;

class FilamentFabricatorServiceProvider extends PackageServiceProvider
{
Expand Down Expand Up @@ -43,6 +48,7 @@ protected function getCommands(): array
$commands = [
Commands\MakeLayoutCommand::class,
Commands\MakePageBlockCommand::class,
Commands\ClearRoutesCacheCommand::class,
];

$aliases = [];
Expand All @@ -65,24 +71,19 @@ public function packageRegistered(): void
parent::packageRegistered();

$this->app->singleton('filament-fabricator', function () {
return new FilamentFabricatorManager();
return resolve(FilamentFabricatorManager::class);
});
}

public function bootingPackage(): void
{
Route::bind('filamentFabricatorPage', function ($value) {
$pageModel = FilamentFabricator::getPageModel();
/**
* @var PageRoutesService $routesService
*/
$routesService = resolve(PageRoutesService::class);

$pageUrls = FilamentFabricator::getPageUrls();

$value = Str::start($value, '/');

$pageId = array_search($value, $pageUrls);

return $pageModel::query()
->where('id', $pageId)
->firstOrFail();
return $routesService->findPageOrFail($value);
});

$this->registerComponentsFromDirectory(
Expand All @@ -100,6 +101,17 @@ public function bootingPackage(): void
);
}

public function packageBooted()
{
parent::packageBooted();

FilamentFabricator::getPageModel()::observe(PageRoutesObserver::class);

if ((bool) config('filament-fabricator.hook-to-commands')) {
Event::listen(CommandFinished::class, OptimizeWithLaravel::class);
}
}

protected function registerComponentsFromDirectory(string $baseClass, array $register, ?string $directory, ?string $namespace): void
{
if (blank($directory) || blank($namespace)) {
Expand Down
12 changes: 6 additions & 6 deletions src/Http/Controllers/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
use Z3d0X\FilamentFabricator\Facades\FilamentFabricator;
use Z3d0X\FilamentFabricator\Layouts\Layout;
use Z3d0X\FilamentFabricator\Models\Contracts\Page;
use Z3d0X\FilamentFabricator\Services\PageRoutesService;

class PageController
{
public function __invoke(?Page $filamentFabricatorPage = null): string
{
// Handle root (home) page
if (blank($filamentFabricatorPage)) {
$pageUrls = FilamentFabricator::getPageUrls();

$pageId = array_search('/', $pageUrls);
/**
* @var PageRoutesService $routesService
*/
$routesService = resolve(PageRoutesService::class);

/** @var Page $filamentFabricatorPage */
$filamentFabricatorPage = FilamentFabricator::getPageModel()::query()
->where('id', $pageId)
->firstOrFail();
$filamentFabricatorPage = $routesService->findPageOrFail('/');
}

/** @var ?class-string<Layout> $layout */
Expand Down
66 changes: 66 additions & 0 deletions src/Listeners/OptimizeWithLaravel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Z3d0X\FilamentFabricator\Listeners;

use Illuminate\Console\Command;
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Support\Facades\Artisan;
use Z3d0X\FilamentFabricator\Commands\ClearRoutesCacheCommand;

class OptimizeWithLaravel
{
const COMMANDS = [
'cache:clear',
'config:cache',
'config:clear',
'optimize',
'optimize:clear',
'route:clear',
];

const REFRESH_COMMANDS = [
'config:cache',
'optimize',
];

public function handle(CommandFinished $event): void
{
if (! $this->shouldHandleEvent($event)) {
return;
}

if ($this->shouldRefresh($event)) {
$this->refresh();
} else {
$this->clear();
}
}

public function shouldHandleEvent(CommandFinished $event)
{
return $event->exitCode === Command::SUCCESS
&& in_array($event->command, static::COMMANDS);
}

public function shouldRefresh(CommandFinished $event)
{
return in_array($event->command, static::REFRESH_COMMANDS);
}

public function refresh()
{
$this->callCommand([
'--refresh' => true,
]);
}

public function clear()
{
$this->callCommand();
}

public function callCommand(array $params = [])
{
Artisan::call(ClearRoutesCacheCommand::class, $params);
}
}
Loading