1: <?php
2: namespace Charcoal\Object;
3:
4: use \DateTime;
5: use \InvalidArgumentException;
6: use \RuntimeException;
7:
8: use \Pimple\Container;
9:
10: // Dependencies from 'charcoal-core'
11: use \Charcoal\Model\AbstractModel;
12: use \Charcoal\Loader\CollectionLoader;
13:
14: // Dependency from 'charcoal-factory'
15: use \Charcoal\Factory\FactoryInterface;
16:
17: // Local Dependency
18: use \Charcoal\Object\ObjectRouteInterface;
19:
20: /**
21: * Represents a route to an object (i.e., a permalink).
22: *
23: * Intended to be used to collect all routes related to models
24: * under a single source (e.g., database table).
25: *
26: * {@see Charcoal\Object\ObjectRevision} for a similar model that aggregates data
27: * under a common source.
28: *
29: * Requirements:
30: *
31: * - 'model/factory'
32: * - 'model/collection/loader'
33: */
34: class ObjectRoute extends AbstractModel implements
35: ObjectRouteInterface
36: {
37: /**
38: * A route is active by default.
39: *
40: * @var boolean
41: */
42: protected $active = true;
43:
44: /**
45: * The route's URI.
46: *
47: * @var string
48: */
49: protected $slug;
50:
51: /**
52: * The route's locale.
53: *
54: * @var string
55: */
56: protected $lang;
57:
58: /**
59: * The creation timestamp.
60: *
61: * @var DateTime
62: */
63: protected $creationDate;
64:
65: /**
66: * The last modification timestamp.
67: *
68: * @var DateTime
69: */
70: protected $lastModificationDate;
71:
72: /**
73: * The foreign object type related to this route.
74: *
75: * @var string
76: */
77: protected $routeObjType;
78:
79: /**
80: * The foreign object ID related to this route.
81: *
82: * @var mixed
83: */
84: protected $routeObjId;
85:
86: /**
87: * The foreign object's template identifier.
88: *
89: * @var string
90: */
91: protected $routeTemplate;
92:
93: /**
94: * Store a copy of the original—_preferred_—slug before alterations are made.
95: *
96: * @var string
97: */
98: private $originalSlug;
99:
100: /**
101: * Store the increment used to create a unique slug.
102: *
103: * @var integer
104: */
105: private $slugInc = 0;
106:
107: /**
108: * Store the factory instance for the current class.
109: *
110: * @var FactoryInterface
111: */
112: private $modelFactory;
113:
114: /**
115: * Store the collection loader for the current class.
116: *
117: * @var CollectionLoader
118: */
119: private $collectionLoader;
120:
121: /**
122: * Inject dependencies from a DI Container.
123: *
124: * @param Container $container A dependencies container instance.
125: * @return void
126: */
127: public function setDependencies(Container $container)
128: {
129: $this->setModelFactory($container['model/factory']);
130: $this->setCollectionLoader($container['model/collection/loader']);
131: }
132:
133: /**
134: * Event called before _creating_ the object.
135: *
136: * @see Charcoal\Source\StorableTrait::preSave() For the "create" Event.
137: * @return boolean
138: */
139: public function preSave()
140: {
141: $this->generateUniqueSlug();
142: $this->setCreationDate('now');
143: $this->setLastModificationDate('now');
144:
145: return parent::preSave();
146: }
147:
148: /**
149: * Event called before _updating_ the object.
150: *
151: * @see Charcoal\Source\StorableTrait::preUpdate() For the "update" Event.
152: * @param array $properties Optional. The list of properties to update.
153: * @return boolean
154: */
155: public function preUpdate(array $properties = null)
156: {
157: $this->setCreationDate('now');
158: $this->setLastModificationDate('now');
159:
160: return parent::preUpdate($properties);
161: }
162:
163: /**
164: * Determine if the current slug is unique.
165: *
166: * @return boolean
167: */
168: public function isSlugUnique()
169: {
170: $proto = $this->modelFactory()->get(self::class);
171: $loader = $this->collectionLoader();
172: $loader
173: ->reset()
174: ->setModel($proto)
175: ->addFilter('active', true)
176: ->addFilter('slug', $this->slug())
177: ->addOrder('creation_date', 'desc')
178: ->setPage(1)
179: ->setNumPerPage(1);
180: $routes = $loader->load()->objects();
181: if (!$routes) {
182: return true;
183: }
184: $obj = reset($routes);
185: if (!$obj->id()) {
186: return true;
187: }
188: if ($obj->id() === $this->id()) {
189: return true;
190: }
191: if ($obj->routeObjId() === $this->routeObjId() &&
192: $obj->routeObjType() === $this->routeObjType() &&
193: $obj->lang() === $this->lang()) {
194: $this->setId($obj->id());
195: return true;
196: }
197: return false;
198: }
199:
200: /**
201: * Generate a unique URL slug for routable object.
202: *
203: * @return self
204: */
205: public function generateUniqueSlug()
206: {
207: if (!$this->isSlugUnique()) {
208: if (!$this->originalSlug) {
209: $this->originalSlug = $this->slug();
210: }
211: $this->slugInc++;
212: $this->setSlug($this->originalSlug.'-'.$this->slugInc);
213: return $this->generateUniqueSlug();
214: }
215: return $this;
216: }
217:
218: /**
219: * Set an object model factory.
220: *
221: * @param FactoryInterface $factory The model factory, to create objects.
222: * @return self
223: */
224: protected function setModelFactory(FactoryInterface $factory)
225: {
226: $this->modelFactory = $factory;
227: return $this;
228: }
229:
230: /**
231: * Set a model collection loader.
232: *
233: * @param CollectionLoader $loader The collection loader.
234: * @return self
235: */
236: protected function setCollectionLoader(CollectionLoader $loader)
237: {
238: $this->collectionLoader = $loader;
239: return $this;
240: }
241:
242: /**
243: * Set the object route URI.
244: *
245: * @param string $slug The route.
246: * @return self
247: */
248: public function setSlug($slug)
249: {
250: $this->slug = $slug;
251:
252: return $this;
253: }
254:
255: /**
256: * Set the locale of the object route.
257: *
258: * @param string $lang The route's locale.
259: * @return self
260: */
261: public function setLang($lang)
262: {
263: $this->lang = $lang;
264:
265: return $this;
266: }
267:
268: /**
269: * @param \DateTime|string|null $creationDate The Creation Date date/time.
270: * @throws InvalidArgumentException If the date/time is invalid.
271: * @return self
272: */
273: public function setCreationDate($creationDate)
274: {
275: if ($creationDate === null) {
276: $this->creationDate = null;
277: return $this;
278: }
279:
280: if (is_string($creationDate)) {
281: $creationDate = new DateTime($creationDate);
282: }
283: if (!($creationDate instanceof DateTime)) {
284: throw new InvalidArgumentException(
285: 'Invalid "Creation Date" value. Must be a date/time string or a DateTime object.'
286: );
287: }
288: $this->creationDate = $creationDate;
289:
290: return $this;
291: }
292:
293: /**
294: * @param \DateTime|string|null $lastModificationDate The Last modification date date/time.
295: * @throws InvalidArgumentException If the date/time is invalid.
296: * @return self
297: */
298: public function setLastModificationDate($lastModificationDate)
299: {
300: if ($lastModificationDate === null) {
301: $this->lastModificationDate = null;
302: return $this;
303: }
304: if (is_string($lastModificationDate)) {
305: $lastModificationDate = new DateTime($lastModificationDate);
306: }
307: if (!($lastModificationDate instanceof DateTime)) {
308: throw new InvalidArgumentException(
309: 'Invalid "Creation Date" value. Must be a date/time string or a DateTime object.'
310: );
311: }
312: $this->lastModificationDate = $lastModificationDate;
313: return $this;
314: }
315:
316: /**
317: * Set the foreign object type related to this route.
318: *
319: * @param string $type The object type.
320: * @return self
321: */
322: public function setRouteObjType($type)
323: {
324: $this->routeObjType = $type;
325:
326: return $this;
327: }
328:
329: /**
330: * Set the foreign object ID related to this route.
331: *
332: * @param string $id The object ID.
333: * @return self
334: */
335: public function setRouteObjId($id)
336: {
337: $this->routeObjId = $id;
338:
339: return $this;
340: }
341:
342: /**
343: * Set the foreign object's template identifier.
344: *
345: * @param string $template The template identifier.
346: * @return self
347: */
348: public function setRouteTemplate($template)
349: {
350: $this->routeTemplate = $template;
351:
352: return $this;
353: }
354:
355: /**
356: * Retrieve the object model factory.
357: *
358: * @throws RuntimeException If the model factory was not previously set.
359: * @return FactoryInterface
360: */
361: public function modelFactory()
362: {
363: if (!isset($this->modelFactory)) {
364: throw new RuntimeException(
365: sprintf('Model Factory is not defined for "%s"', get_class($this))
366: );
367: }
368: return $this->modelFactory;
369: }
370:
371: /**
372: * Retrieve the model collection loader.
373: *
374: * @throws RuntimeException If the collection loader was not previously set.
375: * @return CollectionLoader
376: */
377: public function collectionLoader()
378: {
379: if (!isset($this->collectionLoader)) {
380: throw new RuntimeException(
381: sprintf('Collection Loader is not defined for "%s"', get_class($this))
382: );
383: }
384: return $this->collectionLoader;
385: }
386:
387: /**
388: * Retrieve the object route.
389: *
390: * @return string
391: */
392: public function slug()
393: {
394: return $this->slug;
395: }
396:
397: /**
398: * Retrieve the locale of the object route.
399: *
400: * @return string
401: */
402: public function lang()
403: {
404: return $this->lang;
405: }
406:
407: /**
408: * Creation date.
409: * @return DateTime Creation date.
410: */
411: public function creationDate()
412: {
413: return $this->creationDate;
414: }
415:
416: /**
417: * Last modification date.
418: * @return DateTime Last modification date.
419: */
420: public function lastModificationDate()
421: {
422: return $this->lastModificationDate;
423: }
424:
425: /**
426: * Retrieve the foreign object type related to this route.
427: *
428: * @return string
429: */
430: public function routeObjType()
431: {
432: return $this->routeObjType;
433: }
434:
435: /**
436: * Retrieve the foreign object ID related to this route.
437: *
438: * @return string
439: */
440: public function routeObjId()
441: {
442: return $this->routeObjId;
443: }
444:
445: /**
446: * Retrieve the foreign object's template identifier.
447: *
448: * @return string
449: */
450: public function routeTemplate()
451: {
452: return $this->routeTemplate;
453: }
454:
455: /**
456: * Alias of {@see self::slug()}.
457: *
458: * @return string
459: */
460: public function __toString()
461: {
462: return (string)$this->slug();
463: }
464: }
465: