Skip to content

Commit

Permalink
Add a layer to transform the models into renderable content
Browse files Browse the repository at this point in the history
  • Loading branch information
BeneRoch committed Jan 19, 2023
1 parent c71564c commit 20c6d24
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 14 deletions.
61 changes: 47 additions & 14 deletions src/Charcoal/Sitemap/Service/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ class Builder
*/
private $collectionLoader;

/**
* @var SitemapPresenter
*/
private $sitemapPresenter;

/**
* Create the Sitemap Builder.
*
Expand Down Expand Up @@ -80,6 +85,7 @@ public function __construct(array $data)
$this->setModelFactory($data['model/factory']);
$this->setCollectionLoader($data['model/collection/loader']);
$this->setTranslator($data['translator']);
$this->setSitemapPresenter($data['sitemap/presenter']);

return $this;
}
Expand Down Expand Up @@ -114,12 +120,13 @@ protected function defaultOptions()
'l10n' => true,
'check_active_routes' => true,
'relative_urls' => true,
'transformer' => null,
'objects' => [
'label' => '{{title}}',
'url' => '{{url}}',
'children' => [],
'data' => [],
],
]
];
}

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

if (!isset($options['transformer'])) {
$options['transformer'] = $defaults['transformer'];
}

$out[] = $this->buildObject($class, $options);
}

Expand Down Expand Up @@ -212,7 +223,7 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
if (isset($options['filters'])) {
$filters = $options['filters'];
if ($parent) {
$filters = $this->renderData($parent, $filters);
$filters = $this->renderData($parent, $filters, $options['transformer']);
}
$loader->addFilters($filters);
}
Expand All @@ -221,7 +232,7 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
if (isset($options['orders'])) {
$orders = $options['orders'];
if ($parent) {
$orders = $this->renderData($parent, $orders);
$orders = $this->renderData($parent, $orders, $options['transformer']);
}
$loader->addOrders($orders);
}
Expand All @@ -239,6 +250,7 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
$defaultLocale = $options['locale'];
$checkActiveRoutes = $options['check_active_routes'];
$relativeUrls = $options['relative_urls'];
$transformer = $options['transformer'];

// Locales
$availableLocales = $l10n
Expand All @@ -264,38 +276,40 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
continue;
}

$presentedParent = $this->sitemapPresenter()->transform($object, $transformer);

// Hierarchical (children, when defined)
$cs = [];
if (!empty($children)) {
foreach ($children as $cname => $opts) {
$opts = array_merge($this->defaultOptions(), $opts);
$cs[] = $this->buildObject($cname, $opts, $object, $level);
$cs[] = $this->buildObject($cname, $opts, $presentedParent, $level);
}
}

$url = $relativeUrls
? trim($this->renderData($object, $options['url']))
: $this->withBaseUrl(trim($this->renderData($object, $options['url'])));
? trim($this->renderData($object, $options['url'], $transformer))
: $this->withBaseUrl(trim($this->renderData($object, $options['url'], $transformer)));
$tmp = [
'label' => trim($this->renderData($object, $options['label'])),
'label' => trim($this->renderData($object, $options['label'], $transformer)),
'url' => $url,
'children' => $cs,
'data' => $this->renderData($object, $options['data']),
'data' => $this->renderData($object, $options['data'], $transformer),
'level' => $level,
'lang' => $locale,
];

// If you need a priority, fix your own rules
$priority = '';
if (isset($options['priority']) && $options['priority']) {
$priority = $this->renderData($object, (string)$options['priority']);
$priority = $this->renderData($object, (string)$options['priority'], $transformer);
}
$tmp['priority'] = $priority;

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

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

$url = $relativeUrls
? trim($this->renderData($object, $options['url']))
: $this->withBaseUrl(trim($this->renderData($object, $options['url'])));
? trim($this->renderData($object, $options['url'], $transformer))
: $this->withBaseUrl(trim($this->renderData($object, $options['url'], $transformer)));

$alternates[] = [
'url' => $url,
Expand All @@ -336,10 +350,11 @@ protected function buildObject($class, $options, ViewableInterface $parent = nul
* @param mixed $data Pretty much anything to be rendered
* @return mixed Rendered data.
*/
protected function renderData(ViewableInterface $obj, $data)
protected function renderData(ViewableInterface $obj, $data, $transformer = null)
{
if (is_scalar($data)) {
return $obj->view()->render($data, $obj);
$presentedObject = $this->sitemapPresenter()->transform($obj, $transformer);
return $obj->view()->render($data, $presentedObject);
}

if (is_array($data)) {
Expand Down Expand Up @@ -429,6 +444,24 @@ protected function setCollectionLoader(CollectionLoader $loader)
$this->collectionLoader = $loader;
}

/**
* @return SitemapPresenter
*/
public function sitemapPresenter()
{
return $this->sitemapPresenter;
}

/**
* @param SitemapPresenter $sitemapPresenter
* @return Builder
*/
public function setSitemapPresenter(SitemapPresenter $sitemapPresenter)
{
$this->sitemapPresenter = $sitemapPresenter;
return $this;
}

/**
* Get the website's base URL.
*
Expand Down
205 changes: 205 additions & 0 deletions src/Charcoal/Sitemap/Service/SitemapPresenter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

namespace Charcoal\Sitemap\Service;

use ArrayAccess;
use Charcoal\Cache\Facade\CachePoolFacade;
use Charcoal\Factory\FactoryInterface;
use InvalidArgumentException;
use Traversable;

/**
* Presenter provides a presentation and transformation layer for a "model".
*
* It transforms (serializes) any data model (objects or array) into a presentation array, according to a
* **transformer**.
*
* A **transformer** defines the morph rules
*
* - A simple array or Traversable object, contain
*/
class SitemapPresenter
{
/**
* @var FactoryInterface $transformer
*/
private $transformerFactory;

/**
* @var string $getterPattern
*/
private $getterPattern;

/**
* @var CachePoolFacade
*/
protected $cacheFacade;

/**
* @param array|Traversable|callable $transformer The data-view transformation array (or Traversable) object.
* @param string $getterPattern The string pattern to match string with. Must have a single
* catch-block.
*/
public function __construct($transformerFactory, $cacheFacade, $getterPattern = '~{{(\w*?)}}~')
{
$this->setCacheFacade($cacheFacade);
$this->setTransformerFactory($transformerFactory);
$this->getterPattern = $getterPattern;
}

/**
* @param mixed $obj The model or value object.
* @param string|null $transformer The specific transformer to use.
* @return array Normalized data, suitable as presentation (view) layer
*/
public function transform($obj, $transformer = null)
{
if (!is_object($transformer)) {
$transformer = $this->getTransformerFactory()->create($transformer ?? $obj->objType());
}

$key = sprintf(
'%s_%s_%s',
get_class($transformer),
$obj->objType(),
$obj->id()
);

$that = $this;

return $this->getCacheFacade()->get($key,
function () use ($obj, $transformer, $that) {
return $that->transmogrify($obj, $transformer($obj));
});
}

/**
* Transmogrify an object into an other structure.
*
* @param mixed $obj Source object.
* @param mixed $val Modifier.
* @throws InvalidArgumentException If the modifier is not callable, traversable (array) or string.
* @return mixed The transformed data (type depends on modifier).
*/
private function transmogrify($obj, $val)
{
// Callbacks (lambda or callable) are supported. They must accept the source object as argument.
if (!is_string($val) && is_callable($val)) {
return $val($obj);
}

// Arrays or traversables are handled recursively.
// This also converts / casts any Traversable into a simple array.
if (is_array($val) || $val instanceof Traversable) {
$data = [];
foreach ($val as $k => $v) {
if (!is_string($k)) {
if (is_string($v)) {
$data[$v] = $this->objectGet($obj, $v);
} else {
$data[] = $v;
}
} else {
$data[$k] = $this->transmogrify($obj, $v);
}
}
return $data;
}

// Strings are handled by rendering {{property}} with dynamic object getter pattern.
if (is_string($val)) {
return preg_replace_callback($this->getterPattern, function (array $matches) use ($obj) {
return $this->objectGet($obj, $matches[1]);
}, $val);
}

if (is_numeric($val)) {
return $val;
}

if (is_bool($val)) {
return !!$val;
}

if ($val === null) {
return null;
}

// Any other
throw new InvalidArgumentException(
sprintf(
'Presenter\'s transmogrify val needs to be callable, traversable (array) or a string. "%s" given.',
gettype($val)
)
);
}

/**
* General-purpose dynamic object "getter".
*
* This method tries to fetch a "property" from any type of object (or array),
* trying to figure out the best possible way:
*
* - Method call (`$obj->property()`)
* - Public property get (`$obj->property`)
* - Array access, if available (`$obj[property]`)
* - Returns the property unchanged, otherwise
*
* @param mixed $obj The model (object or array) to retrieve the property's value from.
* @param string $propertyName The property name (key) to retrieve from model.
* @throws InvalidArgumentException If the property name is not a string.
* @return mixed The object property, if available. The property name, unchanged, if it's not available.
*/
private function objectGet($obj, $propertyName)
{
if (is_callable([$obj, $propertyName])) {
return $obj->{$propertyName}();
}

if (isset($obj->{$propertyName})) {
return $obj->{$propertyName};
}

if (is_string($propertyName) && (is_array($obj) || $obj instanceof ArrayAccess) && (isset($obj[$propertyName]))) {
return $obj[$propertyName];
}

return null;
}

/**
* @return FactoryInterface
*/
protected function getTransformerFactory()
{
return $this->transformerFactory;
}

/**
* @param FactoryInterface $transformerFactory
* @return Presenter
*/
public function setTransformerFactory(FactoryInterface $transformerFactory)
{
$this->transformerFactory = $transformerFactory;
return $this;
}

/**
* @return mixed
*/
protected function getCacheFacade()
{
return $this->cacheFacade;
}

/**
* @param mixed $cacheFacade
* @return Presenter
*/
protected function setCacheFacade($cacheFacade)
{
$this->cacheFacade = $cacheFacade;
return $this;
}
}
Loading

0 comments on commit 20c6d24

Please sign in to comment.