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