Skip to content

Instantly share code, notes, and snippets.

@bakura10
Created March 1, 2014 21:06
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save bakura10/9297321 to your computer and use it in GitHub Desktop.
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