├── examples
├── cache
│ └── .gitkeep
├── log
│ └── .gitkeep
├── demo-with-config.php
├── demo-clear-cache.php
├── demo-monolog.php
├── demo-default-logger.php
├── demo.php
├── demo-headers.php
├── demo-api.php
├── demo-mobiledetect.php
├── config.php
├── demo-session-support.php
└── demo-session-exclude-keys.php
├── tests
├── tmp
│ ├── cache
│ │ └── .gitignore
│ └── .gitignore
├── Integration
│ ├── www
│ │ ├── cache
│ │ │ └── .gitignore
│ │ ├── logs
│ │ │ └── .gitignore
│ │ ├── 1.php
│ │ ├── 2.php
│ │ ├── 3.php
│ │ ├── 4.php
│ │ ├── index.php
│ │ └── 5.php
│ └── IntegrationPsrCacheTest.php
├── bootstrap.php
├── config_wrong_test.php
├── Strategy
│ ├── DefaultStrategyTest.php
│ └── MobileStrategyTest.php
├── config_test.php
├── Storage
│ └── FileSystem
│ │ ├── FileSystemCacheAdapterTest.php
│ │ ├── HashDirectoryTest.php
│ │ └── FileSystemTest.php
├── SessionHandlerTest.php
├── ConfigTest.php
└── PageCacheTest.php
├── .gitignore
├── .travis.yml
├── .php_cs
├── src
├── PageCacheException.php
├── Storage
│ ├── CacheAdapterException.php
│ ├── InvalidArgumentException.php
│ ├── CacheItemInterface.php
│ ├── CacheItem.php
│ ├── CacheItemStorage.php
│ └── FileSystem
│ │ ├── FileSystem.php
│ │ ├── HashDirectory.php
│ │ └── FileSystemCacheAdapter.php
├── StrategyInterface.php
├── Strategy
│ ├── DefaultStrategy.php
│ └── MobileStrategy.php
├── DefaultLogger.php
├── SessionHandler.php
├── HttpHeaders.php
├── Config.php
└── PageCache.php
├── phpunit.xml
├── LICENSE.txt
├── composer.json
├── CHANGELOG.md
└── README.md
/examples/cache/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/tmp/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/tests/tmp/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !cache
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/tests/Integration/www/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
4 |
--------------------------------------------------------------------------------
/tests/Integration/www/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | .DS_Store
3 | vendor/
4 | composer.lock
5 | phpdoc.xml
6 | docs/
7 | /examples/log/*
8 | !examples/log/.gitkeep
9 | /examples/cache/*
10 | !examples/cache/.gitkeep
11 | coverage/
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 5.6
4 | - 7.0
5 | - 7.1
6 | - 7.2
7 |
8 | branches:
9 | only:
10 | - master
11 |
12 | before_script:
13 | - phpenv config-rm xdebug.ini
14 | - composer require phpunit/phpunit ^5
15 | - composer install -o
16 | - phpunit --version
17 |
18 | script:
19 | - ./vendor/bin/phpunit -v -c phpunit.xml
20 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | session_start();
12 |
13 | require __DIR__.'/../vendor/autoload.php';
14 |
--------------------------------------------------------------------------------
/.php_cs:
--------------------------------------------------------------------------------
1 | exclude('vendor')
5 | ->exclude('examples/cache')
6 | ->exclude('examples/log')
7 | ->exclude('tests/tmp')
8 | ->exclude('docs')
9 | ->in(__DIR__)
10 | ;
11 |
12 | return Symfony\CS\Config\Config::create()
13 | ->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
14 | ->fixers(array('-psr0'))
15 | ->finder($finder)
16 | ;
17 |
--------------------------------------------------------------------------------
/src/PageCacheException.php:
--------------------------------------------------------------------------------
1 |
6 | * @package PageCache
7 | * @copyright 2017
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | namespace PageCache;
14 |
15 | class PageCacheException extends \Exception
16 | {
17 | }
18 |
--------------------------------------------------------------------------------
/src/Storage/CacheAdapterException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Storage;
12 |
13 | use Psr\SimpleCache\CacheException;
14 |
15 | class CacheAdapterException extends \Exception implements CacheException
16 | {
17 | }
18 |
--------------------------------------------------------------------------------
/src/Storage/InvalidArgumentException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Storage;
12 |
13 | class InvalidArgumentException extends \InvalidArgumentException implements \Psr\SimpleCache\InvalidArgumentException
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | tests
5 |
6 |
7 |
8 |
9 | src
10 |
11 | examples/config.php
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/config_wrong_test.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 |
12 | /**
13 | * Testing parameters for PageCache
14 | */
15 | return [
16 | //current page's cache expiration in seconds. Set to 10 minutes:
17 | 'cache_expiration_in_seconds' => -10,
18 | ];
19 |
--------------------------------------------------------------------------------
/src/StrategyInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache;
13 |
14 | /**
15 | * Interface for cache file naming strategy
16 | *
17 | * @package PageCache
18 | */
19 | interface StrategyInterface
20 | {
21 | /**
22 | * Returns cache data key
23 | *
24 | * @return string Cache data key
25 | */
26 | public function strategy();
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Integration/IntegrationPsrCacheTest.php:
--------------------------------------------------------------------------------
1 |
6 | * @author Muhammed Mamedov
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache\Tests\Integration;
13 |
14 | use Cache\IntegrationTests\SimpleCacheTest;
15 | use PageCache\Storage\FileSystem\FileSystemCacheAdapter;
16 |
17 | /**
18 | * Class IntegrationPsrCacheTest
19 | *
20 | * @package PageCache\Tests\Integration
21 | * @group psr16
22 | */
23 | class IntegrationPsrCacheTest extends SimpleCacheTest
24 | {
25 | public function createSimpleCache()
26 | {
27 | $directory = realpath(__DIR__.DIRECTORY_SEPARATOR.'www'.DIRECTORY_SEPARATOR.'cache');
28 |
29 | return new FileSystemCacheAdapter(
30 | $directory,
31 | LOCK_EX | LOCK_NB,
32 | 0 // Always create cache file
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Muhammed Mamedov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/tests/Strategy/DefaultStrategyTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Tests\Strategy;
12 |
13 | use PageCache\SessionHandler;
14 | use PageCache\Strategy\DefaultStrategy;
15 | use PageCache\StrategyInterface;
16 |
17 | class DefaultStrategyTest extends \PHPUnit\Framework\TestCase
18 | {
19 | public function testStrategy()
20 | {
21 | $strategy = new DefaultStrategy();
22 | $this->assertTrue($strategy instanceof StrategyInterface);
23 |
24 | SessionHandler::disable();
25 |
26 | $uri = empty($_SERVER['REQUEST_URI']) ? 'uri' : $_SERVER['REQUEST_URI'];
27 | $query = empty($_SERVER['QUERY_STRING']) ? 'query' : $_SERVER['QUERY_STRING'];
28 | $md5 = md5($uri . $_SERVER['SCRIPT_NAME'] . $query);
29 |
30 | $this->assertEquals($md5, $strategy->strategy());
31 |
32 | SessionHandler::enable();
33 | $this->assertNotEmpty($strategy->strategy());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Strategy/DefaultStrategy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache\Strategy;
13 |
14 | use PageCache\SessionHandler;
15 | use PageCache\StrategyInterface;
16 |
17 | /**
18 | * DefaultStrategy is a default cache file naming strategy, based on incoming url and session(or not)
19 | *
20 | * @package PageCache
21 | */
22 | class DefaultStrategy implements StrategyInterface
23 | {
24 | /**
25 | * Generate cache filename
26 | *
27 | * @return string md5
28 | */
29 | public function strategy()
30 | {
31 | //when session support is enabled add that to file name
32 | $session_str = SessionHandler::process();
33 |
34 | $uri = empty($_SERVER['REQUEST_URI']) ? 'uri' : $_SERVER['REQUEST_URI'];
35 | $query = empty($_SERVER['QUERY_STRING']) ? 'query' : $_SERVER['QUERY_STRING'];
36 |
37 | return md5($uri . $_SERVER['SCRIPT_NAME'] . $query . $session_str);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/demo-with-config.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates use of global config.php config file in PageCache.
16 | *
17 | * It's useful to have settings defined in one file, to avoid repeating yourself
18 | *
19 | */
20 |
21 | require_once __DIR__ . '/../vendor/autoload.php';
22 |
23 | use PageCache\PageCache;
24 |
25 | //PageCache configuration in a file
26 | $config_file = __DIR__ . '/config.php';
27 |
28 | //pass config file
29 | $cache = new PageCache($config_file);
30 | //To clear this page's cache
31 | //$cache->clearCache();
32 | $cache->init();
33 |
34 | ?>
35 |
36 |
37 |
Example with Configuration file
38 |
This is a demo PageCache page that is going to be cached.
39 |
Demo with conf.php configuration file usage.
40 |
This is a dynamic PHP date('H:i:s')
41 | call, note that time doesn't change on refresh: .
42 |
43 |
Check examples/cache/ directory to see cached content.
44 | Erase this file to regenerate cache, or it will automatically be regenerated in 10 minutes, as per conf.php
45 |
46 |
--------------------------------------------------------------------------------
/src/DefaultLogger.php:
--------------------------------------------------------------------------------
1 |
6 | * @package PageCache
7 | * @copyright 2017
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | namespace PageCache;
14 |
15 | use Psr\Log\AbstractLogger;
16 |
17 | class DefaultLogger extends AbstractLogger
18 | {
19 | private $file;
20 |
21 | /**
22 | * DefaultLogger constructor.
23 | *
24 | * @param $file
25 | */
26 | public function __construct($file)
27 | {
28 | $this->file = $file;
29 | }
30 |
31 | /**
32 | * Logs with an arbitrary level.
33 | *
34 | * @param mixed $level
35 | * @param string $message
36 | * @param array $context
37 | *
38 | * @return void
39 | */
40 | public function log($level, $message, array $context = [])
41 | {
42 | $exception = isset($context['exception']) ? $context['exception'] : null;
43 | $microTime = microtime(true);
44 | $micro = sprintf("%06d", ($microTime - floor($microTime)) * 1000000);
45 | $logTime = (new \DateTime(date('Y-m-d H:i:s.' . $micro, $microTime)))->format('Y-m-d H:i:s.u');
46 | error_log(
47 | '[' . $logTime . '] '
48 | .$message.($exception ? ' {Exception: '.$exception->getMessage().'}' : '')."\n",
49 | 3,
50 | $this->file,
51 | null
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Strategy/MobileStrategyTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace PageCache\Tests\Strategy;
11 |
12 | use PageCache\SessionHandler;
13 | use PageCache\Strategy\MobileStrategy;
14 | use PageCache\StrategyInterface;
15 |
16 | class MobileStrategyTest extends \PHPUnit\Framework\TestCase
17 | {
18 | public function testStrategy()
19 | {
20 | //MobileDetection stub, to simulate a mobile device
21 | $mobilestub = $this->getMockBuilder('Mobile_Detect')
22 | ->setMethods(array('isMobile', 'isTablet'))
23 | ->getMock();
24 |
25 | $mobilestub->method('isMobile')
26 | ->willReturn(true);
27 |
28 | $mobilestub->method('isTablet')
29 | ->willReturn(false);
30 |
31 | $strategy = new MobileStrategy($mobilestub);
32 |
33 | //expected string, with -mob in the end
34 | SessionHandler::disable();
35 | $uri = empty($_SERVER['REQUEST_URI']) ? 'uri' : $_SERVER['REQUEST_URI'];
36 | $query = empty($_SERVER['QUERY_STRING']) ? 'query' : $_SERVER['QUERY_STRING'];
37 | $md5 = md5($uri . $_SERVER['SCRIPT_NAME'] . $query) . '-mob';
38 |
39 | $this->assertTrue($mobilestub instanceof \Mobile_Detect);
40 | $this->assertTrue($strategy instanceof StrategyInterface);
41 | $this->assertEquals($md5, $strategy->strategy());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/Integration/www/1.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test 1
6 |
7 |
8 | Lorem ipsum dolor sit amet, vocibus corpora interesset per ea, eu enim melius diceret mel, vel ei dolorem epicuri eleifend? Omnis quodsi omnesque ut duo, vel rebum debet et. Integre iuvaret facilisi pro at. Ad eum decore elaboraret, in sea mutat consequat, nobis fabellas cu quo? Aliquip ornatus urbanitas duo no, pri vidit solet dignissim eu. At dicit oblique mediocritatem quo, ius te populo iuvaret inciderint, sonet nostrud eu vis.
9 |
10 | Albucius accusata liberavisse ut nam? Vivendum sapientem te has. His essent dignissim ea, ut feugiat percipitur mea. Ut sed meis tation. Sit quas tempor in, ut est doming epicuri theophrastus!
11 |
12 | Eu erat equidem eligendi vim. Pri hinc soluta eu. Eius tantas tempor ne duo, vitae principes forensibus sea in! Duo ei populo referrentur, duo et possim salutandi intellegam, an vim eius semper recteque?
13 |
14 | Usu et iudico offendit forensibus, ea assum aliquando nec. Ei dolor scripta sea, cum ex admodum argumentum, ad vix utamur facilisis evertitur. No pri viderer antiopam, et porro mundi recteque has, ex autem aperiri vis. Mea ea alterum disputando omittantur, dolores ocurreret in pri. Enim animal expetenda mei ad, solet voluptatum temporibus ne usu, mea an eius audire recusabo!
15 |
16 | Nec justo laudem postea ea, mei eu quando libris, ex imperdiet vituperata constituam ius. Ut usu viris deserunt. Mea adhuc pertinacia ei! Ius primis vocibus te, oportere ocurreret mel ei, mel ei delectus definiebas?
17 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/demo-clear-cache.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates basic caching functionality of PageCache.
16 | *
17 | */
18 |
19 | require_once __DIR__ . '/../vendor/autoload.php';
20 |
21 | use PageCache\PageCache;
22 |
23 | $cache = new PageCache();
24 | $cache->config()->setCachePath(__DIR__ . '/cache/');
25 |
26 | //Clear cache.
27 | //Note that setPath() must be called prior to this, or 'cache_path' config parameter must be configured.
28 | $cache->clearAllCache();
29 | $cache->init();
30 |
31 | ?>
32 |
33 |
34 |
Example Clear Cache
35 |
Notice this page is not being cached, because clearCache() is being called before init()
36 |
Default cache expiration time for this page is 20 minutes. You can change this value in your conf.php
37 | and passing its file path to PageCache constructor, or by calling setExpiration() method.
38 | Refresh browser to see changes.
39 |
This is a dynamic PHP date('H:i:s')
40 | call, note that time doesn't change on refresh: .
41 |
42 |
Check examples/cache/ directory to see cached content.
43 | Erase this file to regenerate cache, or it will automatically be regenerated in 20 minutes.
44 |
45 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mmamedov/page-cache",
3 | "type": "library",
4 | "description": "PageCache is a lightweight PHP library for full page cache. It uses various strategies to differentiate among separate versions of the same page.",
5 | "keywords": ["cache", "page cache", "caching", "full page cache", "file based cache", "php cache", "php page cache"],
6 | "homepage": "https://github.com/mmamedov/page-cache",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Muhammed Mamedov",
11 | "email": "mm@turkmenweb.net",
12 | "homepage": "http://www.turkmenweb.com/en/",
13 | "role": "Developer"
14 | }
15 | ],
16 | "require": {
17 | "php": ">=5.6.0",
18 | "psr/simple-cache": "^1.0",
19 | "psr/log": "^1.0"
20 | },
21 | "require-dev": {
22 | "mobiledetect/mobiledetectlib": "^2.8",
23 | "monolog/monolog": "^1.23",
24 | "mikey179/vfsStream": "^1.6",
25 | "guzzlehttp/guzzle": "^6.3",
26 | "cache/integration-tests": "dev-master",
27 | "symfony/cache": "^3.4 || ^4.0",
28 | "symfony/process": "^3.4 || ^4.0",
29 | "phpunit/phpunit": "^5"
30 | },
31 | "suggest": {
32 | "monolog/monolog": "Allows using monolog for logging",
33 | "mobiledetect/mobiledetectlib": "Allows separate cache versions for mobile pages"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "PageCache\\": "src/"
38 | }
39 | },
40 | "autoload-dev": {
41 | "psr-4": {
42 | "PageCache\\Tests\\": "tests/"
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/Storage/CacheItemInterface.php:
--------------------------------------------------------------------------------
1 |
6 | * @package PageCache
7 | * @copyright 2017
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | namespace PageCache\Storage;
14 |
15 | use DateTime;
16 |
17 | /**
18 | * Describes data stored in cache item
19 | */
20 | interface CacheItemInterface
21 | {
22 | /**
23 | * @return string
24 | */
25 | public function getKey();
26 |
27 | /**
28 | * @return \DateTime
29 | */
30 | public function getCreatedAt();
31 |
32 | /**
33 | * @return \DateTime
34 | */
35 | public function getLastModified();
36 |
37 | /**
38 | * @param \DateTime $time
39 | *
40 | * @return $this
41 | */
42 | public function setLastModified(DateTime $time);
43 |
44 | /**
45 | * @return \DateTime
46 | */
47 | public function getExpiresAt();
48 |
49 | /**
50 | * @param \DateTime $time
51 | *
52 | * @return $this
53 | */
54 | public function setExpiresAt(DateTime $time);
55 |
56 | /**
57 | * @return string
58 | */
59 | public function getETagString();
60 |
61 | /**
62 | * @param string $value
63 | *
64 | * @return $this
65 | */
66 | public function setETagString($value);
67 |
68 | /**
69 | * @return string
70 | */
71 | public function getContent();
72 |
73 | /**
74 | * @param string $data
75 | *
76 | * @return $this
77 | */
78 | public function setContent($data);
79 | }
80 |
--------------------------------------------------------------------------------
/examples/demo-monolog.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates integration with Monolog logger
16 | *
17 | */
18 |
19 | require_once __DIR__ . '/../vendor/autoload.php';
20 |
21 | use PageCache\PageCache;
22 | use Monolog\Logger;
23 | use Monolog\Handler\StreamHandler;
24 |
25 | //Setup PageCache
26 | $cache = new PageCache();
27 | $cache->config()->setCachePath(__DIR__ . '/cache/')
28 | //Enable logging
29 | ->setEnableLog(true);
30 |
31 | //Monolog setup. More info on https://github.com/Seldaek/monolog
32 | $logger = new Logger('PageCache');
33 | $logger->pushHandler(new StreamHandler(__DIR__ . '/log/monolog.log', Logger::DEBUG));
34 |
35 | //pass Monolog to PageCache
36 | $cache->setLogger($logger);
37 |
38 | //Initiate cache engine
39 | $cache->init();
40 |
41 | ?>
42 |
43 |
44 |
PageCache logging with monolog example
45 |
This is a demo PageCache page that is going to be cached. Monolog log entries are saved.
46 |
Check out examples/log/monolog.log file for Monolog entries.
47 |
Default cache expiration time for this page is 20 minutes. You can change this value in your conf.php
48 | and passing its file path to PageCache constructor, or by calling setExpiration() method.
49 | Refresh browser to see changes.
50 |
This is a dynamic PHP date('H:i:s')
51 | call, note that time doesn't change on refresh: .
52 |
53 |
Check examples/cache/ directory to see cached content.
54 | Erase this file to regenerate cache, or it will automatically be regenerated in 20 minutes.
55 |
56 |
--------------------------------------------------------------------------------
/examples/demo-default-logger.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2018
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates usage of default logger
16 | *
17 | */
18 |
19 | require_once __DIR__ . '/../vendor/autoload.php';
20 |
21 | use PageCache\PageCache;
22 |
23 | //Setup PageCache
24 | try {
25 | $cache = new PageCache();
26 | $cache->config()
27 | ->setCachePath(__DIR__ . '/cache/')
28 | //Enable logging: set log file path and enable log
29 | ->setEnableLog(true)
30 | ->setLogFilePath(__DIR__.'/log/default_logger.log')
31 | ;
32 |
33 | //Initiate cache engine
34 | $cache->init();
35 | } catch (\Exception $e) {
36 | // Log PageCache error or simply do nothing.
37 | // In case of PageCache error, page will load normally
38 |
39 | // Do not enable line below in Production. Error output should be used during development only.
40 | echo '
'.$e->getMessage().'
';
41 | }
42 |
43 |
44 | ?>
45 |
46 |
47 |
PageCache logging with default Logger example
48 |
This is a demo PageCache page that is going to be cached.
49 |
Check out examples/log/default_logger.log file for logger entries.
50 |
Default cache expiration time for this page is 20 minutes. You can change this value in your conf.php
51 | and passing its file path to PageCache constructor, or by calling setExpiration() method.
52 | Refresh browser to see changes.
53 |
This is a dynamic PHP date('H:i:s')
54 | call, note that time doesn't change on refresh: .
55 |
56 |
Check examples/cache/ directory to see cached content.
57 | Erase this file to regenerate cache, or it will automatically be regenerated in 20 minutes.
58 |
59 |
--------------------------------------------------------------------------------
/examples/demo.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates basic caching functionality of PageCache.
16 | *
17 | */
18 |
19 | require_once __DIR__ . '/../vendor/autoload.php';
20 |
21 | /**
22 | * When not specified using a config file or by calling methods, the following parameters are set automatically:
23 | *
24 | * cache_expire = 1200 seconds
25 | * min_cache_file_size = 10
26 | * file_lock = LOCK_EX | LOCK_NB
27 | * use_session = false
28 | * send_headers = false
29 | * forward_headers = false
30 | * enable_log = false
31 | * .. For full list of default values check Config class file
32 | *
33 | */
34 | use PageCache\PageCache;
35 |
36 | try {
37 | $cache = new PageCache();
38 | $cache->config()
39 | ->setCachePath(__DIR__ . '/cache/')
40 | ->setSendHeaders(true);
41 | $cache->init();
42 | } catch (\Exception $e) {
43 | // Log PageCache error or simply do nothing.
44 | // In case of PageCache error, page will load normally
45 |
46 | // Do not enable line below in Production. Error output should be used during development only.
47 | echo '
'.$e->getMessage().'
';
48 | }
49 |
50 | ?>
51 |
52 |
53 |
Example #1
54 |
This is a basic demo PageCache page that is going to be cached.
55 |
Default cache expiration time for this page is 20 minutes. You can change this value in your conf.php
56 | and passing its file path to PageCache constructor, or by calling setExpiration() method.
57 | Refresh browser to see changes.
58 |
This is a dynamic PHP date('H:i:s')
59 | call, note that time doesn't change on refresh: .
60 |
61 |
Check examples/cache/ directory to see cached content.
62 | Erase this file to regenerate cache, or it will automatically be regenerated in 20 minutes.
63 |
64 |
--------------------------------------------------------------------------------
/tests/config_test.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 |
12 | /**
13 | * Testing parameters for PageCache
14 | *
15 | * If any of the parameters are changed, please change test cases (esp. ConfigTest)
16 | */
17 | return [
18 |
19 | //generated cache files less than this many bytes, are considered invalid and are regenerated
20 | //adjust accordingly
21 | 'min_cache_file_size' => 1,
22 |
23 | // set true to enable loging, not recommended for production use, only for debugging
24 | 'enable_log' => false,
25 |
26 | //current page's cache expiration in seconds. Set to 10 minutes:
27 | 'cache_expiration_in_seconds' => 10 * 60,
28 |
29 | //log file location, enable_log must be true for loging to work
30 | 'log_file_path' => __DIR__ . '/tmp',
31 |
32 | //cache directory location (mind the trailing slash "/")
33 | 'cache_path' => __DIR__ . '/tmp/cache/',
34 |
35 | /**
36 | * Use session or not
37 | */
38 | 'use_session' => false,
39 |
40 | /**
41 | * Exclude $_SESSION key(s) from caching strategies. Pass session name as keys to the array.
42 | *
43 | *
44 | * When to use: Your application changes $_SESSION['count'] variable, but that doesn't reflect on the page
45 | * content. Exclude this variable, otherwise PageCache will generate seperate cache files for each
46 | * value of $_SESSION['count] session variable.
47 | * Example: 'session_exclude_keys'=>array('count')
48 | */
49 | 'session_exclude_keys' => [],
50 |
51 | /**
52 | *
53 | * Locking mechanism to use when writing cache files. Default is LOCK_EX | LOCK_NB, which locks for
54 | * exclusive write while being non-blocking. Set whatever you want.
55 | * Read for details (http://php.net/manual/en/function.flock.php)
56 | *
57 | * Set file_lock = false to disable file locking.
58 | */
59 | 'file_lock' => LOCK_EX | LOCK_NB,
60 |
61 | //Send HTTP headers
62 | 'send_headers' => false,
63 |
64 | 'forward_headers' => false,
65 | ];
66 |
--------------------------------------------------------------------------------
/examples/demo-headers.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2017
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates how HTTP cache related headers are sent in PageCache
16 | *
17 | */
18 |
19 | /**
20 | * To test concept, once files are cached uncomment 2 lines below.
21 | * Nothing at all will be sent to the browser except the 304 Header, and you should still see page contents.
22 | */
23 | //header('HTTP/1.1 304 Not Modified');
24 | //exit();
25 |
26 |
27 | require_once __DIR__ . '/../vendor/autoload.php';
28 |
29 | use PageCache\PageCache;
30 |
31 | $cache = new PageCache();
32 | $cache->config()->setCachePath(__DIR__ . '/cache/')
33 | ->setEnableLog(true)
34 | ->setLogFilePath(__DIR__ . '/log/cache.log')
35 | ->setCacheExpirationInSeconds(600)
36 | // Uncomment to enable Dry Run mode
37 | // ->setDryRunMode(false)
38 | ->setSendHeaders(true);
39 | // Uncomment to delete this page from cache
40 | //$cache->clearPageCache();
41 | $cache->init();
42 |
43 | ?>
44 |
45 |
46 |
Example with HTTP caching headers
47 |
This is a demo PageCache page that is going to be cached.
48 |
Notice that first call to this URL resulted in HTTP Response code 200. Consequent calls,
49 | until page expiration, will result in 304 Not Modified. When 304 is being returned, no content is
50 | retrieved from the server, which makes your application load super fast - cached content comes
51 | from web browser.
52 |
Default cache expiration time for this page is 1 minutes. You can change this value in your conf.php
53 | and passing its file path to PageCache constructor, or by calling setExpiration() method.
54 | Refresh browser to see changes.
55 |
This is a dynamic PHP date('H:i:s')
56 | call, note that time doesn't change on refresh: .
57 |
58 |
Check examples/cache/ directory to see cached content.
59 | Erase this file to regenerate cache, or it will automatically be regenerated in 1 minute.
60 |
61 |
--------------------------------------------------------------------------------
/tests/Integration/www/2.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test 2
6 |
7 |
8 | Lorem ipsum dolor sit amet, eam viderer molestiae an. No illum dictas singulis pri, vidit simul aliquando sed ex. At eos prima torquatos, agam nobis minimum sea in. Ei mentitum omittantur pri? Laudem mediocrem constituto pri no, eos id animal fabulas, mutat viderer detracto vim ne? Vide reformidans eu eos, no quas error vis, est etiam nullam sensibus cu.
9 |
10 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
11 |
12 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
13 |
14 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
15 |
16 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
17 |
18 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
19 |
20 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
21 |
22 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/Integration/www/3.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test 3
6 |
7 |
8 | Lorem ipsum dolor sit amet, eam viderer molestiae an. No illum dictas singulis pri, vidit simul aliquando sed ex. At eos prima torquatos, agam nobis minimum sea in. Ei mentitum omittantur pri? Laudem mediocrem constituto pri no, eos id animal fabulas, mutat viderer detracto vim ne? Vide reformidans eu eos, no quas error vis, est etiam nullam sensibus cu.
9 |
10 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
11 |
12 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
13 |
14 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
15 |
16 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
17 |
18 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
19 |
20 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
21 |
22 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Strategy/MobileStrategy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache\Strategy;
13 |
14 | use PageCache\SessionHandler;
15 | use PageCache\StrategyInterface;
16 |
17 | /**
18 | *
19 | * MobileStrategy uses "serbanghita/Mobile-Detect" package and provides a different cached version for mobile devices.
20 | * Use enableSession() to enable session support
21 | *
22 | * If you are displaying a different version of your page for mobile devices use:
23 | * $cache->setStrategy(new \PageCache\MobileStrategy() );
24 | *
25 | */
26 | class MobileStrategy implements StrategyInterface
27 | {
28 | /**
29 | * Mobile Detect instance
30 | *
31 | * @var \Mobile_Detect|null
32 | */
33 | private $MobileDetect;
34 |
35 | /**
36 | * MobileStrategy constructor.
37 | * $mobileDetect object can be passed as a parameter. Useful for testing.
38 | *
39 | * @param \Mobile_Detect|null $mobileDetect
40 | */
41 | public function __construct(\Mobile_Detect $mobileDetect = null)
42 | {
43 | $this->MobileDetect = $mobileDetect ?: new \Mobile_Detect;
44 | }
45 |
46 | /**
47 | * Generate cache file name
48 | * Sets a "-mob" ending to cache files for visitors coming from mobile devices (phones but not tablets)
49 | *
50 | * @return string file name
51 | */
52 | public function strategy()
53 | {
54 | $ends = $this->currentMobile() ? '-mob' : '';
55 |
56 | //when session support is enabled add that to file name
57 | $session_str = SessionHandler::process();
58 |
59 | $uri = empty($_SERVER['REQUEST_URI']) ? 'uri' : $_SERVER['REQUEST_URI'];
60 | $query = empty($_SERVER['QUERY_STRING']) ? 'query' : $_SERVER['QUERY_STRING'];
61 |
62 | return md5($uri . $_SERVER['SCRIPT_NAME'] . $query . $session_str) . $ends;
63 | }
64 |
65 | /**
66 | * Whether current page was accessed from a mobile phone
67 | *
68 | * @return bool true for phones, else false
69 | */
70 | private function currentMobile()
71 | {
72 | if (!$this->MobileDetect || !($this->MobileDetect instanceof \Mobile_Detect)) {
73 | return false;
74 | }
75 |
76 | //for phones only, not tablets
77 | //create your own Strategy if needed, and change this functionality
78 | return $this->MobileDetect->isMobile() && !$this->MobileDetect->isTablet();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | PageCache ChangeLog
2 | ===================
3 |
4 | ### WIP
5 |
6 | * Dry Run mode introduced
7 | * More logs added, milliseconds in log times.
8 |
9 | ### 2.0 (2017-06-05)
10 |
11 | * Backwards incompatible refactoring.
12 | * PSR-16 introduced. PageCache now supports distributed file systems (Redis, Memcached, etc).
13 | * New PHP requirements >= 5.6.
14 | * Config file now must return `return [...]`.
15 | * Config `expiration` setting was renamed to `cache_expiration_in_seconds`
16 |
17 | ### 1.3.1 (2017-01-15)
18 |
19 | * HTTP Headers introduced (optional). Thanks to @spotman.
20 | * Clean cache directory with clearCache() (removes all files and directories inside main cache directory).
21 | * [Refactoring] Removed static method from HashDirectory, other improvements.
22 |
23 | ### 1.3.0 (2016-05-23)
24 |
25 | * PSR-2 coding style adopted (php-cs-fixer and phpcs are being used).
26 | * File locking mechanism using flock(). Single write - many reads of the same cache file.
27 | * Cache stampede protection (dog-piling effect) added, file lock and logarithmic random early and late expiration.
28 | * PSR-3 Logger integration (PageCache already comes with a simple logging support).
29 | * Storage support added. Currently FileSystem only.
30 | * PSR-0 support removed from php-cs-fixer (PSR-0 is deprecated)
31 | * vfsStream adopted for mocking virtual file system in PHPUnit tests.
32 | * Tests for PageCache class added.
33 | * Added testing for PHP 7 on Travis.
34 |
35 | ### 1.2.3 (2016-05-10)
36 |
37 | * PHPUnit tests added to tests/, along with needed phpunit.xml settings and testing bootstrap. You can run PHPUnit tests easily on entire library.
38 | * PHPUnit tests developed with full coverage for all classes, except PageCache class.
39 | * Composer autoload-dev now loads tests/
40 | * SessionHandler uses serialize() now.
41 | * MobileStrategy now accepts $mobileDetect parameter. Useful for testing, but not only.
42 | * Travis support added. PageCache successfully passes on PHP 5.5, 5.6 on Travis.
43 |
44 | ### 1.2.2 (2016-05-23)
45 |
46 | * More session support improvements. SessionHandler class added.
47 | * session_exclude_keys config parameter added, along with sessionExclude() method.
48 | * HashDirectory update.
49 | * General improvements.
50 |
51 | ### 1.2.1 (2016-04-22)
52 |
53 | * MobileStrategy bug fixed, session wasn't contained inside md5().
54 |
55 | ### 1.2.0 (2016-04-22)
56 |
57 | * Session support added to PageCache.
58 | * All Strategies were updated to support sessions.
59 | * Composer installation instructions added
60 |
61 | ### 1.1.0 (2016-04-20)
62 |
63 | * clearPageCache() introduced to clear current page cache.
64 | * Added getPageCache() and getFile() methods to PageCache class.
65 | * Default value for min_cache_file_size was reduced to 10 from 1000.
66 | * Examples updates
67 |
68 | ### 1.0.1 (2016-04-19)
69 |
70 | * Improvements.
71 | * Examples, README updates.
72 |
73 | ### 1.0.0 (2016-04-17)
74 |
75 | * Initial release.
--------------------------------------------------------------------------------
/examples/demo-api.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates use of global config.php config file in PageCache.
16 | *
17 | * It's useful to have settings defined in one file, to avoid repeating yourself
18 | *
19 | */
20 |
21 | require_once __DIR__ . '/../vendor/autoload.php';
22 |
23 | use PageCache\PageCache;
24 |
25 | //PageCache configuration in a file
26 | $config_file = __DIR__ . '/config.php';
27 |
28 | //pass config file
29 | $cache = new PageCache($config_file);
30 |
31 | //enable log, by default disabled in config.php
32 | $cache->config()->setEnableLog(true);
33 |
34 | echo 'Cache key, getCurrentKey(): ' . $cache->getCurrentKey() . ' ';
35 | echo '';
36 |
37 | //cache present already?
38 | echo 'isChached(): ';
39 | if ($cache->isCached()) {
40 | echo 'cache exists';
41 | } else {
42 | echo 'cache does not exist';
43 | }
44 | echo '';
45 |
46 | //get cache file contents
47 | echo 'Cache file contents: ';
48 | $str = $cache->getPageCache();
49 | var_dump($str);
50 | echo '';
51 |
52 |
53 | //Clear cache for this page
54 | //
55 | //Uncomment this line below to force cache clear of this page
56 | //
57 | //$cache->clearPageCache();
58 |
59 | //disable log, overrides config.php
60 | //$cache->disableLog();
61 |
62 | //Change log file path, overrides config.php
63 | //$cache->logFilePath(__DIR__.'/log/cache.log');
64 |
65 | //Set cache expiration for this current page, overrides config.php
66 | //$cache->setExpiration(3600);
67 |
68 | //Cache file minimum size, if it's less than this many bytes cache considered invalid.
69 | //This value is important, set to your minimum page size.
70 | //This will ensure that no visitor will see an empty page or some error, in case cache generation fails
71 | //$cache->setMinCacheFileSize(100);
72 |
73 | //Change cache file location. If needed you can have cache for each URL in a seperate location.
74 | //$cache->setPath(__DIR__.'/cache/');
75 |
76 | //Set cache strategy. If needed implemet a new strategy class (see src/Strategy/ for code of built-in strategies)
77 | //$cache->setStrategy( new \PageCache\Strategy\MobileStrategy());
78 |
79 | //start PageCache
80 | $cache->init();
81 |
82 |
83 | ?>
84 |
85 |
86 |
Example #4 - API. Call to some methods of PageCache.
87 |
This is a demo PageCache page that is going to be cached.
88 | Refresh browser to see changes.
89 |
90 |
Demo with conf.php configuration file usage, see source code for this file.
91 |
This is a dynamic PHP date('H:i:s')
92 | call, note that time doesn't change on refresh: .
93 |
94 |
Check examples/cache/ directory to see cached content.
95 | Erase this file to regenerate cache, or it will automatically be regenerated in 10 minutes, as per conf.php
96 |
97 |
98 |
--------------------------------------------------------------------------------
/tests/Storage/FileSystem/FileSystemCacheAdapterTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Tests\Storage\FileSystem;
12 |
13 | use PageCache\Storage\FileSystem\FileSystemCacheAdapter;
14 |
15 | /**
16 | * Class FileSystemCacheAdapterTest
17 | * @package PageCache\Tests\Storage\FileSystem
18 | */
19 | class FileSystemCacheAdapterTest extends \PHPUnit\Framework\TestCase
20 | {
21 |
22 | /**
23 | * @throws \Exception
24 | * @throws \PageCache\PageCacheException
25 | * @throws \PageCache\Storage\CacheAdapterException
26 | * @throws \Psr\SimpleCache\InvalidArgumentException
27 | *
28 | * @dataProvider invalidKeys
29 | *
30 | * @expectedException \Psr\SimpleCache\InvalidArgumentException
31 | */
32 | public function testInvalidCacheKey($key)
33 | {
34 | $fileAdapter = new FileSystemCacheAdapter(__DIR__ . '/../../tmp', LOCK_EX, 0);
35 | $fileAdapter->set($key, '1');
36 | }
37 |
38 | /**
39 | * @throws \Exception
40 | * @throws \PageCache\PageCacheException
41 | * @throws \PageCache\Storage\CacheAdapterException
42 | * @throws \Psr\SimpleCache\InvalidArgumentException
43 | *
44 | * @dataProvider validKeys
45 | *
46 | */
47 | public function testValidCacheKey($key)
48 | {
49 | $fileAdapter = new FileSystemCacheAdapter(__DIR__ . '/../../tmp', LOCK_EX, 0);
50 | $fileAdapter->set($key, '1');
51 | }
52 |
53 | /**
54 | * @throws \Exception
55 | * @throws \PageCache\PageCacheException
56 | * @throws \PageCache\Storage\CacheAdapterException
57 | * @throws \Psr\SimpleCache\InvalidArgumentException
58 | *
59 | * @dataProvider validKeys
60 | *
61 | */
62 | public function testHas($key)
63 | {
64 | $fileAdapter = new FileSystemCacheAdapter(__DIR__ . '/../../tmp', LOCK_EX, 0);
65 | $fileAdapter->set($key, '1');
66 |
67 | $this->assertTrue($fileAdapter->has($key));
68 | }
69 |
70 | /**
71 | * @throws \Exception
72 | * @throws \PageCache\PageCacheException
73 | * @throws \PageCache\Storage\CacheAdapterException
74 | * @throws \Psr\SimpleCache\InvalidArgumentException
75 | *
76 | * @dataProvider validKeys
77 | *
78 | */
79 | public function testGet($key)
80 | {
81 | $fileAdapter = new FileSystemCacheAdapter(__DIR__ . '/../../tmp', LOCK_EX, 0);
82 | $fileAdapter->set($key, 'RandomValue');
83 |
84 | $this->assertSame('RandomValue', $fileAdapter->get($key));
85 | }
86 |
87 | public function invalidKeys()
88 | {
89 | return [
90 | ['**Dasf'],
91 | ['==asdfdasf'],
92 | ['[[['],
93 | ['~~!#$GFDSAS'],
94 | ['']
95 | ];
96 | }
97 |
98 | public function validKeys()
99 | {
100 | return [
101 | ['asdfasdfasdf'],
102 | ['ASDFAFKASLPQWE'],
103 | ['MNDJDSJaadoikekk1230988813'],
104 | ['OASDFHabsdfgajskdf123098689-mob'],
105 | ['234324.FKKAjdld-2341_PIYNx']
106 | ];
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/examples/demo-mobiledetect.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates Mobile_Detect integration into PageCache.
16 | * You can implement your own strategies and pass them into setStrategy().
17 | * Examine code of Strateg/DefaultStrategy() to give you idea how to write one.
18 | *
19 | *
20 | * !!! IMPORTANT: To use this demo you need to composer install require_dev mobiledetect/mobiledetectlib:
21 | * composer require mobiledetect/mobiledetectlib
22 | * or whichever other way you prefer. mobiledetect is being suggested during PageCage install.
23 | *
24 | */
25 |
26 | /**
27 | * Composer autoload, or use any other means
28 | */
29 | require_once __DIR__ . '/../vendor/autoload.php';
30 |
31 | use PageCache\PageCache;
32 | use PageCache\Strategy\MobileStrategy;
33 |
34 | /**
35 | * PageCache setup
36 | */
37 | $config_file = __DIR__ . '/config.php';
38 | $cache = new PageCache($config_file);
39 | $cache->setStrategy(new MobileStrategy());
40 | $cache->config()->setEnableLog(true);
41 | //Enable session support if needed, check demos and README for details
42 | //uncomment for session support
43 | //$cache->config()->setUseSession(true);
44 | $cache->init();
45 |
46 | /**
47 | * Mobile detect helper function for detecting mobile devices.
48 | * Tablets are excluded on purpose here, suit your own needs.
49 | *
50 | * Mobile_detect project URL: http://www.mobiletedect.net
51 | * : https://packagist.org/packages/mobiledetect/mobiledetectlib
52 | *
53 | */
54 | function isMobileDevice()
55 | {
56 | $mobileDetect = new \Mobile_Detect();
57 |
58 | /**
59 | * Check for mobile devices, that are not tables. We want phones only.
60 | * If you need ALL mobile devices use this: if($mobileDetect->isMobile())
61 | *
62 | */
63 | return $mobileDetect->isMobile() && !$mobileDetect->isTablet();
64 | }
65 | ?>
66 |
67 |
68 |
Example #3
69 |
This is a basic MobileStrategy() PageCache page that is going to be cached, uses optional
70 | Mobile_Detect package
71 |
72 | Visit this page with a desktop browser on your computer, and then using a mobile phone.
73 | You will notice 2 files inside cache/ directory, one regular cache file and the other same file but with "-mob"
74 | added to it.
75 |
76 |
82 |
This section will be displayed on mobile phones only
83 |
89 |
This section will be displayed on desktop devices, but not on mobile phones
90 |
91 |
93 |
94 |
This is a dynamic PHP date('H:i:s')
95 | call, note that time doesn't change on refresh: .
96 |
97 |
Check examples/cache/ directory to see cached content.
98 | Erase this file to regenerate cache, or it will automatically be regenerated in 10 minutes, as per conf.php
99 |
100 |
--------------------------------------------------------------------------------
/src/Storage/CacheItem.php:
--------------------------------------------------------------------------------
1 |
6 | * @package PageCache
7 | * @copyright 2017
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | namespace PageCache\Storage;
14 |
15 | use DateTime;
16 |
17 | /**
18 | * Cache Item Storage.
19 | * Contents of the item are stored as a string.
20 | *
21 | * Class CacheItem
22 | * @package PageCache\Storage
23 | */
24 | class CacheItem implements CacheItemInterface
25 | {
26 | /**
27 | * @var string
28 | */
29 | private $key;
30 |
31 | /**
32 | * @var \DateTime
33 | */
34 | private $createdAt;
35 |
36 | /**
37 | * @var DateTime
38 | */
39 | private $lastModified;
40 |
41 | /**
42 | * @var DateTime
43 | */
44 | private $expiresAt;
45 |
46 | /**
47 | * @var string
48 | */
49 | private $eTagString;
50 |
51 | /**
52 | * @var string
53 | */
54 | private $content;
55 |
56 | /**
57 | * CacheItem constructor.
58 | *
59 | * @param string $key
60 | */
61 | public function __construct($key)
62 | {
63 | $this->key = $key;
64 | $this->createdAt = new DateTime();
65 | }
66 |
67 | /**
68 | * @return string
69 | */
70 | public function getKey()
71 | {
72 | return $this->key;
73 | }
74 |
75 | /**
76 | * @return \DateTime
77 | */
78 | public function getCreatedAt()
79 | {
80 | return $this->createdAt;
81 | }
82 |
83 | /**
84 | * @return \DateTime
85 | */
86 | public function getLastModified()
87 | {
88 | return $this->lastModified;
89 | }
90 |
91 | /**
92 | * @param \DateTime $time
93 | *
94 | * @return $this
95 | */
96 | public function setLastModified(\DateTime $time)
97 | {
98 | $this->lastModified = $time;
99 |
100 | return $this;
101 | }
102 |
103 | /**
104 | * @return \DateTime
105 | */
106 | public function getExpiresAt()
107 | {
108 | return $this->expiresAt;
109 | }
110 |
111 | /**
112 | * @param \DateTime $time
113 | *
114 | * @return $this
115 | */
116 | public function setExpiresAt(\DateTime $time)
117 | {
118 | $this->expiresAt = $time;
119 |
120 | return $this;
121 | }
122 |
123 | /**
124 | * @return string
125 | */
126 | public function getETagString()
127 | {
128 | return $this->eTagString;
129 | }
130 |
131 | /**
132 | * @param string $value
133 | *
134 | * @return $this
135 | */
136 | public function setETagString($value)
137 | {
138 | $this->eTagString = (string)$value;
139 |
140 | return $this;
141 | }
142 |
143 | public function __toString()
144 | {
145 | return $this->getContent();
146 | }
147 |
148 | /**
149 | * @return string
150 | */
151 | public function getContent()
152 | {
153 | return $this->content;
154 | }
155 |
156 | /**
157 | * @param string $data
158 | *
159 | * @return $this
160 | */
161 | public function setContent($data)
162 | {
163 | $this->content = (string)$data;
164 |
165 | return $this;
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/SessionHandler.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache;
13 |
14 | /**
15 | * SessionHandler is responsible for caching based on $_SESSION.
16 | * For different session values different cache files created. This behaviour is disabled by default.
17 | */
18 | class SessionHandler
19 | {
20 | /**
21 | * Session support enabled/disabled
22 | * @var bool
23 | */
24 | private static $status = false;
25 |
26 | /**
27 | * Session keys to exclude
28 | *
29 | * @var null|array
30 | */
31 | private static $exclude_keys = null;
32 |
33 | /**
34 | * Serialize session. Exclude $_SESSION[key], if key is defined in excludeKeys()
35 | *
36 | * @return string
37 | */
38 | public static function process()
39 | {
40 | $out = null;
41 |
42 | //session handler enabled
43 | if (self::$status) {
44 | //get session into array
45 | $tmp = isset($_SESSION) ? $_SESSION : [];
46 |
47 | //remove excluded keys if were set, and if session is set
48 | if (!empty(self::$exclude_keys) && isset($_SESSION) && !empty($_SESSION)) {
49 | foreach (self::$exclude_keys as $key) {
50 | if (isset($tmp[$key])) {
51 | unset($tmp[$key]);
52 | }
53 | }
54 | }
55 |
56 | $out = serialize($tmp);
57 | }
58 |
59 | return $out;
60 | }
61 |
62 | /**
63 | * Exclude $_SESSION key(s) from caching strategies.
64 | *
65 | * When to use: i.e. Your application changes $_SESSION['count'] variable, but that does not reflect on the page
66 | * content. Exclude this variable, otherwise PageCache will generate seperate cache files for each
67 | * value of $_SESSION['count] session variable.
68 | *
69 | * @param array $keys $_SESSION keys to exclude from caching strategies
70 | */
71 | public static function excludeKeys(array $keys)
72 | {
73 | self::$exclude_keys = $keys;
74 | }
75 |
76 | /**
77 | * Enable or disable Session support
78 | *
79 | * @param bool $status
80 | */
81 | public static function setStatus($status)
82 | {
83 | if ($status === true || $status === false) {
84 | self::$status = $status;
85 | }
86 | }
87 |
88 | /**
89 | * Get excluded $_SESSION keys
90 | *
91 | * @return array|null
92 | */
93 | public static function getExcludeKeys()
94 | {
95 | return self::$exclude_keys;
96 | }
97 |
98 | /**
99 | * Enable session support. Use sessions when caching page.
100 | * For the same URL session enabled page might be displayed differently, when for example user has logged in.
101 | */
102 | public static function enable()
103 | {
104 | self::$status = true;
105 | }
106 |
107 | /**
108 | * Disable session support
109 | */
110 | public static function disable()
111 | {
112 | self::$status = false;
113 | }
114 |
115 | public static function getStatus()
116 | {
117 | return self::$status;
118 | }
119 |
120 | public static function reset()
121 | {
122 | self::$exclude_keys=null;
123 | self::$status=false;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tests/SessionHandlerTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Tests;
12 |
13 | use PageCache\SessionHandler;
14 |
15 | class SessionHandlerTest extends \PHPUnit\Framework\TestCase
16 | {
17 | public function setUp()
18 | {
19 | SessionHandler::disable();
20 | }
21 |
22 | public function tearDown()
23 | {
24 | SessionHandler::disable();
25 | }
26 |
27 | public function testGetStatus()
28 | {
29 | $this->assertFalse(SessionHandler::getStatus());
30 | }
31 |
32 | /**
33 | * @depends testGetStatus
34 | */
35 | public function testEnableDisableStatus()
36 | {
37 | SessionHandler::enable();
38 | $this->assertTrue(SessionHandler::getStatus());
39 | $this->assertAttributeEquals(true, 'status', SessionHandler::class);
40 |
41 | SessionHandler::disable();
42 | $this->assertFalse(SessionHandler::getStatus());
43 | $this->assertAttributeEquals(false, 'status', SessionHandler::class);
44 |
45 | SessionHandler::setStatus(true);
46 | $this->assertTrue(SessionHandler::getStatus());
47 | $this->assertAttributeEquals(true, 'status', SessionHandler::class);
48 |
49 | SessionHandler::setStatus(false);
50 | $this->assertFalse(SessionHandler::getStatus());
51 | $this->assertAttributeEquals(false, 'status', SessionHandler::class);
52 | }
53 |
54 | public function testExcludeKeys()
55 | {
56 | SessionHandler::excludeKeys(array('count'));
57 | $this->assertEquals(array('count'), SessionHandler::getExcludeKeys());
58 | $this->assertAttributeEquals(array('count'), 'exclude_keys', SessionHandler::class);
59 |
60 | SessionHandler::excludeKeys(array('1', '2', 'another'));
61 | $this->assertCount(3, SessionHandler::getExcludeKeys());
62 | $this->assertAttributeEquals(array('1', '2', 'another'), 'exclude_keys', SessionHandler::class);
63 | }
64 |
65 | /**
66 | * @depends testEnableDisableStatus
67 | * @depends testExcludeKeys
68 | */
69 | public function testProcess()
70 | {
71 | $_SESSION['testing'] = 'somevar';
72 |
73 | SessionHandler::setStatus(false);
74 | $this->assertNull(SessionHandler::process());
75 |
76 | SessionHandler::enable();
77 | $this->assertContains('somevar', SessionHandler::process());
78 | $this->assertEquals(serialize($_SESSION), SessionHandler::process());
79 |
80 | $_SESSION['process'] = 'ignorethis';
81 | $this->assertEquals(serialize($_SESSION), SessionHandler::process());
82 |
83 | SessionHandler::excludeKeys(array('NonExistingSessionVariable'));
84 | SessionHandler::excludeKeys(array('process'));
85 | $process = SessionHandler::process();
86 | $this->assertEquals(array('testing' => 'somevar'), unserialize($process));
87 | }
88 |
89 | /**
90 | * @doesNotPerformAssertions
91 | */
92 | public function testExceptionArray()
93 | {
94 | try {
95 | SessionHandler::excludeKeys('stringvalue is not scalar array');
96 | $this->expectException('PHPUnit_Framework_Error');
97 | } catch (\Throwable $e) {
98 | // echo '~~~~As expected PHP7 throws Throwable.';
99 | } catch (\Exception $e) {
100 | // echo '~~~~As expected PHP5 throws Exception.';
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/Integration/www/4.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test 3
6 |
7 |
8 | Lorem ipsum dolor sit amet, eam viderer molestiae an. No illum dictas singulis pri, vidit simul aliquando sed ex. At eos prima torquatos, agam nobis minimum sea in. Ei mentitum omittantur pri? Laudem mediocrem constituto pri no, eos id animal fabulas, mutat viderer detracto vim ne? Vide reformidans eu eos, no quas error vis, est etiam nullam sensibus cu.
9 |
10 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
11 |
12 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
13 |
14 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
15 |
16 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
17 |
18 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
19 |
20 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
21 |
22 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
23 |
24 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
25 |
26 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
27 |
28 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
29 |
30 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
31 |
32 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
33 |
34 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
35 |
36 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/Storage/CacheItemStorage.php:
--------------------------------------------------------------------------------
1 |
6 | * @package PageCache
7 | * @copyright 2017
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | namespace PageCache\Storage;
14 |
15 | use Psr\SimpleCache\CacheInterface;
16 | use DateTime;
17 |
18 | /**
19 | * Class CacheItemStorage
20 | * Wrapper for PSR-16 SimpleCache adapter
21 | *
22 | * @package PageCache
23 | */
24 | class CacheItemStorage
25 | {
26 | /**
27 | * @var \Psr\SimpleCache\CacheInterface
28 | */
29 | protected $adapter;
30 |
31 | /**
32 | * @var int
33 | */
34 | protected $cacheExpiresIn;
35 |
36 | /**
37 | * This a wrapper for PSR-16 adapter
38 | *
39 | * @param \Psr\SimpleCache\CacheInterface $adapter
40 | * @param int $cacheExpiresIn
41 | */
42 | public function __construct(CacheInterface $adapter, $cacheExpiresIn)
43 | {
44 | $this->adapter = $adapter;
45 | $this->cacheExpiresIn = $cacheExpiresIn;
46 | }
47 |
48 | /**
49 | * @param string $key
50 | *
51 | * @return \PageCache\Storage\CacheItemInterface|null
52 | * @throws \Psr\SimpleCache\InvalidArgumentException
53 | */
54 | public function get($key)
55 | {
56 | /** @var \PageCache\Storage\CacheItemInterface $item */
57 | $item = $this->adapter->get($key);
58 |
59 | if (!$item) {
60 | return null;
61 | }
62 |
63 | $this->randomizeExpirationTime($item);
64 |
65 | // Cache expired?
66 | if ($this->isExpired($item)) {
67 | return null;
68 | }
69 |
70 | return $item;
71 | }
72 |
73 | /**
74 | * @param CacheItemInterface $item
75 | * @throws \Psr\SimpleCache\InvalidArgumentException
76 | */
77 | public function set(CacheItemInterface $item)
78 | {
79 | // Add ttl for buggy adapters (double time for correct cache stampede preventing algorithm)
80 | $this->adapter->set($item->getKey(), $item, $this->cacheExpiresIn * 2);
81 | }
82 |
83 | /**
84 | * @param CacheItemInterface $item
85 | * @throws \Psr\SimpleCache\InvalidArgumentException
86 | */
87 | public function delete(CacheItemInterface $item)
88 | {
89 | $this->adapter->delete($item->getKey());
90 | }
91 |
92 | /**
93 | * Wipes clean the entire cache.
94 | */
95 | public function clear()
96 | {
97 | $this->adapter->clear();
98 | }
99 |
100 | /**
101 | * @param \PageCache\Storage\CacheItemInterface $item
102 | * @param \DateTime|null $time
103 | *
104 | * @return bool
105 | */
106 | private function isExpired(CacheItemInterface $item, DateTime $time = null)
107 | {
108 | $time = $time ?: new DateTime();
109 | return ($time > $item->getExpiresAt());
110 | }
111 |
112 | /**
113 | * Calculate and returns item's expiration time.
114 | *
115 | * Cache expiration is cacheExpire seconds +/- a random value of seconds, from -6 to 6.
116 | *
117 | * So although expiration is set for example 200 seconds, it is not guaranteed that it will expire in exactly
118 | * that many seconds. It could expire at 200 seconds, but also could expire in 206 seconds, or 194 seconds, or
119 | * anywhere in between 206 and 194 seconds. This is done to better deal with cache stampede, and improve cache
120 | * hit rate.
121 | *
122 | * @param \PageCache\Storage\CacheItemInterface $item
123 | */
124 | private function randomizeExpirationTime(CacheItemInterface $item)
125 | {
126 | // Get expires time (if previously set by headers forwarding)
127 | $expiresAtTimestamp = $item->getExpiresAt() ? $item->getExpiresAt()->getTimestamp() : null;
128 |
129 | // Generate expires time from creation date and default interval
130 | $expiresAtTimestamp = $expiresAtTimestamp ?: ($item->getCreatedAt()->getTimestamp() + $this->cacheExpiresIn);
131 |
132 | // Slightly random offset
133 | $offset = log10(mt_rand(10, 1000)) * mt_rand(-2, 2);
134 | $item->setExpiresAt((new DateTime())->setTimestamp($expiresAtTimestamp + $offset));
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/examples/config.php:
--------------------------------------------------------------------------------
1 |
6 | * @copyright 2016
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | /**
13 | *
14 | * Example configuration file
15 | *
16 | * Sample config array with all possible values.
17 | * Copy this configuration file to your local location and edit values that you need.
18 | *
19 | * You do not need to copy this config file and use it, you could set up all parameters directly inside code.
20 | * If you have caching enabled on several pages, and do not want to repeat cache settings, then config file is for you.
21 | *
22 | * Options `file_lock`, `cache_path` and `min_cache_file_size` are ignored if custom PSR-16 SimpleCache adapter is used.
23 | *
24 | * NOTE: Parameters defined here in $config array are used by all pages using PageCache within you application.
25 | * You can override any of these settings in your code.
26 | *
27 | * Feel free to comment out any settings you don't need.
28 | *
29 | */
30 | return [
31 |
32 | /**
33 | * Minimum cache file size.
34 | * Generated cache files less than this many bytes, are considered invalid and are regenerated
35 | * Default 10
36 | */
37 | 'min_cache_file_size' => 10,
38 |
39 | /**
40 | * Set true to enable logging, not recommended for production use, only for debugging
41 | * Default: false
42 | *
43 | * Effects both internal logger, and any external PSR-3 logger (if any) activated via setLogger() method
44 | */
45 | 'enable_log' => false,
46 |
47 | /**
48 | * Internal log file location, enable_log must be true for logging to work
49 | * When external logger is provided via setLogger(), internal logging is disabled.
50 | */
51 | 'log_file_path' => __DIR__ . '/log/cache.log',
52 |
53 | /**
54 | * Current page's cache expiration in seconds.
55 | * Default: 20 minutes, 1200 seconds.
56 | */
57 | 'cache_expiration_in_seconds' => 1200,
58 |
59 | /**
60 | * Cache directory location (mind the trailing slash "/").
61 | * Cache files are saved here.
62 | */
63 | 'cache_path' => __DIR__ . '/cache/',
64 |
65 | /**
66 | * Use session support, if you have a login area or similar.
67 | * When page content changes according to some Session value, although URL remains the same.
68 | * Disabled by default.
69 | */
70 | 'use_session' => false,
71 |
72 | /**
73 | * Exclude $_SESSION key(s) from caching strategies. Pass session name as keys to the array.
74 | *
75 | * When to use: Your application changes $_SESSION['count'] variable, but that doesn't reflect on the page
76 | * content. Exclude this variable, otherwise PageCache will generate seperate cache files for each
77 | * value of $_SESSION['count] session variable.
78 | * Example: 'session_exclude_keys'=>array('count')
79 | */
80 | 'session_exclude_keys' => [],
81 |
82 | /**
83 | *
84 | * Locking mechanism to use when writing cache files. Default is LOCK_EX | LOCK_NB, which locks for
85 | * exclusive write while being non-blocking. Set whatever you want.
86 | * Read for details (http://php.net/manual/en/function.flock.php)
87 | *
88 | * Set file_lock = false to disable file locking.
89 | */
90 | 'file_lock' => LOCK_EX | LOCK_NB,
91 |
92 | /**
93 | * Send appropriate HTTP cache related headers in response or not.
94 | * When true headers are sent, when false not being sent.
95 | *
96 | * When set to true:
97 | * First call to your URL results in HTTP Response code 200.
98 | * Consequent calls, until page expiration, will result in 304 Not Modified.
99 | * When 304 is being returned, no content is retrieved from the server.
100 | * This makes your application load super fast - cached content comes from web browser.
101 | */
102 | 'send_headers' => false,
103 |
104 | /**
105 | * Use Last-Modified and ETag values generated by application or not.
106 | * Values will be fetched from the current response.
107 | */
108 | 'forward_headers' => false,
109 |
110 | /**
111 | * Dry Run Mode.
112 | * When enabled no cache headers will be sent, no cache output will be sent.
113 | * Cache items will be saved.
114 | * Use this to test system.
115 | */
116 | 'dry_run_mode' =>false
117 | ];
118 |
--------------------------------------------------------------------------------
/examples/demo-session-support.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates basic caching functionality of PageCache.
16 | *
17 | * Two versions of this page will be saved.
18 | * If your cache directory is cache, then look for cache/54/3/668.. and cache/97/52/ead.. files.
19 | *
20 | * Despite that URL doesn't change (PageCache DefaultStrategy doesn't support $_POST()), PageCache is able to
21 | * save two different versions of the page based on $_SESSION data.
22 | *
23 | * enableSession() is useful if you are going to cache a dynamic PHP application where you use $_SESSION
24 | * for various things, like user login and etc.
25 | *
26 | *
27 | * NOTE: If you want to cache only URLs before user login or other session manipulations, you could put
28 | * PageCache call inside if(!isset($_SESSION[..])) { //run PageCache only on pages without certain Session variable }
29 | *
30 | */
31 |
32 | require_once __DIR__ . '/../vendor/autoload.php';
33 |
34 | /**
35 | * session is started only when "Enable session" button is pressed
36 | */
37 |
38 | if (isset($_POST['withsessions']) && $_POST['withsessions'] == '1') {
39 | session_start();
40 |
41 | //sample session data
42 | $_SESSION['demo_session'] = array('my val1', 'my val2');
43 | $_SESSION['user'] = 12;
44 | $_SESSION['login'] = true;
45 | }
46 |
47 | $cache = new PageCache\PageCache();
48 |
49 | //cache path
50 | $cache->config()->setCachePath(__DIR__ . '/cache/');
51 |
52 | //Disable line below and you will see only 1 cache file generated,
53 | // no differentiation between session and without session calls
54 | //
55 | //use session support in cache
56 | //
57 | $cache->config()->setUseSession(true);
58 |
59 | //do disable session cache uncomment this line, or comment line above and see
60 | //$cache->config()->setUseSession(false);
61 |
62 | //enable log
63 | //$cache->config()->setEnableLog(true);
64 | //$cache->config()->setLogFilePath(__DIR__.'/log/cache.log');
65 |
66 |
67 | //start cache;
68 | $cache->init();
69 |
70 | echo 'Everything below is cached, including this line';
71 |
72 | ?>
73 |
74 |
75 |
94 |
95 |
96 |
Demo using Session Support
97 | Click on the links below to see how PageCache works with sessions. Although page URL doesn't change, PageCache is able
98 | to cache 2 different version of this page based on Session variables.
99 |
This is a Session demo PageCache page that is going to be cached.
128 |
Default cache expiration time for this page is 20 minutes. You can change this value in your conf.php
129 | and passing its file path to PageCache constructor, or by calling setExpiration() method.
130 |
This is a dynamic PHP date('H:i:s')
131 | call, note that time doesn't change on refresh: .
132 |
133 |
Check examples/cache/ directory to see cached content.
134 | Erase this file to regenerate cache, or it will automatically be regenerated in 20 minutes.
135 |
136 |
--------------------------------------------------------------------------------
/examples/demo-session-exclude-keys.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2016
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | /**
14 | *
15 | * This demo demonstrates caching functionality of PageCache.
16 | *
17 | * We exclude certain session keys from caching logic
18 | *
19 | */
20 |
21 | require_once __DIR__ . '/../vendor/autoload.php';
22 |
23 | session_start();
24 | session_destroy();
25 |
26 | if (isset($_POST['withsessions'])) {
27 | if ($_POST['withsessions'] == '1') {
28 | $_SESSION['excl'] = 1;
29 | $_SESSION['PageCache'] = 'PHP full page caching';
30 | } elseif ($_POST['withsessions'] == '2') {
31 | $_SESSION['excl'] = 555;
32 | $_SESSION['PageCache'] = 'PHP full page caching';
33 | }
34 | }
35 |
36 | echo '';
42 |
43 | $cache = new PageCache\PageCache(__DIR__ . '/config.php');
44 | $cache->config()->setUseSession(true)
45 | // Exclude $_SESSION['exc'] from cache strategy.
46 | // Comment line below, and cached version for each 'excl' session variable
47 | // will be saved in a different cache file
48 | ->setSessionExcludeKeys(array('excl'));
49 |
50 | //init
51 | $cache->init();
52 |
53 |
54 | ?>
55 |
56 |
57 |
76 |
77 |
78 |
Demo using Session Support + Exclude Session Keys
79 | Click on the links below to see how PageCache works with session exclude. Although page URL doesn't change, PageCache is
80 | able to cache 2 different version of this page based on Session variables.
81 | Session key 'exc' changes, but PageCache ignores it and produces only a single cached page. If you don't call
82 | excludeKeys(), 2 versions of the page will be generated.
83 | Whichever button you press first, it will be cached, while other won't - because 'excl' parameter doesn't effect
84 | cache. But you will still get 2 caches of this page because $_SESSION['PageCache'] is being set, and this change is
85 | being recorded.
86 |
This is a Session Exclude demo PageCache page that is going to be cached.
120 |
Default cache expiration time for this page is 20 minutes. You can change this value in your conf.php
121 | and passing its file path to PageCache constructor, or by calling setExpiration() method.
122 |
This is a dynamic PHP date('H:i:s')
123 | call, note that time doesn't change on refresh: .
124 |
125 |
Check examples/cache/ directory to see cached content.
126 | Erase this file to regenerate cache, or it will automatically be regenerated in 20 minutes.
127 |
128 |
129 |
--------------------------------------------------------------------------------
/tests/Integration/www/index.php:
--------------------------------------------------------------------------------
1 | config()
55 | // 60 seconds is enough for testing both concurrency and single requests
56 | ->setCacheExpirationInSeconds(60)
57 | ->setLogFilePath($logsDirectory.'/page-cache.log')
58 | ->setEnableLog(true)
59 | ->setUseSession(true)
60 | ->setForwardHeaders(true)
61 | ->setSendHeaders(true);
62 |
63 | setCacheImplementation($pc, $cacheName, $cacheDirectory);
64 | setLoggerImplementation($pc, $loggerName, $logsDirectory);
65 |
66 | // Clear cache if needed (trying to create race conditions on empty cache)
67 | if ($clearCache) {
68 | $pc->clearAllCache();
69 | }
70 |
71 | // Initialize
72 | $pc->init();
73 |
74 | // Send headers (Last-Modified, Expires)
75 | $lastModifiedTimestamp = filemtime($contentFile);
76 | $expiresInTimestamp = time() + $pc->config()->getCacheExpirationInSeconds();
77 |
78 | header('Last-Modified: '.gmdate("D, d M Y H:i:s \G\M\T", $lastModifiedTimestamp));
79 | header('Expires: '.gmdate("D, d M Y H:i:s \G\M\T", $expiresInTimestamp));
80 |
81 | // Send content
82 | include $contentFile;
83 |
84 |
85 | function setCacheImplementation(PageCache $pc, $name, $cacheDirectory)
86 | {
87 | if (!$name) {
88 | throw new RuntimeException('No cache implementation set');
89 | }
90 |
91 | switch ($name) {
92 | case IntegrationWebServerTest::CACHE_TYPE_INTERNAL:
93 | // Internal cache is used by default, needs only directory to set
94 | $pc->config()->setCachePath($cacheDirectory.DIRECTORY_SEPARATOR);
95 | break;
96 |
97 | case IntegrationWebServerTest::CACHE_TYPE_SYMFONY_FILESYSTEM:
98 | // Using basic symfony/cache
99 | $ttl = $pc->config()->getCacheExpirationInSeconds() * 2;
100 | $cacheAdapter = new FilesystemCache('symfony-cache', $ttl, $cacheDirectory);
101 | $pc->setCacheAdapter($cacheAdapter);
102 | break;
103 |
104 | default:
105 | throw new RuntimeException('Unknown cache implementation key: '.$name);
106 | }
107 | }
108 |
109 | function setLoggerImplementation(PageCache $pc, $name, $logsDirectory)
110 | {
111 | if (!$name) {
112 | throw new RuntimeException('No logger implementation set');
113 | }
114 |
115 | switch ($name) {
116 | case IntegrationWebServerTest::LOGGER_INTERNAL:
117 | // Internal logger is used by default, needs only cache file to set
118 | $pc->config()->setLogFilePath($logsDirectory.DIRECTORY_SEPARATOR.'page-cache.internal.log');
119 | break;
120 |
121 | case IntegrationWebServerTest::LOGGER_MONOLOG:
122 | $logger = new Logger('default');
123 | $logger->pushHandler(new StreamHandler($logsDirectory.DIRECTORY_SEPARATOR.'page-cache.monolog.log'));
124 |
125 | $pc->setLogger($logger);
126 | break;
127 |
128 | default:
129 | throw new RuntimeException('Unknown logger implementation key: '.$name);
130 | }
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/src/Storage/FileSystem/FileSystem.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Storage\FileSystem;
12 |
13 | use PageCache\PageCacheException;
14 |
15 | /**
16 | *
17 | * File system storage for cache, page cache is saved into file.
18 | *
19 | * Class FileSystem
20 | * @package PageCache\Storage
21 | *
22 | */
23 | class FileSystem
24 | {
25 | /**
26 | * Content to be written into a file
27 | *
28 | * @var string
29 | */
30 | private $content = null;
31 |
32 | /**
33 | * File lock to be used when writing. Support default PHP flock() parameters.
34 | *
35 | * @var int|null
36 | */
37 | private $file_lock = null;
38 |
39 | /**
40 | * File path where to write
41 | *
42 | * @var string
43 | */
44 | private $filepath = null;
45 |
46 | /**
47 | * writeAttempt successful
48 | */
49 | const OK = 1;
50 |
51 | /**
52 | * writeAttempt parameters error
53 | */
54 | const ERROR = 2;
55 |
56 | /**
57 | * writeAttempt fopen() error
58 | */
59 | const ERROR_OPEN = 3;
60 |
61 | /**
62 | * writeAttempt fwrite() error
63 | */
64 | const ERROR_WRITE = 4;
65 |
66 | /**
67 | * writeAttempt failed to aquire lock on the file.
68 | */
69 | const ERROR_LOCK = 5;
70 |
71 | /**
72 | * FileSystem constructor.
73 | * @param string $content page content to be written into file
74 | */
75 | public function __construct($content)
76 | {
77 | $this->content = $content;
78 | }
79 |
80 | /**
81 | * Attempt to write into the file. If lock is not acquired, file is not written.
82 | *
83 | * @return bool
84 | * @throws \Exception
85 | */
86 | public function writeAttempt()
87 | {
88 | $result = self::OK;
89 |
90 | if (empty($this->filepath)) {
91 | return self::ERROR;
92 | }
93 |
94 | /**
95 | * Open the file for writing only. If the file does not exist, it is created.
96 | * If it exists, it is not truncated (as opposed to 'w'). File pointer is moved to beginning.
97 | *
98 | * "c" is needed instead of "w", because if lock is not acquired, old version of the file is returned.
99 | * If "w" option is used, file is truncated no matter what, and empty file is returned.
100 | */
101 | $fp = fopen($this->filepath, 'c');
102 |
103 | if ($fp === false) {
104 | return self::ERROR_OPEN;
105 | }
106 |
107 | /**
108 | * File locking disabled?
109 | */
110 | if (empty($this->file_lock)) {
111 | ftruncate($fp, 0);
112 | if (fwrite($fp, $this->content) === false) {
113 | $result = self::ERROR_WRITE;
114 | }
115 | fclose($fp);
116 |
117 | return $result;
118 | }
119 |
120 | /**
121 | * File locking is enabled
122 | *
123 | * Try to acquire File Write Lock. If lock is not acquired read file and return old contents.
124 | *
125 | * Recommended is: LOCK_EX | LOCK_NB.
126 | * LOCK_EX to acquire an exclusive lock (writer).
127 | * LOCK_NB - prevents flock() from blocking while locking, so that others could still read
128 | * (but not write) while lock is active
129 | */
130 | if (flock($fp, $this->file_lock)) {
131 | /**
132 | * since "c" was used with fopen, file is not truncated. Truncate manually.
133 | */
134 | ftruncate($fp, 0);
135 |
136 | //write content
137 | if (fwrite($fp, $this->content) === false) {
138 | $result = self::ERROR_WRITE;
139 | }
140 |
141 | //release lock
142 | flock($fp, LOCK_UN);
143 | } else {
144 | /**
145 | * Lock was not granted.
146 | */
147 |
148 | $result = self::ERROR_LOCK;
149 | }
150 |
151 | fclose($fp);
152 |
153 | return $result;
154 | }
155 |
156 | /**
157 | * Set file locking logic
158 | *
159 | * @param false|int $file_lock PHP file lock constant or false for disabling
160 | * @throws \Exception
161 | */
162 | public function setFileLock($file_lock)
163 | {
164 | if (empty($file_lock) && $file_lock !== false) {
165 | throw new PageCacheException(__CLASS__ . ' file lock can not be empty. To disable set to boolean false.');
166 | }
167 | $this->file_lock = $file_lock;
168 | }
169 |
170 | /**
171 | * Get file lock value
172 | *
173 | * @return int|null
174 | */
175 | public function getFileLock()
176 | {
177 | return $this->file_lock;
178 | }
179 |
180 | /**
181 | * Get filepath of file to be written
182 | *
183 | * @return null|string
184 | */
185 | public function getFilepath()
186 | {
187 | return $this->filepath;
188 | }
189 |
190 | /**
191 | * Set filepath of the cache file.
192 | *
193 | * @param string $filepath cache file path
194 | * @throws \Exception
195 | */
196 | public function setFilePath($filepath)
197 | {
198 | if (!isset($filepath) || empty($filepath)) {
199 | throw new PageCacheException(__CLASS__ . ' file path not set or empty');
200 | }
201 |
202 | $this->filepath = $filepath;
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/tests/ConfigTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Tests;
12 |
13 | use PageCache\Config;
14 | use PageCache\PageCache;
15 | use PageCache\PageCacheException;
16 |
17 | class ConfigTest extends \PHPUnit\Framework\TestCase
18 | {
19 | /**
20 | * @var Config
21 | */
22 | private $config;
23 |
24 | public function setUp()
25 | {
26 | date_default_timezone_set('UTC');
27 | $this->config = new Config();
28 | }
29 |
30 | public function testPageCacheIntegrationWithSetPath()
31 | {
32 | $pc = new PageCache();
33 | //initially cachePath is null
34 | $this->assertSame(null, $this->config->getCachePath());
35 | $this->assertNull($this->config->getCachePath());
36 | $dir = __DIR__.'/';
37 | $this->config->setCachePath($dir);
38 | $this->assertSame($this->config->getCachePath(), $dir);
39 | $this->config->setCachePath($dir);
40 | $this->assertSame($dir, $this->config->getCachePath());
41 | }
42 |
43 | /**
44 | * @expectedException \Exception
45 | */
46 | public function testSetPath2()
47 | {
48 | $this->config->setCachePath('nonexistant_dir');
49 | }
50 |
51 | public function testSetGetExpiration()
52 | {
53 | $this->config->setCacheExpirationInSeconds(10);
54 | $this->assertSame(10, $this->config->getCacheExpirationInSeconds());
55 | $this->assertSame(10, $this->config->getCacheExpirationInSeconds());
56 | }
57 |
58 | public function testSetExpirationException()
59 | {
60 | $this->expectException(PageCacheException::class);
61 | $this->config->setCacheExpirationInSeconds(-1);
62 | }
63 |
64 | public function testSetEnableLogIsEnableLog()
65 | {
66 | $this->config->setEnableLog(true);
67 | $this->assertAttributeSame(true, 'enableLog', $this->config);
68 | $this->assertTrue($this->config->isEnableLog());
69 |
70 | $this->config->setEnableLog(false);
71 | $this->assertAttributeSame(false, 'enableLog', $this->config);
72 | $this->assertFalse($this->config->isEnableLog());
73 | }
74 |
75 |
76 | public function testSetGetMinCacheFileSize()
77 | {
78 | $this->config->setMinCacheFileSize(0);
79 | $this->assertAttributeSame(0, 'minCacheFileSize', $this->config);
80 | $this->assertSame(0, $this->config->getMinCacheFileSize());
81 |
82 | $this->config->setMinCacheFileSize(10000);
83 | $this->assertAttributeSame(10000, 'minCacheFileSize', $this->config);
84 | $this->assertSame(10000, $this->config->getMinCacheFileSize());
85 | }
86 |
87 | public function testSetUseSessionIsUseSession()
88 | {
89 | //initially useSession is false
90 | $this->assertAttributeSame(false, 'useSession', $this->config);
91 | $this->config->setUseSession(true);
92 | $this->assertAttributeSame(true, 'useSession', $this->config);
93 | $this->assertTrue($this->config->isUseSession());
94 | }
95 |
96 | public function testSetGetSessionExclude()
97 | {
98 | $this->assertAttributeSame([], 'sessionExcludeKeys', $this->config);
99 | $this->config->setSessionExcludeKeys([1, 2, 3]);
100 | $this->assertAttributeSame([1,2,3], 'sessionExcludeKeys', $this->config);
101 | $this->assertSame([1,2,3], $this->config->getSessionExcludeKeys());
102 | }
103 |
104 | public function testSetFileLock()
105 | {
106 | $this->config->setFileLock(LOCK_EX);
107 | $this->assertAttributeEquals(LOCK_EX, 'fileLock', $this->config);
108 |
109 | $this->config->setFileLock(LOCK_EX | LOCK_NB);
110 | $this->assertAttributeEquals(LOCK_EX | LOCK_NB, 'fileLock', $this->config);
111 | }
112 |
113 | public function testGetFileLock()
114 | {
115 | $this->assertSame(LOCK_EX | LOCK_NB, $this->config->getFileLock());
116 | $this->config->setFileLock(LOCK_EX);
117 | $this->assertSame(LOCK_EX, $this->config->getFileLock());
118 | }
119 |
120 | public function testSetGetCacheExpirationInSeconds()
121 | {
122 | $this->assertSame(1200, $this->config->getCacheExpirationInSeconds());
123 | $this->config->setCacheExpirationInSeconds(20);
124 | $this->assertSame(20, $this->config->getCacheExpirationInSeconds());
125 | }
126 |
127 | public function testGetPath()
128 | {
129 | $this->assertNull($this->config->getCachePath());
130 | $dir = __DIR__.'/';
131 | $this->config->setCachePath($dir);
132 | $this->assertSame($dir, $this->config->getCachePath());
133 | }
134 |
135 | public function testGetLogFilePath()
136 | {
137 | $path = 'somepath/to/file';
138 | $this->expectException(PageCacheException::class);
139 | $this->config->setLogFilePath($path);
140 | }
141 |
142 | public function testGetMinCacheFileSize()
143 | {
144 | $this->assertAttributeSame(10, 'minCacheFileSize', $this->config);
145 | $this->config->setMinCacheFileSize(10240);
146 | $this->assertSame(10240, $this->config->getMinCacheFileSize());
147 | }
148 |
149 | public function testLoadConfigurationFile()
150 | {
151 | $config = new Config(__DIR__.'/config_test.php');
152 |
153 | $this->assertAttributeSame(1, 'minCacheFileSize', $config);
154 | $this->assertAttributeSame(false, 'enableLog', $config);
155 | $this->assertAttributeSame(600, 'cacheExpirationInSeconds', $config);
156 | $this->assertAttributeContains('/tmp/cache/', 'cachePath', $config);
157 | $this->assertAttributeContains('/tmp', 'logFilePath', $config);
158 | $this->assertSame(false, $config->isUseSession());
159 | $this->assertSame([], $config->getSessionExcludeKeys());
160 | $this->assertAttributeSame(LOCK_EX | LOCK_NB, 'fileLock', $config);
161 | $this->assertAttributeSame(false, 'forwardHeaders', $config);
162 | }
163 |
164 | /**
165 | * Config expiration value is negative -> throws exception
166 | */
167 | public function testWrongConfigFile()
168 | {
169 | $this->expectException(PageCacheException::class);
170 | new Config(__DIR__.'/config_wrong_test.php');
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/tests/Storage/FileSystem/HashDirectoryTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Tests\Storage\FileSystem;
12 |
13 | use PageCache\Storage\FileSystem\HashDirectory;
14 | use org\bovigo\vfs\vfsStream;
15 |
16 | /**
17 | * HasDirectory creates directories based on cache filename to be used in storage
18 | * Virtual directory structure is used in testing
19 | *
20 | * Class HashDirectoryTest
21 | * @package PageCache\Tests
22 | */
23 | class HashDirectoryTest extends \PHPUnit\Framework\TestCase
24 | {
25 |
26 | private $dir;
27 | private $filename;
28 |
29 | /** @var HashDirectory */
30 | private $hd;
31 |
32 | public function setUp()
33 | {
34 | //setup virtual dir
35 | vfsStream::setup('tmpdir');
36 | $this->dir = vfsStream::url('tmpdir').'/';
37 |
38 | //dummy file name for testing
39 | $this->filename = '18a3938de0087a87d3530084cd46edf4';
40 | $this->hd = new HashDirectory($this->dir);
41 | $this->hd->setFile($this->filename);
42 | }
43 |
44 | public function tearDown()
45 | {
46 | unset($this->hd);
47 | }
48 |
49 | public function testGetHash()
50 | {
51 | $val1 = ord('8'); //56
52 | $val2 = ord('3'); //51
53 |
54 | //normalize to 99
55 | $val1 = $val1 % 99; //56
56 | $val2 = $val2 % 99; //51
57 |
58 | $returned = $val1 . '/' . $val2 . '/';
59 |
60 | $this->assertEquals($returned, $this->hd->createSubdirectoriesForFile());
61 | $this->assertFileExists($this->dir . '56/51');
62 | $this->assertEquals($returned, $this->hd->getLocation($this->filename));
63 |
64 | //new object
65 | $newFilename = '93f0938de0087a87d3530084cd46edf4';
66 | $newHd = new HashDirectory($this->dir);
67 | $newHd->setFile($newFilename);
68 |
69 | $this->assertFileNotExists($this->dir . '51/48');
70 | $this->assertEquals('51/48/', $newHd->createSubdirectoriesForFile());
71 | $this->assertAttributeEquals('93f0938de0087a87d3530084cd46edf4', 'file', $newHd);
72 | $this->assertFileExists($this->dir . '51/48');
73 | }
74 |
75 | public function testGetLocation()
76 | {
77 | $this->assertEquals('56/51/', $this->hd->getLocation($this->filename));
78 | }
79 |
80 | public function testGetLocationEmptyFilename()
81 | {
82 | $this->assertNull($this->hd->getLocation(''));
83 | }
84 |
85 | public function testCreateSubDirsWithExistingDirectory()
86 | {
87 | //lets create first dir ->56, and leave 51 uncreated
88 | mkdir($this->dir.'56');
89 | $this->assertNotEmpty($this->hd->createSubdirectoriesForFile());
90 | }
91 |
92 | /**
93 | * @expectedException \Exception
94 | */
95 | public function testCreateSubDirsWithException()
96 | {
97 | //make cache directory non writable, this will prevent from them being created
98 | chmod($this->dir, '000');
99 | $this->hd->createSubdirectoriesForFile();
100 | }
101 |
102 | /**
103 | * @expectedException \Exception
104 | */
105 | public function testConstructorException()
106 | {
107 | new HashDirectory('false directory');
108 | }
109 |
110 | public function testClearDirectory()
111 | {
112 | $dirContent = [
113 | '25' => [
114 | '59' => [
115 | 'cacheFile' => 'file content goes here'
116 | ],
117 | '14' => []
118 | ],
119 | 'Core' => [
120 | 'AbstractFactory' => [
121 | 'test.php' => 'some text content',
122 | 'other.php' => 'Some more text content',
123 | 'Invalid.csv' => 'Something else',
124 | ],
125 | 'AnEmptyFolder' => [],
126 | 'badlocation.php' => 'some bad content',
127 | ]
128 | ];
129 |
130 | vfsStream::create($dirContent);
131 |
132 | $this->assertFileExists($this->dir.'25/');
133 | $this->assertFileExists($this->dir . '25/59/cacheFile');
134 | $this->assertTrue(is_dir($this->dir . '25/14'));
135 | $this->assertFileExists($this->dir . '25/14');
136 | $this->assertFileExists($this->dir . 'Core/AbstractFactory/test.php');
137 |
138 | //remove directory contents
139 | $this->hd->clearDirectory($this->dir . '25');
140 | $this->assertFileNotExists($this->dir . '25/59/cacheFile');
141 | $this->assertFileNotExists($this->dir . '25/14');
142 | $this->assertFileExists($this->dir . 'Core/AbstractFactory/test.php');
143 |
144 | //remove empty directory contents, make sure root directory is there
145 | $this->hd->clearDirectory($this->dir . '25');
146 | $this->assertFileExists($this->dir.'25');
147 |
148 | $this->assertFileExists($this->dir . 'Core/AbstractFactory/test.php');
149 | $this->hd->clearDirectory($this->dir . 'Core/AbstractFactory/');
150 | $this->assertFileNotExists($this->dir . 'Core/AbstractFactory/test.php');
151 | }
152 |
153 | public function testClearDirectoryRoot()
154 | {
155 | $dirContent = [
156 | '25' => [
157 | '59' => [
158 | 'cacheFile' => 'file content goes here'
159 | ],
160 | '14' => []
161 | ],
162 | 'Core' => [
163 | 'AbstractFactory' => [
164 | 'test.php' => 'some text content',
165 | 'other.php' => 'Some more text content',
166 | 'Invalid.csv' => 'Something else',
167 | ],
168 | 'AnEmptyFolder' => [],
169 | 'badlocation.php' => 'some bad content',
170 | ]
171 | ];
172 |
173 | vfsStream::create($dirContent);
174 |
175 | $this->assertFileExists($this->dir . '25');
176 | $this->assertFileExists($this->dir . 'Core');
177 |
178 | //Delete starting from root directory
179 | $this->hd->clearDirectory($this->dir);
180 | $this->assertFileNotExists($this->dir . '25');
181 | $this->assertFileNotExists($this->dir . '25/59/cacheFile');
182 | $this->assertFileNotExists($this->dir . 'Core');
183 | $this->assertFileNotExists($this->dir . 'Core/AnEmptyFolder');
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/Storage/FileSystem/HashDirectory.php:
--------------------------------------------------------------------------------
1 |
6 | * @copyright 2016
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache\Storage\FileSystem;
13 |
14 | use PageCache\PageCacheException;
15 |
16 | /**
17 | * Class HashDirectory.
18 | * HashDirectory creates subdirectories where cache files are stored, based on cache file name.
19 | *
20 | * @package PageCache
21 | */
22 | class HashDirectory
23 | {
24 | /**
25 | * Filename
26 | *
27 | * @var string|null
28 | */
29 | private $file;
30 |
31 | /**
32 | * Directory where filename will be stored.
33 | * Subdirectories are going to be created inside this directory, if necessary.
34 | *
35 | * @var string|null
36 | */
37 | private $dir;
38 |
39 | /**
40 | * Filesystem permissions in octal form
41 | * Server umask must be configured properly to prevent data leakage
42 | *
43 | * @var int
44 | */
45 | private $directoryPermissions = 0777;
46 |
47 | /**
48 | * Subdirectories for storing file based on hash
49 | *
50 | * @var string
51 | */
52 | private $hashSubDirectories;
53 |
54 | /**
55 | * HashDirectory constructor.
56 | *
57 | * @param string|null $dir
58 | *
59 | * @throws \PageCache\PageCacheException
60 | */
61 | public function __construct($dir = null)
62 | {
63 | $this->setDir($dir);
64 | }
65 |
66 | /**
67 | * Set directory
68 | *
69 | * @param string|null $dir
70 | *
71 | * @throws \PageCache\PageCacheException
72 | */
73 | public function setDir($dir)
74 | {
75 | if (empty($dir) || !@is_dir($dir)) {
76 | throw new PageCacheException(__METHOD__ . ': ' . (string)$dir . ' in constructor is not a directory');
77 | }
78 |
79 | // Check for trailing slash and add it if not exists
80 | if (mb_substr($dir, -1, 1) !== DIRECTORY_SEPARATOR) {
81 | $dir .= DIRECTORY_SEPARATOR;
82 | }
83 |
84 | $this->dir = $dir;
85 | }
86 |
87 | /**
88 | * Set file
89 | *
90 | * @param string|null $file
91 | */
92 | public function setFile($file)
93 | {
94 | $this->file = $file;
95 | }
96 |
97 | /**
98 | * Sets file parameter and creates needed subdirectories for storing it
99 | *
100 | * @param mixed $file cache key
101 | *
102 | * @return void
103 | * @throws PageCacheException
104 | */
105 | public function processFile($file)
106 | {
107 | $this->setFile($file);
108 | $this->hashSubDirectories = $this->createSubdirectoriesForFile();
109 | }
110 |
111 | /**
112 | * @return string full file storage path
113 | */
114 | public function getFileStoragePath()
115 | {
116 | return $this->dir . $this->hashSubDirectories . $this->file;
117 | }
118 |
119 | /**
120 | * Based on incoming string (filename) return 2 directories to store cache file.
121 | * If directories(one or both) not present create whichever is not there yet.
122 | *
123 | * Returns null if $this->file or $this->dir is not set.
124 | *
125 | * @return string|null with two directory names like '10/55/', ready to be appended to cache_dir
126 | * @throws PageCacheException
127 | */
128 | public function createSubdirectoriesForFile()
129 | {
130 | if (empty($this->file) || empty($this->dir)) {
131 | throw new PageCacheException(
132 | __METHOD__ . ': file or directory not set. file: ' . $this->file . ' , directory: ' . $this->dir
133 | );
134 | }
135 |
136 | $path = $this->getDirectoryPathByHash($this->file);
137 |
138 | $this->createSubDirs($path);
139 | return $path;
140 | }
141 |
142 | /**
143 | * Inside $this->dir (Cache Directory), create 2 sub directories to store current cache file
144 | *
145 | * @param string $path Relative directory path
146 | *
147 | * @throws \PageCache\PageCacheException directories not created
148 | */
149 | private function createSubDirs($path)
150 | {
151 | $fullPath = $this->dir . $path;
152 |
153 | if (!\file_exists($fullPath) && !mkdir($fullPath, $this->directoryPermissions, true) && !is_dir($fullPath)) {
154 | throw new PageCacheException(__METHOD__ . ': ' . $fullPath . ' cache directory could not be created');
155 | }
156 | }
157 |
158 | /**
159 | * Get subdirectories for location of where cache file would be placed.
160 | * Returns null when filename is empty, otherwise 2 subdirectories where filename would be located.
161 | *
162 | * @param string $filename Cache file name
163 | *
164 | * @return null|string null
165 | */
166 | public function getLocation($filename)
167 | {
168 | return empty($filename) ? null : $this->getDirectoryPathByHash($filename);
169 | }
170 |
171 | /**
172 | * Get a path with 2 directories, based on filename hash
173 | *
174 | * @param string $filename
175 | *
176 | * @return string directory path
177 | */
178 | private function getDirectoryPathByHash($filename)
179 | {
180 | //get 2 number
181 | $val1 = ord($filename[1]);
182 | $val2 = ord($filename[3]);
183 |
184 | //normalize to 99
185 | $val1 %= 99;
186 | $val2 %= 99;
187 | return $val1 . '/' . $val2 . '/';
188 | }
189 |
190 | /**
191 | * Removes all files and directories inside a directory.
192 | * Used for deleting all cache content.
193 | *
194 | * @param string $dir
195 | *
196 | * @return bool
197 | */
198 | public function clearDirectory($dir)
199 | {
200 | if (empty($dir) || !@is_dir($dir)) {
201 | return false;
202 | }
203 |
204 | $iterator = new \RecursiveDirectoryIterator($dir);
205 | $filter = new \RecursiveCallbackFilterIterator($iterator, function ($current) {
206 | /** @var \SplFileInfo $current */
207 | $filename = $current->getBasename();
208 | // Check for files and dirs starting with "dot" (.gitignore, etc)
209 | return !($filename && $filename[0] === '.');
210 | });
211 |
212 | /** @var \SplFileInfo[] $listing */
213 | $listing = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::CHILD_FIRST);
214 | foreach ($listing as $item) {
215 | $path = $item->getPathname();
216 | $item->isDir() ? rmdir($path) : unlink($path);
217 | }
218 | return true;
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/tests/Storage/FileSystem/FileSystemTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Tests\Storage\FileSystem;
12 |
13 | use PageCache\Storage\FileSystem\FileSystem;
14 | use org\bovigo\vfs\vfsStream;
15 | use org\bovigo\vfs\vfsStreamDirectory;
16 |
17 | /**
18 | * FileSystem is a file storage for cache.
19 | * Virtual directory structure is used in testing
20 | *
21 | * Class FileSystemTest
22 | * @package PageCache\Tests\Storage
23 | */
24 | class FileSystemTest extends \PHPUnit\Framework\TestCase
25 | {
26 | /**
27 | * @var vfsStreamDirectory
28 | */
29 | private $virtualRoot;
30 |
31 | public function setUp()
32 | {
33 | $this->virtualRoot = vfsStream::setup('fileSystemDir');
34 | }
35 |
36 | private function getVirtualDirectory()
37 | {
38 | //setup virtual dir
39 | return vfsStream::url('fileSystemDir/');
40 | }
41 |
42 | public function testConstructor()
43 | {
44 | $fs = new FileSystem('somevalue');
45 | $this->assertAttributeEquals('somevalue', 'content', $fs);
46 | $this->assertAttributeEquals(null, 'file_lock', $fs);
47 | }
48 |
49 | public function testConst()
50 | {
51 | $this->assertEquals(1, FileSystem::OK);
52 | $this->assertEquals(2, FileSystem::ERROR);
53 | $this->assertEquals(3, FileSystem::ERROR_OPEN);
54 | $this->assertEquals(4, FileSystem::ERROR_WRITE);
55 | $this->assertEquals(5, FileSystem::ERROR_LOCK);
56 | }
57 |
58 | public function testSetFileLock()
59 | {
60 | $content = 'some content';
61 | $fs = new FileSystem($content);
62 | $this->assertAttributeEmpty('file_lock', $fs);
63 |
64 | $fs->setFileLock(LOCK_EX);
65 | $this->assertAttributeEquals(LOCK_EX, 'file_lock', $fs);
66 |
67 | $fs->setFileLock(LOCK_EX | LOCK_NB);
68 | $this->assertAttributeEquals(LOCK_EX | LOCK_NB, 'file_lock', $fs);
69 | }
70 |
71 | /**
72 | * @expectedException \Exception
73 | */
74 | public function testSetFileLockException()
75 | {
76 | $fs = new FileSystem('a');
77 | $fs->setFileLock('');
78 | }
79 |
80 | public function testSetFilePath()
81 | {
82 | $content = 'some content';
83 | $fs = new FileSystem($content);
84 | $this->assertAttributeEmpty('filepath', $fs);
85 |
86 | $path = '/some/path/to/cache/file/74bf6b958564c606d2672751fc82b8e6';
87 | $fs->setFilePath($path);
88 | $this->assertAttributeEquals($path, 'filepath', $fs);
89 | }
90 |
91 | /**
92 | * @expectedException \Exception
93 | */
94 | public function testSetFilePathException()
95 | {
96 | $fs = new FileSystem('a');
97 | $fs->setFilePath('');
98 | }
99 |
100 | /**
101 | * @depends testSetFilePath
102 | */
103 | public function testGetFilePath()
104 | {
105 | $fs = new FileSystem('aa');
106 | $path = '/file/74bf6b958564c606d2672751fc82b8e6';
107 | $fs->setFilePath($path);
108 |
109 | $this->assertEquals($path, $fs->getFilepath());
110 | }
111 |
112 | /**
113 | * @depends testSetFileLock
114 | */
115 | public function testGetFileLock()
116 | {
117 | $fs = new FileSystem('a');
118 | $this->assertEquals(null, $fs->getFileLock());
119 |
120 | $fs->setFileLock(LOCK_EX);
121 | $this->assertEquals(2, $fs->getFileLock());
122 |
123 | $fs->setFileLock(LOCK_EX | LOCK_NB);
124 | $this->assertEquals(6, $fs->getFileLock());
125 | $this->assertEquals(LOCK_EX | LOCK_NB, $fs->getFileLock());
126 | }
127 |
128 | public function testWriteAttempt()
129 | {
130 | $fpath = $this->getVirtualDirectory() . 'testfiletowrite.txt';
131 |
132 | $fs = new FileSystem('content');
133 | $result = $fs->writeAttempt();
134 |
135 | //no filepath, error
136 | $this->assertEquals(FileSystem::ERROR, $result);
137 |
138 | $this->assertFileNotExists($fpath);
139 | $fs->setFilePath($fpath);
140 | $result = $fs->writeAttempt();
141 | $this->assertEquals(FileSystem::OK, $result);
142 | $this->assertFileExists($fpath);
143 | $this->assertEquals('content', file_get_contents($fpath));
144 | }
145 |
146 | public function testWriteAttemptWithLock()
147 | {
148 | $fpath = $this->getVirtualDirectory() . 'testfiletowriteWithLock.txt';
149 |
150 | $fs = new FileSystem('content written with lock');
151 | $fs->setFilePath($fpath);
152 | $fs->setFileLock(LOCK_EX | LOCK_NB);
153 |
154 | $this->assertFileNotExists($fpath);
155 | $result = $fs->writeAttempt();
156 | $this->assertEquals(FileSystem::OK, $result);
157 | $this->assertFileExists($fpath);
158 | $this->assertEquals('content written with lock', file_get_contents($fpath));
159 |
160 | //multiple writes
161 | $result1 = $fs->writeAttempt();
162 | $result2 = $fs->writeAttempt();
163 | $result3 = $fs->writeAttempt();
164 | $this->assertEquals(FileSystem::OK, $result1);
165 | $this->assertEquals(FileSystem::OK, $result2);
166 | $this->assertEquals(FileSystem::OK, $result3);
167 | $this->assertEquals('content written with lock', file_get_contents($fpath));
168 | }
169 |
170 | /**
171 | * When directory/file are not writable and writeAttempt is being made.
172 | */
173 | public function testWriteAttemptForErrorOpen()
174 | {
175 | $fpath = $this->getVirtualDirectory() . 'SomeNewFile';
176 |
177 | //make base directory not writable
178 | chmod($this->getVirtualDirectory(), 0111);
179 |
180 | $fs = new FileSystem('write attempt');
181 | $fs->setFilePath($fpath);
182 |
183 | //@supress warning from fopen not being able to open file
184 | $this->assertEquals(FileSystem::ERROR_OPEN, @$fs->writeAttempt());
185 | }
186 |
187 | public function testPHPflock()
188 | {
189 | $fpath = $this->getVirtualDirectory() . 'testfiletowriteWithLock.txt';
190 |
191 | $fp = fopen($fpath, 'c');
192 |
193 | flock($fp, LOCK_EX);
194 | $new = fopen($fpath, 'c');
195 |
196 | flock($fp, LOCK_UN);
197 |
198 | if (flock($new, LOCK_EX)) {
199 | $this->assertTrue(true);
200 | } else {
201 | $this->assertTrue(false, 'Lock was not obtained, it should have.');
202 | }
203 | }
204 |
205 | public function testPHPflockBlocking()
206 | {
207 | $fpath = $this->getVirtualDirectory() . 'testfiletowriteWithLock.txt';
208 |
209 | $fp = fopen($fpath, 'c');
210 | flock($fp, LOCK_EX);
211 |
212 | $new_fp = fopen($fpath, 'c');
213 |
214 | //since $fp, has Exclusive lock, no one else should have it
215 | $this->assertFalse(flock($new_fp, LOCK_EX));
216 |
217 | //release lock
218 | flock($fp, LOCK_UN);
219 | //now it should work
220 | $this->assertTrue(flock($new_fp, LOCK_EX));
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/src/HttpHeaders.php:
--------------------------------------------------------------------------------
1 |
8 | * @copyright 2016
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | /**
15 | * Class HttpHeaders
16 | *
17 | * @package PageCache
18 | */
19 | class HttpHeaders
20 | {
21 | const HEADER_EXPIRES = 'Expires';
22 | const HEADER_LAST_MODIFIED = 'Last-Modified';
23 | const HEADER_NOT_MODIFIED = 'HTTP/1.1 304 Not Modified';
24 | const HEADER_ETAG = 'ETag';
25 | const HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE';
26 | const HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH';
27 |
28 | const DATE_FORMAT = 'D, d M Y H:i:s';
29 | const DATE_FORMAT_CREATE = self::DATE_FORMAT . ' \G\M\T';
30 | const DATE_FORMAT_PARSE = self::DATE_FORMAT . ' T';
31 |
32 | /**
33 | * Last modified time of the cache item
34 | *
35 | * @var \DateTime
36 | */
37 | private $itemLastModified;
38 |
39 | /**
40 | * @var \DateTime
41 | */
42 | private $itemExpiresAt;
43 |
44 | /**
45 | * @var string
46 | */
47 | private $itemETagString;
48 |
49 | /**
50 | * @var array
51 | */
52 | private $responseHeaders = [];
53 |
54 | /**
55 | * Set Last-Modified header
56 | *
57 | * @param \DateTime $lastModified
58 | *
59 | * @return $this
60 | */
61 | public function setLastModified(\DateTime $lastModified)
62 | {
63 | $this->itemLastModified = $lastModified;
64 |
65 | return $this;
66 | }
67 |
68 | /**
69 | * Set ETag header
70 | *
71 | * @param string $value
72 | *
73 | * @return $this
74 | */
75 | public function setETag($value)
76 | {
77 | $this->itemETagString = (string)$value;
78 |
79 | return $this;
80 | }
81 |
82 | /**
83 | * Set Expires header
84 | *
85 | * @param \DateTime $expirationTime
86 | *
87 | * @return $this
88 | */
89 | public function setExpires(\DateTime $expirationTime)
90 | {
91 | $this->itemExpiresAt = $expirationTime;
92 |
93 | return $this;
94 | }
95 |
96 | /**
97 | * Send Headers
98 | */
99 | public function send()
100 | {
101 | // Last-Modified
102 | if ($this->itemLastModified) {
103 | $this->setHeader(
104 | self::HEADER_LAST_MODIFIED,
105 | $this->itemLastModified->format(self::DATE_FORMAT_CREATE)
106 | );
107 | }
108 |
109 | // Expires
110 | if ($this->itemExpiresAt) {
111 | $this->setHeader(
112 | self::HEADER_EXPIRES,
113 | $this->itemExpiresAt->format(self::DATE_FORMAT_CREATE)
114 | );
115 | }
116 |
117 | // ETag
118 | if ($this->itemETagString) {
119 | $this->setHeader(
120 | self::HEADER_ETAG,
121 | $this->itemETagString
122 | );
123 | }
124 | }
125 |
126 | /**
127 | * @return array
128 | */
129 | public function getSentHeaders()
130 | {
131 | /** @link http://php.net/manual/ru/function.headers-list.php#120539 */
132 | return (PHP_SAPI === 'cli' && \function_exists('xdebug_get_headers'))
133 | ? xdebug_get_headers()
134 | : headers_list();
135 | }
136 |
137 | /**
138 | * Sends HTTP Header
139 | *
140 | * @param string $name Header name
141 | * @param string|null $value Header value
142 | * @param int $httpResponseCode HTTP response code
143 | */
144 | private function setHeader($name, $value = null, $httpResponseCode = null)
145 | {
146 | header($name . ($value ? ': ' . $value : ''), true, $httpResponseCode);
147 | }
148 |
149 | /**
150 | * Set Not Modified header, only if HTTP_IF_MODIFIED_SINCE was set or ETag matches
151 | * Content body is not sent when this header is set. Client/browser will use its local copy.
152 | *
153 | * When return is true, 304 header will be sent later and exit() will be called.
154 | *
155 | * @return bool To end execution or not
156 | */
157 | public function checkIfNotModified()
158 | {
159 | $lastModifiedTimestamp = $this->itemLastModified->getTimestamp();
160 | $modifiedSinceTimestamp = $this->getIfModifiedSinceTimestamp();
161 |
162 | $notModified = false;
163 |
164 | // Do we have matching ETags
165 | if (!empty($_SERVER[self::HTTP_IF_NONE_MATCH])) {
166 | $notModified = $_SERVER[self::HTTP_IF_NONE_MATCH] === $this->itemETagString;
167 | }
168 |
169 | // Client's version older than server's?
170 | // If ETags matched ($notModified=true), we skip this step.
171 | // Because same hash means same file contents, no need to further check if-modified-since header
172 | if (!$notModified && $modifiedSinceTimestamp !== false) {
173 | $notModified = $modifiedSinceTimestamp >= $lastModifiedTimestamp;
174 | }
175 |
176 | return $notModified;
177 | }
178 |
179 | /**
180 | * Send 304 Not Modified header.
181 | * Keeping this separate because right after this is set, we exit().
182 | */
183 | public function sendNotModifiedHeader()
184 | {
185 | http_response_code(304);
186 | $this->setHeader(self::HEADER_NOT_MODIFIED);
187 | }
188 |
189 | /**
190 | * Get timestamp value from If-Modified-Since request header
191 | *
192 | * @return false|int Timestamp or false when header not found
193 | */
194 | private function getIfModifiedSinceTimestamp()
195 | {
196 | if (!empty($_SERVER[self::HTTP_IF_MODIFIED_SINCE])) {
197 | $modifiedSinceString = $_SERVER[self::HTTP_IF_MODIFIED_SINCE];
198 | } elseif (!empty($_ENV[self::HTTP_IF_MODIFIED_SINCE])) {
199 | $modifiedSinceString = $_ENV[self::HTTP_IF_MODIFIED_SINCE];
200 | } else {
201 | $modifiedSinceString = null;
202 | }
203 |
204 | if ($modifiedSinceString) {
205 | // Some versions of IE 6 append "; length=##"
206 | if (($pos = strpos($modifiedSinceString, ';')) !== false) {
207 | $modifiedSinceString = substr($modifiedSinceString, 0, $pos);
208 | }
209 |
210 | return strtotime($modifiedSinceString);
211 | }
212 |
213 | return false;
214 | }
215 |
216 | /**
217 | * @return bool|\DateTime
218 | */
219 | public function detectResponseLastModified()
220 | {
221 | $value = $this->detectResponseHeaderValue(self::HEADER_LAST_MODIFIED);
222 |
223 | return $value
224 | ? \DateTime::createFromFormat(self::DATE_FORMAT_PARSE, $value)
225 | : false;
226 | }
227 |
228 | /**
229 | * @return bool|\DateTime
230 | */
231 | public function detectResponseExpires()
232 | {
233 | $value = $this->detectResponseHeaderValue(self::HEADER_EXPIRES);
234 |
235 | return $value
236 | ? \DateTime::createFromFormat(self::DATE_FORMAT_PARSE, $value)
237 | : false;
238 | }
239 |
240 | /**
241 | * @return mixed|null
242 | */
243 | public function detectResponseETagString()
244 | {
245 | return $this->detectResponseHeaderValue(self::HEADER_ETAG);
246 | }
247 |
248 | /**
249 | * @param string $name
250 | *
251 | * @return mixed|null
252 | */
253 | private function detectResponseHeaderValue($name)
254 | {
255 | $headers = $this->getResponseHeaders();
256 |
257 | return isset($headers[$name]) ? $headers[$name] : null;
258 | }
259 |
260 | /**
261 | * Get headers and populate local responseHeaders variable
262 | *
263 | * @return array
264 | */
265 | private function getResponseHeaders()
266 | {
267 | if (!$this->responseHeaders) {
268 | foreach ($this->getSentHeaders() as $item) {
269 | list($key, $value) = explode(':', $item, 2);
270 |
271 | $this->responseHeaders[$key] = trim($value);
272 | }
273 | }
274 |
275 | return $this->responseHeaders;
276 | }
277 |
278 | }
279 |
--------------------------------------------------------------------------------
/tests/PageCacheTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace PageCache\Tests;
12 |
13 | use Monolog\Handler\StreamHandler;
14 | use Monolog\Logger;
15 | use org\bovigo\vfs\vfsStream;
16 | use org\bovigo\vfs\vfsStreamDirectory;
17 | use PageCache\Config;
18 | use PageCache\Storage\CacheItemStorage;
19 | use PageCache\DefaultLogger;
20 | use PageCache\PageCache;
21 | use PageCache\SessionHandler;
22 | use PageCache\Strategy\DefaultStrategy;
23 | use PageCache\Strategy\MobileStrategy;
24 |
25 | class PageCacheTest extends \PHPUnit\Framework\TestCase
26 | {
27 | /** @var vfsStreamDirectory */
28 | private $root;
29 |
30 | public function setUp()
31 | {
32 | date_default_timezone_set('UTC');
33 | $this->setServerParameters();
34 | $this->root = vfsStream::setup('tmpdir');
35 | }
36 |
37 | protected function tearDown()
38 | {
39 | PageCache::destroy();
40 | SessionHandler::reset();
41 | }
42 |
43 | /**
44 | * Multiple Instances
45 | *
46 | * @expectedException \PageCache\PageCacheException
47 | */
48 | public function testSingleton()
49 | {
50 | $pc = new PageCache();
51 | $another = new PageCache();
52 | }
53 |
54 | /**
55 | * Without config file
56 | */
57 | public function testConstructWithoutConfig()
58 | {
59 | $pc = new PageCache();
60 | $this->assertFalse(SessionHandler::getStatus());
61 |
62 | $this->assertAttributeInstanceOf(\PageCache\HttpHeaders::class, 'httpHeaders', $pc);
63 | $this->assertAttributeInstanceOf(DefaultStrategy::class, 'strategy', $pc);
64 | $this->assertAttributeInstanceOf(Config::class, 'config', $pc);
65 | }
66 |
67 | /**
68 | * Default init
69 | */
70 | public function testInit()
71 | {
72 | $pc = new PageCache();
73 | $pc->config()->setCachePath(vfsStream::url('tmpdir').'/');
74 |
75 | // No CacheItemStorage before init()
76 | $this->assertAttributeEquals(null, 'itemStorage', $pc);
77 |
78 | $pc->init();
79 |
80 | // CacheItemStorage created
81 | $this->assertAttributeInstanceOf(CacheItemStorage::class, 'itemStorage', $pc);
82 |
83 | $output = 'Testing output for testInit()';
84 | $this->expectOutputString($output);
85 | echo $output;
86 |
87 | $this->assertFalse($pc->isCached());
88 | ob_end_flush();
89 | $this->assertTrue($pc->isCached());
90 | }
91 |
92 | public function testInitWithHeaders()
93 | {
94 | $pc = new PageCache();
95 | $pc->config()->setCachePath(vfsStream::url('tmpdir').'/')
96 | ->setSendHeaders(true);
97 |
98 | $pc->init();
99 | $output = 'Testing output for InitWithHeaders() with Headers enabled';
100 | $this->expectOutputString($output);
101 | echo $output;
102 | $this->assertFalse($pc->isCached());
103 | ob_end_flush();
104 | $this->assertTrue($pc->isCached());
105 | }
106 |
107 | public function testSetStrategy()
108 | {
109 | $pc = new PageCache();
110 |
111 | $pc->setStrategy(new MobileStrategy());
112 | $this->assertAttributeInstanceOf(MobileStrategy::class, 'strategy', $pc);
113 |
114 | $pc->setStrategy(new DefaultStrategy());
115 | $this->assertAttributeInstanceOf(DefaultStrategy::class, 'strategy', $pc);
116 | }
117 |
118 | public function testSetStrategyException()
119 | {
120 | $pc = new PageCache();
121 | $this->expectException(\InvalidArgumentException::class);
122 |
123 | try {
124 | $pc->setStrategy(new \stdClass());
125 | } catch (\Throwable $e) {
126 | throw new \InvalidArgumentException;
127 | // echo '~~~~As expected PHP7 throws Throwable.';
128 | } catch (\Exception $e) {
129 | throw new \InvalidArgumentException;
130 | // echo '~~~~As expected PHP5 throws Exception.';
131 | }
132 | }
133 |
134 | public function testClearPageCache()
135 | {
136 | $pc = new PageCache(__DIR__.'/config_test.php');
137 | $pc->config()->setCachePath(vfsStream::url('tmpdir').'/');
138 |
139 | $pc->init();
140 | $output = 'Testing output for clearPageCache()';
141 | $this->expectOutputString($output);
142 | echo $output;
143 | ob_end_flush();
144 | $this->assertTrue($pc->isCached(), 'cache does not exist');
145 |
146 | $pc->clearPageCache();
147 | $this->assertFalse($pc->isCached(), 'cache exists');
148 | }
149 |
150 | public function testGetPageCache()
151 | {
152 | $cachePath = vfsStream::url('tmpdir').'/';
153 |
154 | $pc = new PageCache();
155 | $pc->config()->setCachePath($cachePath);
156 | $this->assertSame(false, $pc->getPageCache());
157 | $pc->destroy();
158 |
159 | $pc = new PageCache(__DIR__.'/config_test.php');
160 | $pc->config()->setCachePath($cachePath);
161 | $pc->init();
162 | $output = 'Testing output for getPageCache()';
163 | $this->expectOutputString($output);
164 | echo $output;
165 | ob_end_flush();
166 | $this->assertSame($output, $pc->getPageCache());
167 | }
168 |
169 | public function testIsCached()
170 | {
171 | $pc = new PageCache(__DIR__.'/config_test.php');
172 | $pc->config()->setCachePath(vfsStream::url('tmpdir').'/');
173 |
174 | //no cache exists
175 | $this->assertFalse($pc->isCached(), ' is cached');
176 |
177 | //cache page
178 | $pc->init();
179 | $output = 'testIsCached() being test... this line is going to populate cache file for testing...';
180 | $this->expectOutputString($output);
181 | echo $output;
182 |
183 | //manually end output buffering. file cache must exist
184 | ob_end_flush();
185 |
186 | //cache exists now
187 | $this->assertTrue(
188 | $pc->isCached(),
189 | __METHOD__.' after init cache item does not exist'
190 | );
191 | $this->assertEquals($output, $pc->getPageCache(), 'Cache file contents not as expected.');
192 | }
193 |
194 | public function testSetLogger()
195 | {
196 | $pc = new PageCache();
197 | $this->assertAttributeEmpty('logger', $pc);
198 | $logger = new Logger('testmonolog');
199 | $pc->setLogger($logger);
200 | $this->assertAttributeEquals($logger, 'logger', $pc);
201 | }
202 |
203 | public function testDefaultLogger()
204 | {
205 | $tmpDir = vfsStream::url('tmpdir');
206 | $tmpFile = $tmpDir.'/log.txt';
207 |
208 | $pc = new PageCache();
209 | $pc->config()->setEnableLog(true)
210 | ->setCachePath($tmpDir.'/')
211 | ->setLogFilePath($tmpFile);
212 |
213 | // No logger
214 | $this->assertAttributeEquals(null, 'logger', $pc);
215 |
216 | //During init logger is initialized
217 | $pc->init();
218 | $output = 'testLog() method testing, output testing.';
219 | $this->expectOutputString($output);
220 | echo $output;
221 | ob_end_flush();
222 |
223 | $this->assertAttributeInstanceOf(DefaultLogger::class, 'logger', $pc);
224 | $this->assertContains('PageCache\PageCache::init', file_get_contents($tmpFile));
225 | $this->assertContains('PageCache\PageCache::storePageContent', file_get_contents($tmpFile));
226 | }
227 |
228 | public function testLogWithMonolog()
229 | {
230 | $cachePath = vfsStream::url('tmpdir').'/';
231 | $defaultLogFile = vfsStream::url('tmpdir').'/log.txt';
232 | $monologLogFile = vfsStream::url('tmpdir').'/monolog.log';
233 |
234 | $pc = new PageCache();
235 | $pc->config()->setEnableLog(true)
236 | ->setCachePath($cachePath)
237 | ->setLogFilePath($defaultLogFile); //internal logger, should be ignored
238 |
239 | $monolog_logger = new Logger('PageCache');
240 | $monolog_logger->pushHandler(new StreamHandler($monologLogFile, Logger::DEBUG));
241 | $pc->setLogger($monolog_logger);
242 |
243 | $pc->init();
244 | ob_end_flush();
245 | $this->assertContains(
246 | 'PageCache\PageCache::init',
247 | file_get_contents($monologLogFile)
248 | );
249 | $this->assertFalse(file_exists($defaultLogFile));
250 | }
251 |
252 | /**
253 | * @throws \Exception
254 | * @doesNotPerformAssertions
255 | */
256 | public function testDestroy()
257 | {
258 | $pc = new PageCache();
259 | $pc->config()->setUseSession(true);
260 | $pc->destroy();
261 |
262 | new PageCache();
263 | }
264 |
265 | public function testGetStrategy()
266 | {
267 | $pc = new PageCache();
268 |
269 | $pc->setStrategy(new DefaultStrategy());
270 | $this->assertInstanceOf(DefaultStrategy::class, $pc->getStrategy());
271 | }
272 |
273 | /**
274 | * Tests edge case when cache file might have an empty content.
275 | * In this case existing cache file must be ignored and page content must be displayed.
276 | *
277 | * @throws \PageCache\PageCacheException
278 | * @throws \Psr\SimpleCache\InvalidArgumentException
279 | */
280 | public function testEmptyCacheContentToBeServed()
281 | {
282 | /**
283 | * First hit on a page. Cache content.
284 | */
285 | $pc = new PageCache(__DIR__.'/config_test.php');
286 | $pc->config()->setCachePath(vfsStream::url('tmpdir').'/');
287 | $pc->init();
288 | $output = 'testEmptyCacheContentToBeServed() is being tested. Line 1.';
289 | echo $output;
290 | ob_end_flush();
291 |
292 | //echo 'Full contents:'.file_get_contents(vfsStream::url('tmpdir').'/48/2/709e95a30a05ae82a5b8ee166fb10fbf075ac0be');
293 |
294 | /**
295 | * Imitate a cache error.
296 | * Update cached content with empty string.
297 | */
298 | $item = $pc->getItemStorage()->get($pc->getCurrentKey());
299 | $item->setContent('');
300 | $pc->getItemStorage()->set($item);
301 | $pc::destroy();
302 |
303 | //echo '\nEmpty contents:'.file_get_contents(vfsStream::url('tmpdir').'/48/2/709e95a30a05ae82a5b8ee166fb10fbf075ac0be');
304 |
305 | /**
306 | * Imitates a new hit on the same page
307 | * Cached content of this page is empty: should ignore cache, and output new content.
308 | */
309 | $pc2 = new PageCache(__DIR__.'/config_test.php');
310 | $pc2->config()->setCachePath(vfsStream::url('tmpdir').'/');
311 | $pc2->init();
312 | $output2 = 'New content. Line 2';
313 | echo $output2;
314 | ob_end_flush();
315 |
316 | $this->assertEquals($pc2->getItemStorage()->get($pc2->getCurrentKey())->getContent(), $output2);
317 | $this->expectOutputString($output . $output2);
318 | }
319 |
320 |
321 | private function setServerParameters()
322 | {
323 | if (!isset($_SERVER['REQUEST_URI'])) {
324 | $_SERVER['REQUEST_URI'] = '/';
325 | }
326 |
327 | if (!isset($_SERVER['SCRIPT_NAME'])) {
328 | $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'];
329 | }
330 |
331 | if (!isset($_SERVER['QUERY_STRING'])) {
332 | $_SERVER['QUERY_STRING'] = '';
333 | }
334 | }
335 | }
336 |
--------------------------------------------------------------------------------
/src/Config.php:
--------------------------------------------------------------------------------
1 |
6 | * @copyright 2017
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache;
13 |
14 | class Config
15 | {
16 | /**
17 | * Configuration array
18 | *
19 | * @var string
20 | */
21 | private $config = [];
22 |
23 | /**
24 | * Regenerate cache if cached content is less that this many bytes (some error occurred)
25 | *
26 | * @var int
27 | */
28 | private $minCacheFileSize = 10;
29 |
30 | /**
31 | * @var bool
32 | */
33 | private $enableLog = false;
34 |
35 | /**
36 | * File path for internal log file
37 | *
38 | * @var string
39 | */
40 | private $logFilePath;
41 |
42 | /**
43 | * Cache expiration in seconds
44 | *
45 | * @var int
46 | */
47 | private $cacheExpirationInSeconds = 1200;
48 |
49 | /**
50 | * Cache directory
51 | *
52 | * @var string
53 | */
54 | private $cachePath;
55 |
56 | /**
57 | * @var bool
58 | */
59 | private $useSession = false;
60 |
61 | /**
62 | * @var array
63 | */
64 | private $sessionExcludeKeys = [];
65 |
66 | /**
67 | * File locking preference for flock() function.
68 | * Default is a non-blocking exclusive write lock: LOCK_EX | LOCK_NB = 6
69 | * When false, file locking is disabled.
70 | *
71 | * @var false|int
72 | */
73 | private $fileLock = LOCK_EX | LOCK_NB;
74 |
75 | /**
76 | * @var bool
77 | */
78 | private $sendHeaders = false;
79 |
80 | /**
81 | * When true enables a dry run of the system. Useful for testing.
82 | * Default is false
83 | *
84 | * @var bool
85 | */
86 | private $dryRunMode = false;
87 |
88 | /**
89 | * Forward Last-Modified, Expires and ETag headers from application
90 | *
91 | * @var bool
92 | */
93 | private $forwardHeaders = false;
94 |
95 | /**
96 | * Config constructor.
97 | *
98 | * @param string $config_file_path Config File path
99 | * @throws PageCacheException
100 | */
101 | public function __construct($config_file_path = null)
102 | {
103 | // Load configuration file
104 | if (!empty($config_file_path)) {
105 | if (!file_exists($config_file_path)) {
106 | throw new PageCacheException(__METHOD__ . ' Config file path not valid: ' . $config_file_path);
107 | }
108 | /** @noinspection PhpIncludeInspection */
109 | $this->config = include $config_file_path;
110 | $this->setConfigValues();
111 | }
112 | }
113 |
114 | /**
115 | * Reads config array and assigns config values
116 | *
117 | * @throws PageCacheException
118 | */
119 | private function setConfigValues()
120 | {
121 | if (isset($this->config['min_cache_file_size']) && is_numeric($this->config['min_cache_file_size'])) {
122 | $this->setMinCacheFileSize($this->config['min_cache_file_size']);
123 | }
124 |
125 | //Enable Log
126 | if (isset($this->config['enable_log']) && $this->isBool($this->config['enable_log'])) {
127 | $this->setEnableLog($this->config['enable_log']);
128 | }
129 |
130 | //Cache Expiration Time
131 | if (isset($this->config['cache_expiration_in_seconds'])) {
132 | $this->setCacheExpirationInSeconds($this->config['cache_expiration_in_seconds']);
133 | }
134 |
135 | // Path to store cache files
136 | if (isset($this->config['cache_path'])) {
137 | $this->setCachePath($this->config['cache_path']);
138 | }
139 |
140 | // Log file path
141 | if (isset($this->config['log_file_path']) && !empty($this->config['log_file_path'])) {
142 | $this->setLogFilePath($this->config['log_file_path']);
143 | }
144 |
145 | // Use $_SESSION while caching or not
146 | if (isset($this->config['use_session']) && $this->isBool($this->config['use_session'])) {
147 | $this->setUseSession($this->config['use_session']);
148 | }
149 |
150 | // Session exclude key
151 | if (isset($this->config['session_exclude_keys']) && is_array($this->config['session_exclude_keys'])) {
152 | $this->setSessionExcludeKeys($this->config['session_exclude_keys']);
153 | }
154 |
155 | // File Locking
156 | if (isset($this->config['file_lock']) && !empty($this->config['file_lock'])) {
157 | $this->setFileLock($this->config['file_lock']);
158 | }
159 |
160 | // Send HTTP headers
161 | if (isset($this->config['send_headers']) && $this->isBool($this->config['send_headers'])) {
162 | $this->setSendHeaders($this->config['send_headers']);
163 | }
164 |
165 | // Forward Last-Modified and ETag headers to cache item
166 | if (isset($this->config['forward_headers']) && $this->isBool($this->config['forward_headers'])) {
167 | $this->setForwardHeaders($this->config['forward_headers']);
168 | }
169 |
170 | // Enable Dry run mode
171 | if (isset($this->config['dry_run_mode']) && $this->isBool($this->config['dry_run_mode'])) {
172 | $this->setDryRunMode($this->config['dry_run_mode']);
173 | }
174 | }
175 |
176 | /**
177 | * Checks if given variable is a boolean value.
178 | * For PHP < 5.5 (boolval alternative)
179 | *
180 | * @param mixed $var
181 | *
182 | * @return bool true if is boolean, false if is not
183 | */
184 | public function isBool($var)
185 | {
186 | return ($var === true || $var === false);
187 | }
188 |
189 | /**
190 | * Get minimum allowed size of a cache file.
191 | *
192 | * @return int
193 | */
194 | public function getMinCacheFileSize()
195 | {
196 | return $this->minCacheFileSize;
197 | }
198 |
199 | /**
200 | * When generated cache file is less that this size, it is considered as invalid (will be regenerated on next call)
201 |
202 | * @param int $minCacheFileSize
203 | *
204 | * @return Config for chaining
205 | */
206 | public function setMinCacheFileSize($minCacheFileSize)
207 | {
208 | $this->minCacheFileSize = (int)$minCacheFileSize;
209 | return $this;
210 | }
211 |
212 | /**
213 | * @return bool
214 | */
215 | public function isEnableLog()
216 | {
217 | return $this->enableLog;
218 | }
219 |
220 | /**
221 | * Disable or enable logging
222 | *
223 | * @param bool $enableLog
224 | * @return Config for chaining
225 | */
226 | public function setEnableLog($enableLog)
227 | {
228 | $this->enableLog = (bool)$enableLog;
229 | return $this;
230 | }
231 |
232 | /**
233 | * Get file path for internal log file
234 | *
235 | * @return string
236 | */
237 | public function getLogFilePath()
238 | {
239 | return $this->logFilePath;
240 | }
241 |
242 | /**
243 | * Set path for internal log file
244 | *
245 | * @param string $logFilePath
246 | * @return Config for chaining
247 | * @throws PageCacheException
248 | */
249 | public function setLogFilePath($logFilePath)
250 | {
251 | if (!$this->isParentDirectoryExists($logFilePath)) {
252 | throw new PageCacheException('Log file directory does not exist for the path provided '
253 | . $logFilePath);
254 | }
255 |
256 | $this->logFilePath = $logFilePath;
257 | return $this;
258 | }
259 |
260 | /**
261 | * @return int
262 | */
263 | public function getCacheExpirationInSeconds()
264 | {
265 | return $this->cacheExpirationInSeconds;
266 | }
267 |
268 | /**
269 | * @param int $seconds
270 | * @return Config for chaining
271 | * @throws PageCacheException
272 | */
273 | public function setCacheExpirationInSeconds($seconds)
274 | {
275 | if ($seconds < 0 || !is_numeric($seconds)) {
276 | throw new PageCacheException(__METHOD__ . ' Invalid expiration value, < 0.');
277 | }
278 |
279 | $this->cacheExpirationInSeconds = (int)$seconds;
280 | return $this;
281 | }
282 |
283 | /**
284 | * Get cache directory path
285 | *
286 | * @return string
287 | */
288 | public function getCachePath()
289 | {
290 | return $this->cachePath;
291 | }
292 |
293 | /**
294 | * Set cache path directory
295 | *
296 | * @param string $cachePath Full path of cache files
297 | *
298 | * @return Config for chaining
299 | * @throws PageCacheException
300 | */
301 | public function setCachePath($cachePath)
302 | {
303 | if (empty($cachePath) || !is_writable($cachePath)) {
304 | throw new PageCacheException(__METHOD__.' - Cache path not writable: '.$cachePath);
305 | }
306 | if (substr($cachePath, -1) !== '/') {
307 | throw new PageCacheException(__METHOD__.' - / trailing slash is expected at the end of cache_path.');
308 | }
309 | $this->cachePath = $cachePath;
310 | return $this;
311 | }
312 |
313 | /**
314 | * @return bool
315 | */
316 | public function isUseSession()
317 | {
318 | return $this->useSession;
319 | }
320 |
321 | /**
322 | * Enable or disable Session handeling in cache files
323 | *
324 | * @param bool $useSession
325 | * @return Config for chaining
326 | */
327 | public function setUseSession($useSession)
328 | {
329 | $this->useSession = (bool)$useSession;
330 | if ($useSession) {
331 | SessionHandler::enable();
332 | } else {
333 | SessionHandler::disable();
334 | }
335 | return $this;
336 | }
337 |
338 | /**
339 | * @return array
340 | */
341 | public function getSessionExcludeKeys()
342 | {
343 | return $this->sessionExcludeKeys;
344 | }
345 |
346 | /**
347 | * @param array $sessionExcludeKeys
348 | * @return Config for chaining
349 | */
350 | public function setSessionExcludeKeys(array $sessionExcludeKeys)
351 | {
352 | $this->sessionExcludeKeys = $sessionExcludeKeys;
353 | SessionHandler::excludeKeys($sessionExcludeKeys);
354 | return $this;
355 | }
356 |
357 | /**
358 | * @return false|int
359 | */
360 | public function getFileLock()
361 | {
362 | return $this->fileLock;
363 | }
364 |
365 | /**
366 | * Set file_lock value
367 | *
368 | * @param false|int $fileLock
369 | * @return Config for chaining
370 | */
371 | public function setFileLock($fileLock)
372 | {
373 | $this->fileLock = $fileLock;
374 | return $this;
375 | }
376 |
377 | /**
378 | * @return bool
379 | */
380 | public function isSendHeaders()
381 | {
382 | return $this->sendHeaders;
383 | }
384 |
385 | /**
386 | * Enable or disable headers.
387 | * @param bool $sendHeaders
388 | * @return Config for chaining
389 | */
390 | public function setSendHeaders($sendHeaders)
391 | {
392 | $this->sendHeaders = (bool)$sendHeaders;
393 | return $this;
394 | }
395 |
396 | /**
397 | * @return bool
398 | */
399 | public function isForwardHeaders()
400 | {
401 | return $this->forwardHeaders;
402 | }
403 |
404 | /**
405 | * Enable or disable HTTP headers forwarding.
406 | * Works only if headers are enabled via PageCache::enableHeaders() method or via config
407 | *
408 | * @param bool $forwardHeaders True to enable, false to disable
409 | * @return Config for chaining
410 | */
411 | public function setForwardHeaders($forwardHeaders)
412 | {
413 | $this->forwardHeaders = (bool)$forwardHeaders;
414 | return $this;
415 | }
416 |
417 | /**
418 | * Enable or disable Dry Run Mode. Output will not be changed, everything else will function.
419 | *
420 | * @param bool $dryRunMode
421 | * @return Config for chaining
422 | */
423 | public function setDryRunMode($dryRunMode)
424 | {
425 | $this->dryRunMode = (bool)$dryRunMode;
426 | return $this;
427 | }
428 |
429 | /**
430 | * Whether Dry run mode is enabled
431 | *
432 | * @return bool
433 | */
434 | public function isDryRunMode()
435 | {
436 | return $this->dryRunMode;
437 | }
438 |
439 | /**
440 | * Checks if the parent directory of the file path provided exists
441 | *
442 | * @param string $file_path File Path
443 | * @return bool true if exists, false if not
444 | */
445 | private function isParentDirectoryExists($file_path)
446 | {
447 | $dir = dirname($file_path);
448 | return file_exists($dir);
449 | }
450 | }
451 |
--------------------------------------------------------------------------------
/tests/Integration/www/5.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test 3
6 |
7 |
8 | Lorem ipsum dolor sit amet, eam viderer molestiae an. No illum dictas singulis pri, vidit simul aliquando sed ex. At eos prima torquatos, agam nobis minimum sea in. Ei mentitum omittantur pri? Laudem mediocrem constituto pri no, eos id animal fabulas, mutat viderer detracto vim ne? Vide reformidans eu eos, no quas error vis, est etiam nullam sensibus cu.
9 |
10 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
11 |
12 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
13 |
14 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
15 |
16 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
17 |
18 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
19 |
20 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
21 |
22 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
23 |
24 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
25 |
26 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
27 |
28 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
29 |
30 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
31 |
32 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
33 |
34 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
35 |
36 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
37 |
38 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
39 |
40 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
41 |
42 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
43 |
44 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
45 |
46 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
47 |
48 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
49 |
50 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
51 |
52 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
53 |
54 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
55 |
56 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
57 |
58 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
59 |
60 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
61 |
62 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
63 |
64 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
65 |
66 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
67 |
68 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
69 |
70 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
71 |
72 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
73 |
74 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
75 |
76 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
77 |
78 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
79 |
80 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
81 |
82 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
83 |
84 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
85 |
86 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
87 |
88 | An nam alterum convenire! No ius detraxit atomorum sapientem, probo option mei ut. Eum at putant iisque eligendi, augue nominati molestiae nec ei. Natum tritani vulputate sit et, ne adhuc verear oblique eos! Cu vocent scripta has, congue facete temporibus ne vix!
89 |
90 | Pri et aeque eloquentiam, in vel latine explicari, cu elit mediocrem tincidunt vim. Eos harum tempor ex! Ne mei vidisse vocibus. Vero ubique sed ad.
91 |
92 | Vis omnis pertinax disputando ut, id fuisset legendos iudicabit usu, ei nec detracto pertinax assentior. No dolore qualisque consequuntur qui, choro disputando an qui, impedit mandamus eu mel. Mei iusto dolore no, at legimus legendos oportere nam. An elit modus cum.
93 |
94 | Pri posse congue at. Ne sea habemus tractatos, probo brute an qui. Placerat urbanitas quo ex, te accusam dolores ponderum sea, an vim feugait referrentur. Eu brute salutatus eos, ea nostrud consetetur posidonium sea, splendide hendrerit no sea? Atqui officiis signiferumque ne pri, quodsi deleniti argumentum sit an. Liber patrioque ut usu.
95 |
96 | Aperiri tibique ad eos. Est ullum laudem ei, cu dicant inimicus nec! Ne labores aliquando vis, in sit consectetuer reprehendunt. Ex quo animal feugiat, ei sit aliquid feugait laboramus. No vim corpora lobortis, an has augue dicam. Te posse voluptua ullamcorper pri? Vis te causae diceret facilis.
97 |
98 | An has utamur utroque facilisi, eam tractatos inciderint ei. Vis cibo vidit at. Cu quidam debitis officiis mei, in eos sensibus oportere. Eos in modo omittam perfecto, ut aeque animal voluptatibus eam, nobis inermis moderatius in vix! No has altera vivendo instructior. Vel ei sint prompta facilis, eos id alia consequat? Eos cu alia vidit eleifend!
99 |
100 | Eam cu falli melius labores, ius ut rebum tempor singulis, nec id laudem eruditi accommodare. Et vel novum euismod feugait! Vix et iusto bonorum eloquentiam, duo et simul singulis postulant. No est graeci delicatissimi? An pri fugit viris clita. At sit facer inermis.
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/Storage/FileSystem/FileSystemCacheAdapter.php:
--------------------------------------------------------------------------------
1 |
6 | * @package PageCache
7 | * @copyright 2017
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | namespace PageCache\Storage\FileSystem;
14 |
15 | use DateInterval;
16 | use PageCache\Storage\CacheAdapterException;
17 | use PageCache\Storage\InvalidArgumentException;
18 | use Psr\SimpleCache\CacheInterface;
19 |
20 | /**
21 | * Class FileSystemPsrCacheAdapter
22 | *
23 | * @package PageCache
24 | */
25 | class FileSystemCacheAdapter implements CacheInterface
26 | {
27 | const DEFAULT_TTL = 3600;
28 |
29 | /**
30 | * @var string
31 | */
32 | protected $path;
33 |
34 | /**
35 | * @var int
36 | */
37 | protected $fileLock;
38 |
39 | /**
40 | * @var int
41 | */
42 | protected $minFileSize;
43 |
44 | /**
45 | * @var \PageCache\Storage\FileSystem\HashDirectory
46 | */
47 | protected $hashDirectory;
48 |
49 | /**
50 | * FileSystemPsrCacheAdapter constructor.
51 | *
52 | * @param string $path
53 | * @param int $fileLock
54 | * @param int $minFileSize
55 | * @throws \PageCache\PageCacheException
56 | */
57 | public function __construct($path, $fileLock, $minFileSize)
58 | {
59 | $this->path = $path;
60 | $this->fileLock = $fileLock;
61 | $this->minFileSize = $minFileSize;
62 |
63 | $this->hashDirectory = new HashDirectory($this->path);
64 | }
65 |
66 | /**
67 | * Determines whether an item is present in the cache.
68 | *
69 | * NOTE: It is recommended that has() is only to be used for cache warming type purposes
70 | * and not to be used within your live applications operations for get/set, as this method
71 | * is subject to a race condition where your has() will return true and immediately after,
72 | * another script can remove it making the state of your app out of date.
73 | *
74 | * @param string $key The cache item key.
75 | *
76 | * @return bool
77 | *
78 | * @throws \Psr\SimpleCache\InvalidArgumentException
79 | * MUST be thrown if the $key string is not a legal value.
80 | * @throws \PageCache\PageCacheException
81 | */
82 | public function has($key)
83 | {
84 | $path = $this->getKeyPath($key);
85 |
86 | return $this->isValidFile($path);
87 | }
88 |
89 | /**
90 | * Fetches a value from the cache.
91 | *
92 | * @param string $key The unique key of this item in the cache.
93 | * @param mixed $default Default value to return if the key does not exist.
94 | *
95 | * @return mixed The value of the item from the cache, or $default in case of cache miss.
96 | *
97 | * @throws \Psr\SimpleCache\InvalidArgumentException
98 | * MUST be thrown if the $key string is not a legal value.
99 | */
100 | public function get($key, $default = null)
101 | {
102 | $path = $this->getKeyPath($key);
103 |
104 | if (!$this->isValidFile($path)) {
105 | return $default;
106 | }
107 |
108 | /** @noinspection PhpIncludeInspection */
109 | $data = include $path;
110 |
111 | if (!$data || !\is_array($data) || !isset($data['ttl'], $data['item'])) {
112 | // Prevent errors on broken files, they would be overwritten later by set() call in client logic
113 | return $default;
114 | }
115 |
116 | $ttl = (int)$data['ttl'];
117 |
118 | // 0 TTL means expired (allow negative values like -1)
119 | if ($ttl < 1) {
120 | // Do not delete cache files, they would be overwritten later by set() call in client logic
121 | return $default;
122 | }
123 |
124 | // Process item TTL
125 | if (filemtime($path) + $ttl < time()) {
126 | // Do not delete cache files, they would be overwritten later by set() call in client logic
127 | return $default;
128 | }
129 |
130 | return $data['item'];
131 | }
132 |
133 | /**
134 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
135 | *
136 | * @param string $key The key of the item to store.
137 | * @param mixed $value The value of the item to store, must be serializable.
138 | * @param null|int|DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
139 | * the driver supports TTL then the library may set a default value
140 | * for it or let the driver take care of that.
141 | *
142 | * @return bool True on success
143 | *
144 | * @throws \Psr\SimpleCache\InvalidArgumentException If the $key string is not a legal value.
145 | * @throws \PageCache\Storage\CacheAdapterException
146 | * @throws \Exception
147 | */
148 | public function set($key, $value, $ttl = null)
149 | {
150 | $ttl = $this->normalizeTtl($ttl);
151 |
152 | if ($ttl < 1) {
153 | // Item marked as expired, delete it
154 | return $this->delete($key);
155 | }
156 |
157 | $path = $this->getKeyPath($key);
158 | $data = $this->prepareItemData($value, $ttl);
159 | $storage = new FileSystem($data);
160 |
161 | try {
162 | $storage->setFileLock($this->fileLock);
163 | $storage->setFilePath($path);
164 | } catch (\Throwable $t) {
165 | throw new CacheAdapterException(__METHOD__ . ' FileSystem Exception', 0, $t);
166 | } catch (\Exception $e) {
167 | throw new CacheAdapterException(__METHOD__ . ' FileSystem Exception', 0, $e);
168 | }
169 |
170 | $result = $storage->writeAttempt();
171 |
172 | if ($result !== FileSystem::OK) {
173 | throw new CacheAdapterException(__METHOD__ . ' FileSystem writeAttempt not an OK result: ' . $result);
174 | }
175 |
176 | return true;
177 | }
178 |
179 | /**
180 | * Delete an item from the cache by its unique key.
181 | *
182 | * @param string $key The unique cache key of the item to delete.
183 | *
184 | * @return bool True if the item was successfully removed. False if there was an error.
185 | *
186 | * @throws \Psr\SimpleCache\InvalidArgumentException
187 | * MUST be thrown if the $key string is not a legal value.
188 | */
189 | public function delete($key)
190 | {
191 | $path = $this->getKeyPath($key);
192 |
193 | /**
194 | * If init() wasn't called on this page before, there won't be any cache saved, so we check with file_exists.
195 | */
196 | if (!\file_exists($path)) {
197 | // Probably the file already deleted in another thread, PSR requires return value to be "true" in this case
198 | return true;
199 | }
200 |
201 | if (is_file($path)) {
202 | return unlink($path);
203 | }
204 |
205 | return false;
206 | }
207 |
208 | /**
209 | * Wipes clean the entire cache's keys.
210 | *
211 | * @return bool True on success and false on failure.
212 | */
213 | public function clear()
214 | {
215 | return $this->hashDirectory->clearDirectory($this->path);
216 | }
217 |
218 | /**
219 | * Obtains multiple cache items by their unique keys.
220 | *
221 | * @param iterable $keys A list of keys that can obtained in a single operation.
222 | * @param mixed $default Default value to return for keys that do not exist.
223 | *
224 | * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
225 | *
226 | * @throws \Psr\SimpleCache\InvalidArgumentException
227 | * MUST be thrown if $keys is neither an array nor a Traversable,
228 | * or if any of the $keys are not a legal value.
229 | */
230 | public function getMultiple($keys, $default = null)
231 | {
232 | if (!$keys || (!\is_array($keys) && !$keys instanceof \Traversable)) {
233 | throw new InvalidArgumentException('Cache keys must be an array or Traversable');
234 | }
235 |
236 | $values = [];
237 |
238 | foreach ($keys as $key) {
239 | $values[$key] = $this->get($key, $default);
240 | }
241 |
242 | return $values;
243 | }
244 |
245 | /**
246 | * Persists a set of key => value pairs in the cache, with an optional TTL.
247 | *
248 | * @param iterable $values A list of key => value pairs for a multiple-set operation.
249 | * @param null|int|DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
250 | * the driver supports TTL then the library may set a default value
251 | * for it or let the driver take care of that.
252 | *
253 | * @return bool True on success and false on failure.
254 | *
255 | * @throws \PageCache\Storage\CacheAdapterException
256 | * @throws \Psr\SimpleCache\InvalidArgumentException
257 | * MUST be thrown if $values is neither an array nor a Traversable,
258 | * or if any of the $values are not a legal value.
259 | */
260 | public function setMultiple($values, $ttl = null)
261 | {
262 | if (!$values || (!\is_array($values) && !$values instanceof \Traversable)) {
263 | throw new InvalidArgumentException('Cache values must be an array or Traversable');
264 | }
265 |
266 | $result = true;
267 |
268 | foreach ($values as $key => $value) {
269 | if (\is_int($key)) {
270 | $key = (string)$key;
271 | }
272 |
273 | $result = $this->set($key, $value, $ttl) && $result;
274 | }
275 |
276 | return $result;
277 | }
278 |
279 | /**
280 | * Deletes multiple cache items in a single operation.
281 | *
282 | * @param iterable $keys A list of string-based keys to be deleted.
283 | *
284 | * @return bool True if the items were successfully removed. False if there was an error.
285 | *
286 | * @throws \Psr\SimpleCache\InvalidArgumentException
287 | * MUST be thrown if $keys is neither an array nor a Traversable,
288 | * or if any of the $keys are not a legal value.
289 | */
290 | public function deleteMultiple($keys)
291 | {
292 | if (!\is_array($keys) && !$keys instanceof \Traversable) {
293 | throw new InvalidArgumentException('Cache keys must be an array or Traversable');
294 | }
295 |
296 | $result = true;
297 |
298 | foreach ($keys as $key) {
299 | $result = $this->delete($key) && $result;
300 | }
301 |
302 | return $result;
303 | }
304 |
305 | /**
306 | * Check if $path is a valid cache file
307 | *
308 | * @param string $path Cache file path
309 | *
310 | * @return bool True if valid file, false otherwise
311 | */
312 | private function isValidFile($path)
313 | {
314 | return (file_exists($path) && filesize($path) >= $this->minFileSize);
315 | }
316 |
317 | /**
318 | * Format $data value to be used in cache
319 | *
320 | * @param mixed $itemData
321 | *
322 | * @param int|\DateInterval|null $ttl
323 | *
324 | * @return string
325 | * @throws \PageCache\Storage\InvalidArgumentException
326 | */
327 | private function prepareItemData($itemData, $ttl = null)
328 | {
329 | $ttl = $this->normalizeTtl($ttl);
330 |
331 | // Integrate TTL into data (it will be checked later in the get() method)
332 | $fileData = [
333 | 'ttl' => $ttl,
334 | 'item' => $itemData,
335 | ];
336 |
337 | return 'validateKey($key);
352 | $file = sha1($key);
353 | $this->hashDirectory->processFile($file);
354 | return $this->hashDirectory->getFileStoragePath();
355 | }
356 |
357 | /**
358 | * Check the key is valid and throw an exception if not.
359 | * Accepted key characters: letters, numbers, underscores, dashes and dots
360 | *
361 | * @param string|mixed $key
362 | *
363 | * @throws \Psr\SimpleCache\InvalidArgumentException
364 | */
365 | private function validateKey($key)
366 | {
367 | if (!\is_string($key)) {
368 | throw new InvalidArgumentException(
369 | sprintf(
370 | 'Cache key must be string, "%s" given',
371 | is_object($key) ? get_class($key) : gettype($key)
372 | )
373 | );
374 | }
375 |
376 | if ($key === '') {
377 | throw new InvalidArgumentException('Cache key length must be greater than zero');
378 | }
379 |
380 | if (strpbrk($key, '{}()/\@:') !== false) {
381 | throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key));
382 | }
383 |
384 | if (preg_match('/[^A-Za-z0-9_\.\-]+/', $key)) {
385 | throw new InvalidArgumentException('Invalid PSR SimpleCache key: ' . $key .
386 | ', must contain letters, numbers, underscores, dashes and dots only');
387 | }
388 | }
389 |
390 | /**
391 | * Convert TTL to seconds
392 | *
393 | * @param int|DateInterval|null $ttl
394 | *
395 | * @return int
396 | * @throws \PageCache\Storage\InvalidArgumentException
397 | */
398 | private function normalizeTtl($ttl)
399 | {
400 | if ($ttl === null) {
401 | return self::DEFAULT_TTL; // Default TTL is one hour
402 | }
403 |
404 | if (\is_int($ttl)) {
405 | return $ttl;
406 | }
407 |
408 | if ($ttl instanceof DateInterval) {
409 | $currentDateTime = new \DateTimeImmutable();
410 | $ttlDateTime = $currentDateTime->add($ttl);
411 |
412 | return $ttlDateTime->getTimestamp() - $currentDateTime->getTimestamp();
413 | }
414 |
415 | throw new InvalidArgumentException('Invalid TTL: ' . print_r($ttl, true));
416 | }
417 | }
418 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/mmamedov/page-cache) [](https://packagist.org/packages/mmamedov/page-cache) [](https://packagist.org/packages/mmamedov/page-cache)
2 |
3 | Full-page PHP Caching library
4 | ----
5 | PageCache is a lightweight PHP library for full page cache, works out of the box with zero configuration.
6 | Use it when you need a simple yet powerful file based PHP caching solution. Page caching for mobile devices is built-in.
7 |
8 | Install PHP PageCache and start caching your PHP's browser output code using Composer:
9 | ```
10 | composer require mmamedov/page-cache
11 | ```
12 | Or manually add to your composer.json file:
13 | ```json
14 | {
15 | "require": {
16 | "mmamedov/page-cache": "^2.0"
17 | }
18 | }
19 | ```
20 | Once PageCache is installed, include Composer's autoload.php file, or implement your own autoloader.
21 | Composer autoloader is recommended.
22 |
23 | Do not use `master` branch, as it may contain unstable code, use versioned branches instead.
24 |
25 | #### Upgrading to to v2.*
26 | Version 2.0 is not backwards compatible with versions starting with v1.0. Version 2.0 introduces new features and code
27 | was refactored to enable us deliver more features.
28 |
29 | When upgrading to version 2.0, please note the followings:
30 | - PHP requirements >= 5.6.
31 | - Your config file must be like this `return [...]` and not `$config = array(...);` like in previous version.
32 | - Config `expiration` setting was renamed to `cache_expiration_in_seconds`
33 | - Use `try/catch` to ensure proper page load in case of PageCache error.
34 |
35 | If you find any other notable incompatibilities please let us know we will include them here.
36 |
37 | No Database calls
38 | ----
39 | Once page is cached, there are no more database calls needed! Even if your page contains many database calls and complex logic,
40 | it will be executed once and cached for period you specify. No more overload!
41 |
42 | This is a very efficient and simple method, to cache your most visited dynamic pages.
43 | [Tmawto.com](https://www.tmawto.com) website is built on PageCache, and is very fast.
44 |
45 | Why another PHP Caching class?
46 | ----
47 | Short answer - simplicity. If you want to include a couple lines of code on top of your dynamic PHP pages and be able
48 | to cache them fully, then PageCache is for you. No worrying about cache file name setup for each URL, no worries
49 | about your dynamically generated URL parameters and changing URLs. PageCache detects those changed and caches accordingly.
50 |
51 | PageCache also detects $_SESSION changes and caches those pages correctly. This is useful if you have user
52 | authentication enabled on your site, and page contents change per user login while URL remains the same.
53 |
54 | Lots of caching solutions focus on keyword-based approach, where you need to setup a keyword for your
55 | content (be it a full page cache, or a variable, etc.). There are great packages for keyword based approach.
56 | One could also use a more complex solution like a cache proxy, Varnish.
57 | PageCache on the other hand is a simple full page only caching solution, that does exactly what its name says -
58 | generates page cache in PHP.
59 |
60 | How PageCache works
61 | ----
62 | PageCache doesn't ask you for a keyword, it automatically generates them based on Strategies implementing StrategyInterface.
63 | You can define your own naming strategy, based on your application needs.
64 | Strategy class is responsible for generating a unique key for current request, key becomes file name for the
65 | cache file (if FileSystem storage is used).
66 |
67 | ```php
68 | config()
74 | ->setCachePath('/your/path/')
75 | ->setEnableLog(true);
76 | $cache->init();
77 | } catch (\Exception $e) {
78 | // Log PageCache error or simply do nothing.
79 | // In case of PageCache error, page will load normally, without cache.
80 | }
81 |
82 | //rest of your PHP page code, everything below will be cached
83 | ```
84 |
85 | Using PSR-16 compatible cache adapter
86 | ----
87 |
88 | PageCache is built on top of [PSR-16 SimpleCache](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) "key"=>"value" storage and has default file-based cache adapter in class `FileSystemPsrCacheAdapter`. This implementation is fast and uses `var_export` + `include` internally so every cache file is also automatically cached in OpCache or APC (if you have configured opcode caching for your project). This is perfect choice for single-server applications but if you have multi-server application you should want to share page cache content between servers. In this case you may use any PSR-16 compatible cache adapter for network-based "key"=>"value" storage like Memcached or Redis:
89 |
90 | ```php
91 | 'tcp',
99 | 'host' => 'localhost',
100 | 'port' => 6379
101 | );
102 |
103 | $redis = new Redis(new Client($config));
104 |
105 | $cache = new PageCache\PageCache();
106 | $cache->setCacheAdapter($redis);
107 | $cache->init();
108 |
109 | //rest of your PHP page code, everything below will be cached
110 | ```
111 | PageCache also has "Dry Run mode" option, is is turned off by default.
112 | Dry Run mode enables all functionality except that users won't be getting the cached content, they will be getting live content.
113 | No cache headers and no cached content will be send, if Dry Run mode is enabled.
114 | But cache will be stored in its as if it would run with Dry mode disabled.
115 | This mode lets PageCache dry run, useful if you want to test, debug or to see how your cache content populates.
116 |
117 | For more examples see code inside [PageCache examples](examples/) directory.
118 |
119 | For those who wonder, cache is saved into path specified in config file or using API, inside directories based on file hash.
120 | Based on the hash of the filename, 2 subdirectories will be created (if not created already),
121 | this is to avoid numerous files in a single cache directory.
122 |
123 | Caching Strategies
124 | ------------------
125 | PageCache uses various strategies to differentiate among separate versions of the same page.
126 |
127 | All PageCache Strategies support sessions. See PageCache [cache page with sessions](examples/demo-session-support.php) example.
128 |
129 | `DefaultStrategy()` is the default behaviour of PageCache. It caches pages and generated cache filenames using this PHP code:
130 | ```php
131 | md5($_SERVER['REQUEST_URI'] . $_SERVER['SCRIPT_NAME'] . $_SERVER['QUERY_STRING'] . $session_str)`
132 | ```
133 | String `$session_str` is a serialized $_SESSION variable, with some keys ignored/or not based on whether session
134 | support is enabled or if sessionExclude() was called.
135 |
136 | You could create your own naming strategy and pass it to PageCache:
137 | ```php
138 | $cache = new PageCache\PageCache();
139 | $cache->setStrategy(new MyOwnStrategy());
140 | ```
141 |
142 | Included with the PageCache is the `MobileStrategy()` based on [Mobile_Detect](https://github.com/serbanghita/Mobile-Detect).
143 | It is useful if you are serving the same URL differently across devices.
144 | See [cache_mobiledetect.php PageCache example](examples/cache_mobiledetect.php) file for demo using MobileDetect._
145 |
146 | You can define your own naming strategy based on the needs of your application.
147 |
148 | Config file
149 | ----
150 | Although not required, configuration file can be specified during PageCache initialization for system wide caching properties
151 |
152 | ```php
153 | // Optional system-wide cache config
154 | use PageCache\PageCache;
155 | $config_file_ = __DIR__.'/config.php';
156 | $cache = new PageCache($config_file_);
157 | // You can overwrite or get configuration parameters like this:
158 | $cache->config()->getFileLock();
159 | $cache->config()->setUseSession(true);
160 | ```
161 |
162 | All available configuration options are documented in [config](examples/config.php) file. Be sure to check it.
163 |
164 | API - PageCache access methods
165 | ------------------------------------
166 | The following are public methods of PageCache class that you could call from your application.
167 | This is not a complete list. Check out examples and source code for more.
168 |
169 | | Method | Description |
170 | | --- | --- |
171 | | init():void | initiate cache, this should be your last method to call on PageCache object.|
172 | | setStrategy(\PageCache\StrategyInterface):void | set cache file strategy. Built-in strategies are DefaultStrategy() and MobileStrategy(). Define your own if needed.|
173 | | setCacheAdapter(CacheInterface) | Set cache adapter. |
174 | | getCurrentKey() : string | Get cache key value for this page.|
175 | | getStrategy() : Strategy | Get set Strategy object. |
176 | | setStrategy(Strategy) : void | Set Strategy object. |
177 | | clearPageCache():void | Clear cache for current page, if this page was cached before. |
178 | | getPageCache():string | Return current page cache as a string or false on error, if this page was cached before.|
179 | | isCached():bool | Checks if current page is in cache, true if exists false if not cached yet.|
180 | | setLogger(\Psr\Log\LoggerInterface):void | Set PSR-3 compliant logger.|
181 | | clearAllCache() | Removes all content from cache storage.|
182 | | destroy() : void | Destroy PageCache instance, reset SessionHandler |
183 | | config() : Config | Get Config element. Setting and getting configuration values is done via this method. |
184 |
185 | Check source code for more available methods.
186 |
187 | Caching pages using Sessions (i.e. User Login enabled applications)
188 | -------------------------------------------------------------------
189 | PageCache makes it simple to maintain a full page cache in PHP while using sessions.
190 |
191 | One example for using session feature could be when you need to incorporate logged in users into your applications.
192 | In that case URL might remain same (if you rely on sessions to log users in), while content of the page will be different for each logged in user.
193 |
194 | For PageCache to be aware of your $_SESSION, in config file or in your PHP file you must enable session support.
195 | In your PHP file, before calling `init()` call `$cache->config()->setUseSession(true)`. That's it!
196 | Now your session pages will be cached seperately for your different session values.
197 |
198 | Another handy method is `config()->setSessionExcludeKeys()`. Check out [Session exclude keys](examples/demo-session-exclude-keys.php)
199 | example for code.
200 |
201 | When to use `config()->setSessionExcludeKeys()`: For example let's assume that your application changes $_SESSION['count'] variable,
202 | but that doesn't reflect on the page content.
203 | Exclude this variable, otherwise PageCache will generate seperate cache files for each value of $_SESSION['count]
204 | session variable. To exclude 'count' session variable:
205 | ```php
206 | // ...
207 | $cache->config()->setUseSession(true);
208 | $cache->config()->setSessionExcludeKeys(array('count'));
209 | // ...
210 | $cache->init();
211 | ```
212 |
213 | HTTP Headers
214 | ----------------------------------
215 | PageCache can send cache related HTTP Headers. This helps browsers to load your pages faster and makes your
216 | application SEO aware. Search engines will read this headers and know whether your page was modified or expired.
217 |
218 | By default, HTTP headers are disabled. You can enable appropriate headers to be sent with the response to the client.
219 | This is done by calling `config()->setSendHeaders(true)`, prior to `init()`, or `send_headers = true` in config file.
220 | Although disabled by default, we encourage you to use this feature.
221 | Test on your local application before deploying it to your live version.
222 |
223 | When HTTP headers are enabled, PageCache will attempt to send the following HTTP headers automatically with each response:
224 | - `Last-Modified`
225 | - `Expires`
226 | - `ETag`
227 | - `HTTP/1.1 304 Not Modified`
228 |
229 | PageCache will attempt to send `HTTP/1.1 304 Not Modified` header along with cached content. When this header is sent, content
230 | is omitted from the response. This makes your application super fast. Browser is responsible for fetching a locally
231 | cached version when this header is present.
232 |
233 | There is also `forward_headers` option in config, or `config()->setForwardHeaders(true)` which allows PageCache to fetch
234 | values of these HTTP headers from the app response and store them into cache item so headers would be cached too.
235 | This approach is useful if your app has fine-grained control.
236 |
237 | Check out [HTTP Headers demo](examples/demo-headers.php) for code.
238 |
239 | Cache Stampede (dog-piling) protection
240 | -----------------------
241 | Under a heavy load or when multiple calls to the web server are made to the same URL when it has been expired,
242 | there might occur a condition where system might become unresponsive or when all clients will try to regenerate
243 | cache of the same page. This effect is called [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede).
244 |
245 | PageCache uses 2 strategies to defeat cache stampede, and even when thousands request are made to the same page system
246 | continues to function normally.
247 |
248 | ###### File Lock mechanism
249 | By default PageCache sets file lock to `LOCK_EX | LOCK_NB`, which means that only 1 process will be able to write into
250 | a cache file. During that write, all other processes will read old version of the file. This ensures that no user is
251 | presented with a blank page, and that all users receive a cached version of the URL - without exceptions.
252 |
253 | This eliminates possibility of cache stampede, since there can only be a single write to a file, even though thousands
254 | of clients have reached a page when it has expired (one client generates new cache and sees the new cached version,
255 | the rest will receive old version). Number of users hitting page when it has expired is also reduced by random
256 | early and late expiration.
257 |
258 | ###### Random early and late expiration
259 | Using random logarithmic calculations, producing sometimes negative and at times positive results, cache expiration
260 | for each client hitting the same URL at the same time is going to be different. While some clients might still get
261 | the same cache expiration value as others, overall distribution of cache expiration value among clients is random.
262 |
263 | Making pages expire randomly at most 6 seconds before or after actual cache expiration of the page, ensures that we have
264 | less clients trying to regenerate cache content. File locking already takes care of simultaneous writes of cache page,
265 | random expiration takes it a step further minimizing the number of such required attempts.
266 |
267 | To give an example, consider you have set expiration to 10 minutes `config()->setCacheExpirationInSeconds(600)`.
268 | Page will expire for some clients in 594 seconds, for some in 606 seconds, and for some in 600 seconds.
269 | Actual page expiration is going to be anywhere in between 594 and 606 seconds inclusive, this is randomly calculated.
270 | Expiration value is not an integer internally, so there are a lot more of random expiration values than you can think of.
271 |
272 |
273 | That's it!
274 |
275 | Check out [PageCache examples](examples/) folder for sample code.
276 |
277 |
--------------------------------------------------------------------------------
/src/PageCache.php:
--------------------------------------------------------------------------------
1 |
6 | * @copyright 2016
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace PageCache;
13 |
14 | use DateTime;
15 | use PageCache\Storage\CacheItem;
16 | use PageCache\Storage\CacheItemInterface;
17 | use PageCache\Storage\CacheItemStorage;
18 | use PageCache\Storage\FileSystem\FileSystemCacheAdapter;
19 | use PageCache\Strategy\DefaultStrategy;
20 | use Psr\Log\LoggerInterface;
21 | use Psr\Log\LogLevel;
22 | use Psr\SimpleCache\CacheInterface;
23 |
24 | /**
25 | * Class PageCache
26 | * PageCache is the main class, create PageCache object and call init() to start caching
27 | *
28 | * @package PageCache
29 | */
30 | class PageCache
31 | {
32 | /**
33 | * Make sure only one instance of PageCache is created
34 | *
35 | * @var bool
36 | */
37 | private static $ins = null;
38 |
39 | /**
40 | * @var HttpHeaders
41 | */
42 | protected $httpHeaders;
43 |
44 | /**
45 | * @var \PageCache\Storage\CacheItemStorage
46 | */
47 | private $itemStorage;
48 |
49 | /**
50 | * @var \Psr\SimpleCache\CacheInterface
51 | */
52 | private $cacheAdapter;
53 |
54 | /**
55 | * Cache data key based on Strategy
56 | *
57 | * @var string
58 | */
59 | private $currentKey;
60 |
61 | /**
62 | * @var \PageCache\Storage\CacheItemInterface
63 | */
64 | private $currentItem;
65 |
66 | /**
67 | * StrategyInterface for cache naming strategy
68 | *
69 | * @var StrategyInterface
70 | */
71 | private $strategy;
72 |
73 | /**
74 | * Configuration object
75 | *
76 | * @var Config
77 | */
78 | private $config;
79 |
80 | /**
81 | * When logging is enabled, defines a PSR logging library for logging exceptions and errors.
82 | *
83 | * @var \Psr\Log\LoggerInterface
84 | */
85 | private $logger = null;
86 |
87 | /**
88 | * PageCache constructor.
89 | *
90 | * @param null|string $config_file_path
91 | *
92 | * @throws \PageCache\PageCacheException
93 | */
94 | public function __construct($config_file_path = null)
95 | {
96 | if (PageCache::$ins) {
97 | throw new PageCacheException('PageCache already created.');
98 | }
99 |
100 | $this->config = new Config($config_file_path);
101 | $this->httpHeaders = new HttpHeaders();
102 | $this->strategy = new DefaultStrategy();
103 |
104 | // Disable Session by default
105 | if (!$this->config->isUseSession()) {
106 | SessionHandler::disable();
107 | }
108 |
109 | PageCache::$ins = true;
110 | }
111 |
112 | /**
113 | * Set Cache Adapter
114 | *
115 | * @param CacheInterface $cache Cache Interface compatible Adapter
116 | */
117 | public function setCacheAdapter(CacheInterface $cache)
118 | {
119 | $this->cacheAdapter = $cache;
120 | }
121 |
122 | /**
123 | * Initialize cache.
124 | * If you need to set configuration options, do so before calling this method.
125 | */
126 | public function init()
127 | {
128 | if ($this->config()->isDryRunMode()) {
129 | $this->log('Dry run mode is on. Live content is displayed, no cached output.');
130 | }
131 |
132 | $this->log(__METHOD__ . ' uri:' . $_SERVER['REQUEST_URI']
133 | . '; script:' . $_SERVER['SCRIPT_NAME'] . '; query:' . $_SERVER['QUERY_STRING'] . '.');
134 |
135 | // Search for valid cache item for current request
136 | $item = $this->getCurrentItem();
137 |
138 | // Display cache item if found (it might have empty contents, extra check is made inside displayItem)
139 | if ($item) {
140 | $this->displayItem($item);
141 | }
142 |
143 | $this->log(__METHOD__ . ' Cache item not found for hash ' . $this->getCurrentKey());
144 |
145 | /**
146 | * Cache item not found. Fetch content, save it, display it on this run.
147 | */
148 | ob_start([$this, 'storePageHandler']);
149 | }
150 |
151 | /**
152 | * Invoked by ob_start() to store page content
153 | *
154 | * @param string $content from ob_start
155 | * @return string Contents of the page
156 | * @throws \Psr\SimpleCache\InvalidArgumentException
157 | */
158 | private function storePageHandler($content)
159 | {
160 | try {
161 | return $this->storePageContent($content);
162 | } catch (\Throwable $t) {
163 | $this->log('', $t);
164 | } catch (\Exception $e) {
165 | $this->log('', $e);
166 | }
167 |
168 | return $content;
169 | }
170 |
171 | /**
172 | * Get Default Cache Adapter
173 | *
174 | * @return FileSystemCacheAdapter
175 | * @throws PageCacheException
176 | */
177 | private function getDefaultCacheAdapter()
178 | {
179 | return new FileSystemCacheAdapter(
180 | $this->config->getCachePath(),
181 | $this->config->getFileLock(),
182 | $this->config->getMinCacheFileSize()
183 | );
184 | }
185 |
186 | /**
187 | * Get current key
188 | *
189 | * @return string
190 | */
191 | public function getCurrentKey()
192 | {
193 | if (!$this->currentKey) {
194 | $this->currentKey = $this->getStrategy()->strategy();
195 | }
196 |
197 | return $this->currentKey;
198 | }
199 |
200 | /**
201 | * Display cache item, send headers if necessary.
202 | *
203 | * @param \PageCache\Storage\CacheItemInterface $item
204 | */
205 | private function displayItem(CacheItemInterface $item)
206 | {
207 | $isDryRun = $this->config()->isDryRunMode();
208 | $itemContent = $item->getContent();
209 |
210 | if (empty($itemContent)) {
211 | $this->log(__METHOD__ . ' Empty cache item found, skipping cache.', print_r($item, true));
212 | return;
213 | }
214 |
215 | $this->httpHeaders
216 | ->setLastModified($item->getLastModified())
217 | ->setExpires($item->getExpiresAt())
218 | ->setETag($item->getETagString());
219 |
220 | // Decide if sending headers from Config
221 | // Send headers (if not disabled) and process If-Modified-Since header
222 | if ($this->config->isSendHeaders()) {
223 | $logHeaders = sprintf(
224 | '%s: %s, %s: %s,%s: %s',
225 | HttpHeaders::HEADER_LAST_MODIFIED,
226 | $item->getLastModified()->format(HttpHeaders::DATE_FORMAT_CREATE),
227 | HttpHeaders::HEADER_EXPIRES,
228 | $item->getExpiresAt()->format(HttpHeaders::DATE_FORMAT_CREATE),
229 | HttpHeaders::HEADER_ETAG,
230 | $item->getETagString()
231 | );
232 | $this->log(__METHOD__ . ' uri:' . $_SERVER['REQUEST_URI']
233 | . '; Headers {' . $logHeaders . '}');
234 |
235 | if (!$isDryRun) {
236 | $this->httpHeaders->send();
237 | $this->log(
238 | __METHOD__ . ' Headers sent: ' . PHP_EOL . implode(
239 | PHP_EOL,
240 | $this->httpHeaders->getSentHeaders()
241 | )
242 | );
243 | }
244 |
245 | // Check if conditions for the If-Modified-Since header are met
246 | if ($this->httpHeaders->checkIfNotModified()) {
247 | if (!$isDryRun) {
248 | $this->httpHeaders->sendNotModifiedHeader();
249 | $this->log(__METHOD__ . ' 304 Not Modified header was set. Exiting w/o content.');
250 | $this->log(__METHOD__ . ' Response status: ' . http_response_code());
251 | exit();
252 | }
253 | $this->log(__METHOD__ . ' 304 Not Modified header was set. Not exiting w/o content - Dry Mode.');
254 | }
255 | }
256 |
257 | // Show cached content
258 | $this->log(__METHOD__ . ' Cache item found: ' . $this->getCurrentKey());
259 | $this->log(__METHOD__ . ' Response status: ' . http_response_code());
260 |
261 | if (!$isDryRun) {
262 | echo $itemContent;
263 | exit();
264 | }
265 | }
266 |
267 | /**
268 | * Write page to cache, and display it.
269 | * When write is unsuccessful, string content is returned.
270 | *
271 | * @param string $content String from ob_start
272 | *
273 | * @return string Page content
274 | * @throws \Psr\SimpleCache\InvalidArgumentException
275 | */
276 | private function storePageContent($content)
277 | {
278 | $key = $this->getCurrentKey();
279 | $item = new CacheItem($key);
280 |
281 | // When enabled we store original header values with the item
282 | $isHeadersForwardingEnabled = $this->config->isSendHeaders() && $this->config->isForwardHeaders();
283 |
284 | $this->log(__METHOD__ . ' Header forwarding is ' . ($isHeadersForwardingEnabled ? 'enabled' : 'disabled'));
285 |
286 | $expiresAt = $isHeadersForwardingEnabled
287 | ? $this->httpHeaders->detectResponseExpires()
288 | : null;
289 |
290 | $lastModified = $isHeadersForwardingEnabled
291 | ? $this->httpHeaders->detectResponseLastModified()
292 | : null;
293 |
294 | $eTagString = $isHeadersForwardingEnabled
295 | ? $this->httpHeaders->detectResponseETagString()
296 | : null;
297 |
298 | // Store original Expires header time if set
299 | if (!empty($expiresAt)) {
300 | $item->setExpiresAt($expiresAt);
301 | }
302 |
303 | // Set current time as last modified if none provided
304 | if (empty($lastModified)) {
305 | $lastModified = new DateTime();
306 | }
307 |
308 | /**
309 | * Set ETag from from last modified time if none provided
310 | *
311 | * @link https://github.com/mmamedov/page-cache/issues/1#issuecomment-273875002
312 | */
313 | if (empty($eTagString)) {
314 | $eTagString = md5($lastModified->getTimestamp());
315 | }
316 |
317 | $item
318 | ->setContent($content)
319 | ->setLastModified($lastModified)
320 | ->setETagString($eTagString);
321 |
322 | $this->getItemStorage()->set($item);
323 |
324 | $logHeaders = sprintf(
325 | '%s: %s, %s: %s',
326 | HttpHeaders::HEADER_LAST_MODIFIED,
327 | $item->getLastModified()->format(HttpHeaders::DATE_FORMAT_CREATE),
328 | HttpHeaders::HEADER_ETAG,
329 | $item->getETagString()
330 | );
331 |
332 | $this->log(__METHOD__ . ' Data stored for key ' . $key . '; Headers {' . $logHeaders . '}');
333 |
334 | // Return page content
335 | return $content;
336 | }
337 |
338 | /**
339 | * Get current Strategy.
340 | *
341 | * @return StrategyInterface
342 | */
343 | public function getStrategy()
344 | {
345 | return $this->strategy;
346 | }
347 |
348 | /**
349 | * Caching strategy - expected file name for this current page.
350 | *
351 | * @param StrategyInterface $strategy object for choosing appropriate cache file name
352 | */
353 | public function setStrategy(StrategyInterface $strategy)
354 | {
355 | $this->strategy = $strategy;
356 | }
357 |
358 | /**
359 | * Clear cache for provided page (or current page if none given)
360 | *
361 | * @param \PageCache\Storage\CacheItemInterface|null $item
362 | *
363 | * @throws \PageCache\PageCacheException
364 | * @throws \Psr\SimpleCache\InvalidArgumentException
365 | */
366 | public function clearPageCache(CacheItemInterface $item = null)
367 | {
368 | // Use current item if not provided in arguments
369 | if (is_null($item)) {
370 | $item = $this->getCurrentItem();
371 | }
372 |
373 | // Current item might have returned null
374 | if (is_null($item)) {
375 | throw new PageCacheException(__METHOD__ . ' Page cache item can not be detected');
376 | }
377 |
378 | $this->getItemStorage()->delete($item);
379 | }
380 |
381 | /**
382 | * Return current page cache as a string or false on error, if this page was cached before.
383 | *
384 | * @return string|false
385 | * @throws \Psr\SimpleCache\InvalidArgumentException
386 | */
387 | public function getPageCache()
388 | {
389 | $key = $this->getCurrentKey();
390 | $item = $this->getItemStorage()->get($key);
391 |
392 | return $item ? $item->getContent() : false;
393 | }
394 |
395 | /**
396 | * Checks if current page is in cache.
397 | *
398 | * @param \PageCache\Storage\CacheItemInterface|null $item
399 | *
400 | * @return bool Returns true if page has a valid cache file saved, false if not
401 | * @throws \Psr\SimpleCache\InvalidArgumentException
402 | */
403 | public function isCached(CacheItemInterface $item = null)
404 | {
405 | if (!$item) {
406 | $key = $this->getCurrentKey();
407 | $item = $this->getItemStorage()->get($key);
408 | }
409 |
410 | return $item ? true : false;
411 | }
412 |
413 | /**
414 | * Create and return cache storage instance.
415 | * If cache adapter was not set previously, sets Default cache adapter(FileSystem)
416 | *
417 | * @return \PageCache\Storage\CacheItemStorage
418 | */
419 | public function getItemStorage()
420 | {
421 | // Hack for weird initialization logic
422 | if (!$this->itemStorage) {
423 | $this->itemStorage = new CacheItemStorage(
424 | $this->cacheAdapter ?: $this->getDefaultCacheAdapter(),
425 | $this->config->getCacheExpirationInSeconds()
426 | );
427 | }
428 |
429 | return $this->itemStorage;
430 | }
431 |
432 | /**
433 | * Detect and return current page cached item (or null if current page was not cached yet)
434 | *
435 | * @return \PageCache\Storage\CacheItemInterface|null
436 | * @throws \Psr\SimpleCache\InvalidArgumentException
437 | */
438 | private function getCurrentItem()
439 | {
440 | // Hack for weird initialization logic
441 | if (!$this->currentItem) {
442 | $key = $this->getCurrentKey();
443 | $this->currentItem = $this->getItemStorage()->get($key);
444 | }
445 |
446 | return $this->currentItem;
447 | }
448 |
449 | /**
450 | * Set logger
451 | *
452 | * @param \Psr\Log\LoggerInterface $logger
453 | */
454 | public function setLogger(LoggerInterface $logger)
455 | {
456 | $this->logger = $logger;
457 | }
458 |
459 | /**
460 | * Delete everything from cache.
461 | */
462 | public function clearAllCache()
463 | {
464 | $this->log(__METHOD__ . ' Clearing all cache.');
465 | $this->getItemStorage()->clear();
466 | }
467 |
468 | /**
469 | * Log message using PSR Logger, or error_log.
470 | * Works only when logging was enabled and log file path was define.
471 | * Attempts to create default logger if no logger exists.
472 | *
473 | * @param string $msg Message
474 | * @param null|\Exception $exception
475 | *
476 | * @return bool true when logged, false when didn't log
477 | */
478 | private function log($msg, $exception = null)
479 | {
480 | if (!$this->config->isEnableLog()) {
481 | return false;
482 | }
483 |
484 | if (empty($this->logger) && !empty($this->config->getLogFilePath())) {
485 | $this->logger = new DefaultLogger($this->config->getLogFilePath());
486 | }
487 |
488 | if ($exception) {
489 | $this->logger->log(LogLevel::ALERT, $msg, ['exception' => $exception]);
490 | } else {
491 | $this->logger->log(LogLevel::DEBUG, $msg, []);
492 | }
493 |
494 | return true;
495 | }
496 |
497 | /**
498 | * Destroy PageCache instance
499 | */
500 | public static function destroy()
501 | {
502 | if (isset(PageCache::$ins)) {
503 | PageCache::$ins = null;
504 | SessionHandler::reset();
505 | }
506 | }
507 |
508 | /**
509 | * For changing config values
510 | *
511 | * @return Config
512 | */
513 | public function config()
514 | {
515 | return $this->config;
516 | }
517 | }
518 |
--------------------------------------------------------------------------------