Skip to content

Commit 20c6d24

Browse files
committed
Add a layer to transform the models into renderable content
1 parent c71564c commit 20c6d24

File tree

4 files changed

+318
-14
lines changed

4 files changed

+318
-14
lines changed

src/Charcoal/Sitemap/Service/Builder.php

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class Builder
5151
*/
5252
private $collectionLoader;
5353

54+
/**
55+
* @var SitemapPresenter
56+
*/
57+
private $sitemapPresenter;
58+
5459
/**
5560
* Create the Sitemap Builder.
5661
*
@@ -80,6 +85,7 @@ public function __construct(array $data)
8085
$this->setModelFactory($data['model/factory']);
8186
$this->setCollectionLoader($data['model/collection/loader']);
8287
$this->setTranslator($data['translator']);
88+
$this->setSitemapPresenter($data['sitemap/presenter']);
8389

8490
return $this;
8591
}
@@ -114,12 +120,13 @@ protected function defaultOptions()
114120
'l10n' => true,
115121
'check_active_routes' => true,
116122
'relative_urls' => true,
123+
'transformer' => null,
117124
'objects' => [
118125
'label' => '{{title}}',
119126
'url' => '{{url}}',
120127
'children' => [],
121128
'data' => [],
122-
],
129+
]
123130
];
124131
}
125132

@@ -179,6 +186,10 @@ public function build($ident = 'default')
179186
$options['relative_urls'] = $defaults['relative_urls'];
180187
}
181188

189+
if (!isset($options['transformer'])) {
190+
$options['transformer'] = $defaults['transformer'];
191+
}
192+
182193
$out[] = $this->buildObject($class, $options);
183194
}
184195

@@ -212,7 +223,7 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
212223
if (isset($options['filters'])) {
213224
$filters = $options['filters'];
214225
if ($parent) {
215-
$filters = $this->renderData($parent, $filters);
226+
$filters = $this->renderData($parent, $filters, $options['transformer']);
216227
}
217228
$loader->addFilters($filters);
218229
}
@@ -221,7 +232,7 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
221232
if (isset($options['orders'])) {
222233
$orders = $options['orders'];
223234
if ($parent) {
224-
$orders = $this->renderData($parent, $orders);
235+
$orders = $this->renderData($parent, $orders, $options['transformer']);
225236
}
226237
$loader->addOrders($orders);
227238
}
@@ -239,6 +250,7 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
239250
$defaultLocale = $options['locale'];
240251
$checkActiveRoutes = $options['check_active_routes'];
241252
$relativeUrls = $options['relative_urls'];
253+
$transformer = $options['transformer'];
242254

243255
// Locales
244256
$availableLocales = $l10n
@@ -264,38 +276,40 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
264276
continue;
265277
}
266278

279+
$presentedParent = $this->sitemapPresenter()->transform($object, $transformer);
280+
267281
// Hierarchical (children, when defined)
268282
$cs = [];
269283
if (!empty($children)) {
270284
foreach ($children as $cname => $opts) {
271285
$opts = array_merge($this->defaultOptions(), $opts);
272-
$cs[] = $this->buildObject($cname, $opts, $object, $level);
286+
$cs[] = $this->buildObject($cname, $opts, $presentedParent, $level);
273287
}
274288
}
275289

276290
$url = $relativeUrls
277-
? trim($this->renderData($object, $options['url']))
278-
: $this->withBaseUrl(trim($this->renderData($object, $options['url'])));
291+
? trim($this->renderData($object, $options['url'], $transformer))
292+
: $this->withBaseUrl(trim($this->renderData($object, $options['url'], $transformer)));
279293
$tmp = [
280-
'label' => trim($this->renderData($object, $options['label'])),
294+
'label' => trim($this->renderData($object, $options['label'], $transformer)),
281295
'url' => $url,
282296
'children' => $cs,
283-
'data' => $this->renderData($object, $options['data']),
297+
'data' => $this->renderData($object, $options['data'], $transformer),
284298
'level' => $level,
285299
'lang' => $locale,
286300
];
287301

288302
// If you need a priority, fix your own rules
289303
$priority = '';
290304
if (isset($options['priority']) && $options['priority']) {
291-
$priority = $this->renderData($object, (string)$options['priority']);
305+
$priority = $this->renderData($object, (string)$options['priority'], $transformer);
292306
}
293307
$tmp['priority'] = $priority;
294308

295309
// If you need a date of last modification, fix your own rules
296310
$last = '';
297311
if (isset($options['last_modified']) && $options['last_modified']) {
298-
$last = $this->renderData($object, $options['last_modified']);
312+
$last = $this->renderData($object, $options['last_modified'], $transformer);
299313
}
300314
$tmp['last_modified'] = $last;
301315

@@ -310,8 +324,8 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
310324
}
311325

312326
$url = $relativeUrls
313-
? trim($this->renderData($object, $options['url']))
314-
: $this->withBaseUrl(trim($this->renderData($object, $options['url'])));
327+
? trim($this->renderData($object, $options['url'], $transformer))
328+
: $this->withBaseUrl(trim($this->renderData($object, $options['url'], $transformer)));
315329

316330
$alternates[] = [
317331
'url' => $url,
@@ -336,10 +350,11 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
336350
* @param mixed $data Pretty much anything to be rendered
337351
* @return mixed Rendered data.
338352
*/
339-
protected function renderData(ViewableInterface $obj, $data)
353+
protected function renderData(ViewableInterface $obj, $data, $transformer = null)
340354
{
341355
if (is_scalar($data)) {
342-
return $obj->view()->render($data, $obj);
356+
$presentedObject = $this->sitemapPresenter()->transform($obj, $transformer);
357+
return $obj->view()->render($data, $presentedObject);
343358
}
344359

345360
if (is_array($data)) {
@@ -429,6 +444,24 @@ protected function setCollectionLoader(CollectionLoader $loader)
429444
$this->collectionLoader = $loader;
430445
}
431446

447+
/**
448+
* @return SitemapPresenter
449+
*/
450+
public function sitemapPresenter()
451+
{
452+
return $this->sitemapPresenter;
453+
}
454+
455+
/**
456+
* @param SitemapPresenter $sitemapPresenter
457+
* @return Builder
458+
*/
459+
public function setSitemapPresenter(SitemapPresenter $sitemapPresenter)
460+
{
461+
$this->sitemapPresenter = $sitemapPresenter;
462+
return $this;
463+
}
464+
432465
/**
433466
* Get the website's base URL.
434467
*
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?php
2+
3+
namespace Charcoal\Sitemap\Service;
4+
5+
use ArrayAccess;
6+
use Charcoal\Cache\Facade\CachePoolFacade;
7+
use Charcoal\Factory\FactoryInterface;
8+
use InvalidArgumentException;
9+
use Traversable;
10+
11+
/**
12+
* Presenter provides a presentation and transformation layer for a "model".
13+
*
14+
* It transforms (serializes) any data model (objects or array) into a presentation array, according to a
15+
* **transformer**.
16+
*
17+
* A **transformer** defines the morph rules
18+
*
19+
* - A simple array or Traversable object, contain
20+
*/
21+
class SitemapPresenter
22+
{
23+
/**
24+
* @var FactoryInterface $transformer
25+
*/
26+
private $transformerFactory;
27+
28+
/**
29+
* @var string $getterPattern
30+
*/
31+
private $getterPattern;
32+
33+
/**
34+
* @var CachePoolFacade
35+
*/
36+
protected $cacheFacade;
37+
38+
/**
39+
* @param array|Traversable|callable $transformer The data-view transformation array (or Traversable) object.
40+
* @param string $getterPattern The string pattern to match string with. Must have a single
41+
* catch-block.
42+
*/
43+
public function __construct($transformerFactory, $cacheFacade, $getterPattern = '~{{(\w*?)}}~')
44+
{
45+
$this->setCacheFacade($cacheFacade);
46+
$this->setTransformerFactory($transformerFactory);
47+
$this->getterPattern = $getterPattern;
48+
}
49+
50+
/**
51+
* @param mixed $obj The model or value object.
52+
* @param string|null $transformer The specific transformer to use.
53+
* @return array Normalized data, suitable as presentation (view) layer
54+
*/
55+
public function transform($obj, $transformer = null)
56+
{
57+
if (!is_object($transformer)) {
58+
$transformer = $this->getTransformerFactory()->create($transformer ?? $obj->objType());
59+
}
60+
61+
$key = sprintf(
62+
'%s_%s_%s',
63+
get_class($transformer),
64+
$obj->objType(),
65+
$obj->id()
66+
);
67+
68+
$that = $this;
69+
70+
return $this->getCacheFacade()->get($key,
71+
function () use ($obj, $transformer, $that) {
72+
return $that->transmogrify($obj, $transformer($obj));
73+
});
74+
}
75+
76+
/**
77+
* Transmogrify an object into an other structure.
78+
*
79+
* @param mixed $obj Source object.
80+
* @param mixed $val Modifier.
81+
* @throws InvalidArgumentException If the modifier is not callable, traversable (array) or string.
82+
* @return mixed The transformed data (type depends on modifier).
83+
*/
84+
private function transmogrify($obj, $val)
85+
{
86+
// Callbacks (lambda or callable) are supported. They must accept the source object as argument.
87+
if (!is_string($val) && is_callable($val)) {
88+
return $val($obj);
89+
}
90+
91+
// Arrays or traversables are handled recursively.
92+
// This also converts / casts any Traversable into a simple array.
93+
if (is_array($val) || $val instanceof Traversable) {
94+
$data = [];
95+
foreach ($val as $k => $v) {
96+
if (!is_string($k)) {
97+
if (is_string($v)) {
98+
$data[$v] = $this->objectGet($obj, $v);
99+
} else {
100+
$data[] = $v;
101+
}
102+
} else {
103+
$data[$k] = $this->transmogrify($obj, $v);
104+
}
105+
}
106+
return $data;
107+
}
108+
109+
// Strings are handled by rendering {{property}} with dynamic object getter pattern.
110+
if (is_string($val)) {
111+
return preg_replace_callback($this->getterPattern, function (array $matches) use ($obj) {
112+
return $this->objectGet($obj, $matches[1]);
113+
}, $val);
114+
}
115+
116+
if (is_numeric($val)) {
117+
return $val;
118+
}
119+
120+
if (is_bool($val)) {
121+
return !!$val;
122+
}
123+
124+
if ($val === null) {
125+
return null;
126+
}
127+
128+
// Any other
129+
throw new InvalidArgumentException(
130+
sprintf(
131+
'Presenter\'s transmogrify val needs to be callable, traversable (array) or a string. "%s" given.',
132+
gettype($val)
133+
)
134+
);
135+
}
136+
137+
/**
138+
* General-purpose dynamic object "getter".
139+
*
140+
* This method tries to fetch a "property" from any type of object (or array),
141+
* trying to figure out the best possible way:
142+
*
143+
* - Method call (`$obj->property()`)
144+
* - Public property get (`$obj->property`)
145+
* - Array access, if available (`$obj[property]`)
146+
* - Returns the property unchanged, otherwise
147+
*
148+
* @param mixed $obj The model (object or array) to retrieve the property's value from.
149+
* @param string $propertyName The property name (key) to retrieve from model.
150+
* @throws InvalidArgumentException If the property name is not a string.
151+
* @return mixed The object property, if available. The property name, unchanged, if it's not available.
152+
*/
153+
private function objectGet($obj, $propertyName)
154+
{
155+
if (is_callable([$obj, $propertyName])) {
156+
return $obj->{$propertyName}();
157+
}
158+
159+
if (isset($obj->{$propertyName})) {
160+
return $obj->{$propertyName};
161+
}
162+
163+
if (is_string($propertyName) && (is_array($obj) || $obj instanceof ArrayAccess) && (isset($obj[$propertyName]))) {
164+
return $obj[$propertyName];
165+
}
166+
167+
return null;
168+
}
169+
170+
/**
171+
* @return FactoryInterface
172+
*/
173+
protected function getTransformerFactory()
174+
{
175+
return $this->transformerFactory;
176+
}
177+
178+
/**
179+
* @param FactoryInterface $transformerFactory
180+
* @return Presenter
181+
*/
182+
public function setTransformerFactory(FactoryInterface $transformerFactory)
183+
{
184+
$this->transformerFactory = $transformerFactory;
185+
return $this;
186+
}
187+
188+
/**
189+
* @return mixed
190+
*/
191+
protected function getCacheFacade()
192+
{
193+
return $this->cacheFacade;
194+
}
195+
196+
/**
197+
* @param mixed $cacheFacade
198+
* @return Presenter
199+
*/
200+
protected function setCacheFacade($cacheFacade)
201+
{
202+
$this->cacheFacade = $cacheFacade;
203+
return $this;
204+
}
205+
}

0 commit comments

Comments
 (0)