1: <?php
2:
3: namespace Charcoal\Model\Service;
4:
5: use ArrayAccess;
6: use Exception;
7: use LogicException;
8: use InvalidArgumentException;
9:
10: // From PSR-6
11: use Psr\Cache\CacheItemPoolInterface;
12:
13: // From 'charcoal-factory'
14: use Charcoal\Factory\FactoryInterface;
15:
16: // From 'charcoal-core'
17: use Charcoal\Model\ModelInterface;
18:
19: /**
20: * Load a single model from its source, of from cache.
21: *
22: * Use the magic methods to load automatically from __call and __get; this allows
23: * for easy integration in templating engines like Mustache.
24: *
25: * > This object is immutable.
26: */
27: final class ModelLoader implements ArrayAccess
28: {
29: /**
30: * The object type.
31: *
32: * The base model (FQCN) to load objects of.
33: *
34: * @var string
35: */
36: private $objType;
37:
38: /**
39: * The object key.
40: *
41: * The model property name, or SQL column name, to load objects by.
42: *
43: * @var string
44: */
45: private $objKey;
46:
47: /**
48: * The model factory.
49: *
50: * @var FactoryInterface
51: */
52: private $factory;
53:
54: /**
55: * The PSR-6 caching service.
56: *
57: * @var CacheItemPoolInterface
58: */
59: private $cachePool;
60:
61: /**
62: * Construct a Model Loader with the dependencies
63: *
64: * # Required Dependencies
65: *
66: * - `obj_type`
67: * - `factory`
68: * - `cache`
69: *
70: * # Optional Dependencies
71: *
72: * - `obj_key`
73: *
74: * @param array $data Loader dependencies.
75: */
76: public function __construct(array $data)
77: {
78: $this->setObjType($data['obj_type']);
79: $this->setFactory($data['factory']);
80: $this->setCachePool($data['cache']);
81:
82: if (isset($data['obj_key'])) {
83: $this->setObjKey($data['obj_key']);
84: }
85: }
86:
87: // Magic Methods
88: // =============================================================================================
89:
90: /**
91: * Retrieve an object by its key.
92: *
93: * @param string|integer $ident The object identifier to load.
94: * @param mixed $args Unused; Method arguments.
95: * @return ModelInterface
96: */
97: public function __call($ident, $args = null)
98: {
99: unset($args);
100:
101: return $this->load($ident);
102: }
103:
104: /**
105: * Retrieve an object by its key.
106: *
107: * @param string|integer $ident The object identifier to load.
108: * @return ModelInterface
109: */
110: public function __get($ident)
111: {
112: return $this->load($ident);
113: }
114:
115: /**
116: * Determine if an object exists by its key.
117: *
118: * @todo Needs implementation
119: * @param string $ident The object identifier to lookup.
120: * @return boolean
121: */
122: public function __isset($ident)
123: {
124: return true;
125: }
126:
127: /**
128: * Remove an object by its key.
129: *
130: * @param string|integer $ident The object identifier to remove.
131: * @throws LogicException This method should never be called.
132: * @return void
133: */
134: public function __unset($ident)
135: {
136: throw new LogicException(
137: 'Can not unset value on a loader'
138: );
139: }
140:
141: // Satisfies ArrayAccess
142: // =============================================================================================
143:
144: /**
145: * Determine if an object exists by its key.
146: *
147: * @todo Needs implementation
148: * @see ArrayAccess::offsetExists
149: * @param string $ident The object identifier to lookup.
150: * @return boolean
151: */
152: public function offsetExists($ident)
153: {
154: return true;
155: }
156:
157: /**
158: * Retrieve an object by its key.
159: *
160: * @see ArrayAccess::offsetGet
161: * @param string|integer $ident The object identifier to load.
162: * @return ModelInterface
163: */
164: public function offsetGet($ident)
165: {
166: return $this->load($ident);
167: }
168:
169: /**
170: * Add an object.
171: *
172: * @see ArrayAccess::offsetSet
173: * @param string|integer $ident The $object identifier.
174: * @param mixed $obj The object to add.
175: * @throws LogicException This method should never be called.
176: * @return void
177: */
178: public function offsetSet($ident, $obj)
179: {
180: throw new LogicException(
181: 'Can not set value on a loader'
182: );
183: }
184:
185: /**
186: * Remove an object by its key.
187: *
188: * @see ArrayAccess::offsetUnset()
189: * @param string|integer $ident The object identifier to remove.
190: * @throws LogicException This method should never be called.
191: * @return void
192: */
193: public function offsetUnset($ident)
194: {
195: throw new LogicException(
196: 'Can not unset value on a loader'
197: );
198: }
199:
200: // =============================================================================================
201:
202: /**
203: * Retrieve an object, by its key, from its source or from the cache.
204: *
205: * When the cache is enabled, only the object's _data_ is stored. This prevents issues
206: * when unserializing a class that might have dependencies.
207: *
208: * @param string|integer $ident The object identifier to load.
209: * @param boolean $useCache If FALSE, ignore the cached object. Defaults to TRUE.
210: * @param boolean $reloadObj If TRUE, refresh the cached object. Defaults to FALSE.
211: * @return ModelInterface
212: */
213: public function load($ident, $useCache = true, $reloadObj = false)
214: {
215: if (!$useCache) {
216: return $this->loadFromSource($ident);
217: }
218:
219: $cacheKey = $this->cacheKey($ident);
220: $cacheItem = $this->cachePool->getItem($cacheKey);
221:
222: if (!$reloadObj) {
223: $objData = $cacheItem->get();
224: if ($cacheItem->isHit()) {
225: $obj = $this->factory->create($this->objType);
226: $obj->setData($objData);
227:
228: return $obj;
229: }
230: }
231:
232: $obj = $this->loadFromSource($ident);
233: $objData = $obj->data();
234: $this->cachePool->save($cacheItem->set($objData));
235:
236: return $obj;
237: }
238:
239: /**
240: * Load an objet from its soure.
241: *
242: * @param string|integer $ident The object identifier to load.
243: * @return ModelInterface
244: */
245: private function loadFromSource($ident)
246: {
247: $obj = $this->factory->create($this->objType);
248: if ($this->objKey) {
249: $obj->loadFrom($this->objKey, $ident);
250: } else {
251: $obj->load($ident);
252: }
253:
254: return $obj;
255: }
256:
257: /**
258: * Generate a cache key.
259: *
260: * @param string|integer $ident The object identifier to load.
261: * @return string
262: */
263: private function cacheKey($ident)
264: {
265: if ($this->objKey === null) {
266: $model = $this->factory->get($this->objType);
267: $this->setObjKey($model->key());
268: }
269:
270: $cacheKey = 'objects/'.str_replace('/', '.', $this->objType.'.'.$this->objKey.'.'.$ident);
271:
272: return $cacheKey;
273: }
274:
275: /**
276: * Set the object type.
277: *
278: * @param string $objType The object type to load with this loader.
279: * @throws InvalidArgumentException If the object type is not a string.
280: * @return ModelLoader Chainable
281: */
282: private function setObjType($objType)
283: {
284: if (!is_string($objType)) {
285: throw new InvalidArgumentException(
286: 'Can not set model loader object type: not a string'
287: );
288: }
289:
290: $this->objType = $objType;
291: return $this;
292: }
293:
294: /**
295: * Set the object key.
296: *
297: * @param string $objKey The object key to use for laoding.
298: * @throws InvalidArgumentException If the object key is not a string.
299: * @return ModelLoader Chainable
300: */
301: private function setObjKey($objKey)
302: {
303: if (empty($objKey) && !is_numeric($objKey)) {
304: $this->objKey = null;
305: return $this;
306: }
307:
308: if (!is_string($objKey)) {
309: throw new InvalidArgumentException(
310: 'Can not set model loader object type: not a string'
311: );
312: }
313:
314: $this->objKey = $objKey;
315: return $this;
316: }
317:
318: /**
319: * Set the model factory.
320: *
321: * @param FactoryInterface $factory The factory to create models.
322: * @return ModelLoader Chainable
323: */
324: private function setFactory(FactoryInterface $factory)
325: {
326: $this->factory = $factory;
327: return $this;
328: }
329:
330: /**
331: * Set the cache pool handler.
332: *
333: * @param CacheItemPoolInterface $cachePool A PSR-6 compatible cache pool.
334: * @return ModelLoader Chainable
335: */
336: private function setCachePool(CacheItemPoolInterface $cachePool)
337: {
338: $this->cachePool = $cachePool;
339: return $this;
340: }
341: }
342: