This is a renderer for Ember-Data for ZfrRest
<?php | |
/* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
* | |
* This software consists of voluntary contributions made by many individuals | |
* and is licensed under the MIT license. | |
*/ | |
namespace Application\View\Renderer; | |
use Doctrine\Common\Util\Inflector; | |
use Zend\Paginator\Paginator; | |
use Zend\Stdlib\Hydrator\HydratorPluginManager; | |
use Zend\View\HelperPluginManager as ViewHelperPluginManager; | |
use ZfrRest\Resource\Metadata\ResourceMetadataFactory; | |
use ZfrRest\Resource\Metadata\ResourceMetadataInterface; | |
use ZfrRest\Resource\ResourceInterface; | |
use ZfrRest\View\Model\ResourceModel; | |
use ZfrRest\View\Renderer\AbstractResourceRenderer; | |
/** | |
* Renderer that output Ember-Data compliant data | |
* | |
* @author Michaël Gallego <mic.gallego@gmail.com> | |
* @licence MIT | |
*/ | |
class EmberResourceRenderer extends AbstractResourceRenderer | |
{ | |
/** | |
* @var ResourceMetadataFactory | |
*/ | |
protected $resourceMetadataFactory; | |
/** | |
* @var HydratorPluginManager | |
*/ | |
protected $hydratorManager; | |
/** | |
* @var ViewHelperPluginManager | |
*/ | |
protected $viewHelperManager; | |
/** | |
* @var \Zend\View\Helper\Url | |
*/ | |
protected $urlHelper; | |
/** | |
* @param ResourceMetadataFactory $resourceMetadataFactory | |
* @param HydratorPluginManager $hydratorManager | |
* @param ViewHelperPluginManager $viewHelperManager | |
*/ | |
public function __construct( | |
ResourceMetadataFactory $resourceMetadataFactory, | |
HydratorPluginManager $hydratorManager, | |
ViewHelperPluginManager $viewHelperManager | |
) { | |
$this->resourceMetadataFactory = $resourceMetadataFactory; | |
$this->hydratorManager = $hydratorManager; | |
$this->viewHelperManager = $viewHelperManager; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
public function render($nameOrModel, $values = null) | |
{ | |
if (!$nameOrModel instanceof ResourceModel) { | |
return; | |
} | |
$resource = $nameOrModel->getResource(); | |
$this->urlHelper = $this->viewHelperManager->get('Url'); | |
if ($resource->isCollection()) { | |
$payload = $this->renderCollection($resource); | |
} else { | |
$payload = $this->renderItem($resource); | |
} | |
// EmberJS accepts a "meta" section where data like pagination is inserted | |
$payload = array_merge($payload, $this->renderMeta($resource)); | |
return json_encode($payload); | |
} | |
public function renderItem(ResourceInterface $resource) | |
{ | |
$metadata = $resource->getMetadata(); | |
/** @var \Zend\Stdlib\Hydrator\HydratorInterface $hydrator */ | |
$hydrator = $this->hydratorManager->get($metadata->getHydratorName()); | |
$values = $hydrator->extract($resource->getData()); | |
// We need to wrap the data around the root key | |
$rootKey = $this->getRootKey($metadata->getReflectionClass()->getName(), false); | |
$payload[$rootKey] = &$values; | |
$embedded = $this->normalizeAssociations($resource, $values); | |
return array_merge($payload, $embedded); | |
} | |
public function renderCollection(ResourceInterface $resource) | |
{ | |
$metadata = $resource->getMetadata(); | |
/** @var \Zend\Stdlib\Hydrator\HydratorInterface $hydrator */ | |
$hydrator = $this->hydratorManager->get($metadata->getCollectionMetadata()->getHydratorName()); | |
$values = []; | |
foreach ($resource->getData() as $data) { | |
$values[] = $hydrator->extract($data); | |
} | |
$rootKey = $this->getRootKey($metadata->getReflectionClass()->getName(), true); | |
$payload[$rootKey] = &$values; | |
$embedded = []; | |
foreach ($values as &$value) { | |
$embedded = array_merge($embedded, $this->normalizeAssociations($resource, $value)); | |
} | |
return array_merge($payload, $embedded); | |
} | |
/** | |
* Render meta data | |
* | |
* @param ResourceInterface $resource | |
* @return array | |
*/ | |
protected function renderMeta(ResourceInterface $resource) | |
{ | |
$data = $resource->getData(); | |
$meta = []; | |
if ($data instanceof Paginator) { | |
$meta = array_merge($meta, [ | |
'limit' => $data->getItemCountPerPage(), | |
'offset' => ($data->getCurrentPageNumber() - 1) * $data->getItemCountPerPage(), | |
'total' => $data->getTotalItemCount() | |
]); | |
} | |
// Ember-Data expects all meta to be wrap around a "meta" top-key | |
return ['meta' => $meta]; | |
} | |
/** | |
* Generate the root key | |
* | |
* @param string $className | |
* @param bool $isCollection | |
* @return string | |
*/ | |
protected function getRootKey($className, $isCollection) | |
{ | |
$parts = explode('\\', $className); | |
$key = lcfirst(end($parts)); | |
return $isCollection ? Inflector::pluralize($key) : $key; | |
} | |
/** | |
* @param ResourceInterface $resource | |
* @param array $values | |
* @return array | |
*/ | |
private function normalizeAssociations(ResourceInterface $resource, array &$values) | |
{ | |
$resourceMetadata = $resource->getMetadata(); | |
$classMetadata = $resourceMetadata->getClassMetadata(); | |
$urlHelper = $this->urlHelper; | |
$links = []; | |
$embedded = []; | |
$associations = $classMetadata->getAssociationNames(); | |
foreach ($associations as $association) { | |
// If association IS NOT in the payload but is exposed in the resource metadata, we | |
// generate a link | |
if (!isset($values[$association])) { | |
if ($resourceMetadata->hasAssociationMetadata($association)) { | |
$links[$association] = $urlHelper(null, ['resource' => $resource, 'association' => $association]); | |
} | |
continue; | |
} | |
$associationValues = &$values[$association]; | |
// If the representation is not embedded, we do nothing | |
if (!$this->isAssociationEmbedded($associationValues)) { | |
continue; | |
} | |
/** @var ResourceMetadataInterface $associationResourceMetadata */ | |
$associationResourceMetadata = $this->resourceMetadataFactory->getMetadataForClass( | |
$classMetadata->getAssociationTargetClass($association) | |
); | |
$associationResource = $associationResourceMetadata->createResource(); | |
// Otherwise, we extract the identifiers, let them in the original payload, and extract | |
// everything else to embedded association | |
$identifier = array_flip($classMetadata->getIdentifierFieldNames()); | |
$multiValued = $classMetadata->isCollectionValuedAssociation($association); | |
$embeddedRootKey = Inflector::pluralize($association); | |
if (!$multiValued) { | |
$identifierValue = array_intersect_key($associationValues, $identifier); | |
$identifierValue = reset($identifierValue); | |
$embedded = array_merge( | |
$embedded, | |
$this->normalizeAssociations($associationResource, $associationValues) | |
); | |
$embedded[$embeddedRootKey][$identifierValue] = $associationValues; | |
$associationValues = $identifierValue; | |
} else { | |
foreach ($associationValues as &$associationValue) { | |
$identifierValue = array_intersect_key($associationValue, $identifier); | |
$identifierValue = reset($identifierValue); | |
$embedded = array_merge( | |
$embedded, | |
$this->normalizeAssociations($associationResource, $associationValue) | |
); | |
$embedded[$embeddedRootKey][$identifierValue] = $associationValue; | |
$associationValue = $identifierValue; | |
} | |
} | |
$embedded[$embeddedRootKey] = array_values($embedded[$embeddedRootKey]); | |
} | |
if (!empty($links)) { | |
$values['links'] = $links; | |
} | |
return $embedded; | |
} | |
/** | |
* Detect if association values represent an embedded representation of this resource | |
* | |
* @param mixed $associationValues | |
* @return bool | |
*/ | |
private function isAssociationEmbedded($associationValues) | |
{ | |
return !is_scalar($associationValues) | |
&& (is_array(reset($associationValues)) || array_values($associationValues) !== $associationValues); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment