/ */ namespace Record\Cache; /** * Memory efficient iterator for arrays that have been written AND indexed by ArrayWriter */ class ArrayReader implements \ArrayAccess, \Iterator { protected $file = null; protected $handle = null; protected $index = null; /** * Setup a new array reader * * @param string $file the file to read array from */ public function __construct($file) { $this->file = $file; } public function openFile() { $file = $this->file; $indexFile = $file . ArrayWriter::INDEX_SUFFIX; if (!is_string($file) || !file_exists($file)) { throw new \RuntimeException("Cannot open file '" . $file . "'. File does not exist."); } $this->handle = @fopen($file, 'r'); if ($this->handle === false) { throw new \RuntimeException("Unable to open file ('" . $file . "') for reading."); } // if anything goes wrong past this point, make sure we close/unlock our file try { // wait for a read lock to ensure file is not being actively written to $locked = flock($this->handle, LOCK_SH); if ($locked === false) { throw new \RuntimeException("Unable to lock file ('" . $file . "') for reading."); } if (!file_exists($indexFile)) { throw new \RuntimeException("Cannot open index file '" . $indexFile . "'. File does not exist."); } // read the entire index into memory (impractical to stream) $this->index = unserialize(file_get_contents($indexFile)); if ($this->index === false) { throw new \RuntimeException("Cannot unserialize index file ('" . $indexFile . "')."); } } catch (\Exception $e) { $this->closeFile(); throw $e; } return $this; } public function closeFile() { if (!is_resource($this->handle)) { throw new \RuntimeException("Cannot close file. File is not open."); } flock($this->handle, LOCK_UN); fclose($this->handle); return $this; } /** * Do a case-insensitive lookup for the given key - first match wins * * @param mixed $key the array key to look for * @return mixed the matching key (in original case) or false if no match */ public function noCaseLookup($key) { // note we make a local-scope (lazy) copy to avoid mucking the cursor $index = $this->index; foreach ($index as $candidate => $value) { if (strcasecmp($key, $candidate) === 0) { return $candidate; } } return false; } public function offsetExists($key) { // attempt to match PHP's key casting behavior // http://php.net/manual/en/language.types.array.php if (is_object($key) || is_array($key)) { return false; } if (is_null($key)) { $key = ""; } if (!is_string($key) && !is_int($key)) { $key = (int) $key; } return array_key_exists($key, $this->index); } public function offsetGet($key) { if (!$this->offsetExists($key)) { return null; } $offset = $this->index[$key][0]; $length = $this->index[$key][1]; fseek($this->handle, $offset); // need to wrap serialized key/value in array format 'a:1{...}' // so that it will unserialize correctly into key/value $entry = unserialize('a:1:{' . fread($this->handle, $length) . '}'); return $entry ? current($entry) : false; } public function offsetSet($key, $value) { throw new \BadMethodCallException("Cannot set element. Array is read-only."); } public function offsetUnset($offset) { throw new \BadMethodCallException("Cannot unset element. Array is read-only."); } public function current() { return $this->offsetGet(key($this->index)); } public function key() { return key($this->index); } public function next() { next($this->index); } public function rewind() { reset($this->index); } public function valid() { return key($this->index) !== null; } }