Overview

Namespaces

  • Net
    • Bazzline
      • Component
        • Database
          • FileStorage
            • IdGenerator
            • Storage
            • Writer

Classes

  • Net\Bazzline\Component\Database\FileStorage\IdGenerator\UUIDGenerator
  • Net\Bazzline\Component\Database\FileStorage\Storage\Storage
  • Net\Bazzline\Component\Database\FileStorage\Storage\StorageFactory
  • Net\Bazzline\Component\Database\FileStorage\Writer\LockableWriter
  • Net\Bazzline\Component\Database\FileStorage\Writer\LockableWriterFactory
  • Net\Bazzline\Component\Database\FileStorage\Writer\LockableWriterForPhp5Dot3

Interfaces

  • Net\Bazzline\Component\Database\FileStorage\IdGenerator\IdGeneratorInterface
  • Net\Bazzline\Component\Database\FileStorage\Storage\StorageInterface
  • Net\Bazzline\Component\Database\FileStorage\Writer\LockableWriterInterface

Exceptions

  • Net\Bazzline\Component\Database\FileStorage\InvalidArgumentException
  • Net\Bazzline\Component\Database\FileStorage\RuntimeException
  • Overview
  • Namespace
  • Class
  1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 
<?php
/**
 * @author stev leibelt <artodeto@bazzline.net>
 * @since 2015-04-12 
 */

namespace Net\Bazzline\Component\Database\FileStorage\Storage;

use Net\Bazzline\Component\Database\FileStorage\IdGenerator\IdGeneratorInterface;
use Net\Bazzline\Component\Database\FileStorage\InvalidArgumentException;
use Net\Bazzline\Component\Database\FileStorage\RuntimeException;
use Net\Bazzline\Component\Csv\Reader\Reader;
use Net\Bazzline\Component\Database\FileStorage\Writer\LockableWriterInterface;

/**
 * Class Storage
 *
 * @package Net\Bazzline\Component\Database\FileStorage\Storage
 */
class Storage implements StorageInterface
{
    const KEY_ID    = 0;
    const KEY_DATA  = 1;

    /** @var array */
    private $filters;

    /** @var null|mixed */
    private $filterById;

    /** @var IdGeneratorInterface */
    private $generator;

    /** @var boolean */
    private $hasFilterById;

    /** @var boolean */
    private $hasFilters;

    /** @var null|int */
    private $limit;

    /** @var null|int */
    private $offset;

    /** @var string */
    private $path;

    /** @var Reader */
    private $reader;

    /** @var LockableWriterInterface */
    private $writer;

    public function __construct()
    {
        $this->resetRuntimeProperties();
    }

    /**
     * @param IdGeneratorInterface $generator
     * @return $this
     */
    public function injectGenerator(IdGeneratorInterface $generator)
    {
        $this->generator = $generator;

        return $this;
    }

    /**
     * @param string $path
     * @return $this
     * @throws InvalidArgumentException|RuntimeException
     */
    public function injectPath($path)
    {
        $this->createPathIfNotAvailable($path);
        $this->validatePath($path);
        $this->path = $path . DIRECTORY_SEPARATOR . 'database.csv';

        if (!is_file($this->path)) {
            touch($this->path);
        }

        if (!is_null($this->reader)) {
            $this->reader->setPath($this->path);
        }
        if (!is_null($this->writer)) {
            $this->writer->setPath($this->path);
        }

        return $this;
    }

    /**
     * @param Reader $reader
     * @return $this
     */
    public function injectReader(Reader $reader)
    {
        $this->reader = $reader;

        if (!is_null($this->path)) {
            $this->reader->setPath($this->path);
        }

        return $this;
    }

    /**
     * @param LockableWriterInterface $writer
     * @return $this
     */
    public function injectWriter(LockableWriterInterface $writer)
    {
        $this->writer = $writer;

        if (!is_null($this->path)) {
            $this->writer->setPath($this->path);
        }

        return $this;
    }

    /**
     * @param array $data
     * @param bool $resetRuntimeProperties
     * @return string - unique identifier
     */
    public function create(array $data, $resetRuntimeProperties = true)
    {
        $id     = $this->generator->generate();
        $line   = $this->createLine($id, $data);
        $writer = $this->writer;

        $this->acquireLock();
        $writer($line);
        $this->releaseLockIfNeeded();

        $this->resetRuntimePropertiesIfNeeded($resetRuntimeProperties);

        return $id;
    }



