├── 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 |

100 |
101 |
102 | 103 | 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 | 112 | SESSION IS ENABLED. Session contents:
';
118 |     print_r($_SESSION);
119 |     echo '
'; 120 | } else { 121 | echo '
No session data
'; 122 | } 123 | 124 | ?> 125 | 126 |
127 |

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 '
var_dump($_SESSION) call, before init(), 37 | so this content is not cached. 38 | Notice how with each button click below actual session value changes, but since it is excluded from tracking, 39 | same cache for different session values is generated: '; 40 | var_dump($_SESSION); 41 | echo 'var_dump ends. All below is cached.
'; 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 |

87 |
88 |
89 | Main 90 |
91 |
92 | 93 | 94 |
95 |
96 | 97 | 98 |
99 | 100 |

101 | PageCache call demo: $cache->config()->setSessionExcludeKeys(array('excl')); 102 |
103 | 104 | 105 | Stored under cache key:
' 114 | . ($cache->getCurrentKey()) 115 | . '
'; 116 | ?> 117 | 118 |
119 |

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 | [![Build Status](https://travis-ci.org/mmamedov/page-cache.svg?branch=master)](https://travis-ci.org/mmamedov/page-cache) [![Latest Stable Version](http://img.shields.io/packagist/v/mmamedov/page-cache.svg)](https://packagist.org/packages/mmamedov/page-cache) [![License](https://img.shields.io/packagist/l/mmamedov/page-cache.svg)](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 | --------------------------------------------------------------------------------