Merge pull request #665 from mcfedr/json-serializable

Add support for JsonSerializable classes
This commit is contained in:
William Durand 2015-07-04 13:52:22 +02:00
commit 787f69561b
8 changed files with 287 additions and 2 deletions

View File

@ -0,0 +1,94 @@
<?php
/**
* Created by mcfedr on 30/06/15 21:03
*/
namespace Nelmio\ApiDocBundle\Parser;
class JsonSerializableParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function supports(array $item)
{
if (!is_subclass_of($item['class'], 'JsonSerializable')) {
return false;
}
$ref = new \ReflectionClass($item['class']);
if ($ref->hasMethod('__construct')) {
foreach ($ref->getMethod('__construct')->getParameters() as $parameter) {
if (!$parameter->isOptional()) {
return false;
}
}
}
return true;
}
/**
* {@inheritdoc}
*/
public function parse(array $input)
{
/** @var \JsonSerializable $obj */
$obj = new $input['class']();
$encoded = $obj->jsonSerialize();
$parsed = $this->getItemMetaData($encoded);
if (isset($input['name']) && !empty($input['name'])) {
$output = array();
$output[$input['name']] = $parsed;
return $output;
}
return $parsed['children'];
}
public function getItemMetaData($item)
{
$type = gettype($item);
$meta = array(
'dataType' => $type == 'NULL' ? null : $type,
'actualType' => $type,
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null
);
if ($type == 'object' && $item instanceof \JsonSerializable) {
$meta = $this->getItemMetaData($item->jsonSerialize());
$meta['class'] = get_class($item);
} elseif (($type == 'object' && $item instanceof \stdClass) || ($type == 'array' && !$this->isSequential($item))) {
$meta['dataType'] = 'object';
$meta['children'] = array();
foreach ($item as $key => $value) {
$meta['children'][$key] = $this->getItemMetaData($value);
}
}
return $meta;
}
/**
* Check for numeric sequential keys, just like the json encoder does
* Credit: http://stackoverflow.com/a/25206156/859027
*
* @param array $arr
* @return bool
*/
private function isSequential(array $arr)
{
for ($i = count($arr) - 1; $i >= 0; $i--) {
if (!isset($arr[$i]) && !array_key_exists($i, $arr)) {
return false;
}
}
return true;
}
}

View File

@ -69,7 +69,24 @@ class ValidationParser implements ParserInterface, PostParserInterface
{ {
$className = $input['class']; $className = $input['class'];
return $this->doParse($className, array()); $parsed = $this->doParse($className, array());
if (isset($input['name']) && !empty($input['name'])) {
$output = array();
$output[$input['name']] = array(
'dataType' => 'object',
'actualType' => 'object',
'class' => $className,
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null,
'children' => $parsed
);
return $output;
}
return $parsed;
} }
/** /**

View File

@ -16,6 +16,7 @@
<parameter key="nelmio_api_doc.parser.collection_parser.class">Nelmio\ApiDocBundle\Parser\CollectionParser</parameter> <parameter key="nelmio_api_doc.parser.collection_parser.class">Nelmio\ApiDocBundle\Parser\CollectionParser</parameter>
<parameter key="nelmio_api_doc.parser.form_errors_parser.class">Nelmio\ApiDocBundle\Parser\FormErrorsParser</parameter> <parameter key="nelmio_api_doc.parser.form_errors_parser.class">Nelmio\ApiDocBundle\Parser\FormErrorsParser</parameter>
<parameter key="nelmio_api_doc.parser.json_serializable_parser.class">Nelmio\ApiDocBundle\Parser\JsonSerializableParser</parameter>
</parameters> </parameters>
<services> <services>
@ -68,6 +69,11 @@
<service id="nelmio_api_doc.parser.form_errors_parser" class="%nelmio_api_doc.parser.form_errors_parser.class%"> <service id="nelmio_api_doc.parser.form_errors_parser" class="%nelmio_api_doc.parser.form_errors_parser.class%">
<tag name="nelmio_api_doc.extractor.parser" /> <tag name="nelmio_api_doc.extractor.parser" />
</service> </service>
<!-- priority=1 means it comes before the validation parser, which can often add better type information -->
<service id="nelmio_api_doc.parser.json_serializable_parser" class="%nelmio_api_doc.parser.json_serializable_parser.class%">
<tag name="nelmio_api_doc.extractor.parser" priority="1" />
</service>
</services> </services>
</container> </container>

View File

@ -164,7 +164,7 @@ class YourController
* `parameters`: an array of parameters; * `parameters`: an array of parameters;
* `input`: the input type associated to the method (currently this supports Form Types, classes with JMS Serializer * `input`: the input type associated to the method (currently this supports Form Types, classes with JMS Serializer
metadata, and classes with Validation component metadata) useful for POST|PUT methods, either as FQCN or as form type metadata, classes with Validation component metadata and classes that implement JsonSerializable) useful for POST|PUT methods, either as FQCN or as form type
(if it is registered in the form factory in the container). (if it is registered in the form factory in the container).
* `output`: the output type associated with the response. Specified and parsed the same way as `input`. * `output`: the output type associated with the response. Specified and parsed the same way as `input`.

View File

@ -0,0 +1,22 @@
<?php
/**
* Created by mcfedr on 30/06/15 21:05
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
class JsonSerializableOptionalConstructorTest implements \JsonSerializable
{
public function __construct($optional = null)
{
}
/**
* {@inheritdoc}
*/
public function jsonSerialize()
{
return array();
}
}

