├── litecache.sublime-project
├── .idea
├── copyright
│ └── profiles_settings.xml
├── vcs.xml
├── fileTemplates
│ └── includes
│ │ └── PHP File Header.php
├── inspectionProfiles
│ ├── profiles_settings.xml
│ └── Project_Default.xml
├── modules.xml
├── codeStyleSettings.xml
├── php.xml
├── misc.xml
└── litecache.iml
├── examples
├── sample_data
│ ├── test_ini.ini
│ ├── test_file.txt
│ ├── test_json.json
│ └── slow_script.php
├── cache-file.php
├── cache-json.php
├── cache-ini.php
├── cache-output.php
└── cache-request.php
├── .gitignore
├── .travis.yml
├── phpunit.xml
├── CONTRIBUTING.md
├── tests
├── bootstrap.php
└── SilentByte
│ └── LiteCache
│ ├── FileProducerTest.php
│ ├── IniProducerTest.php
│ ├── OutputProducerTest.php
│ ├── VirtualFileSystemTrait.php
│ ├── JsonProducerTest.php
│ ├── PathHelperTest.php
│ ├── ObjectComplexityAnalyzerTest.php
│ └── LiteCacheTest.php
├── phpdoc.dist.xml
├── psalm.xml
├── makefile
├── src
└── SilentByte
│ └── LiteCache
│ ├── CacheProducerException.php
│ ├── CacheArgumentException.php
│ ├── CacheException.php
│ ├── FileProducer.php
│ ├── IniProducer.php
│ ├── OutputProducer.php
│ ├── JsonProducer.php
│ ├── PathHelper.php
│ ├── ObjectComplexityAnalyzer.php
│ └── LiteCache.php
├── sami.php
├── LICENSE.txt
├── composer.json
├── CHANGELOG.md
└── README.md
/litecache.sublime-project:
--------------------------------------------------------------------------------
1 | {
2 | "folders":
3 | [
4 | {
5 | "path": "."
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/examples/sample_data/test_ini.ini:
--------------------------------------------------------------------------------
1 |
2 | [server]
3 | host = myhost.test.com
4 | user = root
5 | password = root
6 |
7 |
--------------------------------------------------------------------------------
/examples/sample_data/test_file.txt:
--------------------------------------------------------------------------------
1 | Hello World!
2 |
3 | The purpose of this file is to demonstrate
4 | file cache functionality.
5 |
6 | Cheers!
7 |
--------------------------------------------------------------------------------
/examples/sample_data/test_json.json:
--------------------------------------------------------------------------------
1 | {
2 | "server": {
3 | "host": "myhost.test.com",
4 | "user": "root",
5 | "password": "root"
6 | }
7 | }
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | docs/
3 | .litecache/
4 | .sami/
5 |
6 | composer.lock
7 | *.test.php
8 | *.sublime-workspace
9 |
10 | .idea/workspace.xml
11 | .idea/tasks.xml
12 | .idea/dictionaries
13 | .idea/markdown*
14 |
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Travis CI Configuration
3 | #
4 |
5 | language: php
6 |
7 | php:
8 | - 7.0
9 | - 7.1
10 |
11 | install:
12 | - composer install --no-interaction
13 |
14 | script:
15 | - vendor/bin/phpunit
16 |
17 |
--------------------------------------------------------------------------------
/.idea/fileTemplates/includes/PHP File Header.php:
--------------------------------------------------------------------------------
1 | /**
2 | * SilentByte LiteCache Library
3 | *
4 | * @copyright 2017 SilentByte
5 | * @license https://opensource.org/licenses/MIT MIT
6 | */
7 |
8 | declare(strict_types = 1);
9 |
10 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests/
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | Contributing
3 | ============
4 |
5 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you shall be licensed under the [MIT License](https://opensource.org/licenses/MIT), without any additional terms or conditions.
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | require __DIR__ . '/../vendor/autoload.php';
12 |
13 |
--------------------------------------------------------------------------------
/examples/sample_data/slow_script.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/phpdoc.dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | LiteCache
4 |
5 | docs
6 |
7 |
8 | docs
9 |
10 |
11 | src
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/cache-file.php:
--------------------------------------------------------------------------------
1 | '.litecache',
12 |
13 | // Cache objects permanently.
14 | 'ttl' => LiteCache::EXPIRE_NEVER
15 | ]);
16 |
17 | // Load the specified file and cache it.
18 | $content = $cache->cache('file-cache', new FileProducer('./sample_data/test_file.txt'));
19 |
20 | echo "---- (File Content) -------------------\n";
21 | echo $content;
22 | echo "---------------------------------------\n";
23 |
24 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 |
2 | .PHONY: all
3 | all:
4 | @echo 'LiteCache Makefile'
5 | @echo ' make clean Delete docs and cache files.'
6 | @echo ' make docs Generate PHPDocs documentation.'
7 |
8 | .PHONY: clear
9 | clean:
10 | rm -rf docs/
11 | rm -rf .litecache/*.php
12 | rm -rf examples/.litecache/*.php
13 |
14 | .PHONY: docs
15 | docs: .sami/themes/sami-silentbyte
16 | ./vendor/sami/sami/sami.php update sami.php
17 |
18 | .PHONY: check
19 | check:
20 | @echo '---- (Running Psalm) --------------------------'
21 | psalm
22 | @echo '---- (Running Unit Tests) .--------------------'
23 | phpunit
24 |
25 | .sami/themes/sami-silentbyte:
26 | git clone https://github.com/SilentByte/sami-silentbyte.git .sami/themes/sami-silentbyte
27 |
28 |
--------------------------------------------------------------------------------
/examples/cache-json.php:
--------------------------------------------------------------------------------
1 | '.litecache',
12 |
13 | // Cache objects permanently.
14 | 'ttl' => LiteCache::EXPIRE_NEVER
15 | ]);
16 |
17 | // Load the specified JSON configuration file and cache it.
18 | $config = $cache->cache('json-cache', new JsonProducer('./sample_data/test_json.json'));
19 |
20 | echo "Host: ", $config->server->host, "\n",
21 | "User: ", $config->server->user, "\n",
22 | "Password: ", $config->server->password, "\n";
23 |
24 |
--------------------------------------------------------------------------------
/examples/cache-ini.php:
--------------------------------------------------------------------------------
1 | '.litecache',
12 |
13 | // Cache objects permanently.
14 | 'ttl' => LiteCache::EXPIRE_NEVER
15 | ]);
16 |
17 | // Load the specified INI configuration file and cache it.
18 | $config = $cache->cache('ini-cache', new IniProducer('./sample_data/test_ini.ini'));
19 |
20 | echo "Host: ", $config['server']['host'], "\n",
21 | "User: ", $config['server']['user'], "\n",
22 | "Password: ", $config['server']['password'], "\n";
23 |
24 |
--------------------------------------------------------------------------------
/examples/cache-output.php:
--------------------------------------------------------------------------------
1 | '.litecache',
12 |
13 | // Cache objects for 30 seconds.
14 | 'ttl' => '30 seconds'
15 | ]);
16 |
17 | // Load the specified file and cache it.
18 | $output = $cache->cache('script-cache', new OutputProducer(function () {
19 | include('./sample_data/slow_script.php');
20 | }));
21 |
22 | echo "---- (Script Output) -------------------\n";
23 | echo $output;
24 | echo "---------------------------------------\n";
25 |
26 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/CacheProducerException.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use Throwable;
14 |
15 | /**
16 | * Will be thrown if the specified producer (for LiteCache::cache()) throws an exception.
17 | *
18 | * @package SilentByte\LiteCache
19 | */
20 | class CacheProducerException extends CacheException
21 | {
22 | /**
23 | * Creates the exception object.
24 | *
25 | * @param Throwable $previous
26 | */
27 | public function __construct(Throwable $previous)
28 | {
29 | parent::__construct('Cache producer has thrown an exception.', $previous);
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/FileProducerTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use PHPUnit\Framework\TestCase;
14 |
15 | class FileProducerTest extends TestCase
16 | {
17 | use VirtualFileSystemTrait;
18 |
19 | protected function setUp()
20 | {
21 | $this->vfs();
22 | }
23 |
24 | public function testInvokeReadsContents()
25 | {
26 | $expected = "Test\n123456789\nabcdefghijklmnopqrstuvwxyz";
27 |
28 | $this->file('test.txt', $expected);
29 |
30 | $producer = new FileProducer($this->url('root/test.txt'));
31 | $actual = $producer();
32 |
33 | $this->assertEquals($expected, $actual);
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/CacheArgumentException.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use Psr\SimpleCache\InvalidArgumentException;
14 | use Throwable;
15 |
16 | /**
17 | * Will be thrown if arguments given to caching functions are invalid or unacceptable.
18 | *
19 | * @package SilentByte\LiteCache
20 | */
21 | class CacheArgumentException extends CacheException
22 | implements InvalidArgumentException
23 | {
24 | /**
25 | * Creates the exception object.
26 | *
27 | * @param string $message
28 | * @param Throwable|null $previous
29 | */
30 | public function __construct(string $message, Throwable $previous = null)
31 | {
32 | parent::__construct($message, $previous);
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/sami.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | $version = exec('git symbolic-ref -q --short HEAD || git describe --tags --exact-match');
12 | $dir = __DIR__ . '/src';
13 |
14 | $iterator = Symfony\Component\Finder\Finder::create()
15 | ->files()
16 | ->name('*.php')
17 | ->in($dir);
18 |
19 | $options = [
20 | 'title' => 'SilentByte LiteCache 2.0 Documentation',
21 | 'theme' => 'sami-silentbyte',
22 | 'build_dir' => __DIR__ . "/docs/{$version}",
23 | 'cache_dir' => __DIR__ . "/.sami/.twig/{$version}",
24 | ];
25 |
26 | $sami = new Sami\Sami($iterator, $options);
27 |
28 | if (!is_dir(__DIR__ . '/.sami/themes')) {
29 | mkdir(__DIR__ . '/.sami/themes', 0777, true);
30 | }
31 |
32 | $templates = $sami['template_dirs'];
33 | $templates[] = __DIR__ . '/.sami/themes';
34 |
35 | $sami['template_dirs'] = $templates;
36 |
37 | return $sami;
38 |
39 |
--------------------------------------------------------------------------------
/examples/cache-request.php:
--------------------------------------------------------------------------------
1 | '.litecache',
9 |
10 | // Make cached objects expire after 10 minutes.
11 | 'ttl' => '10 minutes'
12 | ]);
13 |
14 | // Issue a Github API request and cache it under the specified name ('git-request').
15 | // Subsequent calls to $cache->cache() will be fetched from cache;
16 | // after expiration, a new request will be issued.
17 | $response = $cache->cache('git-request', function () {
18 | $ch = curl_init('https://api.github.com/users/SilentByte');
19 | curl_setopt($ch, CURLOPT_USERAGENT, 'SilentByte/litecache');
20 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
21 | return json_decode(curl_exec($ch));
22 | });
23 |
24 | echo "Name: ", $response->login, "\n",
25 | "Website: ", $response->blog, "\n",
26 | "Update: ", $response->updated_at, "\n";
27 |
28 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/CacheException.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use Exception;
14 | use Psr\SimpleCache\CacheException as PsrCacheException;
15 | use Throwable;
16 |
17 | /**
18 | * Base class for all cache related exceptions.
19 | *
20 | * @package SilentByte\LiteCache
21 | */
22 | class CacheException extends Exception
23 | implements PsrCacheException
24 | {
25 | /**
26 | * Creates the exception object.
27 | *
28 | * @param string $message Message indicating what caused the exception.
29 | * @param Throwable|null $previous The exception that was the cause of this cache exception.
30 | */
31 | public function __construct(string $message,
32 | Throwable $previous = null)
33 | {
34 | parent::__construct($message, 0, $previous);
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | MIT License
3 |
4 | Copyright (c) 2016 SilentByte
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
24 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/IniProducerTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use PHPUnit\Framework\TestCase;
14 |
15 | class IniProducerTest extends TestCase
16 | {
17 | use VirtualFileSystemTrait;
18 |
19 | protected function setUp()
20 | {
21 | $this->vfs();
22 | }
23 |
24 | public function testInvokeReadsContents()
25 | {
26 | $expected = [
27 | 'server' => [
28 | 'host' => 'myhost.test.com',
29 | 'user' => 'root',
30 | 'password' => 'root'
31 | ]
32 | ];
33 |
34 | $ini
35 | = "[server]\n"
36 | . "host = myhost.test.com\n"
37 | . "user = root\n"
38 | . "password = root\n";
39 |
40 | $this->file('test.ini', $ini);
41 |
42 | $producer = new IniProducer($this->url('root/test.ini'));
43 | $actual = $producer();
44 |
45 | $this->assertEquals($expected, $actual);
46 | }
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/.idea/litecache.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/OutputProducerTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use PHPUnit\Framework\TestCase;
14 | use RuntimeException;
15 |
16 | class OutputProducerTest extends TestCase
17 | {
18 | public function testInvokeReadsOutput()
19 | {
20 | $expected = "Test\n123456789\nabcdefghijklmnopqrstuvwxyz";
21 |
22 | $producer = new OutputProducer(function () use ($expected) {
23 | echo $expected;
24 | });
25 |
26 | $actual = $producer();
27 | $this->assertEquals($expected, $actual);
28 | }
29 |
30 | /**
31 | * @expectedException \SilentByte\LiteCache\CacheProducerException
32 | */
33 | public function testInvokeRecoversThrowingUserDefinedProducer()
34 | {
35 | $expected = "Test\n123456789\nabcdefghijklmnopqrstuvwxyz";
36 |
37 | $producer = new OutputProducer(function () use ($expected) {
38 | throw new RuntimeException('User Defined Producer.');
39 | });
40 |
41 | $producer();
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/VirtualFileSystemTrait.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use org\bovigo\vfs\vfsStream;
14 | use org\bovigo\vfs\visitor\vfsStreamStructureVisitor;
15 |
16 | trait VirtualFileSystemTrait
17 | {
18 | private $vfs;
19 |
20 | protected function vfs()
21 | {
22 | if ($this->vfs) {
23 | return $this->vfs;
24 | }
25 |
26 | $this->vfs = vfsStream::setup('root');
27 | return $this->vfs;
28 | }
29 |
30 | protected function url($url)
31 | {
32 | return vfsStream::url($url);
33 | }
34 |
35 | protected function tree()
36 | {
37 | /** @noinspection PhpUndefinedMethodInspection */
38 | return vfsStream::inspect(new vfsStreamStructureVisitor(),
39 | $this->vfs())->getStructure();
40 | }
41 |
42 | protected function file($filename, $content = null)
43 | {
44 | vfsStream::newFile($filename)
45 | ->at($this->vfs())
46 | ->setContent($content);
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "silentbyte/litecache",
3 | "description": "A lightweight, easy-to-use, and straightforward caching library for PHP.",
4 | "keywords": [
5 | "cache",
6 | "caching",
7 | "OPCache",
8 | "config"
9 | ],
10 | "homepage": "https://github.com/SilentByte/litecache",
11 | "type": "library",
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "Rico Beti",
16 | "email": "rico.beti@silentbyte.com",
17 | "homepage": "https://silentbyte.com/"
18 | }
19 | ],
20 | "suggest": {
21 | "monolog/monolog": "Allows more advanced logging of the application flow"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "SilentByte\\LiteCache\\": "src/SilentByte/LiteCache"
26 | }
27 | },
28 | "autoload-dev": {
29 | "psr-4": {
30 | "SilentByte\\LiteCache\\": "tests/SilentByte/LiteCache"
31 | }
32 | },
33 | "require": {
34 | "psr/simple-cache": "^1.0",
35 | "psr/log": "^1.0"
36 | },
37 | "require-dev": {
38 | "phpunit/phpunit": "^5.7",
39 | "mikey179/vfsstream": "^1.6",
40 | "monolog/monolog": "^1.22",
41 | "sami/sami": "^4.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/JsonProducerTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use PHPUnit\Framework\TestCase;
14 | use stdClass;
15 |
16 | class JsonProducerTest extends TestCase
17 | {
18 | use VirtualFileSystemTrait;
19 |
20 | protected function setUp()
21 | {
22 | $this->vfs();
23 | }
24 |
25 | public function testInvokeReadsContents()
26 | {
27 | $expected = new stdClass();
28 | $expected->server = new stdClass();
29 | $expected->server->host = 'myhost.test.com';
30 | $expected->server->user = 'root';
31 | $expected->server->password = 'root';
32 |
33 | $ini
34 | = "{\n"
35 | . " \"server\": {"
36 | . " \"host\": \"myhost.test.com\",\n"
37 | . " \"user\": \"root\",\n"
38 | . " \"password\": \"root\"\n"
39 | . " }\n"
40 | . "}";
41 |
42 | $this->file('test.json', $ini);
43 |
44 | $producer = new JsonProducer($this->url('root/test.json'));
45 | $actual = $producer();
46 |
47 | $this->assertEquals($expected, $actual);
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/FileProducer.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use RuntimeException;
14 |
15 | /**
16 | * Provides the ability to load a file and produces
17 | * a string object corresponding to the content.
18 | *
19 | * @package SilentByte\LiteCache
20 | */
21 | class FileProducer
22 | {
23 | /**
24 | * @var string
25 | */
26 | private $filename;
27 |
28 | /**
29 | * Creates a producer that loads the specified file.
30 | *
31 | * @param string $filename File to be loaded.
32 | */
33 | public function __construct(string $filename)
34 | {
35 | $this->filename = $filename;
36 | }
37 |
38 | /**
39 | * Loads the file and produces the object.
40 | *
41 | * @return mixed The file's content.
42 | */
43 | public function __invoke()
44 | {
45 | $content = @file_get_contents($this->filename);
46 |
47 | if ($content === null) {
48 | throw new RuntimeException("Could not load file '{$this->filename}'.");
49 | }
50 |
51 | return $content;
52 | }
53 |
54 | /**
55 | * Gets the filename.
56 | *
57 | * @return string The file's path.
58 | */
59 | public function getFileName() : string
60 | {
61 | return $this->filename;
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/IniProducer.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use RuntimeException;
14 |
15 | /**
16 | * Provides the ability to load and parse an INI configuration file
17 | * and produces an object corresponding to the data defined.
18 | *
19 | * @package SilentByte\LiteCache
20 | */
21 | class IniProducer
22 | {
23 | /**
24 | * @var string
25 | */
26 | private $filename;
27 |
28 | /**
29 | * Creates a producer that loads the specified file.
30 | *
31 | * @param string $filename INI configuration file to be loaded.
32 | */
33 | public function __construct(string $filename)
34 | {
35 | $this->filename = $filename;
36 | }
37 |
38 | /**
39 | * Loads the INI configuration file and produces the object.
40 | *
41 | * @return mixed Object defined in the INI configuration file.
42 | */
43 | public function __invoke()
44 | {
45 | $config = @parse_ini_file($this->filename, true);
46 |
47 | if ($config === null) {
48 | throw new RuntimeException("Could not load configuration file '{$this->filename}'.");
49 | }
50 |
51 | return $config;
52 | }
53 |
54 | /**
55 | * Gets the filename of the INI configuration file.
56 | *
57 | * @return string INI configuration filename.
58 | */
59 | public function getFileName() : string
60 | {
61 | return $this->filename;
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/OutputProducer.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use Throwable;
14 |
15 | /**
16 | * Provides the ability to cache the output stream
17 | * and subsequently cache it.
18 | *
19 | * @package SilentByte\LiteCache
20 | */
21 | class OutputProducer
22 | {
23 | /**
24 | * @var callable
25 | */
26 | private $producer;
27 |
28 | /**
29 | * Creates a producer that buffers the output producer by the specified producer.
30 | *
31 | * @param callable $producer User defined callable that writes content onto the output stream.
32 | */
33 | public function __construct(callable $producer)
34 | {
35 | $this->producer = $producer;
36 | }
37 |
38 | /**
39 | * Executes the producer, buffers its output and returns it.
40 | *
41 | * @return mixed The buffered content that has been written onto the output stream.
42 | *
43 | * @throws Throwable
44 | * If the user defined callable throws an exception,
45 | * it will be re-thrown by this producer.
46 | */
47 | public function __invoke()
48 | {
49 | $producer = $this->producer;
50 |
51 | if (!ob_start()) {
52 | return null;
53 | } else {
54 | try {
55 | $producer();
56 | return ob_get_clean();
57 | } catch (Throwable $t) {
58 | ob_end_clean();
59 | throw new CacheProducerException($t);
60 | }
61 | }
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/PathHelperTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use PHPUnit\Framework\TestCase;
14 |
15 | class PathHelperTest extends TestCase
16 | {
17 | use VirtualFileSystemTrait;
18 |
19 | protected function setUp()
20 | {
21 | $this->vfs();
22 | }
23 |
24 | public function testDirectoryStripsSlashes()
25 | {
26 | $this->assertEquals('/root/directory',
27 | PathHelper::directory('/root/directory/'));
28 | }
29 |
30 | public function testCombine()
31 | {
32 | $expected = 'root' . DIRECTORY_SEPARATOR
33 | . 'directory' . DIRECTORY_SEPARATOR
34 | . 'file.txt';
35 |
36 | $this->assertEquals($expected,
37 | PathHelper::combine('root',
38 | 'directory',
39 | 'file.txt'));
40 | }
41 |
42 | public function testMakePathCreatesStructure()
43 | {
44 | $path = $this->url('root/test/directory');
45 |
46 | PathHelper::makePath($path, 777);
47 | $this->assertFileExists($path);
48 | }
49 |
50 | public function testMakePathCreatesDirectoryWithPermission()
51 | {
52 | $path1 = $this->url('root/test/directory1');
53 | $path2 = $this->url('root/test/directory2');
54 |
55 | PathHelper::makePath($path1, 0777);
56 | $this->assertEquals(0777, fileperms($path1) & 0777);
57 |
58 | PathHelper::makePath($path2, 0765);
59 | $this->assertEquals(0765, fileperms($path2) & 0765);
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/JsonProducer.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use RuntimeException;
14 |
15 | /**
16 | * Provides the ability to load and parse a JSON file and
17 | * produces an object corresponding to the data defined.
18 | *
19 | * @package SilentByte\LiteCache
20 | */
21 | class JsonProducer
22 | {
23 | /**
24 | * @var string
25 | */
26 | private $filename;
27 |
28 | /**
29 | * Creates a producer that loads the specified file.
30 | *
31 | * @param string $filename JSON file to be loaded.
32 | */
33 | public function __construct(string $filename)
34 | {
35 | $this->filename = $filename;
36 | }
37 |
38 | /**
39 | * Loads the JSON file and produces the object.
40 | *
41 | * @return mixed Object defined in the JSON file.
42 | */
43 | public function __invoke()
44 | {
45 | $content = @file_get_contents($this->filename);
46 |
47 | if ($content === null) {
48 | throw new RuntimeException("Could not load file '{$this->filename}'.");
49 | }
50 |
51 | $json = json_decode($content);
52 | if ($json === null && json_last_error() !== JSON_ERROR_NONE) {
53 | throw new RuntimeException("Could not parse JSON file '{$this->filename}': " .
54 | 'Reason: ' . json_last_error_msg());
55 | }
56 |
57 | return $json;
58 | }
59 |
60 | /**
61 | * Gets the filename of the JSON file.
62 | *
63 | * @return string JSON filename.
64 | */
65 | public function getFileName() : string
66 | {
67 | return $this->filename;
68 | }
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/PathHelper.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use RuntimeException;
14 |
15 | /**
16 | * Provides useful functions for dealing with paths and filenames.
17 | *
18 | * @package SilentByte\LiteCache
19 | */
20 | final class PathHelper
21 | {
22 | /**
23 | * Disallow instantiation (Static Class).
24 | */
25 | private function __construct()
26 | {
27 | // Static Class.
28 | }
29 |
30 | /**
31 | * Treats the path as a directory and removes trailing slashes.
32 | *
33 | * @param string $path Path representing a directory.
34 | *
35 | * @return string Returns the specified path without trailing slashes.
36 | */
37 | public static function directory(string $path) : string
38 | {
39 | return rtrim($path, '/\\');
40 | }
41 |
42 | /**
43 | * Combines the specified path parts with the system's directory separator.
44 | *
45 | * @param string[] ...$parts Parts to be combined
46 | *
47 | * @return string Resulting path including all parts.
48 | */
49 | public static function combine(string ...$parts) : string
50 | {
51 | return implode(DIRECTORY_SEPARATOR, $parts);
52 | }
53 |
54 | /**
55 | * Creates a directory structure recursively at the specified path
56 | * with the given permissions.
57 | *
58 | * @param string $path Location of the directory.
59 | * @param int $permissions Permissions to be set for the directory.
60 | */
61 | public static function makePath(string $path, int $permissions)
62 | {
63 | // Nothing to do if directory already exists.
64 | if (is_dir($path)) {
65 | return;
66 | }
67 |
68 | if (!mkdir($path, $permissions, true)) {
69 | throw new RuntimeException("Directory '{$path}' could not be created.");
70 | }
71 | }
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | Change Log
3 | ==========
4 |
5 | ## 2.1.1 (2017-02-20)
6 | Version 2.1.1 fixes several issues.
7 |
8 | ### Changed
9 | - `setMultiple()` and `getMultiple()` now consider integers valid keys. PHP automatically coerces integral string keys to integers (see [PHP Manual](http://php.net/manual/en/language.types.array.php)), making it impossible to distinguish between the two types due to loss of type information.
10 | - More detailed exception messages.
11 |
12 | ### Fixed
13 | - `delete()` and `clear()` now correctly respect the current pool.
14 | - `clear()` now works correctly with option `subdivision` enabled.
15 | - String `"0"` can now be used as the name for keys, pools, and cache directories.
16 |
17 |
18 |
19 | ## 2.1 (2017-02-19)
20 | Version 2.1 is a small update adding the 'subdivision' feature.
21 |
22 | ### Added
23 | - Option `subdivision` to place cache files in sub-directories to avoid having a large number of files in the same directory.
24 |
25 | ### Fixed
26 | - Keys are now properly validated according to PSR-16.
27 | - General improvements and fixes in code base and tests.
28 |
29 |
30 |
31 | ## 2.0 (2017-01-26)
32 | Version 2.0 implements major improvements in terms of the API, error handling, error reporting and logging, and general stability. Additionally, LiteCache is now [PSR-16](http://www.php-fig.org/psr/psr-16/) compliant.
33 |
34 | ### Added
35 | - PSR-16 compatibility.
36 | - Lots of unit tests for core functionality.
37 | - Ability to use multiple independent caching pools (see option `pool`).
38 | - A [PSR-3](http://www.php-fig.org/psr/psr-3/) compliant logger can now be specified to capture logging output (see option `logger`).
39 | - Option `strategy` to tune strategy behavior.
40 | - EXPIRE_NEVER and EXPIRE_IMMEDIATELY constants.
41 | - `ObjectComplexityAnalyzer` class to analyze objects and choose optimal caching strategy.
42 | - TTL values can now be specified as an integer indicating the number of seconds, as a DateInterval instance, or as a duration string (e.g. '20 seconds').
43 | - Ability to specify an object's default value on cache miss.
44 | - All required methods from PSR-16's CacheInterface: `get()`, `set()`, `delete()`, `clear()`, `getMultiple()`, `setMultiple()`, `deleteMultiple()`, and `has()`.
45 | - `getCacheDirectory()` method.
46 | - `getDefaultTimeToLive()` method.
47 | - `OutputProducer` class to capture output.
48 | - `FileProducer` class for easy caching of files.
49 | - `IniProducer` class for dealing with `*.ini` files.
50 | - `JsonProducer` class for dealing with `*.json` files.
51 | - `CacheArgumentException` class for cache related issues with arguments.
52 | - `CacheProducerException` class for exceptions occurring in producers.
53 |
54 | ### Changed
55 | - Renamed option `expiration` to `ttl`.
56 | - Renamed method `get()` to `cache()` due to name clash with PSR-16.
57 | - Updated method `cache()` (previously `get()`).
58 | - Updated signature of previously existing methods to comply with PSR-16.
59 | - `CacheException` class is now base class for exceptions.
60 | - Updated all examples accordingly.
61 | - Changed documentation generator from PHPDoc to Sami.
62 | - Removed `CacheObject` class which became unnecessary due to improved caching strategies.
63 |
64 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/ObjectComplexityAnalyzer.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | /**
14 | * Examines an object and determines whether it is 'simple' or 'complex'.
15 | * - Simple: null, integer, float, string, boolean, and array (only containing 'simple' objects).
16 | * - Complex: object, resource, and array (containing 'complex' objects).
17 | *
18 | * @package SilentByte\LiteCache
19 | */
20 | class ObjectComplexityAnalyzer
21 | {
22 | const UNLIMITED = -1;
23 | const SIMPLE = 0;
24 | const COMPLEX = 1;
25 |
26 | /**
27 | * @var int
28 | */
29 | private $maxEntryCount;
30 |
31 | /**
32 | * @var int
33 | */
34 | private $maxDepth;
35 |
36 | /**
37 | * @var int
38 | */
39 | private $currentDepth;
40 |
41 | /**
42 | * @var int
43 | */
44 | private $currentEntryCount;
45 |
46 | /**
47 | * Creates the object based on the specified restrictions.
48 | *
49 | * @param int $maxEntryCount Total maximum number of entries.
50 | * @param int $maxDepth Total maximum depth.
51 | */
52 | public function __construct(int $maxEntryCount = self::UNLIMITED,
53 | int $maxDepth = self::UNLIMITED)
54 | {
55 | $this->maxEntryCount = ($maxEntryCount !== self::UNLIMITED)
56 | ? $maxEntryCount : PHP_INT_MAX;
57 |
58 | $this->maxDepth = ($maxDepth !== self::UNLIMITED)
59 | ? $maxDepth : PHP_INT_MAX;
60 | }
61 |
62 | /**
63 | * Iterates through the object recursively and checks the type of each entry
64 | * until the the complexity has been determined or the previously specified
65 | * limits have been reached.
66 | *
67 | * @param mixed $object Object to analyze.
68 | *
69 | * @return int Either SIMPLE or COMPLEX.
70 | */
71 | private function analyzeRecursive(&$object) : int
72 | {
73 | if ($object === null
74 | || is_scalar($object)
75 | ) {
76 | return ObjectComplexityAnalyzer::SIMPLE;
77 | }
78 |
79 | if (is_object($object)
80 | || !is_array($object)
81 | ) {
82 | return ObjectComplexityAnalyzer::COMPLEX;
83 | }
84 |
85 | $this->currentDepth++;
86 | if ($this->currentDepth > $this->maxDepth) {
87 | return ObjectComplexityAnalyzer::COMPLEX;
88 | }
89 |
90 | foreach ($object as $entry) {
91 | $this->currentEntryCount++;
92 | if ($this->currentEntryCount > $this->maxEntryCount) {
93 | return ObjectComplexityAnalyzer::COMPLEX;
94 | }
95 |
96 | if ($this->analyzeRecursive($entry) !== ObjectComplexityAnalyzer::SIMPLE) {
97 | return ObjectComplexityAnalyzer::COMPLEX;
98 | }
99 | }
100 | $this->currentDepth--;
101 |
102 | return ObjectComplexityAnalyzer::SIMPLE;
103 | }
104 |
105 | /**
106 | * Analyzes the specified object and determines its complexity.
107 | *
108 | * @param mixed $object Object to analyze.
109 | *
110 | * @return int Either SIMPLE or COMPLEX.
111 | */
112 | public function analyze(&$object) : int
113 | {
114 | $this->currentEntryCount = 0;
115 | $this->currentDepth = 0;
116 |
117 | return $this->analyzeRecursive($object);
118 | }
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/ObjectComplexityAnalyzerTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use PHPUnit\Framework\TestCase;
14 | use SilentByte\LiteCache\ObjectComplexityAnalyzer as OCA;
15 | use stdClass;
16 |
17 | class ObjectComplexityAnalyzerTest extends TestCase
18 | {
19 | use VirtualFileSystemTrait;
20 |
21 | private $resource;
22 |
23 | public function setUp()
24 | {
25 | $this->vfs();
26 | $this->resource = fopen($this->url('root/test.txt'), 'w');
27 | }
28 |
29 | public function tearDown()
30 | {
31 | fclose($this->resource);
32 | }
33 |
34 | public function objectProvider()
35 | {
36 | $object = new stdClass();
37 | $object->foo = 'bar';
38 | $object->xyz = 1234;
39 | $object->array = [10, 20, 30, 40, 50];
40 |
41 | return [
42 | 'null' => [OCA::SIMPLE, null],
43 | 'int' => [OCA::SIMPLE, 1234],
44 | 'string' => [OCA::SIMPLE, 'test'],
45 | 'float' => [OCA::SIMPLE, 3.1415],
46 | 'true' => [OCA::SIMPLE, true],
47 | 'false' => [OCA::SIMPLE, false],
48 | 'resource' => [OCA::SIMPLE, $this->resource],
49 | 'empty-array' => [OCA::SIMPLE, []],
50 | 'mixed-array' => [OCA::SIMPLE, [1234, 'test', true, 3.1515, false, null]],
51 | 'assoc-array' => [OCA::SIMPLE, [
52 | 'aaa' => 1234,
53 | 'bbb' => 3.1415,
54 | 'ccc' => 'test'
55 | ]],
56 | 'nested-array' => [OCA::SIMPLE, [
57 | 'aaa' => 1234,
58 | 'bbb' => [
59 | 'xxx' => 1234,
60 | 'yyy' => 3.1415,
61 | 'zzz' => 'test',
62 | 'nested' => [
63 | 'nested-1' => 'first',
64 | 'nested-2' => 'second'
65 | ]
66 | ]
67 | ]],
68 | 'object' => [OCA::COMPLEX, $object],
69 | 'nested-array-with-object' => [OCA::COMPLEX, [
70 | 'aaa' => 1234,
71 | 'bbb' => [
72 | 'xxx' => 1234,
73 | 'yyy' => 3.1415,
74 | 'zzz' => 'test',
75 | 'nested' => [
76 | 'nested-1' => 'first',
77 | 'object' => $object,
78 | 'nested-2' => 'second'
79 | ]
80 | ]
81 | ]]
82 | ];
83 | }
84 |
85 | /**
86 | * @dataProvider objectProvider
87 | */
88 | public function testAnalyze($complexity, $object)
89 | {
90 | $oca = new ObjectComplexityAnalyzer(PHP_INT_MAX, PHP_INT_MAX);
91 | $this->assertEquals($complexity, $oca->analyze($object));
92 | }
93 |
94 | public function testAnalyzeReportsComplexOnHighEntryCount()
95 | {
96 | $object = [
97 | 'aaa' => 1234,
98 | 'bbb' => [
99 | 'xxx' => 1234,
100 | 'yyy' => 3.1415,
101 | 'zzz' => 'test',
102 | 'nested' => [
103 | 'nested-1' => 'first',
104 | 'nested-2' => 'second'
105 | ]
106 | ]
107 | ];
108 |
109 | $oca = new ObjectComplexityAnalyzer(4, PHP_INT_MAX);
110 | $this->assertEquals(OCA::COMPLEX, $oca->analyze($object));
111 | }
112 |
113 | public function testAnalyzeReportsComplexOnDeepHierarchy()
114 | {
115 | $object = [
116 | 'aaa' => 1234,
117 | 'bbb' => [
118 | 'xxx' => 1234,
119 | 'yyy' => 3.1415,
120 | 'zzz' => 'test',
121 | 'nested' => [
122 | 'nested-1' => 'first',
123 | 'nested-2' => 'second'
124 | ]
125 | ]
126 | ];
127 |
128 | $oca = new ObjectComplexityAnalyzer(PHP_INT_MAX, 2);
129 | $this->assertEquals(OCA::COMPLEX, $oca->analyze($object));
130 | }
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | LiteCache 2.1
3 | =============
4 | [](https://travis-ci.org/SilentByte/litecache)
5 | [](https://packagist.org/packages/silentbyte/litecache)
6 | [](https://opensource.org/licenses/MIT)
7 |
8 | This is the official repository of the SilentByte LiteCache Library.
9 |
10 | LiteCache is a lightweight, easy-to-use, and [PSR-16](http://www.php-fig.org/psr/psr-16/) compliant caching library for PHP 7.0+ that tries to utilize PHP's built in caching mechanisms. Advanced caching systems such as [Memcached](https://memcached.org/) are often not available on low-cost hosting providers. However, code/opcode caching is normally enabled to speed up execution. LiteCache leverages this functionality by generating `*.php` files for cached objects which are then optimized and cached by the execution environment.
11 |
12 |
13 | ## Installation
14 | The easiest way to install the latest version of LiteCache is using [Composer](https://getcomposer.org/):
15 |
16 | ```bash
17 | $ composer require silentbyte/litecache
18 | ```
19 |
20 | More information can be found on [Packagist](https://packagist.org/packages/silentbyte/litecache).
21 |
22 | If you would like to check out and include the source directly without using Composer, simply clone this repository:
23 | ```bash
24 | $ git clone https://github.com/SilentByte/litecache.git
25 | ```
26 |
27 |
28 | ## General Usage
29 | LiteCache implements [PSR-16](http://www.php-fig.org/psr/psr-16/) and thus provides a standardized API for storing and retrieving data. The full API documentation is available here: [LiteCache 2.0 API Documentation](https://docs.silentbyte.com/litecache/).
30 |
31 |
32 | ### Caching 101
33 | Let's get started with the following basic example that demonstrates how to load and cache an application's configuration from a JSON file.
34 |
35 | ```php
36 | $cache = new \SilentByte\LiteCache\LiteCache();
37 |
38 | $config = $cache->get('config');
39 | if ($config === null) {
40 | $config = json_decode(file_get_contents('config.json'), true);
41 | $cache->set('config', $config);
42 | }
43 |
44 | var_dump($config);
45 | ```
46 |
47 | The methods `$cache->get($key, $default = null)` and `$cache->set($key, $value, $ttl = null)` are used to retrieve and save the configuration from and to the cache under the unique name `config`, respecting the defined TTL. In case of a _cache miss_, the data will be loaded from the actual JSON file and is then immediately cached.
48 |
49 | Without the cache as an intermediary layer, the JSON file would have to be loaded and parsed upon every request. LiteCache avoids this issue by utilizing PHP's code caching mechanisms.
50 |
51 | The library is designed to cache data of any kind, including integers, floats, strings, booleans, arrays, and objects. In addition, LiteCache provides the ability to cache files and content from the output buffer to provide faster access.
52 |
53 |
54 | ### Advanced Caching
55 | The main function for storing and retrieving objects to and from the cache is the method `$cache->cache($name, $producer, $ttl)`. The first parameter `$name` is the unique name of the object to be stored. `$producer` is a generator function that will be called if the object has expired or not yet been cached. The return value of this `callable` will be stored in the cache. `$ttl`, or _time-to-live_, defines the number of seconds before the objects expires. If `$ttl` is not specified, the cache's default time-to-live will be used (in the listed code below, that is 10 minutes).
56 |
57 | The following example issues a Github API request using cURL and caches the result for 10 minutes. When the code is run for the first time, it will fetch the data from the Github server. Subsequent calls to the script will access the cached value without issuing a time-expensive request.
58 |
59 | ```php
60 | // Create the cache object with a customized configuration.
61 | $cache = new \SilentByte\LiteCache\LiteCache([
62 | // Specify the caching directory.
63 | 'directory' => '.litecache',
64 |
65 | // Make cached objects expire after 10 minutes.
66 | 'ttl' => '10 minutes'
67 | ]);
68 |
69 | // Issue a Github API request and cache it under the specified name ('git-request').
70 | // Subsequent calls to $cache->cache() will be fetched from cache;
71 | // after expiration, a new request will be issued.
72 | $response = $cache->cache('git-request', function () {
73 | $ch = curl_init('https://api.github.com/users/SilentByte');
74 | curl_setopt($ch, CURLOPT_USERAGENT, 'SilentByte/litecache');
75 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
76 | return json_decode(curl_exec($ch));
77 | });
78 |
79 | echo "Name: ", $response->login, "\n",
80 | "Website: ", $response->blog, "\n",
81 | "Update: ", $response->updated_at, "\n";
82 | ```
83 |
84 | Further examples can be found in the `./examples/` directory within this repository.
85 |
86 |
87 | ### Using Producers
88 | LiteCache's `cache($key, $producer, $ttl)` method uses producers (implemented as function objects) that yield the data for storage in the cache. A couple of useful producers are already shipped with LiteCache, namely: `FileProducer`, `IniProducer`, `JsonProducer`, and `OutputProducer`.
89 |
90 | Using the `IniProducer` with the following `*.ini` file...
91 |
92 | ```ini
93 | [server]
94 | host = myhost.test.com
95 | user = root
96 | password = root
97 | ```
98 |
99 | ...and this code...
100 |
101 | ```php
102 | use SilentByte\LiteCache\IniProducer;
103 | use SilentByte\LiteCache\LiteCache;
104 |
105 | // Create the cache object with a customized configuration.
106 | $cache = new LiteCache([
107 | // Cache objects permanently.
108 | 'ttl' => LiteCache::EXPIRE_NEVER
109 | ]);
110 |
111 | // Load the specified INI configuration file and cache it.
112 | $config = $cache->cache('ini-cache', new IniProducer('./sample_data/test_ini.ini'));
113 |
114 | echo "Host: ", $config['server']['host'], "\n",
115 | "User: ", $config['server']['user'], "\n",
116 | "Password: ", $config['server']['password'], "\n";
117 | ```
118 |
119 | ...will result in the configuration file being cached for all further calls of the script, thus avoiding unnecessary parsing on every request. The same concept applies to the other types or producers.
120 |
121 | The same concept can be applied to cache PHP's output, e.g. caching a web page in order to avoid having to re-render it upon every request. The easiest way to achieve this is by using the integrated `OutputProducer`:
122 |
123 | ```php
124 | use SilentByte\LiteCache\LiteCache;
125 | use SilentByte\LiteCache\OutputProducer;
126 |
127 | // Create the cache object with a customized configuration.
128 | $cache = new LiteCache([
129 | // Specify the caching directory.
130 | 'directory' => '.litecache',
131 |
132 | // Cache objects for 30 seconds.
133 | 'ttl' => '30 seconds'
134 | ]);
135 |
136 | // Load the specified file and cache it.
137 | $output = $cache->cache('script-cache', new OutputProducer(function () {
138 | include('./sample_data/slow_script.php');
139 | }));
140 |
141 | echo "---- (Script Output) -------------------\n";
142 | echo $output;
143 | echo "---------------------------------------\n";
144 | ```
145 |
146 | All output from the included PHP script (e.g. generated via `echo`) will be cached for 30 seconds. If you are using a templating engine such as [Twig](http://twig.sensiolabs.org/), `OutputProducer` can be used to cache the rendered page. In case the data is directly available as a string, a simple call to `$cache->set($key, $value)` will suffice.
147 |
148 | See the `./examples/` folder for more details.
149 |
150 |
151 | ### Options
152 | LiteCache's constructor accepts an array that specifies user-defined options.
153 |
154 | ```php
155 | // LiteCache 2.1 Default Options.
156 | $options = [
157 | 'directory' => '.litecache',
158 | 'subdivision' => false,
159 | 'pool' => 'default',
160 | 'ttl' => LiteCache::EXPIRE_NEVER,
161 | 'logger' => null
162 | ];
163 |
164 | $cache = new LiteCache($options);
165 | ```
166 |
167 | Option | Type | Description
168 | ---------------|---------------------------------------|------------
169 | directory | string | Location (path) indicating where the cache files are to be stored.
170 | subdivision | bool | Places cache files into different sub-directories to avoid having many files in the same directory.
171 | pool | string | Defines the name of the cache pool. A pool is a logical separation of cache objects. Cache objects in different pools are independent of each other and may thus share the same unique name. See [PSR-6 #pool](http://www.php-fig.org/psr/psr-6/#pool).
172 | ttl | null
int
string
DateInterval | Time-To-Live. Defines a time interval that signaling when cache objects expire by default. This value may be specified as an integer indicating seconds (e.g. 10), a time interval string (e.g '10 seconds'), an instance of DateInterval, or `LiteCache::EXPIRE_NEVER` / `LiteCache::EXPIRE_IMMEDIATELY`.
173 | logger | LoggerInterface
null | An instance of a [PSR-3](http://www.php-fig.org/psr/psr-3/) compliant logger class (implementing `\Psr\Log\LoggerInterface`) that is used to receive logging information. May be `null` if not required.
174 |
175 |
176 | ## Contributing
177 | See [CONTRIBUTING.md](CONTRIBUTING.md).
178 |
179 |
180 | ## Change Log
181 | See [CHANGELOG.md](CHANGELOG.md).
182 |
183 |
184 | ## FAQ
185 |
186 | ### Under what license is LiteCache released?
187 | MIT license. Check out [LICENSE.txt](LICENSE.txt) for details. More information regarding the MIT license can be found here:
188 |
189 | ### How do I permanently cache static files, i.e. configuration files?
190 | Setting the `$ttl` value to `LiteCache::EXPIRE_NEVER` will cause objects to remain in the cache until the cache file is deleted manually, either by physically deleting the file or by calling `$cache->delete($key)` or `$cache->clean()`.
191 |
192 |
--------------------------------------------------------------------------------
/tests/SilentByte/LiteCache/LiteCacheTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use DateInterval;
14 | use PHPUnit\Framework\TestCase;
15 | use stdClass;
16 |
17 | class LiteCacheTest extends TestCase
18 | {
19 | use VirtualFileSystemTrait;
20 |
21 | protected function setUp()
22 | {
23 | $this->vfs();
24 | }
25 |
26 | public function configProvider()
27 | {
28 | return [
29 | [['subdivision' => false]],
30 | [['subdivision' => true]],
31 | [[
32 | 'subdivision' => false,
33 | 'pool' => 'test-pool'
34 | ]],
35 | [[
36 | 'subdivision' => true,
37 | 'pool' => 'test-pool'
38 | ]]
39 | ];
40 | }
41 |
42 | public function invalidKeyProvider()
43 | {
44 | // Integers are considered valid keys due to PHP's quirk where integral string
45 | // hash map keys are coerced to integers (see http://php.net/manual/en/language.types.array.php).
46 | $invalidKeys = [
47 | null,
48 | '',
49 | 3.14,
50 | 'foo{bar',
51 | 'foo}bar',
52 | 'foo(bar',
53 | 'foo)bar',
54 | 'foo/bar',
55 | 'foo\\bar',
56 | 'foo@bar',
57 | 'foo:bar',
58 | '{}()/\@:'
59 | ];
60 |
61 | $nested = [];
62 | foreach ($invalidKeys as $key) {
63 | $nested[] = [$key];
64 | }
65 |
66 | return $nested;
67 | }
68 |
69 | public function keyObjectProvider()
70 | {
71 | $object = new stdClass();
72 | $object->foo = 'bar';
73 | $object->xyz = 1234;
74 | $object->array = [10, 20, 30, 40, 50];
75 |
76 | $data = [
77 | ['ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.', 'psr16-minimum-requirement'],
78 | ['key-string', 'test'],
79 | ['key-integer', 1234],
80 | ['key-float', 3.1415],
81 | ['key-boolean-true', true],
82 | ['key-boolean-false', false],
83 | ['key-array', [
84 | 'foo' => 'bar',
85 | 'xyz' => 1234,
86 | 'array' => [10, 20, 30, 40, 50]
87 | ]],
88 | ['key-object', $object],
89 | ['key-array-object', [$object, $object, $object]],
90 | ['0', 0]
91 | ];
92 |
93 | $permutations = [];
94 | foreach ($this->configProvider()[0] as $config) {
95 | foreach ($data as $entry) {
96 | $permutations[] = array_merge([$config], $entry);
97 | }
98 | }
99 |
100 | return $permutations;
101 | }
102 |
103 | public function multipleKeyObjectProvider()
104 | {
105 | $object = new stdClass();
106 | $object->foo = 'bar';
107 | $object->xyz = 1234;
108 | $object->array = [10, 20, 30, 40, 50];
109 |
110 | $data = [
111 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.'
112 | => 'psr16-minimum-requirement',
113 | 'key-string' => 'test',
114 | 'key-integer' => 1234,
115 | 'key-float' => 3.1415,
116 | 'key-boolean-true' => true,
117 | 'key-boolean-false' => false,
118 | 'key-array' => [
119 | 'foo' => 'bar',
120 | 'xyz' => 1234,
121 | 'array' => [10, 20, 30, 40, 50]
122 | ],
123 | 'key-object' => $object,
124 | 'key-array-object' => [$object, $object, $object],
125 | '0' => 0
126 | ];
127 |
128 | $permutations = [];
129 | foreach ($this->configProvider()[0] as $config) {
130 | $permutations[] = [$config, $data];
131 | }
132 |
133 | return $permutations;
134 | }
135 |
136 | public function create(array $config = null)
137 | {
138 | $defaultConfig = [
139 | 'directory' => $this->url('root/.litecache'),
140 | 'pool' => 'test-pool',
141 | 'ttl' => LiteCache::EXPIRE_NEVER
142 | ];
143 |
144 | $config = array_replace_recursive($defaultConfig,
145 | $config !== null ? $config : []);
146 |
147 | return new LiteCache($config);
148 | }
149 |
150 | public function testConstructorCreatesCacheDirectoryWithPermissions()
151 | {
152 | $cache = $this->create();
153 |
154 | $this->assertTrue(file_exists($cache->getCacheDirectory()));
155 | $this->assertEquals(0766, fileperms($cache->getCacheDirectory()) & 0777);
156 | }
157 |
158 | public function testConstructorCreatesCachePoolDirectoryWithPermissions()
159 | {
160 | $cache = $this->create();
161 |
162 | $poolDirectory = $cache->getCacheDirectory()
163 | . DIRECTORY_SEPARATOR
164 | . 'af2bc12271d88af325dd44d9029b197d';
165 |
166 | $this->assertTrue(file_exists($poolDirectory));
167 | $this->assertEquals(0766, fileperms($poolDirectory) & 0777);
168 | }
169 |
170 | public function testConstructorAcceptsDateInterval()
171 | {
172 | $interval = DateInterval::createFromDateString('10 seconds');
173 |
174 | $cache = $this->create(['ttl' => $interval]);
175 | $this->assertEquals(10, $cache->getDefaultTimeToLive());
176 | }
177 |
178 | public function testConstructorAcceptsDateIntervalString()
179 | {
180 | $cache = $this->create(['ttl' => '10 seconds']);
181 | $this->assertEquals(10, $cache->getDefaultTimeToLive());
182 | }
183 |
184 | public function testConstructorAcceptsPoolName0()
185 | {
186 | $this->create(['pool' => '0']);
187 | }
188 |
189 | public function testConstructorAcceptsCacheDirectory0()
190 | {
191 | $cache = $this->create(['directory' => '0']);
192 | $this->assertEquals('0', $cache->getCacheDirectory());
193 | }
194 |
195 | /**
196 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
197 | */
198 | public function testConstructorThrowsOnInvalidLogger()
199 | {
200 | $this->create([
201 | 'logger' => 1234
202 | ]);
203 | }
204 |
205 | /**
206 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
207 | */
208 | public function testConstructorThrowsOnInvalidTimeToLive()
209 | {
210 | $this->create([
211 | 'ttl' => 'invalid'
212 | ]);
213 | }
214 |
215 | /**
216 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
217 | */
218 | public function testConstructorThrowsOnInvalidCacheDirectory()
219 | {
220 | $this->create([
221 | 'directory' => null
222 | ]);
223 | }
224 |
225 | /**
226 | * @dataProvider configProvider
227 | */
228 | public function testGetReturnsNullForUncachedObjects(array $config)
229 | {
230 | $cache = $this->create($config);
231 | $this->assertNull($cache->get('uncached-object'));
232 | }
233 |
234 | /**
235 | * @dataProvider configProvider
236 | */
237 | public function testGetReturnsDefaultForUncachedObjects(array $config)
238 | {
239 | $cache = $this->create($config);
240 |
241 | $this->assertEquals(10, $cache->get('uncached-object', 10));
242 | $this->assertEquals('string', $cache->get('uncached-object', 'string'));
243 | $this->assertEquals([
244 | 'first' => 'one',
245 | 'second' => 2
246 | ],
247 | $cache->get('uncached-object', [
248 | 'first' => 'one',
249 | 'second' => 2
250 | ]));
251 | }
252 |
253 | /**
254 | * @dataProvider keyObjectProvider
255 | */
256 | public function testGetReturnsCachedObject(array $config, $key, $object)
257 | {
258 | $cache = $this->create($config);
259 | $cache->set($key, $object);
260 |
261 | $this->assertEquals($object, $cache->get($key));
262 | }
263 |
264 | /**
265 | * @dataProvider invalidKeyProvider
266 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
267 | */
268 | public function testGetThrowsOnInvalidKey($key)
269 | {
270 | $cache = $this->create();
271 | $cache->get($key);
272 | }
273 |
274 | /**
275 | * @dataProvider invalidKeyProvider
276 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
277 | */
278 | public function testSetThrowsOnInvalidKey($key)
279 | {
280 | $cache = $this->create();
281 | $cache->set($key, 'test');
282 | }
283 |
284 | public function testSetCreatesCacheFile()
285 | {
286 | $cache = $this->create();
287 | $cache->set('test', 1234);
288 |
289 | $this->assertFileExists($cache->getCacheDirectory()
290 | . DIRECTORY_SEPARATOR
291 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
292 | . DIRECTORY_SEPARATOR
293 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
294 | }
295 |
296 | public function testSetCreatesSubdivisionCacheFile()
297 | {
298 | $cache = $this->create(['subdivision' => true]);
299 | $cache->set('test', 1234);
300 |
301 | $this->assertFileExists($cache->getCacheDirectory()
302 | . DIRECTORY_SEPARATOR
303 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
304 | . DIRECTORY_SEPARATOR
305 | . '09'
306 | . DIRECTORY_SEPARATOR
307 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
308 | }
309 |
310 | /**
311 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
312 | */
313 | public function testCacheThrowsOnEmptyKey()
314 | {
315 | $cache = $this->create();
316 | $cache->cache('', function () {
317 | return 1234;
318 | });
319 | }
320 |
321 | /**
322 | * @dataProvider configProvider
323 | */
324 | public function testCacheExecutesProducerOnUncachedObject(array $config)
325 | {
326 | $cache = $this->create($config);
327 |
328 | $executed = false;
329 | $cache->cache('test', function () use (&$executed) {
330 | $executed = true;
331 | return 1234;
332 | });
333 |
334 | $this->assertTrue($executed);
335 | }
336 |
337 | /**
338 | * @dataProvider configProvider
339 | */
340 | public function testCacheIgnoresProducerOnUncachedObject(array $config)
341 | {
342 | $cache = $this->create($config);
343 | $executed = false;
344 |
345 | $cache->cache('test', function () {
346 | return 1234;
347 | }, LiteCache::EXPIRE_NEVER);
348 |
349 | $cache->cache('test', function () use (&$executed) {
350 | $executed = true;
351 | return 5678;
352 | }, LiteCache::EXPIRE_NEVER);
353 |
354 | $this->assertFalse($executed);
355 | }
356 |
357 | /**
358 | * @dataProvider keyObjectProvider
359 | */
360 | public function testCacheReturnsCachedObject(array $config, $key, $object)
361 | {
362 | $cache = $this->create($config);
363 | $cached = $cache->cache($key, function () use ($object) {
364 | return $object;
365 | });
366 |
367 | $this->assertEquals($object, $cached);
368 | }
369 |
370 | public function testCacheCreatesCacheFile()
371 | {
372 | $cache = $this->create();
373 | $cache->cache('test', function () {
374 | return 1234;
375 | });
376 |
377 | $this->assertFileExists($cache->getCacheDirectory()
378 | . DIRECTORY_SEPARATOR
379 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
380 | . DIRECTORY_SEPARATOR
381 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
382 | }
383 |
384 | public function testCacheCreatesSubdivisionCacheFile()
385 | {
386 | $cache = $this->create(['subdivision' => true]);
387 | $cache->cache('test', function () {
388 | return 1234;
389 | });
390 |
391 | $this->assertFileExists($cache->getCacheDirectory()
392 | . DIRECTORY_SEPARATOR
393 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
394 | . DIRECTORY_SEPARATOR
395 | . '09'
396 | . DIRECTORY_SEPARATOR
397 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
398 | }
399 |
400 | /**
401 | * @dataProvider configProvider
402 | */
403 | public function testDeleteReturnsFalseOnUncachedObject(array $config)
404 | {
405 | $cache = $this->create($config);
406 | $this->assertFalse($cache->delete('uncached'));
407 | }
408 |
409 | public function testDeleteActuallyDeletesCacheFile()
410 | {
411 | $cache = $this->create();
412 |
413 | $cache->set('test', 1234);
414 | $this->assertFileExists($cache->getCacheDirectory()
415 | . DIRECTORY_SEPARATOR
416 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
417 | . DIRECTORY_SEPARATOR
418 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
419 |
420 | $cache->delete('test');
421 |
422 | $this->assertFileNotExists($cache->getCacheDirectory()
423 | . DIRECTORY_SEPARATOR
424 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
425 | . DIRECTORY_SEPARATOR
426 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
427 | }
428 |
429 | public function testDeleteActuallyDeletesSubdivisionCacheFile()
430 | {
431 | $cache = $this->create(['subdivision' => true]);
432 |
433 | $cache->set('test', 1234);
434 | $this->assertFileExists($cache->getCacheDirectory()
435 | . DIRECTORY_SEPARATOR
436 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
437 | . DIRECTORY_SEPARATOR
438 | . '09'
439 | . DIRECTORY_SEPARATOR
440 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
441 |
442 | $cache->delete('test');
443 |
444 | $this->assertFileNotExists($cache->getCacheDirectory()
445 | . DIRECTORY_SEPARATOR
446 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
447 | . DIRECTORY_SEPARATOR
448 | . '09'
449 | . DIRECTORY_SEPARATOR
450 | . '098f6bcd4621d373cade4e832627b4f6.litecache.php'); // "test".
451 | }
452 |
453 | /**
454 | * @dataProvider configProvider
455 | */
456 | public function testDeleteReturnsTrueOnSuccess(array $config)
457 | {
458 | $cache = $this->create($config);
459 |
460 | $cache->set('test', 1234);
461 | $this->assertTrue($cache->delete('test'));
462 | }
463 |
464 | /**
465 | * @dataProvider invalidKeyProvider
466 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
467 | */
468 | public function testDeleteThrowsThrowsOnInvalidKey($key)
469 | {
470 | $cache = $this->create();
471 | $cache->delete($key);
472 | }
473 |
474 | /**
475 | * @dataProvider configProvider
476 | */
477 | public function testClearReturnsTrueOnEmptyCache(array $config)
478 | {
479 | $cache = $this->create($config);
480 | $this->assertTrue($cache->clear());
481 | }
482 |
483 | public function testClearClearsAllCacheFiles()
484 | {
485 | $cache = $this->create();
486 |
487 | $cache->set('aaa', 1234);
488 | $cache->set('bbb', 'test');
489 | $cache->set('ccc', 3.1415);
490 |
491 | $this->assertNotEmpty($this->tree()
492 | ['root']['.litecache']['af2bc12271d88af325dd44d9029b197d']);
493 |
494 | $this->assertTrue($cache->clear());
495 |
496 | $this->assertEmpty($this->tree()
497 | ['root']['.litecache']['af2bc12271d88af325dd44d9029b197d']);
498 | }
499 |
500 | public function testClearClearsAllSubdivisionCacheFiles()
501 | {
502 | $cache = $this->create(['subdivision' => true]);
503 |
504 | $cache->set('aaa', 1234);
505 | $cache->set('bbb', 'test');
506 | $cache->set('ccc', 3.1415);
507 |
508 | $this->assertNotEmpty($this->tree()
509 | ['root']['.litecache']['af2bc12271d88af325dd44d9029b197d']);
510 |
511 | $this->assertTrue($cache->clear());
512 |
513 | $this->assertEmpty($this->tree()
514 | ['root']['.litecache']['af2bc12271d88af325dd44d9029b197d']);
515 | }
516 |
517 | /**
518 | * @dataProvider configProvider
519 | */
520 | public function testGetMultipleReturnsNullForUncachedObjects(array $config)
521 | {
522 | $cache = $this->create($config);
523 | $objects = $cache->getMultiple([
524 | 'uncached-object-1',
525 | 'uncached-object-2',
526 | 'uncached-object-3',
527 | 'uncached-object-4'
528 | ]);
529 |
530 | $this->assertEquals([
531 | 'uncached-object-1' => null,
532 | 'uncached-object-2' => null,
533 | 'uncached-object-3' => null,
534 | 'uncached-object-4' => null
535 | ],
536 | $objects);
537 | }
538 |
539 | /**
540 | * @dataProvider configProvider
541 | */
542 | public function testGetMultipleReturnsDefaultForUncachedObjects(array $config)
543 | {
544 | $cache = $this->create($config);
545 | $objects = $cache->getMultiple([
546 | 'uncached-object-1',
547 | 'uncached-object-2',
548 | 'uncached-object-3',
549 | 'uncached-object-4'
550 | ], 1234);
551 |
552 | $this->assertEquals([
553 | 'uncached-object-1' => 1234,
554 | 'uncached-object-2' => 1234,
555 | 'uncached-object-3' => 1234,
556 | 'uncached-object-4' => 1234
557 | ],
558 | $objects);
559 | }
560 |
561 | /**
562 | * @dataProvider multipleKeyObjectProvider
563 | */
564 | public function testGetMultipleReturnsCachedObjects(array $config, array $data)
565 | {
566 | $cache = $this->create($config);
567 | $cache->setMultiple($data);
568 |
569 | $objects = $cache->getMultiple(array_keys($data));
570 | $this->assertEquals($data, $objects);
571 | }
572 |
573 | /**
574 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
575 | */
576 | public function testGetMultipleThrowsOnNoArrayNoTraversable()
577 | {
578 | $cache = $this->create();
579 | $cache->getMultiple(1234);
580 | }
581 |
582 | /**
583 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
584 | */
585 | public function testGetMultipleThrowsOnNull()
586 | {
587 | $cache = $this->create();
588 | $cache->getMultiple(null);
589 | }
590 |
591 | /**
592 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
593 | */
594 | public function testGetMultipleThrowsOnInvalidKey()
595 | {
596 | $cache = $this->create();
597 |
598 | $invalidKeys = [];
599 | foreach ($this->invalidKeyProvider() as $entry) {
600 | $invalidKeys[] = $entry[0];
601 | }
602 |
603 | $cache->getMultiple($invalidKeys);
604 | }
605 |
606 | /**
607 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
608 | */
609 | public function testSetMultipleThrowsOnNoArrayNoTraversable()
610 | {
611 | $cache = $this->create();
612 | $cache->setMultiple(1234);
613 | }
614 |
615 | /**
616 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
617 | */
618 | public function testSetMultipleThrowsOnNull()
619 | {
620 | $cache = $this->create();
621 | $cache->setMultiple(null);
622 | }
623 |
624 | /**
625 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
626 | */
627 | public function testSetMultipleThrowsOnInvalidKey()
628 | {
629 | $cache = $this->create();
630 |
631 | $invalidKeys = [];
632 | foreach ($this->invalidKeyProvider() as $entry) {
633 | $invalidKeys[$entry[0]] = 'test';
634 | }
635 |
636 | $cache->setMultiple($invalidKeys);
637 | }
638 |
639 | public function testSetMultipleCreatesCacheFiles()
640 | {
641 | $cache = $this->create();
642 |
643 | $cache->setMultiple([
644 | 'test-1' => 1234,
645 | 'test-2' => 'test',
646 | 'test-3' => [10, 20, 30, 40, 50]
647 | ]);
648 |
649 | $this->assertFileExists($cache->getCacheDirectory()
650 | . DIRECTORY_SEPARATOR
651 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
652 | . DIRECTORY_SEPARATOR
653 | . '70a37754eb5a2e7db8cd887aaf11cda7.litecache.php'); // "test-1".
654 |
655 | $this->assertFileExists($cache->getCacheDirectory()
656 | . DIRECTORY_SEPARATOR
657 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
658 | . DIRECTORY_SEPARATOR
659 | . '282ff2cb3d9dadeb831bb3ba0128f2f4.litecache.php'); // "test-2".
660 |
661 | $this->assertFileExists($cache->getCacheDirectory()
662 | . DIRECTORY_SEPARATOR
663 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
664 | . DIRECTORY_SEPARATOR
665 | . '2b61ddda48445374b35a927b6ae2cd6d.litecache.php'); // "test-3".
666 | }
667 |
668 | public function testSetMultipleCreatesSubdivisionCacheFiles()
669 | {
670 | $cache = $this->create(['subdivision' => true]);
671 |
672 | $cache->setMultiple([
673 | 'test-1' => 1234,
674 | 'test-2' => 'test',
675 | 'test-3' => [10, 20, 30, 40, 50]
676 | ]);
677 |
678 | $this->assertFileExists($cache->getCacheDirectory()
679 | . DIRECTORY_SEPARATOR
680 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
681 | . DIRECTORY_SEPARATOR
682 | . '70'
683 | . DIRECTORY_SEPARATOR
684 | . '70a37754eb5a2e7db8cd887aaf11cda7.litecache.php'); // "test-1".
685 |
686 | $this->assertFileExists($cache->getCacheDirectory()
687 | . DIRECTORY_SEPARATOR
688 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
689 | . DIRECTORY_SEPARATOR
690 | . '28'
691 | . DIRECTORY_SEPARATOR
692 | . '282ff2cb3d9dadeb831bb3ba0128f2f4.litecache.php'); // "test-2".
693 |
694 | $this->assertFileExists($cache->getCacheDirectory()
695 | . DIRECTORY_SEPARATOR
696 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
697 | . DIRECTORY_SEPARATOR
698 | . '2b'
699 | . DIRECTORY_SEPARATOR
700 | . '2b61ddda48445374b35a927b6ae2cd6d.litecache.php'); // "test-3".
701 | }
702 |
703 | /**
704 | * @dataProvider configProvider
705 | */
706 | public function testDeleteMultipleReturnsFalseOnUncachedObject(array $config)
707 | {
708 | $cache = $this->create($config);
709 |
710 | $cache->set('cached-1', 1234);
711 | $this->assertFalse($cache->deleteMultiple(
712 | [
713 | 'cached-1',
714 | 'uncached-1',
715 | 'uncached-2',
716 | 'uncached-3',
717 | ]));
718 | }
719 |
720 | public function testDeleteMultipleActuallyDeletesCacheFiles()
721 | {
722 | $cache = $this->create();
723 |
724 | $objects = [
725 | 'test-1' => 1234,
726 | 'test-2' => 'test',
727 | ];
728 |
729 | $cache->setMultiple($objects);
730 |
731 | $this->assertFileExists($cache->getCacheDirectory()
732 | . DIRECTORY_SEPARATOR
733 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
734 | . DIRECTORY_SEPARATOR
735 | . '70a37754eb5a2e7db8cd887aaf11cda7.litecache.php');
736 |
737 | $this->assertFileExists($cache->getCacheDirectory()
738 | . DIRECTORY_SEPARATOR
739 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
740 | . DIRECTORY_SEPARATOR
741 | . '282ff2cb3d9dadeb831bb3ba0128f2f4.litecache.php');
742 |
743 | $this->assertTrue($cache->deleteMultiple(array_keys($objects)));
744 |
745 | $this->assertFileNotExists($cache->getCacheDirectory()
746 | . DIRECTORY_SEPARATOR
747 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
748 | . DIRECTORY_SEPARATOR
749 | . '70a37754eb5a2e7db8cd887aaf11cda7.litecache.php');
750 |
751 | $this->assertFileNotExists($cache->getCacheDirectory()
752 | . DIRECTORY_SEPARATOR
753 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
754 | . DIRECTORY_SEPARATOR
755 | . '282ff2cb3d9dadeb831bb3ba0128f2f4.litecache.php');
756 | }
757 |
758 | public function testDeleteMultipleActuallyDeletesSubdivisionCacheFiles()
759 | {
760 | $cache = $this->create(['subdivision' => true]);
761 |
762 | $objects = [
763 | 'test-1' => 1234,
764 | 'test-2' => 'test',
765 | ];
766 |
767 | $cache->setMultiple($objects);
768 |
769 | $this->assertFileExists($cache->getCacheDirectory()
770 | . DIRECTORY_SEPARATOR
771 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
772 | . DIRECTORY_SEPARATOR
773 | . '70'
774 | . DIRECTORY_SEPARATOR
775 | . '70a37754eb5a2e7db8cd887aaf11cda7.litecache.php');
776 |
777 | $this->assertFileExists($cache->getCacheDirectory()
778 | . DIRECTORY_SEPARATOR
779 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
780 | . DIRECTORY_SEPARATOR
781 | . '28'
782 | . DIRECTORY_SEPARATOR
783 | . '282ff2cb3d9dadeb831bb3ba0128f2f4.litecache.php');
784 |
785 | $this->assertTrue($cache->deleteMultiple(array_keys($objects)));
786 |
787 | $this->assertFileNotExists($cache->getCacheDirectory()
788 | . DIRECTORY_SEPARATOR
789 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
790 | . DIRECTORY_SEPARATOR
791 | . '70'
792 | . DIRECTORY_SEPARATOR
793 | . '70a37754eb5a2e7db8cd887aaf11cda7.litecache.php');
794 |
795 | $this->assertFileNotExists($cache->getCacheDirectory()
796 | . DIRECTORY_SEPARATOR
797 | . 'af2bc12271d88af325dd44d9029b197d' // "test-pool".
798 | . DIRECTORY_SEPARATOR
799 | . '28'
800 | . DIRECTORY_SEPARATOR
801 | . '282ff2cb3d9dadeb831bb3ba0128f2f4.litecache.php');
802 | }
803 |
804 | /**
805 | * @dataProvider configProvider
806 | */
807 | public function testDeleteMultipleReturnsTrueOnSuccess(array $config)
808 | {
809 | $cache = $this->create($config);
810 |
811 | $objects = [
812 | 'test-1' => 1234,
813 | 'test-2' => 'test',
814 | 'test-3' => [10, 20, 30, 40, 50]
815 | ];
816 |
817 | $cache->setMultiple($objects);
818 | $this->assertTrue($cache->deleteMultiple(array_keys($objects)));
819 | }
820 |
821 | /**
822 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
823 | */
824 | public function testDeleteMultipleThrowsThrowsOnInvalidKey()
825 | {
826 | $cache = $this->create();
827 |
828 | $invalidKeys = [];
829 | foreach ($this->invalidKeyProvider() as $entry) {
830 | $invalidKeys[] = $entry[0];
831 | }
832 |
833 | $cache->deleteMultiple($invalidKeys);
834 | }
835 |
836 | /**
837 | * @dataProvider configProvider
838 | */
839 | public function testHasReturnsTrueOnCachedObject(array $config)
840 | {
841 | $cache = $this->create($config);
842 | $cache->set('test', 1234);
843 |
844 | $this->assertTrue($cache->has('test'));
845 | }
846 |
847 | /**
848 | * @dataProvider configProvider
849 | */
850 | public function testHasReturnsFalseOnUncachedObject(array $config)
851 | {
852 | $cache = $this->create($config);
853 | $this->assertFalse($cache->has('uncached'));
854 | }
855 |
856 | /**
857 | * @dataProvider invalidKeyProvider
858 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
859 | */
860 | public function testHasThrowsOnInvalidKey($key)
861 | {
862 | $cache = $this->create();
863 | $cache->has($key);
864 | }
865 | }
866 |
867 |
--------------------------------------------------------------------------------
/src/SilentByte/LiteCache/LiteCache.php:
--------------------------------------------------------------------------------
1 |
6 | * @license https://opensource.org/licenses/MIT MIT
7 | */
8 |
9 | declare(strict_types = 1);
10 |
11 | namespace SilentByte\LiteCache;
12 |
13 | use DateInterval;
14 | use DirectoryIterator;
15 | use Psr\Log\LoggerInterface;
16 | use Psr\Log\NullLogger;
17 | use Psr\SimpleCache\CacheInterface;
18 | use Throwable;
19 | use Traversable;
20 |
21 | /**
22 | * Main class of the LiteCache library that allows the user to cache and load objects
23 | * into and from PHP cache files which can be optimized by the execution environment.
24 | *
25 | * @package SilentByte\LiteCache
26 | */
27 | class LiteCache implements CacheInterface
28 | {
29 | /**
30 | * Indicates that objects cached with this setting will never expire
31 | * and must thus be deleted manually to trigger an update.
32 | */
33 | const EXPIRE_NEVER = -1;
34 |
35 | /**
36 | * Indicates that objects cached with this setting expire immediately.
37 | * This setting can also be used to manually trigger an update.
38 | */
39 | const EXPIRE_IMMEDIATELY = 0;
40 |
41 | /**
42 | * Specifies the default configuration.
43 | */
44 | const DEFAULT_CONFIG = [
45 | 'logger' => null,
46 | 'directory' => '.litecache',
47 | 'subdivision' => false,
48 | 'pool' => 'default',
49 | 'ttl' => LiteCache::EXPIRE_NEVER,
50 | 'strategy' => [
51 | 'code' => [
52 | 'entries' => 1000,
53 | 'depth' => 16
54 | ]
55 | ]
56 | ];
57 |
58 | /**
59 | * User defined PSR-3 compliant logger.
60 | *
61 | * @var LoggerInterface
62 | */
63 | private $logger;
64 |
65 | /**
66 | * User defined path to the cache directory.
67 | *
68 | * @var string
69 | */
70 | private $cacheDirectory;
71 |
72 | /**
73 | * Indicates whether or not cache files should be placed in sub-directories.
74 | *
75 | * @var bool
76 | */
77 | private $subdivision;
78 |
79 | /**
80 | * User defined pool for this instance.
81 | *
82 | * @var string
83 | */
84 | private $pool;
85 |
86 | /**
87 | * Hash of the pool name used for the name of the pool's subdirectory.
88 | *
89 | * @var string
90 | */
91 | private $poolHash;
92 |
93 | /**
94 | * Path to the directory of the user defined pool.
95 | *
96 | * @var string
97 | */
98 | private $poolDirectory;
99 |
100 | /**
101 | * User defined default time to live in seconds.
102 | *
103 | * @var int
104 | */
105 | private $defaultTimeToLive;
106 |
107 | /**
108 | * Used to determine whether an object is 'simple' or 'complex'.
109 | *
110 | * @var ObjectComplexityAnalyzer
111 | */
112 | private $oca;
113 |
114 | /**
115 | * Escapes the given text so that it can be placed inside a PHP multi-line comment.
116 | *
117 | * @param string $text Text to be escaped.
118 | *
119 | * @return string Escaped text.
120 | */
121 | private static function escapeComment(string $text) : string
122 | {
123 | return str_replace(['*/', "\r", "\n"],
124 | ['* /', ' ', ' '],
125 | $text);
126 | }
127 |
128 | /**
129 | * Generates the header comment for cache file, which includes key, timestamp and TTL.
130 | *
131 | * @param string $type Type of the export.
132 | * @param string $key Unique name of the object.
133 | * @param int $ttl The object's time to live.
134 | * @param int $timestamp Indicates at what point in time the object has been stored.
135 | *
136 | * @return string Header comment.
137 | */
138 | private static function generateCacheFileComment(string $type, string $key, int $ttl, int $timestamp) : string
139 | {
140 | $time = date('c', $timestamp);
141 | $duration = sprintf('%02d:%02d:%02d',
142 | floor($ttl / 3600),
143 | ($ttl / 60) % 60,
144 | $ttl % 60);
145 |
146 | return self::escapeComment("{$type} '{$key}' {$time} {$duration}");
147 | }
148 |
149 | /**
150 | * Generates a condition (resulting in a boolean) that indicates whether
151 | * the cache file has expired or not.
152 | *
153 | * @param int $ttl The object's time to live.
154 | * @param int $timestamp Indicates at what point in time the object has been stored.
155 | *
156 | * @return string A string representing a valid PHP boolean condition.
157 | */
158 | private static function generateCacheFileExpirationCondition(int $ttl, int $timestamp) : string
159 | {
160 | if ($ttl === self::EXPIRE_NEVER) {
161 | return 'false';
162 | } else {
163 | $relativeTtl = $timestamp + $ttl;
164 | return "time() > {$relativeTtl}";
165 | }
166 | }
167 |
168 | /**
169 | * Converts the specified DateInterval instance to a value indicating
170 | * the number of seconds within that interval.
171 | *
172 | * @param DateInterval $interval Interval to be converted to seconds.
173 | *
174 | * @return int Number of seconds in the interval.
175 | */
176 | private static function dateIntervalToSeconds(DateInterval $interval) : int
177 | {
178 | return ($interval->s // Seconds.
179 | + ($interval->i * 60) // Minutes.
180 | + ($interval->h * 60 * 60) // Hours.
181 | + ($interval->d * 60 * 60 * 24) // Days.
182 | + ($interval->m * 60 * 60 * 24 * 30) // Months.
183 | + ($interval->y * 60 * 60 * 24 * 365)); // Years.
184 | }
185 |
186 | /**
187 | * Creates the object based on the specified configuration.
188 | *
189 | * @param array $config Passes in the user's cache configuration.
190 | * - directory: Defines the location where cache files are to be stored.
191 | * - ttl: Default time to live for cache files in seconds or a DateInterval object.
192 | */
193 | public function __construct(array $config = null)
194 | {
195 | $config = array_replace_recursive(self::DEFAULT_CONFIG,
196 | $config !== null ? $config : []);
197 |
198 | $this->ensureLoggerValidity($config['logger']);
199 | $this->logger = $config['logger'] ?? new NullLogger();
200 |
201 | $this->ensureCacheDirectoryValidity($config['directory']);
202 | $this->cacheDirectory = PathHelper::directory($config['directory']);
203 | $this->subdivision = (bool)$config['subdivision'];
204 |
205 | $this->ensurePoolNameValidity($config['pool']);
206 | $this->pool = $config['pool'];
207 | $this->poolHash = $this->getHash($this->pool);
208 | $this->poolDirectory = PathHelper::combine($this->cacheDirectory, $this->poolHash);
209 |
210 | $this->ensureTimeToLiveValidity($config['ttl']);
211 | $this->defaultTimeToLive = $this->normalizeTimeToLive($config['ttl']);
212 |
213 | $this->oca = new ObjectComplexityAnalyzer($config['strategy']['code']['entries'],
214 | $config['strategy']['code']['depth']);
215 |
216 | PathHelper::makePath($this->cacheDirectory, 0766);
217 | PathHelper::makePath($this->poolDirectory, 0766);
218 | }
219 |
220 | /**
221 | * Ensures that the specified logger is valid, i.e. an instance of a class
222 | * implementing Psr\Log\LoggerInterface, or null.
223 | *
224 | * @param LoggerInterface|null $logger Logger to be checked.
225 | *
226 | * @throws CacheArgumentException
227 | * If the specified logger is neither null nor an instance of a class
228 | * implementing LoggerInterface.
229 | */
230 | private function ensureLoggerValidity($logger)
231 | {
232 | if ($logger !== null && !($logger instanceof LoggerInterface)) {
233 | throw new CacheArgumentException('The specified logger must be an instance'
234 | . ' of Psr\Log\LoggerInterface or null.');
235 | }
236 | }
237 |
238 | /**
239 | * Ensures that the specified pool name is valid.
240 | * If this condition is not met, CacheArgumentException will be thrown.
241 | *
242 | * @param string $pool Pool name to be checked.
243 | *
244 | * @throws CacheArgumentException
245 | * If the specified pool name is invalid.
246 | */
247 | private function ensurePoolNameValidity($pool)
248 | {
249 | if (!is_string($pool) || strlen($pool) === 0) {
250 | $this->logger->error('Pool name \'{pool}\' is invalid.', ['pool' => $pool]);
251 | throw new CacheArgumentException("Pool name '{$pool}' must not be null or empty.");
252 | }
253 | }
254 |
255 | /**
256 | * Ensures that the specified time to live is valid.
257 | * If this condition is not met, CacheArgumentException will be thrown.
258 | *
259 | * @param null|int|string|DateInterval $ttl TTL value in seconds (or as a DateInterval) where null
260 | * indicates this cache instance's default TTL.
261 | *
262 | * @throws CacheArgumentException
263 | * If the specified TTL is invalid.
264 | */
265 | private function ensureTimeToLiveValidity($ttl)
266 | {
267 | if ($ttl === null
268 | || is_int($ttl)
269 | ) {
270 | return;
271 | }
272 |
273 | if (is_string($ttl)
274 | && strtotime($ttl) !== false
275 | ) {
276 | return;
277 | }
278 |
279 | if ($ttl instanceof DateInterval) {
280 | return;
281 | }
282 |
283 | $this->logger->error('TTL \'{ttl}\' is invalid.', ['ttl' => $ttl]);
284 | throw new CacheArgumentException("Time to live '{$ttl}' is invalid.");
285 | }
286 |
287 | /**
288 | * Ensures that the specified cache directory is valid.
289 | * If this condition is not met, CacheArgumentException will be thrown.
290 | *
291 | * @param string $directory Cache directory.
292 | *
293 | * @throws CacheArgumentException
294 | * If the specified cache directory is invalid.
295 | */
296 | private function ensureCacheDirectoryValidity($directory)
297 | {
298 | if (!is_string($directory) || strlen($directory) === 0) {
299 | $this->logger->error('Cache directory \'{directory}\' is invalid.', ['directory' => $directory]);
300 | throw new CacheArgumentException("Cache directory '{$directory}' is invalid.");
301 | }
302 | }
303 |
304 | /**
305 | * Ensures that the specified string is valid cache key.
306 | * If this condition is not met, CacheArgumentException will be thrown.
307 | *
308 | * @param mixed $key Key to be checked.
309 | *
310 | * @throws CacheArgumentException
311 | * If the specified key is neither an array nor a Traversable.
312 | */
313 | private function ensureKeyValidity($key)
314 | {
315 | if (!is_string($key)
316 | || strlen($key) === 0
317 | || preg_match('~[{}()/\\\\@:]~', $key) === 1
318 | ) {
319 | $this->logger->error('Key \'{key}\' must be a non-empty string and must not contain \'{}()/\\@:\'.',
320 | ['key' => $key]);
321 |
322 | throw new CacheArgumentException("Key '{$key}' must be a non-empty string and must not contain '{}()/\\@:'.");
323 | }
324 | }
325 |
326 | /**
327 | * Ensures that the specified argument is either an array or an instance of Traversable.
328 | * If these conditions are not met, CacheArgumentException will be thrown.
329 | *
330 | * @param mixed $argument Argument to be checked.
331 | *
332 | * @throws CacheArgumentException
333 | * If the specified argument is neither an array nor a Traversable.
334 | */
335 | private function ensureArrayOrTraversable($argument)
336 | {
337 | if (!is_array($argument) && !$argument instanceof Traversable) {
338 | $message = 'Argument is neither an array nor a Traversable';
339 |
340 | $this->logger->error($message);
341 | throw new CacheArgumentException($message);
342 | }
343 | }
344 |
345 | /**
346 | * Normalizes the given TTL (time to live) value to an integer representing
347 | * the number of seconds an object is to be cached.
348 | *
349 | * @param null|int|string|DateInterval $ttl TTL value in seconds (or as a DateInterval) where null
350 | * indicates this cache instance's default TTL.
351 | *
352 | * @return int TTL in seconds.
353 | */
354 | private function normalizeTimeToLive($ttl) : int
355 | {
356 | if ($ttl === null) {
357 | return $this->defaultTimeToLive;
358 | } else if (is_string($ttl)) {
359 | return self::dateIntervalToSeconds(DateInterval::createFromDateString($ttl));
360 | } else if ($ttl instanceof DateInterval) {
361 | return self::dateIntervalToSeconds($ttl);
362 | } else {
363 | $ttl = (int)$ttl;
364 | return $ttl;
365 | }
366 | }
367 |
368 | /**
369 | * Checks whether the specified object is 'simple' or 'complex'.
370 | *
371 | * - Simple: null, integer, float, string, boolean, and array (only containing 'simple' objects).
372 | * - Complex: object, resource, and array (containing 'complex' objects).
373 | *
374 | * @param mixed $object Object to be analyzed.
375 | *
376 | * @return bool True if the specified object is considered 'simple', false otherwise.
377 | */
378 | private function isSimpleObject($object) : bool
379 | {
380 | return $this->oca->analyze($object) === ObjectComplexityAnalyzer::SIMPLE;
381 | }
382 |
383 | /**
384 | * Computes and returns the hash for the specified name, which is
385 | * either the key of an object or the name of a pool.
386 | *
387 | * @param string $name Name to be hashed.
388 | *
389 | * @return string
390 | */
391 | private function getHash(string $name) : string
392 | {
393 | return md5($name);
394 | }
395 |
396 | /**
397 | * Computes the filename of the cache file for the specified object.
398 | *
399 | * @param string $key Unique name of the object.
400 | *
401 | * @return string The filename of the cache file including *.php extension.
402 | */
403 | private function getCacheFileName(string $key) : string
404 | {
405 | $hash = $this->getHash($key);
406 | if ($this->subdivision) {
407 | // Take the first two characters of the hashed key as the name of the sub-directory.
408 | $cacheFileName = PathHelper::combine($this->cacheDirectory,
409 | $this->poolHash,
410 | substr($hash, 0, 2),
411 | $hash . '.litecache.php');
412 | } else {
413 | $cacheFileName = PathHelper::combine($this->cacheDirectory,
414 | $this->poolHash,
415 | $hash . '.litecache.php');
416 | }
417 |
418 | return $cacheFileName;
419 | }
420 |
421 | /**
422 | * Creates the sub-directory for the specified cache file.
423 | *
424 | * @param string $filename The filename of the cache file.
425 | */
426 | private function createCacheSubDirectory(string $filename)
427 | {
428 | if ($this->subdivision) {
429 | PathHelper::makePath(dirname($filename), 0766);
430 | }
431 | }
432 |
433 | /**
434 | * Writes data into the specified file using an exclusive lock.
435 | *
436 | * @param string $key Unique name of the object.
437 | * @param string[] $parts Array of strings, where each entry will be written
438 | * into the file in consecutive order.
439 | *
440 | * @return bool True on success and false on failure.
441 | *
442 | */
443 | private function writeDataToFile(string $key, array $parts) : bool
444 | {
445 | $filename = $this->getCacheFileName($key);
446 | $this->createCacheSubDirectory($filename);
447 |
448 | if (!$fp = @fopen($filename, 'c')) {
449 | $this->logger->error('File {filename} could not be created.', ['filename' => $filename]);
450 | return false;
451 | }
452 |
453 | if (!flock($fp, LOCK_EX)) {
454 | $this->logger->error('Lock on file {filename} could not be acquired.', ['filename' => $filename]);
455 | fclose($fp);
456 | return false;
457 | } else {
458 | ftruncate($fp, 0);
459 |
460 | foreach ($parts as $part) {
461 | fwrite($fp, $part);
462 | }
463 |
464 | fflush($fp);
465 | flock($fp, LOCK_UN);
466 | }
467 |
468 | fclose($fp);
469 | return true;
470 | }
471 |
472 | /**
473 | * Exports the object into its cache file using 'var_export()'.
474 | *
475 | * @param string $key Unique name of the object.
476 | * @param mixed $object Actual value to be cached.
477 | * @param int $ttl Time to live in seconds.
478 | * @param int $timestamp Indicates at what point in time the object has been stored.
479 | *
480 | * @return bool True on success, false on failure.
481 | */
482 | private function writeCodeCache(string $key, $object, int $ttl, int $timestamp) : bool
483 | {
484 | $comment = self::generateCacheFileComment('code', $key, $ttl, $timestamp);
485 | $condition = self::generateCacheFileExpirationCondition($ttl, $timestamp);
486 |
487 | $export = var_export($object, true);
488 | $code = "writeDataToFile($key, [$code]);
492 | }
493 |
494 | /**
495 | * Exports the object into its cache file using 'serialize()'.
496 | *
497 | * @param string $key Unique name of the object.
498 | * @param mixed $object Actual value to be cached.
499 | * @param int $ttl Time to live in seconds.
500 | * @param int $timestamp Indicates at what point in time the object has been stored.
501 | *
502 | * @return bool True on success, false on failure.
503 | */
504 | private function writeSerializedCache(string $key, $object, int $ttl, int $timestamp) : bool
505 | {
506 | $comment = self::generateCacheFileComment('serialized', $key, $ttl, $timestamp);
507 | $condition = self::generateCacheFileExpirationCondition($ttl, $timestamp);
508 |
509 | $code = "writeDataToFile($key, [$code, serialize($object)]);
525 | }
526 |
527 | /**
528 | * Caches (i.e. persists) the object under the given name with the specified TTL (time to live).
529 | *
530 | * @param string $key Unique name of the object.
531 | * @param mixed $object Actual value to be cached.
532 | * @param int $ttl Time to live in seconds or null for persistent caching.
533 | *
534 | * @return bool True on success and false on failure.
535 | */
536 | private function storeObject(string $key, $object, int $ttl) : bool
537 | {
538 | if ($this->isSimpleObject($object)) {
539 | $this->logger->info('Object {key} is \'simple\'.', ['key' => $key]);
540 | $result = $this->writeCodeCache($key, $object, $ttl, time());
541 | } else {
542 | $this->logger->info('Object {key} is \'complex\'.', ['key' => $key]);
543 | $result = $this->writeSerializedCache($key, $object, $ttl, time());
544 | }
545 |
546 | if (!$result) {
547 | $this->logger->error('Object {key} could not be cached.', ['key' => $key]);
548 | }
549 |
550 | return $result;
551 | }
552 |
553 | /**
554 | * Loads the cached object from the cache file.
555 | *
556 | * @param string $key Unique name of the object.
557 | *
558 | * @return mixed The cached object's value.
559 | */
560 | private function loadObject(string $key)
561 | {
562 | $cacheFileName = $this->getCacheFileName($key);
563 |
564 | /** @noinspection PhpIncludeInspection */
565 | $value = @include($cacheFileName);
566 | if (!$value) {
567 | $this->logger->notice('Cache miss on object {key}.', ['key' => $key]);
568 |
569 | // As per PSR-16, cache misses result in null.
570 | return null;
571 | }
572 |
573 | // Actual value is wrapped in an array in order to distinguish
574 | // between 'include' returning FALSE and FALSE as a value.
575 | return $value[0];
576 | }
577 |
578 | /**
579 | * Iterates through the specified directory and deletes all cache files.
580 | *
581 | * @param string $directory Directory to be iterated.
582 | *
583 | * @return bool True if all cache files were successfully deleted, false otherwise.
584 | */
585 | private function deleteAllCacheFilesInDirectory(string $directory)
586 | {
587 | $iterator = new DirectoryIterator($directory);
588 | foreach ($iterator as $file) {
589 | if (!$file->isDot()
590 | && !$file->isDir()
591 | && preg_match('/[0-9a-f]{32}\\.litecache.php/', $file->getFilename())
592 | ) {
593 | if (!@unlink($file->getPathname())) {
594 | $this->logger->error('Cache file {filename} could not be deleted.',
595 | ['filename' => $file->getFilename()]);
596 | return false;
597 | }
598 | }
599 | }
600 |
601 | return true;
602 | }
603 |
604 | /**
605 | * Iterates through the specified cache directory and recursively deletes
606 | * all cache files from all subdirectories.
607 | *
608 | * @param string $directory Directory to be iterated.
609 | *
610 | * @return bool True if all cache files were successfully deleted, false otherwise.
611 | */
612 | private function deleteAllCacheFilesInAllSubDirectories(string $directory)
613 | {
614 | $iterator = new DirectoryIterator($directory);
615 | foreach ($iterator as $file) {
616 | if (!$file->isDot()
617 | && $file->isDir()
618 | && preg_match('/[0-9a-f]{2}/', $file->getFilename())
619 | ) {
620 | if (!$this->deleteAllCacheFilesInDirectory($file->getPathname())) {
621 | return false;
622 | }
623 |
624 | if (!@rmdir($file->getPathname())) {
625 | $this->logger->warning('Could not delete subdirectory {directory} (it may not be empty).',
626 | ['directory' => $file->getPathname()]);
627 | }
628 | }
629 | }
630 |
631 | return true;
632 | }
633 |
634 | /**
635 | * Gets the user defined cache directory.
636 | *
637 | * @return string
638 | */
639 | public function getCacheDirectory() : string
640 | {
641 | return $this->cacheDirectory;
642 | }
643 |
644 | /**
645 | * Gets the user defined default TTL (time to live) in seconds.
646 | *
647 | * @return int
648 | */
649 | public function getDefaultTimeToLive() : int
650 | {
651 | return $this->defaultTimeToLive;
652 | }
653 |
654 | /**
655 | * Fetches a value from the cache.
656 | *
657 | * @param string $key The unique key of this item in the cache.
658 | * @param mixed $default Default value to return if the key does not exist.
659 | *
660 | * @return mixed The value of the item from the cache, or $default in case of cache miss.
661 | *
662 | * @throws \Psr\SimpleCache\InvalidArgumentException
663 | * MUST be thrown if the $key string is not a legal value.
664 | */
665 | public function get($key, $default = null)
666 | {
667 | $this->ensureKeyValidity($key);
668 | $object = $this->loadObject($key);
669 |
670 | if ($object === null) {
671 | return $default;
672 | }
673 |
674 | $this->logger->info('Object {key} loaded from cache.', ['key' => $key]);
675 | return $object;
676 | }
677 |
678 | /**
679 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
680 | *
681 | * @param string $key The key of the item to store.
682 | * @param mixed $value The value of the item to store, must be serializable.
683 | * @param null|int|string|DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
684 | * the driver supports TTL then the library may set a default value
685 | * for it or let the driver take care of that.
686 | *
687 | * @return bool True on success and false on failure.
688 | *
689 | * @throws \Psr\SimpleCache\InvalidArgumentException
690 | * MUST be thrown if the $key string is not a legal value.
691 | */
692 | public function set($key, $value, $ttl = null)
693 | {
694 | $this->ensureKeyValidity($key);
695 |
696 | // According to PSR-16, it is not possible to distinguish between
697 | // null and a cache miss; there is no need to store null.
698 | if ($value === null) {
699 | $this->logger->info('Object {key} skipped (was null).', ['key' => $key]);
700 | return true;
701 | }
702 |
703 | $ttl = $this->normalizeTimeToLive($ttl);
704 | $result = $this->storeObject($key, $value, $ttl);
705 |
706 | if ($result) {
707 | $this->logger->info('Object {key} cached.', ['key' => $key]);
708 | }
709 |
710 | return $result;
711 | }
712 |
713 | /**
714 | * Gets the object with the specified name. If the object has been previously cached
715 | * and has not expired yet, the cached version will be returned. If the object has not
716 | * been previously cached or the cache file has expired, the specified producer will be
717 | * called and the new version will be cached and returned.
718 | *
719 | * @param string $key Unique name of the object.
720 | * @param callable $producer Producer that will be called to generate the data
721 | * if the cached object has expired.
722 | * @param null|int|string|DateInterval $ttl The TTL (time to live) value for the object.
723 | *
724 | * @return mixed The cached object or a newly created version if it has expired.
725 | *
726 | * @throws CacheArgumentException
727 | * If the object could not be cached or loaded.
728 | *
729 | * @throws CacheProducerException
730 | * If the specified producers throws an exception.
731 | */
732 | public function cache(string $key, callable $producer, $ttl = null)
733 | {
734 | $this->ensureKeyValidity($key);
735 |
736 | $object = $this->get($key);
737 | if ($object !== null) {
738 | // Object's still in cache and has not expired.
739 | return $object;
740 | } else {
741 |
742 | try {
743 | // If object is not cached or has expired, call producer to obtain
744 | // the new value and subsequently cache it.
745 | $object = $producer();
746 | } catch (Throwable $t) {
747 | $this->logger->error('Producer for \'{key}\' has thrown an exception.', ['key' => $key]);
748 | throw new CacheProducerException($t);
749 | }
750 |
751 | $this->set($key, $object, $ttl);
752 | return $object;
753 | }
754 | }
755 |
756 | /**
757 | * Delete an item from the cache by its unique key.
758 | *
759 | * @param string $key The unique cache key of the item to delete.
760 | *
761 | * @return bool True if the item was successfully removed.
762 | * False if there was an error.
763 | *
764 | * @throws \Psr\SimpleCache\InvalidArgumentException
765 | * MUST be thrown if the $key string is not a legal value.
766 | */
767 | public function delete($key)
768 | {
769 | $this->ensureKeyValidity($key);
770 |
771 | $cacheFileName = $this->getCacheFileName($key);
772 | if (!@unlink($cacheFileName)) {
773 | $this->logger->error('Object {key} could not be deleted.', ['key' => $key]);
774 | return false;
775 | }
776 |
777 | return true;
778 | }
779 |
780 | /**
781 | * Wipes clean the entire cache's keys.
782 | *
783 | * @return bool True on success and false on failure.
784 | */
785 | public function clear()
786 | {
787 | return ($this->deleteAllCacheFilesInDirectory($this->poolDirectory)
788 | && $this->deleteAllCacheFilesInAllSubDirectories($this->poolDirectory));
789 | }
790 |
791 | /** @noinspection PhpUndefinedClassInspection */
792 | /**
793 | * Obtains multiple cache items by their unique keys.
794 | *
795 | * @param mixed $keys A list of keys that can obtained in a single operation.
796 | * @param mixed $default Default value to return for keys that do not exist.
797 | *
798 | * @return mixed A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
799 | *
800 | * @throws \Psr\SimpleCache\InvalidArgumentException
801 | * MUST be thrown if $keys is neither an array nor a Traversable,
802 | * or if any of the $keys are not a legal value.
803 | */
804 | public function getMultiple($keys, $default = null)
805 | {
806 | $this->ensureArrayOrTraversable($keys);
807 |
808 | $objects = [];
809 | foreach ($keys as $key) {
810 | $objects[$key] = $this->get((string)$key, $default);
811 | }
812 |
813 | return $objects;
814 | }
815 |
816 | /** @noinspection PhpUndefinedClassInspection */
817 | /**
818 | * Persists a set of key => value pairs in the cache, with an optional TTL.
819 | *
820 | * @param mixed $values A list of key => value pairs for a multiple-set operation.
821 | * @param null|int|string|DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
822 | * the driver supports TTL then the library may set a default value
823 | * for it or let the driver take care of that.
824 | *
825 | * @return bool True on success and false on failure.
826 | *
827 | * @throws \Psr\SimpleCache\InvalidArgumentException
828 | * MUST be thrown if $values is neither an array nor a Traversable,
829 | * or if any of the $values are not a legal value.
830 | */
831 | public function setMultiple($values, $ttl = null)
832 | {
833 | $this->ensureArrayOrTraversable($values);
834 |
835 | foreach ($values as $key => $value) {
836 | if (is_int($key)) {
837 | $key = (string)$key;
838 | }
839 |
840 | if (!$this->set($key, $value, $ttl)) {
841 | return false;
842 | }
843 | }
844 |
845 | return true;
846 | }
847 |
848 | /** @noinspection PhpUndefinedClassInspection */
849 | /**
850 | * Deletes multiple cache items in a single operation.
851 | *
852 | * @param mixed $keys A list of string-based keys to be deleted.
853 | *
854 | * @return bool True if the items were successfully removed. False if there was an error.
855 | *
856 | * @throws \Psr\SimpleCache\InvalidArgumentException
857 | * MUST be thrown if $keys is neither an array nor a Traversable,
858 | * or if any of the $keys are not a legal value.
859 | */
860 | public function deleteMultiple($keys)
861 | {
862 | $this->ensureArrayOrTraversable($keys);
863 |
864 | foreach ($keys as $key) {
865 | if (!$this->delete($key)) {
866 | return false;
867 | }
868 | }
869 |
870 | return true;
871 | }
872 |
873 | /**
874 | * Determines whether an item is present in the cache.
875 | *
876 | * NOTE: It is recommended that has() is only to be used for cache warming type purposes
877 | * and not to be used within your live applications operations for get/set, as this method
878 | * is subject to a race condition where your has() will return true and immediately after,
879 | * another script can remove it making the state of your app out of date.
880 | *
881 | * @param string $key The cache item key.
882 | *
883 | * @return bool
884 | *
885 | * @throws \Psr\SimpleCache\InvalidArgumentException
886 | * MUST be thrown if the $key string is not a legal value.
887 | */
888 | public function has($key)
889 | {
890 | $this->ensureKeyValidity($key);
891 |
892 | $cacheFileName = $this->getCacheFileName($key);
893 | return file_exists($cacheFileName);
894 | }
895 | }
896 |
897 |
--------------------------------------------------------------------------------