Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * The library file for the session cache store.
  19   *
  20   * This file is part of the session cache store, it contains the API for interacting with an instance of the store.
  21   * This is used as a default cache store within the Cache API. It should never be deleted.
  22   *
  23   * @package    cachestore_session
  24   * @category   cache
  25   * @copyright  2012 Sam Hemelryk
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /**
  32   * The session data store class.
  33   *
  34   * @copyright  2012 Sam Hemelryk
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  abstract class session_data_store extends cache_store {
  38  
  39      /**
  40       * Used for the actual storage.
  41       * @var array
  42       */
  43      private static $sessionstore = null;
  44  
  45      /**
  46       * Returns a static store by reference... REFERENCE SUPER IMPORTANT.
  47       *
  48       * @param string $id
  49       * @return array
  50       */
  51      protected static function &register_store_id($id) {
  52          if (is_null(self::$sessionstore)) {
  53              global $SESSION;
  54              if (!isset($SESSION->cachestore_session)) {
  55                  $SESSION->cachestore_session = array();
  56              }
  57              self::$sessionstore =& $SESSION->cachestore_session;
  58          }
  59          if (!array_key_exists($id, self::$sessionstore)) {
  60              self::$sessionstore[$id] = array();
  61          }
  62          return self::$sessionstore[$id];
  63      }
  64  
  65      /**
  66       * Flushes the data belong to the given store id.
  67       * @param string $id
  68       */
  69      protected static function flush_store_by_id($id) {
  70          unset(self::$sessionstore[$id]);
  71          self::$sessionstore[$id] = array();
  72      }
  73  
  74      /**
  75       * Flushes the store of all data.
  76       */
  77      protected static function flush_store() {
  78          $ids = array_keys(self::$sessionstore);
  79          unset(self::$sessionstore);
  80          self::$sessionstore = array();
  81          foreach ($ids as $id) {
  82              self::$sessionstore[$id] = array();
  83          }
  84      }
  85  }
  86  
  87  /**
  88   * The Session store class.
  89   *
  90   * @copyright  2012 Sam Hemelryk
  91   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  92   */
  93  class cachestore_session extends session_data_store implements cache_is_key_aware, cache_is_searchable {
  94  
  95      /**
  96       * The name of the store
  97       * @var store
  98       */
  99      protected $name;
 100  
 101      /**
 102       * The store id (should be unique)
 103       * @var string
 104       */
 105      protected $storeid;
 106  
 107      /**
 108       * The store we use for data.
 109       * @var array
 110       */
 111      protected $store;
 112  
 113      /**
 114       * The ttl if there is one. Hopefully not.
 115       * @var int
 116       */
 117      protected $ttl = 0;
 118  
 119      /**
 120       * The maximum size for the store, or false if there isn't one.
 121       * @var bool|int
 122       */
 123      protected $maxsize = false;
 124  
 125      /**
 126       * The number of items currently being stored.
 127       * @var int
 128       */
 129      protected $storecount = 0;
 130  
 131      /**
 132       * Constructs the store instance.
 133       *
 134       * Noting that this function is not an initialisation. It is used to prepare the store for use.
 135       * The store will be initialised when required and will be provided with a cache_definition at that time.
 136       *
 137       * @param string $name
 138       * @param array $configuration
 139       */
 140      public function __construct($name, array $configuration = array()) {
 141          $this->name = $name;
 142      }
 143  
 144      /**
 145       * Returns the supported features as a combined int.
 146       *
 147       * @param array $configuration
 148       * @return int
 149       */
 150      public static function get_supported_features(array $configuration = array()) {
 151          return self::SUPPORTS_DATA_GUARANTEE +
 152                 self::SUPPORTS_NATIVE_TTL +
 153                 self::IS_SEARCHABLE;
 154      }
 155  
 156      /**
 157       * Returns false as this store does not support multiple identifiers.
 158       * (This optional function is a performance optimisation; it must be
 159       * consistent with the value from get_supported_features.)
 160       *
 161       * @return bool False
 162       */
 163      public function supports_multiple_identifiers() {
 164          return false;
 165      }
 166  
 167      /**
 168       * Returns the supported modes as a combined int.
 169       *
 170       * @param array $configuration
 171       * @return int
 172       */
 173      public static function get_supported_modes(array $configuration = array()) {
 174          return self::MODE_SESSION;
 175      }
 176  
 177      /**
 178       * Returns true if the store requirements are met.
 179       *
 180       * @return bool
 181       */
 182      public static function are_requirements_met() {
 183          return true;
 184      }
 185  
 186      /**
 187       * Returns true if the given mode is supported by this store.
 188       *
 189       * @param int $mode One of cache_store::MODE_*
 190       * @return bool
 191       */
 192      public static function is_supported_mode($mode) {
 193          return ($mode === self::MODE_SESSION);
 194      }
 195  
 196      /**
 197       * Initialises the cache.
 198       *
 199       * Once this has been done the cache is all set to be used.
 200       *
 201       * @param cache_definition $definition
 202       */
 203      public function initialise(cache_definition $definition) {
 204          $this->storeid = $definition->generate_definition_hash();
 205          $this->store = &self::register_store_id($this->name.'-'.$definition->get_id());
 206          $this->ttl = $definition->get_ttl();
 207          $maxsize = $definition->get_maxsize();
 208          if ($maxsize !== null) {
 209              // Must be a positive int.
 210              $this->maxsize = abs((int)$maxsize);
 211              $this->storecount = count($this->store);
 212          }
 213          $this->check_ttl();
 214      }
 215  
 216      /**
 217       * Returns true once this instance has been initialised.
 218       *
 219       * @return bool
 220       */
 221      public function is_initialised() {
 222          return (is_array($this->store));
 223      }
 224  
 225      /**
 226       * Retrieves an item from the cache store given its key.
 227       *
 228       * @param string $key The key to retrieve
 229       * @return mixed The data that was associated with the key, or false if the key did not exist.
 230       */
 231      public function get($key) {
 232          if (isset($this->store[$key])) {
 233              if ($this->ttl == 0) {
 234                  $value = $this->store[$key][0];
 235                  if ($this->maxsize !== false) {
 236                      // Make sure the element is now in the end of array.
 237                      $this->set($key, $value);
 238                  }
 239                  return $value;
 240              } else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) {
 241                  return $this->store[$key][0];
 242              } else {
 243                  // Element is present but has expired.
 244                  $this->check_ttl();
 245              }
 246          }
 247          return false;
 248      }
 249  
 250      /**
 251       * Retrieves several items from the cache store in a single transaction.
 252       *
 253       * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
 254       *
 255       * @param array $keys The array of keys to retrieve
 256       * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
 257       *      be set to false.
 258       */
 259      public function get_many($keys) {
 260          $return = array();
 261          $maxtime = 0;
 262          if ($this->ttl != 0) {
 263              $maxtime = cache::now() - $this->ttl;
 264          }
 265  
 266          $hasexpiredelements = false;
 267          foreach ($keys as $key) {
 268              $return[$key] = false;
 269              if (isset($this->store[$key])) {
 270                  if ($this->ttl == 0) {
 271                      $return[$key] = $this->store[$key][0];
 272                      if ($this->maxsize !== false) {
 273                          // Make sure the element is now in the end of array.
 274                          $this->set($key, $return[$key], false);
 275                      }
 276                  } else if ($this->store[$key][1] >= $maxtime) {
 277                      $return[$key] = $this->store[$key][0];
 278                  } else {
 279                      $hasexpiredelements = true;
 280                  }
 281              }
 282          }
 283          if ($hasexpiredelements) {
 284              // There are some elements that are present but have expired.
 285              $this->check_ttl();
 286          }
 287          return $return;
 288      }
 289  
 290      /**
 291       * Sets an item in the cache given its key and data value.
 292       *
 293       * @param string $key The key to use.
 294       * @param mixed $data The data to set.
 295       * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required. If this is set to false you will
 296       *      need to perform these checks yourself. This allows for bulk set's to be performed and maxsize tests performed once.
 297       * @return bool True if the operation was a success false otherwise.
 298       */
 299      public function set($key, $data, $testmaxsize = true) {
 300          $testmaxsize = ($testmaxsize && $this->maxsize !== false);
 301          $increment = $this->maxsize !== false && !isset($this->store[$key]);
 302          if (($this->maxsize !== false && !$increment) || $this->ttl != 0) {
 303              // Make sure the element is added to the end of $this->store array.
 304              unset($this->store[$key]);
 305          }
 306          if ($this->ttl === 0) {
 307              $this->store[$key] = array($data, 0);
 308          } else {
 309              $this->store[$key] = array($data, cache::now());
 310          }
 311          if ($increment) {
 312              $this->storecount++;
 313          }
 314          if ($testmaxsize && $this->storecount > $this->maxsize) {
 315              $this->reduce_for_maxsize();
 316          }
 317          return true;
 318      }
 319  
 320      /**
 321       * Sets many items in the cache in a single transaction.
 322       *
 323       * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
 324       *      keys, 'key' and 'value'.
 325       * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
 326       *      sent ... if they care that is.
 327       */
 328      public function set_many(array $keyvaluearray) {
 329          $count = 0;
 330          $increment = 0;
 331          foreach ($keyvaluearray as $pair) {
 332              $key = $pair['key'];
 333              $data = $pair['value'];
 334              $count++;
 335              if ($this->maxsize !== false || $this->ttl !== 0) {
 336                  // Make sure the element is added to the end of $this->store array.
 337                  $this->delete($key);
 338                  $increment++;
 339              } else if (!isset($this->store[$key])) {
 340                  $increment++;
 341              }
 342              if ($this->ttl === 0) {
 343                  $this->store[$key] = array($data, 0);
 344              } else {
 345                  $this->store[$key] = array($data, cache::now());
 346              }
 347          }
 348          if ($this->maxsize !== false) {
 349              $this->storecount += $increment;
 350              if ($this->storecount > $this->maxsize) {
 351                  $this->reduce_for_maxsize();
 352              }
 353          }
 354          return $count;
 355      }
 356  
 357      /**
 358       * Checks if the store has a record for the given key and returns true if so.
 359       *
 360       * @param string $key
 361       * @return bool
 362       */
 363      public function has($key) {
 364          if (isset($this->store[$key])) {
 365              if ($this->ttl == 0) {
 366                  return true;
 367              } else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) {
 368                  return true;
 369              }
 370          }
 371          return false;
 372      }
 373  
 374      /**
 375       * Returns true if the store contains records for all of the given keys.
 376       *
 377       * @param array $keys
 378       * @return bool
 379       */
 380      public function has_all(array $keys) {
 381          $maxtime = 0;
 382          if ($this->ttl != 0) {
 383              $maxtime = cache::now() - $this->ttl;
 384          }
 385  
 386          foreach ($keys as $key) {
 387              if (!isset($this->store[$key])) {
 388                  return false;
 389              }
 390              if ($this->ttl != 0 && $this->store[$key][1] < $maxtime) {
 391                  return false;
 392              }
 393          }
 394          return true;
 395      }
 396  
 397      /**
 398       * Returns true if the store contains records for any of the given keys.
 399       *
 400       * @param array $keys
 401       * @return bool
 402       */
 403      public function has_any(array $keys) {
 404          $maxtime = 0;
 405          if ($this->ttl != 0) {
 406              $maxtime = cache::now() - $this->ttl;
 407          }
 408  
 409          foreach ($keys as $key) {
 410              if (isset($this->store[$key]) && ($this->ttl == 0 || $this->store[$key][1] >= $maxtime)) {
 411                  return true;
 412              }
 413          }
 414          return false;
 415      }
 416  
 417      /**
 418       * Deletes an item from the cache store.
 419       *
 420       * @param string $key The key to delete.
 421       * @return bool Returns true if the operation was a success, false otherwise.
 422       */
 423      public function delete($key) {
 424          if (!isset($this->store[$key])) {
 425              return false;
 426          }
 427          unset($this->store[$key]);
 428          if ($this->maxsize !== false) {
 429              $this->storecount--;
 430          }
 431          return true;
 432      }
 433  
 434      /**
 435       * Deletes several keys from the cache in a single action.
 436       *
 437       * @param array $keys The keys to delete
 438       * @return int The number of items successfully deleted.
 439       */
 440      public function delete_many(array $keys) {
 441          // The number of items that have actually being removed.
 442          $reduction = 0;
 443          foreach ($keys as $key) {
 444              if (isset($this->store[$key])) {
 445                  $reduction++;
 446              }
 447              unset($this->store[$key]);
 448          }
 449          if ($this->maxsize !== false) {
 450              $this->storecount -= $reduction;
 451          }
 452          return $reduction;
 453      }
 454  
 455      /**
 456       * Purges the cache deleting all items within it.
 457       *
 458       * @return boolean True on success. False otherwise.
 459       */
 460      public function purge() {
 461          $this->store = array();
 462          // Don't worry about checking if we're using max size just set it as thats as fast as the check.
 463          $this->storecount = 0;
 464          return true;
 465      }
 466  
 467      /**
 468       * Reduces the size of the array if maxsize has been hit.
 469       *
 470       * This function reduces the size of the store reducing it by 10% of its maxsize.
 471       * It removes the oldest items in the store when doing this.
 472       * The reason it does this an doesn't use a least recently used system is purely the overhead such a system
 473       * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
 474       * and avoiding more is of benefit.
 475       *
 476       * @return int
 477       */
 478      protected function reduce_for_maxsize() {
 479          $diff = $this->storecount - $this->maxsize;
 480          if ($diff < 1) {
 481              return 0;
 482          }
 483          // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
 484          $diff += floor($this->maxsize / 10);
 485          $this->store = array_slice($this->store, $diff, null, true);
 486          $this->storecount -= $diff;
 487          return $diff;
 488      }
 489  
 490      /**
 491       * Returns true if the user can add an instance of the store plugin.
 492       *
 493       * @return bool
 494       */
 495      public static function can_add_instance() {
 496          return false;
 497      }
 498  
 499      /**
 500       * Performs any necessary clean up when the store instance is being deleted.
 501       */
 502      public function instance_deleted() {
 503          $this->purge();
 504      }
 505  
 506      /**
 507       * Generates an instance of the cache store that can be used for testing.
 508       *
 509       * @param cache_definition $definition
 510       * @return cachestore_session
 511       */
 512      public static function initialise_test_instance(cache_definition $definition) {
 513          // Do something here perhaps.
 514          $cache = new cachestore_session('Session test');
 515          $cache->initialise($definition);
 516          return $cache;
 517      }
 518  
 519      /**
 520       * Generates the appropriate configuration required for unit testing.
 521       *
 522       * @return array Array of unit test configuration data to be used by initialise().
 523       */
 524      public static function unit_test_configuration() {
 525          return array();
 526      }
 527      /**
 528       * Returns the name of this instance.
 529       * @return string
 530       */
 531      public function my_name() {
 532          return $this->name;
 533      }
 534  
 535      /**
 536       * Removes expired elements.
 537       * @return int number of removed elements
 538       */
 539      protected function check_ttl() {
 540          if ($this->ttl === 0) {
 541              return 0;
 542          }
 543          $maxtime = cache::now() - $this->ttl;
 544          $count = 0;
 545          for ($value = reset($this->store); $value !== false; $value = next($this->store)) {
 546              if ($value[1] >= $maxtime) {
 547                  // We know that elements are sorted by ttl so no need to continue.
 548                  break;
 549              }
 550              $count++;
 551          }
 552          if ($count) {
 553              // Remove first $count elements as they are expired.
 554              $this->store = array_slice($this->store, $count, null, true);
 555              if ($this->maxsize !== false) {
 556                  $this->storecount -= $count;
 557              }
 558          }
 559          return $count;
 560      }
 561  
 562      /**
 563       * Finds all of the keys being stored in the cache store instance.
 564       *
 565       * @return array
 566       */
 567      public function find_all() {
 568          $this->check_ttl();
 569          return array_keys($this->store);
 570      }
 571  
 572      /**
 573       * Finds all of the keys whose keys start with the given prefix.
 574       *
 575       * @param string $prefix
 576       * @return array An array of keys.
 577       */
 578      public function find_by_prefix($prefix) {
 579          $return = array();
 580          foreach ($this->find_all() as $key) {
 581              if (strpos($key, $prefix) === 0) {
 582                  $return[] = $key;
 583              }
 584          }
 585          return $return;
 586      }
 587  
 588      /**
 589       * This store supports native TTL handling.
 590       * @return bool
 591       */
 592      public function store_supports_native_ttl() {
 593          return true;
 594      }
 595  }