View File

@ -0,0 +1,22 @@
<?php
/**
* Created by mcfedr on 30/06/15 21:05
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
class JsonSerializableRequiredConstructorTest implements \JsonSerializable
{
public function __construct($required)
{
}
/**
* {@inheritdoc}
*/
public function jsonSerialize()
{
return array();
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* Created by mcfedr on 30/06/15 21:05
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
class JsonSerializableTest implements \JsonSerializable
{
/**
* {@inheritdoc}
*/
public function jsonSerialize()
{
return array(
'id' => 123,
'name' => 'My name',
'child' => array(
'value' => array(1, 2, 3)
),
'another' => new JsonSerializableOptionalConstructorTest()
);
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* Created by mcfedr on 30/06/15 21:06
*/
namespace NelmioApiDocBundle\Tests\Parser;
use Nelmio\ApiDocBundle\Parser\JsonSerializableParser;
class JsonSerializableParserTest extends \PHPUnit_Framework_TestCase
{
/**
* @var JsonSerializableParser
*/
private $parser;
public function setUp()
{
$this->parser = new JsonSerializableParser();
}
/**
* @dataProvider dataTestParser
*/
public function testParser($property, $expected)
{
$result = $this->parser->parse(array('class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableTest'));
foreach ($expected as $name => $value) {
$this->assertArrayHasKey($property, $result);
$this->assertArrayHasKey($name, $result[$property]);
$this->assertEquals($result[$property][$name], $expected[$name]);
}
}
/**
* @dataProvider dataTestSupports
*/
public function testSupports($class, $expected)
{
$this->assertEquals($this->parser->supports(array('class' => $class)), $expected);
}
public function dataTestParser()
{
return array(
array(
'property' => 'id',
'expected' => array(
'dataType' => 'integer'
)
),
array(
'property' => 'name',
'expected' => array(
'dataType' => 'string'
)
),
array(
'property' => 'child',
'expected' => array(
'dataType' => 'object',
'children' => array(
'value' => array(
'dataType' => 'array',
'actualType' => 'array',
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null
)
)
)
)
);
}
public function dataTestSupports()
{
return array(
array(
'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableTest',
'expected' => true
),
array(
'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableRequiredConstructorTest',
'expected' => false
),
array(
'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JsonSerializableOptionalConstructorTest',
'expected' => true
),
array(
'class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Popo',
'expected' => false
)
);
}
}