<?php
/**
* Perforce Swarm
*
* @copyright 2013 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace Record\Cache;
/**
* Array serializer that streams elements into a file.
* This allows large arrays to be serialized with minimal memory usage.
* Optionally builds an index file for efficient lookups (see ArrayReader).
*/
class ArrayWriter
{
const INDEX_SUFFIX = '.index';
protected $file = null;
protected $handle = null;
protected $count = 0;
protected $index = false;
protected $writing = false;
/**
* Setup a new streaming array writer/serializer
*
* @param string $file the file to write the array to
* @param bool $index optional - build a separate index file of all array entries
* the index provides byte offset information to lookup specific elements
* in the main file without unserializing everything - the index itself is
* a serialized array in the form of: [ key => [byte-offset, length], ... ]
*/
public function __construct($file, $index = false)
{
$this->file = $file;
if ($index) {
$this->index = new static($file . static::INDEX_SUFFIX, false);
}
}
/**
* If the file doesn't exist (or appears to be corrupt), it will be (re)created.
* If the file already exists and has valid content, the file will not be modified
* and a runtime exception will be thrown.
*
* @return $this provides fluent interface
* @throws \RuntimeException if a valid looking file already exists
*/
public function createFile()
{
if (!is_string($this->file) || !strlen($this->file)) {
throw new \RuntimeException("Cannot create file. Filename must be set to a non-empty string.");
}
Cache::ensureWritable(dirname($this->file));
// open with 'c+' (to create if missing, but not truncate if existing)
$this->handle = @fopen($this->file, 'c+');
if ($this->handle === false) {
throw new \RuntimeException("Unable to create file ('" . $this->file . "').");
}
// if anything goes wrong past this point, make sure we close/unlock our file
try {
// write lock to ensure no one else reads/writes until we are done
$locked = flock($this->handle, LOCK_EX);
if ($locked === false) {
throw new \RuntimeException("Unable to lock file ('" . $file . "') for writing.");
}
// now that we have acquired a write lock, we want to verify that the
// file is empty or invalid - if we're indexing, we only validate the
// index file (handled by our index instance), if we're not indexing
// (or we are ourselves the index instance) we validate $this->file
if ($this->index) {
$this->index->createFile();
} elseif (unserialize(file_get_contents($this->file)) !== false) {
throw new \RuntimeException("Unable to create file ('" . $this->file . "'). File has valid content.");
}
// prime file for array entries - we don't know how many entries there are
// so we write a zero-padded length for now and update it on close.
// we ftrunctate in case the file already exists, but has bad content
ftruncate($this->handle, 0);
fwrite($this->handle, 'a:0000000000:{');
} catch (\Exception $e) {
$this->closeFile();
throw $e;
}
// if we get this far we are committed to writing the file
// we need a flag for this so we can safely touch-up in close
$this->writing = true;
return $this;
}
public function writeElement($key, $value)
{
if (!is_resource($this->handle)) {
throw new \RuntimeException("Cannot write element. File is not open.");
}
// offset is needed if we're indexing
$offset = $this->index ? ftell($this->handle) : null;
fwrite($this->handle, serialize($key));
fwrite($this->handle, serialize($value));
$this->count++;
// if we're indexing, record the position and length of this element
if ($this->index) {
$this->index->writeElement($key, array($offset, ftell($this->handle) - $offset));
}
return $this;
}
public function closeFile()
{
if (!$this->isOpen()) {
throw new \RuntimeException("Cannot close file. File is not open.");
}
// close the array, rewind and touch up length (but only if we are writing)
if ($this->writing) {
fwrite($this->handle, '}');
fseek($this->handle, 2);
fwrite($this->handle, str_pad($this->count, 10, '0', STR_PAD_LEFT));
$this->writing = false;
}
// if we're indexing, need to close index file now
if ($this->index && $this->index->isOpen()) {
$this->index->closeFile();
}
// readers don't bother locking the index file (just the main file), so we wait until
// the index is fully closed to unlock the main file - this ensures a complete index
flock($this->handle, LOCK_UN);
fclose($this->handle);
return $this;
}
public function isOpen()
{
return is_resource($this->handle);
}
}