    /**
     * @param bool $resetRuntimeProperties
     * @return array
     * @todo
     */
    public function readMany($resetRuntimeProperties = true)
    {
        $collection = [];
        $reader     = $this->reader;
        $reader->rewind();

        if ($this->hasOffset()) {
            $reader = $this->seekReaderToOffset($reader, $this->offset);
        }

        $iterator   = ($this->hasLimit()) ? $this->limit : -1;

        while ($line = $reader()) {
            $isCountableLine = false;

            if ($this->isValidLine($line)) {
                if ($this->hasFilterById) {
                    if ($this->lineHasId($line, $this->filterById)) {
                        $collection = $this->addLineToCollection($line, $collection);
                        break;
                    }
                } else if ($this->hasFilters) {
                    if ($this->lineHasFilters($line, $this->filters)) {
                        $collection         = $this->addLineToCollection($line, $collection);
                        $isCountableLine    = true;
                    }
                } else {
                    $collection         = $this->addLineToCollection($line, $collection);
                    $isCountableLine    = true;
                }
            }

            if ($isCountableLine) {
                --$iterator;
                if ($iterator === 0) {
                    break;
                }
            }
        }

        $this->resetRuntimePropertiesIfNeeded($resetRuntimeProperties);

        return $collection;
    }



    /**
     * @param bool $resetRuntimeProperties
     * @return null|mixed - nothing or data
     */
    public function readOne($resetRuntimeProperties = true)
    {
        $this->limitBy(1);
        $collection = $this->readMany();
        $this->resetRuntimePropertiesIfNeeded($resetRuntimeProperties);

        return $collection;
    }

    /**
     * @param bool $resetRuntimeProperties
     * @param array $data
     * @return boolean
     */
    public function update(array $data, $resetRuntimeProperties = true)
    {
        return $this->updateOrDelete($data, $resetRuntimeProperties);
    }

    /**
     * @param bool $resetRuntimeProperties
     * @return boolean
     */
    public function delete($resetRuntimeProperties = true)
    {
        return $this->updateOrDelete(null, $resetRuntimeProperties);
    }

    /**
     * @param mixed $key
     * @param mixed $value
     * @return $this
     * @todo implement a way that it is also valid to filter by $key
     *  (without value) and vice versa
     */
    public function filterBy($key, $value)
    {
        $this->filters[$key]    = $value;
        $this->hasFilters       = true;

        return $this;
    }

    /**
     * @param mixed $id
     * @return $this
     */
    public function filterById($id)
    {
        $this->filterById       = $id;
        $this->hasFilterById    = true;

        return $this;
    }

    /**
     * @param int $count
     * @param null|int $offset
     * @return $this
     */
    public function limitBy($count, $offset = null)
    {
        $this->limit = (int) $count;
        if (!is_null($offset)) {
            $this->offset = (int) $offset;
        }

        return $this;
    }

    /**
     * @param int $atLeast
     * @param null|int $atMost
     * @return bool
     */
    public function has($atLeast = 1, $atMost = null)
    {
        if (is_null($atMost)) {
            $this->limitBy($atLeast);
        }

        $numberOfEntries = count($this->readMany(false));

        if (is_null($atMost)) {
            $has = ($numberOfEntries >= $atLeast);
        } else {
            $has = (($numberOfEntries >= $atLeast)
                && ($numberOfEntries <= $atMost));
        }

        return $has;
    }

    public function resetRuntimeProperties()
    {
        $this->filters          = [];
        $this->filterById       = null;
        $this->hasFilterById    = false;
        $this->hasFilters       = false;
        $this->limit            = null;
        $this->offset           = null;
    }

    /**
     * @param array $data - null triggers a deletion of fitting lines
     * @param bool $resetRuntimeProperties
     * @return bool
     */
    private function updateOrDelete(array $data = null, $resetRuntimeProperties = true)
    {
        $collection     = [];
        $delete         = (is_null($data));
        $wasSuccessful  = true;
        $path           = $this->path . ($delete ? '.delete' : '.update');
        $reader         = $this->reader;
        $writer         = $this->writer;
        $reader->rewind();

        $writer->copy($path, true);
        $this->acquireLock();
        $writer->truncate();

        if ($this->hasOffset()) {
            $reader = $this->seekReaderToOffset($reader, $this->offset);
        }

        $iterator = ($this->hasLimit()) ? $this->limit : -1;

        while ($line = $reader()) {
            if ($this->isValidLine($line)) {
                if ($this->hasFilterById) {
                    $addLine = true;
                    if ($this->lineHasId($line, $this->filterById)) {
                        if ($delete) {
                            $addLine = false;
                        } else {
                            $line = $this->setDataInLine($line, $data);
                        }
                    }
                } else if ($this->hasFilters) {
                    $addLine = true;
                    if ($this->lineHasFilters($line, $this->filters)) {
                        if ($delete) {
                            $addLine = false;
                        } else {
                            $line = $this->setDataInLine($line, $data);
                        }
                    }
                } else {
                    $addLine = (!$delete);
                }

                if ($addLine) {
                    $collection = $this->addLineToCollection($line, $collection);
                }
                --$iterator;
                if ($iterator === 0) {
                    break;
                }
            }
        }

        foreach ($collection as $id => $data) {
            $line = $this->createLine($id, $data);

            if ($writer($line) == false) {
                $wasSuccessful = false;
                break;
            } else {
                $wasSuccessful = true;
            }
        }

        $this->releaseLockIfNeeded();
        if ($wasSuccessful) {
            $writer->copy($this->path, true);
            unlink($path);
        } else {
            $writer->setPath($this->path);
        }

        $this->resetRuntimePropertiesIfNeeded($resetRuntimeProperties);

        return $wasSuccessful;
    }

