[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: fire015\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor\ncomposer.lock\n.idea\n.phpunit.result.cache"
  },
  {
    "path": ".travis.yml",
    "content": "language: php\nsudo: false\ndist: bionic\n\nphp:\n  - 7.3\n  - 7.4\n  - 8.0\n  - 8.1.0\n\ninstall:\n  - travis_retry composer install --no-interaction --prefer-dist\n\nscript: vendor/bin/phpunit\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "Change Log\n==========\n\n### 19/01/2021 - 2.3\n* Bump minimum PHP version to 7.3\n* Update PHPUnit to version 9 (ensure Flintstone is compatible with PHP 8)\n\n### 12/03/2019 - 2.2\n* Bump minimum PHP version to 7.0\n* Update PHPUnit to version 6\n* Removed data type validation for storing\n* Added param and return types\n\n### 09/06/2017 - 2.1.1\n* Update `Database::writeTempToFile` to correctly close the file pointer and free up memory\n\n### 24/05/2017 - 2.1\n* Bump minimum PHP version to 5.6\n* Tidy up of Flintstone class, moved some code into `Database`\n* Added `Line` and `Validation` classes\n* Closed off public methods `Database::openFile` and `Database::closeFile`\n\n### 20/01/2016 - 2.0\n* Major refactor, class names have changed and the whole codebase is much more extensible\n* Removed the static `load` and `unload` methods and the `FlinstoneDB` class\n* The `replace` method is no longer public\n* The `getFile` method has been removed\n* Default swap memory limit has been increased to 2MB\n* Ability to pass any instance for cache that implements `Flintstone\\Cache\\CacheInterface`\n\n### 25/03/2015 - 1.9\n* Added `getAll` method and some refactoring\n\n### 15/10/2014 - 1.8\n* Added formatter option so that you can control how data is encoded/decoded (default is serialize but also ships with json)\n\n### 09/10/2014 - 1.7\n* Moved from fopen to SplFileObject\n* Moved composer loader from PSR-0 to PSR-4\n* Code is now PSR-2 compliant\n* Added PHP 5.6 to travis\n\n### 30/09/2014 - 1.6\n* Updated limits on valid characters in key name and size\n* Improved unit tests\n\n### 29/05/2014 - 1.5\n* Reduced some internal complexity\n* Fixed gzip compression\n* Unit tests now running against all options\n* Removed `setOptions` method, must be passed into the `load` method\n\n### 11/03/2014 - 1.4\n* Now using Composer\n\n### 16/07/2013 - 1.3\n* Changed the load method to static so that multiple instances can be loaded without conflict (use Flintstone::load now instead of $db->load)\n* Exception thrown is now FlintstoneException\n\n### 23/01/2013 - 1.2\n* Removed the multibyte unserialize method as it seems to work without\n\n### 22/06/2012 - 1.1\n* Added new method getKeys() to return an array of keys in the database\n\n### 17/06/2011 - 1.0\n* Initial release"
  },
  {
    "path": "LICENSE.md",
    "content": "# MIT License\n\nCopyright (c) 2010-2017 Jason M\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and\nassociated documentation files (the \"Software\"), to deal in the Software without restriction,\nincluding without limitation the rights to use, copy, modify, merge, publish, distribute,\nsublicense, and/or sell copies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial\nportions of the Software.\n\n**THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT\nNOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT\nOF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**\n"
  },
  {
    "path": "README.md",
    "content": "Flintstone\n==========\n\n[![Total Downloads](https://img.shields.io/packagist/dm/fire015/flintstone.svg)](https://packagist.org/packages/fire015/flintstone)\n[![Build Status](https://travis-ci.com/fire015/flintstone.svg?branch=master)](https://travis-ci.com/github/fire015/flintstone)\n\nA key/value database store using flat files for PHP.\n\nFeatures include:\n\n* Memory efficient\n* File locking\n* Caching\n* Gzip compression\n* Easy to use\n\n### Installation\n\nThe easiest way to install Flintstone is via [composer](http://getcomposer.org/). Run the following command to install it.\n\n```\ncomposer require fire015/flintstone\n```\n\n```php\n<?php\nrequire 'vendor/autoload.php';\n\nuse Flintstone\\Flintstone;\n\n$users = new Flintstone('users', ['dir' => '/path/to/database/dir/']);\n```\n\n### Requirements\n\n- PHP 7.3+\n\n### Data types\n\nFlintstone can store any data type that can be formatted into a string. By default this uses `serialize()`. See [Changing the formatter](#changing-the-formatter) for more details.\n\n### Options\n\n|Name\t\t\t\t|Type\t\t|Default Value\t|Description\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n|---\t\t\t\t|---\t\t|---\t\t\t\t|---\t\t\t\t\t\t\t\t\t\t\t\t\t\t|\n|dir\t\t\t\t|string\t\t\t\t|the current working directory\t\t\t|The directory where the database files are stored (this should be somewhere that is not web accessible) e.g. /path/to/database/\t\t\t|\n|ext\t\t\t\t|string\t\t\t\t|.dat\t\t|The database file extension to use\t\t\t\t\t\t\t|\n|gzip\t\t\t\t|boolean\t\t\t|false\t\t|Use gzip to compress the database\t\t\t\t\t\t\t|\n|cache\t\t\t\t|boolean or object\t|true\t\t|Whether to cache `get()` results for faster data retrieval\t\t\t\t\t\t\t\t|\n|formatter\t\t\t|null or object\t\t|null\t\t|The formatter class used to encode/decode data\t\t\t\t|\n|swap_memory_limit\t|integer\t\t\t|2097152\t|The amount of memory to use before writing to a temporary file\t|\n\n\n### Usage examples\n\n```php\n<?php\n\n// Load a database\n$users = new Flintstone('users', ['dir' => '/path/to/database/dir/']);\n\n// Set a key\n$users->set('bob', ['email' => 'bob@site.com', 'password' => '123456']);\n\n// Get a key\n$user = $users->get('bob');\necho 'Bob, your email is ' . $user['email'];\n\n// Retrieve all key names\n$keys = $users->getKeys(); // returns array('bob')\n\n// Retrieve all data\n$data = $users->getAll(); // returns array('bob' => array('email' => 'bob@site.com', 'password' => '123456'));\n\n// Delete a key\n$users->delete('bob');\n\n// Flush the database\n$users->flush();\n```\n\n### Changing the formatter\nBy default Flintstone will encode/decode data using PHP's serialize functions, however you can override this with your own class if you prefer.\n\nJust make sure it implements `Flintstone\\Formatter\\FormatterInterface` and then you can provide it as the `formatter` option.\n\nIf you wish to use JSON as the formatter, Flintstone already ships with this as per the example below:\n\n```php\n<?php\nrequire 'vendor/autoload.php';\n\nuse Flintstone\\Flintstone;\nuse Flintstone\\Formatter\\JsonFormatter;\n\n$users = new Flintstone('users', [\n    'dir' => __DIR__,\n    'formatter' => new JsonFormatter()\n]);\n```\n\n### Changing the cache\nTo speed up data retrieval Flintstone can store the results of `get()` in a cache store. By default this uses a simple array that only persist's for as long as the `Flintstone` object exists.\n\nIf you want to use your own cache store (such as Memcached) you can pass a class as the `cache` option. Just make sure it implements `Flintstone\\Cache\\CacheInterface`.\n"
  },
  {
    "path": "UPGRADE.md",
    "content": "Upgrading from version 1.x to 2.x\n=================================\n\nAs Flintstone is no longer loaded statically the major change required is to switch from using the static `load` method to just instantiating a new instance of Flinstone.\n\nThe `FlinstoneDB` class has also been removed and `Flintstone\\FlintstoneException` is now `Flintstone\\Exception`.\n\n### Version 1.x:\n\n```php\n<?php\nrequire 'vendor/autoload.php';\n\nuse Flintstone\\Flintstone;\nuse Flintstone\\FlintstoneException;\n\ntry {\n    $users = Flintstone::load('users', array('dir' => '/path/to/database/dir/'));\n}\ncatch (FlintstoneException $e) {\n\n}\n```\n\n### Version 2.x:\n\n```php\n<?php\nrequire 'vendor/autoload.php';\n\nuse Flintstone\\Flintstone;\nuse Flintstone\\Exception;\n\ntry {\n    $users = new Flintstone('users', array('dir' => '/path/to/database/dir/'));\n}\ncatch (Exception $e) {\n\n}\n```\n\nSee CHANGELOG.md for further changes."
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"fire015/flintstone\",\n    \"type\": \"library\",\n    \"description\": \"A key/value database store using flat files for PHP\",\n    \"keywords\": [\"flintstone\", \"database\", \"cache\", \"files\", \"memory\"],\n    \"homepage\": \"https://github.com/fire015/flintstone\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Jason M\",\n            \"email\": \"emailfire@gmail.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=7.3\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Flintstone\\\\\": \"src/\"\n        }\n    },\n    \"require-dev\" : {\n        \"phpunit/phpunit\": \"^9\"\n    }\n}\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" backupGlobals=\"false\" backupStaticAttributes=\"false\" bootstrap=\"vendor/autoload.php\" colors=\"true\" convertErrorsToExceptions=\"true\" convertNoticesToExceptions=\"true\" convertWarningsToExceptions=\"true\" processIsolation=\"false\" stopOnFailure=\"false\" verbose=\"true\" xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/9.3/phpunit.xsd\">\n  <coverage processUncoveredFiles=\"true\">\n    <include>\n      <directory suffix=\".php\">./src</directory>\n    </include>\n  </coverage>\n  <testsuites>\n    <testsuite name=\"Flintstone Test Suite\">\n      <directory>./tests</directory>\n    </testsuite>\n  </testsuites>\n</phpunit>\n"
  },
  {
    "path": "src/Cache/ArrayCache.php",
    "content": "<?php\n\nnamespace Flintstone\\Cache;\n\nclass ArrayCache implements CacheInterface\n{\n    /**\n     * Cache data.\n     *\n     * @var array\n     */\n    protected $cache = [];\n\n    /**\n     * {@inheritdoc}\n     */\n    public function contains($key)\n    {\n        return array_key_exists($key, $this->cache);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function get($key)\n    {\n        return $this->cache[$key];\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function set($key, $data)\n    {\n        $this->cache[$key] = $data;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function delete($key)\n    {\n        unset($this->cache[$key]);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function flush()\n    {\n        $this->cache = [];\n    }\n}\n"
  },
  {
    "path": "src/Cache/CacheInterface.php",
    "content": "<?php\n\nnamespace Flintstone\\Cache;\n\ninterface CacheInterface\n{\n    /**\n     * Check if a key exists in the cache.\n     *\n     * @param string $key\n     *\n     * @return bool\n     */\n    public function contains($key);\n\n    /**\n     * Get a key from the cache.\n     *\n     * @param string $key\n     *\n     * @return mixed\n     */\n    public function get($key);\n\n    /**\n     * Set a key in the cache.\n     *\n     * @param string $key\n     * @param mixed $data\n     */\n    public function set($key, $data);\n\n    /**\n     * Delete a key from the cache.\n     *\n     * @param string $key\n     */\n    public function delete($key);\n\n    /**\n     * Flush the cache.\n     */\n    public function flush();\n}\n"
  },
  {
    "path": "src/Config.php",
    "content": "<?php\n\nnamespace Flintstone;\n\nuse Flintstone\\Cache\\ArrayCache;\nuse Flintstone\\Cache\\CacheInterface;\nuse Flintstone\\Formatter\\FormatterInterface;\nuse Flintstone\\Formatter\\SerializeFormatter;\n\nclass Config\n{\n    /**\n     * Config.\n     *\n     * @var array\n     */\n    protected $config = [];\n\n    /**\n     * Constructor.\n     *\n     * @param array $config\n     */\n    public function __construct(array $config = [])\n    {\n        $config = $this->normalizeConfig($config);\n        $this->setDir($config['dir']);\n        $this->setExt($config['ext']);\n        $this->setGzip($config['gzip']);\n        $this->setCache($config['cache']);\n        $this->setFormatter($config['formatter']);\n        $this->setSwapMemoryLimit($config['swap_memory_limit']);\n    }\n\n    /**\n     * Normalize the user supplied config.\n     *\n     * @param array $config\n     *\n     * @return array\n     */\n    protected function normalizeConfig(array $config): array\n    {\n        $defaultConfig = [\n            'dir' => getcwd(),\n            'ext' => '.dat',\n            'gzip' => false,\n            'cache' => true,\n            'formatter' => null,\n            'swap_memory_limit' => 2097152, // 2MB\n        ];\n\n        return array_replace($defaultConfig, $config);\n    }\n\n    /**\n     * Get the dir.\n     *\n     * @return string\n     */\n    public function getDir(): string\n    {\n        return $this->config['dir'];\n    }\n\n    /**\n     * Set the dir.\n     *\n     * @param string $dir\n     *\n     * @throws Exception\n     */\n    public function setDir(string $dir)\n    {\n        if (!is_dir($dir)) {\n            throw new Exception('Directory does not exist: ' . $dir);\n        }\n\n        $this->config['dir'] = rtrim($dir, '/\\\\') . DIRECTORY_SEPARATOR;\n    }\n\n    /**\n     * Get the ext.\n     *\n     * @return string\n     */\n    public function getExt(): string\n    {\n        if ($this->useGzip()) {\n            return $this->config['ext'] . '.gz';\n        }\n\n        return $this->config['ext'];\n    }\n\n    /**\n     * Set the ext.\n     *\n     * @param string $ext\n     */\n    public function setExt(string $ext)\n    {\n        if (substr($ext, 0, 1) !== '.') {\n            $ext = '.' . $ext;\n        }\n\n        $this->config['ext'] = $ext;\n    }\n\n    /**\n     * Use gzip?\n     *\n     * @return bool\n     */\n    public function useGzip(): bool\n    {\n        return $this->config['gzip'];\n    }\n\n    /**\n     * Set gzip.\n     *\n     * @param bool $gzip\n     */\n    public function setGzip(bool $gzip)\n    {\n        $this->config['gzip'] = $gzip;\n    }\n\n    /**\n     * Get the cache.\n     *\n     * @return CacheInterface|false\n     */\n    public function getCache()\n    {\n        return $this->config['cache'];\n    }\n\n    /**\n     * Set the cache.\n     *\n     * @param mixed $cache\n     *\n     * @throws Exception\n     */\n    public function setCache($cache)\n    {\n        if (!is_bool($cache) && !$cache instanceof CacheInterface) {\n            throw new Exception('Cache must be a boolean or an instance of Flintstone\\Cache\\CacheInterface');\n        }\n\n        if ($cache === true) {\n            $cache = new ArrayCache();\n        }\n\n        $this->config['cache'] = $cache;\n    }\n\n    /**\n     * Get the formatter.\n     *\n     * @return FormatterInterface\n     */\n    public function getFormatter(): FormatterInterface\n    {\n        return $this->config['formatter'];\n    }\n\n    /**\n     * Set the formatter.\n     *\n     * @param FormatterInterface|null $formatter\n     *\n     * @throws Exception\n     */\n    public function setFormatter($formatter)\n    {\n        if ($formatter === null) {\n            $formatter = new SerializeFormatter();\n        }\n\n        if (!$formatter instanceof FormatterInterface) {\n            throw new Exception('Formatter must be an instance of Flintstone\\Formatter\\FormatterInterface');\n        }\n\n        $this->config['formatter'] = $formatter;\n    }\n\n    /**\n     * Get the swap memory limit.\n     *\n     * @return int\n     */\n    public function getSwapMemoryLimit(): int\n    {\n        return $this->config['swap_memory_limit'];\n    }\n\n    /**\n     * Set the swap memory limit.\n     *\n     * @param int $limit\n     */\n    public function setSwapMemoryLimit(int $limit)\n    {\n        $this->config['swap_memory_limit'] = $limit;\n    }\n}\n"
  },
  {
    "path": "src/Database.php",
    "content": "<?php\n\nnamespace Flintstone;\n\nuse SplFileObject;\nuse SplTempFileObject;\n\nclass Database\n{\n    /**\n     * File read flag.\n     *\n     * @var int\n     */\n    const FILE_READ = 1;\n\n    /**\n     * File write flag.\n     *\n     * @var int\n     */\n    const FILE_WRITE = 2;\n\n    /**\n     * File append flag.\n     *\n     * @var int\n     */\n    const FILE_APPEND = 3;\n\n    /**\n     * File access mode.\n     *\n     * @var array\n     */\n    protected $fileAccessMode = [\n        self::FILE_READ => [\n            'mode' => 'rb',\n            'operation' => LOCK_SH,\n        ],\n        self::FILE_WRITE => [\n            'mode' => 'wb',\n            'operation' => LOCK_EX,\n        ],\n        self::FILE_APPEND => [\n            'mode' => 'ab',\n            'operation' => LOCK_EX,\n        ],\n    ];\n\n    /**\n     * Database name.\n     *\n     * @var string\n     */\n    protected $name;\n\n    /**\n     * Config class.\n     *\n     * @var Config\n     */\n    protected $config;\n\n    /**\n     * Constructor.\n     *\n     * @param string $name\n     * @param Config|null $config\n     */\n    public function __construct(string $name, Config $config = null)\n    {\n        $this->setName($name);\n\n        if ($config) {\n            $this->setConfig($config);\n        }\n    }\n\n    /**\n     * Get the database name.\n     *\n     * @return string\n     */\n    public function getName(): string\n    {\n        return $this->name;\n    }\n\n    /**\n     * Set the database name.\n     *\n     * @param string $name\n     *\n     * @throws Exception\n     */\n    public function setName(string $name)\n    {\n        Validation::validateDatabaseName($name);\n        $this->name = $name;\n    }\n\n    /**\n     * Get the config.\n     *\n     * @return Config\n     */\n    public function getConfig(): Config\n    {\n        return $this->config;\n    }\n\n    /**\n     * Set the config.\n     *\n     * @param Config $config\n     */\n    public function setConfig(Config $config)\n    {\n        $this->config = $config;\n    }\n\n    /**\n     * Get the path to the database file.\n     *\n     * @return string\n     */\n    public function getPath(): string\n    {\n        return $this->config->getDir() . $this->getName() . $this->config->getExt();\n    }\n\n    /**\n     * Open the database file.\n     *\n     * @param int $mode\n     *\n     * @throws Exception\n     *\n     * @return SplFileObject\n     */\n    protected function openFile(int $mode): SplFileObject\n    {\n        $path = $this->getPath();\n\n        if (!is_file($path) && !@touch($path)) {\n            throw new Exception('Could not create file: ' . $path);\n        }\n\n        if (!is_readable($path) || !is_writable($path)) {\n            throw new Exception('File does not have permission for read and write: ' . $path);\n        }\n\n        if ($this->getConfig()->useGzip()) {\n            $path = 'compress.zlib://' . $path;\n        }\n\n        $res = $this->fileAccessMode[$mode];\n        $file = new SplFileObject($path, $res['mode']);\n\n        if ($mode === self::FILE_READ) {\n            $file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);\n        }\n\n        if (!$this->getConfig()->useGzip() && !$file->flock($res['operation'])) {\n            $file = null;\n            throw new Exception('Could not lock file: ' . $path);\n        }\n\n        return $file;\n    }\n\n    /**\n     * Open a temporary file.\n     *\n     * @return SplTempFileObject\n     */\n    public function openTempFile(): SplTempFileObject\n    {\n        return new SplTempFileObject($this->getConfig()->getSwapMemoryLimit());\n    }\n\n    /**\n     * Close the database file.\n     *\n     * @param SplFileObject $file\n     *\n     * @throws Exception\n     */\n    protected function closeFile(SplFileObject &$file)\n    {\n        if (!$this->getConfig()->useGzip() && !$file->flock(LOCK_UN)) {\n            $file = null;\n            throw new Exception('Could not unlock file');\n        }\n\n        $file = null;\n    }\n\n    /**\n     * Read lines from the database file.\n     *\n     * @return \\Generator\n     */\n    public function readFromFile(): \\Generator\n    {\n        $file = $this->openFile(static::FILE_READ);\n\n        try {\n            foreach ($file as $line) {\n                yield new Line($line);\n            }\n        } finally {\n            $this->closeFile($file);\n        }\n    }\n\n    /**\n     * Append a line to the database file.\n     *\n     * @param string $line\n     */\n    public function appendToFile(string $line)\n    {\n        $file = $this->openFile(static::FILE_APPEND);\n        $file->fwrite($line);\n        $this->closeFile($file);\n    }\n\n    /**\n     * Flush the database file.\n     */\n    public function flushFile()\n    {\n        $file = $this->openFile(static::FILE_WRITE);\n        $this->closeFile($file);\n    }\n\n    /**\n     * Write temporary file contents to database file.\n     *\n     * @param SplTempFileObject $tmpFile\n     */\n    public function writeTempToFile(SplTempFileObject &$tmpFile)\n    {\n        $file = $this->openFile(static::FILE_WRITE);\n\n        foreach ($tmpFile as $line) {\n            $file->fwrite($line);\n        }\n\n        $this->closeFile($file);\n        $tmpFile = null;\n    }\n}\n"
  },
  {
    "path": "src/Exception.php",
    "content": "<?php\n\nnamespace Flintstone;\n\nclass Exception extends \\Exception\n{\n}\n"
  },
  {
    "path": "src/Flintstone.php",
    "content": "<?php\n\nnamespace Flintstone;\n\nclass Flintstone\n{\n    /**\n     * Flintstone version.\n     *\n     * @var string\n     */\n    const VERSION = '2.3';\n\n    /**\n     * Database class.\n     *\n     * @var Database\n     */\n    protected $database;\n\n    /**\n     * Config class.\n     *\n     * @var Config\n     */\n    protected $config;\n\n    /**\n     * Constructor.\n     *\n     * @param Database|string $database\n     * @param Config|array $config\n     */\n    public function __construct($database, $config)\n    {\n        if (is_string($database)) {\n            $database = new Database($database);\n        }\n\n        if (is_array($config)) {\n            $config = new Config($config);\n        }\n\n        $this->setDatabase($database);\n        $this->setConfig($config);\n    }\n\n    /**\n     * Get the database.\n     *\n     * @return Database\n     */\n    public function getDatabase(): Database\n    {\n        return $this->database;\n    }\n\n    /**\n     * Set the database.\n     *\n     * @param Database $database\n     */\n    public function setDatabase(Database $database)\n    {\n        $this->database = $database;\n    }\n\n    /**\n     * Get the config.\n     *\n     * @return Config\n     */\n    public function getConfig(): Config\n    {\n        return $this->config;\n    }\n\n    /**\n     * Set the config.\n     *\n     * @param Config $config\n     */\n    public function setConfig(Config $config)\n    {\n        $this->config = $config;\n        $this->getDatabase()->setConfig($config);\n    }\n\n    /**\n     * Get a key from the database.\n     *\n     * @param string $key\n     *\n     * @return mixed\n     */\n    public function get(string $key)\n    {\n        Validation::validateKey($key);\n\n        // Fetch the key from cache\n        if ($cache = $this->getConfig()->getCache()) {\n            if ($cache->contains($key)) {\n                return $cache->get($key);\n            }\n        }\n\n        // Fetch the key from database\n        $file = $this->getDatabase()->readFromFile();\n        $data = false;\n\n        foreach ($file as $line) {\n            /** @var Line $line */\n            if ($line->getKey() == $key) {\n                $data = $this->decodeData($line->getData());\n                break;\n            }\n        }\n\n        // Save the data to cache\n        if ($cache && $data !== false) {\n            $cache->set($key, $data);\n        }\n\n        return $data;\n    }\n\n    /**\n     * Set a key in the database.\n     *\n     * @param string $key\n     * @param mixed $data\n     */\n    public function set(string $key, $data)\n    {\n        Validation::validateKey($key);\n\n        // If the key already exists we need to replace it\n        if ($this->get($key) !== false) {\n            $this->replace($key, $data);\n            return;\n        }\n\n        // Write the key to the database\n        $this->getDatabase()->appendToFile($this->getLineString($key, $data));\n\n        // Delete the key from cache\n        if ($cache = $this->getConfig()->getCache()) {\n            $cache->delete($key);\n        }\n    }\n\n    /**\n     * Delete a key from the database.\n     *\n     * @param string $key\n     */\n    public function delete(string $key)\n    {\n        Validation::validateKey($key);\n\n        if ($this->get($key) !== false) {\n            $this->replace($key, false);\n        }\n    }\n\n    /**\n     * Flush the database.\n     */\n    public function flush()\n    {\n        $this->getDatabase()->flushFile();\n\n        // Flush the cache\n        if ($cache = $this->getConfig()->getCache()) {\n            $cache->flush();\n        }\n    }\n\n    /**\n     * Get all keys from the database.\n     *\n     * @return array\n     */\n    public function getKeys(): array\n    {\n        $keys = [];\n        $file = $this->getDatabase()->readFromFile();\n\n        foreach ($file as $line) {\n            /** @var Line $line */\n            $keys[] = $line->getKey();\n        }\n\n        return $keys;\n    }\n\n    /**\n     * Get all data from the database.\n     *\n     * @return array\n     */\n    public function getAll(): array\n    {\n        $data = [];\n        $file = $this->getDatabase()->readFromFile();\n\n        foreach ($file as $line) {\n            /** @var Line $line */\n            $data[$line->getKey()] = $this->decodeData($line->getData());\n        }\n\n        return $data;\n    }\n\n    /**\n     * Replace a key in the database.\n     *\n     * @param string $key\n     * @param mixed $data\n     */\n    protected function replace(string $key, $data)\n    {\n        // Write a new database to a temporary file\n        $tmpFile = $this->getDatabase()->openTempFile();\n        $file = $this->getDatabase()->readFromFile();\n\n        foreach ($file as $line) {\n            /** @var Line $line */\n            if ($line->getKey() == $key) {\n                if ($data !== false) {\n                    $tmpFile->fwrite($this->getLineString($key, $data));\n                }\n            } else {\n                $tmpFile->fwrite($line->getLine() . \"\\n\");\n            }\n        }\n\n        $tmpFile->rewind();\n\n        // Overwrite the database with the temporary file\n        $this->getDatabase()->writeTempToFile($tmpFile);\n\n        // Delete the key from cache\n        if ($cache = $this->getConfig()->getCache()) {\n            $cache->delete($key);\n        }\n    }\n\n    /**\n     * Get the line string to write.\n     *\n     * @param string $key\n     * @param mixed $data\n     *\n     * @return string\n     */\n    protected function getLineString(string $key, $data): string\n    {\n        return $key . '=' . $this->encodeData($data) . \"\\n\";\n    }\n\n    /**\n     * Decode a string into data.\n     *\n     * @param string $data\n     *\n     * @return mixed\n     */\n    protected function decodeData(string $data)\n    {\n        return $this->getConfig()->getFormatter()->decode($data);\n    }\n\n    /**\n     * Encode data into a string.\n     *\n     * @param mixed $data\n     *\n     * @return string\n     */\n    protected function encodeData($data): string\n    {\n        return $this->getConfig()->getFormatter()->encode($data);\n    }\n}\n"
  },
  {
    "path": "src/Formatter/FormatterInterface.php",
    "content": "<?php\n\nnamespace Flintstone\\Formatter;\n\ninterface FormatterInterface\n{\n    /**\n     * Encode data into a string.\n     *\n     * @param mixed $data\n     *\n     * @return string\n     */\n    public function encode($data): string;\n\n    /**\n     * Decode a string into data.\n     *\n     * @param string $data\n     *\n     * @return mixed\n     */\n    public function decode(string $data);\n}\n"
  },
  {
    "path": "src/Formatter/JsonFormatter.php",
    "content": "<?php\n\nnamespace Flintstone\\Formatter;\n\nuse Flintstone\\Exception;\n\nclass JsonFormatter implements FormatterInterface\n{\n    /**\n     * @var bool\n     */\n    private $assoc;\n\n    public function __construct(bool $assoc = true)\n    {\n        $this->assoc = $assoc;\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function encode($data): string\n    {\n        $result = json_encode($data);\n\n        if (json_last_error() === JSON_ERROR_NONE) {\n            return $result;\n        }\n\n        throw new Exception(json_last_error_msg());\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function decode(string $data)\n    {\n        $result = json_decode($data, $this->assoc);\n\n        if (json_last_error() === JSON_ERROR_NONE) {\n            return $result;\n        }\n\n        throw new Exception(json_last_error_msg());\n    }\n}\n"
  },
  {
    "path": "src/Formatter/SerializeFormatter.php",
    "content": "<?php\n\nnamespace Flintstone\\Formatter;\n\nclass SerializeFormatter implements FormatterInterface\n{\n    /**\n     * {@inheritdoc}\n     */\n    public function encode($data): string\n    {\n        return serialize($this->preserveLines($data, false));\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function decode(string $data)\n    {\n        return $this->preserveLines(unserialize($data), true);\n    }\n\n    /**\n     * Preserve new lines, recursive function.\n     *\n     * @param mixed $data\n     * @param bool $reverse\n     *\n     * @return mixed\n     */\n    protected function preserveLines($data, bool $reverse)\n    {\n        $search = [\"\\n\", \"\\r\"];\n        $replace = ['\\\\n', '\\\\r'];\n\n        if ($reverse) {\n            $search = ['\\\\n', '\\\\r'];\n            $replace = [\"\\n\", \"\\r\"];\n        }\n\n        if (is_string($data)) {\n            $data = str_replace($search, $replace, $data);\n        } elseif (is_array($data)) {\n            foreach ($data as &$value) {\n                $value = $this->preserveLines($value, $reverse);\n            }\n            unset($value);\n        }\n\n        return $data;\n    }\n}\n"
  },
  {
    "path": "src/Line.php",
    "content": "<?php\n\nnamespace Flintstone;\n\nclass Line\n{\n    /**\n     * @var string\n     */\n    protected $line;\n\n    /**\n     * @var array\n     */\n    protected $pieces = [];\n\n    public function __construct(string $line)\n    {\n        $this->line = $line;\n        $this->pieces = explode('=', $line, 2);\n    }\n\n    public function getLine(): string\n    {\n        return $this->line;\n    }\n\n    public function getKey(): string\n    {\n        return $this->pieces[0];\n    }\n\n    public function getData(): string\n    {\n        return $this->pieces[1];\n    }\n}\n"
  },
  {
    "path": "src/Validation.php",
    "content": "<?php\n\nnamespace Flintstone;\n\nclass Validation\n{\n    /**\n     * Validate the key.\n     *\n     * @param string $key\n     *\n     * @throws Exception\n     */\n    public static function validateKey(string $key)\n    {\n        if (empty($key) || !preg_match('/^[\\w-]+$/', $key)) {\n            throw new Exception('Invalid characters in key');\n        }\n    }\n\n    /**\n     * Check the database name is valid.\n     *\n     * @param string $name\n     *\n     * @throws Exception\n     */\n    public static function validateDatabaseName(string $name)\n    {\n        if (empty($name) || !preg_match('/^[\\w-]+$/', $name)) {\n            throw new Exception('Invalid characters in database name');\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Cache/ArrayCacheTest.php",
    "content": "<?php\n\nuse Flintstone\\Cache\\ArrayCache;\n\nclass ArrayCacheTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @var ArrayCache\n     */\n    private $cache;\n\n    protected function setUp(): void\n    {\n        $this->cache = new ArrayCache();\n    }\n\n    /**\n     * @test\n     */\n    public function canGetAndSet()\n    {\n        $this->cache->set('foo', 'bar');\n        $this->assertTrue($this->cache->contains('foo'));\n        $this->assertEquals('bar', $this->cache->get('foo'));\n    }\n\n    /**\n     * @test\n     */\n    public function canDelete()\n    {\n        $this->cache->set('foo', 'bar');\n        $this->cache->delete('foo');\n        $this->assertFalse($this->cache->contains('foo'));\n    }\n\n    /**\n     * @test\n     */\n    public function canFlush()\n    {\n        $this->cache->set('foo', 'bar');\n        $this->cache->flush();\n        $this->assertFalse($this->cache->contains('foo'));\n    }\n}\n"
  },
  {
    "path": "tests/ConfigTest.php",
    "content": "<?php\n\nuse Flintstone\\Cache\\ArrayCache;\nuse Flintstone\\Config;\nuse Flintstone\\Formatter\\JsonFormatter;\nuse Flintstone\\Formatter\\SerializeFormatter;\n\nclass ConfigTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @test\n     */\n    public function defaultConfigIsSet()\n    {\n        $config = new Config();\n        $this->assertEquals(getcwd().DIRECTORY_SEPARATOR, $config->getDir());\n        $this->assertEquals('.dat', $config->getExt());\n        $this->assertFalse($config->useGzip());\n        $this->assertInstanceOf(ArrayCache::class, $config->getCache());\n        $this->assertInstanceOf(SerializeFormatter::class, $config->getFormatter());\n        $this->assertEquals(2097152, $config->getSwapMemoryLimit());\n    }\n\n    /**\n     * @test\n     */\n    public function constructorConfigOverride()\n    {\n        $config = new Config([\n            'dir' => __DIR__,\n            'ext' => 'test',\n            'gzip' => true,\n            'cache' => false,\n            'formatter' => null,\n            'swap_memory_limit' => 100,\n        ]);\n\n        $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR, $config->getDir());\n        $this->assertEquals('.test.gz', $config->getExt());\n        $this->assertTrue($config->useGzip());\n        $this->assertFalse($config->getCache());\n        $this->assertInstanceOf(SerializeFormatter::class, $config->getFormatter());\n        $this->assertEquals(100, $config->getSwapMemoryLimit());\n    }\n\n    /**\n     * @test\n     */\n    public function setValidFormatter()\n    {\n        $config = new Config();\n        $config->setFormatter(new JsonFormatter());\n        $this->assertInstanceOf(JsonFormatter::class, $config->getFormatter());\n    }\n\n    /**\n     * @test\n     */\n    public function setInvalidFormatter()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        $config = new Config();\n        $config->setFormatter(new self());\n    }\n\n    /**\n     * @test\n     */\n    public function invalidDirSet()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        $config = new Config();\n        $config->setDir('/x/y/z/foo');\n    }\n\n    /**\n     * @test\n     */\n    public function invalidCacheSet()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        $config = new Config();\n        $config->setCache(new self());\n    }\n}\n"
  },
  {
    "path": "tests/DatabaseTest.php",
    "content": "<?php\n\nuse Flintstone\\Config;\nuse Flintstone\\Database;\nuse Flintstone\\Line;\n\nclass DatabaseTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @var Database\n     */\n    private $db;\n\n    protected function setUp(): void\n    {\n        $config = new Config([\n            'dir' => __DIR__,\n        ]);\n\n        $this->db = new Database('test', $config);\n    }\n\n    protected function tearDown(): void\n    {\n        if (is_file($this->db->getPath())) {\n            unlink($this->db->getPath());\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function databaseHasInvalidName()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        $config = new Config();\n        new Database('test!123', $config);\n    }\n\n    /**\n     * @test\n     */\n    public function canGetDatabaseAndConfig()\n    {\n        $this->assertEquals('test', $this->db->getName());\n        $this->assertInstanceOf(Config::class, $this->db->getConfig());\n        $this->assertEquals(__DIR__ . DIRECTORY_SEPARATOR . 'test.dat', $this->db->getPath());\n    }\n\n    /**\n     * @test\n     */\n    public function canAppendToFile()\n    {\n        $this->db->appendToFile('foo=bar');\n        $this->assertEquals('foo=bar', file_get_contents($this->db->getPath()));\n    }\n\n    /**\n     * @test\n     */\n    public function canFlushFile()\n    {\n        $this->db->appendToFile('foo=bar');\n        $this->db->flushFile();\n        $this->assertEmpty(file_get_contents($this->db->getPath()));\n    }\n\n    /**\n     * @test\n     */\n    public function canReadFromFile()\n    {\n        $this->db->appendToFile('foo=bar');\n        $file = $this->db->readFromFile();\n\n        foreach ($file as $line) {\n            $this->assertInstanceOf(Line::class, $line);\n            $this->assertEquals('foo', $line->getKey());\n            $this->assertEquals('bar', $line->getData());\n        }\n    }\n\n    /**\n     * @test\n     */\n    public function canWriteTempToFile()\n    {\n        $tmpFile = new SplTempFileObject();\n        $tmpFile->fwrite('foo=bar');\n        $tmpFile->rewind();\n\n        $this->db->writeTempToFile($tmpFile);\n        $this->assertEquals('foo=bar', file_get_contents($this->db->getPath()));\n    }\n}\n"
  },
  {
    "path": "tests/FlintstoneTest.php",
    "content": "<?php\n\nuse Flintstone\\Config;\nuse Flintstone\\Database;\nuse Flintstone\\Flintstone;\nuse Flintstone\\Formatter\\JsonFormatter;\n\nclass FlintstoneTest extends \\PHPUnit\\Framework\\TestCase\n{\n    public function testGetDatabaseAndConfig()\n    {\n        $db = new Flintstone('test', [\n            'dir' => __DIR__,\n            'cache' => false,\n        ]);\n\n        $this->assertInstanceOf(Database::class, $db->getDatabase());\n        $this->assertInstanceOf(Config::class, $db->getConfig());\n    }\n\n    /**\n     * @test\n     */\n    public function keyHasInvalidName()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        $db = new Flintstone('test', []);\n        $db->get('test!123');\n    }\n\n    /**\n     * @test\n     */\n    public function canRunAllOperations()\n    {\n        $this->runOperationsTests([\n            'dir' => __DIR__,\n            'cache' => false,\n            'gzip' => false,\n        ]);\n\n        $this->runOperationsTests([\n            'dir' => __DIR__,\n            'cache' => true,\n            'gzip' => true,\n        ]);\n\n        $this->runOperationsTests([\n            'dir' => __DIR__,\n            'cache' => false,\n            'gzip' => false,\n            'formatter' => new JsonFormatter(),\n        ]);\n    }\n\n    private function runOperationsTests(array $config)\n    {\n        $db = new Flintstone('test', $config);\n        $arr = ['foo' => \"new\\nline\"];\n\n        $this->assertFalse($db->get('foo'));\n\n        $db->set('foo', 1);\n        $db->set('name', 'john');\n        $db->set('arr', $arr);\n        $this->assertEquals(1, $db->get('foo'));\n        $this->assertEquals('john', $db->get('name'));\n        $this->assertEquals($arr, $db->get('arr'));\n\n        $db->set('foo', 2);\n        $this->assertEquals(2, $db->get('foo'));\n        $this->assertEquals('john', $db->get('name'));\n        $this->assertEquals($arr, $db->get('arr'));\n\n        $db->delete('name');\n        $this->assertFalse($db->get('name'));\n        $this->assertEquals($arr, $db->get('arr'));\n\n        $keys = $db->getKeys();\n        $this->assertEquals(2, count($keys));\n        $this->assertEquals('foo', $keys[0]);\n        $this->assertEquals('arr', $keys[1]);\n\n        $data = $db->getAll();\n        $this->assertEquals(2, count($data));\n        $this->assertEquals(2, $data['foo']);\n        $this->assertEquals($arr, $data['arr']);\n\n        $db->flush();\n        $this->assertFalse($db->get('foo'));\n        $this->assertFalse($db->get('arr'));\n        $this->assertEquals(0, count($db->getKeys()));\n        $this->assertEquals(0, count($db->getAll()));\n\n        unlink($db->getDatabase()->getPath());\n    }\n}\n"
  },
  {
    "path": "tests/Formatter/JsonFormatterTest.php",
    "content": "<?php\n\nuse Flintstone\\Formatter\\JsonFormatter;\n\nclass JsonFormatterTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @var JsonFormatter\n     */\n    private $formatter;\n\n    protected function setUp(): void\n    {\n        $this->formatter = new JsonFormatter();\n    }\n\n    /**\n     * @test\n     * @dataProvider validData\n     */\n    public function encodesValidData($originalValue, $encodedValue)\n    {\n        $this->assertSame($encodedValue, $this->formatter->encode($originalValue));\n    }\n\n    /**\n     * @test\n     * @dataProvider validData\n     */\n    public function decodesValidData($originalValue, $encodedValue)\n    {\n        $this->assertSame($originalValue, $this->formatter->decode($encodedValue));\n    }\n\n    /**\n     * @test\n     */\n    public function decodesAnObject()\n    {\n        $originalValue = (object)['foo' => 'bar'];\n        $formatter = new JsonFormatter(false);\n        $encodedValue = $formatter->encode($originalValue);\n        $this->assertEquals($originalValue, $formatter->decode($encodedValue));\n    }\n\n    /**\n     * @test\n     */\n    public function encodingInvalidDataThrowsException()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        $this->formatter->encode(chr(241));\n    }\n\n    /**\n     * @test\n     */\n    public function decodingInvalidDataThrowsException()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        $this->formatter->decode('{');\n    }\n\n    public function validData(): array\n    {\n        return [\n            [null, 'null'],\n            [1, '1'],\n            ['foo', '\"foo\"'],\n            [[\"test\", \"new\\nline\"], '[\"test\",\"new\\nline\"]'],\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Formatter/SerializeFormatterTest.php",
    "content": "<?php\n\nuse Flintstone\\Formatter\\SerializeFormatter;\n\nclass SerializeFormatterTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @var SerializeFormatter\n     */\n    private $formatter;\n\n    protected function setUp(): void\n    {\n        $this->formatter = new SerializeFormatter();\n    }\n\n    /**\n     * @test\n     * @dataProvider validData\n     */\n    public function encodesValidData($originalValue, $encodedValue)\n    {\n        $this->assertSame($encodedValue, $this->formatter->encode($originalValue));\n    }\n\n    /**\n     * @test\n     * @dataProvider validData\n     */\n    public function decodesValidData($originalValue, $encodedValue)\n    {\n        $this->assertSame($originalValue, $this->formatter->decode($encodedValue));\n    }\n\n    public function validData(): array\n    {\n        return [\n            [null, 'N;'],\n            [1, 'i:1;'],\n            ['foo', 's:3:\"foo\";'],\n            [[\"test\", \"new\\nline\"], 'a:2:{i:0;s:4:\"test\";i:1;s:9:\"new\\nline\";}'],\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/LineTest.php",
    "content": "<?php\n\nuse Flintstone\\Line;\n\nclass LineTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @var Line\n     */\n    private $line;\n\n    protected function setUp(): void\n    {\n        $this->line = new Line('foo=bar');\n    }\n\n    /**\n     * @test\n     */\n    public function canGetLine()\n    {\n        $this->assertEquals('foo=bar', $this->line->getLine());\n    }\n\n    /**\n     * @test\n     */\n    public function canGetKey()\n    {\n        $this->assertEquals('foo', $this->line->getKey());\n    }\n\n    /**\n     * @test\n     */\n    public function canGetData()\n    {\n        $this->assertEquals('bar', $this->line->getData());\n    }\n\n    /**\n     * @test\n     */\n    public function canGetKeyAndDataWithMultipleEquals()\n    {\n        $line = new Line('foo=bar=baz');\n        $this->assertEquals('foo', $line->getKey());\n        $this->assertEquals('bar=baz', $line->getData());\n    }\n}\n"
  },
  {
    "path": "tests/ValidationTest.php",
    "content": "<?php\n\nuse Flintstone\\Validation;\n\nclass ValidationTest extends \\PHPUnit\\Framework\\TestCase\n{\n    /**\n     * @test\n     */\n    public function validateKey()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        Validation::validateKey('test!123');\n    }\n\n    /**\n     * @test\n     */\n    public function validateDatabaseName()\n    {\n        $this->expectException(\\Flintstone\\Exception::class);\n        Validation::validateDatabaseName('test!123');\n    }\n}\n"
  }
]