Elgg  Version master
CompositeCache.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Cache;
4 
5 use Elgg\Config;
8 use Elgg\Values;
9 use Phpfastcache\CacheManager;
10 use Phpfastcache\Cluster\ClusterAggregator;
11 use Phpfastcache\Config\ConfigurationOption;
12 use Phpfastcache\Core\Pool\ExtendedCacheItemPoolInterface;
13 use Phpfastcache\Exceptions\PhpfastcacheRootException;
14 
20 class CompositeCache extends BaseCache {
21 
22  public const CACHE_BLACK_HOLE = 1;
23  public const CACHE_RUNTIME = 2;
24  public const CACHE_FILESYSTEM = 4;
25  public const CACHE_PERSISTENT = 8;
26  public const CACHE_LOCALFILESYSTEM = 32;
27 
31  protected int $ttl = 86400;
32 
36  protected $pool;
37 
46  public function __construct(protected string $namespace, protected Config $config, protected int $flags, protected bool $validate_lastcache = true) {
47  $this->pool = $this->createPool();
48  }
49 
59  public function save($key, $data, $expire_after = null) {
60  if ($this->disabled) {
61  return false;
62  }
63 
64  $expire_after = $expire_after ?: $this->ttl;
65 
66  $item = $this->pool->getItem($this->sanitizeItemKey($key));
67  $item->set($data);
68 
69  if (is_int($expire_after)) {
70  $item->expiresAfter($expire_after);
71  } elseif ($expire_after instanceof \DateTime) {
72  $item->expiresAt($expire_after);
73  }
74 
75  return $this->pool->save($item);
76  }
77 
85  public function load($key) {
86  if ($this->disabled) {
87  return null;
88  }
89 
90  try {
91  $item = $this->pool->getItem($this->sanitizeItemKey($key));
92  if (!$item->isHit()) {
93  return null;
94  }
95 
96  if ($this->validate_lastcache && $this->config->lastcache) {
97  $expiration_date = Values::normalizeTime($this->config->lastcache);
98 
99  if ($item->getCreationDate()->getTimestamp() < $expiration_date->getTimestamp()) {
100  $this->delete($key);
101  return null;
102  }
103  }
104 
105  return $item->get();
106  } catch (PhpfastcacheRootException $e) {
107  // something wrong with the cache
108  elgg_log($e->getMessage(), \Psr\Log\LogLevel::ERROR);
109  return null;
110  }
111  }
112 
116  public function delete($key) {
117  if ($this->disabled) {
118  return false;
119  }
120 
121  return $this->pool->deleteItem($this->sanitizeItemKey($key));
122  }
123 
127  public function clear() {
128  return $this->pool->clear();
129  }
130 
134  public function invalidate() {
135  // Phpfastcache doesn't have invalidation as an action.
136  // This is handled during load
137  return true;
138  }
139 
143  public function purge() {
144  // Phpfastcache doesn't have purge as an action
145  return true;
146  }
147 
157  public function setNamespace($namespace = 'default'): void {
158  $this->namespace = $namespace;
159  }
160 
166  public function getNamespace(): string {
167  return $this->namespace;
168  }
169 
179  protected function prefixInstanceId(string $id): string {
180  return "{$this->getNamespace()}_{$id}";
181  }
182 
194  protected function sanitizeItemKey($key): string {
195  if (!is_string($key) && !is_int($key)) {
196  throw new InvalidArgumentException('key must be string or integer');
197  }
198 
199  return str_replace(['{', '}', '(', ')', '/', '\\', '@', ':'], '_', "{$key}");
200  }
201 
208  protected function createPool(): ExtendedCacheItemPoolInterface {
209 
210  $drivers = [];
211  $drivers[] = $this->buildRedisDriver();
212  $drivers[] = $this->buildMemcachedDriver();
213  $drivers[] = $this->buildFileSystemDriver();
214  $drivers[] = $this->buildLocalFileSystemDriver();
215  $drivers[] = $this->buildBlackHoleDriver();
216  $drivers = array_filter($drivers);
217 
218  if (empty($drivers)) {
219  // the memory driver can only be used as a stand-alone driver (not combined in a cluster)
220  // other drivers already have a built in memory storage (default allowed in config)
221  $ephemeral = $this->buildEphemeralDriver();
222  if (!empty($ephemeral)) {
223  $drivers[] = $ephemeral;
224  }
225  }
226 
227  if (empty($drivers)) {
228  throw new ConfigurationException('Unable to initialize composite cache without drivers');
229  }
230 
231  if (count($drivers) === 1) {
232  return array_shift($drivers);
233  }
234 
235  $cluster = new ClusterAggregator($this->getNamespace());
236  foreach ($drivers as $driver) {
237  $cluster->aggregateDriver($driver);
238  }
239 
240  $cluster_driver = $cluster->getCluster();
241  $cluster_driver->setConfig(new ConfigurationOption([
242  'useStaticItemCaching' => true,
243  'itemDetailedDate' => true,
244  ]));
245 
246  return $cluster_driver;
247  }
248 
253  protected function buildRedisDriver(): ?ExtendedCacheItemPoolInterface {
254  if (!($this->flags & self::CACHE_PERSISTENT)) {
255  return null;
256  }
257 
258  if (!self::isRedisAvailable()) {
259  return null;
260  }
261 
262  $config = \Elgg\Cache\Config\Redis::fromElggConfig($this->namespace, $this->config);
263  if (empty($config)) {
264  return null;
265  }
266 
267  return CacheManager::getInstance('Redis', $config, $this->prefixInstanceId('redis'));
268  }
269 
274  protected function buildMemcachedDriver(): ?ExtendedCacheItemPoolInterface {
275  if (!($this->flags & self::CACHE_PERSISTENT)) {
276  return null;
277  }
278 
279  if (!self::isMemcacheAvailable()) {
280  return null;
281  }
282 
283  $config = \Elgg\Cache\Config\Memcached::fromElggConfig($this->namespace, $this->config);
284  if (empty($config)) {
285  return null;
286  }
287 
288  return CacheManager::getInstance('Memcached', $config, $this->prefixInstanceId('memcache'));
289  }
290 
295  protected function buildFileSystemDriver(): ?ExtendedCacheItemPoolInterface {
296  if (!($this->flags & self::CACHE_FILESYSTEM)) {
297  return null;
298  }
299 
300  $config = \Elgg\Cache\Config\Files::fromElggConfig($this->namespace, $this->config);
301  if (empty($config)) {
302  return null;
303  }
304 
305  try {
306  return CacheManager::getInstance('Files', $config, $this->prefixInstanceId('files'));
307  } catch (\Phpfastcache\Exceptions\PhpfastcacheIOException $e) {
308  if (!$this->config->installer_running) {
309  elgg_log($e, \Psr\Log\LogLevel::ERROR);
310  }
311  }
312 
313  return null;
314  }
315 
320  protected function buildLocalFileSystemDriver(): ?ExtendedCacheItemPoolInterface {
321  if (!($this->flags & self::CACHE_LOCALFILESYSTEM)) {
322  return null;
323  }
324 
325  $config = \Elgg\Cache\Config\LocalFiles::fromElggConfig($this->namespace, $this->config);
326  if (empty($config)) {
327  return null;
328  }
329 
330  try {
331  return CacheManager::getInstance('Files', $config, $this->prefixInstanceId('local_files'));
332  } catch (\Phpfastcache\Exceptions\PhpfastcacheIOException $e) {
333  if (!$this->config->installer_running) {
334  elgg_log($e, \Psr\Log\LogLevel::ERROR);
335  }
336  }
337 
338  return null;
339  }
340 
345  protected function buildEphemeralDriver(): ?ExtendedCacheItemPoolInterface {
346  if (!($this->flags & self::CACHE_RUNTIME)) {
347  return null;
348  }
349 
350  $config = new \Phpfastcache\Drivers\Memory\Config();
351 
352  $config->setUseStaticItemCaching(true);
353  $config->setItemDetailedDate(true);
354 
355  return CacheManager::getInstance('Memory', $config, $this->prefixInstanceId('memory'));
356  }
357 
362  protected function buildBlackHoleDriver(): ?ExtendedCacheItemPoolInterface {
363  if (!($this->flags & self::CACHE_BLACK_HOLE)) {
364  return null;
365  }
366 
367  $config = new \Phpfastcache\Drivers\Devnull\Config();
368 
369  return CacheManager::getInstance('Devnull', $config, $this->prefixInstanceId('devnull'));
370  }
371 
379  public static function isMemcacheAvailable(): bool {
380  return class_exists('Memcached');
381  }
382 
390  public static function isRedisAvailable(): bool {
391  return extension_loaded('Redis');
392  }
393 }
$id
Generic annotation delete action.
Definition: delete.php:6
if(! $items) $item
Definition: delete.php:13
return[ 'admin/delete_admin_notices'=>['access'=> 'admin'], 'admin/menu/save'=>['access'=> 'admin'], 'admin/plugins/activate'=>['access'=> 'admin'], 'admin/plugins/activate_all'=>['access'=> 'admin'], 'admin/plugins/deactivate'=>['access'=> 'admin'], 'admin/plugins/deactivate_all'=>['access'=> 'admin'], 'admin/plugins/set_priority'=>['access'=> 'admin'], 'admin/security/security_txt'=>['access'=> 'admin'], 'admin/security/settings'=>['access'=> 'admin'], 'admin/security/regenerate_site_secret'=>['access'=> 'admin'], 'admin/site/cache/invalidate'=>['access'=> 'admin'], 'admin/site/flush_cache'=>['access'=> 'admin'], 'admin/site/icons'=>['access'=> 'admin'], 'admin/site/set_maintenance_mode'=>['access'=> 'admin'], 'admin/site/set_robots'=>['access'=> 'admin'], 'admin/site/theme'=>['access'=> 'admin'], 'admin/site/unlock_upgrade'=>['access'=> 'admin'], 'admin/site/settings'=>['access'=> 'admin'], 'admin/upgrade'=>['access'=> 'admin'], 'admin/upgrade/reset'=>['access'=> 'admin'], 'admin/user/ban'=>['access'=> 'admin'], 'admin/user/bulk/ban'=>['access'=> 'admin'], 'admin/user/bulk/delete'=>['access'=> 'admin'], 'admin/user/bulk/unban'=>['access'=> 'admin'], 'admin/user/bulk/validate'=>['access'=> 'admin'], 'admin/user/change_email'=>['access'=> 'admin'], 'admin/user/delete'=>['access'=> 'admin'], 'admin/user/login_as'=>['access'=> 'admin'], 'admin/user/logout_as'=>[], 'admin/user/makeadmin'=>['access'=> 'admin'], 'admin/user/resetpassword'=>['access'=> 'admin'], 'admin/user/removeadmin'=>['access'=> 'admin'], 'admin/user/unban'=>['access'=> 'admin'], 'admin/user/validate'=>['access'=> 'admin'], 'annotation/delete'=>[], 'avatar/upload'=>[], 'comment/save'=>[], 'diagnostics/download'=>['access'=> 'admin'], 'entity/chooserestoredestination'=>[], 'entity/delete'=>[], 'entity/mute'=>[], 'entity/restore'=>[], 'entity/subscribe'=>[], 'entity/trash'=>[], 'entity/unmute'=>[], 'entity/unsubscribe'=>[], 'login'=>['access'=> 'logged_out'], 'logout'=>[], 'notifications/mute'=>['access'=> 'public'], 'plugins/settings/remove'=>['access'=> 'admin'], 'plugins/settings/save'=>['access'=> 'admin'], 'plugins/usersettings/save'=>[], 'register'=>['access'=> 'logged_out', 'middleware'=>[\Elgg\Router\Middleware\RegistrationAllowedGatekeeper::class,],], 'river/delete'=>[], 'settings/notifications'=>[], 'settings/notifications/subscriptions'=>[], 'user/changepassword'=>['access'=> 'public'], 'user/requestnewpassword'=>['access'=> 'public'], 'useradd'=>['access'=> 'admin'], 'usersettings/save'=>[], 'widgets/add'=>[], 'widgets/delete'=>[], 'widgets/move'=>[], 'widgets/save'=>[],]
Definition: actions.php:73
if(! $entity instanceof \ElggUser) $data
Definition: attributes.php:13
The Elgg cache base class.
Definition: BaseCache.php:9
Composite cache pool.
buildFileSystemDriver()
Builds file system driver.
int $ttl
TTL of saved items (default timeout after a day to prevent anything getting too stale)
purge()
{Purge old/stale contents of the cache.bool}
save($key, $data, $expire_after=null)
Save data in a cache.
static isRedisAvailable()
Helper function to check if Redis is available.
createPool()
Create a new cluster/pool of drivers.
clear()
{Clear out all the contents of the cache.bool}
__construct(protected string $namespace, protected Config $config, protected int $flags, protected bool $validate_lastcache=true)
Constructor.
setNamespace($namespace='default')
Set the namespace of this cache.
buildEphemeralDriver()
Builds in-memory driver.
sanitizeItemKey($key)
Sanitizes item key for cache.
getNamespace()
Get the namespace currently defined.
static isMemcacheAvailable()
Helper function to check if memcache is available.
prefixInstanceId(string $id)
Prefixes instance ids with namespace.
buildRedisDriver()
Builds Redis driver.
invalidate()
{Invalidate the contents of the cache.bool}
buildBlackHoleDriver()
Builds null cache driver.
buildMemcachedDriver()
Builds Memcached driver.
load($key)
Load data from the cache using a given key.
buildLocalFileSystemDriver()
Builds local file system driver.
static fromElggConfig(string $namespace, \Elgg\Config $config)
Factory to return a config object to be used when starting a driver.
Definition: Files.php:23
static fromElggConfig(string $namespace, \Elgg\Config $config)
Factory to return a config object to be used when starting a driver.
Definition: LocalFiles.php:23
static fromElggConfig(string $namespace, \Elgg\Config $config)
Factory to return a config object to be used when starting a driver.
Definition: Memcached.php:23
static fromElggConfig(string $namespace, \Elgg\Config $config)
Factory to return a config object to be used when starting a driver.
Definition: Redis.php:23
A generic parent class for Configuration exceptions.
Exception thrown if an argument is not of the expected type.
Extension of the DateTime class to support formatting a date using the locale.
Definition: DateTime.php:12
Functions for use as event handlers or other situations where you need a globally accessible callable...
Definition: Values.php:13
static normalizeTime($time)
Returns DateTime object based on time representation.
Definition: Values.php:75
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
if($item instanceof \ElggEntity) elseif($item instanceof \ElggRiverItem) elseif($item instanceof \ElggRelationship) elseif(is_callable([ $item, 'getType']))
Definition: item.php:48
elgg_log($message, $level=\Psr\Log\LogLevel::NOTICE)
Log a message.
Definition: elgglib.php:88
if($container instanceof ElggGroup && $container->guid !=elgg_get_page_owner_guid()) $key
Definition: summary.php:44
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10