NelmioApiDocBundle/Formatter/AbstractFormatter.php

205 lines
6.5 KiB
PHP
Raw Normal View History

2012-04-11 20:00:21 +02:00
<?php
2012-04-13 11:03:05 +02:00
/*
* This file is part of the NelmioApiDocBundle.
*
* (c) Nelmio <hello@nelm.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
2012-04-12 18:37:42 +02:00
namespace Nelmio\ApiDocBundle\Formatter;
2012-04-11 20:00:21 +02:00
2012-04-12 18:37:42 +02:00
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
Unified data types [actualType and subType] This is the result of https://github.com/nelmio/NelmioApiDocBundle/issues/410. This PR aims to provide a uniform way of declaring data-types of parameters for parsers and handlers to follow. In turn, this would allow formatters to determine data-types in a cleaner and less volatile manner. (See use-case that can be improved with this PR: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Formatter/AbstractFormatter.php#L103) This is possible by the addition two properties to each property item in `response`, and `parameters` fields in each API endpoint produced by the `ApiDocExtractor`: * `actualType` Contains a value from one of the `DataTypes` class constants. * `subType` Can contain either `null`, or any other `DataTypes` class constant. This is relevant when the `actualType` is a `DataTypes::COLLECTION`, wherein `subType` would specify the type of the collection items. It is also relevant when `actualType` is a `DataTypes::MODEL`, wherein `subType` would contain an identifier of the model (the FQCN or anything the parser would wish to specify) Examples: ```php array( 'id' => array( 'dataType' => 'integer', 'actualType' => DataTypes::INTEGER, 'subType' => null, ), 'profile' => array( 'dataType' => 'object (Profile)', 'actualType' => DataTypes::MODEL, 'subType' => 'Foo\Entity\Profile', 'children' => array( 'name' => array( 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, ), 'birthDate' => array( 'dataType' => 'date', 'actualType' => DataTypes::DATE, 'subType' => null, ), ) ), 'languages' => array( 'dataType' => 'array of strings', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::STRING, ), 'roles' => array( 'dataType' => 'array of choices', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::ENUM, ), 'groups' => array( 'dataType' => 'array of objects (Group)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Group', ), 'profileRevisions' => array( 'dataType' => 'array of objects (Profile)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Profile', ), 'address' => array( 'dataType' => 'object (a_type_a_custom_JMS_serializer_handler_handles)', 'actualType' => DataTypes::MODEL, 'subType' => 'a_type_a_custom_JMS_serializer_handler_handles', ), ); ``` When a formatter omits the `dataType` property or leaves it blank, it is inferred within `ApiDocExtractor` before everything is passed to formatters.
2014-06-17 17:05:00 -07:00
use Nelmio\ApiDocBundle\DataTypes;
2012-04-11 20:00:21 +02:00
2012-04-12 01:28:36 +02:00
abstract class AbstractFormatter implements FormatterInterface
2012-04-11 20:00:21 +02:00
{
2019-04-24 13:12:35 +03:00
protected $version;
2024-10-01 15:54:04 +03:00
public function setVersion($version): void
2019-04-24 13:12:35 +03:00
{
$this->version = $version;
}
public function formatOne(ApiDoc $annotation)
2012-04-12 01:28:36 +02:00
{
return $this->renderOne(
$this->processAnnotation($annotation->toArray())
);
2012-04-12 01:28:36 +02:00
}
public function format(array $collection)
{
return $this->render(
$this->processCollection($collection)
);
}
/**
* Format a single array of data
*
* @return string|array
*/
2012-05-23 00:33:01 +02:00
abstract protected function renderOne(array $data);
/**
* Format a set of resource sections.
*
* @return string|array
*/
2012-05-23 00:33:01 +02:00
abstract protected function render(array $collection);
2019-04-24 13:12:35 +03:00
/**
* Check that the versions range includes current version
*
2024-10-01 15:54:04 +03:00
* @param string $fromVersion (default: null)
* @param string $toVersion (default: null)
*
* @return bool
2019-04-24 13:12:35 +03:00
*/
protected function rangeIncludesVersion($fromVersion = null, $toVersion = null)
{
if (!$fromVersion && !$toVersion) {
return true;
}
if ($fromVersion && version_compare($fromVersion, $this->version, '>')) {
return false;
}
if ($toVersion && version_compare($toVersion, $this->version, '<')) {
return false;
}
2024-10-01 15:54:04 +03:00
2019-04-24 13:12:35 +03:00
return true;
}
/**
* Compresses nested parameters into a flat by changing the parameter
* names to strings which contain the nested property names, for example:
* `user[group][name]`
*
2024-10-01 15:54:04 +03:00
* @param string $parentName
* @param bool $ignoreNestedReadOnly
*
* @return array
*/
protected function compressNestedParameters(array $data, $parentName = null, $ignoreNestedReadOnly = false)
{
2024-10-01 15:54:04 +03:00
$newParams = [];
foreach ($data as $name => $info) {
2019-04-24 13:12:35 +03:00
if ($this->version && !$this->rangeIncludesVersion(
2024-10-01 15:54:04 +03:00
$info['sinceVersion'] ?? null,
$info['untilVersion'] ?? null
)) {
2019-04-24 13:12:35 +03:00
continue;
}
$newName = $this->getNewName($name, $info, $parentName);
2024-10-01 15:54:04 +03:00
$newParams[$newName] = [
'dataType' => $info['dataType'],
'readonly' => array_key_exists('readonly', $info) ? $info['readonly'] : null,
'required' => $info['required'],
'default' => array_key_exists('default', $info) ? $info['default'] : null,
'description' => array_key_exists('description', $info) ? $info['description'] : null,
'format' => array_key_exists('format', $info) ? $info['format'] : null,
'sinceVersion' => array_key_exists('sinceVersion', $info) ? $info['sinceVersion'] : null,
'untilVersion' => array_key_exists('untilVersion', $info) ? $info['untilVersion'] : null,
2024-10-01 15:54:04 +03:00
'actualType' => array_key_exists('actualType', $info) ? $info['actualType'] : null,
'subType' => array_key_exists('subType', $info) ? $info['subType'] : null,
'parentClass' => array_key_exists('parentClass', $info) ? $info['parentClass'] : null,
'field' => array_key_exists('field', $info) ? $info['field'] : null,
];
if (isset($info['children']) && (!$info['readonly'] || !$ignoreNestedReadOnly)) {
foreach ($this->compressNestedParameters($info['children'], $newName, $ignoreNestedReadOnly) as $nestedItemName => $nestedItemData) {
$newParams[$nestedItemName] = $nestedItemData;
}
}
}
return $newParams;
}
/**
* Returns a new property name, taking into account whether or not the property
* is an array of some other data type.
*
2024-10-01 15:54:04 +03:00
* @param string $name
* @param array $data
* @param string $parentName
*
* @return string
*/
protected function getNewName($name, $data, $parentName = null)
{
2024-10-01 15:54:04 +03:00
$array = '';
$newName = ($parentName) ? sprintf('%s[%s]', $parentName, $name) : $name;
2024-10-01 15:54:04 +03:00
if (isset($data['actualType']) && DataTypes::COLLECTION == $data['actualType']
&& isset($data['subType']) && null !== $data['subType']
) {
$array = '[]';
}
2024-10-01 15:54:04 +03:00
return sprintf('%s%s', $newName, $array);
}
/**
2024-10-01 15:54:04 +03:00
* @param array $annotation
*
* @return array
*/
protected function processAnnotation($annotation)
{
if (isset($annotation['parameters'])) {
$annotation['parameters'] = $this->compressNestedParameters($annotation['parameters'], null, true);
}
if (isset($annotation['response'])) {
$annotation['response'] = $this->compressNestedParameters($annotation['response']);
}
2016-01-26 04:21:08 +01:00
if (isset($annotation['parsedResponseMap'])) {
foreach ($annotation['parsedResponseMap'] as $statusCode => &$data) {
$data['model'] = $this->compressNestedParameters($data['model']);
}
}
2024-10-01 15:54:04 +03:00
$annotation['id'] = strtolower($annotation['method'] ?? '') . '-' . str_replace('/', '-', $annotation['uri'] ?? '');
return $annotation;
}
/**
* @param array[ApiDoc] $collection
2024-10-01 15:54:04 +03:00
*
* @return array
*/
protected function processCollection(array $collection)
{
2024-10-01 15:54:04 +03:00
$array = [];
foreach ($collection as $coll) {
$array[$coll['annotation']->getSection()][$coll['resource']][] = $coll['annotation']->toArray();
}
2024-10-01 15:54:04 +03:00
$processedCollection = [];
foreach ($array as $section => $resources) {
foreach ($resources as $path => $annotations) {
foreach ($annotations as $annotation) {
if ($section) {
$processedCollection[$section][$path][] = $this->processAnnotation($annotation);
} else {
$processedCollection['_others'][$path][] = $this->processAnnotation($annotation);
}
}
}
}
ksort($processedCollection);
return $processedCollection;
}
2012-04-11 20:00:21 +02:00
}