Skip to content
This repository

added support for JMS Serializer GroupsExclusionStrategy #156

Merged
merged 2 commits into from about 1 year ago

9 participants

Jeroen Fiege William Durand Jordi Boggiano Peter Kruithof Pierre-Yves LEBECQ Christophe Coevoet MaksSlesarenko Benjamin Laugueux
Jeroen Fiege
fieg commented

A proposal to support the GroupsExclusionStrategy of the JMS Serializer.

From the updated readme in this PR:

When using a class with JMS Serializer metadata, you can use exclude groups by using this syntax: input="Acme\YourBundle\Entity\User@update,public". In this case the groups 'update' and 'public' are used.

William Durand
Collaborator

looks good to me

Jordi Boggiano
Owner

To me too, except that the annotation syntax is a bit strange and not very future proof. I don't have a better idea though.

README.md
... ... @@ -101,7 +101,9 @@ The following properties are available:
101 101 * `filters`: an array of filters;
102 102
103 103 * `input`: the input type associated to the method, currently this supports Form Types, and classes with JMS Serializer
104   - metadata, useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container)
  104 + metadata, useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container).
  105 + When using a class with JMS Serializer metadata, you can use exclude groups by using this syntax:
1
Peter Kruithof
pkruithof added a note

The term "exclude" implicates that these groups are not used. I understand the relation to the ExclusionStrategy from the Serializer, but I think that term is ambiguous also. It would be more clear if it said: "you can use specific groups by using this syntax".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Peter Kruithof

@Seldaek an alternative would be to use an additional annotation, something like inputgroup=blah. But I think it makes more sense to suffix the input line.

Maybe a # or :: would make more sense as a separator? I don't know what the most commonly used notation for class/property definitions is, but it seems to me that would be useful here too.

Jeroen Fiege
fieg commented

The notation is indeed arguable. Another suggestion might be:

input={
    "class"="Acme\Bundle\Entity\User", 
    "groups"="update, public"
}

This makes it also extendable in the future.

Peter Kruithof

:+1: for that notation

Parser/JmsMetadataParser.php
... ... @@ -80,6 +86,9 @@ protected function doParse($className, $visited = array())
80 86 throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className));
81 87 }
82 88
  89 + $context = new NavigatorContext(GraphNavigator::DIRECTION_SERIALIZATION, 'json'); //TODO: the exclusionStrategy has a hard dependency on this, despite it isn't even used :(
  90 + $exclusionStrategy = new GroupsExclusionStrategy($groups);
1
Christophe Coevoet
stof added a note

What about the VersionExclusionStrategy ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Pierre-Yves LEBECQ

This would be a great feature. And as @stof said, VersionExclusionStrategy would be really nice too.
For the annotation, another proposal, which is a little tweak of @fieg last proposal :

// Supporting the old syntax
input="Acme\Bundle\Entity\User"
// Which would be equals to
input={
    "class"="Acme\Bundle\Entity\User",
    "groups"={"Default"}
}
// And of course could be customized by typing
input={
    "class"="Acme\Bundle\Entity\User",
    "groups"={"Update", "Public"}
}

The only difference here would be to declare groups as an array instead of a string with separators. What dou you think ?

Jeroen Fiege
fieg commented

Good suggestions @stof and @pylebecq. Will look into it.

Jeroen Fiege
fieg commented

Ok, I tackled the input/output notation.

Does anyone want to help with the VersionExclusionStrategy support? Let me know so I can add you as a contributor to the feature branch. I'm running a little short on time here...

Parser/JmsMetadataParser.php
... ... @@ -80,6 +87,9 @@ protected function doParse($className, $visited = array())
80 87 throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className));
81 88 }
82 89
  90 + $context = new NavigatorContext(GraphNavigator::DIRECTION_SERIALIZATION, 'json'); //TODO: the exclusionStrategy has a hard dependency on this, despite it isn't even used :(
1
Pierre-Yves LEBECQ
pylebecq added a note

NavigatorContext won't exist anymore in Serializer 0.12.
It was renamed to Context in schmittjoh/serializer@ffb2d5d and split into two classes in schmittjoh/serializer@0207222

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Pierre-Yves LEBECQ

@fieg If I have time this weekend, I will try to do something about the version exclusion strategy.

Pierre-Yves LEBECQ

Ok, finally I took some time tonight to add the support of the VersionExclusionStrategy. It will show up here as soon as @fieg merges the PR.

My last concern is related to my previous comment about the removal of NavigationContext in Serializer 0.12.
I don't know how to handle this. Should we update the composer.json file to forbid usage of Serializer >= 0.12 or should we update the code to use the new classes of Serializer 0.12 and make this bundle only compatible with Serializer 0.12 ? Or anything else ?

Christophe Coevoet
stof commented

As 0.12 is stable as of yesterday, forbidding it is not an option IMO

MaksSlesarenko

Is this relative to FosRestBundle SerializerGroups option #119 ?

Jeroen Fiege
fieg commented

Does anyone have some thoughts about how to proceed with this PR?

Jeroen Fiege
fieg commented

Hi, I rebased, fixed the unit tests and refactored the NavigatorContext to support the new Serializer version. Hopefully this can get merged now.

Extractor/ApiDocExtractor.php
((12 lines not shown))
  364 + // normalize strings
  365 + if (is_string($input)) {
  366 + $input = array('class' => $input);
  367 + }
  368 +
  369 + // normalize groups
  370 + if (isset($input['groups']) && is_string($input['groups'])) {
  371 + $input['groups'] = array_map('trim', explode(',', $input['groups']));
  372 + }
  373 +
  374 + // normalize version
  375 + if (!array_key_exists('version', $input)) {
  376 + $input['version'] = null;
  377 + }
  378 +
  379 + return $input += $defaults;
1
Jordi Boggiano Owner
Seldaek added a note

The = is kinda superfluous here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Extractor/ApiDocExtractor.php
((10 lines not shown))
  362 + );
  363 +
  364 + // normalize strings
  365 + if (is_string($input)) {
  366 + $input = array('class' => $input);
  367 + }
  368 +
  369 + // normalize groups
  370 + if (isset($input['groups']) && is_string($input['groups'])) {
  371 + $input['groups'] = array_map('trim', explode(',', $input['groups']));
  372 + }
  373 +
  374 + // normalize version
  375 + if (!array_key_exists('version', $input)) {
  376 + $input['version'] = null;
  377 + }
2
Jordi Boggiano Owner
Seldaek added a note

This block seems useless, if the key is not defined, then it'll be added from the defaults already as far as I can see.

Pierre-Yves LEBECQ
pylebecq added a note

Yep, you're right. Sorry, my mistake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jordi Boggiano
Owner

@fieg the README change is out of date I think, and I have one more question: it now only supports the latest Serializer version right? Is it possible to support older ones too without too much hassle? It'd be great, but if not that's fine, but then please change the composer.json to reflect this (I think the conflict should be conflicting with <0.12 now?).

Besides this stuff and the other couple comments I wrote, I think it's good to merge.

Jeroen Fiege
fieg commented

Good comments, I will try to look into it later today.

William Durand
Collaborator

ping @fieg

Pierre-Yves LEBECQ

I'm not completely satisfied by the implementation of the version exclusion strategy, because every time you introduce a new version you have to update all your ApiDoc annotations. Also, by doing this, there is no possibility to get the documentation for a specific version.
Don't you think a new column like the "required" column would be more appropriate ? Instead of excluding the property, we could show the version that applies to the properties. For example : >=1.0,<2.0

Jeroen Fiege
fieg commented

Thanks for the ping @willdurand.

I updated according to your comments @Seldaek and did a rebase.

Jeroen Fiege
fieg commented

Hmm, some whitespace got messed up. Will fix.

Jeroen Fiege
fieg commented

@Seldaek do you see change to review and merge this?

Jordi Boggiano
Owner

I'd rather have @willdurand review/merge it because it's quite involved and I don't know this code in depth.

That said, I think @pylebecq had a point.. This should be carefully considered.

Jeroen Fiege
fieg commented

My suggestion is that we merge this PR as is and then @pylebecq (and others) could improve the Version-part of this feature in some other PR? It would be a shame if this would stall the GroupExclusion feature unnecessarily.

Pierre-Yves LEBECQ

I agree with @fieg, let's not block the group exclusion strategy feature but before merging I think we should remove the work on the version exclusion strategy because the feature seems wrong to me and don't deserve to land in the code base the way it is implemented for now.

Jeroen Fiege
fieg commented

@pylebecq reworked the version support :+1: . @willdurand could you please review it?

Extractor/ApiDocExtractor.php
... ... @@ -266,9 +266,11 @@ protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMeth
266 266 if (null !== $input = $annotation->getInput()) {
267 267 $parameters = array();
268 268
  269 + $normalizedInput = $this->normalizeInputOutputParameter($input);
5
William Durand Collaborator
willdurand added a note

normalizeParameters()?

Jeroen Fiege
fieg added a note

I think normalizeParameters would be to generic; it only normalizes the type of parameter that is used with the input and output option. How about normalizeClassParameter as the type of parameter is best described as a class?

William Durand Collaborator
willdurand added a note

I think normalizeParameters() is fine. Do we have other parameters?

Jeroen Fiege
fieg added a note

Yes, description for example. Also the method only normalizes a single parameter.

William Durand Collaborator
willdurand added a note

ok for normalizeClassParameter then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Extractor/ApiDocExtractor.php
((6 lines not shown))
  359 + $defaults = array(
  360 + 'class' => '',
  361 + 'groups' => array(),
  362 + );
  363 +
  364 + // normalize strings
  365 + if (is_string($input)) {
  366 + $input = array('class' => $input);
  367 + }
  368 +
  369 + // normalize groups
  370 + if (isset($input['groups']) && is_string($input['groups'])) {
  371 + $input['groups'] = array_map('trim', explode(',', $input['groups']));
  372 + }
  373 +
  374 + return $input + $defaults;
3
William Durand Collaborator
willdurand added a note

please, don't do that

Jeroen Fiege
fieg added a note

Can you be more specific?

William Durand Collaborator
willdurand added a note

don't merge arrays using the + sign.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Benjamin Laugueux

ping @fieg please :)

William Durand
Collaborator

ping @fieg

