Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Next Next commit
wip
  • Loading branch information
pxpm committed Jun 6, 2025
commit d500946a27802ff9bb9c33e1bbdd28d756ded7d1
13 changes: 12 additions & 1 deletion src/app/Http/Controllers/Operations/ListOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Backpack\CRUD\app\Library\CrudPanel\Hooks\Facades\LifecycleHook;
use Illuminate\Support\Facades\Route;
use Backpack\CRUD\app\Library\Support\DatatableCache;

trait ListOperation
{
Expand Down Expand Up @@ -78,7 +79,7 @@ public function search()

// If there's a config closure in the cache for this CRUD, run that configuration closure.
// This is done in order to allow the developer to configure the datatable component.
\Backpack\CRUD\app\View\Components\Datatable::applyCachedSetupClosure($this->crud);
$this->applyCachedDatatableSetup();

$this->crud->applyUnappliedFilters();

Expand Down Expand Up @@ -127,6 +128,16 @@ public function search()
return $this->crud->getEntriesAsJsonForDatatables($entries, $totalEntryCount, $filteredEntryCount, $start);
}

/**
* Apply the cached datatable setup configuration directly using DatatableCache.
*
* @return bool Whether the cached setup was successfully applied
*/
protected function applyCachedDatatableSetup()
{
return DatatableCache::instance()->applyFromRequest($this->crud);
}

/**
* Used with AJAX in the list view (datatables) to show extra information about that row that didn't fit in the table.
* It defaults to showing some dummy text.
Expand Down
196 changes: 196 additions & 0 deletions src/app/Library/Support/DatatableCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php

namespace Backpack\CRUD\app\Library\Support;

use Backpack\CRUD\app\Library\CrudPanel\CrudPanel;
use Backpack\CRUD\CrudManager;
use Backpack\CRUD\app\Library\Widget;

class DatatableCache extends SetupCache
{
public function __construct()
{
$this->cachePrefix = 'datatable_config_';
$this->cacheDuration = 60; // 1 hour
}

/**
* Get the instance of DatatableCache.
*/
public static function instance(): self
{
static $instance = null;

if ($instance === null) {
$instance = new static();
}

return $instance;
}

/**
* Cache setup closure for a datatable component.
*
* @param string $tableId The table ID to use as cache key
* @param string $controllerClass The controller class
* @param \Closure|null $setup The setup closure
* @param string|null $name The element name
* @param CrudPanel $crud The CRUD panel instance to update with datatable_id
* @return bool Whether the operation was successful
*/
public function cacheForComponent(string $tableId, string $controllerClass, ?\Closure $setup = null, ?string $name = null, ?CrudPanel $crud = null): bool
{
if (! $setup) {
return false;
}

$cruds = CrudManager::getCrudPanels();
$parentCrud = reset($cruds);

if ($parentCrud && $parentCrud->getCurrentEntry()) {
$parentEntry = $parentCrud->getCurrentEntry();
$parentController = $parentCrud->controller;

// Store in cache
$this->store(
$tableId,
$controllerClass,
$parentController,
$parentEntry,
$name
);

// Set the datatable_id in the CRUD panel if provided
if ($crud) {
$crud->set('list.datatable_id', $tableId);
}

return true;
}

return false;
}

/**
* Apply cached setup to a CRUD instance using the request's datatable_id.
*
* @param CrudPanel $crud The CRUD panel instance
* @return bool Whether the operation was successful
*/
public function applyFromRequest(CrudPanel $crud): bool
{
$tableId = request('datatable_id');

if (! $tableId) {
\Log::debug('Missing datatable_id in request parameters');
return false;
}

return $this->apply($tableId, $crud);
}

/**
* Apply a setup closure to a CrudPanel instance.
*
* @param CrudPanel $crud The CRUD panel instance
* @param string $controllerClass The controller class
* @param \Closure $setupClosure The setup closure
* @param mixed $entry The entry to pass to the setup closure
* @return bool Whether the operation was successful
*/
public function applySetupClosure(CrudPanel $crud, string $controllerClass, \Closure $setupClosure, $entry = null): bool
{
$originalSetup = $setupClosure;
$modifiedSetup = function ($crud, $entry) use ($originalSetup, $controllerClass) {
CrudManager::setActiveController($controllerClass);

// Run the original closure
return ($originalSetup)($crud, $entry);
};

try {
// Execute the modified closure
($modifiedSetup)($crud, $entry);

return true;
} finally {
// Clean up
CrudManager::unsetActiveController();
}
}

