2023-03-11 12:04:29 +03:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Zend Framework (http://framework.zend.com/)
|
|
|
|
*
|
|
|
|
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
2023-04-01 09:03:34 +03:00
|
|
|
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
2023-03-11 12:04:29 +03:00
|
|
|
* @license http://framework.zend.com/license/new-bsd New BSD License
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace Zend\Cache\Storage\Adapter;
|
|
|
|
|
|
|
|
use stdClass;
|
|
|
|
use Traversable;
|
|
|
|
use Zend\Cache\Exception;
|
|
|
|
use Zend\Cache\Storage\AvailableSpaceCapableInterface;
|
|
|
|
use Zend\Cache\Storage\Capabilities;
|
|
|
|
use Zend\Cache\Storage\ClearByNamespaceInterface;
|
|
|
|
use Zend\Cache\Storage\ClearByPrefixInterface;
|
|
|
|
use Zend\Cache\Storage\FlushableInterface;
|
|
|
|
use Zend\Cache\Storage\IterableInterface;
|
|
|
|
use Zend\Cache\Storage\OptimizableInterface;
|
|
|
|
use Zend\Cache\Storage\TotalSpaceCapableInterface;
|
|
|
|
use Zend\Stdlib\ErrorHandler;
|
|
|
|
|
|
|
|
class Dba extends AbstractAdapter implements
|
|
|
|
AvailableSpaceCapableInterface,
|
|
|
|
ClearByNamespaceInterface,
|
|
|
|
ClearByPrefixInterface,
|
|
|
|
FlushableInterface,
|
|
|
|
IterableInterface,
|
|
|
|
OptimizableInterface,
|
|
|
|
TotalSpaceCapableInterface
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* The DBA resource handle
|
|
|
|
*
|
|
|
|
* @var null|resource
|
|
|
|
*/
|
|
|
|
protected $handle;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Buffered total space in bytes
|
|
|
|
*
|
|
|
|
* @var null|int|float
|
|
|
|
*/
|
|
|
|
protected $totalSpace;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor
|
|
|
|
*
|
|
|
|
* @param null|array|Traversable|DbaOptions $options
|
|
|
|
* @throws Exception\ExceptionInterface
|
|
|
|
*/
|
|
|
|
public function __construct($options = null)
|
|
|
|
{
|
|
|
|
if (!extension_loaded('dba')) {
|
|
|
|
throw new Exception\ExtensionNotLoadedException('Missing ext/dba');
|
|
|
|
}
|
|
|
|
|
|
|
|
parent::__construct($options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destructor
|
|
|
|
*
|
|
|
|
* Closes an open dba resource
|
|
|
|
*
|
|
|
|
* @see AbstractAdapter::__destruct()
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function __destruct()
|
|
|
|
{
|
|
|
|
$this->_close();
|
|
|
|
|
|
|
|
parent::__destruct();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* options */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set options.
|
|
|
|
*
|
|
|
|
* @param array|Traversable|DbaOptions $options
|
2023-04-01 09:03:34 +03:00
|
|
|
* @return self
|
2023-03-11 12:04:29 +03:00
|
|
|
* @see getOptions()
|
|
|
|
*/
|
|
|
|
public function setOptions($options)
|
|
|
|
{
|
|
|
|
if (!$options instanceof DbaOptions) {
|
|
|
|
$options = new DbaOptions($options);
|
|
|
|
}
|
|
|
|
|
|
|
|
return parent::setOptions($options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get options.
|
|
|
|
*
|
|
|
|
* @return DbaOptions
|
|
|
|
* @see setOptions()
|
|
|
|
*/
|
|
|
|
public function getOptions()
|
|
|
|
{
|
|
|
|
if (!$this->options) {
|
|
|
|
$this->setOptions(new DbaOptions());
|
|
|
|
}
|
|
|
|
return $this->options;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* TotalSpaceCapableInterface */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get total space in bytes
|
|
|
|
*
|
|
|
|
* @return int|float
|
|
|
|
*/
|
|
|
|
public function getTotalSpace()
|
|
|
|
{
|
|
|
|
if ($this->totalSpace === null) {
|
|
|
|
$pathname = $this->getOptions()->getPathname();
|
|
|
|
|
|
|
|
if ($pathname === '') {
|
|
|
|
throw new Exception\LogicException('No pathname to database file');
|
|
|
|
}
|
|
|
|
|
|
|
|
ErrorHandler::start();
|
|
|
|
$total = disk_total_space(dirname($pathname));
|
|
|
|
$error = ErrorHandler::stop();
|
|
|
|
if ($total === false) {
|
|
|
|
throw new Exception\RuntimeException("Can't detect total space of '{$pathname}'", 0, $error);
|
|
|
|
}
|
|
|
|
$this->totalSpace = $total;
|
|
|
|
|
|
|
|
// clean total space buffer on change pathname
|
|
|
|
$events = $this->getEventManager();
|
|
|
|
$handle = null;
|
|
|
|
$totalSpace = & $this->totalSpace;
|
|
|
|
$callback = function ($event) use (& $events, & $handle, & $totalSpace) {
|
|
|
|
$params = $event->getParams();
|
|
|
|
if (isset($params['pathname'])) {
|
|
|
|
$totalSpace = null;
|
|
|
|
$events->detach($handle);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
$events->attach('option', $callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->totalSpace;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* AvailableSpaceCapableInterface */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get available space in bytes
|
|
|
|
*
|
2023-04-01 09:03:34 +03:00
|
|
|
* @return float
|
2023-03-11 12:04:29 +03:00
|
|
|
*/
|
|
|
|
public function getAvailableSpace()
|
|
|
|
{
|
|
|
|
$pathname = $this->getOptions()->getPathname();
|
|
|
|
|
|
|
|
if ($pathname === '') {
|
|
|
|
throw new Exception\LogicException('No pathname to database file');
|
|
|
|
}
|
|
|
|
|
|
|
|
ErrorHandler::start();
|
|
|
|
$avail = disk_free_space(dirname($pathname));
|
|
|
|
$error = ErrorHandler::stop();
|
|
|
|
if ($avail === false) {
|
|
|
|
throw new Exception\RuntimeException("Can't detect free space of '{$pathname}'", 0, $error);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $avail;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* FlushableInterface */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flush the whole storage
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function flush()
|
|
|
|
{
|
|
|
|
$pathname = $this->getOptions()->getPathname();
|
|
|
|
|
|
|
|
if ($pathname === '') {
|
|
|
|
throw new Exception\LogicException('No pathname to database file');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (file_exists($pathname)) {
|
|
|
|
// close the dba file before delete
|
|
|
|
// and reopen (create) on next use
|
|
|
|
$this->_close();
|
|
|
|
|
|
|
|
ErrorHandler::start();
|
|
|
|
$result = unlink($pathname);
|
|
|
|
$error = ErrorHandler::stop();
|
|
|
|
if (!$result) {
|
|
|
|
throw new Exception\RuntimeException("unlink('{$pathname}') failed", 0, $error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ClearByNamespaceInterface */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove items by given namespace
|
|
|
|
*
|
|
|
|
* @param string $namespace
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function clearByNamespace($namespace)
|
|
|
|
{
|
|
|
|
$namespace = (string) $namespace;
|
|
|
|
if ($namespace === '') {
|
|
|
|
throw new Exception\InvalidArgumentException('No namespace given');
|
|
|
|
}
|
|
|
|
|
|
|
|
$prefix = $namespace . $this->getOptions()->getNamespaceSeparator();
|
|
|
|
$prefixl = strlen($prefix);
|
|
|
|
$result = true;
|
|
|
|
|
|
|
|
$this->_open();
|
|
|
|
|
2023-04-01 09:03:34 +03:00
|
|
|
do {
|
|
|
|
// Workaround for PHP-Bug #62491 & #62492
|
2023-03-11 12:04:29 +03:00
|
|
|
$recheck = false;
|
|
|
|
$internalKey = dba_firstkey($this->handle);
|
|
|
|
while ($internalKey !== false && $internalKey !== null) {
|
|
|
|
if (substr($internalKey, 0, $prefixl) === $prefix) {
|
|
|
|
$result = dba_delete($internalKey, $this->handle) && $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
$internalKey = dba_nextkey($this->handle);
|
|
|
|
}
|
|
|
|
} while ($recheck);
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ClearByPrefixInterface */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove items matching given prefix
|
|
|
|
*
|
|
|
|
* @param string $prefix
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function clearByPrefix($prefix)
|
|
|
|
{
|
|
|
|
$prefix = (string) $prefix;
|
|
|
|
if ($prefix === '') {
|
|
|
|
throw new Exception\InvalidArgumentException('No prefix given');
|
|
|
|
}
|
|
|
|
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$namespace = $options->getNamespace();
|
|
|
|
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator() . $prefix;
|
|
|
|
$prefixL = strlen($prefix);
|
|
|
|
$result = true;
|
|
|
|
|
|
|
|
$this->_open();
|
|
|
|
|
2023-04-01 09:03:34 +03:00
|
|
|
// Workaround for PHP-Bug #62491 & #62492
|
|
|
|
do {
|
2023-03-11 12:04:29 +03:00
|
|
|
$recheck = false;
|
|
|
|
$internalKey = dba_firstkey($this->handle);
|
|
|
|
while ($internalKey !== false && $internalKey !== null) {
|
|
|
|
if (substr($internalKey, 0, $prefixL) === $prefix) {
|
|
|
|
$result = dba_delete($internalKey, $this->handle) && $result;
|
|
|
|
$recheck = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$internalKey = dba_nextkey($this->handle);
|
|
|
|
}
|
|
|
|
} while ($recheck);
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* IterableInterface */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the storage iterator
|
|
|
|
*
|
2023-04-01 09:03:34 +03:00
|
|
|
* @return DbaIterator
|
2023-03-11 12:04:29 +03:00
|
|
|
*/
|
|
|
|
public function getIterator()
|
|
|
|
{
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$namespace = $options->getNamespace();
|
|
|
|
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
|
|
|
|
|
|
|
|
return new DbaIterator($this, $this->handle, $prefix);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* OptimizableInterface */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Optimize the storage
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
* @return Exception\RuntimeException
|
|
|
|
*/
|
|
|
|
public function optimize()
|
|
|
|
{
|
|
|
|
$this->_open();
|
|
|
|
if (!dba_optimize($this->handle)) {
|
|
|
|
throw new Exception\RuntimeException('dba_optimize failed');
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* reading */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method to get an item.
|
|
|
|
*
|
|
|
|
* @param string $normalizedKey
|
|
|
|
* @param bool $success
|
|
|
|
* @param mixed $casToken
|
|
|
|
* @return mixed Data on success, null on failure
|
|
|
|
* @throws Exception\ExceptionInterface
|
|
|
|
*/
|
|
|
|
protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
|
|
|
|
{
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$namespace = $options->getNamespace();
|
|
|
|
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
|
|
|
|
|
|
|
|
$this->_open();
|
|
|
|
$value = dba_fetch($prefix . $normalizedKey, $this->handle);
|
|
|
|
|
|
|
|
if ($value === false) {
|
|
|
|
$success = false;
|
2023-04-01 09:03:34 +03:00
|
|
|
return;
|
2023-03-11 12:04:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
$success = true;
|
|
|
|
$casToken = $value;
|
|
|
|
return $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method to test if an item exists.
|
|
|
|
*
|
|
|
|
* @param string $normalizedKey
|
|
|
|
* @return bool
|
|
|
|
* @throws Exception\ExceptionInterface
|
|
|
|
*/
|
|
|
|
protected function internalHasItem(& $normalizedKey)
|
|
|
|
{
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$namespace = $options->getNamespace();
|
|
|
|
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
|
|
|
|
|
|
|
|
$this->_open();
|
|
|
|
return dba_exists($prefix . $normalizedKey, $this->handle);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* writing */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method to store an item.
|
|
|
|
*
|
|
|
|
* @param string $normalizedKey
|
|
|
|
* @param mixed $value
|
|
|
|
* @return bool
|
|
|
|
* @throws Exception\ExceptionInterface
|
|
|
|
*/
|
|
|
|
protected function internalSetItem(& $normalizedKey, & $value)
|
|
|
|
{
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$namespace = $options->getNamespace();
|
|
|
|
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
|
|
|
|
$internalKey = $prefix . $normalizedKey;
|
|
|
|
|
2023-04-01 09:03:34 +03:00
|
|
|
$cacheableValue = (string) $value; // dba_replace requires a string
|
|
|
|
|
2023-03-11 12:04:29 +03:00
|
|
|
$this->_open();
|
2023-04-01 09:03:34 +03:00
|
|
|
if (!dba_replace($internalKey, $cacheableValue, $this->handle)) {
|
2023-03-11 12:04:29 +03:00
|
|
|
throw new Exception\RuntimeException("dba_replace('{$internalKey}', ...) failed");
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an item.
|
|
|
|
*
|
|
|
|
* @param string $normalizedKey
|
|
|
|
* @param mixed $value
|
|
|
|
* @return bool
|
|
|
|
* @throws Exception\ExceptionInterface
|
|
|
|
*/
|
|
|
|
protected function internalAddItem(& $normalizedKey, & $value)
|
|
|
|
{
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$namespace = $options->getNamespace();
|
|
|
|
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
|
|
|
|
$internalKey = $prefix . $normalizedKey;
|
|
|
|
|
|
|
|
$this->_open();
|
|
|
|
|
|
|
|
// Workaround for PHP-Bug #54242 & #62489
|
|
|
|
if (dba_exists($internalKey, $this->handle)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Workaround for PHP-Bug #54242 & #62489
|
|
|
|
// dba_insert returns true if key already exists
|
|
|
|
ErrorHandler::start();
|
|
|
|
$result = dba_insert($internalKey, $value, $this->handle);
|
|
|
|
$error = ErrorHandler::stop();
|
|
|
|
if (!$result || $error) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method to remove an item.
|
|
|
|
*
|
|
|
|
* @param string $normalizedKey
|
|
|
|
* @return bool
|
|
|
|
* @throws Exception\ExceptionInterface
|
|
|
|
*/
|
|
|
|
protected function internalRemoveItem(& $normalizedKey)
|
|
|
|
{
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$namespace = $options->getNamespace();
|
|
|
|
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
|
|
|
|
$internalKey = $prefix . $normalizedKey;
|
|
|
|
|
|
|
|
$this->_open();
|
|
|
|
|
|
|
|
// Workaround for PHP-Bug #62490
|
|
|
|
if (!dba_exists($internalKey, $this->handle)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return dba_delete($internalKey, $this->handle);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* status */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method to get capabilities of this adapter
|
|
|
|
*
|
|
|
|
* @return Capabilities
|
|
|
|
*/
|
|
|
|
protected function internalGetCapabilities()
|
|
|
|
{
|
|
|
|
if ($this->capabilities === null) {
|
|
|
|
$marker = new stdClass();
|
|
|
|
$capabilities = new Capabilities(
|
|
|
|
$this,
|
|
|
|
$marker,
|
|
|
|
array(
|
|
|
|
'supportedDatatypes' => array(
|
|
|
|
'NULL' => 'string',
|
|
|
|
'boolean' => 'string',
|
|
|
|
'integer' => 'string',
|
|
|
|
'double' => 'string',
|
|
|
|
'string' => true,
|
|
|
|
'array' => false,
|
|
|
|
'object' => false,
|
|
|
|
'resource' => false,
|
|
|
|
),
|
|
|
|
'minTtl' => 0,
|
|
|
|
'supportedMetadata' => array(),
|
|
|
|
'maxKeyLength' => 0, // TODO: maxKeyLength ????
|
|
|
|
'namespaceIsPrefix' => true,
|
|
|
|
'namespaceSeparator' => $this->getOptions()->getNamespaceSeparator(),
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
// update namespace separator on change option
|
|
|
|
$this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) {
|
|
|
|
$params = $event->getParams();
|
|
|
|
|
|
|
|
if (isset($params['namespace_separator'])) {
|
|
|
|
$capabilities->setNamespaceSeparator($marker, $params['namespace_separator']);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
$this->capabilities = $capabilities;
|
|
|
|
$this->capabilityMarker = $marker;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->capabilities;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open the database if not already done.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
* @throws Exception\LogicException
|
|
|
|
* @throws Exception\RuntimeException
|
|
|
|
*/
|
|
|
|
protected function _open()
|
|
|
|
{
|
|
|
|
if (!$this->handle) {
|
|
|
|
$options = $this->getOptions();
|
|
|
|
$pathname = $options->getPathname();
|
|
|
|
$mode = $options->getMode();
|
|
|
|
$handler = $options->getHandler();
|
|
|
|
|
|
|
|
if ($pathname === '') {
|
|
|
|
throw new Exception\LogicException('No pathname to database file');
|
|
|
|
}
|
|
|
|
|
|
|
|
ErrorHandler::start();
|
|
|
|
$dba = dba_open($pathname, $mode, $handler);
|
|
|
|
$err = ErrorHandler::stop();
|
|
|
|
if (!$dba) {
|
|
|
|
throw new Exception\RuntimeException(
|
|
|
|
"dba_open('{$pathname}', '{$mode}', '{$handler}') failed",
|
|
|
|
0,
|
|
|
|
$err
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->handle = $dba;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close database file if opened
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
protected function _close()
|
|
|
|
{
|
|
|
|
if ($this->handle) {
|
|
|
|
ErrorHandler::start(E_NOTICE);
|
|
|
|
dba_close($this->handle);
|
|
|
|
ErrorHandler::stop();
|
|
|
|
$this->handle = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|