Formatter/AbstractFormatter.php
... ... @@ -113,7 +113,13 @@ protected function processAnnotation($annotation)
113 113 }
114 114
115 115 if (isset($annotation['response'])) {
116   - $annotation['response'] = $this->compressNestedParameters($annotation['response']);
  116 + $response = $this->compressNestedParameters($annotation['response']);
  117 + foreach ($response as $name => &$info) {
  118 + $info['sinceVersion'] = array_key_exists('sinceVersion', $annotation['response'][$name]) ? $annotation['response'][$name]['sinceVersion'] : null;
2
Jeroen Fiege
fieg added a note

@pylebecq I'm running into an issue here with nested parameters:

Notice: Undefined index: credit[amount] in vendor/nelmio/api-doc-bundle/Nelmio/ApiDocBundle/Formatter/AbstractFormatter.php line 118
Pierre-Yves LEBECQ
pylebecq added a note

I'll fix it tonight and send you a PR as usual. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Benjamin Laugueux

@fieg can you rebase and squash your commits to have a better readability of your PR please

Jeroen Fiege
fieg commented

I rebased (and resolve some conflicts), but now run into some failing tests which I'm not certain how to fix. @pylebecq I think you might have a better insight in how to fix these tests. You can check the result of the rebase here: https://github.com/fieg/NelmioApiDocBundle/commits/jms-groups-support-rebased

@blaugueux I've never squashed commits before, should all commits be squashed in a single commit? How would this improve the readability?

Benjamin Laugueux

@fieg This will group your 16 commits into 1. It will be more easy to rebase for you and better for us to read :) Here is a small tuto: http://ariejan.net/2011/07/05/git-squash-your-latests-commits-into-one/

Jeroen Fiege
fieg commented

@blaugueux Would author information for the commits of @pylebecq be maintained?

@willdurand what do you think about this squashing of commits?

Pierre-Yves LEBECQ

@fieg I fixed the tests, this was no big deal, probably a mistake when resolving conflics.

I pushed in your rebased branch : https://github.com/fieg/NelmioApiDocBundle/commits/jms-groups-support-rebased .
I also squashed our commits resulting in two commits : the first commit is yours and is about the implementation of the group exclusion strategy. The other commit is mine and is about the version support. I pushed the squashed commits in an other branch : https://github.com/fieg/NelmioApiDocBundle/commits/jms-groups-support-rebased-squashed .

The only difference between the two branches is two use statements not used anymore I removed in the squashed branch :

audrey:ApiDocBundle pierreyves$ git diff origin/jms-groups-support-rebased..origin/jms-groups-support-rebased-squashed
diff --git a/Parser/JmsMetadataParser.php b/Parser/JmsMetadataParser.php
index add1e3d..2edc28e 100644
--- a/Parser/JmsMetadataParser.php
+++ b/Parser/JmsMetadataParser.php
@@ -12,8 +12,6 @@
 namespace Nelmio\ApiDocBundle\Parser;

 use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
-use JMS\Serializer\GraphNavigator;
-use JMS\Serializer\NavigatorContext;
 use JMS\Serializer\SerializationContext;
 use Metadata\MetadataFactoryInterface;
 use Nelmio\ApiDocBundle\Util\DocCommentExtractor;

I did my best to squash like @blaugueux asked and to keep the author information for the both of us. You can view the two commits to see if the split seems fair (it should) and if everything is ok for everyone the last thing to do will be to push the jms-groups-support-rebased-squashed on jms-groups-support (you will need to use --force).

Don't hesitate to contact me via private messages if you have any question.

William Durand
Collaborator

Ok so, can we have a clean PR now? A rebase is still needed, I don't really care about the squashing commits.
You should update the doc though.

Jeroen Fiege
fieg commented

@pylebecq you're the best :) I pushed jms-groups-support-rebased-squashed onto jms-group-support.

I believe @willdurand is pointing at the documentation about the version feature of this PR. @pylebecq if you could only fix this last thing, then we are all set and this can get merged.

William Durand
Collaborator

This needs to be rebased btw

Pierre-Yves LEBECQ

I updated the docs and rebased / fixed tests / squashed / killed some kittens again. :)

William Durand willdurand merged commit ce1b40e into from
William Durand willdurand closed this
William Durand
Collaborator

thanks!

Benjamin Laugueux

@willdurand @Seldaek can you create a tag with this new feature?

