├── tests ├── tmp │ └── .gitkeep ├── travis │ └── php.ini ├── Backend │ ├── DefaultTimeoutTest.php │ ├── NullCacheTest.php │ ├── ChainedTest.php │ ├── KeyPrefixTest.php │ ├── FileTest.php │ └── FactoryTest.php ├── TransientTest.php ├── EagerTest.php ├── LazyTest.php └── BackendTest.php ├── src ├── Backend │ ├── Factory │ │ └── BackendNotFoundException.php │ ├── NullCache.php │ ├── Redis.php │ ├── DefaultTimeoutDecorated.php │ ├── ArrayCache.php │ ├── BaseDecorator.php │ ├── KeyPrefixDecorated.php │ ├── Chained.php │ ├── Factory.php │ └── File.php ├── Cache.php ├── Backend.php ├── Transient.php ├── Lazy.php └── Eager.php ├── composer.json ├── README.md ├── LICENSE └── composer.lock /tests/tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/travis/php.ini: -------------------------------------------------------------------------------- 1 | extension="redis.so" -------------------------------------------------------------------------------- /src/Backend/Factory/BackendNotFoundException.php: -------------------------------------------------------------------------------- 1 | =7.1", 26 | "doctrine/cache": "~1.10" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "~7.0" 30 | }, 31 | "config":{ 32 | "platform": { 33 | "php": "7.1.0" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Backend/DefaultTimeoutDecorated.php: -------------------------------------------------------------------------------- 1 | defaultTTL = $options['defaultTimeout']; 36 | parent::__construct($decorated); 37 | } 38 | 39 | public function doSave($id, $data, $lifeTime = 0) 40 | { 41 | return $this->decorated->doSave( $id, $data, $lifeTime ?: $this->defaultTTL); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Backend/ArrayCache.php: -------------------------------------------------------------------------------- 1 | doContains($id) ? $this->data[$id] : false; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function doContains($id) 29 | { 30 | // isset() is required for performance optimizations, to avoid unnecessary function calls to array_key_exists. 31 | return isset($this->data[$id]) || array_key_exists($id, $this->data); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function doSave($id, $data, $lifeTime = 0) 38 | { 39 | $this->data[$id] = $data; 40 | 41 | return true; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function doDelete($id) 48 | { 49 | unset($this->data[$id]); 50 | 51 | return true; 52 | } 53 | 54 | public function doFlush() 55 | { 56 | $this->data = array(); 57 | 58 | return true; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tests/Backend/DefaultTimeoutTest.php: -------------------------------------------------------------------------------- 1 | backendMock = $this->getMockBuilder(NullCache::class)->getMock(); 36 | 37 | $opts = ['defaultTimeout'=>$this->defaultTTl]; 38 | 39 | $this->cache = new DefaultTimeoutDecorated($this->backendMock, $opts); 40 | } 41 | 42 | public function test_doSave_shouldCallDecoratedWithDefaultTTL() 43 | { 44 | $this->backendMock 45 | ->expects($this->once()) 46 | ->method('doSave') 47 | ->with( $this->anything(), 48 | $this->anything(), 49 | $this->defaultTTl); 50 | 51 | $this->cache->doSave('randomid', 'anyvalue'); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Backend/BaseDecorator.php: -------------------------------------------------------------------------------- 1 | decorated = $decorated; 31 | } 32 | 33 | public function doFetch($id) 34 | { 35 | return $this->decorated->doFetch($id); 36 | } 37 | 38 | public function doContains($id) 39 | { 40 | return $this->decorated->doContains($id); 41 | } 42 | 43 | public function doSave($id, $data, $lifeTime = 0) 44 | { 45 | return $this->decorated->doSave( $id, $data, $lifeTime); 46 | } 47 | 48 | public function doDelete($id) 49 | { 50 | return $this->decorated->doDelete($id); 51 | } 52 | 53 | public function doFlush() 54 | { 55 | return $this->decorated->doFlush(); 56 | } 57 | 58 | public function getBackend() 59 | { 60 | return $this->decorated; 61 | } 62 | } -------------------------------------------------------------------------------- /tests/Backend/NullCacheTest.php: -------------------------------------------------------------------------------- 1 | cache = new NullCache(); 29 | $this->cache->doSave($this->cacheId, 'anyvalue', 100); 30 | } 31 | 32 | public function test_doSave_shouldAlwaysReturnTrue() 33 | { 34 | $this->assertTrue($this->cache->doSave('randomid', 'anyvalue', 100)); 35 | } 36 | 37 | public function test_doFetch_shouldAlwaysReturnFalse_EvenIfSomethingWasSet() 38 | { 39 | $this->assertFalse($this->cache->doFetch($this->cacheId)); 40 | } 41 | 42 | public function test_doContains_shouldAlwaysReturnFalse_EvenIfSomethingWasSet() 43 | { 44 | $this->assertFalse($this->cache->doContains($this->cacheId)); 45 | } 46 | 47 | public function test_doDelete_shouldAlwaysPretendItWorked_EvenIfNoSuchKeyExists() 48 | { 49 | $this->assertTrue($this->cache->doDelete('loremipsum')); 50 | } 51 | 52 | public function test_doFlush_shouldAlwaysPretendItWorked() 53 | { 54 | $this->assertTrue($this->cache->doFlush()); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/Backend/KeyPrefixDecorated.php: -------------------------------------------------------------------------------- 1 | keyPrefix = $options['keyPrefix']; 35 | parent::__construct($decorated); 36 | } 37 | 38 | public function doFetch($id) 39 | { 40 | return $this->decorated->doFetch($this->keyPrefix . $id); 41 | } 42 | 43 | public function doContains($id) 44 | { 45 | return $this->decorated->doContains($this->keyPrefix . $id); 46 | } 47 | 48 | public function doSave($id, $data, $lifeTime = 0) 49 | { 50 | return $this->decorated->doSave($this->keyPrefix . $id, $data, $lifeTime); 51 | } 52 | 53 | public function doDelete($id) 54 | { 55 | return $this->decorated->doDelete($this->keyPrefix . $id); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | infinite lifeTime). 36 | * 37 | * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. 38 | */ 39 | public function save($id, $data, $lifeTime = 0); 40 | 41 | /** 42 | * Deletes a cache entry. 43 | * 44 | * @param string $id The cache id. 45 | * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. 46 | */ 47 | public function delete($id); 48 | 49 | /** 50 | * Flushes all cache entries. 51 | * 52 | * @return boolean TRUE if the cache entries were successfully flushed, FALSE otherwise. 53 | */ 54 | public function flushAll(); 55 | } 56 | -------------------------------------------------------------------------------- /src/Backend.php: -------------------------------------------------------------------------------- 1 | infinite lifeTime). 41 | * 42 | * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. 43 | */ 44 | public function doSave($id, $data, $lifeTime = 0); 45 | 46 | /** 47 | * Deletes a cache entry. 48 | * 49 | * @param string $id The cache id. 50 | * 51 | * @return boolean TRUE if the cache entry was successfully deleted or did not exist, FALSE otherwise. 52 | */ 53 | public function doDelete($id); 54 | 55 | /** 56 | * Flushes all cache entries from the cache. 57 | * 58 | * @return boolean 59 | */ 60 | public function doFlush(); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /tests/Backend/ChainedTest.php: -------------------------------------------------------------------------------- 1 | $arrayCache, 2 => $nullCache); 28 | $cache = $this->createChainedCache($backends); 29 | 30 | $result = $cache->getBackends(); 31 | $this->assertEquals(array($arrayCache, $nullCache), $result); 32 | } 33 | 34 | public function test_doFetch_shouldPopulateOtherCaches() 35 | { 36 | $cacheId = 'myid'; 37 | $cacheValue = 'mytest'; 38 | 39 | $arrayCache1 = new ArrayCache(); 40 | $arrayCache2 = new ArrayCache(); 41 | $arrayCache2->doSave($cacheId, $cacheValue); 42 | $arrayCache3 = new ArrayCache(); 43 | 44 | $cache = $this->createChainedCache(array($arrayCache1, $arrayCache2, $arrayCache3)); 45 | $this->assertEquals($cacheValue, $cache->doFetch($cacheId)); // should find the value from second cache 46 | 47 | // should populate previous cache 48 | $this->assertEquals($cacheValue, $arrayCache1->doFetch($cacheId)); 49 | 50 | // should not populate slower cache 51 | $this->assertFalse($arrayCache3->doContains('myid')); 52 | } 53 | 54 | private function createChainedCache($backends) 55 | { 56 | return new Chained($backends); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/Transient.php: -------------------------------------------------------------------------------- 1 | contains($id)) { 38 | return $this->data[$id]; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function contains($id) 48 | { 49 | return isset($this->data[$id]) || array_key_exists($id, $this->data); 50 | } 51 | 52 | /** 53 | * Puts data into the cache. 54 | * 55 | * @param string $id The cache id. 56 | * @param mixed $content 57 | * @param int $lifeTime Setting a lifetime is not supported by this cache and the parameter will be ignored. 58 | * @return boolean 59 | */ 60 | public function save($id, $content, $lifeTime = 0) 61 | { 62 | $this->data[$id] = $content; 63 | 64 | return true; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function delete($id) 71 | { 72 | if (!$this->contains($id)) { 73 | return false; 74 | } 75 | 76 | unset($this->data[$id]); 77 | return true; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function flushAll() 84 | { 85 | $this->data = array(); 86 | 87 | return true; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Backend/KeyPrefixTest.php: -------------------------------------------------------------------------------- 1 | backendMock = $this->getMockBuilder(NullCache::class)->getMock(); 36 | 37 | 38 | $opts = ['keyPrefix'=>$this->keyPrefix]; 39 | 40 | $this->cache = new KeyPrefixDecorated($this->backendMock, $opts); 41 | } 42 | 43 | public function test_doFetch_shouldCallDecoratedWithKeyPrefix() 44 | { 45 | $this->backendMock 46 | ->expects($this->once()) 47 | ->method('doFetch') 48 | ->with($this->stringStartsWith($this->keyPrefix)); 49 | 50 | $this->cache->doFetch('randomid'); 51 | } 52 | 53 | public function test_doContains_shouldCallDecoratedWithKeyPrefix() 54 | { 55 | $this->backendMock 56 | ->expects($this->once()) 57 | ->method('doContains') 58 | ->with($this->stringStartsWith($this->keyPrefix)); 59 | 60 | $this->cache->doContains('randomid'); 61 | } 62 | 63 | public function test_doSave_shouldCallDecoratedWithKeyPrefix() 64 | { 65 | $this->backendMock 66 | ->expects($this->once()) 67 | ->method('doSave') 68 | ->with($this->stringStartsWith($this->keyPrefix), 69 | $this->anything(), 70 | $this->anything()); 71 | 72 | $this->cache->doSave('randomid', 'anyvalue'); 73 | } 74 | 75 | public function test_doDelete_shouldCallDecoratedWithKeyPrefix() 76 | { 77 | $this->backendMock 78 | ->expects($this->once()) 79 | ->method('doDelete') 80 | ->with($this->stringStartsWith($this->keyPrefix)); 81 | 82 | $this->cache->doDelete('randomid'); 83 | } 84 | } -------------------------------------------------------------------------------- /src/Backend/Chained.php: -------------------------------------------------------------------------------- 1 | backends = array_values($backends); 31 | } 32 | 33 | public function getBackends() 34 | { 35 | return $this->backends; 36 | } 37 | 38 | public function doFetch($id) 39 | { 40 | foreach ($this->backends as $key => $backend) { 41 | $value = $backend->doFetch($id); 42 | if ($value !== false) { 43 | 44 | // EG If chain is ARRAY => REDIS => DB and we find result in DB we will update REDIS and ARRAY 45 | for ($subKey = $key - 1 ; $subKey >= 0 ; $subKey--) { 46 | $this->backends[$subKey]->doSave($id, $value, 300); // TODO we should use the actual TTL here 47 | } 48 | 49 | return $value; 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | 56 | public function doContains($id) 57 | { 58 | foreach ($this->backends as $backend) { 59 | if ($backend->doContains($id)) { 60 | return true; 61 | } 62 | } 63 | 64 | return false; 65 | } 66 | 67 | public function doSave($id, $data, $lifeTime = 0) 68 | { 69 | $stored = true; 70 | 71 | foreach ($this->backends as $backend) { 72 | $stored = $backend->doSave($id, $data, $lifeTime) && $stored; 73 | } 74 | 75 | return $stored; 76 | } 77 | 78 | // returns true even when file does not exist 79 | public function doDelete($id) 80 | { 81 | foreach ($this->backends as $backend) { 82 | if ($backend->doContains($id)) { 83 | $backend->doDelete($id); 84 | } 85 | } 86 | 87 | return true; 88 | } 89 | 90 | public function doFlush() 91 | { 92 | $flushed = true; 93 | 94 | foreach ($this->backends as $backend) { 95 | $flushed = $backend->doFlush() && $flushed; 96 | } 97 | 98 | return $flushed; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /tests/Backend/FileTest.php: -------------------------------------------------------------------------------- 1 | cache = $this->createFileCache(); 29 | $this->cache->doSave($this->cacheId, 'anyvalue', 100); 30 | } 31 | 32 | protected function tearDown() 33 | { 34 | $this->cache->flushAll(); 35 | } 36 | 37 | private function createFileCache($namespace = '') 38 | { 39 | $path = $this->getPath($namespace); 40 | 41 | return new File($path); 42 | } 43 | 44 | private function getPath($namespace = '', $id = '') 45 | { 46 | $path = __DIR__ . '/../tmp'; 47 | 48 | if (!empty($namespace)) { 49 | $path .= '/' . $namespace; 50 | } 51 | 52 | if (!empty($id)) { 53 | $path .= '/' . $id . '.php'; 54 | } 55 | 56 | return $path; 57 | } 58 | 59 | public function test_doSave_shouldCreateDirectoryWith750Permission_IfWritingIntoNewDirectory() 60 | { 61 | $namespace = 'test'; 62 | 63 | $file = $this->createFileCache($namespace); 64 | $file->doSave('myidtest', 'myvalue'); 65 | 66 | $this->assertDirectoryExists($this->getPath($namespace)); 67 | $file->flushAll(); 68 | } 69 | 70 | public function test_doSave_shouldCreateFile() 71 | { 72 | $this->cache->doSave('myidtest', 'myvalue'); 73 | 74 | $this->assertFileExists($this->getPath('', 'myidtest')); 75 | } 76 | 77 | public function test_doSave_shouldSetLifeTime() 78 | { 79 | $this->cache->doSave('myidtest', 'myvalue', 500); 80 | 81 | $path = $this->getPath('', 'myidtest'); 82 | 83 | $contents = include $path; 84 | 85 | $this->assertGreaterThan(time() + 450, $contents['lifetime']); 86 | $this->assertLessThan(time() + 550, $contents['lifetime']); 87 | } 88 | 89 | public function test_doFetch_ParseError() 90 | { 91 | $test = $this->cache->getFilename('foo'); 92 | file_put_contents($test, 'assertFalse($this->cache->doFetch('foo')); 96 | } 97 | 98 | /** 99 | * @dataProvider getTestDataForGetFilename 100 | */ 101 | public function test_getFilename_shouldConstructFilenameFromId($id, $expectedFilename) 102 | { 103 | $this->assertEquals($expectedFilename, $this->cache->getFilename($id)); 104 | } 105 | 106 | public function getTestDataForGetFilename() 107 | { 108 | $dir = realpath($this->getPath()); 109 | 110 | return [ 111 | ['genericid', $dir . '/genericid.php'], 112 | ['id with space', $dir . '/id with space.php'], 113 | ['id \/ with :"?_ spe<>cial cha|rs', $dir . '/id with _ special chars.php'], 114 | ['with % allowed & special chars', $dir . '/with % allowed & special chars.php'], 115 | ]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Lazy.php: -------------------------------------------------------------------------------- 1 | backend = $backend; 25 | } 26 | 27 | /** 28 | * Fetches an entry from the cache. 29 | * 30 | * @param string $id The cache id. 31 | * @return mixed The cached data or FALSE, if no cache entry exists for the given id. 32 | */ 33 | public function fetch($id) 34 | { 35 | $id = $this->getCompletedCacheIdIfValid($id); 36 | 37 | return $this->backend->doFetch($id); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function contains($id) 44 | { 45 | $id = $this->getCompletedCacheIdIfValid($id); 46 | 47 | return $this->backend->doContains($id); 48 | } 49 | 50 | /** 51 | * Puts data into the cache. 52 | * 53 | * @param string $id The cache id. 54 | * @param mixed $data The cache entry/data. 55 | * @param int $lifeTime The cache lifetime in seconds. 56 | * If != 0, sets a specific lifetime for this cache entry (0 => infinite lifeTime). 57 | * 58 | * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. 59 | */ 60 | public function save($id, $data, $lifeTime = 0) 61 | { 62 | $id = $this->getCompletedCacheIdIfValid($id); 63 | 64 | if (is_object($data)) { 65 | throw new \InvalidArgumentException('You cannot use this cache to cache an object, only arrays, strings and numbers. Have a look at Transient cache.'); 66 | // for performance reasons we do currently not recursively search whether any array contains an object. 67 | } 68 | 69 | return $this->backend->doSave($id, $data, $lifeTime); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function delete($id) 76 | { 77 | $id = $this->getCompletedCacheIdIfValid($id); 78 | 79 | return $this->backend->doDelete($id); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function flushAll() 86 | { 87 | return $this->backend->doFlush(); 88 | } 89 | 90 | private function getCompletedCacheIdIfValid($id) 91 | { 92 | $this->checkId($id); 93 | return 'matomocache_' . $id; 94 | } 95 | 96 | private function checkId($id) 97 | { 98 | if (empty($id)) { 99 | throw new \InvalidArgumentException('Empty cache id given'); 100 | } 101 | 102 | if (!$this->isValidId($id)) { 103 | throw new \InvalidArgumentException("Invalid cache id request $id"); 104 | } 105 | } 106 | 107 | /** 108 | * Returns true if the string is a valid id. 109 | * 110 | * Id that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted. 111 | * Id beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example). 112 | * Id containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted). 113 | * 114 | * @param string $id 115 | * @return bool 116 | */ 117 | private function isValidId($id) 118 | { 119 | return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $id)); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/TransientTest.php: -------------------------------------------------------------------------------- 1 | cache = new Transient(); 30 | $this->cache->save($this->cacheId, $this->cacheValue); 31 | } 32 | 33 | public function test_fetch_shouldReturnFalse_IfNoSuchCacheIdExists() 34 | { 35 | $this->assertFalse($this->cache->fetch('randomid')); 36 | } 37 | 38 | public function test_fetch_shouldReturnTheCachedValue_IfCacheIdExists() 39 | { 40 | $this->assertEquals($this->cacheValue, $this->cache->fetch($this->cacheId)); 41 | } 42 | 43 | public function test_contains_shouldReturnFalse_IfNoSuchCacheIdExists() 44 | { 45 | $this->assertFalse($this->cache->contains('randomid')); 46 | } 47 | 48 | public function test_contains_shouldReturnTrue_IfCacheIdExists() 49 | { 50 | $this->assertTrue($this->cache->contains($this->cacheId)); 51 | } 52 | 53 | public function test_delete_shouldReturnTrue_OnSuccess() 54 | { 55 | $this->assertTrue($this->cache->delete($this->cacheId)); 56 | } 57 | 58 | public function test_delete_shouldActuallyDeleteCacheId() 59 | { 60 | $this->assertHasCacheEntry($this->cacheId); 61 | 62 | $this->cache->delete($this->cacheId); 63 | 64 | $this->assertHasNotCacheEntry($this->cacheId); 65 | } 66 | 67 | public function test_delete_shouldNotDeleteAnyOtherCacheIds() 68 | { 69 | $this->cache->save('anyother', 'myvalue'); 70 | $this->assertHasCacheEntry($this->cacheId); 71 | 72 | $this->cache->delete($this->cacheId); 73 | 74 | $this->assertHasCacheEntry('anyother'); 75 | } 76 | 77 | public function test_save_shouldOverwriteAnyValue_IfCacheIdAlreadyExists() 78 | { 79 | $this->assertHasCacheEntry($this->cacheId); 80 | 81 | $value = 'anyotherValuE'; 82 | $this->cache->save($this->cacheId, $value); 83 | 84 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 85 | } 86 | 87 | public function test_save_shouldBeAbleToSetArrays() 88 | { 89 | $value = array('anyotherE' => 'anyOtherValUE', 1 => array(2)); 90 | $this->cache->save($this->cacheId, $value); 91 | 92 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 93 | } 94 | 95 | public function test_save_shouldBeAbleToSetObjects() 96 | { 97 | $value = (object) array('anyotherE' => 'anyOtherValUE', 1 => array(2)); 98 | $this->cache->save($this->cacheId, $value); 99 | 100 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 101 | } 102 | 103 | public function test_save_shouldBeAbleToSetNumbers() 104 | { 105 | $value = 5.4; 106 | $this->cache->save($this->cacheId, $value); 107 | 108 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 109 | } 110 | 111 | public function test_flush_shouldRemoveAllCacheIds() 112 | { 113 | $this->assertHasCacheEntry($this->cacheId); 114 | $this->cache->save('mykey', 'myvalue'); 115 | $this->assertHasCacheEntry('mykey'); 116 | 117 | $this->cache->flushAll(); 118 | 119 | $this->assertHasNotCacheEntry($this->cacheId); 120 | $this->assertHasNotCacheEntry('mykey'); 121 | } 122 | 123 | private function assertHasCacheEntry($cacheId) 124 | { 125 | $this->assertTrue($this->cache->contains($cacheId)); 126 | } 127 | 128 | private function assertHasNotCacheEntry($cacheId) 129 | { 130 | $this->assertFalse($this->cache->contains($cacheId)); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/Eager.php: -------------------------------------------------------------------------------- 1 | fetch('my'id') 23 | * // $cache->save('myid', 'test'); 24 | * 25 | * // ... at some point or at the end of the request 26 | * $cache->persistCacheIfNeeded($lifeTime = 43200); 27 | */ 28 | class Eager implements Cache 29 | { 30 | /** 31 | * @var Backend 32 | */ 33 | private $storage; 34 | private $storageId; 35 | private $content = array(); 36 | private $isDirty = false; 37 | 38 | /** 39 | * Loads the cache entries from the given backend using the given storageId. 40 | * 41 | * @param Backend $storage 42 | * @param $storageId 43 | */ 44 | public function __construct(Backend $storage, $storageId) 45 | { 46 | $this->storage = $storage; 47 | $this->storageId = $storageId; 48 | 49 | $content = $storage->doFetch($storageId); 50 | 51 | if (is_array($content)) { 52 | $this->content = $content; 53 | } 54 | } 55 | 56 | /** 57 | * Fetches an entry from the cache. 58 | * 59 | * Make sure to call the method {@link contains()} to verify whether there is actually any content saved under 60 | * this cache id. 61 | * 62 | * @param string $id The cache id. 63 | * @return int|float|string|boolean|array 64 | */ 65 | public function fetch($id) 66 | { 67 | return $this->content[$id]; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function contains($id) 74 | { 75 | return array_key_exists($id, $this->content); 76 | } 77 | 78 | /** 79 | * Puts data into the cache. 80 | * 81 | * @param string $id The cache id. 82 | * @param int|float|string|boolean|array $content 83 | * @param int $lifeTime Setting a lifetime is not supported by this cache and the parameter will be ignored. 84 | * @return boolean 85 | */ 86 | public function save($id, $content, $lifeTime = 0) 87 | { 88 | if (is_object($content)) { 89 | throw new \InvalidArgumentException('You cannot use this cache to cache an object, only arrays, strings and numbers. Have a look at Transient cache.'); 90 | // for performance reasons we do currently not recursively search whether any array contains an object. 91 | } 92 | 93 | $this->content[$id] = $content; 94 | $this->isDirty = true; 95 | return true; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function delete($id) 102 | { 103 | if ($this->contains($id)) { 104 | $this->isDirty = true; 105 | unset($this->content[$id]); 106 | return true; 107 | } 108 | 109 | return false; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function flushAll() 116 | { 117 | $this->storage->doDelete($this->storageId); 118 | 119 | $this->content = array(); 120 | $this->isDirty = false; 121 | 122 | return true; 123 | } 124 | 125 | /** 126 | * Will persist all previously made changes if there were any. 127 | * 128 | * @param int $lifeTime The cache lifetime in seconds. 129 | * If != 0, sets a specific lifetime for this cache entry (0 => infinite lifeTime). 130 | */ 131 | public function persistCacheIfNeeded($lifeTime) 132 | { 133 | if ($this->isDirty) { 134 | $this->storage->doSave($this->storageId, $this->content, $lifeTime); 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/Backend/Factory.php: -------------------------------------------------------------------------------- 1 | buildBackend($backendToBuild, $backendOptions); 42 | } 43 | 44 | return new Chained($backends); 45 | } 46 | 47 | public function buildRedisCache($options) 48 | { 49 | if (empty($options['unix_socket']) && (empty($options['host']) || empty($options['port']))) { 50 | throw new \InvalidArgumentException('RedisCache is not configured. Please provide at least a host and a port'); 51 | } 52 | 53 | $timeout = 0.0; 54 | if (array_key_exists('timeout', $options)) { 55 | $timeout = $options['timeout']; 56 | } 57 | 58 | $redis = new \Redis(); 59 | if (empty($options['unix_socket'])) { 60 | $redis->connect($options['host'], $options['port'], $timeout); 61 | } else { 62 | $redis->connect($options['unix_socket'], null, $timeout); 63 | } 64 | 65 | if (!empty($options['password'])) { 66 | $redis->auth($options['password']); 67 | } 68 | 69 | if (array_key_exists('database', $options)) { 70 | $redis->select((int) $options['database']); 71 | } 72 | 73 | $redisCache = new Redis(); 74 | $redisCache->setRedis($redis); 75 | 76 | return $redisCache; 77 | } 78 | 79 | /** 80 | * @param string $class 81 | * @param array $options 82 | * 83 | * @return Backend 84 | * 85 | * @throws Factory\BackendNotFoundException 86 | */ 87 | public function buildDecorated( $class, $options ) 88 | { 89 | $backendToBuild = $options["backend"]; 90 | $backendOptions = array(); 91 | if (array_key_exists($backendToBuild, $options)) { 92 | $backendOptions = $options[$backendToBuild]; 93 | } 94 | 95 | $backend = $this->buildBackend($backendToBuild, $backendOptions); 96 | 97 | return new $class($backend, $options); 98 | } 99 | 100 | /** 101 | * Build a specific backend instance. 102 | * 103 | * @param string $type The type of backend you want to create. Eg 'array', 'file', 'chained', 'null', 'redis'. 104 | * @param array $options An array of options for the backend you want to create. 105 | * @return Backend 106 | * @throws Factory\BackendNotFoundException In case the given type was not found. 107 | */ 108 | public function buildBackend($type, array $options) 109 | { 110 | switch ($type) { 111 | case 'array': 112 | 113 | return $this->buildArrayCache(); 114 | 115 | case 'file': 116 | 117 | return $this->buildFileCache($options); 118 | 119 | case 'chained': 120 | 121 | return $this->buildChainedCache($options); 122 | 123 | case 'null': 124 | 125 | return $this->buildNullCache(); 126 | 127 | case 'redis': 128 | 129 | return $this->buildRedisCache($options); 130 | 131 | case 'defaultTimeout': 132 | 133 | return $this->buildDecorated(DefaultTimeoutDecorated::class, $options); 134 | 135 | case 'keyPrefix': 136 | 137 | return $this->buildDecorated(KeyPrefixDecorated::class, $options); 138 | 139 | default: 140 | 141 | throw new Factory\BackendNotFoundException("Cache backend $type not valid"); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Backend/File.php: -------------------------------------------------------------------------------- 1 | createDirectory($directory); 39 | } 40 | $this->supportsParseError = defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION >= 7 && class_exists('\ParseError'); 41 | 42 | parent::__construct($directory, $extension); 43 | } 44 | 45 | public function doFetch($id) 46 | { 47 | if (self::$invalidateOpCacheBeforeRead) { 48 | $this->invalidateCacheFile($id); 49 | } 50 | 51 | if ($this->supportsParseError) { 52 | try { 53 | return parent::doFetch($id); 54 | } catch (\ParseError $e) { 55 | return false; 56 | } 57 | } 58 | 59 | return parent::doFetch($id); 60 | } 61 | 62 | public function doContains($id) 63 | { 64 | return parent::doContains($id); 65 | } 66 | 67 | public function doSave($id, $data, $lifeTime = 0) 68 | { 69 | if (!is_dir($this->directory)) { 70 | $this->createDirectory($this->directory); 71 | } 72 | 73 | $success = parent::doSave($id, $data, $lifeTime); 74 | 75 | if ($success) { 76 | $this->invalidateCacheFile($id); 77 | } 78 | 79 | return $success; 80 | } 81 | 82 | public function doDelete($id) 83 | { 84 | $this->invalidateCacheFile($id); 85 | 86 | $success = parent::doDelete($id); 87 | 88 | $this->invalidateCacheFile($id); // in case file was cached by another request between invalidate and doDelete() 89 | 90 | return $success; 91 | } 92 | 93 | public function doFlush() 94 | { 95 | // if the directory does not exist, do not bother to continue clearing 96 | if (!is_dir($this->directory)) { 97 | return false; 98 | } 99 | 100 | foreach ($this->getFileIterator() as $name => $file) { 101 | $this->opCacheInvalidate($name); 102 | } 103 | 104 | return parent::doFlush(); 105 | } 106 | 107 | private function invalidateCacheFile($id) 108 | { 109 | $filename = $this->getFilename($id); 110 | $this->opCacheInvalidate($filename); 111 | } 112 | 113 | /** 114 | * @param string $id 115 | * 116 | * @return string 117 | */ 118 | public function getFilename($id) 119 | { 120 | $path = $this->directory . DIRECTORY_SEPARATOR; 121 | $id = preg_replace('@[\\\/:"*?<>|]+@', '', $id); 122 | 123 | return $path . $id . $this->getExtension(); 124 | } 125 | 126 | private function opCacheInvalidate($filepath) 127 | { 128 | if (is_file($filepath)) { 129 | if (function_exists('opcache_invalidate')) { 130 | @opcache_invalidate($filepath, $force = true); 131 | } 132 | if (function_exists('apc_delete_file')) { 133 | @apc_delete_file($filepath); 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * @return \Iterator 140 | */ 141 | private function getFileIterator() 142 | { 143 | $pattern = '/^.+\\' . $this->getExtension() . '$/i'; 144 | $iterator = new \RecursiveDirectoryIterator($this->directory); 145 | $iterator = new \RecursiveIteratorIterator($iterator); 146 | return new \RegexIterator($iterator, $pattern); 147 | } 148 | 149 | private function createDirectory($path) 150 | { 151 | if (!is_dir($path)) { 152 | // the mode in mkdir is modified by the current umask 153 | @mkdir($path, 0750, $recursive = true); 154 | } 155 | 156 | // try to overcome restrictive umask (mis-)configuration 157 | if (!is_writable($path)) { 158 | @chmod($path, 0755); 159 | if (!is_writable($path)) { 160 | @chmod($path, 0775); 161 | // enough! we're not going to make the directory world-writeable 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /tests/EagerTest.php: -------------------------------------------------------------------------------- 1 | backend = new ArrayCache(); 38 | $this->backend->doSave($this->storageId, array($this->cacheId => $this->cacheValue)); 39 | 40 | $this->cache = new Eager($this->backend, $this->storageId); 41 | } 42 | 43 | public function test_contains_shouldReturnFalse_IfNoSuchCacheIdExists() 44 | { 45 | $this->assertFalse($this->cache->contains('randomid')); 46 | } 47 | 48 | public function test_contains_shouldReturnTrue_IfSuchCacheIdExists() 49 | { 50 | $this->assertTrue($this->cache->contains($this->cacheId)); 51 | } 52 | 53 | public function test_fetch_shouldReturnTheCachedValue_IfCacheIdExists() 54 | { 55 | $this->assertEquals($this->cacheValue, $this->cache->fetch($this->cacheId)); 56 | } 57 | 58 | public function test_save_shouldOverwriteAnyValue_IfCacheIdAlreadyExists() 59 | { 60 | $this->assertHasCacheEntry($this->cacheId); 61 | 62 | $value = 'anyotherValuE'; 63 | $this->cache->save($this->cacheId, $value); 64 | 65 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 66 | } 67 | 68 | public function test_save_shouldBeAbleToSetArrays() 69 | { 70 | $value = array('anyotherE' => 'anyOtherValUE', 1 => array(2)); 71 | $this->cache->save($this->cacheId, $value); 72 | 73 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 74 | } 75 | 76 | /** 77 | * @expectedException \InvalidArgumentException 78 | * @expectedExceptionMessage cannot use this cache to cache an object 79 | */ 80 | public function test_save_shouldFail_IfTryingToSetAnObject() 81 | { 82 | $value = (object) array('anyotherE' => 'anyOtherValUE', 1 => array(2)); 83 | $this->cache->save($this->cacheId, $value); 84 | 85 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 86 | } 87 | 88 | public function test_save_shouldBeAbleToSetNumbers() 89 | { 90 | $value = 5.4; 91 | $this->cache->save($this->cacheId, $value); 92 | 93 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 94 | } 95 | 96 | public function test_delete_shouldReturnTrue_OnSuccess() 97 | { 98 | $this->assertTrue($this->cache->delete($this->cacheId)); 99 | } 100 | 101 | public function test_delete_shouldReturnFalse_IfCacheIdDoesNotExist() 102 | { 103 | $this->assertFalse($this->cache->delete('IdoNotExisT')); 104 | } 105 | 106 | public function test_delete_shouldActuallyDeleteCacheId() 107 | { 108 | $this->assertHasCacheEntry($this->cacheId); 109 | 110 | $this->cache->delete($this->cacheId); 111 | 112 | $this->assertHasNotCacheEntry($this->cacheId); 113 | } 114 | 115 | public function test_delete_shouldNotDeleteAnyOtherCacheIds() 116 | { 117 | $this->cache->save('anyother', 'myvalue'); 118 | $this->assertHasCacheEntry($this->cacheId); 119 | 120 | $this->cache->delete($this->cacheId); 121 | 122 | $this->assertHasCacheEntry('anyother'); 123 | } 124 | 125 | public function test_flush_shouldRemoveAllCacheIds() 126 | { 127 | $this->assertHasCacheEntry($this->cacheId); 128 | $this->cache->save('mykey', 'myvalue'); 129 | $this->assertHasCacheEntry('mykey'); 130 | $this->assertTrue($this->backend->doContains($this->storageId)); 131 | 132 | $this->cache->flushAll(); 133 | 134 | $this->assertHasNotCacheEntry($this->cacheId); 135 | $this->assertHasNotCacheEntry('mykey'); 136 | $this->assertFalse($this->backend->doContains($this->storageId)); // should also remove the storage entry 137 | } 138 | 139 | public function test_persistCacheIfNeeded_shouldActuallySaveValuesInBackend_IfThereWasSomethingSet() 140 | { 141 | $this->cache->save('mykey', 'myvalue'); 142 | 143 | $expected = array($this->cacheId => $this->cacheValue); 144 | $this->assertEquals($expected, $this->getContentOfStorage()); 145 | 146 | $this->cache->persistCacheIfNeeded(400); 147 | 148 | $expected['mykey'] = 'myvalue'; 149 | 150 | $this->assertEquals($expected, $this->getContentOfStorage()); 151 | } 152 | 153 | public function test_persistCacheIfNeeded_shouldActuallySaveValuesInBackend_IfThereWasSomethingDelete() 154 | { 155 | $this->cache->delete($this->cacheId); 156 | 157 | $expected = array($this->cacheId => $this->cacheValue); 158 | $this->assertEquals($expected, $this->getContentOfStorage()); 159 | 160 | $this->cache->persistCacheIfNeeded(400); 161 | 162 | $this->assertEquals(array(), $this->getContentOfStorage()); 163 | } 164 | 165 | public function test_persistCacheIfNeeded_shouldNotSaveAnyValuesInBackend_IfThereWasNoChange() 166 | { 167 | $this->backend->doDelete($this->storageId); 168 | $this->assertFalse($this->getContentOfStorage()); 169 | 170 | $this->cache->persistCacheIfNeeded(400); 171 | 172 | $this->assertFalse($this->getContentOfStorage()); // should not have set the content of cache ($cacheId => $cacheValue) 173 | } 174 | 175 | private function getContentOfStorage() 176 | { 177 | return $this->backend->doFetch($this->storageId); 178 | } 179 | 180 | private function assertHasCacheEntry($cacheId) 181 | { 182 | $this->assertTrue($this->cache->contains($cacheId)); 183 | } 184 | 185 | private function assertHasNotCacheEntry($cacheId) 186 | { 187 | $this->assertFalse($this->cache->contains($cacheId)); 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /tests/LazyTest.php: -------------------------------------------------------------------------------- 1 | cache = new Lazy($backend); 32 | $this->cache->save($this->cacheId, $this->cacheValue); 33 | } 34 | 35 | /** 36 | * @expectedException \InvalidArgumentException 37 | * @expectedExceptionMessage Empty cache id 38 | */ 39 | public function test_fetch_shouldFail_IfCacheIdIsEmpty() 40 | { 41 | $this->cache->fetch(''); 42 | } 43 | 44 | /** 45 | * @dataProvider getInvalidCacheIds 46 | * @expectedException \InvalidArgumentException 47 | * @expectedExceptionMessage Invalid cache id 48 | */ 49 | public function test_shouldFail_IfCacheIdIsInvalid($method, $id) 50 | { 51 | $this->executeMethodOnCache($method, $id); 52 | } 53 | 54 | /** 55 | * @dataProvider getValidCacheIds 56 | */ 57 | public function test_shouldNotFail_IfCacheIdIsValid($method, $id) 58 | { 59 | $this->executeMethodOnCache($method, $id); 60 | $this->assertTrue(true); 61 | } 62 | 63 | private function executeMethodOnCache($method, $id) 64 | { 65 | if ('save' === $method) { 66 | $this->cache->$method($id, 'val'); 67 | } else { 68 | $this->cache->$method($id); 69 | } 70 | } 71 | 72 | public function test_fetch_shouldReturnFalse_IfNoSuchCacheIdExists() 73 | { 74 | $this->assertFalse($this->cache->fetch('randomid')); 75 | } 76 | 77 | public function test_fetch_shouldReturnTheCachedValue_IfCacheIdExists() 78 | { 79 | $this->assertEquals($this->cacheValue, $this->cache->fetch($this->cacheId)); 80 | } 81 | 82 | public function test_contains_shouldReturnFalse_IfNoSuchCacheIdExists() 83 | { 84 | $this->assertFalse($this->cache->contains('randomid')); 85 | } 86 | 87 | public function test_contains_shouldReturnTrue_IfCacheIdExists() 88 | { 89 | $this->assertTrue($this->cache->contains($this->cacheId)); 90 | } 91 | 92 | public function test_delete_shouldReturnTrue_OnSuccess() 93 | { 94 | $this->assertTrue($this->cache->delete($this->cacheId)); 95 | } 96 | 97 | public function test_delete_shouldActuallyDeleteCacheId() 98 | { 99 | $this->assertHasCacheEntry($this->cacheId); 100 | 101 | $this->cache->delete($this->cacheId); 102 | 103 | $this->assertHasNotCacheEntry($this->cacheId); 104 | } 105 | 106 | public function test_delete_shouldNotDeleteAnyOtherCacheIds() 107 | { 108 | $this->cache->save('anyother', 'myvalue'); 109 | $this->assertHasCacheEntry($this->cacheId); 110 | 111 | $this->cache->delete($this->cacheId); 112 | 113 | $this->assertHasCacheEntry('anyother'); 114 | } 115 | 116 | public function test_save_shouldOverwriteAnyValue_IfCacheIdAlreadyExists() 117 | { 118 | $this->assertHasCacheEntry($this->cacheId); 119 | 120 | $value = 'anyotherValuE'; 121 | $this->cache->save($this->cacheId, $value); 122 | 123 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 124 | } 125 | 126 | public function test_save_shouldBeAbleToSetArrays() 127 | { 128 | $value = array('anyotherE' => 'anyOtherValUE', 1 => array(2)); 129 | $this->cache->save($this->cacheId, $value); 130 | 131 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 132 | } 133 | 134 | /** 135 | * @expectedException \InvalidArgumentException 136 | * @expectedExceptionMessage cannot use this cache to cache an object 137 | */ 138 | public function test_save_shouldFail_IfTryingToSetAnObject() 139 | { 140 | $value = (object) array('anyotherE' => 'anyOtherValUE', 1 => array(2)); 141 | $this->cache->save($this->cacheId, $value); 142 | 143 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 144 | } 145 | 146 | public function test_save_shouldBeAbleToSetNumbers() 147 | { 148 | $value = 5.4; 149 | $this->cache->save($this->cacheId, $value); 150 | 151 | $this->assertSame($value, $this->cache->fetch($this->cacheId)); 152 | } 153 | 154 | public function test_flush_shouldRemoveAllCacheIds() 155 | { 156 | $this->assertHasCacheEntry($this->cacheId); 157 | $this->cache->save('mykey', 'myvalue'); 158 | $this->assertHasCacheEntry('mykey'); 159 | 160 | $this->cache->flushAll(); 161 | 162 | $this->assertHasNotCacheEntry($this->cacheId); 163 | $this->assertHasNotCacheEntry('mykey'); 164 | } 165 | 166 | private function assertHasCacheEntry($cacheId) 167 | { 168 | $this->assertTrue($this->cache->contains($cacheId)); 169 | } 170 | 171 | private function assertHasNotCacheEntry($cacheId) 172 | { 173 | $this->assertFalse($this->cache->contains($cacheId)); 174 | } 175 | 176 | public function getInvalidCacheIds() 177 | { 178 | $ids = array(); 179 | $methods = array('fetch', 'save', 'contains', 'delete'); 180 | 181 | foreach ($methods as $method) { 182 | $ids[] = array($method, 'eteer#'); 183 | $ids[] = array($method, '-test'); 184 | $ids[] = array($method, '_test'); 185 | $ids[] = array($method, '.test'); 186 | $ids[] = array($method, 'test/test'); 187 | $ids[] = array($method, '../test/'); 188 | $ids[] = array($method, 'test0*'); 189 | $ids[] = array($method, 'test\\test'); 190 | } 191 | 192 | return $ids; 193 | } 194 | 195 | public function getValidCacheIds() 196 | { 197 | $ids = array(); 198 | $methods = array('fetch', 'save', 'contains', 'delete'); 199 | 200 | foreach ($methods as $method) { 201 | $ids[] = array($method, '012test'); 202 | $ids[] = array($method, 'test012test'); 203 | $ids[] = array($method, 't.est.012test'); 204 | $ids[] = array($method, 't-est-test'); 205 | $ids[] = array($method, 't_est_tes4t'); 206 | $ids[] = array($method, 't_est.te-s2t'); 207 | $ids[] = array($method, 't_est...te-s2t'); 208 | } 209 | 210 | return $ids; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /tests/BackendTest.php: -------------------------------------------------------------------------------- 1 | doFlush(); 31 | $success = $backend[0]->doSave($this->cacheId, $this->cacheValue); 32 | $this->assertTrue($success); 33 | } 34 | } 35 | 36 | public static function tearDownAfterClass() 37 | { 38 | foreach (self::$backends as $backend) { 39 | /** @var Backend[] $backend */ 40 | $backend[0]->doFlush(); 41 | } 42 | 43 | self::$backends = array(); 44 | } 45 | 46 | /** 47 | * @dataProvider getBackends 48 | */ 49 | public function test_doFetch_shouldReturnFalse_IfNoSuchCacheIdExists(Backend $backend) 50 | { 51 | $this->assertFalse($backend->doFetch('randomid')); 52 | } 53 | 54 | /** 55 | * @dataProvider getBackends 56 | */ 57 | public function test_doFetch_shouldReturnTheCachedValue_IfCacheIdExists(Backend $backend) 58 | { 59 | $this->assertEquals($this->cacheValue, $backend->doFetch($this->cacheId)); 60 | } 61 | 62 | /** 63 | * @dataProvider getBackends 64 | */ 65 | public function test_doContains_shouldReturnFalse_IfNoSuchCacheIdExists(Backend $backend) 66 | { 67 | $this->assertFalse($backend->doContains('randomid')); 68 | } 69 | 70 | /** 71 | * @dataProvider getBackends 72 | */ 73 | public function test_doContains_shouldReturnTrue_IfCacheIdExists(Backend $backend) 74 | { 75 | $this->assertTrue($backend->doContains($this->cacheId)); 76 | } 77 | 78 | /** 79 | * @dataProvider getBackends 80 | */ 81 | public function test_doDelete_shouldReturnTrue_OnSuccess(Backend $backend) 82 | { 83 | $this->assertTrue($backend->doDelete($this->cacheId)); 84 | } 85 | 86 | /** 87 | * @dataProvider getBackends 88 | */ 89 | public function test_doDelete_shouldActuallyDeleteCacheId(Backend $backend) 90 | { 91 | $this->assertHasCacheEntry($backend, $this->cacheId); 92 | 93 | $backend->doDelete($this->cacheId); 94 | 95 | $this->assertHasNotCacheEntry($backend, $this->cacheId); 96 | } 97 | 98 | /** 99 | * @dataProvider getBackends 100 | */ 101 | public function test_doDelete_shouldNotDeleteAnyOtherCacheIds(Backend $backend) 102 | { 103 | $backend->doSave('anyother', 'myvalue'); 104 | $this->assertHasCacheEntry($backend, $this->cacheId); 105 | 106 | $backend->doDelete($this->cacheId); 107 | 108 | $this->assertHasCacheEntry($backend, 'anyother'); 109 | } 110 | 111 | /** 112 | * @dataProvider getBackends 113 | */ 114 | public function test_doDelete_shouldNotFail_IfCacheEntryDoesNotExist(Backend $backend) 115 | { 116 | $success = $backend->doDelete('anYRandoOmId'); 117 | 118 | $this->assertTrue($success); 119 | } 120 | 121 | /** 122 | * @dataProvider getBackends 123 | */ 124 | public function test_doSave_shouldOverwriteAnyValue_IfCacheIdAlreadyExists(Backend $backend) 125 | { 126 | $this->assertHasCacheEntry($backend, $this->cacheId); 127 | 128 | $value = 'anyotherValuE'; 129 | $backend->doSave($this->cacheId, $value); 130 | 131 | $this->assertSame($value, $backend->doFetch($this->cacheId)); 132 | } 133 | 134 | /** 135 | * @dataProvider getBackends 136 | */ 137 | public function test_doSave_shouldBeAbleToSetArrays(Backend $backend) 138 | { 139 | $value = array('anyotherE' => 'anyOtherValUE', 1 => array(2)); 140 | $backend->doSave($this->cacheId, $value); 141 | 142 | $this->assertSame($value, $backend->doFetch($this->cacheId)); 143 | } 144 | 145 | /** 146 | * @dataProvider getBackends 147 | */ 148 | public function test_doSave_shouldBeAbleToSetNumbers(Backend $backend) 149 | { 150 | $value = 5.4; 151 | $backend->doSave($this->cacheId, $value); 152 | 153 | $this->assertSame($value, $backend->doFetch($this->cacheId)); 154 | } 155 | 156 | /** 157 | * @dataProvider getBackends 158 | */ 159 | public function test_doFlush_shouldRemoveAllCacheIds(Backend $backend) 160 | { 161 | $this->assertHasCacheEntry($backend, $this->cacheId); 162 | $backend->doSave('mykey', 'myvalue'); 163 | $this->assertHasCacheEntry($backend, 'mykey'); 164 | 165 | $backend->doFlush(); 166 | 167 | $this->assertHasNotCacheEntry($backend, $this->cacheId); 168 | $this->assertHasNotCacheEntry($backend, 'mykey'); 169 | } 170 | 171 | public function getBackends() 172 | { 173 | if (!empty(self::$backends)) { 174 | return self::$backends; 175 | } 176 | 177 | /** @var Backend[] $backends */ 178 | $backends = array(); 179 | 180 | $arrayCache = new ArrayCache(); 181 | $fileCache = new File($this->getPathToCacheDir()); 182 | 183 | $chainedFileCache = new File( $this->getPathToCacheDir() . '/chain' ); 184 | $chainCache = new Chained( array(new ArrayCache(), $chainedFileCache) ); 185 | 186 | $timeoutDecorated = new Backend\DefaultTimeoutDecorated( new ArrayCache(), ['defaultTimeout' => 8866] ); 187 | $prefixDecorated = new Backend\KeyPrefixDecorated( new ArrayCache(), ['keyPrefix' => 'prefix123'] ); 188 | 189 | $backends[] = $arrayCache; 190 | $backends[] = $fileCache; 191 | $backends[] = $chainCache; 192 | $backends[] = $timeoutDecorated; 193 | $backends[] = $prefixDecorated; 194 | 195 | if (extension_loaded('redis')) { 196 | $factory = new Factory(); 197 | $backends[] = $factory->buildRedisCache(array('host' => '127.0.0.1', 'port' => '6379')); 198 | } 199 | 200 | foreach ($backends as $backend) { 201 | self::$backends[] = array($backend); 202 | } 203 | 204 | return self::$backends; 205 | } 206 | 207 | private function getPathToCacheDir() 208 | { 209 | return __DIR__ . '/tmp'; 210 | } 211 | 212 | private function assertHasCacheEntry(Backend $backend, $cacheId) 213 | { 214 | $this->assertTrue($backend->doContains($cacheId)); 215 | } 216 | 217 | private function assertHasNotCacheEntry(Backend $backend, $cacheId) 218 | { 219 | $this->assertFalse($backend->doContains($cacheId)); 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /tests/Backend/FactoryTest.php: -------------------------------------------------------------------------------- 1 | factory = new Factory(); 34 | } 35 | 36 | public function test_buildArrayCache_ShouldReturnInstanceOfArray() 37 | { 38 | $cache = $this->factory->buildArrayCache(); 39 | $this->assertInstanceOf(ArrayCache::class, $cache); 40 | } 41 | 42 | public function test_buildNullCache_ShouldReturnInstanceOfNull() 43 | { 44 | $cache = $this->factory->buildNullCache(); 45 | $this->assertInstanceOf(NullCache::class, $cache); 46 | } 47 | 48 | public function test_buildFileCache_ShouldReturnInstanceOfFile() 49 | { 50 | $cache = $this->factory->buildFileCache(array('directory' => __DIR__)); 51 | $this->assertInstanceOf(File::class, $cache); 52 | } 53 | 54 | public function test_buildChainedCache_ShouldReturnInstanceOfChained() 55 | { 56 | $cache = $this->factory->buildChainedCache(array('backends' => array())); 57 | $this->assertInstanceOf(Chained::class, $cache); 58 | } 59 | 60 | public function test_buildBackend_Chained_ShouldActuallyCreateInstancesOfNestedBackends() 61 | { 62 | $options = array( 63 | 'backends' => array('array', 'file'), 64 | 'file' => array('directory' => __DIR__), 65 | 'array' => array() 66 | ); 67 | 68 | /** @var Chained $cache */ 69 | $cache = $this->factory->buildBackend('chained', $options); 70 | 71 | $backends = $cache->getBackends(); 72 | 73 | $this->assertInstanceOf(ArrayCache::class, $backends[0]); 74 | $this->assertInstanceOf(File::class, $backends[1]); 75 | } 76 | 77 | public function test_buildRedisCache_ShouldReturnInstanceOfRedis() 78 | { 79 | $this->skipTestIfRedisIsNotInstalled(); 80 | 81 | $cache = $this->factory->buildRedisCache(array('host' => '127.0.0.1', 'port' => '6379', 'timeout' => 0.0)); 82 | $this->assertInstanceOf(Redis::class, $cache); 83 | } 84 | 85 | public function test_buildBackend_Redis_ShouldReturnInstanceOfRedis() 86 | { 87 | $this->skipTestIfRedisIsNotInstalled(); 88 | 89 | $options = array('host' => '127.0.0.1', 'port' => '6379', 'timeout' => 0.0); 90 | 91 | $cache = $this->factory->buildBackend('redis', $options); 92 | $this->assertInstanceOf(Redis::class, $cache); 93 | } 94 | 95 | public function test_buildBackend_Redis_ShouldForwardOptionsToRedisInstance() 96 | { 97 | $this->skipTestIfRedisIsNotInstalled(); 98 | 99 | $options = array('host' => '127.0.0.1', 'port' => '6379', 'timeout' => 4.2, 'database' => 5); 100 | 101 | /** @var Redis $cache */ 102 | $cache = $this->factory->buildBackend('redis', $options); 103 | $redis = $cache->getRedis(); 104 | 105 | $this->assertEquals('127.0.0.1', $redis->getHost()); 106 | $this->assertEquals(6379, $redis->getPort()); 107 | $this->assertEquals(4.2, $redis->getTimeout()); 108 | $this->assertEquals(5, $redis->getDBNum()); 109 | } 110 | 111 | /** 112 | * @expectedException \InvalidArgumentException 113 | * @expectedExceptionMessage RedisCache is not configured 114 | */ 115 | public function test_buildRedisCache_ShouldFail_IfPortIsMissing() 116 | { 117 | $this->factory->buildRedisCache(array('host' => '127.0.0.1')); 118 | } 119 | 120 | /** 121 | * @expectedException \InvalidArgumentException 122 | * @expectedExceptionMessage RedisCache is not configured 123 | */ 124 | public function test_buildRedisCache_ShouldFail_IfHostIsMissing() 125 | { 126 | $this->factory->buildRedisCache(array('port' => '6379')); 127 | } 128 | 129 | public function test_buildBackend_ArrayCache_ShouldReturnInstanceOfArray() 130 | { 131 | $cache = $this->factory->buildBackend('array', array()); 132 | $this->assertInstanceOf(ArrayCache::class, $cache); 133 | } 134 | 135 | public function test_buildBackend_NullCache_ShouldReturnInstanceOfNull() 136 | { 137 | $cache = $this->factory->buildBackend('null', array()); 138 | $this->assertInstanceOf(NullCache::class, $cache); 139 | } 140 | 141 | public function test_buildBackend_FileCache_ShouldReturnInstanceOfFile() 142 | { 143 | $cache = $this->factory->buildBackend('file', array('directory' => __DIR__)); 144 | $this->assertInstanceOf(File::class, $cache); 145 | } 146 | 147 | public function test_buildBackend_Chained_ShouldReturnInstanceOfChained() 148 | { 149 | $cache = $this->factory->buildBackend('chained', array('backends' => array())); 150 | $this->assertInstanceOf(Chained::class, $cache); 151 | } 152 | 153 | /** 154 | * @expectedException \Matomo\Cache\Backend\Factory\BackendNotFoundException 155 | */ 156 | public function test_buildBackend_ShouldThrowException_IfInvalidTypeGiven() 157 | { 158 | $this->factory->buildBackend('noTValId', array()); 159 | } 160 | 161 | private function skipTestIfRedisIsNotInstalled() 162 | { 163 | if (!extension_loaded('redis')) { 164 | $this->markTestSkipped('The test ' . __METHOD__ . ' requires the use of redis'); 165 | } 166 | } 167 | 168 | 169 | public function test_buildBackend_Chained_ShouldCreateInstancesOfNestedDecorators() 170 | {} 171 | 172 | public function test_buildBackend_Decorated_DefaultTimeoutDecorated_ShouldActuallyCreateInstanceOfNestedBackend() 173 | { 174 | $options = array( 175 | 'backend' => 'array', 176 | 'array' => array(), 177 | 'defaultTimeout' => 555 178 | ); 179 | 180 | /** @var DefaultTimeoutDecorated $cache */ 181 | $cache = $this->factory->buildBackend('defaultTimeout', $options); 182 | 183 | 184 | $backend = $cache->getBackend(); 185 | $this->assertInstanceOf(ArrayCache::class, $backend); 186 | } 187 | 188 | public function test_buildBackend_Decorated_KeyPrefixDecorated_ShouldActuallyCreateInstanceOfNestedBackend() 189 | { 190 | $options = array( 191 | 'backend' => 'array', 192 | 'array' => array(), 193 | 'keyPrefix' => '555' 194 | ); 195 | 196 | /** @var KeyPrefixDecorated $cache */ 197 | $cache = $this->factory->buildBackend('keyPrefix', $options); 198 | 199 | 200 | $backend = $cache->getBackend(); 201 | $this->assertInstanceOf(ArrayCache::class, $backend); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matomo/Cache 2 | 3 | This is a PHP caching library based on [Doctrine cache](https://github.com/doctrine/cache) that supports different backends. 4 | At [Matomo](https://matomo.org) we developed this library with the focus on speed as we make heavy use of caching and 5 | sometimes fetch hundreds of entries from the cache in one request. 6 | 7 | [![Build Status](https://travis-ci.com/matomo-org/component-cache.svg?branch=master)](https://travis-ci.com/matomo-org/component-cache) 8 | 9 | ## Installation 10 | 11 | With Composer: 12 | 13 | ```json 14 | { 15 | "require": { 16 | "matomo/cache": "*" 17 | } 18 | } 19 | ``` 20 | 21 | ## Supported backends 22 | * Array (holds cache entries only during one request but is very fast) 23 | * Null (useful for development, won't cache anything) 24 | * File (stores the cache entry on the file system) 25 | * Redis (stores the cache entry on a Redis server, requires [phpredis](https://github.com/nicolasff/phpredis)) 26 | * Chained (allows to chain multiple backends to make sure it will read from a fast cache if possible) 27 | * DefaultTimeout and KeyPrefix can be used to wrap other backend with default parameters. 28 | 29 | Doctrine cache provides support for many more backends and adding one of those is easy. For example: 30 | * APC 31 | * Couchbase 32 | * Memcache 33 | * MongoDB 34 | * Riak 35 | * WinCache 36 | * Xcache 37 | * ZendData 38 | 39 | Please send a pull request in case you have added one. 40 | 41 | ## Different caches 42 | 43 | This library comes with three different types of caches. The naming is not optimal right now. 44 | 45 | ### Lazy 46 | 47 | This can be considered as the default cache to use in case you don't know which one to pick. The lazy cache works with 48 | any backend so you can decide whether you want to persist cache entries between requests or not. It does not support 49 | the caching of any objects. Only boolean, numbers, strings and arrays are supported. Whenever you request an entry 50 | from the cache it will fetch the entry from the defined backend again which can cause many reads depending on your 51 | application. 52 | 53 | ### Eager 54 | 55 | This cache stores all its cache entries under one "cache" entry in a configurable backend. 56 | 57 | This comes handy for things that you need very often, nearly in every request. Instead of having to read eg. 58 | a hundred cache entries from files it only loads one cache entry which contains the hundred keys. Should be used only 59 | for things that you need very often and only for cache entries that are not too large to keep loading and parsing the 60 | single cache entry fast. This cache is even more useful in case you are using a slow backend such as a file or a database. 61 | Instead of having a hundred stat calls there will be only one. All cache entries it contains have the same life time. 62 | For fast performance it won't validate any cache ids. It is not possible to cache any objects using this cache. 63 | 64 | ### Transient 65 | 66 | This class is used to cache any data during one request. It won't be persisted. 67 | 68 | All cache entries will be cached in a simple array meaning it is very fast to save and fetch cache entries. You can 69 | basically achieve the same by using a lazy cache and a backend that does not persist any data such as the array cache 70 | but this one will be a bit faster as it won't validate any cache ids and it allows you to cache any kind of objects. 71 | Compared to the lazy cache it does not support setting any life time as it will be only valid during one request anyway. 72 | Use this one if you read hundreds or thousands of cache entries and if performance really matters to you. 73 | 74 | ## Usage 75 | 76 | ### Creating a file backend 77 | 78 | ```php 79 | $options = array('directory' => '/path/to/cache'); 80 | $factory = new \Matomo\Cache\Backend\Factory(); 81 | $backend = $factory->buildBackend('file', $options); 82 | ``` 83 | 84 | ### Creating a Redis backend 85 | 86 | ```php 87 | $options = array( 88 | 'host' => '127.0.0.1', 89 | 'port' => '6379', 90 | 'timeout' => 0.0, 91 | 'database' => 15, // optional 92 | 'password' => 'secret', // optional 93 | ); 94 | $factory = new \Matomo\Cache\Backend\Factory(); 95 | $backend = $factory->buildBackend('redis', $options); 96 | ``` 97 | 98 | ### Creating a chained backend 99 | 100 | ```php 101 | $options = array( 102 | 'backends' => array('array', 'file'), 103 | 'file' => array('directory' => '/path/to/cache') 104 | ); 105 | $factory = new \Matomo\Cache\Backend\Factory(); 106 | $backend = $factory->buildBackend('redis', $options); 107 | ``` 108 | 109 | Whenever you set a cache entry it will save it in the array and in the file cache. Whenever you are trying to read a cache 110 | entry it will first try to get it from the fast array cache. In case it is not available there it will try to fetch 111 | the cache entry from the file system. If the cache entry exists on the file system it will cache the entry automatically 112 | using the array cache so the next read within this request will be fast and won't cause a stat call again. If you delete 113 | a cache entry it will be removed from all configured backends. You can chain any backends. It is recommended to list 114 | faster backends first. 115 | 116 | ### Creating a decorated backend with `DefaultTimeout` or `KeyPrefix` 117 | 118 | ```php 119 | $options = array( 120 | 'backend' => 'array', 121 | 'array' => array(), 122 | 'keyPrefix' => 'someKeyPrefixStr' 123 | ); 124 | $factory = new \Matomo\Cache\Backend\Factory(); 125 | $backend = $factory->buildBackend('keyPrefix', $options); 126 | ``` 127 | 128 | Sometimes its useful to set default a default parameter for the timeout to force prevent infinite lifetimes, or to 129 | specify a key prefix to prevent different versions of the app from reading and writing the same cache entry. 130 | `DefaultTimeout` and `KeyPrefix` are "decorators" that wrap another backend. Currently each takes a single configuration 131 | argument with the same name as the backend. 132 | 133 | |backend | argument | description | 134 | | --- | --- | --- | 135 | |DefaultTimeout|`defaultTimeout`| Uses the `integer` value as the default timeout for defining cache entry lifetime. Only comes to effect when no lifetime is specified; Prevents infinite lifetimes.| 136 | |KeyPrefix|`keyPrefix`| Prefixes the given value to the keys for any operation. For example `'Key123'` would become `'SomePrefixKey123'` | 137 | 138 | 139 | ### Creating a lazy cache 140 | 141 | [Description lazy cache.](#lazy) 142 | 143 | ```php 144 | $factory = new \Matomo\Cache\Backend\Factory(); 145 | $backend = $factory->buildBackend('file', array('directory' => '/path/to/cache')); 146 | 147 | $cache = new \Matomo\Cache\Lazy($backend); 148 | $cache->fetch('myid'); 149 | $cache->contains('myid'); 150 | $cache->delete('myid'); 151 | $cache->save('myid', 'myvalue', $lifeTimeInSeconds = 300); 152 | $cache->flushAll(); 153 | ``` 154 | 155 | ### Creating an eager cache 156 | 157 | [Description eager cache.](#eager) 158 | 159 | ```php 160 | $cache = new \Matomo\Cache\Eager($backend, $storageId = 'eagercache'); 161 | $cache->fetch('myid'); 162 | $cache->contains('myid'); 163 | $cache->delete('myid'); 164 | $cache->save('myid', new \stdClass()); 165 | $cache->persistCacheIfNeeded($lifeTimeInSeconds = 300); 166 | $cache->flushAll(); 167 | ``` 168 | 169 | It will cache all set cache entries under the cache entry `eagercache`. 170 | 171 | ### Creating a transient cache 172 | 173 | [Description transient cache.](#transient) 174 | 175 | ```php 176 | $cache = new \Matomo\Cache\Transient(); 177 | $cache->fetch('myid'); 178 | $cache->contains('myid'); 179 | $cache->delete('myid'); 180 | $cache->save('myid', new \stdClass()); 181 | $cache->flushAll(); 182 | ``` 183 | 184 | ## License 185 | 186 | The Cache component is released under the [LGPL v3.0](http://choosealicense.com/licenses/lgpl-3.0/). 187 | 188 | ## Changelog 189 | 190 | * 0.2.5: updating to doctrine/cache 1.4 which contains our fix 191 | * 0.2.4: do not throw exception when clearing a file cache if the cache dir doesn't exist 192 | * 0.2.3: fixed another race condition in file cache 193 | * 0.2.2: fixed a race condition in file cache 194 | * 0.2.0: Initial release 195 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "db4a0147a6469ac7376d7d4d8c92e796", 8 | "packages": [ 9 | { 10 | "name": "doctrine/cache", 11 | "version": "1.10.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/doctrine/cache.git", 15 | "reference": "13e3381b25847283a91948d04640543941309727" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727", 20 | "reference": "13e3381b25847283a91948d04640543941309727", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "~7.1 || ^8.0" 25 | }, 26 | "conflict": { 27 | "doctrine/common": ">2.2,<2.4" 28 | }, 29 | "require-dev": { 30 | "alcaeus/mongo-php-adapter": "^1.1", 31 | "doctrine/coding-standard": "^6.0", 32 | "mongodb/mongodb": "^1.1", 33 | "phpunit/phpunit": "^7.0", 34 | "predis/predis": "~1.0" 35 | }, 36 | "suggest": { 37 | "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" 38 | }, 39 | "type": "library", 40 | "extra": { 41 | "branch-alias": { 42 | "dev-master": "1.9.x-dev" 43 | } 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" 48 | } 49 | }, 50 | "notification-url": "https://packagist.org/downloads/", 51 | "license": [ 52 | "MIT" 53 | ], 54 | "authors": [ 55 | { 56 | "name": "Guilherme Blanco", 57 | "email": "guilhermeblanco@gmail.com" 58 | }, 59 | { 60 | "name": "Roman Borschel", 61 | "email": "roman@code-factory.org" 62 | }, 63 | { 64 | "name": "Benjamin Eberlei", 65 | "email": "kontakt@beberlei.de" 66 | }, 67 | { 68 | "name": "Jonathan Wage", 69 | "email": "jonwage@gmail.com" 70 | }, 71 | { 72 | "name": "Johannes Schmitt", 73 | "email": "schmittjoh@gmail.com" 74 | } 75 | ], 76 | "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", 77 | "homepage": "https://www.doctrine-project.org/projects/cache.html", 78 | "keywords": [ 79 | "abstraction", 80 | "apcu", 81 | "cache", 82 | "caching", 83 | "couchdb", 84 | "memcached", 85 | "php", 86 | "redis", 87 | "xcache" 88 | ], 89 | "support": { 90 | "issues": "https://github.com/doctrine/cache/issues", 91 | "source": "https://github.com/doctrine/cache/tree/1.10.x" 92 | }, 93 | "funding": [ 94 | { 95 | "url": "https://www.doctrine-project.org/sponsorship.html", 96 | "type": "custom" 97 | }, 98 | { 99 | "url": "https://www.patreon.com/phpdoctrine", 100 | "type": "patreon" 101 | }, 102 | { 103 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", 104 | "type": "tidelift" 105 | } 106 | ], 107 | "time": "2020-07-07T18:54:01+00:00" 108 | } 109 | ], 110 | "packages-dev": [ 111 | { 112 | "name": "doctrine/instantiator", 113 | "version": "1.4.0", 114 | "source": { 115 | "type": "git", 116 | "url": "https://github.com/doctrine/instantiator.git", 117 | "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" 118 | }, 119 | "dist": { 120 | "type": "zip", 121 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", 122 | "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", 123 | "shasum": "" 124 | }, 125 | "require": { 126 | "php": "^7.1 || ^8.0" 127 | }, 128 | "require-dev": { 129 | "doctrine/coding-standard": "^8.0", 130 | "ext-pdo": "*", 131 | "ext-phar": "*", 132 | "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", 133 | "phpstan/phpstan": "^0.12", 134 | "phpstan/phpstan-phpunit": "^0.12", 135 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" 136 | }, 137 | "type": "library", 138 | "autoload": { 139 | "psr-4": { 140 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 141 | } 142 | }, 143 | "notification-url": "https://packagist.org/downloads/", 144 | "license": [ 145 | "MIT" 146 | ], 147 | "authors": [ 148 | { 149 | "name": "Marco Pivetta", 150 | "email": "ocramius@gmail.com", 151 | "homepage": "https://ocramius.github.io/" 152 | } 153 | ], 154 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 155 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 156 | "keywords": [ 157 | "constructor", 158 | "instantiate" 159 | ], 160 | "support": { 161 | "issues": "https://github.com/doctrine/instantiator/issues", 162 | "source": "https://github.com/doctrine/instantiator/tree/1.4.0" 163 | }, 164 | "funding": [ 165 | { 166 | "url": "https://www.doctrine-project.org/sponsorship.html", 167 | "type": "custom" 168 | }, 169 | { 170 | "url": "https://www.patreon.com/phpdoctrine", 171 | "type": "patreon" 172 | }, 173 | { 174 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 175 | "type": "tidelift" 176 | } 177 | ], 178 | "time": "2020-11-10T18:47:58+00:00" 179 | }, 180 | { 181 | "name": "myclabs/deep-copy", 182 | "version": "1.10.2", 183 | "source": { 184 | "type": "git", 185 | "url": "https://github.com/myclabs/DeepCopy.git", 186 | "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" 187 | }, 188 | "dist": { 189 | "type": "zip", 190 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", 191 | "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", 192 | "shasum": "" 193 | }, 194 | "require": { 195 | "php": "^7.1 || ^8.0" 196 | }, 197 | "replace": { 198 | "myclabs/deep-copy": "self.version" 199 | }, 200 | "require-dev": { 201 | "doctrine/collections": "^1.0", 202 | "doctrine/common": "^2.6", 203 | "phpunit/phpunit": "^7.1" 204 | }, 205 | "type": "library", 206 | "autoload": { 207 | "psr-4": { 208 | "DeepCopy\\": "src/DeepCopy/" 209 | }, 210 | "files": [ 211 | "src/DeepCopy/deep_copy.php" 212 | ] 213 | }, 214 | "notification-url": "https://packagist.org/downloads/", 215 | "license": [ 216 | "MIT" 217 | ], 218 | "description": "Create deep copies (clones) of your objects", 219 | "keywords": [ 220 | "clone", 221 | "copy", 222 | "duplicate", 223 | "object", 224 | "object graph" 225 | ], 226 | "support": { 227 | "issues": "https://github.com/myclabs/DeepCopy/issues", 228 | "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" 229 | }, 230 | "funding": [ 231 | { 232 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 233 | "type": "tidelift" 234 | } 235 | ], 236 | "time": "2020-11-13T09:40:50+00:00" 237 | }, 238 | { 239 | "name": "phar-io/manifest", 240 | "version": "1.0.3", 241 | "source": { 242 | "type": "git", 243 | "url": "https://github.com/phar-io/manifest.git", 244 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" 245 | }, 246 | "dist": { 247 | "type": "zip", 248 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 249 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 250 | "shasum": "" 251 | }, 252 | "require": { 253 | "ext-dom": "*", 254 | "ext-phar": "*", 255 | "phar-io/version": "^2.0", 256 | "php": "^5.6 || ^7.0" 257 | }, 258 | "type": "library", 259 | "extra": { 260 | "branch-alias": { 261 | "dev-master": "1.0.x-dev" 262 | } 263 | }, 264 | "autoload": { 265 | "classmap": [ 266 | "src/" 267 | ] 268 | }, 269 | "notification-url": "https://packagist.org/downloads/", 270 | "license": [ 271 | "BSD-3-Clause" 272 | ], 273 | "authors": [ 274 | { 275 | "name": "Arne Blankerts", 276 | "email": "arne@blankerts.de", 277 | "role": "Developer" 278 | }, 279 | { 280 | "name": "Sebastian Heuer", 281 | "email": "sebastian@phpeople.de", 282 | "role": "Developer" 283 | }, 284 | { 285 | "name": "Sebastian Bergmann", 286 | "email": "sebastian@phpunit.de", 287 | "role": "Developer" 288 | } 289 | ], 290 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 291 | "support": { 292 | "issues": "https://github.com/phar-io/manifest/issues", 293 | "source": "https://github.com/phar-io/manifest/tree/master" 294 | }, 295 | "time": "2018-07-08T19:23:20+00:00" 296 | }, 297 | { 298 | "name": "phar-io/version", 299 | "version": "2.0.1", 300 | "source": { 301 | "type": "git", 302 | "url": "https://github.com/phar-io/version.git", 303 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" 304 | }, 305 | "dist": { 306 | "type": "zip", 307 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", 308 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", 309 | "shasum": "" 310 | }, 311 | "require": { 312 | "php": "^5.6 || ^7.0" 313 | }, 314 | "type": "library", 315 | "autoload": { 316 | "classmap": [ 317 | "src/" 318 | ] 319 | }, 320 | "notification-url": "https://packagist.org/downloads/", 321 | "license": [ 322 | "BSD-3-Clause" 323 | ], 324 | "authors": [ 325 | { 326 | "name": "Arne Blankerts", 327 | "email": "arne@blankerts.de", 328 | "role": "Developer" 329 | }, 330 | { 331 | "name": "Sebastian Heuer", 332 | "email": "sebastian@phpeople.de", 333 | "role": "Developer" 334 | }, 335 | { 336 | "name": "Sebastian Bergmann", 337 | "email": "sebastian@phpunit.de", 338 | "role": "Developer" 339 | } 340 | ], 341 | "description": "Library for handling version information and constraints", 342 | "support": { 343 | "issues": "https://github.com/phar-io/version/issues", 344 | "source": "https://github.com/phar-io/version/tree/master" 345 | }, 346 | "time": "2018-07-08T19:19:57+00:00" 347 | }, 348 | { 349 | "name": "phpdocumentor/reflection-common", 350 | "version": "2.1.0", 351 | "source": { 352 | "type": "git", 353 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 354 | "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b" 355 | }, 356 | "dist": { 357 | "type": "zip", 358 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b", 359 | "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b", 360 | "shasum": "" 361 | }, 362 | "require": { 363 | "php": ">=7.1" 364 | }, 365 | "type": "library", 366 | "extra": { 367 | "branch-alias": { 368 | "dev-master": "2.x-dev" 369 | } 370 | }, 371 | "autoload": { 372 | "psr-4": { 373 | "phpDocumentor\\Reflection\\": "src/" 374 | } 375 | }, 376 | "notification-url": "https://packagist.org/downloads/", 377 | "license": [ 378 | "MIT" 379 | ], 380 | "authors": [ 381 | { 382 | "name": "Jaap van Otterdijk", 383 | "email": "opensource@ijaap.nl" 384 | } 385 | ], 386 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 387 | "homepage": "http://www.phpdoc.org", 388 | "keywords": [ 389 | "FQSEN", 390 | "phpDocumentor", 391 | "phpdoc", 392 | "reflection", 393 | "static analysis" 394 | ], 395 | "support": { 396 | "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", 397 | "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master" 398 | }, 399 | "time": "2020-04-27T09:25:28+00:00" 400 | }, 401 | { 402 | "name": "phpdocumentor/reflection-docblock", 403 | "version": "4.3.4", 404 | "source": { 405 | "type": "git", 406 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 407 | "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" 408 | }, 409 | "dist": { 410 | "type": "zip", 411 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", 412 | "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", 413 | "shasum": "" 414 | }, 415 | "require": { 416 | "php": "^7.0", 417 | "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", 418 | "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", 419 | "webmozart/assert": "^1.0" 420 | }, 421 | "require-dev": { 422 | "doctrine/instantiator": "^1.0.5", 423 | "mockery/mockery": "^1.0", 424 | "phpdocumentor/type-resolver": "0.4.*", 425 | "phpunit/phpunit": "^6.4" 426 | }, 427 | "type": "library", 428 | "extra": { 429 | "branch-alias": { 430 | "dev-master": "4.x-dev" 431 | } 432 | }, 433 | "autoload": { 434 | "psr-4": { 435 | "phpDocumentor\\Reflection\\": [ 436 | "src/" 437 | ] 438 | } 439 | }, 440 | "notification-url": "https://packagist.org/downloads/", 441 | "license": [ 442 | "MIT" 443 | ], 444 | "authors": [ 445 | { 446 | "name": "Mike van Riel", 447 | "email": "me@mikevanriel.com" 448 | } 449 | ], 450 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 451 | "support": { 452 | "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", 453 | "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x" 454 | }, 455 | "time": "2019-12-28T18:55:12+00:00" 456 | }, 457 | { 458 | "name": "phpdocumentor/type-resolver", 459 | "version": "1.0.1", 460 | "source": { 461 | "type": "git", 462 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 463 | "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" 464 | }, 465 | "dist": { 466 | "type": "zip", 467 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", 468 | "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", 469 | "shasum": "" 470 | }, 471 | "require": { 472 | "php": "^7.1", 473 | "phpdocumentor/reflection-common": "^2.0" 474 | }, 475 | "require-dev": { 476 | "ext-tokenizer": "^7.1", 477 | "mockery/mockery": "~1", 478 | "phpunit/phpunit": "^7.0" 479 | }, 480 | "type": "library", 481 | "extra": { 482 | "branch-alias": { 483 | "dev-master": "1.x-dev" 484 | } 485 | }, 486 | "autoload": { 487 | "psr-4": { 488 | "phpDocumentor\\Reflection\\": "src" 489 | } 490 | }, 491 | "notification-url": "https://packagist.org/downloads/", 492 | "license": [ 493 | "MIT" 494 | ], 495 | "authors": [ 496 | { 497 | "name": "Mike van Riel", 498 | "email": "me@mikevanriel.com" 499 | } 500 | ], 501 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 502 | "support": { 503 | "issues": "https://github.com/phpDocumentor/TypeResolver/issues", 504 | "source": "https://github.com/phpDocumentor/TypeResolver/tree/0.7.2" 505 | }, 506 | "time": "2019-08-22T18:11:29+00:00" 507 | }, 508 | { 509 | "name": "phpspec/prophecy", 510 | "version": "v1.10.3", 511 | "source": { 512 | "type": "git", 513 | "url": "https://github.com/phpspec/prophecy.git", 514 | "reference": "451c3cd1418cf640de218914901e51b064abb093" 515 | }, 516 | "dist": { 517 | "type": "zip", 518 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", 519 | "reference": "451c3cd1418cf640de218914901e51b064abb093", 520 | "shasum": "" 521 | }, 522 | "require": { 523 | "doctrine/instantiator": "^1.0.2", 524 | "php": "^5.3|^7.0", 525 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 526 | "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", 527 | "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" 528 | }, 529 | "require-dev": { 530 | "phpspec/phpspec": "^2.5 || ^3.2", 531 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 532 | }, 533 | "type": "library", 534 | "extra": { 535 | "branch-alias": { 536 | "dev-master": "1.10.x-dev" 537 | } 538 | }, 539 | "autoload": { 540 | "psr-4": { 541 | "Prophecy\\": "src/Prophecy" 542 | } 543 | }, 544 | "notification-url": "https://packagist.org/downloads/", 545 | "license": [ 546 | "MIT" 547 | ], 548 | "authors": [ 549 | { 550 | "name": "Konstantin Kudryashov", 551 | "email": "ever.zet@gmail.com", 552 | "homepage": "http://everzet.com" 553 | }, 554 | { 555 | "name": "Marcello Duarte", 556 | "email": "marcello.duarte@gmail.com" 557 | } 558 | ], 559 | "description": "Highly opinionated mocking framework for PHP 5.3+", 560 | "homepage": "https://github.com/phpspec/prophecy", 561 | "keywords": [ 562 | "Double", 563 | "Dummy", 564 | "fake", 565 | "mock", 566 | "spy", 567 | "stub" 568 | ], 569 | "support": { 570 | "issues": "https://github.com/phpspec/prophecy/issues", 571 | "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" 572 | }, 573 | "time": "2020-03-05T15:02:03+00:00" 574 | }, 575 | { 576 | "name": "phpunit/php-code-coverage", 577 | "version": "6.1.4", 578 | "source": { 579 | "type": "git", 580 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 581 | "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" 582 | }, 583 | "dist": { 584 | "type": "zip", 585 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", 586 | "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", 587 | "shasum": "" 588 | }, 589 | "require": { 590 | "ext-dom": "*", 591 | "ext-xmlwriter": "*", 592 | "php": "^7.1", 593 | "phpunit/php-file-iterator": "^2.0", 594 | "phpunit/php-text-template": "^1.2.1", 595 | "phpunit/php-token-stream": "^3.0", 596 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 597 | "sebastian/environment": "^3.1 || ^4.0", 598 | "sebastian/version": "^2.0.1", 599 | "theseer/tokenizer": "^1.1" 600 | }, 601 | "require-dev": { 602 | "phpunit/phpunit": "^7.0" 603 | }, 604 | "suggest": { 605 | "ext-xdebug": "^2.6.0" 606 | }, 607 | "type": "library", 608 | "extra": { 609 | "branch-alias": { 610 | "dev-master": "6.1-dev" 611 | } 612 | }, 613 | "autoload": { 614 | "classmap": [ 615 | "src/" 616 | ] 617 | }, 618 | "notification-url": "https://packagist.org/downloads/", 619 | "license": [ 620 | "BSD-3-Clause" 621 | ], 622 | "authors": [ 623 | { 624 | "name": "Sebastian Bergmann", 625 | "email": "sebastian@phpunit.de", 626 | "role": "lead" 627 | } 628 | ], 629 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 630 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 631 | "keywords": [ 632 | "coverage", 633 | "testing", 634 | "xunit" 635 | ], 636 | "support": { 637 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 638 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master" 639 | }, 640 | "time": "2018-10-31T16:06:48+00:00" 641 | }, 642 | { 643 | "name": "phpunit/php-file-iterator", 644 | "version": "2.0.2", 645 | "source": { 646 | "type": "git", 647 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 648 | "reference": "050bedf145a257b1ff02746c31894800e5122946" 649 | }, 650 | "dist": { 651 | "type": "zip", 652 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", 653 | "reference": "050bedf145a257b1ff02746c31894800e5122946", 654 | "shasum": "" 655 | }, 656 | "require": { 657 | "php": "^7.1" 658 | }, 659 | "require-dev": { 660 | "phpunit/phpunit": "^7.1" 661 | }, 662 | "type": "library", 663 | "extra": { 664 | "branch-alias": { 665 | "dev-master": "2.0.x-dev" 666 | } 667 | }, 668 | "autoload": { 669 | "classmap": [ 670 | "src/" 671 | ] 672 | }, 673 | "notification-url": "https://packagist.org/downloads/", 674 | "license": [ 675 | "BSD-3-Clause" 676 | ], 677 | "authors": [ 678 | { 679 | "name": "Sebastian Bergmann", 680 | "email": "sebastian@phpunit.de", 681 | "role": "lead" 682 | } 683 | ], 684 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 685 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 686 | "keywords": [ 687 | "filesystem", 688 | "iterator" 689 | ], 690 | "support": { 691 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 692 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.2" 693 | }, 694 | "time": "2018-09-13T20:33:42+00:00" 695 | }, 696 | { 697 | "name": "phpunit/php-text-template", 698 | "version": "1.2.1", 699 | "source": { 700 | "type": "git", 701 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 702 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 703 | }, 704 | "dist": { 705 | "type": "zip", 706 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 707 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 708 | "shasum": "" 709 | }, 710 | "require": { 711 | "php": ">=5.3.3" 712 | }, 713 | "type": "library", 714 | "autoload": { 715 | "classmap": [ 716 | "src/" 717 | ] 718 | }, 719 | "notification-url": "https://packagist.org/downloads/", 720 | "license": [ 721 | "BSD-3-Clause" 722 | ], 723 | "authors": [ 724 | { 725 | "name": "Sebastian Bergmann", 726 | "email": "sebastian@phpunit.de", 727 | "role": "lead" 728 | } 729 | ], 730 | "description": "Simple template engine.", 731 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 732 | "keywords": [ 733 | "template" 734 | ], 735 | "support": { 736 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 737 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" 738 | }, 739 | "time": "2015-06-21T13:50:34+00:00" 740 | }, 741 | { 742 | "name": "phpunit/php-timer", 743 | "version": "2.1.2", 744 | "source": { 745 | "type": "git", 746 | "url": "https://github.com/sebastianbergmann/php-timer.git", 747 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" 748 | }, 749 | "dist": { 750 | "type": "zip", 751 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", 752 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", 753 | "shasum": "" 754 | }, 755 | "require": { 756 | "php": "^7.1" 757 | }, 758 | "require-dev": { 759 | "phpunit/phpunit": "^7.0" 760 | }, 761 | "type": "library", 762 | "extra": { 763 | "branch-alias": { 764 | "dev-master": "2.1-dev" 765 | } 766 | }, 767 | "autoload": { 768 | "classmap": [ 769 | "src/" 770 | ] 771 | }, 772 | "notification-url": "https://packagist.org/downloads/", 773 | "license": [ 774 | "BSD-3-Clause" 775 | ], 776 | "authors": [ 777 | { 778 | "name": "Sebastian Bergmann", 779 | "email": "sebastian@phpunit.de", 780 | "role": "lead" 781 | } 782 | ], 783 | "description": "Utility class for timing", 784 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 785 | "keywords": [ 786 | "timer" 787 | ], 788 | "support": { 789 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 790 | "source": "https://github.com/sebastianbergmann/php-timer/tree/master" 791 | }, 792 | "time": "2019-06-07T04:22:29+00:00" 793 | }, 794 | { 795 | "name": "phpunit/php-token-stream", 796 | "version": "3.1.1", 797 | "source": { 798 | "type": "git", 799 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 800 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" 801 | }, 802 | "dist": { 803 | "type": "zip", 804 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", 805 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", 806 | "shasum": "" 807 | }, 808 | "require": { 809 | "ext-tokenizer": "*", 810 | "php": "^7.1" 811 | }, 812 | "require-dev": { 813 | "phpunit/phpunit": "^7.0" 814 | }, 815 | "type": "library", 816 | "extra": { 817 | "branch-alias": { 818 | "dev-master": "3.1-dev" 819 | } 820 | }, 821 | "autoload": { 822 | "classmap": [ 823 | "src/" 824 | ] 825 | }, 826 | "notification-url": "https://packagist.org/downloads/", 827 | "license": [ 828 | "BSD-3-Clause" 829 | ], 830 | "authors": [ 831 | { 832 | "name": "Sebastian Bergmann", 833 | "email": "sebastian@phpunit.de" 834 | } 835 | ], 836 | "description": "Wrapper around PHP's tokenizer extension.", 837 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 838 | "keywords": [ 839 | "tokenizer" 840 | ], 841 | "support": { 842 | "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", 843 | "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.1" 844 | }, 845 | "abandoned": true, 846 | "time": "2019-09-17T06:23:10+00:00" 847 | }, 848 | { 849 | "name": "phpunit/phpunit", 850 | "version": "7.5.20", 851 | "source": { 852 | "type": "git", 853 | "url": "https://github.com/sebastianbergmann/phpunit.git", 854 | "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" 855 | }, 856 | "dist": { 857 | "type": "zip", 858 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", 859 | "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", 860 | "shasum": "" 861 | }, 862 | "require": { 863 | "doctrine/instantiator": "^1.1", 864 | "ext-dom": "*", 865 | "ext-json": "*", 866 | "ext-libxml": "*", 867 | "ext-mbstring": "*", 868 | "ext-xml": "*", 869 | "myclabs/deep-copy": "^1.7", 870 | "phar-io/manifest": "^1.0.2", 871 | "phar-io/version": "^2.0", 872 | "php": "^7.1", 873 | "phpspec/prophecy": "^1.7", 874 | "phpunit/php-code-coverage": "^6.0.7", 875 | "phpunit/php-file-iterator": "^2.0.1", 876 | "phpunit/php-text-template": "^1.2.1", 877 | "phpunit/php-timer": "^2.1", 878 | "sebastian/comparator": "^3.0", 879 | "sebastian/diff": "^3.0", 880 | "sebastian/environment": "^4.0", 881 | "sebastian/exporter": "^3.1", 882 | "sebastian/global-state": "^2.0", 883 | "sebastian/object-enumerator": "^3.0.3", 884 | "sebastian/resource-operations": "^2.0", 885 | "sebastian/version": "^2.0.1" 886 | }, 887 | "conflict": { 888 | "phpunit/phpunit-mock-objects": "*" 889 | }, 890 | "require-dev": { 891 | "ext-pdo": "*" 892 | }, 893 | "suggest": { 894 | "ext-soap": "*", 895 | "ext-xdebug": "*", 896 | "phpunit/php-invoker": "^2.0" 897 | }, 898 | "bin": [ 899 | "phpunit" 900 | ], 901 | "type": "library", 902 | "extra": { 903 | "branch-alias": { 904 | "dev-master": "7.5-dev" 905 | } 906 | }, 907 | "autoload": { 908 | "classmap": [ 909 | "src/" 910 | ] 911 | }, 912 | "notification-url": "https://packagist.org/downloads/", 913 | "license": [ 914 | "BSD-3-Clause" 915 | ], 916 | "authors": [ 917 | { 918 | "name": "Sebastian Bergmann", 919 | "email": "sebastian@phpunit.de", 920 | "role": "lead" 921 | } 922 | ], 923 | "description": "The PHP Unit Testing framework.", 924 | "homepage": "https://phpunit.de/", 925 | "keywords": [ 926 | "phpunit", 927 | "testing", 928 | "xunit" 929 | ], 930 | "support": { 931 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 932 | "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20" 933 | }, 934 | "time": "2020-01-08T08:45:45+00:00" 935 | }, 936 | { 937 | "name": "sebastian/code-unit-reverse-lookup", 938 | "version": "1.0.1", 939 | "source": { 940 | "type": "git", 941 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 942 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 943 | }, 944 | "dist": { 945 | "type": "zip", 946 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 947 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 948 | "shasum": "" 949 | }, 950 | "require": { 951 | "php": "^5.6 || ^7.0" 952 | }, 953 | "require-dev": { 954 | "phpunit/phpunit": "^5.7 || ^6.0" 955 | }, 956 | "type": "library", 957 | "extra": { 958 | "branch-alias": { 959 | "dev-master": "1.0.x-dev" 960 | } 961 | }, 962 | "autoload": { 963 | "classmap": [ 964 | "src/" 965 | ] 966 | }, 967 | "notification-url": "https://packagist.org/downloads/", 968 | "license": [ 969 | "BSD-3-Clause" 970 | ], 971 | "authors": [ 972 | { 973 | "name": "Sebastian Bergmann", 974 | "email": "sebastian@phpunit.de" 975 | } 976 | ], 977 | "description": "Looks up which function or method a line of code belongs to", 978 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 979 | "support": { 980 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 981 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/master" 982 | }, 983 | "time": "2017-03-04T06:30:41+00:00" 984 | }, 985 | { 986 | "name": "sebastian/comparator", 987 | "version": "3.0.2", 988 | "source": { 989 | "type": "git", 990 | "url": "https://github.com/sebastianbergmann/comparator.git", 991 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" 992 | }, 993 | "dist": { 994 | "type": "zip", 995 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 996 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 997 | "shasum": "" 998 | }, 999 | "require": { 1000 | "php": "^7.1", 1001 | "sebastian/diff": "^3.0", 1002 | "sebastian/exporter": "^3.1" 1003 | }, 1004 | "require-dev": { 1005 | "phpunit/phpunit": "^7.1" 1006 | }, 1007 | "type": "library", 1008 | "extra": { 1009 | "branch-alias": { 1010 | "dev-master": "3.0-dev" 1011 | } 1012 | }, 1013 | "autoload": { 1014 | "classmap": [ 1015 | "src/" 1016 | ] 1017 | }, 1018 | "notification-url": "https://packagist.org/downloads/", 1019 | "license": [ 1020 | "BSD-3-Clause" 1021 | ], 1022 | "authors": [ 1023 | { 1024 | "name": "Jeff Welch", 1025 | "email": "whatthejeff@gmail.com" 1026 | }, 1027 | { 1028 | "name": "Volker Dusch", 1029 | "email": "github@wallbash.com" 1030 | }, 1031 | { 1032 | "name": "Bernhard Schussek", 1033 | "email": "bschussek@2bepublished.at" 1034 | }, 1035 | { 1036 | "name": "Sebastian Bergmann", 1037 | "email": "sebastian@phpunit.de" 1038 | } 1039 | ], 1040 | "description": "Provides the functionality to compare PHP values for equality", 1041 | "homepage": "https://github.com/sebastianbergmann/comparator", 1042 | "keywords": [ 1043 | "comparator", 1044 | "compare", 1045 | "equality" 1046 | ], 1047 | "support": { 1048 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 1049 | "source": "https://github.com/sebastianbergmann/comparator/tree/master" 1050 | }, 1051 | "time": "2018-07-12T15:12:46+00:00" 1052 | }, 1053 | { 1054 | "name": "sebastian/diff", 1055 | "version": "3.0.2", 1056 | "source": { 1057 | "type": "git", 1058 | "url": "https://github.com/sebastianbergmann/diff.git", 1059 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" 1060 | }, 1061 | "dist": { 1062 | "type": "zip", 1063 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 1064 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 1065 | "shasum": "" 1066 | }, 1067 | "require": { 1068 | "php": "^7.1" 1069 | }, 1070 | "require-dev": { 1071 | "phpunit/phpunit": "^7.5 || ^8.0", 1072 | "symfony/process": "^2 || ^3.3 || ^4" 1073 | }, 1074 | "type": "library", 1075 | "extra": { 1076 | "branch-alias": { 1077 | "dev-master": "3.0-dev" 1078 | } 1079 | }, 1080 | "autoload": { 1081 | "classmap": [ 1082 | "src/" 1083 | ] 1084 | }, 1085 | "notification-url": "https://packagist.org/downloads/", 1086 | "license": [ 1087 | "BSD-3-Clause" 1088 | ], 1089 | "authors": [ 1090 | { 1091 | "name": "Kore Nordmann", 1092 | "email": "mail@kore-nordmann.de" 1093 | }, 1094 | { 1095 | "name": "Sebastian Bergmann", 1096 | "email": "sebastian@phpunit.de" 1097 | } 1098 | ], 1099 | "description": "Diff implementation", 1100 | "homepage": "https://github.com/sebastianbergmann/diff", 1101 | "keywords": [ 1102 | "diff", 1103 | "udiff", 1104 | "unidiff", 1105 | "unified diff" 1106 | ], 1107 | "support": { 1108 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1109 | "source": "https://github.com/sebastianbergmann/diff/tree/master" 1110 | }, 1111 | "time": "2019-02-04T06:01:07+00:00" 1112 | }, 1113 | { 1114 | "name": "sebastian/environment", 1115 | "version": "4.2.3", 1116 | "source": { 1117 | "type": "git", 1118 | "url": "https://github.com/sebastianbergmann/environment.git", 1119 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" 1120 | }, 1121 | "dist": { 1122 | "type": "zip", 1123 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 1124 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 1125 | "shasum": "" 1126 | }, 1127 | "require": { 1128 | "php": "^7.1" 1129 | }, 1130 | "require-dev": { 1131 | "phpunit/phpunit": "^7.5" 1132 | }, 1133 | "suggest": { 1134 | "ext-posix": "*" 1135 | }, 1136 | "type": "library", 1137 | "extra": { 1138 | "branch-alias": { 1139 | "dev-master": "4.2-dev" 1140 | } 1141 | }, 1142 | "autoload": { 1143 | "classmap": [ 1144 | "src/" 1145 | ] 1146 | }, 1147 | "notification-url": "https://packagist.org/downloads/", 1148 | "license": [ 1149 | "BSD-3-Clause" 1150 | ], 1151 | "authors": [ 1152 | { 1153 | "name": "Sebastian Bergmann", 1154 | "email": "sebastian@phpunit.de" 1155 | } 1156 | ], 1157 | "description": "Provides functionality to handle HHVM/PHP environments", 1158 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1159 | "keywords": [ 1160 | "Xdebug", 1161 | "environment", 1162 | "hhvm" 1163 | ], 1164 | "support": { 1165 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1166 | "source": "https://github.com/sebastianbergmann/environment/tree/4.2.3" 1167 | }, 1168 | "time": "2019-11-20T08:46:58+00:00" 1169 | }, 1170 | { 1171 | "name": "sebastian/exporter", 1172 | "version": "3.1.2", 1173 | "source": { 1174 | "type": "git", 1175 | "url": "https://github.com/sebastianbergmann/exporter.git", 1176 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" 1177 | }, 1178 | "dist": { 1179 | "type": "zip", 1180 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", 1181 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", 1182 | "shasum": "" 1183 | }, 1184 | "require": { 1185 | "php": "^7.0", 1186 | "sebastian/recursion-context": "^3.0" 1187 | }, 1188 | "require-dev": { 1189 | "ext-mbstring": "*", 1190 | "phpunit/phpunit": "^6.0" 1191 | }, 1192 | "type": "library", 1193 | "extra": { 1194 | "branch-alias": { 1195 | "dev-master": "3.1.x-dev" 1196 | } 1197 | }, 1198 | "autoload": { 1199 | "classmap": [ 1200 | "src/" 1201 | ] 1202 | }, 1203 | "notification-url": "https://packagist.org/downloads/", 1204 | "license": [ 1205 | "BSD-3-Clause" 1206 | ], 1207 | "authors": [ 1208 | { 1209 | "name": "Sebastian Bergmann", 1210 | "email": "sebastian@phpunit.de" 1211 | }, 1212 | { 1213 | "name": "Jeff Welch", 1214 | "email": "whatthejeff@gmail.com" 1215 | }, 1216 | { 1217 | "name": "Volker Dusch", 1218 | "email": "github@wallbash.com" 1219 | }, 1220 | { 1221 | "name": "Adam Harvey", 1222 | "email": "aharvey@php.net" 1223 | }, 1224 | { 1225 | "name": "Bernhard Schussek", 1226 | "email": "bschussek@gmail.com" 1227 | } 1228 | ], 1229 | "description": "Provides the functionality to export PHP variables for visualization", 1230 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1231 | "keywords": [ 1232 | "export", 1233 | "exporter" 1234 | ], 1235 | "support": { 1236 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1237 | "source": "https://github.com/sebastianbergmann/exporter/tree/master" 1238 | }, 1239 | "time": "2019-09-14T09:02:43+00:00" 1240 | }, 1241 | { 1242 | "name": "sebastian/global-state", 1243 | "version": "2.0.0", 1244 | "source": { 1245 | "type": "git", 1246 | "url": "https://github.com/sebastianbergmann/global-state.git", 1247 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" 1248 | }, 1249 | "dist": { 1250 | "type": "zip", 1251 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1252 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1253 | "shasum": "" 1254 | }, 1255 | "require": { 1256 | "php": "^7.0" 1257 | }, 1258 | "require-dev": { 1259 | "phpunit/phpunit": "^6.0" 1260 | }, 1261 | "suggest": { 1262 | "ext-uopz": "*" 1263 | }, 1264 | "type": "library", 1265 | "extra": { 1266 | "branch-alias": { 1267 | "dev-master": "2.0-dev" 1268 | } 1269 | }, 1270 | "autoload": { 1271 | "classmap": [ 1272 | "src/" 1273 | ] 1274 | }, 1275 | "notification-url": "https://packagist.org/downloads/", 1276 | "license": [ 1277 | "BSD-3-Clause" 1278 | ], 1279 | "authors": [ 1280 | { 1281 | "name": "Sebastian Bergmann", 1282 | "email": "sebastian@phpunit.de" 1283 | } 1284 | ], 1285 | "description": "Snapshotting of global state", 1286 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1287 | "keywords": [ 1288 | "global state" 1289 | ], 1290 | "support": { 1291 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1292 | "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" 1293 | }, 1294 | "time": "2017-04-27T15:39:26+00:00" 1295 | }, 1296 | { 1297 | "name": "sebastian/object-enumerator", 1298 | "version": "3.0.3", 1299 | "source": { 1300 | "type": "git", 1301 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1302 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 1303 | }, 1304 | "dist": { 1305 | "type": "zip", 1306 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1307 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1308 | "shasum": "" 1309 | }, 1310 | "require": { 1311 | "php": "^7.0", 1312 | "sebastian/object-reflector": "^1.1.1", 1313 | "sebastian/recursion-context": "^3.0" 1314 | }, 1315 | "require-dev": { 1316 | "phpunit/phpunit": "^6.0" 1317 | }, 1318 | "type": "library", 1319 | "extra": { 1320 | "branch-alias": { 1321 | "dev-master": "3.0.x-dev" 1322 | } 1323 | }, 1324 | "autoload": { 1325 | "classmap": [ 1326 | "src/" 1327 | ] 1328 | }, 1329 | "notification-url": "https://packagist.org/downloads/", 1330 | "license": [ 1331 | "BSD-3-Clause" 1332 | ], 1333 | "authors": [ 1334 | { 1335 | "name": "Sebastian Bergmann", 1336 | "email": "sebastian@phpunit.de" 1337 | } 1338 | ], 1339 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1340 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1341 | "support": { 1342 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1343 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master" 1344 | }, 1345 | "time": "2017-08-03T12:35:26+00:00" 1346 | }, 1347 | { 1348 | "name": "sebastian/object-reflector", 1349 | "version": "1.1.1", 1350 | "source": { 1351 | "type": "git", 1352 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1353 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 1354 | }, 1355 | "dist": { 1356 | "type": "zip", 1357 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 1358 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 1359 | "shasum": "" 1360 | }, 1361 | "require": { 1362 | "php": "^7.0" 1363 | }, 1364 | "require-dev": { 1365 | "phpunit/phpunit": "^6.0" 1366 | }, 1367 | "type": "library", 1368 | "extra": { 1369 | "branch-alias": { 1370 | "dev-master": "1.1-dev" 1371 | } 1372 | }, 1373 | "autoload": { 1374 | "classmap": [ 1375 | "src/" 1376 | ] 1377 | }, 1378 | "notification-url": "https://packagist.org/downloads/", 1379 | "license": [ 1380 | "BSD-3-Clause" 1381 | ], 1382 | "authors": [ 1383 | { 1384 | "name": "Sebastian Bergmann", 1385 | "email": "sebastian@phpunit.de" 1386 | } 1387 | ], 1388 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1389 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1390 | "support": { 1391 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1392 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/master" 1393 | }, 1394 | "time": "2017-03-29T09:07:27+00:00" 1395 | }, 1396 | { 1397 | "name": "sebastian/recursion-context", 1398 | "version": "3.0.0", 1399 | "source": { 1400 | "type": "git", 1401 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1402 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 1403 | }, 1404 | "dist": { 1405 | "type": "zip", 1406 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1407 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1408 | "shasum": "" 1409 | }, 1410 | "require": { 1411 | "php": "^7.0" 1412 | }, 1413 | "require-dev": { 1414 | "phpunit/phpunit": "^6.0" 1415 | }, 1416 | "type": "library", 1417 | "extra": { 1418 | "branch-alias": { 1419 | "dev-master": "3.0.x-dev" 1420 | } 1421 | }, 1422 | "autoload": { 1423 | "classmap": [ 1424 | "src/" 1425 | ] 1426 | }, 1427 | "notification-url": "https://packagist.org/downloads/", 1428 | "license": [ 1429 | "BSD-3-Clause" 1430 | ], 1431 | "authors": [ 1432 | { 1433 | "name": "Jeff Welch", 1434 | "email": "whatthejeff@gmail.com" 1435 | }, 1436 | { 1437 | "name": "Sebastian Bergmann", 1438 | "email": "sebastian@phpunit.de" 1439 | }, 1440 | { 1441 | "name": "Adam Harvey", 1442 | "email": "aharvey@php.net" 1443 | } 1444 | ], 1445 | "description": "Provides functionality to recursively process PHP variables", 1446 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1447 | "support": { 1448 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1449 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" 1450 | }, 1451 | "time": "2017-03-03T06:23:57+00:00" 1452 | }, 1453 | { 1454 | "name": "sebastian/resource-operations", 1455 | "version": "2.0.1", 1456 | "source": { 1457 | "type": "git", 1458 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1459 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" 1460 | }, 1461 | "dist": { 1462 | "type": "zip", 1463 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1464 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1465 | "shasum": "" 1466 | }, 1467 | "require": { 1468 | "php": "^7.1" 1469 | }, 1470 | "type": "library", 1471 | "extra": { 1472 | "branch-alias": { 1473 | "dev-master": "2.0-dev" 1474 | } 1475 | }, 1476 | "autoload": { 1477 | "classmap": [ 1478 | "src/" 1479 | ] 1480 | }, 1481 | "notification-url": "https://packagist.org/downloads/", 1482 | "license": [ 1483 | "BSD-3-Clause" 1484 | ], 1485 | "authors": [ 1486 | { 1487 | "name": "Sebastian Bergmann", 1488 | "email": "sebastian@phpunit.de" 1489 | } 1490 | ], 1491 | "description": "Provides a list of PHP built-in functions that operate on resources", 1492 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1493 | "support": { 1494 | "issues": "https://github.com/sebastianbergmann/resource-operations/issues", 1495 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" 1496 | }, 1497 | "time": "2018-10-04T04:07:39+00:00" 1498 | }, 1499 | { 1500 | "name": "sebastian/version", 1501 | "version": "2.0.1", 1502 | "source": { 1503 | "type": "git", 1504 | "url": "https://github.com/sebastianbergmann/version.git", 1505 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1506 | }, 1507 | "dist": { 1508 | "type": "zip", 1509 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1510 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1511 | "shasum": "" 1512 | }, 1513 | "require": { 1514 | "php": ">=5.6" 1515 | }, 1516 | "type": "library", 1517 | "extra": { 1518 | "branch-alias": { 1519 | "dev-master": "2.0.x-dev" 1520 | } 1521 | }, 1522 | "autoload": { 1523 | "classmap": [ 1524 | "src/" 1525 | ] 1526 | }, 1527 | "notification-url": "https://packagist.org/downloads/", 1528 | "license": [ 1529 | "BSD-3-Clause" 1530 | ], 1531 | "authors": [ 1532 | { 1533 | "name": "Sebastian Bergmann", 1534 | "email": "sebastian@phpunit.de", 1535 | "role": "lead" 1536 | } 1537 | ], 1538 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1539 | "homepage": "https://github.com/sebastianbergmann/version", 1540 | "support": { 1541 | "issues": "https://github.com/sebastianbergmann/version/issues", 1542 | "source": "https://github.com/sebastianbergmann/version/tree/master" 1543 | }, 1544 | "time": "2016-10-03T07:35:21+00:00" 1545 | }, 1546 | { 1547 | "name": "symfony/polyfill-ctype", 1548 | "version": "v1.20.0", 1549 | "source": { 1550 | "type": "git", 1551 | "url": "https://github.com/symfony/polyfill-ctype.git", 1552 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" 1553 | }, 1554 | "dist": { 1555 | "type": "zip", 1556 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", 1557 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", 1558 | "shasum": "" 1559 | }, 1560 | "require": { 1561 | "php": ">=7.1" 1562 | }, 1563 | "suggest": { 1564 | "ext-ctype": "For best performance" 1565 | }, 1566 | "type": "library", 1567 | "extra": { 1568 | "branch-alias": { 1569 | "dev-main": "1.20-dev" 1570 | }, 1571 | "thanks": { 1572 | "name": "symfony/polyfill", 1573 | "url": "https://github.com/symfony/polyfill" 1574 | } 1575 | }, 1576 | "autoload": { 1577 | "psr-4": { 1578 | "Symfony\\Polyfill\\Ctype\\": "" 1579 | }, 1580 | "files": [ 1581 | "bootstrap.php" 1582 | ] 1583 | }, 1584 | "notification-url": "https://packagist.org/downloads/", 1585 | "license": [ 1586 | "MIT" 1587 | ], 1588 | "authors": [ 1589 | { 1590 | "name": "Gert de Pagter", 1591 | "email": "BackEndTea@gmail.com" 1592 | }, 1593 | { 1594 | "name": "Symfony Community", 1595 | "homepage": "https://symfony.com/contributors" 1596 | } 1597 | ], 1598 | "description": "Symfony polyfill for ctype functions", 1599 | "homepage": "https://symfony.com", 1600 | "keywords": [ 1601 | "compatibility", 1602 | "ctype", 1603 | "polyfill", 1604 | "portable" 1605 | ], 1606 | "support": { 1607 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" 1608 | }, 1609 | "funding": [ 1610 | { 1611 | "url": "https://symfony.com/sponsor", 1612 | "type": "custom" 1613 | }, 1614 | { 1615 | "url": "https://github.com/fabpot", 1616 | "type": "github" 1617 | }, 1618 | { 1619 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1620 | "type": "tidelift" 1621 | } 1622 | ], 1623 | "time": "2020-10-23T14:02:19+00:00" 1624 | }, 1625 | { 1626 | "name": "theseer/tokenizer", 1627 | "version": "1.1.3", 1628 | "source": { 1629 | "type": "git", 1630 | "url": "https://github.com/theseer/tokenizer.git", 1631 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" 1632 | }, 1633 | "dist": { 1634 | "type": "zip", 1635 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1636 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1637 | "shasum": "" 1638 | }, 1639 | "require": { 1640 | "ext-dom": "*", 1641 | "ext-tokenizer": "*", 1642 | "ext-xmlwriter": "*", 1643 | "php": "^7.0" 1644 | }, 1645 | "type": "library", 1646 | "autoload": { 1647 | "classmap": [ 1648 | "src/" 1649 | ] 1650 | }, 1651 | "notification-url": "https://packagist.org/downloads/", 1652 | "license": [ 1653 | "BSD-3-Clause" 1654 | ], 1655 | "authors": [ 1656 | { 1657 | "name": "Arne Blankerts", 1658 | "email": "arne@blankerts.de", 1659 | "role": "Developer" 1660 | } 1661 | ], 1662 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1663 | "support": { 1664 | "issues": "https://github.com/theseer/tokenizer/issues", 1665 | "source": "https://github.com/theseer/tokenizer/tree/master" 1666 | }, 1667 | "time": "2019-06-13T22:48:21+00:00" 1668 | }, 1669 | { 1670 | "name": "webmozart/assert", 1671 | "version": "1.9.1", 1672 | "source": { 1673 | "type": "git", 1674 | "url": "https://github.com/webmozart/assert.git", 1675 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" 1676 | }, 1677 | "dist": { 1678 | "type": "zip", 1679 | "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", 1680 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", 1681 | "shasum": "" 1682 | }, 1683 | "require": { 1684 | "php": "^5.3.3 || ^7.0 || ^8.0", 1685 | "symfony/polyfill-ctype": "^1.8" 1686 | }, 1687 | "conflict": { 1688 | "phpstan/phpstan": "<0.12.20", 1689 | "vimeo/psalm": "<3.9.1" 1690 | }, 1691 | "require-dev": { 1692 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 1693 | }, 1694 | "type": "library", 1695 | "autoload": { 1696 | "psr-4": { 1697 | "Webmozart\\Assert\\": "src/" 1698 | } 1699 | }, 1700 | "notification-url": "https://packagist.org/downloads/", 1701 | "license": [ 1702 | "MIT" 1703 | ], 1704 | "authors": [ 1705 | { 1706 | "name": "Bernhard Schussek", 1707 | "email": "bschussek@gmail.com" 1708 | } 1709 | ], 1710 | "description": "Assertions to validate method input/output with nice error messages.", 1711 | "keywords": [ 1712 | "assert", 1713 | "check", 1714 | "validate" 1715 | ], 1716 | "support": { 1717 | "issues": "https://github.com/webmozart/assert/issues", 1718 | "source": "https://github.com/webmozart/assert/tree/master" 1719 | }, 1720 | "time": "2020-07-08T17:02:28+00:00" 1721 | } 1722 | ], 1723 | "aliases": [], 1724 | "minimum-stability": "stable", 1725 | "stability-flags": [], 1726 | "prefer-stable": false, 1727 | "prefer-lowest": false, 1728 | "platform": { 1729 | "php": ">=7.1" 1730 | }, 1731 | "platform-dev": [], 1732 | "platform-overrides": { 1733 | "php": "7.1.0" 1734 | }, 1735 | "plugin-api-version": "2.0.0" 1736 | } 1737 | --------------------------------------------------------------------------------