    /**
     * @param string $path
     * @throws InvalidArgumentException
     */
    private function createPathIfNotAvailable($path)
    {
        if (!is_dir($path)) {
            //@todo replace by command if needed
            //http://php.net/manual/en/function.clearstatcache.php
            //$couldNotBeCreated = (!mkdir($path, 0755, true));
            exec('/usr/bin/env mkdir -p ' . $path);
            $couldNotBeCreated = false; //@todo implement

            if ($couldNotBeCreated) {
                $message = 'could not create directory "' . $path . '""';

                throw new InvalidArgumentException($message);
            }
        }
    }

    /**
     * @return bool
     */
    private function hasLimit()
    {
        return (is_int($this->limit));
    }

    /**
     * @return bool
     */
    private function hasOffset()
    {
        return (is_int($this->offset));
    }

    /**
     * @param mixed|array $line
     * @return bool
     */
    private function isValidLine($line)
    {
        return ((is_array($line) && count($line) === 2));
    }

    /**
     * @param bool|true $isNeeded
     */
    private function resetRuntimePropertiesIfNeeded($isNeeded = true)
    {
        if ($isNeeded) {
            $this->resetRuntimeProperties();
        }
    }

    /**
     * @param string $path
     * @throws InvalidArgumentException
     */
    private function validatePath($path)
    {
        if (!is_dir($path)) {
            $message = 'path "' . $path . '" must be a directory';

            throw new InvalidArgumentException($message);
        }

        if (!is_writable($path)) {
            $message = 'directory "' . $path . '" is not writable';

            throw new InvalidArgumentException($message);
        }
    }

    /**
     * @param Reader $reader
     * @param int $offset
     * @return Reader
     */
    private function seekReaderToOffset(Reader $reader, $offset)
    {
        $reader(($offset - 1));

        return $reader;
    }

    /**
     * @param array $line
     * @param string $id
     * @return bool
     */
    private function lineHasId(array $line, $id)
    {
        return ($line[self::KEY_ID] === $id);
    }

    /**
     * @param array $line
     * @param array $filters
     * @return bool
     */
    private function lineHasFilters(array &$line, array &$filters)
    {
        $lineHasFilters = false;
        $data = $this->getDataFromLine($line);

        foreach ($filters as $key => $value) {
            if ((isset($data[$key]))
                && ($data[$key] === $value)) {
                $lineHasFilters = true;
            } else {
                $lineHasFilters = false;
                break;
            }
        }

        return $lineHasFilters;
    }

    /**
     * @param array $line
     * @param array $collection
     * @return array
     */
    private function addLineToCollection(array &$line, array &$collection)
    {
        $collection[$line[self::KEY_ID]] = $this->getDataFromLine($line);

        return $collection;
    }

    /**
     * @param array $line
     * @return array
     */
    private function getDataFromLine(array &$line)
    {
        return (array) json_decode($line[self::KEY_DATA]);
    }

    /**
     * @param array $line
     * @param array $data
     * @return array
     */
    private function setDataInLine(array &$line, array &$data)
    {
        $line[self::KEY_DATA] = json_encode($data);

        return $line;
    }

    /**
     * @param string $id
     * @param array $data
     * @return array
     */
    private function createLine($id, array $data)
    {
        $line = [
            self::KEY_ID => $id
        ];
        $line = $this->setDataInLine($line, $data);

        return $line;
    }

    /**
     * @throws RuntimeException
     */
    private function acquireLock()
    {
        $this->writer->acquireLock();
    }

    /**
     * @throws RuntimeException
     */
    private function releaseLockIfNeeded()
    {
        if ($this->writer->isLocked()) {
            $this->writer->releaseLock();
        }
    }
}
PHP Database File Storage by bazzline.net API documentation generated by ApiGen