1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Cache\Storage\Adapter;
11
12use MongoCollection as MongoResource;
13use MongoDate;
14use MongoException as MongoResourceException;
15use stdClass;
16use Zend\Cache\Exception;
17use Zend\Cache\Storage\Capabilities;
18use Zend\Cache\Storage\FlushableInterface;
19
20class MongoDb extends AbstractAdapter implements FlushableInterface
21{
22    /**
23     * Has this instance be initialized
24     *
25     * @var bool
26     */
27    private $initialized = false;
28
29    /**
30     * the mongodb resource manager
31     *
32     * @var null|MongoDbResourceManager
33     */
34    private $resourceManager;
35
36    /**
37     * The mongodb resource id
38     *
39     * @var null|string
40     */
41    private $resourceId;
42
43    /**
44     * The namespace prefix
45     *
46     * @var string
47     */
48    private $namespacePrefix = '';
49
50    /**
51     * {@inheritDoc}
52     *
53     * @throws Exception\ExtensionNotLoadedException
54     */
55    public function __construct($options = null)
56    {
57        if (!class_exists('Mongo') || !class_exists('MongoClient')) {
58            throw new Exception\ExtensionNotLoadedException('MongoDb extension not loaded or Mongo polyfill not included');
59        }
60
61        parent::__construct($options);
62
63        $initialized = & $this->initialized;
64
65        $this->getEventManager()->attach(
66            'option',
67            function () use (& $initialized) {
68                $initialized = false;
69            }
70        );
71    }
72
73    /**
74     * get mongodb resource
75     *
76     * @return MongoResource
77     */
78    private function getMongoDbResource()
79    {
80        if (! $this->initialized) {
81            $options = $this->getOptions();
82
83            $this->resourceManager = $options->getResourceManager();
84            $this->resourceId      = $options->getResourceId();
85            $namespace             = $options->getNamespace();
86            $this->namespacePrefix = ($namespace === '' ? '' : $namespace . $options->getNamespaceSeparator());
87            $this->initialized     = true;
88        }
89
90        return $this->resourceManager->getResource($this->resourceId);
91    }
92
93    /**
94     * {@inheritDoc}
95     */
96    public function setOptions($options)
97    {
98        return parent::setOptions($options instanceof MongoDbOptions ? $options : new MongoDbOptions($options));
99    }
100
101    /**
102     * Get options.
103     *
104     * @return MongoDbOptions
105     * @see    setOptions()
106     */
107    public function getOptions()
108    {
109        return $this->options;
110    }
111
112    /**
113     * {@inheritDoc}
114     *
115     * @throws Exception\RuntimeException
116     */
117    protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
118    {
119        $result  = $this->fetchFromCollection($normalizedKey);
120        $success = false;
121
122        if (null === $result) {
123            return;
124        }
125
126        if (isset($result['expires'])) {
127            if (! $result['expires'] instanceof MongoDate) {
128                throw new Exception\RuntimeException(sprintf(
129                    "The found item _id '%s' for key '%s' is not a valid cache item"
130                    . ": the field 'expired' isn't an instance of MongoDate, '%s' found instead",
131                    (string) $result['_id'],
132                    $this->namespacePrefix . $normalizedKey,
133                    is_object($result['expires']) ? get_class($result['expires']) : gettype($result['expires'])
134                ));
135            }
136
137            if ($result['expires']->sec < time()) {
138                $this->internalRemoveItem($key);
139
140                return;
141            }
142        }
143
144        if (! array_key_exists('value', $result)) {
145            throw new Exception\RuntimeException(sprintf(
146                "The found item _id '%s' for key '%s' is not a valid cache item: missing the field 'value'",
147                (string) $result['_id'],
148                $this->namespacePrefix . $normalizedKey
149            ));
150        }
151
152        $success = true;
153
154        return $casToken = $result['value'];
155    }
156
157    /**
158     * {@inheritDoc}
159     *
160     * @throws Exception\RuntimeException
161     */
162    protected function internalSetItem(& $normalizedKey, & $value)
163    {
164        $mongo     = $this->getMongoDbResource();
165        $key       = $this->namespacePrefix . $normalizedKey;
166        $ttl       = $this->getOptions()->getTTl();
167        $expires   = null;
168        $cacheItem = array(
169            'key' => $key,
170            'value' => $value,
171        );
172
173        if ($ttl > 0) {
174            $expiresMicro         = microtime(true) + $ttl;
175            $expiresSecs          = (int) $expiresMicro;
176            $cacheItem['expires'] = new MongoDate($expiresSecs, $expiresMicro - $expiresSecs);
177        }
178
179        try {
180            $mongo->remove(array('key' => $key));
181
182            $result = $mongo->insert($cacheItem);
183        } catch (MongoResourceException $e) {
184            throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
185        }
186
187        return null !== $result && ((double) 1) === $result['ok'];
188    }
189
190    /**
191     * {@inheritDoc}
192     *
193     * @throws Exception\RuntimeException
194     */
195    protected function internalRemoveItem(& $normalizedKey)
196    {
197        try {
198            $result = $this->getMongoDbResource()->remove(array('key' => $this->namespacePrefix . $normalizedKey));
199        } catch (MongoResourceException $e) {
200            throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
201        }
202
203        return false !== $result
204            && ((double) 1) === $result['ok']
205            && $result['n'] > 0;
206    }
207
208    /**
209     * {@inheritDoc}
210     */
211    public function flush()
212    {
213        $result = $this->getMongoDbResource()->drop();
214
215        return ((double) 1) === $result['ok'];
216    }
217
218    /**
219     * {@inheritDoc}
220     */
221    protected function internalGetCapabilities()
222    {
223        if ($this->capabilities) {
224            return $this->capabilities;
225        }
226
227        return $this->capabilities = new Capabilities(
228            $this,
229            $this->capabilityMarker = new stdClass(),
230            array(
231                'supportedDatatypes' => array(
232                    'NULL'     => true,
233                    'boolean'  => true,
234                    'integer'  => true,
235                    'double'   => true,
236                    'string'   => true,
237                    'array'    => true,
238                    'object'   => false,
239                    'resource' => false,
240                ),
241                'supportedMetadata'  => array(
242                    '_id',
243                ),
244                'minTtl'             => 0,
245                'maxTtl'             => 0,
246                'staticTtl'          => true,
247                'ttlPrecision'       => 1,
248                'useRequestTime'     => false,
249                'expiredRead'        => false,
250                'maxKeyLength'       => 255,
251                'namespaceIsPrefix'  => true,
252            )
253        );
254    }
255
256    /**
257     * {@inheritDoc}
258     *
259     * @throws Exception\ExceptionInterface
260     */
261    protected function internalGetMetadata(& $normalizedKey)
262    {
263        $result = $this->fetchFromCollection($normalizedKey);
264
265        return null !== $result ? array('_id' => $result['_id']) : false;
266    }
267
268    /**
269     * Return raw records from MongoCollection
270     *
271     * @param string $normalizedKey
272     *
273     * @return array|null
274     *
275     * @throws Exception\RuntimeException
276     */
277    private function fetchFromCollection(& $normalizedKey)
278    {
279        try {
280            return $this->getMongoDbResource()->findOne(array('key' => $this->namespacePrefix . $normalizedKey));
281        } catch (MongoResourceException $e) {
282            throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
283        }
284    }
285}
286