Jeroen Fiege fieg deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
32 Extractor/ApiDocExtractor.php
@@ -266,9 +266,11 @@ protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMeth
266 266 if (null !== $input = $annotation->getInput()) {
267 267 $parameters = array();
268 268
  269 + $normalizedInput = $this->normalizeClassParameter($input);
  270 +
269 271 foreach ($this->parsers as $parser) {
270   - if ($parser->supports($input)) {
271   - $parameters = $parser->parse($input);
  272 + if ($parser->supports($normalizedInput)) {
  273 + $parameters = $parser->parse($normalizedInput);
272 274 break;
273 275 }
274 276 }
@@ -287,9 +289,11 @@ protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMeth
287 289 if (null !== $output = $annotation->getOutput()) {
288 290 $response = array();
289 291
  292 + $normalizedOutput = $this->normalizeClassParameter($output);
  293 +
290 294 foreach ($this->parsers as $parser) {
291   - if ($parser->supports($output)) {
292   - $response = $parser->parse($output);
  295 + if ($parser->supports($normalizedOutput)) {
  296 + $response = $parser->parse($normalizedOutput);
293 297 break;
294 298 }
295 299 }
@@ -350,6 +354,26 @@ protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMeth
350 354 return $annotation;
351 355 }
352 356
  357 + protected function normalizeClassParameter($input)
  358 + {
  359 + $defaults = array(
  360 + 'class' => '',
  361 + 'groups' => array(),
  362 + );
  363 +
  364 + // normalize strings
  365 + if (is_string($input)) {
  366 + $input = array('class' => $input);
  367 + }
  368 +
  369 + // normalize groups
  370 + if (isset($input['groups']) && is_string($input['groups'])) {
  371 + $input['groups'] = array_map('trim', explode(',', $input['groups']));
  372 + }
  373 +
  374 + return array_merge($defaults, $input);
  375 + }
  376 +
353 377 /**
354 378 * Parses annotations for a given method, and adds new information to the given ApiDoc
355 379 * annotation. Useful to extract information from the FOSRestBundle annotations.
2  Formatter/AbstractFormatter.php
@@ -73,6 +73,8 @@ protected function compressNestedParameters(array $data, $parentName = null, $ig
73 73 'dataType' => $info['dataType'],
74 74 'readonly' => $info['readonly'],
75 75 'required' => $info['required'],
  76 + 'sinceVersion' => array_key_exists('sinceVersion', $info) ? $info['sinceVersion'] : null,
  77 + 'untilVersion' => array_key_exists('untilVersion', $info) ? $info['untilVersion'] : null,
76 78 );
77 79
78 80 if (isset($info['children']) && (!$info['readonly'] || !$ignoreNestedReadOnly)) {
14 Formatter/MarkdownFormatter.php
@@ -103,6 +103,20 @@ protected function renderOne(array $data)
103 103 $markdown .= sprintf(" * description: %s\n", $parameter['description']);
104 104 }
105 105
  106 + if (null !== $parameter['sinceVersion'] || null !== $parameter['untilVersion']) {
  107 + $markdown .= " * versions: ";
  108 + if ($parameter['sinceVersion']) {
  109 + $markdown .= '>='.$parameter['sinceVersion'];
  110 + }
  111 + if ($parameter['untilVersion']) {
  112 + if ($parameter['sinceVersion']) {
  113 + $markdown .= ',';
  114 + }
  115 + $markdown .= '<='.$parameter['untilVersion'];
  116 + }
  117 + $markdown .= "\n";
  118 + }
  119 +
106 120 $markdown .= "\n";
107 121 }
108 122 }
10 Parser/FormTypeParser.php
@@ -47,10 +47,12 @@ public function __construct(FormFactoryInterface $formFactory, FormRegistry $for
47 47 /**
48 48 * {@inheritdoc}
49 49 */
50   - public function supports($item)
  50 + public function supports(array $item)
51 51 {
  52 + $className = $item['class'];
  53 +
52 54 try {
53   - if ($this->createForm($item)) {
  55 + if ($this->createForm($className)) {
54 56 return true;
55 57 }
56 58 } catch (FormException $e) {
@@ -65,8 +67,10 @@ public function supports($item)
65 67 /**
66 68 * {@inheritdoc}
67 69 */
68   - public function parse($type)
  70 + public function parse(array $item)
69 71 {
  72 + $type = $item['class'];
  73 +
70 74 if ($this->implementsType($type)) {
71 75 $type = $this->getTypeInstance($type);
72 76 }
45 Parser/JmsMetadataParser.php
@@ -11,6 +11,8 @@
11 11
12 12 namespace Nelmio\ApiDocBundle\Parser;
13 13
  14 +use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
  15 +use JMS\Serializer\SerializationContext;
14 16 use Metadata\MetadataFactoryInterface;
15 17 use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
16 18 use JMS\Serializer\Metadata\PropertyMetadata;
@@ -22,7 +24,6 @@
22 24 */
23 25 class JmsMetadataParser implements ParserInterface
24 26 {
25   -
26 27 /**
27 28 * @var \Metadata\MetadataFactoryInterface
28 29 */
@@ -54,10 +55,12 @@ public function __construct(
54 55 /**
55 56 * {@inheritdoc}
56 57 */
57   - public function supports($input)
  58 + public function supports(array $input)
58 59 {
  60 + $className = $input['class'];
  61 +
59 62 try {
60   - if ($meta = $this->factory->getMetadataForClass($input)) {
  63 + if ($meta = $this->factory->getMetadataForClass($className)) {
61 64 return true;
62 65 }
63 66 } catch (\ReflectionException $e) {
@@ -69,9 +72,12 @@ public function supports($input)
69 72 /**
70 73 * {@inheritdoc}
71 74 */
72   - public function parse($input)
  75 + public function parse(array $input)
73 76 {
74   - return $this->doParse($input);
  77 + $className = $input['class'];
  78 + $groups = $input['groups'];
  79 +
  80 + return $this->doParse($className, array(), $groups);
75 81 }
76 82
77 83 /**
@@ -79,10 +85,11 @@ public function parse($input)
79 85 *
80 86 * @param string $className Class to get all metadata for
81 87 * @param array $visited Classes we've already visited to prevent infinite recursion.
  88 + * @param array $groups Serialization groups to include.
82 89 * @return array metadata for given class
83 90 * @throws \InvalidArgumentException
84 91 */
85   - protected function doParse($className, $visited = array())
  92 + protected function doParse($className, $visited = array(), array $groups = array())
86 93 {
87 94 $meta = $this->factory->getMetadataForClass($className);
88 95
@@ -90,6 +97,9 @@ protected function doParse($className, $visited = array())
90 97 throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className));
91 98 }
92 99
  100 + $exclusionStrategies = array();
  101 + $exclusionStrategies[] = new GroupsExclusionStrategy($groups);
  102 +
93 103 $params = array();
94 104
95 105 // iterate over property metadata
@@ -99,11 +109,21 @@ protected function doParse($className, $visited = array())
99 109
100 110 $dataType = $this->processDataType($item);
101 111
  112 + // apply exclusion strategies
  113 + foreach ($exclusionStrategies as $strategy) {
  114 + if (true === $strategy->shouldSkipProperty($item, SerializationContext::create())) {
  115 + continue 2;
  116 + }
  117 + }
  118 +
102 119 $params[$name] = array(
103   - 'dataType' => $dataType['normalized'],
104   - 'required' => false, //TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
105   - 'description' => $this->getDescription($className, $item),
106   - 'readonly' => $item->readOnly
  120 + 'dataType' => $dataType['normalized'],
  121 + 'required' => false,
  122 + //TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
  123 + 'description' => $this->getDescription($className, $item),
  124 + 'readonly' => $item->readOnly,
  125 + 'sinceVersion' => $item->sinceVersion,
  126 + 'untilVersion' => $item->untilVersion,
107 127 );
108 128
109 129 // if class already parsed, continue, to avoid infinite recursion
@@ -113,8 +133,8 @@ protected function doParse($className, $visited = array())
113 133
114 134 // check for nested classes with JMS metadata
115 135 if ($dataType['class'] && null !== $this->factory->getMetadataForClass($dataType['class'])) {
116   - $visited[] = $dataType['class'];
117   - $params[$name]['children'] = $this->doParse($dataType['class'], $visited);
  136 + $visited[] = $dataType['class'];
  137 + $params[$name]['children'] = $this->doParse($dataType['class'], $visited, $groups);
118 138 }
119 139 }
120 140 }
@@ -206,5 +226,4 @@ protected function getDescription($className, PropertyMetadata $item)
206 226
207 227 return $extracted;
208 228 }
209   -
210 229 }
7 Parser/ParserInterface.php
@@ -19,10 +19,10 @@
19 19 /**
20 20 * Return true/false whether this class supports parsing the given class.
21 21 *
22   - * @param string $item The string type of input to parse.
  22 + * @param array $item containing the following fields: class, groups. Of which groups is optional
23 23 * @return boolean
24 24 */
25   - public function supports($item);
  25 + public function supports(array $item);
26 26
27 27 /**
28 28 * Returns an array of class property metadata where each item is a key (the property name) and
@@ -37,6 +37,5 @@ public function supports($item);
37 37 * @param string $item The string type of input to parse.
38 38 * @return array
39 39 */
40   - public function parse($item);
41   -
  40 + public function parse(array $item);
42 41 }
27 README.md
Source Rendered
@@ -105,7 +105,7 @@ The following properties are available:
105 105 * `filters`: an array of filters;
106 106
107 107 * `input`: the input type associated to the method, currently this supports Form Types, and classes with JMS Serializer
108   - metadata, useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container)
  108 + metadata, useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container).
109 109
110 110 * `output`: the output type associated with the response. Specified and parsed the same way as `input`.
111 111
@@ -183,6 +183,31 @@ Also bundle will get information from the other annotations:
183 183
184 184 Route functions marked as @deprecated will be set method as deprecation in documentation.
185 185
  186 +#### JMS Serializer features ####
  187 +
  188 +The bundle has support for some of the JMS Serializer features and use these extra information in the generated documentation.
  189 +
  190 +##### Group Exclusion Strategy #####
  191 +
  192 +If your classes use [JMS Group Exclusion Strategy](http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#creating-different-views-of-your-objects),
  193 +you can specify which groups to use when generating the documentation by using this syntax :
  194 +
  195 + ```
  196 + input={
  197 + "class"="Acme\Bundle\Entity\User",
  198 + "groups"={"update", "public"}
  199 + }
  200 + ```
  201 +
  202 + In this case the groups 'update' and 'public' are used.
  203 +
  204 + This feature also works for the `output` property.
  205 +
  206 +##### Versioning Objects #####
  207 +
  208 +If your `output` classes use [versioning capabilities of JMS Serializer](http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#versioning-objects),
  209 +the versioning information will be automatically used when generating the documentation.
  210 +
186 211 ### Documentation on-the-fly ###
187 212
188 213 By calling an URL with the parameter `?_doc=1`, you will get the corresponding documentation if available.
8 Resources/views/Components/version.html.twig
... ... @@ -0,0 +1,8 @@
  1 +{% if sinceVersion is empty and untilVersion is empty %}
  2 +*
  3 +{% else %}
  4 + {% if sinceVersion is not empty %}&gt;={{ sinceVersion }}{% endif %}
  5 + {% if untilVersion is not empty %}
  6 + {% if sinceVersion is not empty %},{% endif %}&lt;={{ untilVersion }}
  7 + {% endif %}
  8 +{% endif %}
2  Resources/views/method.html.twig
@@ -134,6 +134,7 @@
134 134 <tr>
135 135 <th>Parameter</th>
136 136 <th>Type</th>
  137 + <th>Versions</th>
137 138 <th>Description</th>
138 139 </tr>
139 140 </thead>
@@ -142,6 +143,7 @@
142 143 <tr>
143 144 <td>{{ name }}</td>
144 145 <td>{{ infos.dataType }}</td>
  146 + <td>{% include 'NelmioApiDocBundle:Components:version.html.twig' with {'sinceVersion': infos.sinceVersion, 'untilVersion': infos.untilVersion} only %}</td>
145 147 <td>{{ infos.description }}</td>
146 148 </tr>
147 149 {% endfor %}
6 Tests/Extractor/ApiDocExtractorTest.php
@@ -15,7 +15,7 @@
15 15
16 16 class ApiDocExtractorTest extends WebTestCase
17 17 {
18   - const ROUTES_QUANTITY = 19;
  18 + const ROUTES_QUANTITY = 20;
19 19
20 20 public function testAll()
21 21 {
@@ -66,7 +66,7 @@ public function testAll()
66 66 $this->assertFalse(isset($array2['filters']));
67 67 $this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
68 68
69   - $a3 = $data['12']['annotation'];
  69 + $a3 = $data['13']['annotation'];
70 70 $this->assertTrue($a3->getHttps());
71 71 }
72 72
@@ -155,7 +155,7 @@ public function testGetWithDocComment()
155 155 "This method is useful to test if the getDocComment works.",
156 156 $annotation->getDescription()
157 157 );
158   -
  158 +
159 159 $data = $annotation->toArray();
160 160 $this->assertEquals(
161 161 4,
11 Tests/Fixtures/Controller/TestController.php
@@ -172,7 +172,7 @@ public function authenticatedAction()
172 172 public function cachedAction()
173 173 {
174 174 }
175   -
  175 +
176 176 /**
177 177 * @ApiDoc()
178 178 * @deprecated
@@ -180,4 +180,13 @@ public function cachedAction()
180 180 public function deprecatedAction()
181 181 {
182 182 }
  183 +
  184 + /**
  185 + * @ApiDoc(
  186 + * output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
  187 + * )
  188 + */
  189 + public function jmsReturnNestedOutputAction()
  190 + {
  191 + }
183 192 }
18 Tests/Fixtures/Model/JmsNested.php
@@ -36,4 +36,22 @@ class JmsNested
36 36 */
37 37 public $parent;
38 38
  39 + /**
  40 + * @Jms\Type("string")
  41 + * @Jms\Since("0.2")
  42 + */
  43 + public $since;
  44 +
  45 + /**
  46 + * @Jms\Type("string")
  47 + * @Jms\Until("0.3")
  48 + */
  49 + public $until;
  50 +
  51 + /**
  52 + * @Jms\Type("string")
  53 + * @Jms\Since("0.4")
  54 + * @Jms\Until("0.5")
  55 + */
  56 + public $sinceAndUntil;
39 57 }
18 Tests/Fixtures/app/AppKernel.php
@@ -12,23 +12,7 @@
12 12 namespace Nelmio\ApiDocBundle\Tests\Functional;
13 13
14 14 // get the autoload file
15   -$dir = __DIR__;
16   -$lastDir = null;
17   -while ($dir !== $lastDir) {
18   - $lastDir = $dir;
19   -
20   - if (is_file($dir.'/autoload.php')) {
21   - require_once $dir.'/autoload.php';
22   - break;
23   - }
24   -
25   - if (is_file($dir.'/autoload.php.dist')) {
26   - require_once $dir.'/autoload.php.dist';
27   - break;
28   - }
29   -
30   - $dir = dirname($dir);
31   -}
  15 +require_once __DIR__.'/../../../vendor/autoload.php';
32 16
33 17 use Symfony\Component\Config\Loader\LoaderInterface;
34 18 use Symfony\Component\HttpKernel\Kernel;
4 Tests/Fixtures/app/config/routing.yml
@@ -117,3 +117,7 @@ test_route_17:
117 117 defaults: { _controller: NelmioApiDocTestBundle:Test:deprecated }
118 118 requirements:
119 119 _method: GET
  120 +
  121 +test_return_nested_output:
  122 + pattern: /return-nested-output
  123 + defaults: { _controller: NelmioApiDocTestBundle:Test:jmsReturnNestedOutput, _format: json }
107 Tests/Formatter/MarkdownFormatterTest.php
@@ -244,6 +244,21 @@ public function testFormat()
244 244 * type: array of objects (JmsNested)
245 245 * required: false
246 246
  247 +nested[since]:
  248 +
  249 + * type: string
  250 + * required: false
  251 +
  252 +nested[until]:
  253 +
  254 + * type: string
  255 + * required: false
  256 +
  257 +nested[since_and_until]:
  258 +
  259 + * type: string
  260 + * required: false
  261 +
247 262 nested_array[]:
248 263
249 264 * type: array of objects (JmsNested)
@@ -285,6 +300,98 @@ public function testFormat()
285 300 - Description: The param id
286 301
287 302
  303 +### `ANY` /return-nested-output ###
  304 +
  305 +
  306 +#### Response ####
  307 +
  308 +foo:
  309 +
  310 + * type: string
  311 +
  312 +bar:
  313 +
  314 + * type: DateTime
  315 +
  316 +number:
  317 +
  318 + * type: double
  319 +
  320 +arr:
  321 +
  322 + * type: array
  323 +
  324 +nested:
  325 +
  326 + * type: object (JmsNested)
  327 +
  328 +nested[foo]:
  329 +
  330 + * type: DateTime
  331 +
  332 +nested[bar]:
  333 +
  334 + * type: string
  335 +
  336 +nested[baz][]:
  337 +
  338 + * type: array of integers
  339 + * description: Epic description.
  340 +
  341 +With multiple lines.
  342 +
  343 +nested[circular]:
  344 +
  345 + * type: object (JmsNested)
  346 +
  347 +nested[parent]:
  348 +
  349 + * type: object (JmsTest)
  350 +
  351 +nested[parent][foo]:
  352 +
  353 + * type: string
  354 +
  355 +nested[parent][bar]:
  356 +
  357 + * type: DateTime
  358 +
  359 +nested[parent][number]:
  360 +
  361 + * type: double
  362 +
  363 +nested[parent][arr]:
  364 +
  365 + * type: array
  366 +
  367 +nested[parent][nested]:
  368 +
  369 + * type: object (JmsNested)
  370 +
  371 +nested[parent][nested_array][]:
  372 +
  373 + * type: array of objects (JmsNested)
  374 +
  375 +nested[since]:
  376 +
  377 + * type: string
  378 + * versions: >=0.2
  379 +
  380 +nested[until]:
  381 +
  382 + * type: string
  383 + * versions: <=0.3
  384 +
  385 +nested[since_and_until]:
  386 +
  387 + * type: string
  388 + * versions: >=0.4,<=0.5
  389 +
  390 +nested_array[]:
  391 +
  392 + * type: array of objects (JmsNested)
  393 +
  394 +
288 395 ### `ANY` /secure-route ###
289 396
290 397
272 Tests/Formatter/SimpleFormatterTest.php
@@ -252,6 +252,8 @@ public function testFormat()
252 252 'required' => false,
253 253 'description' => '',
254 254 'readonly' => false,
  255 + 'sinceVersion' => null,
  256 + 'untilVersion' => null,
255 257 ),
256 258 'bar' =>
257 259 array(
@@ -259,6 +261,8 @@ public function testFormat()
259 261 'required' => false,
260 262 'description' => '',
261 263 'readonly' => true,
  264 + 'sinceVersion' => null,
  265 + 'untilVersion' => null,
262 266 ),
263 267 'number' =>
264 268 array(
@@ -266,6 +270,8 @@ public function testFormat()
266 270 'required' => false,
267 271 'description' => '',
268 272 'readonly' => false,
  273 + 'sinceVersion' => null,
  274 + 'untilVersion' => null,
269 275 ),
270 276 'arr' =>
271 277 array(
@@ -273,6 +279,8 @@ public function testFormat()
273 279 'required' => false,
274 280 'description' => '',
275 281 'readonly' => false,
  282 + 'sinceVersion' => null,
  283 + 'untilVersion' => null,
276 284 ),
277 285 'nested' =>
278 286 array(
@@ -280,6 +288,8 @@ public function testFormat()
280 288 'required' => false,
281 289 'description' => '',
282 290 'readonly' => false,
  291 + 'sinceVersion' => null,
  292 + 'untilVersion' => null,
283 293 'children' =>
284 294 array(
285 295 'foo' =>
@@ -288,6 +298,8 @@ public function testFormat()
288 298 'required' => false,
289 299 'description' => '',
290 300 'readonly' => true,
  301 + 'sinceVersion' => null,
  302 + 'untilVersion' => null,
291 303 ),
292 304 'bar' =>
293 305 array(
@@ -295,6 +307,8 @@ public function testFormat()
295 307 'required' => false,
296 308 'description' => '',
297 309 'readonly' => false,
  310 + 'sinceVersion' => null,
  311 + 'untilVersion' => null,
298 312 ),
299 313 'baz' =>
300 314 array(
@@ -304,6 +318,8 @@ public function testFormat()
304 318
305 319 With multiple lines.',
306 320 'readonly' => false,
  321 + 'sinceVersion' => null,
  322 + 'untilVersion' => null,
307 323 ),
308 324 'circular' =>
309 325 array(
@@ -311,6 +327,8 @@ public function testFormat()
311 327 'required' => false,
312 328 'description' => '',
313 329 'readonly' => false,
  330 + 'sinceVersion' => null,
  331 + 'untilVersion' => null,
314 332 ),
315 333 'parent' =>
316 334 array(
@@ -318,6 +336,8 @@ public function testFormat()
318 336 'required' => false,
319 337 'description' => '',
320 338 'readonly' => false,
  339 + 'sinceVersion' => null,
  340 + 'untilVersion' => null,
321 341 'children' =>
322 342 array(
323 343 'foo' =>
@@ -326,6 +346,8 @@ public function testFormat()
326 346 'required' => false,
327 347 'description' => '',
328 348 'readonly' => false,
  349 + 'sinceVersion' => null,
  350 + 'untilVersion' => null,
329 351 ),
330 352 'bar' =>
331 353 array(
@@ -333,6 +355,8 @@ public function testFormat()
333 355 'required' => false,
334 356 'description' => '',
335 357 'readonly' => true,
  358 + 'sinceVersion' => null,
  359 + 'untilVersion' => null,
336 360 ),
337 361 'number' =>
338 362 array(
@@ -340,6 +364,8 @@ public function testFormat()
340 364 'required' => false,
341 365 'description' => '',
342 366 'readonly' => false,
  367 + 'sinceVersion' => null,
  368 + 'untilVersion' => null,
343 369 ),
344 370 'arr' =>
345 371 array(
@@ -347,6 +373,8 @@ public function testFormat()
347 373 'required' => false,
348 374 'description' => '',
349 375 'readonly' => false,
  376 + 'sinceVersion' => null,
  377 + 'untilVersion' => null,
350 378 ),
351 379 'nested' =>
352 380 array(
@@ -354,6 +382,8 @@ public function testFormat()
354 382 'required' => false,
355 383 'description' => '',
356 384 'readonly' => false,
  385 + 'sinceVersion' => null,
  386 + 'untilVersion' => null,
357 387 ),
358 388 'nested_array' =>
359 389 array(
@@ -361,9 +391,38 @@ public function testFormat()
361 391 'required' => false,
362 392 'description' => '',
363 393 'readonly' => false,
  394 + 'sinceVersion' => null,
  395 + 'untilVersion' => null,
364 396 ),
365 397 ),
366 398 ),
  399 + 'since' =>
  400 + array (
  401 + 'dataType' => 'string',
  402 + 'required' => false,
  403 + 'description' => '',
  404 + 'readonly' => false,
  405 + 'sinceVersion' => '0.2',
  406 + 'untilVersion' => null,
  407 + ),
  408 + 'until' =>
  409 + array (
  410 + 'dataType' => 'string',
  411 + 'required' => false,
  412 + 'description' => '',
  413 + 'readonly' => false,
  414 + 'sinceVersion' => null,
  415 + 'untilVersion' => '0.3',
  416 + ),
  417 + 'since_and_until' =>
  418 + array (
  419 + 'dataType' => 'string',
  420 + 'required' => false,
  421 + 'description' => '',
  422 + 'readonly' => false,
  423 + 'sinceVersion' => '0.4',
  424 + 'untilVersion' => '0.5',
  425 + ),
367 426 ),
368 427 ),
369 428 'nested_array' =>
@@ -372,6 +431,8 @@ public function testFormat()
372 431 'required' => false,
373 432 'description' => '',
374 433 'readonly' => false,
  434 + 'sinceVersion' => null,
  435 + 'untilVersion' => null,
375 436 ),
376 437 ),
377 438 'https' => false,
@@ -440,6 +501,205 @@ public function testFormat()
440 501 7 =>
441 502 array(
442 503 'method' => 'ANY',
  504 + 'uri' => '/return-nested-output',
  505 + 'https' => false,
  506 + 'authentication' => false,
  507 + 'deprecated' => false,
  508 + 'response' =>
  509 + array (
  510 + 'foo' =>
  511 + array (
  512 + 'dataType' => 'string',
  513 + 'required' => false,
  514 + 'description' => '',
  515 + 'readonly' => false,
  516 + 'sinceVersion' => null,
  517 + 'untilVersion' => null,
  518 + ),
  519 + 'bar' =>
  520 + array (
  521 + 'dataType' => 'DateTime',
  522 + 'required' => false,
  523 + 'description' => '',
  524 + 'readonly' => true,
  525 + 'sinceVersion' => null,
  526 + 'untilVersion' => null,
  527 + ),
  528 + 'number' =>
  529 + array (
  530 + 'dataType' => 'double',
  531 + 'required' => false,
  532 + 'description' => '',
  533 + 'readonly' => false,
  534 + 'sinceVersion' => null,
  535 + 'untilVersion' => null,
  536 + ),
  537 + 'arr' =>
  538 + array (
  539 + 'dataType' => 'array',
  540 + 'required' => false,
  541 + 'description' => '',
  542 + 'readonly' => false,
  543 + 'sinceVersion' => null,
  544 + 'untilVersion' => null,
  545 + ),
  546 + 'nested' =>
  547 + array (
  548 + 'dataType' => 'object (JmsNested)',
  549 + 'required' => false,
  550 + 'description' => '',
  551 + 'readonly' => false,
  552 + 'sinceVersion' => null,
  553 + 'untilVersion' => null,
  554 + 'children' =>
  555 + array (
  556 + 'foo' =>
  557 + array (
  558 + 'dataType' => 'DateTime',
  559 + 'required' => false,
  560 + 'description' => '',
  561 + 'readonly' => true,
  562 + 'sinceVersion' => null,
  563 + 'untilVersion' => null,
  564 + ),
  565 + 'bar' =>
  566 + array (
  567 + 'dataType' => 'string',
  568 + 'required' => false,
  569 + 'description' => '',
  570 + 'readonly' => false,
  571 + 'sinceVersion' => null,
  572 + 'untilVersion' => null,
  573 + ),
  574 + 'baz' =>
  575 + array (
  576 + 'dataType' => 'array of integers',
  577 + 'required' => false,
  578 + 'description' => 'Epic description.
  579 +
  580 +With multiple lines.',
  581 + 'readonly' => false,
  582 + 'sinceVersion' => null,
  583 + 'untilVersion' => null,
  584 + ),
  585 + 'circular' =>
  586 + array (
  587 + 'dataType' => 'object (JmsNested)',
  588 + 'required' => false,
  589 + 'description' => '',
  590 + 'readonly' => false,
  591 + 'sinceVersion' => null,
  592 + 'untilVersion' => null,
  593 + ),
  594 + 'parent' =>
  595 + array (
  596 + 'dataType' => 'object (JmsTest)',
  597 + 'required' => false,
  598 + 'description' => '',
  599 + 'readonly' => false,
  600 + 'sinceVersion' => null,
  601 + 'untilVersion' => null,
  602 + 'children' =>
  603 + array (
  604 + 'foo' =>
  605 + array (
  606 + 'dataType' => 'string',
  607 + 'required' => false,
  608 + 'description' => '',
  609 + 'readonly' => false,
  610 + 'sinceVersion' => null,
  611 + 'untilVersion' => null,
  612 + ),
  613 + 'bar' =>
  614 + array (
  615 + 'dataType' => 'DateTime',
  616 + 'required' => false,
  617 + 'description' => '',
  618 + 'readonly' => true,
  619 + 'sinceVersion' => null,
  620 + 'untilVersion' => null,
  621 + ),
  622 + 'number' =>
  623 + array (
  624 + 'dataType' => 'double',
  625 + 'required' => false,
  626 + 'description' => '',
  627 + 'readonly' => false,
  628 + 'sinceVersion' => null,
  629 + 'untilVersion' => null,
  630 + ),
  631 + 'arr' =>
  632 + array (
  633 + 'dataType' => 'array',
  634 + 'required' => false,
  635 + 'description' => '',
  636 + 'readonly' => false,
  637 + 'sinceVersion' => null,
  638 + 'untilVersion' => null,
  639 + ),
  640 + 'nested' =>
  641 + array (
  642 + 'dataType' => 'object (JmsNested)',
  643 + 'required' => false,
  644 + 'description' => '',
  645 + 'readonly' => false,
  646 + 'sinceVersion' => null,
  647 + 'untilVersion' => null,
  648 + ),
  649 + 'nested_array' =>
  650 + array (
  651 + 'dataType' => 'array of objects (JmsNested)',
  652 + 'required' => false,
  653 + 'description' => '',
  654 + 'readonly' => false,
  655 + 'sinceVersion' => null,
  656 + 'untilVersion' => null,
  657 + ),
  658 + ),
  659 + ),
  660 + 'since' =>
  661 + array (
  662 + 'dataType' => 'string',
  663 + 'required' => false,
  664 + 'description' => '',
  665 + 'readonly' => false,
  666 + 'sinceVersion' => '0.2',
  667 + 'untilVersion' => null,
  668 + ),