Chris@0: bin = $bin; Chris@0: $this->connection = $connection; Chris@0: $this->checksumProvider = $checksum_provider; Chris@0: $this->maxRows = $max_rows === NULL ? static::DEFAULT_MAX_ROWS : $max_rows; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function get($cid, $allow_invalid = FALSE) { Chris@0: $cids = [$cid]; Chris@0: $cache = $this->getMultiple($cids, $allow_invalid); Chris@0: return reset($cache); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getMultiple(&$cids, $allow_invalid = FALSE) { Chris@0: $cid_mapping = []; Chris@0: foreach ($cids as $cid) { Chris@0: $cid_mapping[$this->normalizeCid($cid)] = $cid; Chris@0: } Chris@0: // When serving cached pages, the overhead of using ::select() was found Chris@0: // to add around 30% overhead to the request. Since $this->bin is a Chris@0: // variable, this means the call to ::query() here uses a concatenated Chris@0: // string. This is highly discouraged under any other circumstances, and Chris@0: // is used here only due to the performance overhead we would incur Chris@0: // otherwise. When serving an uncached page, the overhead of using Chris@0: // ::select() is a much smaller proportion of the request. Chris@0: $result = []; Chris@0: try { Chris@0: $result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN ( :cids[] ) ORDER BY cid', [':cids[]' => array_keys($cid_mapping)]); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: // Nothing to do. Chris@0: } Chris@0: $cache = []; Chris@0: foreach ($result as $item) { Chris@0: // Map the cache ID back to the original. Chris@0: $item->cid = $cid_mapping[$item->cid]; Chris@0: $item = $this->prepareItem($item, $allow_invalid); Chris@0: if ($item) { Chris@0: $cache[$item->cid] = $item; Chris@0: } Chris@0: } Chris@0: $cids = array_diff($cids, array_keys($cache)); Chris@0: return $cache; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares a cached item. Chris@0: * Chris@0: * Checks that items are either permanent or did not expire, and unserializes Chris@0: * data as appropriate. Chris@0: * Chris@0: * @param object $cache Chris@18: * An item loaded from self::get() or self::getMultiple(). Chris@0: * @param bool $allow_invalid Chris@0: * If FALSE, the method returns FALSE if the cache item is not valid. Chris@0: * Chris@0: * @return mixed|false Chris@0: * The item with data unserialized as appropriate and a property indicating Chris@0: * whether the item is valid, or FALSE if there is no valid item to load. Chris@0: */ Chris@0: protected function prepareItem($cache, $allow_invalid) { Chris@0: if (!isset($cache->data)) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: $cache->tags = $cache->tags ? explode(' ', $cache->tags) : []; Chris@0: Chris@0: // Check expire time. Chris@0: $cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME; Chris@0: Chris@0: // Check if invalidateTags() has been called with any of the items's tags. Chris@0: if (!$this->checksumProvider->isValid($cache->checksum, $cache->tags)) { Chris@0: $cache->valid = FALSE; Chris@0: } Chris@0: Chris@0: if (!$allow_invalid && !$cache->valid) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: // Unserialize and return the cached data. Chris@0: if ($cache->serialized) { Chris@0: $cache->data = unserialize($cache->data); Chris@0: } Chris@0: Chris@0: return $cache; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { Chris@0: $this->setMultiple([ Chris@0: $cid => [ Chris@0: 'data' => $data, Chris@0: 'expire' => $expire, Chris@0: 'tags' => $tags, Chris@0: ], Chris@0: ]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function setMultiple(array $items) { Chris@0: $try_again = FALSE; Chris@0: try { Chris@0: // The bin might not yet exist. Chris@0: $this->doSetMultiple($items); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: // If there was an exception, try to create the bins. Chris@0: if (!$try_again = $this->ensureBinExists()) { Chris@0: // If the exception happened for other reason than the missing bin Chris@0: // table, propagate the exception. Chris@0: throw $e; Chris@0: } Chris@0: } Chris@0: // Now that the bin has been created, try again if necessary. Chris@0: if ($try_again) { Chris@0: $this->doSetMultiple($items); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Stores multiple items in the persistent cache. Chris@0: * Chris@0: * @param array $items Chris@0: * An array of cache items, keyed by cid. Chris@0: * Chris@0: * @see \Drupal\Core\Cache\CacheBackendInterface::setMultiple() Chris@0: */ Chris@0: protected function doSetMultiple(array $items) { Chris@0: $values = []; Chris@0: Chris@0: foreach ($items as $cid => $item) { Chris@0: $item += [ Chris@0: 'expire' => CacheBackendInterface::CACHE_PERMANENT, Chris@0: 'tags' => [], Chris@0: ]; Chris@0: Chris@14: assert(Inspector::assertAllStrings($item['tags']), 'Cache Tags must be strings.'); Chris@0: $item['tags'] = array_unique($item['tags']); Chris@0: // Sort the cache tags so that they are stored consistently in the DB. Chris@0: sort($item['tags']); Chris@0: Chris@0: $fields = [ Chris@0: 'cid' => $this->normalizeCid($cid), Chris@0: 'expire' => $item['expire'], Chris@0: 'created' => round(microtime(TRUE), 3), Chris@0: 'tags' => implode(' ', $item['tags']), Chris@0: 'checksum' => $this->checksumProvider->getCurrentChecksum($item['tags']), Chris@0: ]; Chris@0: Chris@0: if (!is_string($item['data'])) { Chris@0: $fields['data'] = serialize($item['data']); Chris@0: $fields['serialized'] = 1; Chris@0: } Chris@0: else { Chris@0: $fields['data'] = $item['data']; Chris@0: $fields['serialized'] = 0; Chris@0: } Chris@0: $values[] = $fields; Chris@0: } Chris@0: Chris@0: // Use an upsert query which is atomic and optimized for multiple-row Chris@0: // merges. Chris@0: $query = $this->connection Chris@0: ->upsert($this->bin) Chris@0: ->key('cid') Chris@0: ->fields(['cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized']); Chris@0: foreach ($values as $fields) { Chris@0: // Only pass the values since the order of $fields matches the order of Chris@0: // the insert fields. This is a performance optimization to avoid Chris@0: // unnecessary loops within the method. Chris@0: $query->values(array_values($fields)); Chris@0: } Chris@0: Chris@0: $query->execute(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function delete($cid) { Chris@0: $this->deleteMultiple([$cid]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function deleteMultiple(array $cids) { Chris@0: $cids = array_values(array_map([$this, 'normalizeCid'], $cids)); Chris@0: try { Chris@0: // Delete in chunks when a large array is passed. Chris@0: foreach (array_chunk($cids, 1000) as $cids_chunk) { Chris@0: $this->connection->delete($this->bin) Chris@0: ->condition('cid', $cids_chunk, 'IN') Chris@0: ->execute(); Chris@0: } Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: // Create the cache table, which will be empty. This fixes cases during Chris@0: // core install where a cache table is cleared before it is set Chris@0: // with {cache_render} and {cache_data}. Chris@0: if (!$this->ensureBinExists()) { Chris@0: $this->catchException($e); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function deleteAll() { Chris@0: try { Chris@0: $this->connection->truncate($this->bin)->execute(); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: // Create the cache table, which will be empty. This fixes cases during Chris@0: // core install where a cache table is cleared before it is set Chris@0: // with {cache_render} and {cache_data}. Chris@0: if (!$this->ensureBinExists()) { Chris@0: $this->catchException($e); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function invalidate($cid) { Chris@0: $this->invalidateMultiple([$cid]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function invalidateMultiple(array $cids) { Chris@0: $cids = array_values(array_map([$this, 'normalizeCid'], $cids)); Chris@0: try { Chris@0: // Update in chunks when a large array is passed. Chris@0: foreach (array_chunk($cids, 1000) as $cids_chunk) { Chris@0: $this->connection->update($this->bin) Chris@0: ->fields(['expire' => REQUEST_TIME - 1]) Chris@0: ->condition('cid', $cids_chunk, 'IN') Chris@0: ->execute(); Chris@0: } Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: $this->catchException($e); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function invalidateAll() { Chris@0: try { Chris@0: $this->connection->update($this->bin) Chris@0: ->fields(['expire' => REQUEST_TIME - 1]) Chris@0: ->execute(); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: $this->catchException($e); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function garbageCollection() { Chris@0: try { Chris@0: // Bounded size cache bin, using FIFO. Chris@0: if ($this->maxRows !== static::MAXIMUM_NONE) { Chris@0: $first_invalid_create_time = $this->connection->select($this->bin) Chris@0: ->fields($this->bin, ['created']) Chris@0: ->orderBy("{$this->bin}.created", 'DESC') Chris@0: ->range($this->maxRows, $this->maxRows + 1) Chris@0: ->execute() Chris@0: ->fetchField(); Chris@0: Chris@0: if ($first_invalid_create_time) { Chris@0: $this->connection->delete($this->bin) Chris@0: ->condition('created', $first_invalid_create_time, '<=') Chris@0: ->execute(); Chris@0: } Chris@0: } Chris@0: Chris@0: $this->connection->delete($this->bin) Chris@0: ->condition('expire', Cache::PERMANENT, '<>') Chris@0: ->condition('expire', REQUEST_TIME, '<') Chris@0: ->execute(); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: // If the table does not exist, it surely does not have garbage in it. Chris@0: // If the table exists, the next garbage collection will clean up. Chris@0: // There is nothing to do. Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function removeBin() { Chris@0: try { Chris@0: $this->connection->schema()->dropTable($this->bin); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: $this->catchException($e); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Check if the cache bin exists and create it if not. Chris@0: */ Chris@0: protected function ensureBinExists() { Chris@0: try { Chris@0: $database_schema = $this->connection->schema(); Chris@0: if (!$database_schema->tableExists($this->bin)) { Chris@0: $schema_definition = $this->schemaDefinition(); Chris@0: $database_schema->createTable($this->bin, $schema_definition); Chris@0: return TRUE; Chris@0: } Chris@0: } Chris@0: // If another process has already created the cache table, attempting to Chris@0: // recreate it will throw an exception. In this case just catch the Chris@0: // exception and do nothing. Chris@0: catch (SchemaObjectExistsException $e) { Chris@0: return TRUE; Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Act on an exception when cache might be stale. Chris@0: * Chris@0: * If the table does not yet exist, that's fine, but if the table exists and Chris@0: * yet the query failed, then the cache is stale and the exception needs to Chris@0: * propagate. Chris@0: * Chris@0: * @param $e Chris@0: * The exception. Chris@0: * @param string|null $table_name Chris@0: * The table name. Defaults to $this->bin. Chris@0: * Chris@0: * @throws \Exception Chris@0: */ Chris@0: protected function catchException(\Exception $e, $table_name = NULL) { Chris@0: if ($this->connection->schema()->tableExists($table_name ?: $this->bin)) { Chris@0: throw $e; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Normalizes a cache ID in order to comply with database limitations. Chris@0: * Chris@0: * @param string $cid Chris@0: * The passed in cache ID. Chris@0: * Chris@0: * @return string Chris@0: * An ASCII-encoded cache ID that is at most 255 characters long. Chris@0: */ Chris@0: protected function normalizeCid($cid) { Chris@0: // Nothing to do if the ID is a US ASCII string of 255 characters or less. Chris@0: $cid_is_ascii = mb_check_encoding($cid, 'ASCII'); Chris@0: if (strlen($cid) <= 255 && $cid_is_ascii) { Chris@0: return $cid; Chris@0: } Chris@0: // Return a string that uses as much as possible of the original cache ID Chris@0: // with the hash appended. Chris@0: $hash = Crypt::hashBase64($cid); Chris@0: if (!$cid_is_ascii) { Chris@0: return $hash; Chris@0: } Chris@0: return substr($cid, 0, 255 - strlen($hash)) . $hash; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Defines the schema for the {cache_*} bin tables. Chris@0: * Chris@0: * @internal Chris@0: */ Chris@0: public function schemaDefinition() { Chris@0: $schema = [ Chris@0: 'description' => 'Storage for the cache API.', Chris@0: 'fields' => [ Chris@0: 'cid' => [ Chris@0: 'description' => 'Primary Key: Unique cache ID.', Chris@0: 'type' => 'varchar_ascii', Chris@0: 'length' => 255, Chris@0: 'not null' => TRUE, Chris@0: 'default' => '', Chris@0: 'binary' => TRUE, Chris@0: ], Chris@0: 'data' => [ Chris@0: 'description' => 'A collection of data to cache.', Chris@0: 'type' => 'blob', Chris@0: 'not null' => FALSE, Chris@0: 'size' => 'big', Chris@0: ], Chris@0: 'expire' => [ Chris@0: 'description' => 'A Unix timestamp indicating when the cache entry should expire, or ' . Cache::PERMANENT . ' for never.', Chris@0: 'type' => 'int', Chris@0: 'not null' => TRUE, Chris@0: 'default' => 0, Chris@0: ], Chris@0: 'created' => [ Chris@0: 'description' => 'A timestamp with millisecond precision indicating when the cache entry was created.', Chris@0: 'type' => 'numeric', Chris@0: 'precision' => 14, Chris@0: 'scale' => 3, Chris@0: 'not null' => TRUE, Chris@0: 'default' => 0, Chris@0: ], Chris@0: 'serialized' => [ Chris@0: 'description' => 'A flag to indicate whether content is serialized (1) or not (0).', Chris@0: 'type' => 'int', Chris@0: 'size' => 'small', Chris@0: 'not null' => TRUE, Chris@0: 'default' => 0, Chris@0: ], Chris@0: 'tags' => [ Chris@0: 'description' => 'Space-separated list of cache tags for this entry.', Chris@0: 'type' => 'text', Chris@0: 'size' => 'big', Chris@0: 'not null' => FALSE, Chris@0: ], Chris@0: 'checksum' => [ Chris@0: 'description' => 'The tag invalidation checksum when this entry was saved.', Chris@0: 'type' => 'varchar_ascii', Chris@0: 'length' => 255, Chris@0: 'not null' => TRUE, Chris@0: ], Chris@0: ], Chris@0: 'indexes' => [ Chris@0: 'expire' => ['expire'], Chris@0: 'created' => ['created'], Chris@0: ], Chris@0: 'primary key' => ['cid'], Chris@0: ]; Chris@0: return $schema; Chris@0: } Chris@0: Chris@0: /** Chris@0: * The maximum number of rows that this cache bin table is allowed to store. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public function getMaxRows() { Chris@0: return $this->maxRows; Chris@0: } Chris@0: Chris@0: }