diff --git a/config/vufind/config.ini b/config/vufind/config.ini index cfac1e0e703..1fe79509fe2 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -312,6 +312,16 @@ driver = Sample ; library_cards setting (see below). allowUserLogin = true +; ILS data cache life time in seconds per function. The cache is used to avoid +; repeating requests too often (but often enough to not use stale data). +; Currently supported for the functions listed below. Default is 60 seconds. +; Set to 0 to disable caching. +;cacheLifeTime[patronLogin] = 60 +;cacheLifeTime[getProxiedUsers] = 60 +;cacheLifeTime[getProxyingUsers] = 60 +;cacheLifeTime[getPickUpLocations] = 60 +;cacheLifeTime[getPurchaseHistory] = 60 + ; loadNoILSOnFailure - Whether or not to load the NoILS driver if the main driver fails loadNoILSOnFailure = false diff --git a/module/VuFind/src/VuFind/ILS/Connection.php b/module/VuFind/src/VuFind/ILS/Connection.php index 0767d7ba4cf..60f5c88a45d 100644 --- a/module/VuFind/src/VuFind/ILS/Connection.php +++ b/module/VuFind/src/VuFind/ILS/Connection.php @@ -34,6 +34,7 @@ namespace VuFind\ILS; use Laminas\Log\LoggerAwareInterface; +use Laminas\Session\Container; use VuFind\Exception\BadConfig; use VuFind\Exception\ILS as ILSException; use VuFind\I18n\Translator\TranslatorAwareInterface; @@ -64,6 +65,10 @@ */ class Connection implements TranslatorAwareInterface, LoggerAwareInterface { + use \VuFind\Cache\CacheTrait { + getCachedData as getSharedCachedData; + putCachedData as putSharedCachedData; + } use \VuFind\I18n\Translator\TranslatorAwareTrait; use \VuFind\Log\LoggerAwareTrait; @@ -130,6 +135,39 @@ class Connection implements TranslatorAwareInterface, LoggerAwareInterface */ protected $request; + /** + * Cache life time per method + * + * @var array + */ + protected $cacheLifeTime = [ + 'patronLogin' => 60, + 'getProxiedUsers' => 60, + 'getProxyingUsers' => 60, + 'getPickUpLocations' => 60, + 'getPurchaseHistory' => 60, + ]; + + /** + * Cache storage per method + * + * @var array + */ + protected $cacheStorage = [ + 'patronLogin' => 'session', + 'getProxiedUsers' => 'session', + 'getProxyingUsers' => 'session', + 'getPickUpLocations' => 'session', + 'getPurchaseHistory' => 'shared', + ]; + + /** + * Session cache + * + * @var Container + */ + protected $sessionCache = null; + /** * Constructor * @@ -171,6 +209,31 @@ public function setHoldConfig($settings) return $this; } + /** + * Set session container for cache. + * + * @param Container $container Session container + * + * @return Connection + */ + public function setSessionCache(Container $container) + { + $this->sessionCache = $container; + return $this; + } + + /** + * Set cache lifetime settings + * + * @param array $settings Lifetime settings + * + * @return void + */ + public function setCacheLifeTime(array $settings): void + { + $this->cacheLifeTime = array_merge($this->cacheLifeTime, $settings); + } + /** * Get class name of the driver object. * @@ -1198,9 +1261,7 @@ public function getStatusParser() } /** - * Default method -- pass along calls to the driver if available; return - * false otherwise. This allows custom functions to be implemented in - * the driver without constant modification to the connection class. + * Call an ILS method with failover to NoILS if configured. * * @param string $methodName The name of the called method. * @param array $params Array of passed parameters. @@ -1208,7 +1269,7 @@ public function getStatusParser() * @throws ILSException * @return mixed Varies by method (false if undefined method) */ - public function __call($methodName, $params) + public function callIlsWithFailover($methodName, $params) { try { if ($this->checkCapability($methodName, $params)) { @@ -1227,4 +1288,82 @@ public function __call($methodName, $params) 'Cannot call method: ' . $this->getDriverClass() . '::' . $methodName ); } + + /** + * Get data for an ILS method from shared or session cache + * + * @param string $methodName The name of the called method. + * @param array $params Array of passed parameters. + * + * @return ?array + */ + protected function getCachedData($methodName, $params) + { + $cacheLifeTime = $this->cacheLifeTime[$methodName] ?? null; + $cacheStorage = $this->cacheStorage[$methodName] ?? null; + if (!$cacheLifeTime || !$cacheStorage) { + return null; + } + $cacheKey = $methodName . md5(serialize($params)); + if ('shared' === $cacheStorage) { + return $this->getSharedCachedData($cacheKey); + } + if ($this->sessionCache && ($entry = $this->sessionCache[$cacheKey] ?? null)) { + if (time() - $entry['ts'] <= $cacheLifeTime) { + return $entry['data']; + } + unset($this->sessionCache[$cacheKey]); + } + return null; + } + + /** + * Put data for an ILS method to shared or session cache. + * + * @param string $methodName The name of the called method. + * @param array $params Array of passed parameters. + * @param mixed $data Data to cache + * + * @return void + */ + protected function putCachedData($methodName, $params, $data): void + { + $cacheLifeTime = $this->cacheLifeTime[$methodName] ?? null; + $cacheStorage = $this->cacheStorage[$methodName] ?? null; + if (!$cacheLifeTime || !$cacheStorage) { + return; + } + $cacheKey = $methodName . md5(serialize($params)); + if ('shared' === $cacheStorage) { + $this->putSharedCachedData($cacheKey, $data); + return; + } + if ($this->sessionCache) { + $this->sessionCache[$cacheKey] = [ + 'ts' => time(), + 'data' => $data, + ]; + } + } + + /** + * Default method -- pass along calls to the driver if available; return + * false otherwise. This allows custom functions to be implemented in + * the driver without constant modification to the connection class. + * + * @param string $methodName The name of the called method. + * @param array $params Array of passed parameters. + * + * @throws ILSException + * @return mixed Varies by method (false if undefined method) + */ + public function __call($methodName, $params) + { + if ($entry = $this->getCachedData($methodName, $params)) { + return $entry['data']; + } + $data = $this->callIlsWithFailover($methodName, $params); + $this->putCachedData($methodName, $params, compact('data')); + return $data; + } } diff --git a/module/VuFind/src/VuFind/ILS/ConnectionFactory.php b/module/VuFind/src/VuFind/ILS/ConnectionFactory.php index 9521d8c35b4..635dc28024a 100644 --- a/module/VuFind/src/VuFind/ILS/ConnectionFactory.php +++ b/module/VuFind/src/VuFind/ILS/ConnectionFactory.php @@ -69,15 +69,23 @@ public function __invoke( throw new \Exception('Unexpected options sent to factory.'); } $configManager = $container->get(\VuFind\Config\PluginManager::class); + $config = $configManager->get('config'); $request = $container->get('Request'); $catalog = new $requestedName( - $configManager->get('config')->Catalog, + $config->Catalog, $container->get(\VuFind\ILS\Driver\PluginManager::class), $container->get(\VuFind\Config\PluginManager::class), $request instanceof \Laminas\Http\Request ? $request : null ); - return $catalog->setHoldConfig( + $catalog->setHoldConfig( $container->get(\VuFind\ILS\HoldSettings::class) ); + $catalog->setCacheStorage($container->get(\VuFind\Cache\Manager::class)->getCache('object')); + $manager = $container->get(\Laminas\Session\SessionManager::class); + $catalog->setSessionCache(new \Laminas\Session\Container('ILS', $manager)); + if ($cacheLifeTime = $config->Catalog?->cacheLifeTime?->toArray()) { + $catalog->setCacheLifeTime($cacheLifeTime); + } + return $catalog; } }