1<?php 2 3/* 4 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 5 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 6 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 7 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 8 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 9 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 10 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 11 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 12 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 13 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 14 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 * 16 * This software consists of voluntary contributions made by many individuals 17 * and is licensed under the MIT license. For more information, see 18 * <http://www.doctrine-project.org>. 19 */ 20 21namespace Doctrine\ORM\Cache\Region; 22 23use Doctrine\ORM\Cache\CollectionCacheEntry; 24use Doctrine\ORM\Cache\Lock; 25use Doctrine\ORM\Cache\Region; 26use Doctrine\ORM\Cache\CacheKey; 27use Doctrine\ORM\Cache\CacheEntry; 28use Doctrine\ORM\Cache\ConcurrentRegion; 29 30/** 31 * Very naive concurrent region, based on file locks. 32 * 33 * @since 2.5 34 * @author Fabio B. Silva <fabio.bat.silvagmail.com> 35 */ 36class FileLockRegion implements ConcurrentRegion 37{ 38 const LOCK_EXTENSION = 'lock'; 39 40 /** 41 * var \Doctrine\ORM\Cache\Region 42 */ 43 private $region; 44 45 /** 46 * @var string 47 */ 48 private $directory; 49 50 /** 51 * var integer 52 */ 53 private $lockLifetime; 54 55 /** 56 * @param \Doctrine\ORM\Cache\Region $region 57 * @param string $directory 58 * @param string $lockLifetime 59 * 60 * @throws \InvalidArgumentException 61 */ 62 public function __construct(Region $region, $directory, $lockLifetime) 63 { 64 if ( ! is_dir($directory) && ! @mkdir($directory, 0775, true)) { 65 throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory)); 66 } 67 68 if ( ! is_writable($directory)) { 69 throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory)); 70 } 71 72 $this->region = $region; 73 $this->directory = $directory; 74 $this->lockLifetime = $lockLifetime; 75 } 76 77 /** 78 * @param \Doctrine\ORM\Cache\CacheKey $key 79 * @param \Doctrine\ORM\Cache\Lock $lock 80 * 81 * @return boolean 82 */ 83 private function isLocked(CacheKey $key, Lock $lock = null) 84 { 85 $filename = $this->getLockFileName($key); 86 87 if ( ! is_file($filename)) { 88 return false; 89 } 90 91 $time = $this->getLockTime($filename); 92 $content = $this->getLockContent($filename); 93 94 if ( ! $content || ! $time) { 95 @unlink($filename); 96 97 return false; 98 } 99 100 if ($lock && $content === $lock->value) { 101 return false; 102 } 103 104 // outdated lock 105 if (($time + $this->lockLifetime) <= time()) { 106 @unlink($filename); 107 108 return false; 109 } 110 111 return true; 112 } 113 114 /** 115 * @param \Doctrine\ORM\Cache\CacheKey $key 116 * 117 * return string 118 */ 119 private function getLockFileName(CacheKey $key) 120 { 121 return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION; 122 } 123 124 /** 125 * @param string $filename 126 * 127 * return string 128 */ 129 private function getLockContent($filename) 130 { 131 return @file_get_contents($filename); 132 } 133 134 /** 135 * @param string $filename 136 * 137 * return integer 138 */ 139 private function getLockTime($filename) 140 { 141 return @fileatime($filename); 142 } 143 144 /** 145 * {inheritdoc} 146 */ 147 public function getName() 148 { 149 return $this->region->getName(); 150 } 151 152 /** 153 * {inheritdoc} 154 */ 155 public function contains(CacheKey $key) 156 { 157 if ($this->isLocked($key)) { 158 return false; 159 } 160 161 return $this->region->contains($key); 162 } 163 164 /** 165 * {inheritdoc} 166 */ 167 public function get(CacheKey $key) 168 { 169 if ($this->isLocked($key)) { 170 return null; 171 } 172 173 return $this->region->get($key); 174 } 175 176 /** 177 * {@inheritdoc} 178 */ 179 public function getMultiple(CollectionCacheEntry $collection) 180 { 181 if (array_filter(array_map([$this, 'isLocked'], $collection->identifiers))) { 182 return null; 183 } 184 185 return $this->region->getMultiple($collection); 186 } 187 188 /** 189 * {inheritdoc} 190 */ 191 public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) 192 { 193 if ($this->isLocked($key, $lock)) { 194 return false; 195 } 196 197 return $this->region->put($key, $entry); 198 } 199 200 /** 201 * {inheritdoc} 202 */ 203 public function evict(CacheKey $key) 204 { 205 if ($this->isLocked($key)) { 206 @unlink($this->getLockFileName($key)); 207 } 208 209 return $this->region->evict($key); 210 } 211 212 /** 213 * {inheritdoc} 214 */ 215 public function evictAll() 216 { 217 // The check below is necessary because on some platforms glob returns false 218 // when nothing matched (even though no errors occurred) 219 $filenames = glob(sprintf("%s/*.%s" , $this->directory, self::LOCK_EXTENSION)); 220 221 if ($filenames) { 222 foreach ($filenames as $filename) { 223 @unlink($filename); 224 } 225 } 226 227 return $this->region->evictAll(); 228 } 229 230 /** 231 * {inheritdoc} 232 */ 233 public function lock(CacheKey $key) 234 { 235 if ($this->isLocked($key)) { 236 return null; 237 } 238 239 $lock = Lock::createLockRead(); 240 $filename = $this->getLockFileName($key); 241 242 if ( ! @file_put_contents($filename, $lock->value, LOCK_EX)) { 243 return null; 244 } 245 chmod($filename, 0664); 246 247 return $lock; 248 } 249 250 /** 251 * {inheritdoc} 252 */ 253 public function unlock(CacheKey $key, Lock $lock) 254 { 255 if ($this->isLocked($key, $lock)) { 256 return false; 257 } 258 259 if ( ! @unlink($this->getLockFileName($key))) { 260 return false; 261 } 262 263 return true; 264 } 265} 266