Last active
August 4, 2017 08:05
-
-
Save develth/def3ed03dbb6b63e43770b368d3327b4 to your computer and use it in GitHub Desktop.
Parse willdurand/Hateoas Relation into the api doc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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