/**
* Prepare datatable data for storage in the cache.
*
* @param string $controllerClass The controller class
* @param string $parentController The parent controller
* @param mixed $parentEntry The parent entry
* @param string|null $elementName The element name
* @return array The data to be cached
*/
protected function prepareDataForStorage(...$args): array
{
[$controllerClass, $parentController, $parentEntry, $elementName] = $args;

return [
'controller' => $controllerClass,
'parentController' => $parentController,
'parent_entry' => $parentEntry,
'element_name' => $elementName,
'operations' => CrudManager::getInitializedOperations($parentController),
];
}

/**
* Apply data from the cache to configure a datatable.
*
* @param array $cachedData The cached data
* @param CrudPanel $crud The CRUD panel instance
* @return bool Whether the operation was successful
*/
protected function applyFromCache($cachedData, ...$args): bool
{
[$crud] = $args;

try {
// Initialize operations for the parent controller
$this->initializeOperations($cachedData['parentController'], $cachedData['operations']);
$entry = $cachedData['parent_entry'];
$elementName = $cachedData['element_name'];

$widgets = Widget::collection();
$found = false;

foreach ($widgets as $widget) {
if ($widget['type'] === 'datatable' &&
(isset($widget['name']) && $widget['name'] === $elementName) &&
(isset($widget['setup']) && $widget['setup'] instanceof \Closure)) {

$this->applySetupClosure($crud, $cachedData['controller'], $widget['setup'], $entry);
$found = true;
break;
}
}

return $found;
} catch (\Exception $e) {
\Log::error('Error applying cached datatable config: ' . $e->getMessage(), [
'exception' => $e,
]);
return false;
}
}

/**
* Initialize operations for a parent controller.
*/
private function initializeOperations(string $parentController, $operations): void
{
$parentCrud = CrudManager::setupCrudPanel($parentController);

foreach ($operations as $operation) {
$parentCrud->initialized = false;
CrudManager::setupCrudPanel($parentController, $operation);
}
}
}
108 changes: 108 additions & 0 deletions src/app/Library/Support/SetupCache.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very clean. And well-named. I like it. We're done here.

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace Backpack\CRUD\app\Library\Support;

use Illuminate\Support\Facades\Cache;

abstract class SetupCache
{
protected string $cachePrefix = 'setup_cache_';
protected int $cacheDuration = 60; // minutes

/**
* Generate a cache key for the given identifier.
*/
protected function generateCacheKey($identifier): string
{
return $this->cachePrefix . $identifier;
}

/**
* Store data in the cache.
*/
public function store($identifier, ...$args)
{
$cacheKey = $this->generateCacheKey($identifier);
$data = $this->prepareDataForStorage(...$args);

if ($data !== false && $data !== null) {
Cache::forget($cacheKey);
Cache::put($cacheKey, $data, now()->addMinutes($this->cacheDuration));
return true;
}

return false;
}

/**
* Apply cached data.
*/
public function apply($identifier, ...$args)
{
$cacheKey = $this->generateCacheKey($identifier);
$cachedData = Cache::get($cacheKey);

if (!$cachedData) {
return false;
}

return $this->applyFromCache($cachedData, ...$args);
}

/**
* Get cached data without applying it.
*/
public function get($identifier)
{
$cacheKey = $this->generateCacheKey($identifier);
return Cache::get($cacheKey);
}

/**
* Check if cache exists for the given identifier.
*/
public function has($identifier): bool
{
$cacheKey = $this->generateCacheKey($identifier);
return Cache::has($cacheKey);
}

/**
* Remove cached data.
*/
public function forget($identifier): bool
{
$cacheKey = $this->generateCacheKey($identifier);
return Cache::forget($cacheKey);
}

/**
* Set the cache prefix.
*/
public function setCachePrefix(string $prefix): self
{
$this->cachePrefix = $prefix;
return $this;
}

/**
* Set the cache duration in minutes.
*/
public function setCacheDuration(int $minutes): self
{
$this->cacheDuration = $minutes;
return $this;
}

/**
* Prepare data for storage in the cache.
* This method should be implemented by child classes.
*/
abstract protected function prepareDataForStorage(...$args);

/**
* Apply data from the cache.
* This method should be implemented by child classes.
*/
abstract protected function applyFromCache($cachedData, ...$args);
}
Loading