Skip to content

Instantly share code, notes, and snippets.

@develth
Last active August 4, 2017 08:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save develth/def3ed03dbb6b63e43770b368d3327b4 to your computer and use it in GitHub Desktop.
Save develth/def3ed03dbb6b63e43770b368d3327b4 to your computer and use it in GitHub Desktop.
Parse willdurand/Hateoas Relation into the api doc
<?php
namespace AppBundle\Parser;
use Hateoas\Configuration\Exclusion;
use Hateoas\Configuration\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\Parser\ParserInterface;
use Nelmio\ApiDocBundle\Parser\PostParserInterface;
/**
* Uses the metadata factory to extract input/output model information
*/
class HateoasParser implements ParserInterface, PostParserInterface {
/**
* @var \Metadata\MetadataFactoryInterface
*/
private $factory;
/**
* @var \Metadata\MetadataFactoryInterface
*/
private $jmsFactory;
/**
* @var PropertyNamingStrategyInterface
*/
private $namingStrategy;
/**
* Constructor, requires Metadata factory
*
* @param MetadataFactoryInterface $factory
* @param MetadataFactoryInterface $jmsFactory
* @param PropertyNamingStrategyInterface $namingStrategy
*/
public function __construct(MetadataFactoryInterface $factory, MetadataFactoryInterface $jmsFactory, PropertyNamingStrategyInterface $namingStrategy) {
$this->factory = $factory;
$this->jmsFactory = $jmsFactory;
$this->namingStrategy = $namingStrategy;
}
/**
* {@inheritdoc}
*/
public function supports(array $input) {
try {
if ($meta = $this->factory->getMetadataForClass($input['class'])) {
return true;
}
} catch (\ReflectionException $e) {
}
return false;
}
/**
* {@inheritdoc}
*/
public function parse(array $input) {
$className = $input['class'];
$groups = $input['groups'];
return $this->doParse($className, array(), $groups);
}
/**
* Recursively parse all metadata for a class
*
* @param string $className Class to get all metadata for
* @param array $visited Classes we've already visited to prevent infinite recursion.
* @param array $groups Serialization groups to include.
*
* @return array metadata for given class
* @throws \InvalidArgumentException
*/
protected function doParse($className, $visited = array(), array $groups = array()) {
/** @var ClassMetadata $meta */
$meta = $this->factory->getMetadataForClass($className);
$jmsMeta = $this->jmsFactory->getMetadataForClass($className);
$params = array();
if (null === $meta) {
return $params = array();
}
// iterate over property metadata
/** @var \Hateoas\Configuration\Relation $item */
foreach ($meta->getRelations() as $item) {
if (!is_null($item->getHref())) {
$exclusion = $item->getExclusion();
if ($exclusion && (count($groups) > 0)) {
if (true === $this->shouldSkipUsingGroups($exclusion->getGroups(), $groups)) {
continue;
}
}
if (!isset($params['_links'])) {
$params['_links'] = array(
'dataType' => "array of Objects",
'required' => false,
'readonly' => true,
'sinceVersion' => ($exclusion) ? $exclusion->getSinceVersion() : null,
'untilVersion' => ($exclusion) ? $exclusion->getUntilVersion() : null,
);
} else {
$params['_links']['sinceVersion'] = $this->versionDecide($params['_links']['sinceVersion'], ($exclusion) ? $exclusion->getSinceVersion() : null, "<");
$params['_links']['untilVersion'] = $this->versionDecide($params['_links']['untilVersion'], ($exclusion) ? $exclusion->getUntilVersion() : null, ">");
}
$params['_links[]['.$item->getName().']'] = array(
'dataType' => "Relation",
'required' => false,
'readonly' => true,
'sinceVersion' => ($exclusion) ? $exclusion->getSinceVersion() : null,
'untilVersion' => ($exclusion) ? $exclusion->getUntilVersion() : null,
);
$params['_links[]['.$item->getName().'][href]'] = array(
'dataType' => "string",
'required' => false,
'readonly' => true,
'sinceVersion' => ($exclusion) ? $exclusion->getSinceVersion() : null,
'untilVersion' => ($exclusion) ? $exclusion->getUntilVersion() : null,
);
}
}
/** @var \JMS\Serializer\Metadata\PropertyMetadata $item */
foreach ($jmsMeta->propertyMetadata as $item) {
if ($item->groups && (count($groups) > 0)) {
if (true === $this->shouldSkipUsingGroups($item->groups, $groups)) {
continue;
}
}
if (!is_null($item->type)) {
$name = $this->namingStrategy->translateName($item);
$dataType = $this->processDataType($item);
// we can use type property also for custom handlers, then we don't have here real class name
if (!class_exists($dataType['class'])) {
continue;
}
// if class already parsed, continue, to avoid infinite recursion
if (in_array($dataType['class'], $visited)) {
continue;
}
// check for nested classes with JMS metadata
if ($dataType['class'] && false === $this->isPrimitive($item->type['name']) && null !== $this->jmsFactory->getMetadataForClass($dataType['class'])) {
$visited[] = $dataType['class'];
$children = $this->doParse($dataType['class'], $visited, $groups);
if ($dataType['inline']) {
$params = array_merge($params, $children);
} else {
$params[$name]['children'] = $children;
}
}
}
}
return $params;
}
/**
* {@inheritDoc}
*/
public function postParse(array $input, array $parameters) {
return $parameters;
}
/**
* Figure out a normalized data type (for documentation), and get a
* nested class name, if available.
*
* @param $item
* @return array
*/
protected function processDataType($item) {
// check for a type inside something that could be treated as an array
if ($nestedType = $this->getNestedTypeInArray($item)) {
return array(
'class' => $nestedType,
'primitive' => false,
'inline' => false,
);
}
$type = $item->type['name'];
// we can use type property also for custom handlers, then we don't have here real class name
if (!class_exists($type)) {
return array(
'class' => $type,
'primitive' => false,
'inline' => false,
);
}
return array(
'class' => $type,
'primitive' => false,
'inline' => $item->inline,
);
}
protected function isPrimitive($type) {
return in_array($type, array('boolean', 'integer', 'string', 'float', 'double', 'array', 'DateTime'));
}
/**
* Check the various ways JMS describes values in arrays, and
* get the value type in the array
*
* @param PropertyMetadata $item
* @return string|null
*/
protected function getNestedTypeInArray(PropertyMetadata $item) {
if (isset($item->type['name']) && in_array($item->type['name'], array('array', 'ArrayCollection'))) {
if (isset($item->type['params'][1]['name'])) {
// E.g. array<string, MyNamespaceMyObject>
return $item->type['params'][1]['name'];
}
if (isset($item->type['params'][0]['name'])) {
// E.g. array<MyNamespaceMyObject>
return $item->type['params'][0]['name'];
}
}
return null;
}
/**
* Decide if we should provide the hateoas parameters
*
* @param array $itemGroups
* @param array $groups
* @return bool
*/
protected function shouldSkipUsingGroups(array $itemGroups, array $groups) {
foreach ($itemGroups as $group) {
if (in_array($group, $groups)) {
return false;
}
}
return true;
}
/**
* get the version based on operator
*
* @param $current
* @param $new
* @param $operator
*
* @return null
*/
protected function versionDecide($current, $new, $operator) {
if ($new && $current == null) {
return $new;
}
if ($current && $new) {
if (version_compare($current, $new, $operator)) {
return $current;
}
return $new;
}
return null;
}
}
services:
api-doc-hateoas.serializer.metadata.annotation_driver:
class: Hateoas\Configuration\Metadata\Driver\AnnotationDriver
arguments: ['@annotation_reader']
api-doc.hateoas.metadata.lazy_loading_driver:
class: Metadata\Driver\LazyLoadingDriver
arguments: ['@service_container', 'api-doc.hateoas.metadata.chain_driver']
api-doc.hateoas.metadata.chain_driver:
class: Metadata\Driver\DriverChain
arguments:
- ['@api-doc-hateoas.serializer.metadata.annotation_driver']
- ['@jms_serializer.metadata.yaml_driver']
- ['@jms_serializer.metadata.xml_driver']
- ['@jms_serializer.metadata.php_driver']
- ['@jms_serializer.metadata.annotation_driver']
api-doc.hateoas-metafactory:
class: Metadata\MetadataFactory
arguments: ['@api-doc.hateoas.metadata.lazy_loading_driver','Metadata\ClassHierarchyMetadata','']
api-doc.hateoas-parser:
class: AppBundle\Parser\HateoasParser
arguments: ['@api-doc.hateoas-metafactory', '@jms_serializer.metadata_factory', '@jms_serializer.naming_strategy']
tags:
- { name: nelmio_api_doc.extractor.parser, priority: -1 }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment