Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add redis cache support #75

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
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
318 changes: 318 additions & 0 deletions src/PhpSpreadsheet/CachedObjectStorage/Redis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
<?php

namespace PhpOffice\PhpSpreadsheet\CachedObjectStorage;

use PhpOffice\PhpSpreadsheet\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Worksheet;

/**
* Copyright (c) 2006 - 2016 PhpSpreadsheet.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @category PhpSpreadsheet
*
* @copyright Copyright (c) 2006 - 2016 PhpSpreadsheet (https://github.com/PHPOffice/PhpSpreadsheet)
* @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL
*/
class Redis extends CacheBase implements ICache
{
/**
* Prefix used to uniquely identify cache data for this worksheet.
*
* @var string
*/
private $cachePrefix = null;

/**
* Cache timeout.
*
* @var int
*/
private $cacheTime = 600;

/**
* Redis interface.
*
* @var resource
*/
private $redis = null;

/**
* Initialise this new cell collection.
*
* @param Worksheet $parent The worksheet for this cell collection
* @param mixed[] $arguments Additional initialisation arguments
*
* @throws Exception
*/
public function __construct(Worksheet $parent, $arguments)
{
$redisServer = isset($arguments['redisServer']) ? $arguments['redisServer'] : 'localhost';
$redisPort = isset($arguments['redisPort']) ? (int) $arguments['redisPort'] : 6379;
$cacheTime = isset($arguments['cacheTime']) ? (int) $arguments['cacheTime'] : 600;

if (null === $this->cachePrefix) {
$baseUnique = $this->getUniqueID();
$this->cachePrefix = substr(md5($baseUnique), 0, 8) . '.';

// Set a new Redis object and connect to the Redis server
$this->redis = new \Redis();
if (!$this->redis->connect($redisServer, $redisPort, 10)) {
throw new Exception("Could not connect to Redis server at {$redisServer}:{$redisPort}");
}
$this->cacheTime = $cacheTime;

parent::__construct($parent);
}
}

/**
* Identify whether the caching method is currently available
* Some methods are dependent on the availability of certain extensions being enabled in the PHP build.
*
* @return bool
*/
public static function cacheMethodIsAvailable()
{
if (!extension_loaded('redis')) {
return false;
}

return true;
}

/**
* Add or Update a cell in cache identified by coordinate address.
*
* @param string $pCoord Coordinate address of the cell to update
* @param Cell $cell Cell to update
*
* @throws Exception
*
* @return Cell
*/
public function addCacheData($pCoord, Cell $cell)
{
if (($pCoord !== $this->currentObjectID) && ($this->currentObjectID !== null)) {
$this->storeData();
}
$this->cellCache[$pCoord] = true;

$this->currentObjectID = $pCoord;
$this->currentObject = $cell;
$this->currentCellIsDirty = true;

return $cell;
}

/**
* Store cell data in cache for the current cell object if it's "dirty",
* and the 'nullify' the current cell object.
*
* @throws Exception
*/
protected function storeData()
{
if ($this->currentCellIsDirty && !empty($this->currentObjectID)) {
$this->currentObject->detach();

$obj = serialize($this->currentObject);
if (!$this->redis->set($this->cachePrefix . $this->currentObjectID . '.cache', $obj, $this->cacheTime)) {
$this->__destruct();
throw new Exception("Failed to store cell {$this->currentObjectID} in Redis");
}
$this->currentCellIsDirty = false;
}
$this->currentObjectID = $this->currentObject = null;
}

/**
* Destroy this cell collection.
*
* @throws Exception
*/
public function __destruct()
{
$cacheList = $this->getCellList();
foreach ($cacheList as $cellID) {
$this->redis->delete($this->cachePrefix . $cellID . '.cache');
}
}

/**
* Get a list of all cell addresses currently held in cache.
*
* @throws Exception
*
* @return string[]
*/
public function getCellList()
{
if ($this->currentObjectID !== null) {
$this->storeData();
}

return parent::getCellList();
}

/**
* Is a value set in the current \PhpOffice\PhpSpreadsheet\CachedObjectStorage\ICache for an indexed cell?
*
* @param string $pCoord Coordinate address of the cell to check
*
* @throws Exception
*
* @return bool
*/
public function isDataSet($pCoord)
{
// Check if the requested entry is the current object, or exists in the cache
if (parent::isDataSet($pCoord)) {
if ($this->currentObjectID == $pCoord) {
return true;
}
// Check if the requested entry still exists in Redis
$success = $this->redis->get($this->cachePrefix . $pCoord . '.cache');
if ($success === false) {
// Entry no longer exists in Redis, so clear it from the cache array
parent::deleteCacheData($pCoord);
throw new Exception('Cell entry ' . $pCoord . ' no longer exists in Redis');
}

return true;
}

return false;
}

/**
* Get cell at a specific coordinate.
*
* @param string $pCoord Coordinate of the cell
*
* @throws Exception
*
* @return Cell Cell that was found, or null if not found
*/
public function getCacheData($pCoord)
{
if ($pCoord === $this->currentObjectID) {
return $this->currentObject;
}
$this->storeData();

// Check if the entry that has been requested actually exists
if (parent::isDataSet($pCoord)) {
$obj = $this->redis->get($this->cachePrefix . $pCoord . '.cache');
if ($obj === false) {
// Entry no longer exists in Redis, so clear it from the cache array
parent::deleteCacheData($pCoord);
throw new Exception("Cell entry {$pCoord} no longer exists in Redis");
}
} else {
// Return null if requested entry doesn't exist in cache
return null;
}

// Set current entry to the requested entry
$this->currentObjectID = $pCoord;
$this->currentObject = unserialize($obj);
// Re-attach this as the cell's parent
$this->currentObject->attach($this);

// Return requested entry
return $this->currentObject;
}

/**
* Delete a cell in cache identified by coordinate address.
*
* @param string $pCoord Coordinate address of the cell to delete
*
* @throws Exception
*/
public function deleteCacheData($pCoord)
{
// Delete the entry from Redis
$this->redis->delete($this->cachePrefix . $pCoord . '.cache');

// Delete the entry from our cell address array
parent::deleteCacheData($pCoord);
}

/**
* Clone the cell collection.
*
* @param Worksheet $parent The new worksheet that we're copying to
*
* @throws Exception
*/
public function copyCellCollection(Worksheet $parent)
{
parent::copyCellCollection($parent);
// Get a new id for the new file name
$baseUnique = $this->getUniqueID();
$newCachePrefix = substr(md5($baseUnique), 0, 8) . '.';
$cacheList = $this->getCellList();
foreach ($cacheList as $cellID) {
if ($cellID != $this->currentObjectID) {
$obj = $this->redis->get($this->cachePrefix . $cellID . '.cache');
if ($obj === false) {
// Entry no longer exists in Redis, so clear it from the cache array
parent::deleteCacheData($cellID);
throw new Exception("Cell entry {$cellID} no longer exists in Redis");
}
if (!$this->redis->set($newCachePrefix . $cellID . '.cache', $obj, $this->cacheTime)) {
$this->__destruct();
throw new Exception("Failed to store cell {$cellID} in Redis");
}
}
}
$this->cachePrefix = $newCachePrefix;
}

/**
* Clear the cell collection and disconnect from our parent.
*
* @throws Exception
*/
public function unsetWorksheetCells()
{
if (null !== $this->currentObject) {
$this->currentObject->detach();
$this->currentObject = $this->currentObjectID = null;
}

// Flush the Redis cache
$this->__destruct();

$this->cellCache = [];

// detach ourself from the worksheet, so that it can then delete this object successfully
$this->parent = null;
}

/**
* Redis error handler.
*
* @param string $host Redis server
* @param int $port Redis port
*
* @throws Exception
*/
public function failureCallback($host, $port)
{
throw new Exception("redis {$host}:{$port} failed");
}
}
12 changes: 9 additions & 3 deletions src/PhpSpreadsheet/CachedObjectStorageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class CachedObjectStorageFactory
const CACHE_TO_WINCACHE = 'Wincache';
const CACHE_TO_SQLITE = 'SQLite';
const CACHE_TO_SQLITE3 = 'SQLite3';
const CACHE_TO_REDIS = 'Redis';

/**
* Name of the method used for cell cacheing.
Expand Down Expand Up @@ -69,6 +70,7 @@ class CachedObjectStorageFactory
self::CACHE_TO_WINCACHE,
self::CACHE_TO_SQLITE,
self::CACHE_TO_SQLITE3,
self::CACHE_TO_REDIS,
];

/**
Expand Down Expand Up @@ -100,6 +102,11 @@ class CachedObjectStorageFactory
],
self::CACHE_TO_SQLITE => [],
self::CACHE_TO_SQLITE3 => [],
self::CACHE_TO_REDIS => [
'redisServer' => 'localhost',
'redisPort' => 6379,
'cacheTime' => 600,
],
];

/**
Expand Down Expand Up @@ -211,9 +218,8 @@ public static function getInstance(Worksheet $parent)
$parent,
self::$storageMethodParameters[self::$cacheStorageMethod]
);
if ($instance !== null) {
return $instance;
}

return $instance;
}

return false;
Expand Down