Compare commits

...

482 commits

Author SHA1 Message Date
Ilyas Salikhov
10ee31bdaa
Merge pull request #24 from retailcrm/twig-deprecates
fix twig deprecates
2024-10-10 23:14:28 +03:00
Ilyas Salikhov
6946e8bdc9 fix twig deprecates 2024-10-10 23:07:49 +03:00
Ilyas Salikhov
a896abb3e2
Merge pull request #23 from retailcrm/attribute
Migrate annotation ApiDoc to attribute
2024-10-01 23:02:33 +03:00
Ilyas Salikhov
a9fceca8df Migrate annotation ApiDoc to attribute 2024-10-01 23:00:23 +03:00
Ilyas Salikhov
8135dec035
Merge pull request #22 from retailcrm/remove-unused-params
Remove unused params
2024-10-01 18:41:22 +03:00
Ilyas Salikhov
5db963666a remove unused cache annotation param 2024-10-01 18:38:58 +03:00
Ilyas Salikhov
990b781322 remove unused https annotation param 2024-10-01 18:33:15 +03:00
Ilyas Salikhov
e7c9fd4f56 remove unused authentication annotation param 2024-10-01 18:26:30 +03:00
Ilyas Salikhov
abbf032483 Remove unused argument 2024-10-01 17:43:18 +03:00
Ilyas Salikhov
49f4161b23
Merge pull request #21 from retailcrm/fos-rest-remove
Remove the support of FosRestBundle, DunglasApiBundle and JmsSecurityExtra
2024-10-01 17:35:38 +03:00
Ilyas Salikhov
3ef60f2926 remove JmsSecurityExtraHandler 2024-10-01 17:30:02 +03:00
Ilyas Salikhov
636eeb7cae Remove dunglasapibundle support 2024-10-01 17:26:49 +03:00
Ilyas Salikhov
e97eba7c1b Remove the support of fos-rest 2024-10-01 17:18:04 +03:00
Ilyas Salikhov
73f9b9e8a2
Merge pull request #20 from retailcrm/duglas-remove
remove the support of duglas
2024-10-01 16:42:22 +03:00
Ilyas Salikhov
4b6137f618 remove the support of duglas 2024-10-01 16:39:44 +03:00
Ilyas Salikhov
eba661dcb6
Merge pull request #19 from retailcrm/scope
add scope field to ApiDoc
2024-10-01 16:12:45 +03:00
Ilyas Salikhov
d761d890a0 add scope field to ApiDoc 2024-10-01 16:11:23 +03:00
Ilyas Salikhov
551366412e
Merge pull request #18 from retailcrm/php-cs
Add php-cs-fixer to project
2024-10-01 15:57:19 +03:00
Ilyas Salikhov
e06fb926f5 Fix code by php-cs-fixer 2024-10-01 15:54:04 +03:00
Ilyas Salikhov
cb69478c78 Add php-cs-fixer 2024-10-01 15:53:49 +03:00
Ilyas Salikhov
5bad727da1
Merge pull request #17 from retailcrm/sensio-support-remove
Removed support of sensio/framework-extra-bundle
2024-09-30 21:34:55 +03:00
Ilyas Salikhov
45d1ed1735 add doctrine/annotations as dev dependency 2024-09-30 21:33:21 +03:00
Ilyas Salikhov
4d4758cb40 fix jms/serializer dependencies 2024-09-30 21:30:38 +03:00
Ilyas Salikhov
6334d2e40f Removed support of sensio/framework-extra-bundle 2024-09-30 21:23:38 +03:00
Ilyas Salikhov
b72dfea35c
Merge pull request #16 from retailcrm/php83
PHP 8.3 in CI
2024-09-24 15:23:20 +03:00
Ilyas Salikhov
7de616a283 PHP 8.3 in CI 2024-09-24 15:20:04 +03:00
Ilyas Salikhov
0f7ecac668
Merge pull request #15 from retailcrm/container-remove
Add explicit dependencies instead of container
2024-07-02 16:26:10 +03:00
Ilyas Salikhov
0808e8421a Add explicit dependencies instead of container 2024-07-02 16:24:26 +03:00
Ilyas Salikhov
ed2e185fe2
Merge pull request #14 from retailcrm/deprecates
sf 6 deprecation types
2024-06-18 20:02:23 +03:00
Ilyas Salikhov
56dd45e0d1 sf 6 deprecation types 2024-06-18 20:01:11 +03:00
Ilyas Salikhov
1cd20df360
Merge pull request #13 from retailcrm/tests
Actualize the code
2024-06-18 19:14:51 +03:00
Ilyas Salikhov
c8e33918a2 Support of PHP 8.2 2024-06-18 13:29:17 +03:00
Ilyas Salikhov
5cbcba78df Symfony 6 compability 2024-06-18 12:54:58 +03:00
Ilyas Salikhov
72441d6bf3 Run phpunit tests in CI 2024-06-18 12:27:20 +03:00
Ilyas Salikhov
b603381139 1. Up min PHP version to 8.1
2. Up min Symfony verion to 5.0. Adopt code to sf 5.0+
3. Local env to run tests
4. Repare tests
2024-06-18 12:24:32 +03:00
Ilyas Salikhov
0765d2b453 Compability with PHP 8.1 (string functions calling) 2024-01-30 18:39:44 +03:00
Ilyas Salikhov
c764717de4 Compability with PHP 8.1 (class_exists calling) 2024-01-30 18:31:12 +03:00
Alexey Chelnakov
66bcfd82e3 fix twig 2023-01-09 16:28:18 +03:00
Alexey
b49a0eb8e7
Sf54 comp (#12)
* symfony 5.4 compatibility
2023-01-09 15:59:54 +03:00
Alexey Chelnakov
6abd901696 fix null $controller 2022-10-07 19:08:17 +03:00
Alexey
d8b5ab9f71
Fix deprecations (#8)
* fix symfony deprecations
2022-10-07 15:32:38 +03:00
Alexey
8d699084aa
Merge pull request #7 from retailcrm/upgrade
Upgrade
2022-07-12 10:14:30 +03:00
mantis
056087cbbf Fix: Deprecation: A tree builder without a root node is deprecated since symfony 4.2 (#1457)
* Fix: Deprecation: A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.

* Fix: Deprecation: A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.
2022-04-26 16:59:07 +03:00
Alan Poulain
b9718f3400 Compatibility with Symfony 4.3 (#1524)
* Use twig instead of templating

* Fix at least one suite test in Travis

* Use Twig namespace (Twig > 1.10 and TwigBundle > 2.2)
2022-04-26 16:58:48 +03:00
Sergey Linnik
33964a4bfc
Merge pull request #6 from devalex86/form_info_parser
ability to change information for types of forms
2020-07-06 20:59:51 +03:00
Alexander Kulinich
d43396c930 ability to change information for types of forms 2020-07-06 14:53:58 +03:00
Vitaliy Chesnokov
99b9d4f486
Fix generateHumanReadableTypes 2019-04-29 12:39:48 +03:00
Vitaliy Chesnokov
8a6213cbd2
Removed duplicated constraints in Format field. Improved format for field type of form 2019-04-29 12:39:48 +03:00
Vitaliy Chesnokov
39d640cce8
Validation with considering groups 2019-04-29 12:39:48 +03:00
Vitaliy Chesnokov
d78f8887a9
Pass the data about field name and field class to template 2019-04-29 12:39:45 +03:00
Vitaliy Chesnokov
befd12e3e5
Form labels translation support. 2019-04-25 21:55:45 +03:00
Vitaliy Chesnokov
81274509da
Allow to define the input name for the JmsMetadataParser 2019-04-25 21:55:45 +03:00
Vitaliy Chesnokov
b7464a93b9
* Range Constraint parsing in the ValidationParser
* Better formatting form Length constraint description
* Allow to define the input name for the ValidationParser
2019-04-25 21:55:41 +03:00
Vitaliy Chesnokov
cd87930d29
Check the version in methods response 2019-04-25 21:50:32 +03:00
Vitaliy Chesnokov
c6fa1e893e
Locale setting for the Translator 2019-04-25 21:50:32 +03:00
Vitaliy Chesnokov
1c4f003e76
Doc generation for the specific API version 2019-04-25 21:50:28 +03:00
Guilhem N
f0a606b636
Merge pull request #1134 from dunglas/fix-34-40-compat
[2.x] Fix compatibility with Symfony 3.4 and 4.0
2017-12-05 07:14:09 +01:00
Kévin Dunglas
f4b599f6d0
[2.x] Fix compatibility with Symfony 3.4 and 4.0 2017-12-04 23:11:29 +01:00
Guilhem Niot
8886e6eff8 Merge pull request #1043 from xabbuh/patch-1
fix section title markup
2017-07-25 11:28:44 +02:00
Christian Flothmann
83ed0a3407 fix section title markup 2017-07-25 11:26:50 +02:00
Guilhem Niot
17b6c3aeba Update branch alias 2017-05-31 14:17:35 +02:00
Guilhem Niot
adcdd91950 Update the documentation link 2017-05-13 16:53:58 +02:00
Lukas Kahwe Smith
ae18441763 Merge pull request #989 from lsmith77/submit-boolean-as-json-boolean
ensure that boolean fields are submited as boolean values when using json
2017-04-28 17:27:09 +02:00
Lukas Kahwe Smith
e5268e3f7f
ensure that boolean fields are submited as boolean values when using json 2017-04-28 16:56:15 +02:00
Guilhem Niot
572c69a509 Merge pull request #988 from lsmith77/fix-filters-always-as-query-parameters
Fix filters always as query parameters
2017-04-28 14:36:48 +00:00
Lukas Kahwe Smith
33307d750e
fix typo to force filters as query parameters 2017-04-28 15:58:38 +02:00
Soltész Balázs
e2c3d7ce94
Make filters always be passed as query string parameters in sandbox requests. 2017-04-28 12:36:27 +02:00
Guilhem Niot
3727d043cb Merge pull request #963 from shadowjs/patch-1
Update swagger-support.rst
2017-03-15 13:54:04 +01:00
shadowjs
ad1b9eec31 Update swagger-support.rst 2017-02-23 22:51:49 +01:00
Javier Spagnoletti
eab2d5d8fc Bumping to PHP 5.4 since the current codebase is already using features introduced in that version 2017-02-19 02:14:31 -03:00
Guilhem N
b01128dc13 Merge pull request #841 from flip111/patch-1
Update ApiDocExtractor.php
2016-12-29 12:21:15 +01:00
Guilhem N
7a7d743ff6 Merge pull request #915 from mhor/patch-1
docs: fix rst typo
2016-12-29 12:16:30 +01:00
Guilhem N
ff3bf88bed Merge pull request #895 from wouterj/patch-1
Fixed little Markdown syntax
2016-12-29 12:14:59 +01:00
Guilhem N
2d70b08021 Merge pull request #924 from tael/patch-1
swagger-ui repository changed
2016-12-29 12:10:31 +01:00
William Durand
95017bf780 Merge pull request #926 from kustov-an/dump_command_description
Add description for the dump command
2016-10-26 09:33:33 +02:00
Alexander Kustov
8f9d096448 Add description for the dump command 2016-10-26 10:15:13 +05:00
TaeL Kim
66e8e15ff1 swagger-ui repository changed 2016-10-21 14:55:56 +09:00
William Durand
958e697981 Merge pull request #922 from javiereguiluz/fix_doc_errors
Fixed minor RST syntax issues in docs
2016-10-03 21:10:35 +02:00
Javier Eguiluz
c531fcc037 Fixed minor RST syntax issues in docs 2016-10-03 15:42:40 +02:00
Maxime Horcholle
13c25eb4c4 docs: fix rst typo 2016-09-22 10:29:54 +02:00
William Durand
73d4db51e1 Merge pull request #912 from ruscon/patch-1
Update .travis.yml
2016-09-10 08:38:33 +02:00
Coroliov Oleg
9387638079 Update .travis.yml 2016-09-10 00:38:59 +03:00
Wouter J
b5d371d984 Fixed little Markdown syntax 2016-08-10 10:37:07 +02:00
William Durand
e6e424687e Merge pull request #890 from Invis1ble/fix-889
Fixed placeholder translations #889
2016-08-04 11:03:43 +02:00
Invis1ble
97c6b1e857 Fixed placeholder translations #889 2016-07-29 09:20:35 +03:00
William Durand
463dea9af6 Merge pull request #874 from rmzamora/master
Fix error: Key "statusCodes" for array with keys...
2016-06-16 17:04:18 +02:00
mellzamora
f5e38a283a Fix error:
Key "statusCodes" for array with keys "method, uri, description, documentation, filters, requirements, parsedResponseMap, https, authentication, authenticationRoles, deprecated, id" does not exist in NelmioApiDocBundle::method.html.twig at line 182.

Sample:

 /**
     * Retrieves the list of categories (paginated) based on criteria.
     *
     * @ApiDoc(
     *  resource=true,
     *  output={"class"="Sonata\DatagridBundle\Pager\PagerInterface", "groups"={"sonata_api_read"}}
     * )
     *
     *
     * @QueryParam(name="page", requirements="\d+", default="1", description="Page for category list pagination")
     * @QueryParam(name="count", requirements="\d+", default="10", description="Number of categories by page")
     * @QueryParam(name="enabled", requirements="0|1", nullable=true, strict=true, description="Enabled/Disabled categories filter")
     * @QueryParam(name="context", requirements="\S+", nullable=true, strict=true, description="Context of categories")
     *
     * @View(serializerGroups="sonata_api_read", serializerEnableMaxDepthChecks=true)
     *
     * @param ParamFetcherInterface $paramFetcher
     *
     * @return PagerInterface
     */
2016-06-16 22:44:56 +08:00
William Durand
a3a9bb3b70
Prepare 2.13.0 release 2016-06-13 11:12:09 +02:00
William Durand
79716d49f5
Remove a test since #811 changes the behavior 2016-06-13 11:04:16 +02:00
William Durand
4b16f4e068
Do not fail on Symfony 4.0 deprecation messages 2016-06-13 10:53:00 +02:00
William Durand
7aeea0871c Merge pull request #811 from InputOutput/allow-input-and-filters
Allow filter descriptions to be used in conjunction with POST/PUT inp…
2016-06-13 10:01:34 +02:00
William Durand
9444172abc Merge pull request #857 from zanardigit/master
Add navigation index for resources
2016-06-13 10:01:12 +02:00
William Durand
e6ea85157e Merge pull request #868 from dpcat237/502_hide-requirement-when-empty
#502 hide requirement when empty
2016-06-13 10:00:49 +02:00
Denys Pasishnyi
1256275185 #502 Test for the improvement 2016-06-13 00:30:40 +02:00
Denys Pasishnyi
725e4d9dda #502 Hide Requirement when not set 2016-06-13 00:29:59 +02:00
William Durand
a009e97382 Merge pull request #866 from Gregoire-M/master
Usage of OUTPUT_RAW to avoid javascript syntax error when dumping HTML
2016-06-10 13:30:55 +02:00
gmarchal
05d3a51259 Usage of OUTPUT_RAW to avoid javascript syntax error when dumping HTML API doc. Fixes issue #864. 2016-06-10 10:47:43 +02:00
William Durand
6d16486abb Merge pull request #865 from javiereguiluz/fix_doc_links
Fixed the links to some doc articles
2016-06-09 16:50:28 +02:00
Javier Eguiluz
4a37d638f8 Fixed the links to some doc articles 2016-06-09 15:56:07 +02:00
William Durand
d0eaafadcb Merge pull request #861 from Uplink03/master
Links from index.rst to the other doc files
2016-06-03 17:50:40 +02:00
Uplink03
8eee7fec49 Added links form index.rst to the other doc files 2016-06-02 16:30:54 +01:00
William Durand
970a78c614 Merge pull request #860 from jupeter/format-wrapping
Fix wrapping of format table row
2016-06-02 11:29:59 +02:00
Piotr Plenik
12c067154c fix format wrapping 2016-06-02 10:33:39 +02:00
Francesco Abeni
df04b5c871 Add navigation index for resources 2016-05-24 13:13:39 +02:00
William Durand
7288ccad07 Merge pull request #853 from miholeus/master
Headers support
2016-05-19 21:43:45 +02:00
miholeus
5123b49bf7 added tests for headers support 2016-05-19 18:20:41 +03:00
miholeus
d5c1c57cc9 added headers documentation example 2016-05-19 14:35:55 +03:00
miholeus
e141ffd291 added headers support 2016-05-19 14:28:32 +03:00
William Durand
a7ad7ff144 Merge pull request #851 from blitzr/master
Allow custom ApiDoc annotation
2016-05-16 21:08:35 +02:00
Martin Le Guillou
aada3151aa enable ApiDocExtractor overriding 2016-05-16 15:59:41 +02:00
William Durand
b494d8e1ab Merge pull request #821 from tcz/sandbox_output_improvements
Adding request body and curl command to sandbox
2016-05-11 17:32:04 +02:00
William Durand
6c77f6f422 Merge pull request #824 from fstr/master
Change visibility of markdownParser property to protected
2016-05-11 17:18:13 +02:00
William Durand
9dfd984c68 Merge pull request #832 from ismailbaskin/vich-compat
Allow sending empty file data
2016-05-11 17:16:17 +02:00
William Durand
4660805291 Merge pull request #823 from ImmRanneft/master
Little template fixes for sandbox
2016-05-11 17:15:43 +02:00
flip111
8921c48b32 Update ApiDocExtractor.php 2016-04-29 18:29:39 +02:00
William Durand
2d214fc934 Merge pull request #775 from DonPrus/master
Fix problem with cyrillic chars in docs
2016-04-21 17:38:04 +02:00
William Durand
9caa2816e7 Merge pull request #837 from timhovius/master
Added translation to description
2016-04-20 13:41:39 +02:00
Tim Hovius
96cfab973c Added translation to description
The labels of a field are usually always translated. But this does not happen in the documentation.
2016-04-19 21:37:22 +02:00
İsmail BASKIN
246babf209 Allow sending empty file data 2016-04-08 15:26:31 +03:00
Jelte Werkhoven
fcd4d8fa2a Allow filter descriptions to be used in conjunction with POST/PUT input descriptions 2016-03-31 10:16:50 +02:00
Florian Strübe
b510dd2c70 Change visibility of markdownParser property to protected, to improve extendability 2016-03-23 10:25:51 +01:00
William Durand
718dfc6e9c Merge pull request #827 from TannerPO/patch-1
Rename other-bundle-annotations.Rst to other-bundle-annotations.rst
2016-03-22 08:28:24 +01:00
Tanner Newton
8cb2847af4 Rename other-bundle-annotations.Rst to other-bundle-annotations.rst 2016-03-21 19:30:50 -07:00
William Durand
4ed06b40f8 Merge pull request #826 from crazyball/patch-1
Fixed link to documentation
2016-03-21 15:01:15 +01:00
Ambrosini Loïc
f6ec8ba6b1 Fixed link to documentation
Link to ApiDoc documentation is now with ReST format instead of Markdown
2016-03-21 14:47:18 +01:00
William Durand
6793b70157
Prepare 2.12.0 release 2016-03-21 12:19:12 +01:00
William Durand
4a95c81914 Merge pull request #814 from javiereguiluz/fix_813
Transformed documentation to RST format
2016-03-21 11:31:18 +01:00
William Durand
476348de54 Merge pull request #825 from nelmio/conflict
Mark symfony ^2.7.7 as conflict
2016-03-21 11:30:06 +01:00
William Durand
4986e02ec1
Mark symfony ~2.7.8 as conflict 2016-03-21 11:18:46 +01:00
ImmortaL
df10a4769c Little template fixes for sandbox 2016-03-18 21:23:22 +03:00
tcz
aef6a89329 Adding request body and curl command to sandbox 2016-03-17 16:09:47 +01:00
Javier Eguiluz
db5303fcd7 Transformed all files 2016-03-05 19:49:21 +01:00
William Durand
2a0f95eac0 Merge pull request #792 from rodrigm/master
Fix #565
2016-02-24 19:00:28 +01:00
Rodrigo Gómez
89f459dc07 Fix #565
Change PUT by PATCH

Refactor tests for #565
2016-02-24 17:57:22 +01:00
William Durand
4381f065cb Merge pull request #794 from mcfedr/patch-1
Remove incorrect usage of hasOption
2016-02-24 17:51:24 +01:00
William Durand
cec5108133 Merge pull request #781 from AlmogBaku/feature/textareaField
Add textarea type for sandbox
2016-02-24 17:50:13 +01:00
William Durand
364ef73680 Merge pull request #791 from Padam87/response-map
Use the response map in the html view
2016-02-24 17:49:33 +01:00
William Durand
65a5159ce9 Merge pull request #796 from zhukovra/master-array-requirements
Fix Array to string conversion error on array requirements
2016-02-24 17:47:14 +01:00
William Durand
dd6a5e32c3 Merge pull request #799 from debesha/master
Fixed bug with wrong form parsing when several custom inner types exist
2016-02-24 17:46:38 +01:00
William Durand
4e65cd3bc3 Merge pull request #805 from obense/master
Fix Collection Type options - Symfony 2.8 Support
2016-02-24 17:45:32 +01:00
William Durand
f6e952ffe1 Merge pull request #810 from teohhanhui/fix-678
Handle circular references in DunglasApiParser
2016-02-24 17:44:50 +01:00
William Durand
028b09e90a Merge pull request #772 from richaas/master
Added xhrFields: { withCredentials: true } to ajaxOptions
2016-02-24 17:43:14 +01:00
Teoh Han Hui
c1c711bc26 Handle circular references in DunglasApiParser
Fixes #678, reverts #800
2016-02-18 23:18:43 +08:00
William Durand
07545629aa Merge pull request #800 from dunglas/fix_678
Handle circular references in DunglasApiParser
2016-02-14 17:45:10 +01:00
Olivier Bense
18c9fb8c29 Fix Collection Type options - Symfony 2.8 Support 2016-02-09 14:32:49 +01:00
Kévin Dunglas
abb100b29b Handle circular references in DunglasApiParser. Close #678. 2016-02-04 08:34:34 +01:00
Dmitry Malyshenko
0ed53788d2 Fixed bug with wrong form parsing when several custom inner types exist 2016-02-03 10:02:33 +00:00
Zhukov Roman
f2fc6b47c0 Fix Array to string conversion error on array requirements
Fixes #564
2016-02-01 14:33:17 +03:00
Fred Cox
9b734b22bd Remove incorrect usage of hasOption
`hasOption` is confusingly named, it actually checks that the option has been created, not if the user has passed the option. So in this case its always true.
Also changed a weird usage of `in_array`.
2016-01-29 10:56:25 +02:00
Adam Prager
7919b24971 Use the response map in the html view 2016-01-26 04:36:38 +01:00
AlmogBaku
3bc98723fe Add textarea type for sandbox 2016-01-08 20:34:15 +02:00
William Durand
5d9c47bbad Merge pull request #780 from damienalexandre/fixArrayToString
Fix Array to string conversion error on array requirements
2016-01-08 13:33:10 +01:00
Damien Alexandre
306f2c47cf Fix Array to string conversion error on array requirements
Fix https://github.com/nelmio/NelmioApiDocBundle/issues/773
See https://github.com/FriendsOfSymfony/FOSRestBundle/pull/1015
2016-01-08 13:04:25 +01:00
Richard
4744de98fb Added xhrFields: { withCredentials: true } to ajaxOptions to enable
browser authentication for cross origin API requests.
2016-01-06 10:00:21 +01:00
Сомов Игорь Андреевич
8aa5e479d1 JSON UNICODE 2015-12-28 17:07:20 +03:00
William Durand
1ae2cfa9a5 Merge pull request #768 from SHyx0rmZ/fix-sf-30-dump-command-enter-scope
Check for deprecated method enterScope() in DumpCommand
2015-12-16 16:17:51 +01:00
Patrick Pokatilo
8c336487e7 Check for deprecated method enterScope() in DumpCommand 2015-12-15 17:04:17 +01:00
William Durand
93b189b16a Merge pull request #766 from odolbeau/avoid-array-to-string-conversion
Avoid "array to string conversion" error
2015-12-11 09:12:17 +01:00
Olivier Dolbeau
d3bc49c3d1 Avoid "array to string conversion" error 2015-12-10 09:23:57 +01:00
William Durand
19d4e37365 Merge pull request #765 from Ener-Getick/DEPRECATION
Fix a deprecation
2015-12-08 14:19:42 +01:00
Ener-Getick
24fa5dfbc0 Fix a deprecation 2015-12-08 13:06:38 +01:00
William Durand
49238f44aa
Prepare 2.11.0 release 2015-12-04 09:15:49 +01:00
William Durand
d5395cc0ea
Fix CS 2015-12-04 09:15:21 +01:00
William Durand
1df3ddf49f Merge pull request #752 from Ener-Getick/SF3
Add symfony 3.0 support
2015-12-04 09:09:28 +01:00
Ener-Getick
0461f5ce1d Optimize travis 2015-12-04 08:19:54 +01:00
Ener-Getick
5541412e69 Fix FOSRestBundle compatibility 2015-12-02 16:19:24 +01:00
Ener-Getick
67b7b46627 Add symfony 3.0 support 2015-12-02 16:19:12 +01:00
William Durand
97707ea5f4 Merge pull request #750 from piotrantosik/host-placeholder
Fix route host with placeholder
2015-11-25 10:13:47 +01:00
Piotr Antosik
dd8bdd2070 Fix route host with placeholder 2015-11-23 00:18:50 +01:00
William Durand
4fad200d7b Merge pull request #758 from dunglas/master
Fix tests related to DunglasApiBundle
2015-11-22 08:22:33 +01:00
Kévin Dunglas
224af02f7f Fix tests related to DunglasApiBundle 2015-11-21 18:36:13 +01:00
William Durand
eafa2ade0b Merge pull request #749 from fstr/ISSUE-747-JSP
Introduced default-value parameter to JsonSerializableParser
2015-11-10 14:39:07 +01:00
Florian Strübe
aa10f9a2da Introduced default-value parameter to JsonSerializableParser
JSP is now able to set a default value for scalar types
This makes merging the parser result with other parser results easier
and gives some additional info when creating docs

Removed PHP doc comment block
2015-11-10 14:34:53 +01:00
William Durand
a24d81f766 Merge pull request #748 from fstr/ISSUE-747
Empty string is now a valid default parameter value
2015-11-10 09:18:44 +01:00
William Durand
ac07a59ed4 Merge pull request #751 from piotrantosik/test-doc
Fix comment block in tests
2015-11-10 09:06:43 +01:00
Piotr Antosik
3dea122b98 fix comment in tests 2015-11-10 01:28:40 +01:00
Florian Strübe
e8a2f690ab Empty string is now a valid default parameter value that is successfully
merged with other parameters
Preventing access of uninitialized array key 'default'
Added functional test case for mergeParameters that covers this issue
2015-11-09 22:22:00 +01:00
William Durand
be90e8aad6 Merge pull request #740 from fstr/master
[ISSUE-739] ApiDoc parameters setting will override lower parameter definitions in the hierarchy
2015-10-28 10:30:40 +01:00
Florian Strübe
e2d2e41f7b [ISSUE-739] ApiDoc parameters setting will override lower parameter
definitions in the hierarchy
2015-10-28 10:13:24 +01:00
Jonathan Chan
8b7dfdd913
Let the js library set the proper Content-type header when sending files in sandbox - fixes #501 2015-10-27 17:29:36 +01:00
William Durand
0b7c5d6749 Merge pull request #742 from nelmio/sf-2.8
Support Symfony 2.8+
2015-10-27 17:28:11 +01:00
William Durand
9df40af264
Support Symfony 2.8+
Fix #738
2015-10-27 17:15:32 +01:00
William Durand
d25dd30453
Refactor doc 2015-10-27 10:35:58 +01:00
William Durand
d49ac45866 Add FAQ section
Add question/answer from #727
2015-10-27 10:00:23 +01:00
William Durand
09bb83e149
Restore 5.3 compat (implicit)
Implicit means: it's a best effort approach
but there is no guarantee that this bundle
will continue to work well with PHP < 5.4
2015-10-27 09:23:44 +01:00
William Durand
329e226426 Merge pull request #736 from BenjaminPaap/pass_options
Options for form were not passed through
2015-10-26 11:40:51 +01:00
Benjamin Paap
490a9081ec Options for form were not passed through 2015-10-26 11:11:28 +01:00
William Durand
f8b038faf2 Merge pull request #734 from TomasVotruba/patch-2
composer: allow min PHP 5.4
2015-10-23 15:28:30 +02:00
Tomáš Votruba
e0a001081a composer: allow min PHP 5.4
According to Travis, the min supported version is 5.4
2015-10-23 15:21:03 +02:00
William Durand
fb863954d8
Prepare 2.10.0 release 2015-10-23 11:30:55 +02:00
William Durand
7f5f8bd258 Merge pull request #694 from tonivdv/revert_fix_link
revert fix "LINK workaround for firefox"
2015-10-23 11:00:49 +02:00
William Durand
57b9aa659e
skip tests related to DunglasApiBundle 2015-10-23 10:53:47 +02:00
William Durand
beaa25b21c
Fix tests (introduce result files to make things a bit more readable) 2015-10-23 10:36:53 +02:00
William Durand
0dc1bc4469
Revert "Fix test related to DungleApiBundle. Remove some of them."
This reverts commit e66fb209b5.
2015-10-23 10:11:52 +02:00
William Durand
4b72190f4d
Speedup tests 2015-10-23 10:07:45 +02:00
William Durand
e66fb209b5
Fix test related to DungleApiBundle. Remove some of them. 2015-10-23 09:29:55 +02:00
William Durand
b37924d4b1
switch to stable version of DunglasApiBundle 2015-10-22 23:06:01 +02:00
William Durand
716f3f19de
fix test suite by disabling a broken feature 2015-10-22 22:46:25 +02:00
William Durand
96e7d1a201
remove useless file 2015-10-22 17:41:48 +02:00
William Durand
c8cea500fa
Merge pull request #719 from maisoui/patch-1
Update MarkdownExtension.php
2015-10-22 16:20:38 +02:00
William Durand
ce71bf0629
Fix CS 2015-10-22 14:42:59 +02:00
William Durand
3c47357a0e
Merge branch 'Soullivaneuh-deprecated-routes' 2015-10-22 14:42:25 +02:00
Sullivan SENECHAL
32de80f97f
Fix deprecated route options 2015-10-22 14:42:16 +02:00
William Durand
189197dc88
Merge pull request #698 from Soullivaneuh/twig-deprecate
Fix deprecated twig stuff usage
2015-10-22 14:41:15 +02:00
William Durand
f08b957670 Merge pull request #713 from dice4x4/master
Twig 2 compatibility
2015-10-22 14:37:56 +02:00
William Durand
c3092d5e36 Merge pull request #730 from OskarStark/patch-1
wrong parameter??
2015-10-22 14:37:38 +02:00
William Durand
b39b7df0d8 Merge pull request #731 from teohhanhui/fix-720-mistake
Fix mistake in #720
2015-10-22 14:37:08 +02:00
Toni Van de Voorde
d60da7a734 remove 1 empty line to force new tests on travis 2015-10-22 12:10:09 +02:00
Teoh Han Hui
5f61d5faf4 Fix mistake in #720 2015-10-19 11:22:39 +08:00
Oskar Stark
ca0fbe38d7 wrong parameter?? 2015-10-14 18:17:25 +02:00
Ilya Cherepanov
f815162ba7 Using old array syntax 2015-10-01 09:55:48 +03:00
William Durand
9af37448fa Merge pull request #720 from maisoui/patch-2
Update routing.yml
2015-09-30 23:35:45 +02:00
Jonathan
082b2fa86f Update routing.yml
Replace deprecated "pattern" and "_method" by "path" and "methods"
2015-09-22 14:16:09 +02:00
Jonathan
6dc6e7c55a Update MarkdownExtension.php
Replace deprecated 'Twig_Filter_Method' by 'Twig_SimpleFilter'
2015-09-22 14:12:25 +02:00
Ilya Cherepanov
2cbd828973 Twig 2 compatibility 2015-09-15 19:15:08 +03:00
Sullivan SENECHAL
9c0c533b4b Fix deprecated twig stuff usage 2015-08-28 10:50:28 +02:00
William Durand
874c8752e6 Merge pull request #695 from ogizanagi/fix_callable_controllers
Fix ApiDocExtractor to accept callable classes as controllers
2015-08-13 09:46:00 +02:00
maxime.steinhausser
6f4373cda4 Fix ApiDocExtractor to accept callable classes as controllers 2015-08-11 17:12:43 +02:00
Toni Van de Voorde
f57a8bf7ae revert fix "Added workaround for Firefox not sending LINK in uppercase what then" 2015-08-11 11:32:48 +02:00
William Durand
4351ca66d3 Merge pull request #681 from munkie/fix-view-cache
Fix CachingApiDocExtractor caches only first accessed view
2015-07-29 11:00:30 +02:00
William Durand
f77f52c041 Merge pull request #685 from dunglas/patch-1
Prevent BC break in DunglasApiBundle
2015-07-29 08:55:16 +02:00
Kévin Dunglas
840b96f8f3 Prevent BC break in DunglasApiBundle 2015-07-29 00:56:26 +02:00
Mikhail Shamin
be682f7d18 Change getViewCache method visibility to private 2015-07-22 11:20:20 +07:00
Mikhail Shamin
02007e957d Fix extractor view data caching 2015-07-21 17:38:44 +07:00
William Durand
0699c45dcc Merge pull request #656 from soyuka/patch-1
Prevent error "Undefined index" when no subtype provided
2015-07-10 11:15:36 +02:00
William Durand
787f69561b Merge pull request #665 from mcfedr/json-serializable
Add support for JsonSerializable classes
2015-07-04 13:52:22 +02:00
Fred Cox
406a4e1b5b Add support for using name in the input and output options for JsonSerializable and validation parsers 2015-07-01 14:05:10 +03:00
Fred Cox
37c6465700 Add a parser for jsonSerializable classes 2015-07-01 14:04:38 +03:00
William Durand
a343bb7c45 Merge pull request #663 from phansys/doc_typo
[minor] Small doc fix in index.md
2015-06-30 00:05:07 +02:00
Javier Spagnoletti
7eccee0a36 [minor] Small doc fix in index.md
| Q             | A
| ------------- | ---
| Doc fix?      | yess
| Fixed tickets |
| License       | MIT
| Doc PR        |

Small fix in ```index.md```.
2015-06-29 13:30:59 -03:00
Antoine Bluchet
20f4cc673f Prevent error "Undefined index" when no subtype provided 2015-06-22 15:47:46 +02:00
William Durand
7c8b010215 Merge pull request #654 from mRoca/master
DunglasApiBundle : Add doc sections
2015-06-18 15:30:13 +02:00
mRoca
65345912a9 Add DunglasApiBundle doc sections 2015-06-18 12:23:07 +02:00
William Durand
a05ed38501 Merge pull request #653 from staurand/master
Added pipe to escaped symbols of jQuery selector
2015-06-17 17:46:56 +02:00
staurand
a967fce6c7 Added pipe to escaped symbols of jQuery selector
Pipe symbol needs to be escaped in jQuery selector.
This symbol is added when multiple methods are allowed for a route.
e.g /api/doc#get|post--...
2015-06-17 11:48:31 +02:00
William Durand
c7909c576e Merge pull request #641 from Soullivaneuh/configure-options
Fix deprecated FormType::setDefaultOptions usage
2015-06-11 13:51:25 +02:00
William Durand
b07db95eaf Merge pull request #648 from Soullivaneuh/patch-1
Bump Symfony requirements to 2.3
2015-06-11 13:50:23 +02:00
Sullivan SENECHAL
b7dbccbc96 Bump Symfony requirements to 2.3 2015-06-10 12:19:47 +02:00
William Durand
a679eb8089 Merge pull request #645 from ogizanagi/dunglas/filters_collection_only
Only display filters on collections GET services
2015-06-08 14:24:59 +02:00
William Durand
b672df0aca Merge pull request #637 from magnetik/fosrestbundle-array
Handle "array" parameter in FOSRestBundle QueryParam or RequestParam
2015-06-08 14:23:58 +02:00
maxime.steinhausser
12372cca00 Only display filters on collections GET services 2015-06-08 11:42:36 +02:00
Baptiste Lafontaine
213dbdfd1c Handle "array" parameter in FOSRestBundle QueryParam or RequestParam 2015-06-08 08:43:38 +02:00
William Durand
a68dcfe829 Merge pull request #643 from ogizanagi/dunglas/fix_filters_doc
Fix DunglasApiBundle filters documentation support.
2015-06-08 08:37:13 +02:00
William Durand
6be99b53ca Merge pull request #644 from Soullivaneuh/config-improve
Improve true/false/null default values on configuration
2015-06-07 18:02:34 +02:00
Sullivan SENECHAL
839e9ed7e0 Improve true/false/null default values on configuration 2015-06-07 11:34:26 +02:00
maxime.steinhausser
a080692961 Fix DunglasApiBundle filters documentation support. 2015-06-05 16:27:29 +02:00
Sullivan SENECHAL
f5b45607c6 Fix deprecated FormType::setDefaultOptions usage 2015-06-05 09:34:11 +02:00
William Durand
f305905928 Merge pull request #640 from Soullivaneuh/travis-update
Include Symfony 2.7 stable on Travis and improve configuration
2015-06-05 09:04:17 +02:00
Sullivan SENECHAL
3072cc2917 Include Symfony 2.7 stable on Travis and improve configuration 2015-06-04 12:20:14 +02:00
William Durand
36c9f8b7fc Merge pull request #638 from piotrantosik/patch-1
Improve doc format
2015-06-03 20:55:46 +02:00
Piotr Antosik
38a3137465 Improve doc format 2015-06-02 18:15:49 +02:00
William Durand
5de5d530dd Merge pull request #630 from munkie/dump-command-views
Add view option to api:doc:dump command
2015-05-26 17:36:26 +02:00
William Durand
4bee3afe9a Merge pull request #629 from ifdattic/patch-1
Update index.md
2015-05-26 16:21:03 +02:00
Mikhail Shamin
1a92a112bc Added view option to api:doc:dump command 2015-05-26 18:09:29 +07:00
Andrew Marcinkevičius
2d1c9693a1 Update index.md 2015-05-26 13:19:18 +03:00
William Durand
e505139f98 Merge pull request #627 from csuarez/css-fix
Styles for lists inside .content
2015-05-22 11:49:13 +02:00
César Suárez
b801ddbf48 Styles for lists inside .content 2015-05-22 11:33:26 +02:00
William Durand
de31760fd8
Prepare 2.9.0 release 2015-05-16 19:16:14 +02:00
William Durand
cc93ddb306 Merge pull request #550 from thenetexperts/master
better readability when using text inside <pre> tag in motd html
2015-05-16 13:19:05 +02:00
William Durand
647131ab8e Merge pull request #586 from PedroTroller/fix/subform-options
Pass children options to subform
2015-05-16 13:07:10 +02:00
William Durand
a0c0398ca9 Merge pull request #540 from rodrigorigotti/master
Showing 'Documentation' tab makes no sense if the sandbox is disabled.
2015-05-16 12:59:29 +02:00
William Durand
4398a8861c
Test against PHP 5.6 2015-05-16 12:54:31 +02:00
William Durand
14aba75938
remove hhvm-nightly in travis-ci config 2015-05-16 12:53:16 +02:00
William Durand
1e33a8821c
Refactor tests 2015-05-16 12:48:11 +02:00
William Durand
e04981356b
Fix doc: Default => default (views) 2015-05-16 12:22:11 +02:00
William Durand
e46bd73a32
Fix routing definition 2015-05-16 12:21:32 +02:00
William Durand
c71fa155d5
Introduce the concept of 'views'
Rewrite #619
2015-05-16 12:17:59 +02:00
William Durand
94ba751848
fix CS 2015-05-15 17:42:03 +02:00
William Durand
f601dc17fb
Merge branch 'Nyholm-issue-404' 2015-05-15 17:41:37 +02:00
Joshua Thijssen
6052643b9f
Initial setup on a multi-api documentation 2015-05-15 17:41:22 +02:00
William Durand
b3e7c1498d Merge pull request #622 from dunglas/fix_filters
Fix DunglasApi filters support. Fix tests.
2015-05-15 02:28:38 +02:00
Kévin Dunglas
9c465e37ca Fix DunglasApi filters supports. Fix tests. 2015-05-14 23:23:41 +02:00
William Durand
1d7b45e1e7 Merge pull request #543 from mojoLyon/fix/issue-398
Fix/issue 398
2015-05-13 22:57:45 +02:00
Stéphane Reuille
dc20b249e7 fix memory limit issue wirh entity list
Allow to disable mapping of entity list to avoid memory limit error on long list
2015-05-13 14:51:15 +02:00
William Durand
a8f047c78a Merge pull request #617 from Soullivaneuh/deprecated-form-options
Fix deprecated forms setDefaultOptions method
2015-05-13 11:06:52 +02:00
William Durand
54be9c3bf1 Merge pull request #615 from matks/twig-doc
Add documentation statement about twig dependency
2015-05-13 11:06:35 +02:00
Sullivan SENECHAL
c1d7edd162 Fix deprecated forms setDefaultOptions method 2015-05-01 02:16:35 +02:00
Mathieu Ferment
a1ea71e1c3 Add doc statement about twig dependency 2015-04-30 17:40:12 +02:00
William Durand
f7611613af Merge pull request #612 from dunglas/patch-2
Fix broken image in README.md
2015-04-30 09:56:37 +02:00
Kévin Dunglas
a53b061baa Fix broken image in README.md 2015-04-30 09:05:17 +02:00
William Durand
768f45641e Merge pull request #610 from dunglas/enhanced_dngls_api
Enhanced DunglasApiBundle support
2015-04-29 22:42:26 +02:00
William Durand
7336771f87 Merge pull request #611 from dunglas/patch-1
Use new Travis infrastructure. Test with PHP7.
2015-04-29 22:41:19 +02:00
Kévin Dunglas
79c1c4e4f4 Enhanced DunglasApiBundle support 2015-04-29 09:11:54 +02:00
Kévin Dunglas
8ee26388c0 Use new Travis infrastructure. Test with PHP7. 2015-04-28 23:55:57 +02:00
William Durand
ad70ff96fa Merge pull request #602 from snamor/patch-1
Add the commas to the prettified JSON
2015-04-14 17:21:49 +02:00
Pol
226e38851f Optimization on the regex 2015-04-14 17:10:09 +02:00
Pol
186d673517 Add the commas to the prettified JSON
I recently came across that when copying the prettified response there were no comma separating the elements. This solves this problem.
2015-04-14 17:01:01 +02:00
William Durand
8b6186500a Merge pull request #599 from dunglas/fix_dunglas
Fix DunglasJsonLdApiBundle support
2015-04-08 09:07:32 +02:00
Kévin Dunglas
c47265bb71 Fix DunglasJsonLdApiBundle support 2015-04-07 22:21:36 +02:00
William Durand
ef917f257d Merge pull request #597 from leberknecht/feature-collapsible-json-response
[Feature] collapsible sections in json-response
2015-03-31 17:13:44 +02:00
dtonder
01f4efc4e0 -fixed binding of on-click handler for json-toggler, - fixed css-class naming 2015-03-29 15:06:58 +02:00
dtonder
d73531aeb5 - added javascript for collapsing sections in json-response, - fixed usage of private service-alias (that will be removed by RemovePrivateAliases-compiler pass) 2015-03-29 14:37:06 +02:00
William Durand
6a9ca36bb4 Merge pull request #560 from wodka/patch-1
fix sandbox with host
2015-03-25 23:17:02 +01:00
William Durand
d516de98f9 Merge pull request #593 from AlmogBaku/patch-1
fixes: not attaching `api_key` to query if empty
2015-03-25 23:16:28 +01:00
Almog Baku
752b953a07 fixes: not attaching api_key to query if empty 2015-03-25 14:53:31 +02:00
William Durand
26fe302a5a Merge pull request #585 from dunglas/dunglasjsonldapibundle
DunglasJsonLdApiBundle support
2015-03-20 10:51:03 +01:00
Kévin Dunglas
adc03ba26d DunglasJsonLdApiBundle support 2015-03-20 09:53:48 +01:00
pedro
ff38cd4568 Pass children options to subform 2015-03-16 11:12:26 +01:00
William Durand
d8c05dbf57 Merge pull request #584 from PedroTroller/feature/form-options
Add form options to ApiDoc input
2015-03-13 17:22:24 +01:00
pedro
d121e28d69 Add form options to ApiDoc input
Update index.md

Remove unused variable. $arguments is now named $options
2015-03-13 16:15:29 +01:00
William Durand
f23351b132
update branch-alias (composer) 2015-03-06 11:47:58 +01:00
William Durand
0a1cef77ad
cs 2015-03-06 11:19:08 +01:00
William Durand
e23a7d7d66 Merge pull request #573 from jcrombez/patch-1
[doc] missing "sandbox:" in the authentication yaml examples
2015-02-06 16:04:01 +01:00
Jérémy CROMBEZ
6990a11e3a [doc] missing "sandbox:" in the authentication yaml examples 2015-02-06 15:54:35 +01:00
William Durand
d641bbf32f Merge pull request #562 from lmammino/patch-1
Fixed type annotations
2015-01-01 18:25:04 +01:00
Luciano Mammino
e0ff981653 Fixed type annotations
(avoid triggering warning on IDEs like PhpStorm
2014-12-31 17:08:41 +01:00
Michael Schramm
10d9172d26 fix sandbox with host
suggested change in https://github.com/nelmio/NelmioApiDocBundle/pull/353 is not working
2014-12-30 00:00:27 +01:00
William Durand
a84c53bf6a Merge pull request #555 from damienalexandre/fixLinkParser
Fix the PhpDoc Handler for `@link` annotation and add tests
2014-12-12 16:27:51 +01:00
Damien Alexandre
a939fef59b Fix the PhpDoc Handler for @link annotation and add tests 2014-12-12 16:13:55 +01:00
William Durand
8ff30b7dec Merge pull request #554 from greg0ire/documentation
proofread the index
2014-12-11 14:13:04 +01:00
Grégoire Paris
95b99a1129 proofread the index 2014-12-11 12:11:51 +01:00
thenetexperts
c6b57a22d1 better readability 2014-12-08 15:04:40 +01:00
Rodrigo Rigotti
fa4b8f3805 Showing 'Documentation' tab makes no sense if the sandbox is disabled. 2014-11-05 10:32:48 -02:00
William Durand
847b1fe757 Merge pull request #536 from devster/master
Display select to choose http method in sandbox
2014-10-27 00:51:54 +01:00
Jeremy Perret
e7d3c803bb Display select to choose http method in sandbox 2014-10-26 12:28:50 +01:00
Jordi Boggiano
4beb08e587 Update install instructions 2014-10-21 21:24:58 +01:00
Jordi Boggiano
cb6df68d8a Remove outdated note 2014-10-21 21:24:09 +01:00
William Durand
888705cd23 Merge pull request #512 from bezhermoso/form_errors
FOSRest integration: Form errors format
2014-10-12 15:38:01 +02:00
Bez Hermoso
859421df9a Form errors parser. Mirrored actual form-errors response by FOSRest. Made sure that FieldErrors is not duplicated. 2014-10-10 11:59:19 -07:00
William Durand
6d50c200ba Merge pull request #498 from jonmchan/honorBodyFormat
Honor body format before uploading file type parameters
2014-10-08 11:36:12 +02:00
William Durand
c69ab200d0 Merge pull request #524 from lucasvanlierop/fix-no-required-parameters-for-put-requests
Fix no required parameters for PUT requests
2014-10-08 11:34:06 +02:00
William Durand
5950acd73c Merge pull request #490 from ricardclau/store_api_key
Add support for LocalStorage
2014-10-08 11:32:56 +02:00
William Durand
ff5c201880 Merge pull request #519 from mjanser/fix-swagger-formatter
Fix parsing of filters, default values and base path in SwaggerFormatter
2014-10-06 11:47:14 +02:00
Martin Janser
b8cc4d9264 Fix parsing of filters, default values, descriptions and base path in SwaggerFormatter 2014-10-06 10:50:14 +02:00
lucasvanlierop
b4a6825db7 Fixed tests by adding separate form type 2014-09-30 13:54:30 +02:00
William Durand
3889e7ff24 Merge pull request #523 from jeskew/master
Update parser to convert empty array defaults to null
2014-09-29 17:12:56 +02:00
lucasvanlierop
f625d9671c Fixed disabling required for HTTP PUT requests 2014-09-29 16:49:25 +02:00
lucasvanlierop
eaaa54bf11 Fixed checking HTTP method type 2014-09-29 16:17:36 +02:00
Jonathan Eskew
37ae52ba0a Update parser to convert empty array defaults to null
Don't let empty arrays get passed through as defaults.
2014-09-26 14:19:55 -04:00
William Durand
6bcd5e8d81 Merge pull request #518 from AveVlad/patch-1
Add highlight index.md
2014-09-22 10:19:00 +02:00
Vlad
59ea615b7b Update index.md 2014-09-21 05:04:16 +04:00
William Durand
3f3580f9c3 Merge pull request #514 from stof/patch-2
Remove EOLed Symfony versions from Travis
2014-09-11 10:34:36 +02:00
Christophe Coevoet
8861fb9d95 Remove EOLed Symfony versions from Travis
Currently, the testsuite relies on SensioFrameworkExtraBundle 3.x which requires Symfony 2.3+. Given that 2.1 and 2.2 are EOLed, removing them from Travis is simpler than updating the testsuite to support them.
2014-09-10 14:29:24 +02:00
William Durand
4b163e1b80 Merge pull request #469 from bezhermoso/collections
Support collections on output (including named collections)
2014-09-06 11:33:45 +02:00
William Durand
f503b73dc4 Merge pull request #507 from jaugustin/fix-side-effect-list-expand-btn
fix side effect introduce with list/expand buttons
2014-09-06 11:32:20 +02:00
Jérémie Augustin
01fb243751 fix side effect introduce with list/expand buttons 2014-09-05 11:40:06 +02:00
Bez Hermoso
0d17c10b70 Collection handling fix. 2014-09-04 11:29:31 -07:00
Bez Hermoso
5fa69a0504 Tests for aliased collections; Swagger formatting for wrapped collections. 2014-09-04 11:19:54 -07:00
Bez Hermoso
c56aceaef5 Updated regex pattern to base on http://fr2.php.net/manual/en/language.oop5.basic.php 2014-09-04 10:47:58 -07:00
Bez Hermoso
4b7dbcd478 Improved directive parsing, and separate test class for parsing directives. 2014-09-04 10:47:57 -07:00
Bez Hermoso
928a23e2c8 Updated regex pattern matching and added tests for parsing array<..> directives. 2014-09-04 10:47:57 -07:00
Bez Hermoso
f5c1b06807 Support for collections. 2014-09-04 10:47:57 -07:00
Bez Hermoso
06cfe9d48b Allow parsers to remove/replace root parameters. 2014-09-04 10:47:57 -07:00
William Durand
8fe99a9c45 Merge pull request #504 from EmmanuelVella/patch-2
Replace incorrect jQuery.size() method
2014-09-04 10:12:53 +02:00
Emmanuel Vella
2d87ad0fe1 Replace incorrect jQuery.size() method 2014-09-03 15:49:16 +02:00
William Durand
ca0dd69752 Merge pull request #497 from pyrech/security-annotation-support
Added support for Security annotation
2014-09-02 08:55:54 +02:00
Ricard Clau
08153a3071 support for localstorage 2014-08-28 23:02:05 +01:00
Jonathan Chan
493e6066b3 Honor body format before uploading file type parameters 2014-08-28 01:48:10 -04:00
Loick Piera
16b104edec Added support for Security annotation 2014-08-28 00:12:34 +02:00
William Durand
72a1418b7d Merge pull request #456 from bezhermoso/swagger-doc
Updated the docs. Added sections on caching and Swagger support.
2014-08-27 08:59:42 +02:00
Bez Hermoso
4be579f7a2 Same-origin policy violation notes 2014-08-26 17:27:41 -07:00
Bez Hermoso
9b544ef535 Updated the docs. Added sections on caching and Swagger support. 2014-08-26 16:26:39 -07:00
William Durand
d6777b881b Merge pull request #493 from EmmanuelVella/query-parameters
Add query parameters in sandbox request URL
2014-08-26 15:39:14 +02:00
Emmanuel Vella
f9d713f9b4 Add query parameters in sandbox request URL 2014-08-26 12:44:11 +02:00
William Durand
0d45e7f186 Merge pull request #467 from bezhermoso/model_naming_strategy
Swagger: Alternate model naming strategy.
2014-08-26 09:05:18 +02:00
Bez Hermoso
480fcc5ecd Added clear() 2014-08-25 10:14:04 -07:00
Bez Hermoso
a5a13501e2 Alternate model naming strategy. 2014-08-25 10:14:04 -07:00
William Durand
fad6f576ee Merge pull request #455 from bezhermoso/swagger-dump-command
api:swagger:dump update
2014-08-25 12:00:30 +02:00
William DURAND
b2a996e047 Merge pull request #440 from thenetexperts/tags-with-colors 2014-08-25 11:28:31 +02:00
thenetexperts
e1c7e8a5bd adding optional color codes for tags annotation 2014-08-25 11:27:49 +02:00
William Durand
f8793d2439 Merge pull request #478 from spolischook/patch-1
Disallow PhpDocHandler rewrite already existing/parsed "requirements" doc point
2014-08-25 10:46:49 +02:00
William Durand
87d269fedb Merge pull request #491 from jaugustin/feat-section-hide-show
[DX] Sections enhancement #489
2014-08-25 10:45:25 +02:00
jaugustin
ae2c62fad1 add show/hide button and list/expand operations buttons on sections
add a new parameter default_sections_opened: false (default)
To start with sections opened or closed
2014-08-23 17:17:46 +02:00
William Durand
6952c4b32c Fix #486 2014-08-21 22:44:19 +02:00
William Durand
ce54848c31 Merge pull request #486 from JeroenDeDauw/patch-1
Only show request format dropdown when there are multiple formats
2014-08-21 10:40:20 +02:00
Jeroen De Dauw
83fc7d08bc Only show request format dropdown when there are multiple formats 2014-08-21 04:43:44 +02:00
William Durand
a1ec98376b Merge pull request #485 from bezhermoso/cache-param-handling-patch
Caching: Pass in parameters for late resolving
2014-08-20 22:09:17 +02:00
Bez Hermoso
313f0af195 Pass in parameter for late resolving. 2014-08-20 10:26:43 -07:00
William Durand
068d2f1a32 Merge pull request #484 from bezhermoso/fix_items_for_collection_parameters
[Fix] 'items' parameter for collections in parameters.
2014-08-19 22:08:24 +02:00
Bez Hermoso
d99c209f7d Fix: 'items' parameter for collections in parameters. 2014-08-19 12:32:06 -07:00
William Durand
fd0d78e0a8 Merge pull request #472 from sroze/recursive-groups
Added nested JMS groups exclusion
2014-08-18 17:28:42 +02:00
William Durand
57ef437d25 Merge pull request #482 from mathielen/patch-1
Impossible to access an attribute (\"custom_endpoint\") on a NULL variab...
2014-08-18 17:28:31 +02:00
Markus Thielen
b6f7179b59 Impossible to access an attribute (\"custom_endpoint\") on a NULL variable
My setup obviously does not have a authentication property set. This change fixes the error.
2014-08-18 11:55:01 +02:00
Samuel ROZE
3e3ef87b79 Added nested JMS groups exclusion 2014-08-18 11:32:36 +02:00
William Durand
d32381d18d Merge pull request #476 from bezhermoso/swagger-collection-parameter
[Fix] Missing handling for DataTypes::COLLECTION in parameters (input)
2014-08-16 14:29:11 +02:00
Sergey Polischook
3e6a47818e Update PhpDocHandler.php
https://github.com/nelmio/NelmioApiDocBundle/issues/477
2014-08-16 01:59:39 +03:00
Bez Hermoso
b289a6e846 Missing handling for DataTypes::COLLECTION in parameters (input) 2014-08-15 09:47:35 -07:00
William Durand
bc1d3f6f7f Merge pull request #468 from bezhermoso/param_type_options
Swagger: Ability to specify "paramType" for input
2014-08-14 21:44:53 +02:00
Bez Hermoso
18004189b3 Ability to specify param-type of input class. 2014-08-14 12:41:27 -07:00
William Durand
40c76339b6 Merge pull request #471 from yoshz/bugfix/undefined-endpoint
Fixed endpoint is undefined in sandbox when custom_endpoint is disabled
2014-08-14 21:31:58 +02:00
Yosh de Vos
d53028098f Fixed endpoint is undefined in sandbox when custom_endpoint is disabled 2014-08-08 17:55:36 +02:00
William Durand
38f0bec705 Merge pull request #466 from djlemmings/master
Fixed the wrong endpoint value check when no endpoint is set in config.
2014-08-08 17:46:19 +02:00
Bruce HELLER
5d43f463cf Fixed the wrong endpoint value check when no endpoint is set in config. 2014-08-07 16:45:46 +02:00
William Durand
eb08b7af27 Merge pull request #461 from bezhermoso/validation_parser_patch
Default to DataTypes::STRING when no constraints are found
2014-08-06 22:41:22 +02:00
William Durand
80d1c266f6 Merge pull request #462 from bezhermoso/swagger-auth-definitions
Added 'authorizations` definitions when sandbox authentication is configured
2014-08-06 03:45:37 +02:00
Bez Hermoso
b7f5fb58d4 Added definition when is provided. 2014-08-04 10:58:41 -07:00
Bez Hermoso
bcaaf28d61 Default to DataTypes::STRING 2014-08-04 10:02:18 -07:00
William Durand
e3103c073a Merge pull request #460 from lucasdealmeida/master
fix enpoint bug
2014-08-04 15:52:55 +02:00
Lucas Almeida
b4ca14618a fix enpoint bug 2014-08-04 09:58:02 -03:00
Bez Hermoso
0e01a00aaf Behavior and usage updates. 2014-08-01 12:56:46 -07:00
William Durand
4306a1a4a3 Merge pull request #457 from deegital/patch-1
No header deletion for forms with files
2014-08-01 15:47:52 +02:00
Jan Behrens
df7e97a941 No header deletion for forms with files
Fixes #453
2014-08-01 14:38:00 +02:00
Bez Hermoso
ea41c41c7c Swagger command updates. 2014-07-31 12:12:23 -07:00
William Durand
7a364db571 Merge pull request #444 from bezhermoso/caching
Added caching layer
2014-07-31 09:44:46 +02:00
Bez Hermoso
cc0d445601 Added caching layer with the controllers and routing files as resources. 2014-07-31 00:43:59 -07:00
William Durand
310a1f8cfd Merge pull request #438 from nikita2206/patch-1
Add abstract="true" to abstract_formatter
2014-07-30 11:25:40 +02:00
William Durand
75eb7ca356 Merge pull request #418 from bezhermoso/swagger
Swagger support
2014-07-30 11:23:46 +02:00
William DURAND
fb35704d5f Add poser badges 2014-07-30 11:17:22 +02:00
William DURAND
3fdb2d4a81 Prepare 2.7.0 release 2014-07-30 11:11:08 +02:00
William DURAND
9ad7e68703 update config reference 2014-07-30 11:10:39 +02:00
William DURAND
6c7c53e78d Fix CS & file permissions 2014-07-30 10:50:23 +02:00
Bez Hermoso
07c6557fc5 Test fixes. 2014-07-29 10:25:06 -07:00
Bez Hermoso
a8221d4515 Post-parser support for response map models. 2014-07-29 10:25:06 -07:00
Bez Hermoso
9824a6ba3c Added default value handling. 2014-07-29 10:25:06 -07:00
Bez Hermoso
abaeb374e8 Added 'type' to API item if applicable. 2014-07-29 10:25:06 -07:00
Bez Hermoso
9d3c0a8c29 Swagger support:
Unified data types [actualType and subType]
Updated tests.
JMS parsing fixes; updated {Validator,FormType}Parser, FOSRestHandler, and AbstractFormatter, and updated DataTypes enum.
Modified dataType checking.
Updated tests.
Updated DataTypes enum.
Quick fix and added doc comments.
CS fixes.
Refactored FormTypeParser to produce nested parameters. Updated tests accordingly.
Logical and CS fixes.
Sub-forms and more tests.
Logical and CS fixes.
Swagger support: created formatter.
Configuration and resourcePath logic update.
ApiDoc annotation update. Updated formatter and added tests.
Parameter formatting.
Added tests for SwaggerFormatter.
Added  option in annotation, and the corresponding logic for parsing the supplied values and processing them in the formatter.
Routing update.
Updated tests.
Removed unused dependency and updated doc comments.
Renamed 'responseModels' to 'responseMap'
Update the resource filtering and formatting of response messages.
Updated check for 200 response model.
Ignore data_class and always use form-type to avoid conflicts.
Fix: add 'type' even if '' is specified.
Refactored responseMap; added parsedResponseMap. Added tests and updated some.
Fix: add 'type' even if '' is specified.
Initial commit of command.
Finished logic for dumping files.
Updated doc comment; added license and added more meaningful class comment.
Array of models support.
2014-07-29 10:25:06 -07:00
Bez Hermoso
bb723bdb40 Added new tests for Swagger doc controllers. Also some CS fixes. 2014-07-29 10:25:06 -07:00
Bez Hermoso
ee0496af65 Update swagger-support.md 2014-07-29 10:25:05 -07:00
Bez Hermoso
af5cb3dd76 Removed ambiguity 2014-07-29 10:25:05 -07:00
Bez Hermoso
04818b00e5 Added configuration reference. 2014-07-29 10:25:05 -07:00
Bez Hermoso
cfe6cbc134 Create swagger-support.md 2014-07-29 10:25:05 -07:00
Bez Hermoso
6f85aed33c Swagger support:
Unified data types [actualType and subType]
Updated tests.
JMS parsing fixes; updated {Validator,FormType}Parser, FOSRestHandler, and AbstractFormatter, and updated DataTypes enum.
Modified dataType checking.
Updated tests.
Updated DataTypes enum.
Quick fix and added doc comments.
CS fixes.
Refactored FormTypeParser to produce nested parameters. Updated tests accordingly.
Logical and CS fixes.
Sub-forms and more tests.
Logical and CS fixes.
Swagger support: created formatter.
Configuration and resourcePath logic update.
ApiDoc annotation update. Updated formatter and added tests.
Parameter formatting.
Added tests for SwaggerFormatter.
Added  option in annotation, and the corresponding logic for parsing the supplied values and processing them in the formatter.
Routing update.
Updated tests.
Removed unused dependency and updated doc comments.
Renamed 'responseModels' to 'responseMap'
Update the resource filtering and formatting of response messages.
Updated check for 200 response model.
Ignore data_class and always use form-type to avoid conflicts.
Fix: add 'type' even if '' is specified.
Refactored responseMap; added parsedResponseMap. Added tests and updated some.
Fix: add 'type' even if '' is specified.
Initial commit of command.
Finished logic for dumping files.
Updated doc comment; added license and added more meaningful class comment.

Array of models support.
2014-07-29 10:25:05 -07:00
William Durand
6b18d88517 Merge pull request #446 from EmmanuelVella/bearer
Add Bearer authentication
2014-07-29 16:18:39 +02:00
William Durand
79e84c9867 Merge pull request #439 from thenetexperts/tags-fix
fixed css for tags output
2014-07-29 16:06:14 +02:00
William Durand
c30197f63f Merge pull request #445 from ogizanagi/patch-1
Fix embedded collection of custom FormType Error
2014-07-29 15:59:33 +02:00
William Durand
dcf8cdfe72 Merge pull request #447 from EmmanuelVella/toggler
Fix toggler click event
2014-07-29 15:56:48 +02:00
William Durand
f6df97b9d7 Merge pull request #449 from dbu/patch-1
Update branch-alias
2014-07-29 15:54:22 +02:00
David Buchmann
67a1b380ae Update composer.json 2014-07-28 11:08:21 +02:00
Emmanuel Vella
6bc971c50a Refactored authentication config 2014-07-25 13:40:26 +02:00
Emmanuel Vella
abaf38adc3 Fix toggler click event 2014-07-23 13:21:33 +02:00
ogizanagi
4939d116e0 Fix embedded collection of custom FormType Error
Fix an error when trying to use embedded form collections.

Referenced issue: #442
2014-07-21 22:27:09 +02:00
thenetexperts
492bb230a8 fixed css for tags output 2014-07-18 11:12:58 +02:00
Nikita Nefedov
afb8536b41 Add abstract="true" to abstract_formatter
Add abstract="true" to abstract_formatter in container definition
2014-07-16 12:20:49 +04:00
William Durand
c03d35bee4 Merge pull request #434 from ahilles107/patch-2
Don't parse custom properties as classes.
2014-07-14 21:24:21 +02:00
Paweł Mikołajczuk
55f26508ab Don't parse custom properties as classes.
Sometimes instead real class name we can use custom handler name. Then this class can't be initialized and we will get this exception ```ReflectionException: Class custom_handler_name does not exist```
2014-07-10 11:46:30 +02:00
William Durand
b9b453c857 Merge pull request #432 from Prezent/jms-inline
Parse JSM\Inline, fixes #372
2014-07-09 10:29:17 +02:00
Sander Marechal
b66e5c4449 Parse JSM\Inline, fixes #372 2014-07-08 13:20:13 +02:00
William Durand
ae877d74a0 Merge pull request #390 from frastel/params-fix
Added fix for doubled parameters
2014-07-07 11:38:47 +02:00
Frank Stelzer
54e5fae3de added fix for doubled parameters 2014-07-07 09:08:21 +00:00
William Durand
94243f0a3e Merge pull request #428 from gnat42/patch-1
If a description is not provided use form label
2014-07-04 09:38:41 +02:00
gnat42
48e7bd2616 If a description is not provided use form label
It would be nice if there was no description the form label was used instead. In the future I think it would be even better to have the label as an header, and the description as 'additional' instructions.
2014-07-03 13:34:07 -06:00
William Durand
54819590f2 Merge pull request #425 from dmishh/patch-1
Added more clearance to docs
2014-07-02 14:37:52 +02:00
Dmitriy
1c05399261 Added more clearance to docs
Specifying form inputs' names prefix in the Form Types Features documentation section
2014-07-01 12:29:58 +03:00
William DURAND
c92789e00f remove php 5.3 from travis-ci 2014-06-27 14:43:22 +02:00
William DURAND
0151624773 update config reference 2014-06-27 14:13:36 +02:00
William DURAND
94ec568237 Merge pull request #387 from giosh94mhz/form_type_parser_should_use_type_constructor 2014-06-27 10:59:06 +02:00
Giorgio Premi
e2c2d00075 FormTypeParser: FormType constructor should be called when possible 2014-06-27 10:58:53 +02:00
Simon Schick
4fa50ebe53 Added workaround for Firefox not sending LINK in uppercase what then is denied by nginx.
More info: http://stackoverflow.com/questions/9645037/nginx-rejects-custom-http-methods-if-not-all-upper-case
2014-06-27 10:32:32 +02:00
William Durand
c6741d5710 Merge pull request #369 from emmanuelballery/fixes_for_ff36
Fixes HTML structures for FF36 (and maybe other old browsers)
2014-06-27 10:24:27 +02:00
William DURAND
0030ce6825 Merge pull request #358 from pborreli/typos 2014-06-27 10:22:56 +02:00
Pascal Borreli
dbc3fcbb73 Fixed typos 2014-06-27 10:22:36 +02:00
Bez Hermoso
882f658599 Added 'default' parameters in {JmsMetadata,Validator}Parser, and FOSRestHandler. 2014-06-27 10:19:28 +02:00
William Durand
0d1bde9f8a Merge pull request #352 from sroze/output-post-parser
Add PostParserInterface to JmsMetadataParser to get ValidatorParser found children parsed
2014-06-27 10:14:51 +02:00
William DURAND
a817081ab2 Add missing array key checks
Fixes #423
2014-06-27 10:07:03 +02:00
William Durand
03f6142a84 Merge pull request #422 from nelmio/bezhermoso-expand_form_type_parser
Unified data types [actualType and subType]
2014-06-27 09:54:14 +02:00
Bez Hermoso
3a31c93c94 Unified data types [actualType and subType]
Updated tests.

JMS parsing fixes; updated {Validator,FormType}Parser, FOSRestHandler, and AbstractFormatter, and updated DataTypes enum.

Modified dataType checking.

Updated tests.

Updated DataTypes enum.

Quick fix and added doc comments.

CS fixes.

Refactored FormTypeParser to produce nested parameters. Updated tests accordingly.

Logical and CS fixes.

Sub-forms and more tests.

Ignore data_class and always use form-type to avoid conflicts.

Quick fix.
2014-06-27 09:35:18 +02:00
William Durand
a2a4782af5 Merge pull request #408 from jonmchan/ParamDefaults
Add default values support for form types
2014-06-26 17:54:22 +02:00
Jonathan Chan
d4e12d66f2 Add default values support for form types
Adding displaying default value in Markdown

updating tests to work with new default value
2014-06-26 11:20:49 -04:00
William Durand
df7387aec9 Merge pull request #419 from jonmchan/addParameterBugFix
Fixed small bug introduced when adding 'new parameter'
2014-06-26 00:04:36 +02:00
Jonathan Chan
ba5e2d1454 Fixed small bug introduced when adding 'new parameter' 2014-06-25 13:29:59 -04:00
William Durand
ea2201762d Merge pull request #345 from AlexeyKupershtokh/web-profiler
Symfony web profiler integration
2014-06-25 10:56:43 +02:00
William DURAND
39dd41e285 Merge pull request #388 from yoshz/feature/body_format
Added configuration to disable body formats
2014-06-25 10:30:56 +02:00
William DURAND
8ddade0e30 Merge pull request #407 from jonmchan/ParameterTypeSupport 2014-06-25 10:25:59 +02:00
Jonathan Chan
1cd77e2f14 added support to properly handle file upload POST 2014-06-25 10:25:45 +02:00
Jonathan Chan
210596eae9 adding support for different parameter types 2014-06-25 10:25:45 +02:00
Jonathan Chan
b124824a8d added file type to FormType Parser 2014-06-25 10:25:45 +02:00
William DURAND
96e5d15f1b Merge pull request #412 from grEvenX/nullable_request_support 2014-06-25 10:20:00 +02:00
Even André Fiskvik
b4e874e2dc Add support for nullable option in RequestParam 2014-06-25 10:19:30 +02:00
William DURAND
607d031051 Merge pull request #413 from bezhermoso/unified_data_types 2014-06-25 09:06:32 +02:00
Bez Hermoso
14d1021c8b 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-25 09:05:48 +02:00
William DURAND
b48650a9e0 Fix CS 2014-06-25 08:52:01 +02:00
William Durand
87b690d5e1 Merge pull request #409 from stof/patch-2
Simplified the Travis configuration
2014-06-25 08:51:55 +02:00
Christophe Coevoet
8d3fd662bf Fixed the retrieval of the validation MetadataFactory
The service is private so getting it from the container get() method is invalid and it does not work anymore in Symfony 2.5 because the service gets inlined.
2014-06-18 09:38:16 +02:00
Christophe Coevoet
df1c85ae5e Simplified the Travis configuration
The tests against specific Symfony versions are now running only for a single PHP version to limit the number of jobs in the build matrix. They are also installing the full Symfony repo to be sure that all components are actually at the specified version without the need to require each of them explicitly.
2014-06-17 19:01:48 +02:00
William Durand
56124e7c40 Merge pull request #395 from fechu/master
Implement tags in ApiDoc annotation.
2014-06-01 15:56:45 +02:00
Sandro Meier
106b42530c Add documentation for tag property 2014-05-27 19:33:52 +02:00
Sandro Meier
dfd094371d Implement Tags for functions. 2014-05-27 13:33:50 +02:00
William Durand
dfb089f993 Merge pull request #392 from marco-jantke/submit-on-enter-sandbox-mode
Pressing enter in the sandbox mode will now lead to submit the form.
2014-05-26 10:51:28 +02:00
Marco Jantke
d7c70720b0 Pressing enter in the sandbox mode will now lead to submit the form. 2014-05-23 15:30:21 +02:00
Yosh de Vos
624802b57a Added configuration to disable body formats 2014-05-21 15:59:55 +02:00
William Durand
221f109ad6 Merge pull request #386 from marco-jantke/window-location-hash-fix
Remove hash when closing a method container again. Added small hash help...
2014-05-19 12:03:46 +02:00
William Durand
3771e2d834 Merge pull request #385 from yoshz/master
Added request formats configuration
2014-05-19 10:45:49 +02:00
Yosh de Vos
8402c748ee Added request formats configuration 2014-05-18 21:25:30 +02:00
Marco Jantke
df7be182cd Remove hash when closing a method container again. Added small hash helper functions. 2014-05-18 01:24:52 +02:00
William Durand
968a162544 Merge pull request #365 from pyrech/master
Fix #357 - doc broken with Validator Constraints in FOSRestBundle requirements
2014-05-16 23:52:41 +02:00
Loick Piera
d0149c65ab fix #357 2014-05-16 22:04:24 +02:00
Emmanuel BALLERY
0a42d1773c Fix wrong HTML structure a>div 2014-04-24 23:02:56 +02:00
Emmanuel BALLERY
7150ac17de Remove duplicate HTML ID 2014-04-24 23:00:13 +02:00
Emmanuel BALLERY
f2b606fc23 Fix wrong HTML structure ul>li 2014-04-24 22:59:12 +02:00
Emmanuel BALLERY
c78466bbb9 Fix wrong HTML tag 2014-04-24 22:58:34 +02:00
Samuel ROZE
3f66888f00 Add PostParserInterface to JmsMetadataParser to get ValidatorParser found children parsed. 2014-03-20 16:39:36 +01:00
Alexey Kupershtokh
11f163d589 Show web profiler link in sandbox 2014-03-11 07:07:35 +07:00
127 changed files with 12234 additions and 4688 deletions

47
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: CI
on:
pull_request:
push:
branches:
- "*.*"
- master
jobs:
tests:
name: PHPUnit PHP ${{ matrix.php-version }} ${{ matrix.dependency }} (Symfony ${{ matrix.symfony-version }})
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
php-version:
- '8.1'
- '8.2'
- '8.3'
symfony-version:
- '5.4.*'
- '6.4.*'
coverage: [ 'none' ]
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
- name: Configure Symfony
run: composer config extra.symfony.require "${{ matrix.symfony-version }}"
- name: Update project dependencies
run: composer update --no-progress --ansi --prefer-stable
- name: Validate composer
run: composer validate --strict --no-check-lock
- name: "Run code-style check"
run: vendor/bin/php-cs-fixer fix --dry-run --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none -v
- name: Run tests
run: vendor/bin/phpunit

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
vendor/
composer.lock
phpunit.xml
.idea
.phpunit.result.cache

11
.php-cs-fixer.dist.php Normal file
View file

@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
;
return Retailcrm\PhpCsFixer\Defaults::rules()
->setFinder($finder)
->setCacheFile(__DIR__ . '/.php_cs.cache');

View file

@ -1,34 +0,0 @@
language: php
php:
- 5.3
- 5.4
- 5.5
- 5.6
- hhvm
env:
- SYMFONY_VERSION=2.1.*
- SYMFONY_VERSION=2.2.*
- SYMFONY_VERSION=2.3.*
- SYMFONY_VERSION=2.4.*
- SYMFONY_VERSION=dev-master
matrix:
allow_failures:
- env: SYMFONY_VERSION=dev-master
before_script:
- composer self-update
- composer require symfony/twig-bundle:${SYMFONY_VERSION} --no-update
- composer require symfony/twig-bridge:${SYMFONY_VERSION} --no-update
- composer require symfony/framework-bundle:${SYMFONY_VERSION} --no-update
- composer require symfony/validator:${SYMFONY_VERSION} --dev --no-update
- composer require symfony/console:${SYMFONY_VERSION} --no-update
- composer require symfony/css-selector:${SYMFONY_VERSION} --dev --no-update
- composer require symfony/browser-kit:${SYMFONY_VERSION} --dev --no-update
- composer require symfony/yaml:${SYMFONY_VERSION} --dev --no-update
- composer require symfony/form:${SYMFONY_VERSION} --dev --no-update
- composer update
script: phpunit --coverage-text

View file

@ -1,581 +0,0 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Annotation;
use Symfony\Component\Routing\Route;
/**
* @Annotation
*/
class ApiDoc
{
/**
* Requirements are mandatory parameters in a route.
*
* @var array
*/
private $requirements = array();
/**
* Filters are optional parameters in the query string.
*
* @var array
*/
private $filters = array();
/**
* Parameters are data a client can send.
*
* @var array
*/
private $parameters = array();
/**
* @var string
*/
private $input = null;
/**
* @var string
*/
private $output = null;
/**
* @var string
*/
private $link = null;
/**
* Most of the time, a single line of text describing the action.
*
* @var string
*/
private $description = null;
/**
* Section to group actions together.
*
* @var string
*/
private $section = null;
/**
* Extended documentation.
*
* @var string
*/
private $documentation = null;
/**
* @var Boolean
*/
private $resource = false;
/**
* @var string
*/
private $method;
/**
* @var string
*/
private $host;
/**
* @var string
*/
private $uri;
/**
* @var array
*/
private $response = array();
/**
* @var Route
*/
private $route;
/**
* @var boolean
*/
private $https = false;
/**
* @var boolean
*/
private $authentication = false;
/**
* @var array
*/
private $authenticationRoles = array();
/**
* @var int
*/
private $cache;
/**
* @var boolean
*/
private $deprecated = false;
/**
* @var array
*/
private $statusCodes = array();
public function __construct(array $data)
{
$this->resource = !empty($data['resource']) ? $data['resource'] : false;
if (isset($data['description'])) {
$this->description = $data['description'];
}
if (isset($data['input'])) {
$this->input = $data['input'];
} elseif (isset($data['filters'])) {
foreach ($data['filters'] as $filter) {
if (!isset($filter['name'])) {
throw new \InvalidArgumentException('A "filter" element has to contain a "name" attribute');
}
$name = $filter['name'];
unset($filter['name']);
$this->addFilter($name, $filter);
}
}
if (isset($data['requirements'])) {
foreach ($data['requirements'] as $requirement) {
if (!isset($requirement['name'])) {
throw new \InvalidArgumentException('A "requirement" element has to contain a "name" attribute');
}
$name = $requirement['name'];
unset($requirement['name']);
$this->addRequirement($name, $requirement);
}
}
if (isset($data['parameters'])) {
foreach ($data['parameters'] as $parameter) {
if (!isset($parameter['name'])) {
throw new \InvalidArgumentException('A "parameter" element has to contain a "name" attribute');
}
if (!isset($parameter['dataType'])) {
throw new \InvalidArgumentException(sprintf(
'"%s" parameter element has to contain a "dataType" attribute',
$parameter['name']
));
}
$name = $parameter['name'];
unset($parameter['name']);
$this->addParameter($name, $parameter);
}
}
if (isset($data['output'])) {
$this->output = $data['output'];
}
if (isset($data['statusCodes'])) {
foreach ($data['statusCodes'] as $statusCode => $description) {
$this->addStatusCode($statusCode, $description);
}
}
if (isset($data['authentication'])) {
$this->setAuthentication((bool) $data['authentication']);
}
if (isset($data['authenticationRoles'])) {
foreach ($data['authenticationRoles'] as $key => $role) {
$this->authenticationRoles[] = $role;
}
}
if (isset($data['cache'])) {
$this->setCache($data['cache']);
}
if (isset($data['section'])) {
$this->section = $data['section'];
}
if (isset($data['deprecated'])) {
$this->deprecated = $data['deprecated'];
}
if (isset($data['https'])) {
$this->https = $data['https'];
}
}
/**
* @param string $name
* @param array $filter
*/
public function addFilter($name, array $filter)
{
$this->filters[$name] = $filter;
}
/**
* @param string $statusCode
* @param mixed $description
*/
public function addStatusCode($statusCode, $description)
{
$this->statusCodes[$statusCode] = !is_array($description) ? array($description) : $description;
}
/**
* @param string $name
* @param array $requirement
*/
public function addRequirement($name, array $requirement)
{
$this->requirements[$name] = $requirement;
}
/**
* @param array $requirements
*/
public function setRequirements(array $requirements)
{
$this->requirements = array_merge($this->requirements, $requirements);
}
/**
* @return string|null
*/
public function getInput()
{
return $this->input;
}
/**
* @return string|null
*/
public function getOutput()
{
return $this->output;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $description
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* @param string $link
*/
public function setLink($link)
{
$this->link = $link;
}
/**
* @param string $section
*/
public function setSection($section)
{
$this->section = $section;
}
/**
* @return string
*/
public function getSection()
{
return $this->section;
}
/**
* @param string $documentation
*/
public function setDocumentation($documentation)
{
$this->documentation = $documentation;
}
/**
* @return string
*/
public function getDocumentation()
{
return $this->documentation;
}
/**
* @return Boolean
*/
public function isResource()
{
return (bool) $this->resource;
}
/**
* @return mixed
*/
public function getResource()
{
return $this->resource && is_string($this->resource) ? $this->resource : false;
}
/**
* @param string $name
* @param array $parameter
*/
public function addParameter($name, array $parameter)
{
$this->parameters[$name] = $parameter;
}
/**
* @param array $parameters
*/
public function setParameters(array $parameters)
{
$this->parameters = $parameters;
}
/**
* Sets the responsed data as processed by the parsers - same format as parameters
*
* @param array $response
*/
public function setResponse(array $response)
{
$this->response = $response;
}
/**
* @param Route $route
*/
public function setRoute(Route $route)
{
$this->route = $route;
if (method_exists($route, 'getHost')) {
$this->host = $route->getHost() ? : null;
} else {
$this->host = null;
}
$this->uri = $route->getPattern();
$this->method = $route->getRequirement('_method') ?: 'ANY';
}
/**
* @return Route
*/
public function getRoute()
{
return $this->route;
}
/**
* @return string
*/
public function getHost()
{
return $this->host;
}
/**
* @param string $host
*/
public function setHost($host)
{
$this->host = $host;
}
/**
* @return boolean
*/
public function getHttps()
{
return $this->https;
}
/**
* @param boolean $https
*/
public function setHttps($https)
{
$this->https = $https;
}
/**
* @return boolean
*/
public function getAuthentication()
{
return $this->authentication;
}
/**
* @param boolean $authentication
*/
public function setAuthentication($authentication)
{
$this->authentication = $authentication;
}
/**
* @return array
*/
public function getAuthenticationRoles()
{
return $this->authenticationRoles;
}
/**
* @param array $authenticationRoles
*/
public function setAuthenticationRoles($authenticationRoles)
{
$this->authenticationRoles = $authenticationRoles;
}
/**
* @return int
*/
public function getCache()
{
return $this->cache;
}
/**
* @param int $cache
*/
public function setCache($cache)
{
$this->cache = (int) $cache;
}
/**
* @return boolean
*/
public function getDeprecated()
{
return $this->deprecated;
}
/**
* @return array
*/
public function getFilters()
{
return $this->filters;
}
/**
* @return array
*/
public function getRequirements()
{
return $this->requirements;
}
/**
* @param boolean $deprecated
*/
public function setDeprecated($deprecated)
{
$this->deprecated = (bool) $deprecated;
return $this;
}
/**
* @return array
*/
public function toArray()
{
$data = array(
'method' => $this->method,
'uri' => $this->uri,
);
if ($host = $this->host) {
$data['host'] = $host;
}
if ($description = $this->description) {
$data['description'] = $description;
}
if ($link = $this->link) {
$data['link'] = $link;
}
if ($documentation = $this->documentation) {
$data['documentation'] = $documentation;
}
if ($filters = $this->filters) {
$data['filters'] = $filters;
}
if ($parameters = $this->parameters) {
$data['parameters'] = $parameters;
}
if ($requirements = $this->requirements) {
$data['requirements'] = $requirements;
}
if ($response = $this->response) {
$data['response'] = $response;
}
if ($statusCodes = $this->statusCodes) {
$data['statusCodes'] = $statusCodes;
}
if ($section = $this->section) {
$data['section'] = $section;
}
if ($cache = $this->cache) {
$data['cache'] = $cache;
}
$data['https'] = $this->https;
$data['authentication'] = $this->authentication;
$data['authenticationRoles'] = $this->authenticationRoles;
$data['deprecated'] = $this->deprecated;
return $data;
}
}

500
Attribute/ApiDoc.php Normal file
View file

@ -0,0 +1,500 @@
<?php
namespace Nelmio\ApiDocBundle\Attribute;
use Symfony\Component\Routing\Route;
#[\Attribute(\Attribute::TARGET_METHOD)]
class ApiDoc
{
public const DEFAULT_VIEW = 'default';
/**
* Requirements are mandatory parameters in a route.
*
* @var array<string, array<string, string>>
*/
private array $requirements = [];
/**
* Which views is this route used. Defaults to "Default"
*
* @var string[]
*/
private array $views = [];
/**
* Filters are optional parameters in the query string.
*
* @var array<string, array<string, string>>
*/
private array $filters = [];
/**
* Parameters are data a client can send.
*
* @var array<string, array<string, mixed>>
*/
private array $parameters = [];
/**
* Headers that client can send.
*
* @var array<string, array<string, mixed>>
*/
private array $headers = [];
private ?string $link = null;
/**
* Extended documentation.
*/
private ?string $documentation = null;
private Route $route;
private ?string $host = null;
private string $method;
private string $uri;
private array $response = [];
/**
* @var array<int|string, string[]>
*/
private array $statusCodes = [];
/**
* @var array<int, array<mixed>>
*/
private array $responseMap = [];
private array $parsedResponseMap = [];
/**
* @var array<string|int, string>
*/
private array $tags = [];
private ?string $scope = null;
/**
* @param string[]|string|null $description
*/
public function __construct(
private string|bool $resource = false,
private array|string|null $description = null,
private string|array|null $input = null,
private ?array $inputs = null,
private string|array|null $output = null,
private ?string $section = null,
private bool $deprecated = false,
private ?string $resourceDescription = null,
?array $filters = null,
?array $requirements = null,
array|string|null $views = null,
?array $parameters = null,
?array $headers = null,
?array $statusCodes = null,
array|string|int|null $tags = null,
?array $responseMap = null,
) {
if (null !== $filters) {
foreach ($filters as $filter) {
if (!isset($filter['name'])) {
throw new \InvalidArgumentException('A "filter" element has to contain a "name" attribute');
}
$name = $filter['name'];
unset($filter['name']);
$this->addFilter($name, $filter);
}
}
if (null !== $requirements) {
foreach ($requirements as $requirement) {
if (!isset($requirement['name'])) {
throw new \InvalidArgumentException('A "requirement" element has to contain a "name" attribute');
}
$name = $requirement['name'];
unset($requirement['name']);
$this->addRequirement($name, $requirement);
}
}
if (null !== $views) {
if (!is_array($views)) {
$views = [$views];
}
foreach ($views as $view) {
$this->addView($view);
}
}
if (null !== $parameters) {
foreach ($parameters as $parameter) {
if (!isset($parameter['name'])) {
throw new \InvalidArgumentException('A "parameter" element has to contain a "name" attribute');
}
if (!isset($parameter['dataType'])) {
throw new \InvalidArgumentException(sprintf(
'"%s" parameter element has to contain a "dataType" attribute',
$parameter['name']
));
}
$name = $parameter['name'];
unset($parameter['name']);
$this->addParameter($name, $parameter);
}
}
if (null !== $headers) {
foreach ($headers as $header) {
if (!isset($header['name'])) {
throw new \InvalidArgumentException('A "header" element has to contain a "name" attribute');
}
$name = $header['name'];
unset($header['name']);
$this->addHeader($name, $header);
}
}
if (null !== $statusCodes) {
foreach ($statusCodes as $statusCode => $statusDescription) {
$this->addStatusCode($statusCode, $statusDescription);
}
}
if (null !== $tags) {
if (is_array($tags)) {
foreach ($tags as $tag => $colorCode) {
if (is_numeric($tag)) {
$this->addTag($colorCode);
} else {
$this->addTag($tag, $colorCode);
}
}
} else {
$this->tags[] = $tags;
}
}
if (null !== $responseMap) {
$this->responseMap = $responseMap;
if (isset($this->responseMap[200])) {
$this->output = $this->responseMap[200];
}
}
}
public function addFilter(string $name, array $filter): void
{
$this->filters[$name] = $filter;
}
public function addStatusCode(int|string $statusCode, string|array $description): void
{
$this->statusCodes[$statusCode] = !is_array($description) ? [$description] : $description;
}
public function addTag(int|string $tag, string $colorCode = '#d9534f'): void
{
$this->tags[$tag] = $colorCode;
}
public function addRequirement(string $name, array $requirement): void
{
$this->requirements[$name] = $requirement;
}
public function setRequirements(array $requirements): void
{
$this->requirements = array_merge($this->requirements, $requirements);
}
public function getInput(): string|array|null
{
return $this->input;
}
public function getInputs(): ?array
{
return $this->inputs;
}
public function getOutput(): array|string|null
{
return $this->output;
}
/**
* @return string[]|string|null
*/
public function getDescription(): array|string|null
{
return $this->description;
}
/**
* @param string[]|string|null $description
*/
public function setDescription(array|string|null $description): void
{
$this->description = $description;
}
public function setLink(?string $link): void
{
$this->link = $link;
}
public function getSection(): ?string
{
return $this->section;
}
public function addView(string $view): void
{
$this->views[] = $view;
}
/**
* @return string[]
*/
public function getViews(): array
{
return $this->views;
}
public function setDocumentation(?string $documentation): void
{
$this->documentation = $documentation;
}
public function getDocumentation(): ?string
{
return $this->documentation;
}
public function isResource(): bool
{
return (bool) $this->resource;
}
public function getResource(): string|bool
{
return $this->resource && is_string($this->resource) ? $this->resource : false;
}
public function addParameter(string $name, array $parameter): void
{
$this->parameters[$name] = $parameter;
}
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
public function addHeader($name, array $header): void
{
$this->headers[$name] = $header;
}
/**
* Sets the response data as processed by the parsers - same format as parameters
*/
public function setResponse(array $response): void
{
$this->response = $response;
}
public function setRoute(Route $route): void
{
$this->route = $route;
if (method_exists($route, 'getHost')) {
$this->host = $route->getHost() ?: null;
// replace route placeholders
foreach ($route->getDefaults() as $key => $value) {
if (null !== $this->host && is_string($value)) {
$this->host = str_replace('{' . $key . '}', $value, $this->host);
}
}
} else {
$this->host = null;
}
$this->uri = $route->getPath();
$this->method = $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY';
}
public function getRoute(): Route
{
return $this->route;
}
public function getHost(): ?string
{
return $this->host;
}
public function getDeprecated(): bool
{
return $this->deprecated;
}
/**
* @return array<string, array<string, string>>
*/
public function getFilters(): array
{
return $this->filters;
}
public function getRequirements(): array
{
return $this->requirements;
}
public function getParameters(): array
{
return $this->parameters;
}
public function getHeaders(): array
{
return $this->headers;
}
public function setDeprecated(bool $deprecated): void
{
$this->deprecated = $deprecated;
}
public function getMethod(): string
{
return $this->method;
}
public function setScope(string $scope): void
{
$this->scope = $scope;
}
public function getScope(): ?string
{
return $this->scope;
}
/**
* @return array
*/
public function toArray()
{
$data = [
'method' => $this->method ?? null,
'uri' => $this->uri ?? null,
];
if ($host = $this->host) {
$data['host'] = $host;
}
if ($description = $this->description) {
$data['description'] = $description;
}
if ($link = $this->link) {
$data['link'] = $link;
}
if ($documentation = $this->documentation) {
$data['documentation'] = $documentation;
}
if ($filters = $this->filters) {
$data['filters'] = $filters;
}
if ($parameters = $this->parameters) {
$data['parameters'] = $parameters;
}
if ($headers = $this->headers) {
$data['headers'] = $headers;
}
if ($requirements = $this->requirements) {
$data['requirements'] = $requirements;
}
if ($views = $this->views) {
$data['views'] = $views;
}
if ($response = $this->response) {
$data['response'] = $response;
}
if ($parsedResponseMap = $this->parsedResponseMap) {
$data['parsedResponseMap'] = $parsedResponseMap;
}
if ($statusCodes = $this->statusCodes) {
$data['statusCodes'] = $statusCodes;
}
if ($section = $this->section) {
$data['section'] = $section;
}
if ($tags = $this->tags) {
$data['tags'] = $tags;
}
if ($resourceDescription = $this->resourceDescription) {
$data['resourceDescription'] = $resourceDescription;
}
$data['deprecated'] = $this->deprecated;
$data['scope'] = $this->scope;
return $data;
}
public function getResourceDescription(): ?string
{
return $this->resourceDescription;
}
public function getResponseMap(): array
{
if (!isset($this->responseMap[200]) && null !== $this->output) {
$this->responseMap[200] = $this->output;
}
return $this->responseMap;
}
public function getParsedResponseMap(): array
{
return $this->parsedResponseMap;
}
public function setResponseForStatusCode(array $model, array $type, int $statusCode = 200): void
{
$this->parsedResponseMap[$statusCode] = ['type' => $type, 'model' => $model];
if (200 === $statusCode && $this->response !== $model) {
$this->response = $model;
}
}
}

View file

@ -11,64 +11,91 @@
namespace Nelmio\ApiDocBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\HtmlFormatter;
use Nelmio\ApiDocBundle\Formatter\MarkdownFormatter;
use Nelmio\ApiDocBundle\Formatter\SimpleFormatter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class DumpCommand extends ContainerAwareCommand
#[AsCommand(
name: 'api:doc:dump',
description: 'Dumps API documentation in various formats',
)]
class DumpCommand extends Command
{
/**
* @var array
*/
protected $availableFormats = array('markdown', 'json', 'html');
private const AVAILABLE_FORMATS = ['markdown', 'json', 'html'];
protected function configure()
{
$this
->setDescription('')
->addOption(
'format', '', InputOption::VALUE_REQUIRED,
'Output format like: ' . implode(', ', $this->availableFormats),
$this->availableFormats[0]
)
->addOption('no-sandbox', '', InputOption::VALUE_NONE)
->setName('api:doc:dump')
;
/**
* @param TranslatorInterface&LocaleAwareInterface $translator
*/
public function __construct(
private readonly SimpleFormatter $simpleFormatter,
private readonly MarkdownFormatter $markdownFormatter,
private readonly HtmlFormatter $htmlFormatter,
private readonly ApiDocExtractor $apiDocExtractor,
private readonly TranslatorInterface $translator,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output)
protected function configure(): void
{
$this
->addOption(
'format', '', InputOption::VALUE_REQUIRED,
'Output format like: ' . implode(', ', self::AVAILABLE_FORMATS),
self::AVAILABLE_FORMATS[0]
)
->addOption('api-version', null, InputOption::VALUE_REQUIRED, 'The API version')
->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale for translation')
->addOption('view', '', InputOption::VALUE_OPTIONAL, '', ApiDoc::DEFAULT_VIEW)
->addOption('no-sandbox', '', InputOption::VALUE_NONE)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$format = $input->getOption('format');
$routeCollection = $this->getContainer()->get('router')->getRouteCollection();
$view = $input->getOption('view');
if (!$input->hasOption('format') || in_array($format, array('json'))) {
$formatter = $this->getContainer()->get('nelmio_api_doc.formatter.simple_formatter');
} else {
if (!in_array($format, $this->availableFormats)) {
throw new \RuntimeException(sprintf('Format "%s" not supported.', $format));
}
$formatter = match ($format) {
'json' => $this->simpleFormatter,
'markdown' => $this->markdownFormatter,
'html' => $this->htmlFormatter,
default => throw new \RuntimeException(sprintf('Format "%s" not supported.', $format)),
};
$formatter = $this->getContainer()->get(sprintf('nelmio_api_doc.formatter.%s_formatter', $format));
if ($input->hasOption('locale')) {
$this->translator->setLocale($input->getOption('locale') ?? '');
}
if ($input->getOption('no-sandbox') && 'html' === $format) {
if ($input->hasOption('api-version')) {
$formatter->setVersion($input->getOption('api-version'));
}
if ($formatter instanceof HtmlFormatter && $input->getOption('no-sandbox')) {
$formatter->setEnableSandbox(false);
}
if ('html' === $format) {
$this->getContainer()->enterScope('request');
$this->getContainer()->set('request', new Request(), 'request');
}
$extractedDoc = $input->hasOption('api-version') ?
$this->apiDocExtractor->allForVersion($input->getOption('api-version'), $view) :
$this->apiDocExtractor->all($view);
$extractedDoc = $this->getContainer()->get('nelmio_api_doc.extractor.api_doc_extractor')->all();
$formattedDoc = $formatter->format($extractedDoc);
if ('json' === $format) {
$output->writeln(json_encode($formattedDoc));
$output->writeln(json_encode($formattedDoc, JSON_THROW_ON_ERROR));
} else {
$output->writeln($formattedDoc);
$output->writeln($formattedDoc, OutputInterface::OUTPUT_RAW);
}
return 0;
}
}

View file

@ -0,0 +1,164 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Command;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\SwaggerFormatter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
/**
* Console command to dump Swagger-compliant API definitions.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
#[AsCommand(
name: 'api:swagger:dump',
description: 'Dumps Swagger-compliant API definitions.',
)]
class SwaggerDumpCommand extends Command
{
private Filesystem $filesystem;
public function __construct(
private readonly ApiDocExtractor $extractor,
private readonly SwaggerFormatter $formatter,
) {
parent::__construct();
}
protected function configure(): void
{
$this->filesystem = new Filesystem();
$this
->addOption('resource', 'r', InputOption::VALUE_OPTIONAL, 'A specific resource API declaration to dump.')
->addOption('list-only', 'l', InputOption::VALUE_NONE, 'Dump resource list only.')
->addOption('pretty', 'p', InputOption::VALUE_NONE, 'Dump as prettified JSON.')
->addArgument('destination', InputArgument::OPTIONAL, 'Directory to dump JSON files in.', null)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('list-only') && $input->getOption('resource')) {
throw new \RuntimeException('Cannot selectively dump a resource with the --list-only flag.');
}
$apiDocs = $this->extractor->all();
if ($input->getOption('list-only')) {
$data = $this->getResourceList($apiDocs);
$this->dump($data, null, $input, $output);
return 0;
}
if (false !== ($resource = $input->getOption('resource'))) {
$data = $this->getApiDeclaration($apiDocs, $resource);
if (0 === count($data['apis'])) {
throw new \InvalidArgumentException(sprintf('Resource "%s" does not exist.', $resource));
}
$this->dump($data, $resource, $input, $output);
return 0;
}
/*
* If --list-only and --resource is not specified, dump everything.
*/
$data = $this->getResourceList($apiDocs);
if (!$input->getArguments('destination')) {
$output->writeln('');
$output->writeln('<comment>Resource list: </comment>');
}
$this->dump($data, null, $input, $output, false);
foreach ($data['apis'] as $api) {
$resource = substr($api['path'], 1);
if (!$input->getArgument('destination')) {
$output->writeln('');
$output->writeln(sprintf('<comment>API declaration for <info>"%s"</info> resource: </comment>', $resource));
}
$data = $this->getApiDeclaration($apiDocs, $resource, $output);
$this->dump($data, $resource, $input, $output, false);
}
return 0;
}
protected function dump(array $data, $resource, InputInterface $input, OutputInterface $output, $treatAsFile = true): void
{
$destination = $input->getArgument('destination');
$content = json_encode($data, $input->getOption('pretty') ? JSON_PRETTY_PRINT : 0);
if (!$destination) {
$output->writeln($content);
return;
}
if (false === $treatAsFile) {
if (!$this->filesystem->exists($destination)) {
$this->filesystem->mkdir($destination);
}
}
if (!$resource) {
if (!$treatAsFile) {
$destination = sprintf('%s/api-docs.json', rtrim($destination, '\\/'));
}
$message = sprintf('<comment>Dumping resource list to %s: </comment>', $destination);
$this->writeToFile($content, $destination, $output, $message);
return;
}
if (false === $treatAsFile) {
$destination = sprintf('%s/%s.json', rtrim($destination, '\\/'), $resource);
}
$message = sprintf('<comment>Dump API declaration to %s: </comment>', $destination);
$this->writeToFile($content, $destination, $output, $message);
}
protected function writeToFile($content, $file, OutputInterface $output, $message): void
{
try {
$this->filesystem->dumpFile($file, $content);
$message .= ' <info>OK</info>';
} catch (IOException $e) {
$message .= sprintf(' <error>NOT OK - %s</error>', $e->getMessage());
}
$output->writeln($message);
}
protected function getResourceList(array $data)
{
return $this->formatter->format($data);
}
protected function getApiDeclaration(array $data, $resource)
{
return $this->formatter->format($data, '/' . $resource);
}
}

View file

@ -11,16 +11,51 @@
namespace Nelmio\ApiDocBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\HtmlFormatter;
use Nelmio\ApiDocBundle\Formatter\RequestAwareSwaggerFormatter;
use Nelmio\ApiDocBundle\Formatter\SwaggerFormatter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiDocController extends Controller
class ApiDocController extends AbstractController
{
public function indexAction()
{
$extractedDoc = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all();
$htmlContent = $this->get('nelmio_api_doc.formatter.html_formatter')->format($extractedDoc);
public function __construct(
private readonly ApiDocExtractor $extractor,
private readonly HtmlFormatter $htmlFormatter,
private readonly SwaggerFormatter $swaggerFormatter,
) {
}
return new Response($htmlContent, 200, array('Content-Type' => 'text/html'));
public function index(Request $request, $view = ApiDoc::DEFAULT_VIEW)
{
$apiVersion = $request->query->get('_version', null);
if ($apiVersion) {
$this->htmlFormatter->setVersion($apiVersion);
$extractedDoc = $this->extractor->allForVersion($apiVersion, $view);
} else {
$extractedDoc = $this->extractor->all($view);
}
$htmlContent = $this->htmlFormatter->format($extractedDoc);
return new Response($htmlContent, 200, ['Content-Type' => 'text/html']);
}
public function swagger(Request $request, $resource = null)
{
$docs = $this->extractor->all();
$formatter = new RequestAwareSwaggerFormatter($request, $this->swaggerFormatter);
$spec = $formatter->format($docs, $resource ? '/' . $resource : null);
if (null !== $resource && 0 === count($spec['apis'])) {
throw $this->createNotFoundException(sprintf('Cannot find resource "%s"', $resource));
}
return new JsonResponse($spec);
}
}

64
DataTypes.php Normal file
View file

@ -0,0 +1,64 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle;
/**
* All the supported data-types which will be specified in the `actualType` properties in parameters.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class DataTypes
{
public const INTEGER = 'integer';
public const FLOAT = 'float';
public const STRING = 'string';
public const BOOLEAN = 'boolean';
public const FILE = 'file';
public const ENUM = 'choice';
public const COLLECTION = 'collection';
public const MODEL = 'model';
public const DATE = 'date';
public const DATETIME = 'datetime';
public const TIME = 'time';
/**
* Returns true if the supplied `actualType` value is considered a primitive type. Returns false, otherwise.
*
* @param string $type
*
* @return bool
*/
public static function isPrimitive($type)
{
return in_array(strtolower($type), [
static::INTEGER,
static::FLOAT,
static::STRING,
static::BOOLEAN,
static::FILE,
static::DATE,
static::DATETIME,
static::TIME,
static::ENUM,
]);
}
}

View file

@ -16,27 +16,35 @@ use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder();
$treeBuilder
->root('nelmio_api_doc')
$treeBuilder = new TreeBuilder('nelmio_api_doc');
if (method_exists($treeBuilder, 'getRootNode')) {
$rootNode = $treeBuilder->getRootNode();
} else {
// symfony < 4.2 support
$rootNode = $treeBuilder->root('nelmio_api_doc');
}
$rootNode
->children()
->scalarNode('name')->defaultValue('API documentation')->end()
->arrayNode('exclude_sections')
->prototype('scalar')
->end()
->end()
->booleanNode('default_sections_opened')->defaultTrue()->end()
->arrayNode('motd')
->addDefaultsIfNotSet()
->children()
->scalarNode('template')->defaultValue('NelmioApiDocBundle::Components/motd.html.twig')->end()
->scalarNode('template')->defaultValue('@NelmioApiDoc/Components/motd.html.twig')->end()
->end()
->end()
->arrayNode('request_listener')
->beforeNormalization()
->ifTrue(function ($a) { return is_bool($a); })
->then(function ($a) { return array('enabled' => $a); })
->then(function ($a) { return ['enabled' => $a]; })
->end()
->addDefaultsIfNotSet()
->children()
@ -50,39 +58,119 @@ class Configuration implements ConfigurationInterface
->scalarNode('enabled')->defaultTrue()->end()
->scalarNode('endpoint')->defaultNull()->end()
->scalarNode('accept_type')->defaultNull()->end()
->enumNode('body_format')
->values(array('form', 'json'))
->defaultValue('form')
->arrayNode('body_format')
->addDefaultsIfNotSet()
->beforeNormalization()
->ifString()
->then(function ($v) { return ['default_format' => $v]; })
->end()
->children()
->arrayNode('formats')
->defaultValue(['form', 'json'])
->prototype('scalar')->end()
->end()
->enumNode('default_format')
->values(['form', 'json'])
->defaultValue('form')
->end()
->end()
->end()
->arrayNode('request_format')
->addDefaultsIfNotSet()
->children()
->arrayNode('formats')
->defaultValue([
'json' => 'application/json',
'xml' => 'application/xml',
])
->prototype('scalar')->end()
->end()
->enumNode('method')
->values(array('format_param', 'accept_header'))
->values(['format_param', 'accept_header'])
->defaultValue('format_param')
->end()
->enumNode('default_format')
->values(array('json', 'xml'))
->defaultValue('json')
->end()
->scalarNode('default_format')->defaultValue('json')->end()
->end()
->end()
->arrayNode('authentication')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('delivery')
->isRequired()
->validate()
->ifNotInArray(array('query', 'http_basic', 'header'))
->ifNotInArray(['query', 'http', 'header'])
->thenInvalid("Unknown authentication delivery type '%s'.")
->end()
->end()
->scalarNode('name')->isRequired()->end()
->enumNode('type')
->info('Required if http delivery is selected.')
->values(['basic', 'bearer'])
->end()
->booleanNode('custom_endpoint')->defaultFalse()->end()
->end()
->validate()
->ifTrue(function ($v) {
return 'http' === $v['delivery'] && !$v['type'];
})
->thenInvalid('"type" is required when using http delivery.')
->end()
// http_basic BC
->beforeNormalization()
->ifTrue(function ($v) {
return 'http_basic' === $v['delivery'];
})
->then(function ($v) {
$v['delivery'] = 'http';
$v['type'] = 'basic';
return $v;
})
->end()
->beforeNormalization()
->ifTrue(function ($v) {
return 'http' === $v['delivery'];
})
->then(function ($v) {
if ('http' === $v['delivery'] && !isset($v['name'])) {
$v['name'] = 'Authorization';
}
return $v;
})
->end()
->end()
->booleanNode('entity_to_choice')->defaultTrue()->end()
->end()
->end()
->arrayNode('swagger')
->addDefaultsIfNotSet()
->children()
->scalarNode('model_naming_strategy')->defaultValue('dot_notation')->end()
->scalarNode('api_base_path')->defaultValue('/api')->end()
->scalarNode('swagger_version')->defaultValue('1.2')->end()
->scalarNode('api_version')->defaultValue('0.1')->end()
->arrayNode('info')
->addDefaultsIfNotSet()
->children()
->scalarNode('title')->defaultValue('Symfony2')->end()
->scalarNode('description')->defaultValue('My awesome Symfony2 app!')->end()
->scalarNode('TermsOfServiceUrl')->defaultNull()->end()
->scalarNode('contact')->defaultNull()->end()
->scalarNode('license')->defaultNull()->end()
->scalarNode('licenseUrl')->defaultNull()->end()
->end()
->end()
->end()
->end()
->end();
->arrayNode('cache')
->addDefaultsIfNotSet()
->children()
->booleanNode('enabled')->defaultFalse()->end()
->scalarNode('file')->defaultValue('%kernel.cache_dir%/api-doc.cache')->end()
->end()
->end()
->end()
;
return $treeBuilder;
}

View file

@ -11,24 +11,22 @@
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class ExtractorHandlerCompilerPass implements CompilerPassInterface
{
/**
* {@inheritDoc}
*/
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
$handlers = array();
$handlers = [];
foreach ($container->findTaggedServiceIds('nelmio_api_doc.extractor.handler') as $id => $attributes) {
$handlers[] = new Reference($id);
}
$container
->getDefinition('nelmio_api_doc.extractor.api_doc_extractor')
->replaceArgument(4, $handlers);
->replaceArgument(2, $handlers)
;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class FormInfoParserCompilerPass implements CompilerPassInterface
{
public const TAG_NAME = 'nelmio_api_doc.extractor.form_info_parser';
public function process(ContainerBuilder $container): void
{
if (!$container->has('nelmio_api_doc.parser.form_type_parser')) {
return;
}
$formParser = $container->findDefinition('nelmio_api_doc.parser.form_type_parser');
foreach ($container->findTaggedServiceIds(self::TAG_NAME) as $id => $tags) {
$formParser->addMethodCall('addFormInfoParser', [new Reference($id)]);
}
}
}

View file

@ -2,10 +2,10 @@
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
/**
* Loads parsers to extract information from different libraries.
@ -14,9 +14,9 @@ use Symfony\Component\Config\FileLocator;
*/
class LoadExtractorParsersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
// forms may not be installed/enabled, if it is, load that config as well
if ($container->hasDefinition('form.factory')) {

View file

@ -11,18 +11,18 @@
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Nelmio\ApiDocBundle\Parser\FormInfoParser;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class NelmioApiDocExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
public function load(array $configs, ContainerBuilder $container): void
{
$processor = new Processor();
$configuration = new Configuration();
@ -30,15 +30,25 @@ class NelmioApiDocExtension extends Extension
$container->setParameter('nelmio_api_doc.motd.template', $config['motd']['template']);
$container->setParameter('nelmio_api_doc.exclude_sections', $config['exclude_sections']);
$container->setParameter('nelmio_api_doc.default_sections_opened', $config['default_sections_opened']);
$container->setParameter('nelmio_api_doc.api_name', $config['name']);
$container->setParameter('nelmio_api_doc.sandbox.enabled', $config['sandbox']['enabled']);
$container->setParameter('nelmio_api_doc.sandbox.enabled', $config['sandbox']['enabled']);
$container->setParameter('nelmio_api_doc.sandbox.endpoint', $config['sandbox']['endpoint']);
$container->setParameter('nelmio_api_doc.sandbox.request_format.method', $config['sandbox']['request_format']['method']);
$container->setParameter('nelmio_api_doc.sandbox.accept_type', $config['sandbox']['accept_type']);
$container->setParameter('nelmio_api_doc.sandbox.body_format', $config['sandbox']['body_format']);
$container->setParameter('nelmio_api_doc.sandbox.body_format.formats', $config['sandbox']['body_format']['formats']);
$container->setParameter('nelmio_api_doc.sandbox.body_format.default_format', $config['sandbox']['body_format']['default_format']);
$container->setParameter('nelmio_api_doc.sandbox.request_format.method', $config['sandbox']['request_format']['method']);
$container->setParameter('nelmio_api_doc.sandbox.request_format.default_format', $config['sandbox']['request_format']['default_format']);
$container->setParameter('nelmio_api_doc.sandbox.request_format.formats', $config['sandbox']['request_format']['formats']);
$container->setParameter('nelmio_api_doc.sandbox.entity_to_choice', $config['sandbox']['entity_to_choice']);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
if (method_exists($container, 'registerForAutoconfiguration')) {
$container->registerForAutoconfiguration(FormInfoParser::class)
->addTag(FormInfoParserCompilerPass::TAG_NAME)
;
}
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('formatters.xml');
$loader->load('services.xml');
@ -55,6 +65,25 @@ class NelmioApiDocExtension extends Extension
if (!interface_exists('\Symfony\Component\Validator\MetadataFactoryInterface')) {
$container->setParameter('nelmio_api_doc.parser.validation_parser.class', 'Nelmio\ApiDocBundle\Parser\ValidationParserLegacy');
}
$container->setParameter('nelmio_api_doc.swagger.base_path', $config['swagger']['api_base_path']);
$container->setParameter('nelmio_api_doc.swagger.swagger_version', $config['swagger']['swagger_version']);
$container->setParameter('nelmio_api_doc.swagger.api_version', $config['swagger']['api_version']);
$container->setParameter('nelmio_api_doc.swagger.info', $config['swagger']['info']);
$container->setParameter('nelmio_api_doc.swagger.model_naming_strategy', $config['swagger']['model_naming_strategy']);
if (true === $config['cache']['enabled']) {
$arguments = $container->getDefinition('nelmio_api_doc.extractor.api_doc_extractor')->getArguments();
$caching = new Definition('Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor');
$arguments[] = $config['cache']['file'];
$arguments[] = '%kernel.debug%';
$caching->setArguments($arguments);
$caching->setPublic(true);
$container->setDefinition('nelmio_api_doc.extractor.api_doc_extractor', $caching);
}
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('autowired.yaml');
}
/**

View file

@ -2,13 +2,13 @@
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class RegisterExtractorParsersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
if (false === $container->hasDefinition('nelmio_api_doc.extractor.api_doc_extractor')) {
return;
@ -16,23 +16,23 @@ class RegisterExtractorParsersPass implements CompilerPassInterface
$definition = $container->getDefinition('nelmio_api_doc.extractor.api_doc_extractor');
//find registered parsers and sort by priority
$sortedParsers = array();
// find registered parsers and sort by priority
$sortedParsers = [];
foreach ($container->findTaggedServiceIds('nelmio_api_doc.extractor.parser') as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$priority = $attributes['priority'] ?? 0;
$sortedParsers[$priority][] = $id;
}
}
//add parsers if any
// add parsers if any
if (!empty($sortedParsers)) {
krsort($sortedParsers);
$sortedParsers = call_user_func_array('array_merge', $sortedParsers);
//add method call for each registered parsers
// add method call for each registered parsers
foreach ($sortedParsers as $id) {
$definition->addMethodCall('addParser', array(new Reference($id)));
$definition->addMethodCall('addParser', [new Reference($id)]);
}
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Compiler pass that configures the SwaggerFormatter instance.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class SwaggerConfigCompilerPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
*
* @api
*/
public function process(ContainerBuilder $container): void
{
$formatter = $container->getDefinition('nelmio_api_doc.formatter.swagger_formatter');
$formatter->addMethodCall('setBasePath', [$container->getParameter('nelmio_api_doc.swagger.base_path')]);
$formatter->addMethodCall('setApiVersion', [$container->getParameter('nelmio_api_doc.swagger.api_version')]);
$formatter->addMethodCall('setSwaggerVersion', [$container->getParameter('nelmio_api_doc.swagger.swagger_version')]);
$formatter->addMethodCall('setInfo', [$container->getParameter('nelmio_api_doc.swagger.info')]);
$authentication = $container->getParameter('nelmio_api_doc.sandbox.authentication');
$formatter->setArguments([
$container->getParameter('nelmio_api_doc.swagger.model_naming_strategy'),
]);
if (null !== $authentication) {
$formatter->addMethodCall('setAuthenticationConfig', [$authentication]);
}
}
}

6
Dockerfile Normal file
View file

@ -0,0 +1,6 @@
ARG PHP_IMAGE_TAG
FROM php:${PHP_IMAGE_TAG}-cli-alpine
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /opt/test

View file

@ -14,18 +14,18 @@ namespace Nelmio\ApiDocBundle\EventListener;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\FormatterInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestListener
{
/**
* @var \Nelmio\ApiDocBundle\Extractor\ApiDocExtractor
* @var ApiDocExtractor
*/
protected $extractor;
/**
* @var \Nelmio\ApiDocBundle\Formatter\FormatterInterface
* @var FormatterInterface
*/
protected $formatter;
@ -41,12 +41,9 @@ class RequestListener
$this->parameter = $parameter;
}
/**
* {@inheritdoc}
*/
public function onKernelRequest(GetResponseEvent $event)
public function onKernelRequest(RequestEvent $event): void
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
if (HttpKernelInterface::MAIN_REQUEST !== $event->getRequestType()) {
return;
}
@ -57,14 +54,14 @@ class RequestListener
}
$controller = $request->attributes->get('_controller');
$route = $request->attributes->get('_route');
$route = $request->attributes->get('_route');
if (null !== $annotation = $this->extractor->get($controller, $route)) {
$result = $this->formatter->formatOne($annotation);
$event->setResponse(new Response($result, 200, array(
'Content-Type' => 'text/html'
)));
$event->setResponse(new Response($result, 200, [
'Content-Type' => 'text/html',
]));
}
}
}

View file

@ -11,58 +11,31 @@
namespace Nelmio\ApiDocBundle\Extractor;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Util\ClassUtils;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Parser\ParserInterface;
use Nelmio\ApiDocBundle\Parser\PostParserInterface;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
class ApiDocExtractor
{
const ANNOTATION_CLASS = 'Nelmio\\ApiDocBundle\\Annotation\\ApiDoc';
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var RouterInterface
*/
protected $router;
/**
* @var Reader
*/
protected $reader;
/**
* @var DocCommentExtractor
*/
private $commentExtractor;
/**
* @var ParserInterface[]
*/
protected $parsers = array();
protected array $parsers = [];
/**
* @var HandlerInterface[]
* @param HandlerInterface[] $handlers
* @param string[] $excludeSections
*/
protected $handlers;
public function __construct(ContainerInterface $container, RouterInterface $router, Reader $reader, DocCommentExtractor $commentExtractor, array $handlers)
{
$this->container = $container;
$this->router = $router;
$this->reader = $reader;
$this->commentExtractor = $commentExtractor;
$this->handlers = $handlers;
public function __construct(
protected RouterInterface $router,
protected DocCommentExtractor $commentExtractor,
protected array $handlers,
protected array $excludeSections,
) {
}
/**
@ -72,19 +45,39 @@ class ApiDocExtractor
*
* @return Route[] An array of routes
*/
public function getRoutes()
public function getRoutes(): array
{
return $this->router->getRouteCollection()->all();
}
/**
/*
* Extracts annotations from all known routes
*
* @return array
*/
public function all()
public function all($view = ApiDoc::DEFAULT_VIEW): array
{
return $this->extractAnnotations($this->getRoutes());
return $this->extractAnnotations($this->getRoutes(), $view);
}
/**
* Extracts annotations from routes for specific version
*
* @param string $apiVersion API version
* @param string $view
*/
public function allForVersion($apiVersion, $view = ApiDoc::DEFAULT_VIEW): array
{
$data = $this->all($view);
foreach ($data as $k => $a) {
// ignore other api version's routes
if (
$a['annotation']->getRoute()->getDefault('_version')
&& !version_compare($apiVersion ?? '', $a['annotation']->getRoute()->getDefault('_version'), '=')
) {
unset($data[$k]);
}
}
return $data;
}
/**
@ -93,14 +86,11 @@ class ApiDocExtractor
* - resource
*
* @param array $routes array of Route-objects for which the annotations should be extracted
*
* @return array
*/
public function extractAnnotations(array $routes)
public function extractAnnotations(array $routes, $view = ApiDoc::DEFAULT_VIEW): array
{
$array = array();
$resources = array();
$excludeSections = $this->container->getParameter('nelmio_api_doc.exclude_sections');
$array = [];
$resources = [];
foreach ($routes as $route) {
if (!$route instanceof Route) {
@ -108,18 +98,21 @@ class ApiDocExtractor
}
if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) {
$annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS);
if ($annotation && !in_array($annotation->getSection(), $excludeSections)) {
$annotation = $this->getMethodApiDoc($method);
if (
$annotation && !in_array($annotation->getSection(), $this->excludeSections)
&& (in_array($view, $annotation->getViews()) || (0 === count($annotation->getViews()) && ApiDoc::DEFAULT_VIEW === $view))
) {
if ($annotation->isResource()) {
if ($resource = $annotation->getResource()) {
$resources[] = $resource;
} else {
// remove format from routes used for resource grouping
$resources[] = str_replace('.{_format}', '', $route->getPattern());
$resources[] = str_replace('.{_format}', '', $route->getPath() ?: '');
}
}
$array[] = array('annotation' => $this->extractData($annotation, $route, $method));
$array[] = ['annotation' => $this->extractData($annotation, $route, $method)];
}
}
}
@ -127,10 +120,10 @@ class ApiDocExtractor
rsort($resources);
foreach ($array as $index => $element) {
$hasResource = false;
$pattern = $element['annotation']->getRoute()->getPattern();
$path = $element['annotation']->getRoute()->getPath() ?: '';
foreach ($resources as $resource) {
if (0 === strpos($pattern, $resource) || $resource === $element['annotation']->getResource()) {
if (str_starts_with($path, $resource) || $resource === $element['annotation']->getResource()) {
$array[$index]['resource'] = $resource;
$hasResource = true;
@ -143,17 +136,17 @@ class ApiDocExtractor
}
}
$methodOrder = array('GET', 'POST', 'PUT', 'DELETE');
$methodOrder = ['GET', 'POST', 'PUT', 'DELETE'];
usort($array, function ($a, $b) use ($methodOrder) {
if ($a['resource'] === $b['resource']) {
if ($a['annotation']->getRoute()->getPattern() === $b['annotation']->getRoute()->getPattern()) {
$methodA = array_search($a['annotation']->getRoute()->getRequirement('_method'), $methodOrder);
$methodB = array_search($b['annotation']->getRoute()->getRequirement('_method'), $methodOrder);
if ($a['annotation']->getRoute()->getPath() === $b['annotation']->getRoute()->getPath()) {
$methodA = array_search($a['annotation']->getRoute()->getMethods(), $methodOrder);
$methodB = array_search($b['annotation']->getRoute()->getMethods(), $methodOrder);
if ($methodA === $methodB) {
return strcmp(
$a['annotation']->getRoute()->getRequirement('_method'),
$b['annotation']->getRoute()->getRequirement('_method')
implode('|', $a['annotation']->getRoute()->getMethods()),
implode('|', $b['annotation']->getRoute()->getMethods())
);
}
@ -161,8 +154,8 @@ class ApiDocExtractor
}
return strcmp(
$a['annotation']->getRoute()->getPattern(),
$b['annotation']->getRoute()->getPattern()
$a['annotation']->getRoute()->getPath(),
$b['annotation']->getRoute()->getPath()
);
}
@ -176,22 +169,18 @@ class ApiDocExtractor
* Returns the ReflectionMethod for the given controller string.
*
* @param string $controller
* @return \ReflectionMethod|null
*
* @return \ReflectionMethod|null
*/
public function getReflectionMethod($controller)
{
if (null === $controller) {
return null;
}
if (preg_match('#(.+)::([\w]+)#', $controller, $matches)) {
$class = $matches[1];
$method = $matches[2];
} elseif (preg_match('#(.+):([\w]+)#', $controller, $matches)) {
$controller = $matches[1];
$method = $matches[2];
if ($this->container->has($controller)) {
$this->container->enterScope('request');
$this->container->set('request', new Request(), 'request');
$class = ClassUtils::getRealClass(get_class($this->container->get($controller)));
$this->container->leaveScope('request');
}
}
if (isset($class) && isset($method)) {
@ -209,12 +198,13 @@ class ApiDocExtractor
*
* @param string $controller
* @param string $route
* @return ApiDoc|null
*
* @return ApiDoc|null
*/
public function get($controller, $route)
{
if ($method = $this->getReflectionMethod($controller)) {
if ($annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS)) {
if ($annotation = $this->getMethodApiDoc($method)) {
if ($route = $this->router->getRouteCollection()->get($route)) {
return $this->extractData($annotation, $route, $method);
}
@ -224,12 +214,20 @@ class ApiDocExtractor
return null;
}
protected function getMethodApiDoc(\ReflectionMethod $method): ?ApiDoc
{
$attributes = $method->getAttributes(ApiDoc::class, \ReflectionAttribute::IS_INSTANCEOF);
if (!$attributes) {
return null;
}
return $attributes[0]->newInstance();
}
/**
* Registers a class parser to use for parsing input class metadata
*
* @param ParserInterface $parser
*/
public function addParser(ParserInterface $parser)
public function addParser(ParserInterface $parser): void
{
$this->parsers[] = $parser;
}
@ -237,9 +235,6 @@ class ApiDocExtractor
/**
* Returns a new ApiDoc instance with more data.
*
* @param ApiDoc $annotation
* @param Route $route
* @param \ReflectionMethod $method
* @return ApiDoc
*/
protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMethod $method)
@ -256,54 +251,113 @@ class ApiDocExtractor
// route
$annotation->setRoute($route);
$inputs = [];
if (null !== $annotation->getInputs()) {
$inputs = $annotation->getInputs();
} elseif (null !== $annotation->getInput()) {
$inputs[] = $annotation->getInput();
}
// input (populates 'parameters' for the formatters)
if (null !== $input = $annotation->getInput()) {
$parameters = array();
$normalizedInput = $this->normalizeClassParameter($input);
if (count($inputs)) {
$parameters = [];
foreach ($inputs as $input) {
$normalizedInput = $this->normalizeClassParameter($input);
$supportedParsers = [];
foreach ($this->getParsers($normalizedInput) as $parser) {
if ($parser->supports($normalizedInput)) {
$supportedParsers[] = $parser;
$parameters = $this->mergeParameters($parameters, $parser->parse($normalizedInput));
}
}
$supportedParsers = array();
foreach ($this->getParsers($normalizedInput) as $parser) {
if ($parser->supports($normalizedInput)) {
$supportedParsers[] = $parser;
$parameters = $this->mergeParameters($parameters, $parser->parse($normalizedInput));
}
}
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$parameters = $this->mergeParameters(
$parameters,
$parser->postParse($normalizedInput, $parameters)
);
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$parameters = $this->mergeParameters(
$parameters,
$parser->postParse($normalizedInput, $parameters)
);
}
}
}
$parameters = $this->setParentClasses($parameters);
$parameters = $this->clearClasses($parameters);
$parameters = $this->generateHumanReadableTypes($parameters);
if ('PUT' === $method) {
// All parameters are optional with PUT (update)
array_walk($parameters, function ($val, $key) use (&$data) {
if ('PATCH' === $annotation->getMethod()) {
// All parameters are optional with PATCH (update)
foreach ($parameters as $key => $val) {
$parameters[$key]['required'] = false;
});
}
}
// merge parameters with parameters block from ApiDoc annotation in controller method
$parameters = $this->mergeParameters($parameters, $annotation->getParameters());
$annotation->setParameters($parameters);
}
// output (populates 'response' for the formatters)
if (null !== $output = $annotation->getOutput()) {
$response = array();
$response = [];
$supportedParsers = [];
$normalizedOutput = $this->normalizeClassParameter($output);
foreach ($this->getParsers($normalizedOutput) as $parser) {
if ($parser->supports($normalizedOutput)) {
$supportedParsers[] = $parser;
$response = $this->mergeParameters($response, $parser->parse($normalizedOutput));
}
}
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$mp = $parser->postParse($normalizedOutput, $response);
$response = $this->mergeParameters($response, $mp);
}
}
$response = $this->clearClasses($response);
$response = $this->generateHumanReadableTypes($response);
$annotation->setResponse($response);
$annotation->setResponseForStatusCode($response, $normalizedOutput, 200);
}
if (count($annotation->getResponseMap()) > 0) {
foreach ($annotation->getResponseMap() as $code => $modelName) {
if ('200' === (string) $code && isset($modelName['type']) && isset($modelName['model'])) {
/*
* Model was already parsed as the default `output` for this ApiDoc.
*/
continue;
}
$normalizedModel = $this->normalizeClassParameter($modelName);
$parameters = [];
$supportedParsers = [];
foreach ($this->getParsers($normalizedModel) as $parser) {
if ($parser->supports($normalizedModel)) {
$supportedParsers[] = $parser;
$parameters = $this->mergeParameters($parameters, $parser->parse($normalizedModel));
}
}
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$mp = $parser->postParse($normalizedModel, $parameters);
$parameters = $this->mergeParameters($parameters, $mp);
}
}
$parameters = $this->setParentClasses($parameters);
$parameters = $this->clearClasses($parameters);
$parameters = $this->generateHumanReadableTypes($parameters);
$annotation->setResponseForStatusCode($parameters, $normalizedModel, $code);
}
}
return $annotation;
@ -311,14 +365,33 @@ class ApiDocExtractor
protected function normalizeClassParameter($input)
{
$defaults = array(
'class' => '',
'groups' => array(),
);
$defaults = [
'class' => '',
'groups' => [],
'options' => [],
];
// normalize strings
if (is_string($input)) {
$input = array('class' => $input);
$input = ['class' => $input];
}
$collectionData = [];
/*
* Match array<Fully\Qualified\ClassName> as alias; "as alias" optional.
*/
if (preg_match_all("/^array<([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)>(?:\\s+as\\s+(.+))?$/", $input['class'], $collectionData)) {
$input['class'] = $collectionData[1][0];
$input['collection'] = true;
$input['collectionName'] = $collectionData[2][0];
} elseif (preg_match('/^array</', $input['class'])) { // See if a collection directive was attempted. Must be malformed.
throw new \InvalidArgumentException(
sprintf(
'Malformed collection directive: %s. Proper format is: array<Fully\\Qualified\\ClassName> or array<Fully\\Qualified\\ClassName> as collectionName',
$input['class']
)
);
}
// normalize groups
@ -336,9 +409,14 @@ class ApiDocExtractor
* - Requirement parameters are concatenated.
* - Other string values are overridden by later parsers when present.
* - Array parameters are recursively merged.
* - Non-null default values prevail over null default values. Later values overrides previous defaults.
*
* However, if newly-returned parameter array contains a parameter with NULL, the parameter is removed from the merged results.
* If the parameter is not present in the newly-returned array, then it is left as-is.
*
* @param array $p1 The pre-existing parameters array.
* @param array $p2 The newly-returned parameters array.
*
* @param array $p1 The pre-existing parameters array.
* @param array $p2 The newly-returned parameters array.
* @return array The resulting, merged array.
*/
protected function mergeParameters($p1, $p2)
@ -346,9 +424,14 @@ class ApiDocExtractor
$params = $p1;
foreach ($p2 as $propname => $propvalue) {
if (null === $propvalue) {
unset($params[$propname]);
continue;
}
if (!isset($p1[$propname])) {
$params[$propname] = $propvalue;
} else {
} elseif (is_array($propvalue)) {
$v1 = $p1[$propname];
foreach ($propvalue as $name => $value) {
@ -358,15 +441,21 @@ class ApiDocExtractor
} else {
$v1[$name] = $value;
}
} elseif (!is_null($value)) {
if (in_array($name, array('required', 'readonly'))) {
} elseif (null !== $value) {
if (in_array($name, ['required', 'readonly'])) {
$v1[$name] = $v1[$name] || $value;
} elseif (in_array($name, array('requirement'))) {
} elseif ('requirement' === $name) {
if (isset($v1[$name])) {
$v1[$name] .= ', ' . $value;
} else {
$v1[$name] = $value;
}
} elseif ('default' === $name) {
if (isset($v1[$name])) {
$v1[$name] = $value ?? $v1[$name];
} else {
$v1[$name] = $value ?? null;
}
} else {
$v1[$name] = $value;
}
@ -383,23 +472,48 @@ class ApiDocExtractor
/**
* Parses annotations for a given method, and adds new information to the given ApiDoc
* annotation. Useful to extract information from the FOSRestBundle annotations.
*
* @param ApiDoc $annotation
* @param Route $route
* @param ReflectionMethod $method
*/
protected function parseAnnotations(ApiDoc $annotation, Route $route, \ReflectionMethod $method)
protected function parseAnnotations(ApiDoc $annotation, Route $route, \ReflectionMethod $method): void
{
$annots = $this->reader->getMethodAnnotations($method);
foreach ($this->handlers as $handler) {
$handler->handle($annotation, $annots, $route, $method);
$handler->handle($annotation, $route, $method);
}
}
/**
* Set parent class to children
*
* @param array $array The source array.
*
* @return array The updated array.
*/
protected function setParentClasses($array)
{
if (is_array($array)) {
foreach ($array as $k => $v) {
if (isset($v['children'])) {
if (isset($v['class'])) {
foreach ($v['children'] as $key => $item) {
if (empty($item['parentClass'] ?? null)) {
$array[$k]['children'][$key]['parentClass'] = $v['class'];
}
$array[$k]['children'][$key]['field'] = $key;
}
}
$array[$k]['children'] = $this->setParentClasses($array[$k]['children']);
}
}
}
return $array;
}
/**
* Clears the temporary 'class' parameter from the parameters array before it is returned.
*
* @param array $array The source array.
* @param array $array The source array.
*
* @return array The cleared array.
*/
protected function clearClasses($array)
@ -414,12 +528,69 @@ class ApiDocExtractor
return $array;
}
/**
* Populates the `dataType` properties in the parameter array if empty. Recurses through children when necessary.
*
* @return array
*/
protected function generateHumanReadableTypes(array $array)
{
foreach ($array as $name => $info) {
if (empty($info['dataType']) && array_key_exists('subType', $info)) {
$array[$name]['dataType'] = $this->generateHumanReadableType($info['actualType'], $info['subType']);
}
if (isset($info['children'])) {
$array[$name]['children'] = $this->generateHumanReadableTypes($info['children']);
}
}
return $array;
}
/**
* Creates a human-readable version of the `actualType`. `subType` is taken into account.
*
* @param string $actualType
* @param string $subType
*
* @return string
*/
protected function generateHumanReadableType($actualType, $subType)
{
if (DataTypes::MODEL == $actualType) {
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
return sprintf('object (%s)', end($parts));
}
return sprintf('object (%s)', $subType);
}
if (DataTypes::COLLECTION == $actualType) {
if (DataTypes::isPrimitive($subType)) {
return sprintf('array of %ss', $subType);
}
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
return sprintf('array of objects (%s)', end($parts));
}
return sprintf('array of objects (%s)', $subType);
}
return $actualType;
}
private function getParsers(array $parameters)
{
if (isset($parameters['parsers'])) {
$parsers = array();
$parsers = [];
foreach ($this->parsers as $parser) {
if (in_array(get_class($parser), $parameters['parsers'])) {
if (in_array($parser::class, $parameters['parsers'])) {
$parsers[] = $parser;
}
}

View file

@ -0,0 +1,92 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\RouterInterface;
/**
* Class CachingApiDocExtractor
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class CachingApiDocExtractor extends ApiDocExtractor
{
/**
* @param HandlerInterface[] $handlers
* @param string[] $excludeSections
* @param bool|false $debug
*/
public function __construct(
RouterInterface $router,
DocCommentExtractor $commentExtractor,
array $handlers,
array $excludeSections,
private string $cacheFile,
private bool $debug = false,
) {
parent::__construct($router, $commentExtractor, $handlers, $excludeSections);
}
/**
* @param string $view View name
*
* @return array|mixed
*/
public function all($view = ApiDoc::DEFAULT_VIEW): array
{
$cache = $this->getViewCache($view);
if (!$cache->isFresh()) {
$resources = [];
foreach ($this->getRoutes() as $route) {
if (
null !== ($method = $this->getReflectionMethod($route->getDefault('_controller')))
&& null !== $this->getMethodApiDoc($method)
) {
$file = $method->getDeclaringClass()->getFileName();
$resources[] = new FileResource($file);
}
}
$resources = array_merge($resources, $this->router->getRouteCollection()->getResources());
$data = parent::all($view);
$cache->write(serialize($data), $resources);
return $data;
}
// For BC
if (method_exists($cache, 'getPath')) {
$cachePath = $cache->getPath();
} else {
$cachePath = (string) $cache;
}
return unserialize(file_get_contents($cachePath));
}
/**
* @param string $view
*
* @return ConfigCache
*/
private function getViewCache($view)
{
return new ConfigCache($this->cacheFile . '.' . $view, $this->debug);
}
}

View file

@ -1,54 +0,0 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Extractor\Handler;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use FOS\RestBundle\Controller\Annotations\QueryParam;
class FosRestHandler implements HandlerInterface
{
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method)
{
foreach ($annotations as $annot) {
if ($annot instanceof RequestParam) {
$annotation->addParameter($annot->name, array(
'required' => $annot->strict && $annot->default === null,
'dataType' => $annot->requirements,
'description' => $annot->description,
'readonly' => false
));
} elseif ($annot instanceof QueryParam) {
if ($annot->strict && $annot->nullable === false && $annot->default === null) {
$annotation->addRequirement($annot->name, array(
'requirement' => $annot->requirements,
'dataType' => '',
'description' => $annot->description,
));
} elseif ($annot->default !== null) {
$annotation->addFilter($annot->name, array(
'requirement' => $annot->requirements,
'description' => $annot->description,
'default' => $annot->default,
));
} else {
$annotation->addFilter($annot->name, array(
'requirement' => $annot->requirements,
'description' => $annot->description,
));
}
}
}
}
}

View file

@ -1,33 +0,0 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Extractor\Handler;
use JMS\SecurityExtraBundle\Annotation\PreAuthorize;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route;
use JMS\SecurityExtraBundle\Annotation\Secure;
class JmsSecurityExtraHandler implements HandlerInterface
{
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method)
{
foreach ($annotations as $annot) {
if ($annot instanceof PreAuthorize) {
$annotation->setAuthentication(true);
} elseif ($annot instanceof Secure) {
$annotation->setAuthentication(true);
$annotation->setAuthenticationRoles(is_array($annot->roles) ? $annot->roles : explode(',', $annot->roles));
}
}
}
}

View file

@ -11,10 +11,10 @@
namespace Nelmio\ApiDocBundle\Extractor\Handler;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use Symfony\Component\Routing\Route;
class PhpDocHandler implements HandlerInterface
{
@ -28,7 +28,7 @@ class PhpDocHandler implements HandlerInterface
$this->commentExtractor = $commentExtractor;
}
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method)
public function handle(ApiDoc $annotation, Route $route, \ReflectionMethod $method): void
{
// description
if (null === $annotation->getDescription()) {
@ -48,33 +48,24 @@ class PhpDocHandler implements HandlerInterface
$requirements = $annotation->getRequirements();
foreach ($route->getRequirements() as $name => $value) {
if (!isset($requirements[$name]) && '_method' !== $name && '_scheme' !== $name) {
$requirements[$name] = array(
$requirements[$name] = [
'requirement' => $value,
'dataType' => '',
'description' => '',
);
}
if ('_scheme' === $name) {
$https = ('https' == $value);
$annotation->setHttps($https);
];
}
}
if (method_exists($route, 'getSchemes')) {
$annotation->setHttps(in_array('https', $route->getSchemes()));
}
$paramDocs = array();
$paramDocs = [];
foreach (explode("\n", $this->commentExtractor->getDocComment($method)) as $line) {
if (preg_match('{^@param (.+)}', trim($line), $matches)) {
$paramDocs[] = $matches[1];
}
if (preg_match('{^@deprecated\b(.*)}', trim($line), $matches)) {
if (preg_match('{^@deprecated}', trim($line))) {
$annotation->setDeprecated(true);
}
if (preg_match('{^@link\b(.*)}', trim($line), $matches)) {
$annotation->setLink($matches[1]);
if (preg_match('{^@(link|see) (.+)}', trim($line), $matches)) {
$annotation->setLink($matches[2]);
}
}
@ -83,10 +74,17 @@ class PhpDocHandler implements HandlerInterface
$found = false;
foreach ($paramDocs as $paramDoc) {
if (preg_match(sprintf($regexp, preg_quote($var)), $paramDoc, $matches)) {
$requirements[$var]['dataType'] = isset($matches[1]) ? $matches[1] : '';
$requirements[$var]['description'] = $matches[2];
$annotationRequirements = $annotation->getRequirements();
if (!isset($requirements[$var]['requirement'])) {
if (!isset($annotationRequirements[$var]['dataType'])) {
$requirements[$var]['dataType'] = $matches[1] ?? '';
}
if (!isset($annotationRequirements[$var]['description'])) {
$requirements[$var]['description'] = $matches[2];
}
if (!isset($requirements[$var]['requirement']) && !isset($annotationRequirements[$var]['requirement'])) {
$requirements[$var]['requirement'] = '';
}
@ -96,7 +94,7 @@ class PhpDocHandler implements HandlerInterface
}
if (!isset($requirements[$var]) && false === $found) {
$requirements[$var] = array('requirement' => '', 'dataType' => '', 'description' => '');
$requirements[$var] = ['requirement' => '', 'dataType' => '', 'description' => ''];
}
}

View file

@ -1,29 +0,0 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Extractor\Handler;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
class SensioFrameworkExtraHandler implements HandlerInterface
{
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method)
{
foreach ($annotations as $annot) {
if ($annot instanceof Cache) {
$annotation->setCache($annot->getMaxAge());
}
}
}
}

View file

@ -11,18 +11,13 @@
namespace Nelmio\ApiDocBundle\Extractor;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Symfony\Component\Routing\Route;
interface HandlerInterface
{
/**
* Parse route parameters in order to populate ApiDoc.
*
* @param Nelmio\ApiDocBundle\Annotation\ApiDoc $annotation
* @param array $annotations
* @param Symfony\Component\Routing\Route $route
* @param ReflectionMethod $method
*/
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method);
public function handle(ApiDoc $annotation, Route $route, \ReflectionMethod $method);
}

View file

@ -11,45 +11,43 @@
namespace Nelmio\ApiDocBundle\Form\Extension;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class DescriptionFormTypeExtension extends AbstractTypeExtension
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->setAttribute('description', $options['description']);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['description'] = $options['description'];
}
/**
* {@inheritdoc}
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(array(
'description' => '',
));
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
public function configureOptions(OptionsResolver $resolver): void
{
return 'form';
$resolver->setDefaults([
'description' => '',
]);
}
public static function getExtendedTypes(): iterable
{
return [LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\FormType')];
}
}

View file

@ -11,13 +11,18 @@
namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\DataTypes;
abstract class AbstractFormatter implements FormatterInterface
{
/**
* {@inheritdoc}
*/
protected $version;
public function setVersion($version): void
{
$this->version = $version;
}
public function formatOne(ApiDoc $annotation)
{
return $this->renderOne(
@ -25,9 +30,6 @@ abstract class AbstractFormatter implements FormatterInterface
);
}
/**
* {@inheritdoc}
*/
public function format(array $collection)
{
return $this->render(
@ -38,7 +40,6 @@ abstract class AbstractFormatter implements FormatterInterface
/**
* Format a single array of data
*
* @param array $data
* @return string|array
*/
abstract protected function renderOne(array $data);
@ -46,37 +47,70 @@ abstract class AbstractFormatter implements FormatterInterface
/**
* Format a set of resource sections.
*
* @param array $collection
* @return string|array
*/
abstract protected function render(array $collection);
/**
* Check that the versions range includes current version
*
* @param string $fromVersion (default: null)
* @param string $toVersion (default: null)
*
* @return bool
*/
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;
}
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]`
*
* @param string $parentName
* @param bool $ignoreNestedReadOnly
*
* @param array $data
* @param string $parentName
* @param boolean $ignoreNestedReadOnly
* @return array
*/
protected function compressNestedParameters(array $data, $parentName = null, $ignoreNestedReadOnly = false)
{
$newParams = array();
$newParams = [];
foreach ($data as $name => $info) {
if ($this->version && !$this->rangeIncludesVersion(
$info['sinceVersion'] ?? null,
$info['untilVersion'] ?? null
)) {
continue;
}
$newName = $this->getNewName($name, $info, $parentName);
$newParams[$newName] = array(
'dataType' => $info['dataType'],
'readonly' => array_key_exists('readonly', $info) ? $info['readonly'] : null,
'required' => $info['required'],
'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,
);
$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,
'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) {
@ -92,21 +126,29 @@ abstract class AbstractFormatter implements FormatterInterface
* Returns a new property name, taking into account whether or not the property
* is an array of some other data type.
*
* @param string $name
* @param array $data
* @param string $parentName
* @param string $name
* @param array $data
* @param string $parentName
*
* @return string
*/
protected function getNewName($name, $data, $parentName = null)
{
$newName = ($parentName) ? sprintf("%s[%s]", $parentName, $name) : $name;
$array = (false === strpos($data['dataType'], "array of")) ? "" : "[]";
$array = '';
$newName = ($parentName) ? sprintf('%s[%s]', $parentName, $name) : $name;
return sprintf("%s%s", $newName, $array);
if (isset($data['actualType']) && DataTypes::COLLECTION == $data['actualType']
&& isset($data['subType']) && null !== $data['subType']
) {
$array = '[]';
}
return sprintf('%s%s', $newName, $array);
}
/**
* @param array $annotation
* @param array $annotation
*
* @return array
*/
protected function processAnnotation($annotation)
@ -119,23 +161,30 @@ abstract class AbstractFormatter implements FormatterInterface
$annotation['response'] = $this->compressNestedParameters($annotation['response']);
}
$annotation['id'] = strtolower($annotation['method']).'-'.str_replace('/', '-', $annotation['uri']);
if (isset($annotation['parsedResponseMap'])) {
foreach ($annotation['parsedResponseMap'] as $statusCode => &$data) {
$data['model'] = $this->compressNestedParameters($data['model']);
}
}
$annotation['id'] = strtolower($annotation['method'] ?? '') . '-' . str_replace('/', '-', $annotation['uri'] ?? '');
return $annotation;
}
/**
* @param array[ApiDoc] $collection
*
* @return array
*/
protected function processCollection(array $collection)
{
$array = array();
$array = [];
foreach ($collection as $coll) {
$array[$coll['annotation']->getSection()][$coll['resource']][] = $coll['annotation']->toArray();
}
$processedCollection = array();
$processedCollection = [];
foreach ($array as $section => $resources) {
foreach ($resources as $path => $annotations) {
foreach ($annotations as $annotation) {

View file

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
interface FormatterInterface
{
@ -19,6 +19,7 @@ interface FormatterInterface
* Format a collection of documentation data.
*
* @param array[ApiDoc] $collection
*
* @return string|array
*/
public function format(array $collection);
@ -27,7 +28,7 @@ interface FormatterInterface
* Format documentation data for one route.
*
* @param ApiDoc $annotation
* return string|array
* return string|array
*/
public function formatOne(ApiDoc $annotation);
}

View file

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Formatter;
use Symfony\Component\Templating\EngineInterface;
use Twig\Environment as TwigEnvironment;
class HtmlFormatter extends AbstractFormatter
{
@ -31,15 +32,20 @@ class HtmlFormatter extends AbstractFormatter
protected $defaultRequestFormat;
/**
* @var EngineInterface
* @var EngineInterface|TwigEnvironment
*/
protected $engine;
/**
* @var boolean
* @var bool
*/
private $enableSandbox;
/**
* @var array
*/
private $requestFormats;
/**
* @var string
*/
@ -50,10 +56,15 @@ class HtmlFormatter extends AbstractFormatter
*/
private $acceptType;
/**
* @var array
*/
private $bodyFormats;
/**
* @var string
*/
private $bodyFormat;
private $defaultBodyFormat;
/**
* @var array
@ -66,9 +77,11 @@ class HtmlFormatter extends AbstractFormatter
private $motdTemplate;
/**
* @param array $authentication
* @var bool
*/
public function setAuthentication(array $authentication = null)
private $defaultSectionsOpened;
public function setAuthentication(?array $authentication = null): void
{
$this->authentication = $authentication;
}
@ -76,7 +89,7 @@ class HtmlFormatter extends AbstractFormatter
/**
* @param string $apiName
*/
public function setApiName($apiName)
public function setApiName($apiName): void
{
$this->apiName = $apiName;
}
@ -84,23 +97,23 @@ class HtmlFormatter extends AbstractFormatter
/**
* @param string $endpoint
*/
public function setEndpoint($endpoint)
public function setEndpoint($endpoint): void
{
$this->endpoint = $endpoint;
}
/**
* @param boolean $enableSandbox
* @param bool $enableSandbox
*/
public function setEnableSandbox($enableSandbox)
public function setEnableSandbox($enableSandbox): void
{
$this->enableSandbox = $enableSandbox;
}
/**
* @param EngineInterface $engine
* @param EngineInterface|TwigEnvironment $engine
*/
public function setTemplatingEngine(EngineInterface $engine)
public function setTemplatingEngine($engine): void
{
$this->engine = $engine;
}
@ -108,31 +121,41 @@ class HtmlFormatter extends AbstractFormatter
/**
* @param string $acceptType
*/
public function setAcceptType($acceptType)
public function setAcceptType($acceptType): void
{
$this->acceptType = $acceptType;
}
/**
* @param string $bodyFormat
*/
public function setBodyFormat($bodyFormat)
public function setBodyFormats(array $bodyFormats): void
{
$this->bodyFormat = $bodyFormat;
$this->bodyFormats = $bodyFormats;
}
/**
* @param string $defaultBodyFormat
*/
public function setDefaultBodyFormat($defaultBodyFormat): void
{
$this->defaultBodyFormat = $defaultBodyFormat;
}
/**
* @param string $method
*/
public function setRequestFormatMethod($method)
public function setRequestFormatMethod($method): void
{
$this->requestFormatMethod = $method;
}
public function setRequestFormats(array $formats): void
{
$this->requestFormats = $formats;
}
/**
* @param string $format
*/
public function setDefaultRequestFormat($format)
public function setDefaultRequestFormat($format): void
{
$this->defaultRequestFormat = $format;
}
@ -140,7 +163,7 @@ class HtmlFormatter extends AbstractFormatter
/**
* @param string $motdTemplate
*/
public function setMotdTemplate($motdTemplate)
public function setMotdTemplate($motdTemplate): void
{
$this->motdTemplate = $motdTemplate;
}
@ -154,28 +177,30 @@ class HtmlFormatter extends AbstractFormatter
}
/**
* {@inheritdoc}
* @param bool $defaultSectionsOpened
*/
public function setDefaultSectionsOpened($defaultSectionsOpened): void
{
$this->defaultSectionsOpened = $defaultSectionsOpened;
}
protected function renderOne(array $data)
{
return $this->engine->render('NelmioApiDocBundle::resource.html.twig', array_merge(
array(
'data' => $data,
return $this->engine->render('@NelmioApiDoc/resource.html.twig', array_merge(
[
'data' => $data,
'displayContent' => true,
),
],
$this->getGlobalVars()
));
}
/**
* {@inheritdoc}
*/
protected function render(array $collection)
{
return $this->engine->render('NelmioApiDocBundle::resources.html.twig', array_merge(
array(
return $this->engine->render('@NelmioApiDoc/resources.html.twig', array_merge(
[
'resources' => $collection,
),
],
$this->getGlobalVars()
));
}
@ -185,19 +210,22 @@ class HtmlFormatter extends AbstractFormatter
*/
private function getGlobalVars()
{
return array(
'apiName' => $this->apiName,
'authentication' => $this->authentication,
'endpoint' => $this->endpoint,
'enableSandbox' => $this->enableSandbox,
'requestFormatMethod' => $this->requestFormatMethod,
'acceptType' => $this->acceptType,
'bodyFormat' => $this->bodyFormat,
return [
'apiName' => $this->apiName,
'authentication' => $this->authentication,
'endpoint' => $this->endpoint,
'enableSandbox' => $this->enableSandbox,
'requestFormatMethod' => $this->requestFormatMethod,
'acceptType' => $this->acceptType,
'bodyFormats' => $this->bodyFormats,
'defaultBodyFormat' => $this->defaultBodyFormat,
'requestFormats' => $this->requestFormats,
'defaultRequestFormat' => $this->defaultRequestFormat,
'date' => date(DATE_RFC822),
'css' => file_get_contents(__DIR__ . '/../Resources/public/css/screen.css'),
'js' => file_get_contents(__DIR__ . '/../Resources/public/js/all.js'),
'motdTemplate' => $this->motdTemplate
);
'date' => date(DATE_RFC822),
'css' => file_get_contents(__DIR__ . '/../Resources/public/css/screen.css'),
'js' => file_get_contents(__DIR__ . '/../Resources/public/js/all.js'),
'motdTemplate' => $this->motdTemplate,
'defaultSectionsOpened' => $this->defaultSectionsOpened,
];
}
}

View file

@ -13,15 +13,12 @@ namespace Nelmio\ApiDocBundle\Formatter;
class MarkdownFormatter extends AbstractFormatter
{
/**
* {@inheritdoc}
*/
protected function renderOne(array $data)
{
$markdown = sprintf("### `%s` %s ###\n", $data['method'], $data['uri']);
if (isset($data['deprecated']) && false !== $data['deprecated']) {
$markdown .= "### This method is deprecated ###";
$markdown .= '### This method is deprecated ###';
$markdown .= "\n\n";
}
@ -86,6 +83,9 @@ class MarkdownFormatter extends AbstractFormatter
if (isset($parameter['description']) && !empty($parameter['description'])) {
$markdown .= sprintf(" * description: %s\n", $parameter['description']);
}
if (isset($parameter['default']) && !empty($parameter['default'])) {
$markdown .= sprintf(" * default value: %s\n", $parameter['default']);
}
$markdown .= "\n";
}
@ -104,15 +104,15 @@ class MarkdownFormatter extends AbstractFormatter
}
if (null !== $parameter['sinceVersion'] || null !== $parameter['untilVersion']) {
$markdown .= " * versions: ";
$markdown .= ' * versions: ';
if ($parameter['sinceVersion']) {
$markdown .= '>='.$parameter['sinceVersion'];
$markdown .= '>=' . $parameter['sinceVersion'];
}
if ($parameter['untilVersion']) {
if ($parameter['sinceVersion']) {
$markdown .= ',';
}
$markdown .= '<='.$parameter['untilVersion'];
$markdown .= '<=' . $parameter['untilVersion'];
}
$markdown .= "\n";
}
@ -124,9 +124,6 @@ class MarkdownFormatter extends AbstractFormatter
return $markdown;
}
/**
* {@inheritdoc}
*/
protected function render(array $collection)
{
$markdown = '';

View file

@ -0,0 +1,70 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Symfony\Component\HttpFoundation\Request;
/**
* Extends SwaggerFormatter which takes into account the request's base URL when generating the documents for direct swagger-ui consumption.
*
* @author Bezalel Hermoso <bezalelhermoso@gmail.com>
*/
class RequestAwareSwaggerFormatter implements FormatterInterface
{
/**
* @var Request
*/
protected $request;
/**
* @var SwaggerFormatter
*/
protected $formatter;
public function __construct(Request $request, SwaggerFormatter $formatter)
{
$this->request = $request;
$this->formatter = $formatter;
}
/**
* Format a collection of documentation data.
*
* @param null $resource
*
* @internal param $array [ApiDoc] $collection
*
* @return string|array
*/
public function format(array $collection, $resource = null)
{
$result = $this->formatter->format($collection, $resource);
if (null !== $resource) {
$result['basePath'] = $this->request->getBaseUrl() . $result['basePath'];
}
return $result;
}
/**
* Format documentation data for one route.
*
* @param ApiDoc $annotation
* return string|array
*/
public function formatOne(ApiDoc $annotation)
{
return $this->formatter->formatOne($annotation);
}
}

View file

@ -11,42 +11,33 @@
namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
class SimpleFormatter extends AbstractFormatter
{
/**
* {@inheritdoc}
*/
public function formatOne(ApiDoc $annotation)
{
return $annotation->toArray();
}
/**
* {@inheritdoc}
*/
public function format(array $collection)
{
$array = array();
$array = [];
foreach ($collection as $coll) {
$array[$coll['resource']][] = $coll['annotation']->toArray();
$annotationArray = $coll['annotation']->toArray();
unset($annotationArray['parsedResponseMap']);
$array[$coll['resource']][] = $annotationArray;
}
return $array;
}
/**
* {@inheritdoc}
*/
protected function renderOne(array $data)
protected function renderOne(array $data): void
{
}
/**
* {@inheritdoc}
*/
protected function render(array $collection)
protected function render(array $collection): void
{
}
}

View file

@ -0,0 +1,565 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Swagger\ModelRegistry;
use Symfony\Component\HttpFoundation\Response;
/**
* Produces Swagger-compliant resource lists and API declarations as defined here:
* https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md
*
* This formatter produces an array. Therefore output still needs to be `json_encode`d before passing on as HTTP response.
*
* @author Bezalel Hermoso <bezalelhermoso@gmail.com>
*/
class SwaggerFormatter implements FormatterInterface
{
protected $basePath;
protected $apiVersion;
protected $swaggerVersion;
protected $info = [];
protected $typeMap = [
DataTypes::INTEGER => 'integer',
DataTypes::FLOAT => 'number',
DataTypes::STRING => 'string',
DataTypes::BOOLEAN => 'boolean',
DataTypes::FILE => 'string',
DataTypes::DATE => 'string',
DataTypes::DATETIME => 'string',
];
protected $formatMap = [
DataTypes::INTEGER => 'int32',
DataTypes::FLOAT => 'float',
DataTypes::FILE => 'byte',
DataTypes::DATE => 'date',
DataTypes::DATETIME => 'date-time',
];
/**
* @var ModelRegistry
*/
protected $modelRegistry;
public function __construct($namingStategy)
{
$this->modelRegistry = new ModelRegistry($namingStategy);
}
/**
* @var array
*/
protected $authConfig;
public function setAuthenticationConfig(array $config): void
{
$this->authConfig = $config;
}
/**
* Format a collection of documentation data.
*
* If resource is provided, an API declaration for that resource is produced. Otherwise, a resource listing is returned.
*
* @param array|ApiDoc[] $collection
* @param string|null $resource
*
* @return string|array
*/
public function format(array $collection, $resource = null)
{
if (null === $resource) {
return $this->produceResourceListing($collection);
} else {
return $this->produceApiDeclaration($collection, $resource);
}
}
/**
* Formats the collection into Swagger-compliant output.
*
* @return array
*/
public function produceResourceListing(array $collection)
{
$resourceList = [
'swaggerVersion' => (string) $this->swaggerVersion,
'apis' => [],
'apiVersion' => (string) $this->apiVersion,
'info' => $this->getInfo(),
'authorizations' => $this->getAuthorizations(),
];
$apis = &$resourceList['apis'];
foreach ($collection as $item) {
/** @var $apiDoc ApiDoc */
$apiDoc = $item['annotation'];
$resource = $item['resource'];
if (!$apiDoc->isResource()) {
continue;
}
$subPath = $this->stripBasePath($resource);
$normalizedName = $this->normalizeResourcePath($subPath);
$apis[] = [
'path' => '/' . $normalizedName,
'description' => $apiDoc->getResourceDescription(),
];
}
return $resourceList;
}
protected function getAuthorizations()
{
$auth = [];
if (null === $this->authConfig) {
return $auth;
}
$config = $this->authConfig;
if ('http' === $config['delivery']) {
return $auth;
}
$auth['apiKey'] = [
'type' => 'apiKey',
'passAs' => $config['delivery'],
'keyname' => $config['name'],
];
return $auth;
}
/**
* @return array
*/
protected function getInfo()
{
return $this->info;
}
/**
* Format documentation data for one route.
*
* @param ApiDoc $annotation
* return string|array
*
* @throws \BadMethodCallException
*/
public function formatOne(ApiDoc $annotation): void
{
throw new \BadMethodCallException(sprintf('%s does not support formatting a single ApiDoc only.', __CLASS__));
}
/**
* Formats collection to produce a Swagger-compliant API declaration for the given resource.
*
* @param string $resource
*
* @return array
*/
protected function produceApiDeclaration(array $collection, $resource)
{
$apiDeclaration = [
'swaggerVersion' => (string) $this->swaggerVersion,
'apiVersion' => (string) $this->apiVersion,
'basePath' => $this->basePath,
'resourcePath' => $resource,
'apis' => [],
'models' => [],
'produces' => [],
'consumes' => [],
'authorizations' => $this->getAuthorizations(),
];
$main = null;
$apiBag = [];
foreach ($collection as $item) {
/** @var $apiDoc ApiDoc */
$apiDoc = $item['annotation'];
$itemResource = $this->stripBasePath($item['resource']);
$input = $apiDoc->getInput();
if (!is_array($input)) {
$input = [
'class' => $input,
'paramType' => 'form',
];
} elseif (empty($input['paramType'])) {
$input['paramType'] = 'form';
}
$route = $apiDoc->getRoute();
$itemResource = $this->normalizeResourcePath($itemResource);
if ('/' . $itemResource !== $resource) {
continue;
}
$compiled = $route->compile();
$path = $this->stripBasePath($route->getPath());
if (!isset($apiBag[$path])) {
$apiBag[$path] = [];
}
$parameters = [];
$responseMessages = [];
foreach ($compiled->getPathVariables() as $paramValue) {
$parameter = [
'paramType' => 'path',
'name' => $paramValue,
'type' => 'string',
'required' => true,
];
if ('_format' === $paramValue && false != ($req = $route->getRequirement('_format'))) {
$parameter['enum'] = explode('|', $req);
}
$parameters[] = $parameter;
}
$data = $apiDoc->toArray();
if (isset($data['filters'])) {
$parameters = array_merge($parameters, $this->deriveQueryParameters($data['filters']));
}
if (isset($data['parameters'])) {
$parameters = array_merge($parameters, $this->deriveParameters($data['parameters'], $input['paramType']));
}
$responseMap = $apiDoc->getParsedResponseMap();
$statusMessages = $data['statusCodes'] ?? [];
foreach ($responseMap as $statusCode => $prop) {
if (isset($statusMessages[$statusCode])) {
$message = is_array($statusMessages[$statusCode]) ? implode('; ', $statusMessages[$statusCode]) : $statusCode[$statusCode];
} else {
$message = sprintf('See standard HTTP status code reason for %s', $statusCode);
}
$className = !empty($prop['type']['form_errors']) ? $prop['type']['class'] . '.ErrorResponse' : $prop['type']['class'];
if (isset($prop['type']['collection']) && true === $prop['type']['collection']) {
/*
* Without alias: Fully\Qualified\Class\Name[]
* With alias: Fully\Qualified\Class\Name[alias]
*/
$alias = $prop['type']['collectionName'];
$newName = sprintf('%s[%s]', $className, $alias);
$collId =
$this->registerModel(
$newName,
[
$alias => [
'dataType' => null,
'subType' => $className,
'actualType' => DataTypes::COLLECTION,
'required' => true,
'readonly' => true,
'description' => null,
'default' => null,
'children' => $prop['model'][$alias]['children'],
],
],
''
);
$responseModel = [
'code' => $statusCode,
'message' => $message,
'responseModel' => $collId,
];
} else {
$responseModel = [
'code' => $statusCode,
'message' => $message,
'responseModel' => $this->registerModel($className, $prop['model'], ''),
];
}
$responseMessages[$statusCode] = $responseModel;
}
$unmappedMessages = array_diff(array_keys($statusMessages), array_keys($responseMessages));
foreach ($unmappedMessages as $code) {
$responseMessages[$code] = [
'code' => $code,
'message' => is_array($statusMessages[$code]) ? implode('; ', $statusMessages[$code]) : $statusMessages[$code],
];
}
$type = $responseMessages[200]['responseModel'] ?? null;
foreach ($apiDoc->getRoute()->getMethods() as $method) {
$operation = [
'method' => $method,
'summary' => $apiDoc->getDescription(),
'nickname' => $this->generateNickname($method, $itemResource),
'parameters' => $parameters,
'responseMessages' => array_values($responseMessages),
];
if (null !== $type) {
$operation['type'] = $type;
}
$apiBag[$path][] = $operation;
}
}
$apiDeclaration['resourcePath'] = $resource;
foreach ($apiBag as $path => $operations) {
$apiDeclaration['apis'][] = [
'path' => $path,
'operations' => $operations,
];
}
$apiDeclaration['models'] = $this->modelRegistry->getModels();
$this->modelRegistry->clear();
return $apiDeclaration;
}
/**
* Slugify a URL path. Trims out path parameters wrapped in curly brackets.
*
* @return string
*/
protected function normalizeResourcePath($path)
{
$path = preg_replace('/({.*?})/', '', $path);
$path = trim(preg_replace('/[^0-9a-zA-Z]/', '-', $path), '-');
$path = preg_replace('/-+/', '-', $path);
return $path;
}
public function setBasePath($path): void
{
$this->basePath = $path;
}
/**
* Formats query parameters to Swagger-compliant form.
*
* @return array
*/
protected function deriveQueryParameters(array $input)
{
$parameters = [];
foreach ($input as $name => $prop) {
if (!isset($prop['dataType'])) {
$prop['dataType'] = 'string';
}
$parameters[] = [
'paramType' => 'query',
'name' => $name,
'type' => $this->typeMap[$prop['dataType']] ?? 'string',
'description' => $prop['description'] ?? null,
];
}
return $parameters;
}
/**
* Builds a Swagger-compliant parameter list from the provided parameter array. Models are built when necessary.
*
* @param string $paramType
*
* @return array
*/
protected function deriveParameters(array $input, $paramType = 'form')
{
$parameters = [];
foreach ($input as $name => $prop) {
$type = null;
$format = null;
$ref = null;
$enum = null;
$items = null;
if (!isset($prop['actualType'])) {
$prop['actualType'] = 'string';
}
if (isset($this->typeMap[$prop['actualType']])) {
$type = $this->typeMap[$prop['actualType']];
} else {
switch ($prop['actualType']) {
case DataTypes::ENUM:
$type = 'string';
if (isset($prop['format'])) {
$enum = explode('|', rtrim(ltrim($prop['format'], '['), ']'));
}
break;
case DataTypes::MODEL:
$ref =
$this->registerModel(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
break;
case DataTypes::COLLECTION:
$type = 'array';
if (null === $prop['subType']) {
$items = ['type' => 'string'];
} elseif (isset($this->typeMap[$prop['subType']])) {
$items = ['type' => $this->typeMap[$prop['subType']]];
} else {
$ref =
$this->registerModel(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
$items = [
'$ref' => $ref,
];
}
break;
}
}
if (isset($this->formatMap[$prop['actualType']])) {
$format = $this->formatMap[$prop['actualType']];
}
if (null === $type && null === $ref) {
/* `type` or `$ref` is required. Continue to next of none of these was determined. */
continue;
}
$parameter = [
'paramType' => $paramType,
'name' => $name,
];
if (null !== $type) {
$parameter['type'] = $type;
}
if (null !== $ref) {
$parameter['$ref'] = $ref;
$parameter['type'] = $ref;
}
if (null !== $format) {
$parameter['format'] = $format;
}
if (is_array($enum) && count($enum) > 0) {
$parameter['enum'] = $enum;
}
if (isset($prop['default'])) {
$parameter['defaultValue'] = $prop['default'];
}
if (isset($items)) {
$parameter['items'] = $items;
}
if (isset($prop['description'])) {
$parameter['description'] = $prop['description'];
}
$parameters[] = $parameter;
}
return $parameters;
}
/**
* Registers a model into the model array. Returns a unique identifier for the model to be used in `$ref` properties.
*
* @param string $description
*
* @internal param $models
*/
public function registerModel($className, ?array $parameters = null, $description = '')
{
return $this->modelRegistry->register($className, $parameters, $description);
}
public function setSwaggerVersion($swaggerVersion): void
{
$this->swaggerVersion = $swaggerVersion;
}
public function setApiVersion($apiVersion): void
{
$this->apiVersion = $apiVersion;
}
public function setInfo($info): void
{
$this->info = $info;
}
/**
* Strips the base path from a URL path.
*/
protected function stripBasePath($basePath)
{
if ('/' === $this->basePath) {
return $basePath;
}
$path = sprintf('#^%s#', preg_quote($this->basePath));
$subPath = preg_replace($path, '', $basePath);
return $subPath;
}
/**
* Generate nicknames based on support HTTP methods and the resource name.
*
* @return string
*/
protected function generateNickname($method, $resource)
{
$resource = preg_replace('#/^#', '', $resource);
$resource = $this->normalizeResourcePath($resource);
return sprintf('%s_%s', strtolower($method ?: ''), $resource);
}
}

19
Makefile Normal file
View file

@ -0,0 +1,19 @@
ifneq (,$(shell (type docker-compose 2>&1 >/dev/null && echo 1) || true))
PHP=docker-compose run --rm --no-deps php
else
PHP=php
endif
PHP_CONSOLE_DEPS=vendor
vendor: composer.json
@$(PHP) composer install -o -n --no-ansi
@touch vendor || true
php-cs: $(PHP_CONSOLE_DEPS)
@$(PHP) vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --using-cache=no -v
phpunit: $(PHP_CONSOLE_DEPS)
@$(PHP) vendor/bin/phpunit --color=always
check: phpunit

View file

@ -2,20 +2,24 @@
namespace Nelmio\ApiDocBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Nelmio\ApiDocBundle\DependencyInjection\ExtractorHandlerCompilerPass;
use Nelmio\ApiDocBundle\DependencyInjection\FormInfoParserCompilerPass;
use Nelmio\ApiDocBundle\DependencyInjection\LoadExtractorParsersPass;
use Nelmio\ApiDocBundle\DependencyInjection\RegisterExtractorParsersPass;
use Nelmio\ApiDocBundle\DependencyInjection\ExtractorHandlerCompilerPass;
use Nelmio\ApiDocBundle\DependencyInjection\SwaggerConfigCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class NelmioApiDocBundle extends Bundle
{
public function build(ContainerBuilder $container)
public function build(ContainerBuilder $container): void
{
parent::build($container);
$container->addCompilerPass(new LoadExtractorParsersPass());
$container->addCompilerPass(new RegisterExtractorParsersPass());
$container->addCompilerPass(new ExtractorHandlerCompilerPass());
$container->addCompilerPass(new SwaggerConfigCompilerPass());
$container->addCompilerPass(new FormInfoParserCompilerPass());
}
}

View file

@ -0,0 +1,74 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Parser;
use Nelmio\ApiDocBundle\DataTypes;
/**
* Handles models that are specified as collections.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class CollectionParser implements ParserInterface, PostParserInterface
{
/**
* Return true/false whether this class supports parsing the given class.
*
* @param array $item containing the following fields: class, groups. Of which groups is optional
*
* @return bool
*/
public function supports(array $item)
{
return isset($item['collection']) && true === $item['collection'];
}
/**
* This doesn't parse anything at this stage.
*
* @return array
*/
public function parse(array $item)
{
return [];
}
/**
* @param array|string $item The string type of input to parse.
* @param array $parameters The previously-parsed parameters array.
*
* @return array
*/
public function postParse(array $item, array $parameters)
{
$origParameters = $parameters;
foreach ($parameters as $name => $body) {
$parameters[$name] = null;
}
$collectionName = $item['collectionName'] ?? '';
$parameters[$collectionName] = [
'dataType' => null, // Delegates to ApiDocExtractor#generateHumanReadableTypes
'subType' => $item['class'],
'actualType' => DataTypes::COLLECTION,
'readonly' => true,
'required' => true,
'default' => true,
'description' => '',
'children' => $origParameters,
];
return $parameters;
}
}

124
Parser/FormErrorsParser.php Normal file
View file

@ -0,0 +1,124 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Parser;
use Nelmio\ApiDocBundle\DataTypes;
/**
* @author Bez Hermoso <bezalelhermoso@gmail.com>
*/
class FormErrorsParser implements ParserInterface, PostParserInterface
{
/**
* Return true/false whether this class supports parsing the given class.
*
* @param array $item containing the following fields: class, groups. Of which groups is optional
*
* @return bool
*/
public function supports(array $item)
{
return isset($item['form_errors']) && true === $item['form_errors'];
}
public function parse(array $item)
{
return [];
}
/**
* Overrides the root parameters to contain these parameters instead:
* - status_code: 400
* - message: "Validation failed"
* - errors: contains the original parameters, but all types are changed to array of strings (array of errors for each field)
*
* @return array
*/
public function postParse(array $item, array $parameters)
{
$params = $parameters;
foreach ($params as $name => $data) {
$params[$name] = null;
}
$params['status_code'] = [
'dataType' => 'integer',
'actualType' => DataTypes::INTEGER,
'subType' => null,
'required' => false,
'description' => 'The status code',
'readonly' => true,
'default' => 400,
];
$params['message'] = [
'dataType' => 'string',
'actualType' => DataTypes::STRING,
'subType' => null,
'required' => false,
'description' => 'The error message',
'default' => 'Validation failed.',
];
$params['errors'] = [
'dataType' => 'errors',
'actualType' => DataTypes::MODEL,
'subType' => sprintf('%s.FormErrors', $item['class']),
'required' => false,
'description' => 'Errors',
'readonly' => true,
'children' => $this->doPostParse($parameters),
];
return $params;
}
protected function doPostParse(array $parameters, $attachFieldErrors = true, array $propertyPath = [])
{
$data = [];
foreach ($parameters as $name => $parameter) {
$data[$name] = [
'dataType' => 'parameter errors',
'actualType' => DataTypes::MODEL,
'subType' => 'FieldErrors',
'required' => false,
'description' => 'Errors on the parameter',
'readonly' => true,
'children' => [
'errors' => [
'dataType' => 'array of errors',
'actualType' => DataTypes::COLLECTION,
'subType' => 'string',
'required' => false,
'dscription' => '',
'readonly' => true,
],
],
];
if (DataTypes::MODEL === $parameter['actualType']) {
$propertyPath[] = $name;
$data[$name]['subType'] = sprintf('%s.FieldErrors[%s]', $parameter['subType'], implode('.', $propertyPath));
$data[$name]['children'] = $this->doPostParse($parameter['children'], $attachFieldErrors, $propertyPath);
} else {
if (false === $attachFieldErrors) {
unset($data[$name]['children']);
}
$attachFieldErrors = false;
}
}
return $data;
}
}

11
Parser/FormInfoParser.php Normal file
View file

@ -0,0 +1,11 @@
<?php
namespace Nelmio\ApiDocBundle\Parser;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormTypeInterface;
interface FormInfoParser
{
public function parseFormType(FormTypeInterface $type, FormConfigInterface $config): ?array;
}

View file

@ -11,56 +11,126 @@
namespace Nelmio\ApiDocBundle\Parser;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
use Symfony\Component\Form\FormFactoryInterface;
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\Form\ResolvedFormTypeInterface;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
use Symfony\Contracts\Translation\TranslatorInterface;
class FormTypeParser implements ParserInterface
{
/**
* @var \Symfony\Component\Form\FormFactoryInterface
* @var FormFactoryInterface
*/
protected $formFactory;
/**
* @var \Symfony\Component\Form\FormRegistry
* @var \Symfony\Component\Form\FormRegistry
*/
protected $formRegistry;
/**
* @var array
* @var \Symfony\Component\Translation\TranslatorInterface
*/
protected $mapTypes = array(
'text' => 'string',
'date' => 'date',
'datetime' => 'datetime',
'checkbox' => 'boolean',
'time' => 'time',
'number' => 'float',
'integer' => 'int',
'textarea' => 'string',
'country' => 'string',
'choice' => 'choice',
);
public function __construct(FormFactoryInterface $formFactory)
{
$this->formFactory = $formFactory;
}
protected $translator;
/**
* {@inheritdoc}
* @var bool
*/
protected $entityToChoice;
/**
* @var array|FormInfoParser[]
*/
protected $formInfoParsers = [];
/**
* @var array
*
* @deprecated since 2.12, to be removed in 3.0. Use $extendedMapTypes instead.
*/
protected $mapTypes = [
'text' => DataTypes::STRING,
'date' => DataTypes::DATE,
'datetime' => DataTypes::DATETIME,
'checkbox' => DataTypes::BOOLEAN,
'time' => DataTypes::TIME,
'number' => DataTypes::FLOAT,
'integer' => DataTypes::INTEGER,
'textarea' => DataTypes::STRING,
'country' => DataTypes::STRING,
'choice' => DataTypes::ENUM,
'file' => DataTypes::FILE,
];
/**
* @var array
*/
protected $extendedMapTypes = [
DataTypes::STRING => [
'text',
'Symfony\Component\Form\Extension\Core\Type\TextType',
'textarea',
'Symfony\Component\Form\Extension\Core\Type\TextareaType',
'country',
'Symfony\Component\Form\Extension\Core\Type\CountryType',
],
DataTypes::DATE => [
'date',
'Symfony\Component\Form\Extension\Core\Type\DateType',
],
DataTypes::DATETIME => [
'datetime',
'Symfony\Component\Form\Extension\Core\Type\DatetimeType',
],
DataTypes::BOOLEAN => [
'checkbox',
'Symfony\Component\Form\Extension\Core\Type\CheckboxType',
],
DataTypes::TIME => [
'time',
'Symfony\Component\Form\Extension\Core\Type\TimeType',
],
DataTypes::FLOAT => [
'number',
'Symfony\Component\Form\Extension\Core\Type\NumberType',
],
DataTypes::INTEGER => [
'integer',
'Symfony\Component\Form\Extension\Core\Type\IntegerType',
],
DataTypes::ENUM => [
'choice',
'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
],
DataTypes::FILE => [
'file',
'Symfony\Component\Form\Extension\Core\Type\FileType',
],
];
public function __construct(FormFactoryInterface $formFactory, TranslatorInterface $translator, $entityToChoice)
{
$this->formFactory = $formFactory;
$this->translator = $translator;
$this->entityToChoice = (bool) $entityToChoice;
}
public function supports(array $item)
{
$className = $item['class'];
$options = $item['options'];
try {
if ($this->createForm($className)) {
if ($this->createForm($className, null, $options)) {
return true;
}
} catch (FormException $e) {
@ -72,45 +142,158 @@ class FormTypeParser implements ParserInterface
return false;
}
/**
* {@inheritdoc}
*/
public function parse(array $item)
{
$type = $item['class'];
$options = $item['options'];
if ($this->implementsType($type)) {
$type = $this->getTypeInstance($type);
try {
$form = $this->formFactory->create($type, null, $options);
}
// TODO: find a better exception to catch
catch (\Exception $exception) {
if (!LegacyFormHelper::isLegacy()) {
@trigger_error('Using FormTypeInterface instance with required arguments without defining them as service is deprecated in symfony 2.8 and removed in 3.0.', E_USER_DEPRECATED);
}
}
$form = $this->formFactory->create($type);
if (!isset($form)) {
if (!LegacyFormHelper::hasBCBreaks() && $this->implementsType($type)) {
$type = $this->getTypeInstance($type);
$form = $this->formFactory->create($type, null, $options);
} else {
throw new \InvalidArgumentException('Unsupported form type class.');
}
}
return $this->parseForm($form, array_key_exists('name', $item) ? $item['name'] : $form->getName());
$name = array_key_exists('name', $item)
? $item['name']
: (method_exists($form, 'getBlockPrefix') ? $form->getBlockPrefix() : $form->getName());
if (empty($name)) {
return $this->parseForm($form);
}
$subType = is_object($type) ? $type::class : $type;
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
$dataType = sprintf('object (%s)', end($parts));
} else {
$dataType = sprintf('object (%s)', $subType);
}
return [
$name => [
'required' => true,
'readonly' => false,
'description' => '',
'default' => null,
'dataType' => $dataType,
'actualType' => DataTypes::MODEL,
'subType' => $subType,
'children' => $this->parseForm($form),
],
];
}
private function parseForm($form, $prefix = null)
public function addFormInfoParser(FormInfoParser $formInfoParser): void
{
$parameters = array();
$class = $formInfoParser::class;
if (isset($this->formInfoParsers[$class])) {
throw new \InvalidArgumentException($class . ' already added');
}
$this->formInfoParsers[$class] = $formInfoParser;
}
private function parseFormType(FormTypeInterface $type, FormConfigInterface $config): ?array
{
foreach ($this->formInfoParsers as $parser) {
$customInfo = $parser->parseFormType($type, $config);
if ($customInfo) {
return $customInfo;
}
}
return null;
}
private function getDataType($type)
{
foreach ($this->extendedMapTypes as $data => $types) {
if (in_array($type, $types)) {
return $data;
}
}
}
private function parseForm($form)
{
$parameters = [];
$domain = $form->getConfig()->getOption('translation_domain');
foreach ($form as $name => $child) {
$config = $child->getConfig();
if ($prefix) {
$name = sprintf('%s[%s]', $prefix, $name);
}
$options = $config->getOptions();
$bestType = '';
for ($type = $config->getType(); null !== $type; $type = $type->getParent()) {
if (isset($this->mapTypes[$type->getName()])) {
$bestType = $this->mapTypes[$type->getName()];
} elseif ('collection' === $type->getName()) {
if (is_string($config->getOption('type')) && isset($this->mapTypes[$config->getOption('type')])) {
$bestType = sprintf('array of %ss', $this->mapTypes[$config->getOption('type')]);
$actualType = null;
$subType = null;
$children = null;
for ($type = $config->getType();
$type instanceof FormInterface || $type instanceof ResolvedFormTypeInterface;
$type = $type->getParent()
) {
$customInfo = $this->parseFormType($type->getInnerType(), $config);
if ($customInfo) {
$parameters[$name] = array_merge([
'dataType' => 'string',
'actualType' => 'string',
'default' => $config->getData(),
'required' => $config->getRequired(),
'description' => $this->getFormDescription($config, $domain),
'readonly' => $config->getDisabled(),
], $customInfo);
continue 2;
}
$typeName = method_exists($type, 'getBlockPrefix') ?
$type->getBlockPrefix() : $type->getName();
$dataType = $this->getDataType($typeName);
if (null !== $dataType) {
$actualType = $bestType = $dataType;
} elseif ('collection' === $typeName) {
// BC sf < 2.8
$typeOption = $config->hasOption('entry_type') ? $config->getOption('entry_type') : $config->getOption('type');
if (is_object($typeOption)) {
$typeOption = method_exists($typeOption, 'getBlockPrefix') ?
$typeOption->getBlockPrefix() : $typeOption->getName();
}
$dataType = $this->getDataType($typeOption);
if (null !== $dataType) {
$subType = $dataType;
$actualType = DataTypes::COLLECTION;
$bestType = sprintf('array of %ss', $subType);
} else {
// Embedded form collection
$subParameters = $this->parseForm($this->formFactory->create($config->getOption('type'), null, $config->getOption('options', array())), $name . '[]');
$parameters = array_merge($parameters, $subParameters);
// BC sf < 2.8
$embbededType = $config->hasOption('entry_type') ? $config->getOption('entry_type') : $config->getOption('type');
$subForm = $this->formFactory->create($embbededType, null, $config->getOption('entry_options', []));
$children = $this->parseForm($subForm);
$actualType = DataTypes::COLLECTION;
$subType = is_object($embbededType) ? $embbededType::class : $embbededType;
continue 2;
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
$bestType = sprintf('array of objects (%s)', end($parts));
} else {
$bestType = sprintf('array of objects (%s)', $subType);
}
}
}
}
@ -125,10 +308,39 @@ class FormTypeParser implements ParserInterface
*/
$addDefault = false;
try {
$subForm = $this->formFactory->create($type);
if (isset($subForm)) {
unset($subForm);
}
if (LegacyFormHelper::hasBCBreaks()) {
try {
$subForm = $this->formFactory->create($type::class, null, $options);
} catch (\Exception $e) {
}
}
if (!isset($subForm)) {
$subForm = $this->formFactory->create($type, null, $options);
}
$subParameters = $this->parseForm($subForm, $name);
if (!empty($subParameters)) {
$parameters = array_merge($parameters, $subParameters);
$children = $subParameters;
$config = $subForm->getConfig();
$subType = $type::class;
$parts = explode('\\', $subType);
$bestType = sprintf('object (%s)', end($parts));
$parameters[$name] = [
'dataType' => $bestType,
'actualType' => DataTypes::MODEL,
'default' => null,
'subType' => $subType,
'required' => $config->getRequired(),
'description' => $this->getFormDescription($config, $domain),
'readonly' => $config->getDisabled(),
'children' => $children,
];
} else {
$addDefault = true;
}
@ -137,12 +349,14 @@ class FormTypeParser implements ParserInterface
}
if ($addDefault) {
$parameters[$name] = array(
'dataType' => 'string',
'required' => $config->getRequired(),
'description' => $config->getAttribute('description'),
'readonly' => $config->getDisabled(),
);
$parameters[$name] = [
'dataType' => 'string',
'actualType' => 'string',
'default' => $config->getData(),
'required' => $config->getRequired(),
'description' => $this->getFormDescription($config, $domain),
'readonly' => $config->getDisabled(),
];
}
continue;
@ -150,12 +364,19 @@ class FormTypeParser implements ParserInterface
}
}
$parameters[$name] = array(
'dataType' => $bestType,
'required' => $config->getRequired(),
'description' => $config->getAttribute('description'),
'readonly' => $config->getDisabled(),
);
$parameters[$name] = [
'dataType' => $bestType,
'actualType' => $actualType,
'subType' => $subType,
'default' => $config->getData(),
'required' => $config->getRequired(),
'description' => $this->getFormDescription($config, $domain),
'readonly' => $config->getDisabled(),
];
if (null !== $children) {
$parameters[$name]['children'] = $children;
}
switch ($bestType) {
case 'datetime':
@ -175,12 +396,29 @@ class FormTypeParser implements ParserInterface
case 'choice':
if ($config->getOption('multiple')) {
$parameters[$name]['dataType'] = sprintf('array of %ss', $parameters[$name]['dataType']);
$parameters[$name]['actualType'] = DataTypes::COLLECTION;
$parameters[$name]['subType'] = DataTypes::ENUM;
}
if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) {
$parameters[$name]['format'] = json_encode($choices);
} elseif (($choiceList = $config->getOption('choice_list')) && $choiceList instanceof ChoiceListInterface) {
$choices = $this->handleChoiceListValues($choiceList);
$choices = $config->getOption('choices_as_values') ?
array_values($choices) :
array_keys($choices);
sort($choices);
$parameters[$name]['format'] = '[' . implode('|', $choices) . ']';
} elseif ($choiceList = $config->getOption('choice_list')) {
$choiceListType = $config->getType();
$choiceListName = method_exists($choiceListType, 'getBlockPrefix') ?
$choiceListType->getBlockPrefix() : $choiceListType->getName();
if ('entity' === $choiceListName && false === $this->entityToChoice) {
$choices = [];
} else {
// TODO: fixme
// does not work since: https://github.com/symfony/symfony/commit/03efce1b568379eac21d880e427090e43035f505
$choices = [];
}
if (is_array($choices) && count($choices)) {
$parameters[$name]['format'] = json_encode($choices);
}
@ -194,39 +432,46 @@ class FormTypeParser implements ParserInterface
private function implementsType($item)
{
if (!class_exists($item)) {
if (null === $item || !class_exists($item)) {
return false;
}
$refl = new \ReflectionClass($item);
return $refl->implementsInterface('Symfony\Component\Form\FormTypeInterface');
return $refl->implementsInterface('Symfony\Component\Form\FormTypeInterface') || $refl->implementsInterface('Symfony\Component\Form\ResolvedFormTypeInterface');
}
private function getTypeInstance($type)
{
return unserialize(sprintf('O:%d:"%s":0:{}', strlen($type), $type));
$refl = new \ReflectionClass($type);
$constructor = $refl->getConstructor();
// this fallback may lead to runtime exception, but try hard to generate the docs
if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) {
return $refl->newInstanceWithoutConstructor();
}
return $refl->newInstance();
}
private function createForm($item)
private function createForm($type, $data = null, array $options = [])
{
if ($this->implementsType($item)) {
$type = $this->getTypeInstance($item);
return $this->formFactory->create($type);
}
try {
return $this->formFactory->create($item);
} catch (UnexpectedTypeException $e) {
// nothing
} catch (InvalidArgumentException $e) {
// nothing
return $this->formFactory->create($type, null, $options);
} catch (InvalidArgumentException $exception) {
}
if (!LegacyFormHelper::hasBCBreaks() && !isset($form) && $this->implementsType($type)) {
$type = $this->getTypeInstance($type);
return $this->formFactory->create($type, null, $options);
}
}
private function handleChoiceListValues(ChoiceListInterface $choiceList)
{
$choices = array();
foreach (array($choiceList->getPreferredViews(), $choiceList->getRemainingViews()) as $viewList) {
$choices = [];
foreach ([$choiceList->getPreferredViews(), $choiceList->getRemainingViews()] as $viewList) {
$choices = array_merge($choices, $this->handleChoiceViewsHierarchy($viewList));
}
@ -235,7 +480,7 @@ class FormTypeParser implements ParserInterface
private function handleChoiceViewsHierarchy(array $choiceViews)
{
$choices = array();
$choices = [];
foreach ($choiceViews as $item) {
if ($item instanceof ChoiceView) {
$choices[$item->value] = $item->label;
@ -246,4 +491,16 @@ class FormTypeParser implements ParserInterface
return $choices;
}
private function getFormDescription($config, $domain = null)
{
$description = ($config->getOption('description'))
?: $config->getOption('label');
if (null != $description) {
return $this->translator->trans($description, [], $domain);
}
return null;
}
}

View file

@ -12,20 +12,21 @@
namespace Nelmio\ApiDocBundle\Parser;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\SerializationContext;
use Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\SerializationContext;
use Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
/**
* Uses the JMS metadata factory to extract input/output model information
*/
class JmsMetadataParser implements ParserInterface
class JmsMetadataParser implements ParserInterface, PostParserInterface
{
/**
* @var \Metadata\MetadataFactoryInterface
* @var MetadataFactoryInterface
*/
private $factory;
@ -35,26 +36,33 @@ class JmsMetadataParser implements ParserInterface
private $namingStrategy;
/**
* @var \Nelmio\ApiDocBundle\Util\DocCommentExtractor
* @var DocCommentExtractor
*/
private $commentExtractor;
private $typeMap = [
'integer' => DataTypes::INTEGER,
'boolean' => DataTypes::BOOLEAN,
'string' => DataTypes::STRING,
'float' => DataTypes::FLOAT,
'double' => DataTypes::FLOAT,
'array' => DataTypes::COLLECTION,
'DateTime' => DataTypes::DATETIME,
];
/**
* Constructor, requires JMS Metadata factory
*/
public function __construct(
MetadataFactoryInterface $factory,
PropertyNamingStrategyInterface $namingStrategy,
DocCommentExtractor $commentExtractor
DocCommentExtractor $commentExtractor,
) {
$this->factory = $factory;
$this->namingStrategy = $namingStrategy;
$this->commentExtractor = $commentExtractor;
}
/**
* {@inheritdoc}
*/
public function supports(array $input)
{
$className = $input['class'];
@ -69,44 +77,75 @@ class JmsMetadataParser implements ParserInterface
return false;
}
/**
* {@inheritdoc}
*/
public function parse(array $input)
{
$className = $input['class'];
$groups = $input['groups'];
$groups = $input['groups'];
return $this->doParse($className, array(), $groups);
$result = $this->doParse($className, [], $groups);
if (!isset($input['name']) || empty($input['name'])) {
return $result;
}
if ($className && class_exists($className)) {
$parts = explode('\\', $className);
$dataType = sprintf('object (%s)', end($parts));
} else {
$dataType = sprintf('object (%s)', $className);
}
return [
$input['name'] => [
'required' => null,
'readonly' => null,
'default' => null,
'dataType' => $dataType,
'actualType' => DataTypes::MODEL,
'subType' => $dataType,
'children' => $result,
],
];
}
/**
* 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
* @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())
protected function doParse($className, $visited = [], array $groups = [])
{
$meta = $this->factory->getMetadataForClass($className);
if (null === $meta) {
throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className));
throw new \InvalidArgumentException(sprintf('No metadata found for class %s', $className));
}
$exclusionStrategies = array();
$exclusionStrategies = [];
if ($groups) {
$exclusionStrategies[] = new GroupsExclusionStrategy($groups);
}
$params = array();
$params = [];
$reflection = new \ReflectionClass($className);
$defaultProperties = array_map(function ($default) {
if (is_array($default) && 0 === count($default)) {
return null;
}
return $default;
}, $reflection->getDefaultProperties());
// iterate over property metadata
foreach ($meta->propertyMetadata as $item) {
if (!is_null($item->type)) {
if (null !== $item->type) {
$name = $this->namingStrategy->translateName($item);
$dataType = $this->processDataType($item);
@ -118,18 +157,28 @@ class JmsMetadataParser implements ParserInterface
}
}
$params[$name] = array(
'dataType' => $dataType['normalized'],
'required' => false,
//TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
'description' => $this->getDescription($item),
'readonly' => $item->readOnly,
'sinceVersion' => $item->sinceVersion,
'untilVersion' => $item->untilVersion,
);
if (!$dataType['inline']) {
$params[$name] = [
'dataType' => $dataType['normalized'],
'actualType' => $dataType['actualType'],
'subType' => $dataType['class'],
'required' => false,
'default' => $defaultProperties[$item->name] ?? null,
// TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
'description' => $this->getDescription($item),
'readonly' => $item->readOnly,
'sinceVersion' => $item->sinceVersion,
'untilVersion' => $item->untilVersion,
];
if (!is_null($dataType['class'])) {
$params[$name]['class'] = $dataType['class'];
if (null !== $dataType['class'] && false === $dataType['primitive']) {
$params[$name]['class'] = $dataType['class'];
}
}
// we can use type property also for custom handlers, then we don't have here real class name
if (!$dataType['class'] || !class_exists($dataType['class'])) {
continue;
}
// if class already parsed, continue, to avoid infinite recursion
@ -138,9 +187,15 @@ class JmsMetadataParser implements ParserInterface
}
// check for nested classes with JMS metadata
if ($dataType['class'] && null !== $this->factory->getMetadataForClass($dataType['class'])) {
$visited[] = $dataType['class'];
$params[$name]['children'] = $this->doParse($dataType['class'], $visited, $groups);
if ($dataType['class'] && false === $dataType['primitive'] && null !== $this->factory->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;
}
}
}
}
@ -152,7 +207,6 @@ class JmsMetadataParser implements ParserInterface
* Figure out a normalized data type (for documentation), and get a
* nested class name, if available.
*
* @param PropertyMetadata $type
* @return array
*/
protected function processDataType(PropertyMetadata $item)
@ -160,62 +214,105 @@ class JmsMetadataParser implements ParserInterface
// check for a type inside something that could be treated as an array
if ($nestedType = $this->getNestedTypeInArray($item)) {
if ($this->isPrimitive($nestedType)) {
return array(
'normalized' => sprintf("array of %ss", $nestedType),
'class' => null
);
return [
'normalized' => sprintf('array of %ss', $nestedType),
'actualType' => DataTypes::COLLECTION,
'class' => $this->typeMap[$nestedType],
'primitive' => true,
'inline' => false,
];
}
$exp = explode("\\", $nestedType);
$exp = explode('\\', $nestedType);
return array(
'normalized' => sprintf("array of objects (%s)", end($exp)),
'class' => $nestedType
);
return [
'normalized' => sprintf('array of objects (%s)', end($exp)),
'actualType' => DataTypes::COLLECTION,
'class' => $nestedType,
'primitive' => false,
'inline' => false,
];
}
$type = $item->type['name'];
// could be basic type
if ($this->isPrimitive($type)) {
return array(
return [
'normalized' => $type,
'class' => null
);
'actualType' => $this->typeMap[$type],
'class' => null,
'primitive' => true,
'inline' => false,
];
}
// we can use type property also for custom handlers, then we don't have here real class name
if (!class_exists($type)) {
return array(
'normalized' => sprintf("custom handler result for (%s)", $type),
'class' => null
);
if (!$type || !class_exists($type)) {
return [
'normalized' => sprintf('custom handler result for (%s)', $type),
'class' => $type,
'actualType' => DataTypes::MODEL,
'primitive' => false,
'inline' => false,
];
}
// if we got this far, it's a general class name
$exp = explode("\\", $type);
$exp = explode('\\', $type);
return array(
'normalized' => sprintf("object (%s)", end($exp)),
'class' => $type
);
return [
'normalized' => sprintf('object (%s)', end($exp)),
'class' => $type,
'actualType' => DataTypes::MODEL,
'primitive' => false,
'inline' => $item->inline,
];
}
protected function isPrimitive($type)
{
return in_array($type, array('boolean', 'integer', 'string', 'float', 'double', 'array', 'DateTime'));
return in_array($type, ['boolean', 'integer', 'string', 'float', 'double', 'array', 'DateTime']);
}
public function postParse(array $input, array $parameters)
{
return $this->doPostParse($parameters, [], $input['groups'] ?? []);
}
/**
* Recursive `doPostParse` to avoid circular post parsing.
*
* @return array
*/
protected function doPostParse(array $parameters, array $visited = [], array $groups = [])
{
foreach ($parameters as $param => $data) {
if (isset($data['class']) && isset($data['children']) && !in_array($data['class'], $visited)) {
$visited[] = $data['class'];
$input = ['class' => $data['class'], 'groups' => $data['groups'] ?? []];
$parameters[$param]['children'] = array_merge(
$parameters[$param]['children'], $this->doPostParse($parameters[$param]['children'], $visited, $groups)
);
$parameters[$param]['children'] = array_merge(
$parameters[$param]['children'], $this->doParse($input['class'], $visited, $groups)
);
}
}
return $parameters;
}
/**
* 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['name']) && in_array($item->type['name'], ['array', 'ArrayCollection'])) {
if (isset($item->type['params'][1]['name'])) {
// E.g. array<string, MyNamespaceMyObject>
return $item->type['params'][1]['name'];

View file

@ -0,0 +1,90 @@
<?php
/**
* Created by mcfedr on 30/06/15 21:03
*/
namespace Nelmio\ApiDocBundle\Parser;
class JsonSerializableParser implements ParserInterface
{
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;
}
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 = [];
$output[$input['name']] = $parsed;
return $output;
}
return $parsed['children'];
}
public function getItemMetaData($item)
{
$type = gettype($item);
$meta = [
'dataType' => 'NULL' == $type ? null : $type,
'actualType' => $type,
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null,
'default' => is_scalar($item) ? $item : null,
];
if ('object' == $type && $item instanceof \JsonSerializable) {
$meta = $this->getItemMetaData($item->jsonSerialize());
$meta['class'] = $item::class;
} elseif (('object' == $type && $item instanceof \stdClass) || ('array' == $type && !$this->isSequential($item))) {
$meta['dataType'] = 'object';
$meta['children'] = [];
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
*
* @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

@ -19,8 +19,9 @@ interface ParserInterface
/**
* Return true/false whether this class supports parsing the given class.
*
* @param array $item containing the following fields: class, groups. Of which groups is optional
* @return boolean
* @param array $item containing the following fields: class, groups. Of which groups is optional
*
* @return bool
*/
public function supports(array $item);
@ -36,7 +37,8 @@ interface ParserInterface
* - class (optional) the fully-qualified class name of the item, if
* it is represented by an object
*
* @param string $item The string type of input to parse.
* @param array $item The string type of input to parse.
*
* @return array
*/
public function parse(array $item);

View file

@ -30,8 +30,9 @@ interface PostParserInterface
* - children (optional) array of nested property names mapped to arrays
* in the format described here
*
* @param string $item The string type of input to parse.
* @param array $parameters The previously-parsed parameters array.
* @param string $item The string type of input to parse.
* @param array $parameters The previously-parsed parameters array.
*
* @return array
*/
public function postParse(array $item, array $parameters);

View file

@ -11,10 +11,12 @@
namespace Nelmio\ApiDocBundle\Parser;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\DataTypes;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
use Symfony\Component\Validator\MetadataFactoryInterface as LegacyMetadataFactoryInterface;
/**
* Uses the Symfony Validation component to extract information about API objects.
@ -22,23 +24,38 @@ use Symfony\Component\Validator\Constraints\Type;
class ValidationParser implements ParserInterface, PostParserInterface
{
/**
* @var \Symfony\Component\Validator\MetadataFactoryInterface
* @var LegacyMetadataFactoryInterface
*/
protected $factory;
protected $typeMap = [
'integer' => DataTypes::INTEGER,
'int' => DataTypes::INTEGER,
'scalar' => DataTypes::STRING,
'numeric' => DataTypes::INTEGER,
'boolean' => DataTypes::BOOLEAN,
'string' => DataTypes::STRING,
'float' => DataTypes::FLOAT,
'double' => DataTypes::FLOAT,
'long' => DataTypes::INTEGER,
'object' => DataTypes::MODEL,
'array' => DataTypes::COLLECTION,
'DateTime' => DataTypes::DATETIME,
];
/**
* Requires a validation MetadataFactory.
*
* @param MetadataFactoryInterface $factory
* @param MetadataFactoryInterface|LegacyMetadataFactoryInterface $factory
*/
public function __construct(MetadataFactoryInterface $factory)
public function __construct($factory)
{
if (!($factory instanceof MetadataFactoryInterface) && !($factory instanceof LegacyMetadataFactoryInterface)) {
throw new \InvalidArgumentException('Argument 1 of %s constructor must be either an instance of Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface or Symfony\Component\Validator\MetadataFactoryInterface.');
}
$this->factory = $factory;
}
/**
* {@inheritdoc}
*/
public function supports(array $input)
{
$className = $input['class'];
@ -46,45 +63,75 @@ class ValidationParser implements ParserInterface, PostParserInterface
return $this->factory->hasMetadataFor($className);
}
/**
* {@inheritdoc}
*/
public function parse(array $input)
{
$className = $input['class'];
return $this->doParse($className, array());
$groups = [];
if (isset($input['groups']) && $input['groups']) {
$groups = $input['groups'];
}
$parsed = $this->doParse($className, [], $groups);
if (!isset($input['name']) || empty($input['name'])) {
return $parsed;
}
if ($className && class_exists($className)) {
$parts = explode('\\', $className);
$dataType = sprintf('object (%s)', end($parts));
} else {
$dataType = sprintf('object (%s)', $className);
}
return [
$input['name'] => [
'dataType' => $dataType,
'actualType' => DataTypes::MODEL,
'class' => $className,
'subType' => $dataType,
'required' => null,
'readonly' => null,
'children' => $parsed,
'default' => null,
],
];
}
/**
* Recursively parse constraints.
*
* @param $className
* @param array $visited
* @return array
*/
protected function doParse($className, array $visited)
protected function doParse($className, array $visited, array $groups = [])
{
$params = array();
$params = [];
$classdata = $this->factory->getMetadataFor($className);
$properties = $classdata->getConstrainedProperties();
$refl = $classdata->getReflectionClass();
$defaults = $refl->getDefaultProperties();
foreach ($properties as $property) {
$vparams = array();
$vparams = [];
$vparams['default'] = $defaults[$property] ?? null;
$pds = $classdata->getPropertyMetadata($property);
foreach ($pds as $propdata) {
$constraints = $propdata->getConstraints();
foreach ($constraints as $constraint) {
$vparams = $this->parseConstraint($constraint, $vparams, $className, $visited);
$vparams = $this->parseConstraint($constraint, $vparams, $className, $visited, $groups);
}
}
if (isset($vparams['format'])) {
$vparams['format'] = join(', ', $vparams['format']);
$vparams['format'] = implode(', ', array_unique($vparams['format']));
}
foreach (array('dataType', 'readonly', 'required') as $reqprop) {
foreach (['dataType', 'readonly', 'required', 'subType'] as $reqprop) {
if (!isset($vparams[$reqprop])) {
$vparams[$reqprop] = null;
}
@ -93,23 +140,27 @@ class ValidationParser implements ParserInterface, PostParserInterface
// check for nested classes with All constraint
if (isset($vparams['class']) && !in_array($vparams['class'], $visited) && null !== $this->factory->getMetadataFor($vparams['class'])) {
$visited[] = $vparams['class'];
$vparams['children'] = $this->doParse($vparams['class'], $visited);
$vparams['children'] = $this->doParse($vparams['class'], $visited, $groups);
}
$vparams['actualType'] = $vparams['actualType'] ?? DataTypes::STRING;
$params[$property] = $vparams;
}
return $params;
}
/**
* {@inheritDoc}
*/
public function postParse(array $input, array $parameters)
{
$groups = [];
if (isset($input['groups']) && $input['groups']) {
$groups = $input['groups'];
}
foreach ($parameters as $param => $data) {
if (isset($data['class']) && isset($data['children'])) {
$input = array('class' => $data['class']);
$input = ['class' => $data['class'], 'groups' => $groups];
$parameters[$param]['children'] = array_merge(
$parameters[$param]['children'], $this->postParse($input, $parameters[$param]['children'])
);
@ -134,21 +185,48 @@ class ValidationParser implements ParserInterface, PostParserInterface
* - Choice (single and multiple, min and max)
* - Regex (match and non-match)
*
* @param Constraint $constraint The constraint metadata object.
* @param array $vparams The existing validation parameters.
* @return mixed The parsed list of validation parameters.
* @param Constraint $constraint The constraint metadata object.
* @param array $vparams The existing validation parameters.
* @param array $groups Validation groups.
*
* @return mixed The parsed list of validation parameters.
*/
protected function parseConstraint(Constraint $constraint, $vparams, $className, &$visited = array())
{
$class = substr(get_class($constraint), strlen('Symfony\\Component\\Validator\\Constraints\\'));
protected function parseConstraint(
Constraint $constraint,
$vparams,
$className,
&$visited = [],
array $groups = [],
) {
$class = substr($constraint::class, strlen('Symfony\\Component\\Validator\\Constraints\\'));
$vparams['actualType'] = DataTypes::STRING;
$vparams['subType'] = null;
$vparams['groups'] = $constraint->groups;
if ($groups) {
$containGroup = false;
foreach ($groups as $group) {
if (in_array($group, $vparams['groups'])) {
$containGroup = true;
}
}
if (!$containGroup) {
return $vparams;
}
}
switch ($class) {
case 'NotBlank':
$vparams['format'][] = '{not blank}';
// no break
case 'NotNull':
$vparams['required'] = true;
break;
case 'Type':
if (isset($this->typeMap[$constraint->type])) {
$vparams['actualType'] = $this->typeMap[$constraint->type];
}
$vparams['dataType'] = $constraint->type;
break;
case 'Email':
@ -162,36 +240,53 @@ class ValidationParser implements ParserInterface, PostParserInterface
break;
case 'Date':
$vparams['format'][] = '{Date YYYY-MM-DD}';
$vparams['actualType'] = DataTypes::DATE;
break;
case 'DateTime':
$vparams['format'][] = '{DateTime YYYY-MM-DD HH:MM:SS}';
$vparams['actualType'] = DataTypes::DATETIME;
break;
case 'Time':
$vparams['format'][] = '{Time HH:MM:SS}';
$vparams['actualType'] = DataTypes::TIME;
break;
case 'Range':
$messages = [];
if (isset($constraint->min)) {
$messages[] = ">={$constraint->min}";
}
if (isset($constraint->max)) {
$messages[] = "<={$constraint->max}";
}
$vparams['format'][] = '{range: {' . implode(', ', $messages) . '}}';
break;
case 'Length':
$messages = array();
$messages = [];
if (isset($constraint->min)) {
$messages[] = "min: {$constraint->min}";
}
if (isset($constraint->max)) {
$messages[] = "max: {$constraint->max}";
}
$vparams['format'][] = '{length: ' . join(', ', $messages) . '}';
$vparams['format'][] = '{length: {' . implode(', ', $messages) . '}}';
break;
case 'Choice':
$choices = $this->getChoices($constraint, $className);
$format = '[' . join('|', $choices) . ']';
sort($choices);
$format = '[' . implode('|', $choices) . ']';
if ($constraint->multiple) {
$messages = array();
$vparams['actualType'] = DataTypes::COLLECTION;
$vparams['subType'] = DataTypes::ENUM;
$messages = [];
if (isset($constraint->min)) {
$messages[] = "min: {$constraint->min} ";
}
if (isset($constraint->max)) {
$messages[] = "max: {$constraint->max} ";
}
$vparams['format'][] = '{' . join ('', $messages) . 'choice of ' . $format . '}';
$vparams['format'][] = '{' . implode('', $messages) . 'choice of ' . $format . '}';
} else {
$vparams['actualType'] = DataTypes::ENUM;
$vparams['format'][] = $format;
}
break;
@ -206,16 +301,18 @@ class ValidationParser implements ParserInterface, PostParserInterface
foreach ($constraint->constraints as $childConstraint) {
if ($childConstraint instanceof Type) {
$nestedType = $childConstraint->type;
$exp = explode("\\", $nestedType);
if (!class_exists($nestedType)) {
$nestedType = substr($className, 0, strrpos($className, '\\') + 1).$nestedType;
$exp = explode('\\', $nestedType);
if (!$nestedType || !class_exists($nestedType)) {
$nestedType = substr($className, 0, strrpos($className, '\\') + 1) . $nestedType;
if (!class_exists($nestedType)) {
continue;
}
}
$vparams['dataType'] = sprintf("array of objects (%s)", end($exp));
$vparams['dataType'] = sprintf('array of objects (%s)', end($exp));
$vparams['actualType'] = DataTypes::COLLECTION;
$vparams['subType'] = $nestedType;
$vparams['class'] = $nestedType;
if (!in_array($nestedType, $visited)) {
@ -233,16 +330,15 @@ class ValidationParser implements ParserInterface, PostParserInterface
/**
* Return Choice constraint choices.
*
* @param Constraint $constraint
* @param $className
* @return array
* @throws \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*
* @throws ConstraintDefinitionException
*/
protected function getChoices(Constraint $constraint, $className)
{
if ($constraint->callback) {
if (is_callable(array($className, $constraint->callback))) {
$choices = call_user_func(array($className, $constraint->callback));
if (is_callable([$className, $constraint->callback])) {
$choices = call_user_func([$className, $constraint->callback]);
} elseif (is_callable($constraint->callback)) {
$choices = call_user_func($constraint->callback);
} else {

View file

@ -12,7 +12,6 @@
namespace Nelmio\ApiDocBundle\Parser;
use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface;
use Symfony\Component\Validator\Constraint;
/**
* Uses the Symfony Validation component to extract information about API objects. This is a backwards-compatible Validation component for Symfony2.1
@ -20,7 +19,7 @@ use Symfony\Component\Validator\Constraint;
class ValidationParserLegacy extends ValidationParser
{
/**
* @var \Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface
* @var ClassMetadataFactoryInterface
*/
protected $factory;
@ -34,9 +33,6 @@ class ValidationParserLegacy extends ValidationParser
$this->factory = $factory;
}
/**
* {@inheritdoc}
*/
public function supports(array $input)
{
$className = $input['class'];
@ -44,20 +40,22 @@ class ValidationParserLegacy extends ValidationParser
return null !== $this->factory->getClassMetadata($className);
}
/**
* {@inheritdoc}
*/
public function parse(array $input)
{
$params = array();
$params = [];
$className = $input['class'];
$classdata = $this->factory->getClassMetadata($className);
$properties = $classdata->getConstrainedProperties();
$refl = $classdata->getReflectionClass();
$defaults = $refl->getDefaultProperties();
foreach ($properties as $property) {
$vparams = array();
$vparams = [];
$vparams['default'] = $defaults[$property] ?? null;
$pds = $classdata->getMemberMetadatas($property);
@ -70,10 +68,10 @@ class ValidationParserLegacy extends ValidationParser
}
if (isset($vparams['format'])) {
$vparams['format'] = join(', ', $vparams['format']);
$vparams['format'] = implode(', ', $vparams['format']);
}
foreach (array('dataType', 'readonly', 'required') as $reqprop) {
foreach (['dataType', 'readonly', 'required'] as $reqprop) {
if (!isset($vparams[$reqprop])) {
$vparams[$reqprop] = null;
}
@ -84,5 +82,4 @@ class ValidationParserLegacy extends ValidationParser
return $params;
}
}

View file

@ -1,25 +1,13 @@
NelmioApiDocBundle
==================
[![Build
Status](https://secure.travis-ci.org/nelmio/NelmioApiDocBundle.png?branch=master)](http://travis-ci.org/nelmio/NelmioApiDocBundle)
The **NelmioApiDocBundle** bundle allows you to generate a decent documentation
for your APIs.
**Important:** This bundle is developed in sync with [symfony's
repository](https://github.com/symfony/symfony).
For Symfony `2.0.x`, you need to use the `1.*` version of the bundle.
Documentation
-------------
For documentation, see:
Resources/doc/
[Read the documentation](https://github.com/nelmio/NelmioApiDocBundle/blob/master/Resources/doc/index.md)
[Read the documentation on symfony.com](https://symfony.com/doc/current/bundles/NelmioApiDocBundle/index.html)
Contributing

View file

@ -0,0 +1,23 @@
services:
_defaults:
public: false
autowire: true
autoconfigure: true
Nelmio\ApiDocBundle\Command\DumpCommand:
arguments:
$simpleFormatter: '@nelmio_api_doc.formatter.simple_formatter'
$markdownFormatter: '@nelmio_api_doc.formatter.markdown_formatter'
$htmlFormatter: '@nelmio_api_doc.formatter.html_formatter'
$apiDocExtractor: '@nelmio_api_doc.extractor.api_doc_extractor'
Nelmio\ApiDocBundle\Command\SwaggerDumpCommand:
arguments:
$extractor: '@nelmio_api_doc.extractor.api_doc_extractor'
$formatter: '@nelmio_api_doc.formatter.swagger_formatter'
Nelmio\ApiDocBundle\Controller\ApiDocController:
arguments:
$extractor: '@nelmio_api_doc.extractor.api_doc_extractor'
$htmlFormatter: '@nelmio_api_doc.formatter.html_formatter'
$swaggerFormatter: '@nelmio_api_doc.formatter.swagger_formatter'

View file

@ -8,19 +8,20 @@
<parameter key="nelmio_api_doc.formatter.markdown_formatter.class">Nelmio\ApiDocBundle\Formatter\MarkdownFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.simple_formatter.class">Nelmio\ApiDocBundle\Formatter\SimpleFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.html_formatter.class">Nelmio\ApiDocBundle\Formatter\HtmlFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.swagger_formatter.class">Nelmio\ApiDocBundle\Formatter\SwaggerFormatter</parameter>
<parameter key="nelmio_api_doc.sandbox.authentication">null</parameter>
</parameters>
<services>
<service id="nelmio_api_doc.formatter.abstract_formatter" class="%nelmio_api_doc.formatter.abstract_formatter.class%" />
<service id="nelmio_api_doc.formatter.abstract_formatter" class="%nelmio_api_doc.formatter.abstract_formatter.class%" abstract="true" />
<service id="nelmio_api_doc.formatter.markdown_formatter" class="%nelmio_api_doc.formatter.markdown_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter" />
<service id="nelmio_api_doc.formatter.simple_formatter" class="%nelmio_api_doc.formatter.simple_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter" />
<service id="nelmio_api_doc.formatter.html_formatter" class="%nelmio_api_doc.formatter.html_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter">
parent="nelmio_api_doc.formatter.abstract_formatter" public="true">
<call method="setTemplatingEngine">
<argument type="service" id="templating" />
<argument type="service" id="twig" />
</call>
<call method="setMotdTemplate">
<argument>%nelmio_api_doc.motd.template%</argument>
@ -37,19 +38,30 @@
<call method="setRequestFormatMethod">
<argument>%nelmio_api_doc.sandbox.request_format.method%</argument>
</call>
<call method="setRequestFormats">
<argument>%nelmio_api_doc.sandbox.request_format.formats%</argument>
</call>
<call method="setDefaultRequestFormat">
<argument>%nelmio_api_doc.sandbox.request_format.default_format%</argument>
</call>
<call method="setAcceptType">
<argument>%nelmio_api_doc.sandbox.accept_type%</argument>
</call>
<call method="setBodyFormat">
<argument>%nelmio_api_doc.sandbox.body_format%</argument>
<call method="setBodyFormats">
<argument>%nelmio_api_doc.sandbox.body_format.formats%</argument>
</call>
<call method="setDefaultBodyFormat">
<argument>%nelmio_api_doc.sandbox.body_format.default_format%</argument>
</call>
<call method="setAuthentication">
<argument>%nelmio_api_doc.sandbox.authentication%</argument>
</call>
<call method="setDefaultSectionsOpened">
<argument>%nelmio_api_doc.default_sections_opened%</argument>
</call>
</service>
<service id="nelmio_api_doc.formatter.swagger_formatter" class="%nelmio_api_doc.formatter.swagger_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter" public="true" />
</services>
</container>

View file

@ -1,5 +1,4 @@
nelmio_api_doc_index:
pattern: /
defaults: { _controller: NelmioApiDocBundle:ApiDoc:index }
requirements:
_method: GET
path: /{view}
defaults: { _controller: Nelmio\ApiDocBundle\Controller\ApiDocController::index, view: 'default' }
methods: [GET]

View file

@ -62,6 +62,7 @@
<xsd:complexType name="authentication">
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="delivery" type="authentication_delivery_enum"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="custom_endpoint" type="xsd:boolean" default="false"/>
</xsd:complexType>

View file

@ -10,6 +10,8 @@
<services>
<service id="nelmio_api_doc.parser.form_type_parser" class="%nelmio_api_doc.parser.form_type_parser.class%">
<argument type="service" id="form.factory" />
<argument type="service" id="translator" />
<argument>%nelmio_api_doc.sandbox.entity_to_choice%</argument>
<tag name="nelmio_api_doc.extractor.parser" />
</service>
</services>

View file

@ -9,25 +9,29 @@
<parameter key="nelmio_api_doc.twig.extension.extra_markdown.class">Nelmio\ApiDocBundle\Twig\Extension\MarkdownExtension</parameter>
<parameter key="nelmio_api_doc.doc_comment_extractor.class">Nelmio\ApiDocBundle\Util\DocCommentExtractor</parameter>
<parameter key="nelmio_api_doc.extractor.handler.fos_rest.class">Nelmio\ApiDocBundle\Extractor\Handler\FosRestHandler</parameter>
<parameter key="nelmio_api_doc.extractor.handler.jms_security.class">Nelmio\ApiDocBundle\Extractor\Handler\JmsSecurityExtraHandler</parameter>
<parameter key="nelmio_api_doc.extractor.handler.sensio_framework_extra.class">Nelmio\ApiDocBundle\Extractor\Handler\SensioFrameworkExtraHandler</parameter>
<parameter key="nelmio_api_doc.extractor.handler.phpdoc.class">Nelmio\ApiDocBundle\Extractor\Handler\PhpDocHandler</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.json_serializable_parser.class">Nelmio\ApiDocBundle\Parser\JsonSerializableParser</parameter>
</parameters>
<services>
<service id='nelmio_api_doc.doc_comment_extractor' class="%nelmio_api_doc.doc_comment_extractor.class%" />
<service id="nelmio_api_doc.doc_comment_extractor" class="%nelmio_api_doc.doc_comment_extractor.class%" />
<service id="nelmio_api_doc.extractor.api_doc_extractor" class="%nelmio_api_doc.extractor.api_doc_extractor.class%">
<argument type="service" id="service_container"/>
<service id="nelmio_api_doc.controller_name_parser" class="Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser" public="false">
<argument type="service" id="kernel" />
</service>
<service id="nelmio_api_doc.extractor.api_doc_extractor" class="%nelmio_api_doc.extractor.api_doc_extractor.class%" public="true">
<argument type="service" id="router" />
<argument type="service" id="annotation_reader" />
<argument type="service" id="nelmio_api_doc.doc_comment_extractor" />
<argument type="collection"/>
<argument type="collection" />
<argument>%nelmio_api_doc.exclude_sections%</argument>
</service>
<service id="nelmio_api_doc.form.extension.description_form_type_extension" class="%nelmio_api_doc.form.extension.description_form_type_extension.class%">
<tag name="form.type_extension" alias="form" />
<tag name="form.type_extension" alias="form" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
</service>
<service id="nelmio_api_doc.twig.extension.extra_markdown" class="%nelmio_api_doc.twig.extension.extra_markdown.class%">
@ -36,22 +40,22 @@
<!-- Extractor Annotation Handlers -->
<service id="nelmio_api_doc.extractor.handler.fos_rest" class="%nelmio_api_doc.extractor.handler.fos_rest.class%" public="false">
<tag name="nelmio_api_doc.extractor.handler"/>
</service>
<service id="nelmio_api_doc.extractor.handler.jms_security" class="%nelmio_api_doc.extractor.handler.jms_security.class%" public="false">
<tag name="nelmio_api_doc.extractor.handler"/>
</service>
<service id="nelmio_api_doc.extractor.handler.sensio_framework_extra" class="%nelmio_api_doc.extractor.handler.sensio_framework_extra.class%" public="false">
<tag name="nelmio_api_doc.extractor.handler"/>
</service>
<service id="nelmio_api_doc.extractor.handler.phpdoc" class="%nelmio_api_doc.extractor.handler.phpdoc.class%" public="false">
<argument type="service" id="nelmio_api_doc.doc_comment_extractor" />
<tag name="nelmio_api_doc.extractor.handler"/>
</service>
<service id="nelmio_api_doc.parser.collection_parser" class="%nelmio_api_doc.parser.collection_parser.class%">
<tag name="nelmio_api_doc.extractor.parser" />
</service>
<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" />
</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>
</container>

View file

@ -0,0 +1,9 @@
nelmio_api_doc_swagger_resource_list:
path: /
methods: [GET]
defaults: { _controller: Nelmio\ApiDocBundle\Controller\ApiDocController::swagger }
nelmio_api_doc_swagger_api_declaration:
path: /{resource}
methods: [GET]
defaults: { _controller: Nelmio\ApiDocBundle\Controller\ApiDocController::swagger }

View file

@ -0,0 +1,21 @@
Commands
========
A command is provided in order to dump the documentation in ``json``, ``markdown``,
or ``html``.
.. code-block:: bash
$ php app/console api:doc:dump [--format="..."]
The ``--format`` option allows to choose the format (default is: ``markdown``).
For example to generate a static version of your documentation you can use:
.. code-block:: bash
$ php app/console api:doc:dump --format=html > api.html
By default, the generated HTML will add the sandbox feature if you didn't
disable it in the configuration. If you want to generate a static version of
your documentation without sandbox, use the ``--no-sandbox`` option.

View file

@ -0,0 +1,132 @@
Configuration In-Depth
======================
API Name
--------
You can specify your own API name:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
name: My API
Authentication Methods
----------------------
You can choose between different authentication methods:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication:
delivery: header
name: X-Custom
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication:
delivery: query
name: param
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication:
delivery: http
type: basic # or bearer
When choosing an ``http`` delivery, ``name`` defaults to ``Authorization``, and
the header value will automatically be prefixed by the corresponding type (ie.
``Basic`` or ``Bearer``).
Section Exclusion
-----------------
You can specify which sections to exclude from the documentation generation:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
exclude_sections: ["privateapi", "testapi"]
Note that ``exclude_sections`` will literally exclude a section from your api
documentation. It's possible however to create multiple views by specifying the
``views`` parameter within the ``@ApiDoc`` annotations. This allows you to move
private or test methods to a complete different view of your documentation
instead.
Parsers
-------
By default, all registered parsers are used, but sometimes you may want to
define which parsers you want to use. The ``parsers`` attribute is used to
configure a list of parsers that will be used::
output={
"class" = "Acme\Bundle\Entity\User",
"parsers" = {
"Nelmio\ApiDocBundle\Parser\JmsMetadataParser",
"Nelmio\ApiDocBundle\Parser\ValidationParser"
}
}
In this case the parsers ``JmsMetadataParser`` and ``ValidationParser`` are used
to generate returned data. This feature also works for both the ``input`` and
``output`` properties.
Moreover, the bundle provides a way to register multiple ``input`` parsers. The
first parser that can handle the specified input is used, so you can configure
their priorities via container tags. Here's an example parser service
registration:
.. code-block:: yaml
# app/config/config.yml
services:
mybundle.api_doc.extractor.custom_parser:
class: MyBundle\Parser\CustomDocParser
tags:
- { name: nelmio_api_doc.extractor.parser, priority: 2 }
MOTD
----
You can also define your own motd content (above methods list). All you have to
do is add to configuration:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
# ...
motd:
template: AcmeApiBundle::Components/motd.html.twig
Caching
-------
It is a good idea to enable the internal caching mechanism on production:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
cache:
enabled: true
You can define an alternate location where the ApiDoc configurations are to be
cached:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
cache:
enabled: true
file: "/tmp/symfony-app/%kernel.environment%/api-doc.cache"

View file

@ -0,0 +1,55 @@
Configuration Reference
=======================
.. code-block:: yaml
nelmio_api_doc:
name: 'API documentation'
exclude_sections: []
default_sections_opened: true
motd:
template: '@NelmioApiDoc/Components/motd.html.twig'
request_listener:
enabled: true
parameter: _doc
sandbox:
enabled: true
endpoint: null
accept_type: null
body_format:
formats:
# Defaults:
- form
- json
default_format: ~ # One of "form"; "json"
request_format:
formats:
# Defaults:
json: application/json
xml: application/xml
method: ~ # One of "format_param"; "accept_header"
default_format: json
authentication:
delivery: ~ # Required
name: ~ # Required
# Required if http delivery is selected.
type: ~ # One of "basic"; "bearer"
custom_endpoint: false
entity_to_choice: true
swagger:
api_base_path: /api
swagger_version: '1.2'
api_version: '0.1'
info:
title: Symfony2
description: 'My awesome Symfony2 app!'
TermsOfServiceUrl: null
contact: null
license: null
licenseUrl: null
cache:
enabled: false
file: '%kernel.cache_dir%/api-doc.cache'

12
Resources/doc/faq.rst Normal file
View file

@ -0,0 +1,12 @@
Frequently Asked Questions
==========================
How can I remove the parameter ``_format`` sent in ``POST`` and ``PUT`` request?
--------------------------------------------------------------------------------
.. code-block:: yaml
nelmio_api_doc:
sandbox:
request_format:
method: accept_header

View file

@ -1,425 +0,0 @@
NelmioApiDocBundle
==================
The **NelmioApiDocBundle** bundle allows you to generate a decent documentation
for your APIs.
Installation
------------
Add this bundle to your `composer.json` file:
{
"require": {
"nelmio/api-doc-bundle": "@stable"
}
}
**Protip:** you should browse the
[`nelmio/api-doc-bundle`](https://packagist.org/packages/nelmio/api-doc-bundle)
page to choose a stable version to use, avoid the `@stable` meta constraint.
Register the bundle in `app/AppKernel.php`:
// app/AppKernel.php
public function registerBundles()
{
return array(
// ...
new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
);
}
Import the routing definition in `routing.yml`:
# app/config/routing.yml
NelmioApiDocBundle:
resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
prefix: /api/doc
Enable the bundle's configuration in `app/config/config.yml`:
# app/config/config.yml
nelmio_api_doc: ~
Usage
-----
The main problem with documentation is to keep it up to date. That's why the **NelmioApiDocBundle**
uses introspection a lot. Thanks to an annotation, it's really easy to document an API method.
### The ApiDoc() Annotation
The bundle provides an `ApiDoc()` annotation for your controllers:
``` php
<?php
namespace Your\Namespace;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
class YourController extends Controller
{
/**
* This is the documentation description of your method, it will appear
* on a specific pane. It will read all the text until the first
* annotation.
*
* @ApiDoc(
* resource=true,
* description="This is a description of your API method",
* filters={
* {"name"="a-filter", "dataType"="integer"},
* {"name"="another-filter", "dataType"="string", "pattern"="(foo|bar) ASC|DESC"}
* }
* )
*/
public function getAction()
{
}
/**
* @ApiDoc(
* description="Create a new Object",
* input="Your\Namespace\Form\Type\YourType",
* output="Your\Namespace\Class"
* )
*/
public function postAction()
{
}
/**
* @ApiDoc(
* description="Returns a collection of Object",
* requirements={
* {
* "name"="limit",
* "dataType"="integer",
* "requirement"="\d+",
* "description"="how many objects to return"
* }
* },
* parameters={
* {"name"="categoryId", "dataType"="integer", "required"=true, "description"="category id"}
* }
* )
*/
public function cgetAction($id)
{
}
}
```
The following properties are available:
* `section`: allow to group resources
* `resource`: whether the method describes a main resource or not (default: `false`);
* `description`: a description of the API method;
* `https`: whether the method described requires the https protocol (default: `false`);
* `deprecated`: allow to set method as deprecated (default: `false`);
* `filters`: an array of filters;
* `requirements`: an array of requirements;
* `parameters`: an array of parameters;
* `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
(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`.
* `statusCodes`: an array of HTTP status codes and a description of when that status is returned; Example:
``` php
<?php
class YourController
{
/**
* @ApiDoc(
* statusCodes={
* 200="Returned when successful",
* 403="Returned when the user is not authorized to say hello",
* 404={
* "Returned when the user is not found",
* "Returned when something else is not found"
* }
* }
* )
*/
public function myFunction()
{
// ...
}
}
```
Each _filter_ has to define a `name` parameter, but other parameters are free. Filters are often optional
parameters, and you can document them as you want, but keep in mind to be consistent for the whole documentation.
If you set `input`, then the bundle automatically extracts parameters based on the given type,
and determines for each parameter its data type, and if it's required or not.
For classes parsed with JMS metadata, description will be taken from the properties doc comment, if available.
For Form Types, you can add an extra option named `description` on each field:
``` php
<?php
class YourType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('note', null, array(
'description' => 'this is a note',
));
// ...
}
}
```
The bundle will also get information from the routing definition (`requirements`, `pattern`, etc), so to get the
best out of it you should define strict _method requirements etc.
### Other Bundle Annotations
Also bundle will get information from the other annotations:
* @FOS\RestBundle\Controller\Annotations\RequestParam - use as `parameters`
* @FOS\RestBundle\Controller\Annotations\QueryParam - use as `requirements` (when strict parameter is true), `filters` (when strict is false)
* @JMS\SecurityExtraBundle\Annotation\Secure - set `authentication` to true, `authenticationRoles` to the given roles
* @Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache - set `cache`
### PHPDoc
Route functions marked as @deprecated will be set method as deprecation in
documentation.
#### JMS Serializer Features
The bundle has support for some of the JMS Serializer features and use these
extra information in the generated documentation.
##### Group Exclusion Strategy
If your classes use [JMS Group Exclusion
Strategy](http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#creating-different-views-of-your-objects),
you can specify which groups to use when generating the documentation by using
this syntax :
```
input={
"class"="Acme\Bundle\Entity\User",
"groups"={"update", "public"}
}
```
In this case the groups 'update' and 'public' are used.
This feature also works for the `output` property.
##### Versioning Objects
If your `output` classes use [versioning capabilities of JMS
Serializer](http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#versioning-objects),
the versioning information will be automatically used when generating the
documentation.
#### Form Types Features
If you use `FormFactoryInterface::createdNamed('', 'your_form_type'`, then by
default the documentation will use the form type name as the prefix
(`your_form_type[param]` ... instead of just `param`).
You can specify which prefix to use with the `name` key:
```
input = {
"class" = "your_form_type",
"name" = ""
}
```
#### Used Parsers
By default, all registered parsers are used, but sometimes you may want to
define which parsers you want to use. The `parsers` attribute is used to
configure a list of parsers that will be used:
```
output={
"class" = "Acme\Bundle\Entity\User",
"parsers" = {
"Nelmio\ApiDocBundle\Parser\JmsMetadataParser",
"Nelmio\ApiDocBundle\Parser\ValidationParser"
}
}
```
In this case the parsers `JmsMetadataParser` and `ValidationParser` are used to
generate returned data.
This feature also works for both the `input` and `output` properties.
### Web Interface
You can browse the whole documentation at: `http://example.org/api/doc`.
![](https://github.com/nelmio/NelmioApiDocBundle/raw/master/Resources/doc/webview.png)
![](https://github.com/nelmio/NelmioApiDocBundle/raw/master/Resources/doc/webview2.png)
### On-The-Fly Documentation
By calling an URL with the parameter `?_doc=1`, you will get the corresponding
documentation if available.
### Sandbox
This bundle provides a sandbox mode in order to test API methods. You can
configure this sandbox using the following parameters:
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication: # default is `~` (`null`), if set, the sandbox automatically
# send authenticated requests using the configured `delivery`
name: access_token # access token name or query parameter name or header name
delivery: query # `query`, `http_basic`, and `header` are supported
custom_endpoint: true # default is `false`, if `true`, your user will be able to
# specify its own endpoint
enabled: true # default is `true`, you can set this parameter to `false`
# to disable the sandbox
endpoint: http://sandbox.example.com/ # default is `/app_dev.php`, use this parameter
# to define which URL to call through the sandbox
accept_type: application/json # default is `~` (`null`), if set, the value is
# automatically populated as the `Accept` header
body_format: form # default is `form`, determines whether to send
# `x-www-form-urlencoded` data or json-encoded
# data (by setting this parameter to `json`) in
# sandbox requests
request_format:
method: format_param # default `format_param`, alternately `accept_header`,
# decides how to request the response format
default_format: json # default `json`, alternately `xml`, determines which
# content format to request back by default
### Command
A command is provided in order to dump the documentation in `json`, `markdown`, or `html`.
php app/console api:doc:dump [--format="..."]
The `--format` option allows to choose the format (default is: `markdown`).
For example to generate a static version of your documentation you can use:
php app/console api:doc:dump --format=html > api.html
By default, the generated HTML will add the sandbox feature if you didn't disable it in the configuration.
If you want to generate a static version of your documentation without sandbox, use the `--no-sandbox` option.
Configuration In-Depth
----------------------
You can specify your own API name:
# app/config/config.yml
nelmio_api_doc:
name: My API
You can specify which sections to exclude from the documentation generation:
# app/config/config.yml
nelmio_api_doc:
exclude_sections: ["privateapi", "testapi"]
The bundle provides a way to register multiple `input` parsers. The first parser
that can handle the specified input is used, so you can configure their
priorities via container tags. Here's an example parser service registration:
#app/config/config.yml
services:
mybundle.api_doc.extractor.custom_parser:
class: MyBundle\Parser\CustomDocParser
tags:
- { name: nelmio_api_doc.extractor.parser, priority: 2 }
You can also define your own motd content (above methods list). All you have to
do is add to configuration:
#app/config/config.yml
nelmio_api_doc:
# ...
motd:
template: AcmeApiBundle::Components/motd.html.twig
### Using Your Own Annotations
If you have developed your own project-related annotations, and you want to parse them to populate
the `ApiDoc`, you can provide custom handlers as services. You just have to implement the
`Nelmio\ApiDocBundle\Extractor\HandlerInterface` and tag it as `nelmio_api_doc.extractor.handler`:
# app/config/config.yml
services:
mybundle.api_doc.extractor.my_annotation_handler:
class: MyBundle\AnnotationHandler\MyAnnotationHandler
tags:
- { name: nelmio_api_doc.extractor.handler }
Look at the built-in [Handlers](https://github.com/nelmio/NelmioApiDocBundle/tree/master/Extractor/Handler).
### Reference Configuration
``` yaml
nelmio_api_doc:
name: API documentation
exclude_sections: []
motd:
template: NelmioApiDocBundle::Components/motd.html.twig
request_listener:
enabled: true
parameter: _doc
sandbox:
enabled: true
endpoint: ~
accept_type: ~
body_format: form
request_format:
method: format_param
default_format: json
authentication:
name: ~ # Required
delivery: ~ # Required
custom_endpoint: false
```

143
Resources/doc/index.rst Normal file
View file

@ -0,0 +1,143 @@
NelmioApiDocBundle
==================
The **NelmioApiDocBundle** bundle allows you to generate a decent documentation
for your APIs.
Installation
------------
Step 1: Download the Bundle
---------------------------
Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:
.. code-block:: bash
$ composer require nelmio/api-doc-bundle
This command requires you to have Composer installed globally, as explained
in the `installation chapter`_ of the Composer documentation.
Step 2: Enable the Bundle
-------------------------
Then, enable the bundle by adding it to the list of registered bundles
in the ``app/AppKernel.php`` file of your project:
.. code-block:: php
<?php
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
);
// ...
}
// ...
}
Step 3: Register the Routes
---------------------------
Import the routing definition in ``routing.yml``:
.. code-block:: yaml
# app/config/routing.yml
NelmioApiDocBundle:
resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
prefix: /api/doc
Step 4: Configure the Bundle
----------------------------
Enable the bundle's configuration in ``app/config/config.yml``:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc: ~
The **NelmioApiDocBundle** requires Twig as a template engine so do not forget
to enable it:
.. code-block:: yaml
# app/config/config.yml
framework:
templating:
engines: ['twig']
Usage
-----
The main problem with documentation is to keep it up to date. That's why the
**NelmioApiDocBundle** uses introspection a lot. Thanks to an annotation, it's
really easy to document an API method. The following chapters will help you
setup your API documentation:
.. toctree::
:maxdepth: 1
the-apidoc-annotation
multiple-api-doc
other-bundle-annotations
swagger-support
sandbox
commands
configuration-in-depth
configuration-reference
faq
Web Interface
~~~~~~~~~~~~~
You can browse the whole documentation at: ``http://example.org/api/doc``.
.. image:: webview.png
:align: center
.. image:: webview2.png
:align: center
On-The-Fly Documentation
~~~~~~~~~~~~~~~~~~~~~~~~
By calling an URL with the parameter ``?_doc=1``, you will get the corresponding
documentation if available.
.. _`installation chapter`: https://getcomposer.org/doc/00-intro.md
Route versions
~~~~~~~~~~~~~~
You can define version for the API routes:
.. code-block:: yaml
api_v3_products_list:
pattern: /api/v3/products.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Test:routeVersion, _format: json, _version: "3.0" }
requirements:
_method: GET
api_v1_orders:
resource: "@AcmeOrderBundle/Resources/config/routing/orders_v1.yml"
defaults: { _version: "1.0" }
prefix: /api/v1/orders
And generate documentation for specific version by the command:
.. code-block:: bash
php app/console api:doc:dump --format=html --api-version=3.0 > api.html
Or by adding `?_version={version}` to API documentation page URL.

View file

@ -0,0 +1,59 @@
Multiple API Documentation ("Views")
====================================
With the ``views`` tag in the ``@ApiDoc`` annotation, it is possible to create
different views of your API documentation. Without the tag, all methods are
located in the ``default`` view, and can be found under the normal API
documentation url.
You can specify one or more *view* names under which the method will be
visible.
An example::
/**
* A resource
*
* @ApiDoc(
* resource=true,
* description="This is a description of your API method",
* views = { "default", "premium" }
* )
*/
public function getAction()
{
}
/**
* Another resource
*
* @ApiDoc(
* resource=true,
* description="This is a description of another API method",
* views = { "premium" }
* )
*/
public function getAnotherAction()
{
}
In this case, only the first resource will be available under the default view,
while both methods will be available under the ``premium`` view.
Accessing Specific API Views
----------------------------
The ``default`` view can be found at the normal location. Other views can be
found at ``http://your.documentation/<view name>``.
For instance, if your documentation is located at:
.. code-block:: text
http://example.org/doc/api/v1/
then the ``premium`` view will be located at:
.. code-block:: text
http://example.org/doc/api/v1/premium

View file

@ -0,0 +1,81 @@
Other Bundle Annotations
========================
PHPDoc
------
Actions marked as ``@deprecated`` will be marked as such in the interface.
JMS Serializer Features
-----------------------
The bundle has support for some of the JMS Serializer features and uses this
extra piece of information in the generated documentation.
Group Exclusion Strategy
------------------------
If your classes use `JMS Group Exclusion Strategy`_, you can specify which
groups to use when generating the documentation by using this syntax::
input={
"class"="Acme\Bundle\Entity\User",
"groups"={"update", "public"}
}
In this case the groups ``update`` and ``public`` are used. This feature also
works for the ``output`` property.
Versioning Objects
------------------
If your ``output`` classes use `versioning capabilities of JMS Serializer`_, the
versioning information will be automatically used when generating the
documentation.
Form Types Features
-------------------
Even if you use ``FormFactoryInterface::createNamed('', 'your_form_type')`` the
documentation will generate the form type name as the prefix for inputs
(``your_form_type[param]`` ... instead of just ``param``).
You can specify which prefix to use with the ``name`` key in the ``input``
section::
input = {
"class" = "your_form_type",
"name" = ""
}
You can also add some options to pass to the form. You just have to use the
``options`` key::
input = {
"class" = "your_form_type",
"options" = {"method" = "PUT"},
}
Using Your Own Annotations
--------------------------
If you have developed your own project-related annotations, and you want to
parse them to populate the ``ApiDoc``, you can provide custom handlers as
services. You just have to implement the
``Nelmio\ApiDocBundle\Extractor\HandlerInterface`` and tag it as
``nelmio_api_doc.extractor.handler``:
.. code-block:: yaml
# app/config/config.yml
services:
mybundle.api_doc.extractor.my_annotation_handler:
class: MyBundle\AnnotationHandler\MyAnnotationHandler
tags:
- { name: nelmio_api_doc.extractor.handler }
Look at the `built-in Handlers`_.
.. _`JMS Group Exclusion Strategy`: http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#creating-different-views-of-your-objects
.. _`versioning capabilities of JMS Serializer`: http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#versioning-objects
.. _`built-in Handlers`: https://github.com/nelmio/NelmioApiDocBundle/tree/master/Extractor/Handler

54
Resources/doc/sandbox.rst Normal file
View file

@ -0,0 +1,54 @@
Sandbox
=======
This bundle provides a sandbox mode in order to test API methods. You can
configure this sandbox using the following parameters:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication: # default is `~` (`null`), if set, the sandbox automatically
# send authenticated requests using the configured `delivery`
name: access_token # access token name or query parameter name or header name
delivery: http # `query`, `http`, and `header` are supported
# Required if http delivery is selected.
type: basic # `basic`, `bearer` are supported
custom_endpoint: true # default is `false`, if `true`, your user will be able to
# specify its own endpoint
enabled: true # default is `true`, you can set this parameter to `false`
# to disable the sandbox
endpoint: http://sandbox.example.com/ # default is `/app_dev.php`, use this parameter
# to define which URL to call through the sandbox
accept_type: application/json # default is `~` (`null`), if set, the value is
# automatically populated as the `Accept` header
body_format:
formats: [ form, json ] # array of enabled body formats,
# remove all elements to disable the selectbox
default_format: form # default is `form`, determines whether to send
# `x-www-form-urlencoded` data or json-encoded
# data (by setting this parameter to `json`) in
# sandbox requests
request_format:
formats: # default is `json` and `xml`,
json: application/json # override to add custom formats or disable
xml: application/xml # the default formats
method: format_param # default is `format_param`, alternately `accept_header`,
# decides how to request the response format
default_format: json # default is `json`,
# default content format to request (see formats)
entity_to_choice: false # default is `true`, if `false`, entity collection
# will not be mapped as choice

View file

@ -0,0 +1,163 @@
Swagger Support
===============
It is possible to make your application produce Swagger-compliant JSON output
based on ``@ApiDoc`` annotations, which can be used for consumption by
`swagger-ui`_.
Annotation options
------------------
A couple of properties has been added to ``@ApiDoc``:
To define a **resource description**::
/**
* @ApiDoc(
* resource=true,
* resourceDescription="Operations on users.",
* description="Retrieve list of users."
* )
*/
public function listUsersAction()
{
/* Stuff */
}
The ``resourceDescription`` is distinct from ``description`` as it applies to the
whole resource group and not just the particular API endpoint.
Defining a form-type as a GET form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you use forms to capture GET requests, you will have to specify the
``paramType`` to ``query`` in the annotation::
/**
* @ApiDoc(
* input = {"class" = "Foo\ContentBundle\Form\SearchType", "paramType" = "query"},
* ...
* )
*/
public function searchAction(Request $request)
{
//...
}
Multiple response models
------------------------
Swagger provides you the ability to specify alternate output models for
different status codes. Example, ``200`` would return your default resource object
in JSON form, but ``400`` may return a custom validation error list object. This
can be specified through the ``responseMap`` property::
/**
* @ApiDoc(
* description="Retrieve list of users.",
* statusCodes={
* 400 = "Validation failed."
* },
* responseMap={
* 200 = "FooBundle\Entity\User",
* 400 = {
* "class"="CommonBundle\Model\ValidationErrors",
* "parsers"={"Nelmio\ApiDocBundle\Parser\JmsMetadataParser"}
* }
* }
* )
*/
public function updateUserAction()
{
/* Stuff */
}
This will tell Swagger that ``CommonBundle\Model\ValidationErrors`` is returned
when this endpoint returns a ``400 Validation failed.`` HTTP response.
.. note::
You can omit the ``200`` entry in the ``responseMap`` property and specify
the default ``output`` property instead. That will result on the same thing.
Integration with ``swagger-api/swagger-ui``
-------------------------------------------
You could import the routes for use with `swagger-ui`_.
.. code-block:: yaml
#app/config/routing.yml
nelmio_api_swagger:
resource: "@NelmioApiDocBundle/Resources/config/swagger_routing.yml"
prefix: /api-docs
Et voila!, simply specify http://yourdomain.com/api-docs in your ``swagger-ui``
instance and you are good to go.
.. note::
If your ``swagger-ui`` instance does not live under the same domain, you
will probably encounter some problems related to same-origin policy
violations. `NelmioCorsBundle`_ can solve this problem for you. Read through
how to allow cross-site requests for the ``/api-docs/*`` pages.
Dumping the Swagger-compliant JSON API definitions
--------------------------------------------------
To display all JSON definitions:
.. code-block:: bash
$ php app/console api:swagger:dump
To dump just the resource list:
.. code-block:: bash
$ php app/console api:swagger:dump --list-only
To dump just the API definition the ``users`` resource:
.. code-block:: bash
$ php app/console api:swagger:dump --resource=users
Specify the ``--pretty`` flag to display a prettified JSON output.
Dump to files
~~~~~~~~~~~~~
You can specify the destination if you wish to dump the JSON definition to a file:
.. code-block:: bash
$ php app/console api:swagger:dump --list-only swagger-docs/api-docs.json
$ php app/console api:swagger:dump --resource=users swagger-docs/users.json
Or, you can dump everything into a directory in one command:
.. code-block:: bash
$ php app/console api:swagger:dump swagger-docs
Model naming
------------
By default, the model naming strategy used is the ``dot_notation`` strategy. The
model IDs are simply the Fully Qualified Class Name (FQCN) of the class
associated to it, with the ``\`` replaced with ``.``:
.. code-block:: text
Vendor\UserBundle\Entity\User => Vendor.UserBundle.Entity.User
You can also change the ``model_naming_strategy`` in the configuration to
``last_segment_only``, if you want model IDs to be just the class name minus the
namespaces (``Vendor\UserBundle\Entity\User => User``). This will not afford you
the guarantee that model IDs are unique, but that would really just depend on
the classes you have in use.
.. _`swagger-ui`: https://github.com/swagger-api/swagger-ui
.. _`NelmioCorsBundle`: https://github.com/nelmio/NelmioCorsBundle

View file

@ -0,0 +1,181 @@
The ``ApiDoc()`` Annotation
===========================
The bundle provides an ``ApiDoc()`` annotation for your controllers::
namespace Your\Namespace;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
class YourController extends Controller
{
/**
* This is the documentation description of your method, it will appear
* on a specific pane. It will read all the text until the first
* annotation.
*
* @ApiDoc(
* resource=true,
* description="This is a description of your API method",
* filters={
* {"name"="a-filter", "dataType"="integer"},
* {"name"="another-filter", "dataType"="string", "pattern"="(foo|bar) ASC|DESC"}
* }
* )
*/
public function getAction()
{
}
/**
* @ApiDoc(
* description="Create a new Object",
* input="Your\Namespace\Form\Type\YourType",
* output="Your\Namespace\Class"
* )
*/
public function postAction()
{
}
/**
* @ApiDoc(
* description="Returns a collection of Object",
* requirements={
* {
* "name"="limit",
* "dataType"="integer",
* "requirement"="\d+",
* "description"="how many objects to return"
* }
* },
* parameters={
* {"name"="categoryId", "dataType"="integer", "required"=true, "description"="category id"}
* }
* )
*/
public function cgetAction($limit)
{
}
}
The following properties are available:
* ``section``: allow to group resources
* ``resource``: whether the method describes a main resource or not (default:
``false``);
* ``description``: a description of the API method;
* ``deprecated``: allow to set method as deprecated (default: ``false``);
* ``tags``: allow to tag a method (e.g. ``beta`` or ``in-development``). Either
a single tag or an array of tags. Each tag can have an optional hex colorcode
attached.
.. code-block:: php
class YourController
{
/**
* @ApiDoc(
* tags={
* "stable",
* "deprecated" = "#ff0000"
* }
* )
*/
public function myFunction()
{
// ...
}
}
* ``filters``: an array of filters;
* ``requirements``: an array of requirements;
* ``parameters``: an array of parameters;
* ``headers``: an array of headers; available properties are: ``name``, ``description``, ``required``, ``default``. Example:
.. code-block:: php
class YourController
{
/**
* @ApiDoc(
* headers={
* {
* "name"="X-AUTHORIZE-KEY",
* "description"="Authorization key"
* }
* }
* )
*/
public function myFunction()
{
// ...
}
}
* ``input``: the input type associated to the method (currently this supports
Form Types, classes with JMS Serializer 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).
* ``output``: the output type associated with the response. Specified and
parsed the same way as ``input``.
* ``statusCodes``: an array of HTTP status codes and a description of when that
status is returned; Example:
.. code-block:: php
class YourController
{
/**
* @ApiDoc(
* statusCodes={
* 200="Returned when successful",
* 403="Returned when the user is not authorized to say hello",
* 404={
* "Returned when the user is not found",
* "Returned when something else is not found"
* }
* }
* )
*/
public function myFunction()
{
// ...
}
}
* ``views``: the view(s) under which this resource will be shown. Leave empty to
specify the default view. Either a single view, or an array of views.
Each *filter* has to define a ``name`` parameter, but other parameters are free.
Filters are often optional parameters, and you can document them as you want,
but keep in mind to be consistent for the whole documentation.
If you set ``input``, then the bundle automatically extracts parameters based on
the given type, and determines for each parameter its data type, and if it's
required or not.
For classes parsed with JMS metadata, description will be taken from the
properties doc comment, if available.
For Form Types, you can add an extra option named ``description`` on each field::
class YourType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('note', null, array(
'description' => 'this is a note',
));
// ...
}
}
The bundle will also get information from the routing definition
(``requirements``, ``path``, etc), so to get the best out of it you should
define strict methods requirements etc.

View file

@ -99,6 +99,11 @@ em {
code, pre {
font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
background-color: #fcf6db;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
p code {
@ -113,6 +118,11 @@ pre {
line-height:1.2em;
}
div.content ul, div.content ol {
line-height: 1.4em;
color: #333333;
}
table.fullwidth {
width: 100%;
}
@ -135,6 +145,10 @@ table tbody tr td {
line-height: 1.3em;
}
table tbody tr td.format {
word-break: break-word;
}
#header {
background-color: #89BF04;
padding: 1%;
@ -171,11 +185,38 @@ table tbody tr td {
font-size: 0.9em;
}
#section {
border: 1px solid #ddd;
background: #f8f8f8;
padding: 5px 20px;
margin-bottom: 15px;
.section {
padding: 5px 20px;
border-bottom: 1px solid #ddd;
}
.section h1 {
padding: 0;
}
.section.active {
border: 1px solid #ddd;
background: #f8f8f8;
margin: 15px 0;
}
.section.active h1 {
padding: 10px 0;
}
.section .actions {
text-align: right;
float: right;
margin-top: 10px;
}
.section .actions a {
cursor: pointer;
margin-left: 10px;
}
.section .actions a:hover {
text-decoration: underline;
}
li.resource {
@ -188,14 +229,14 @@ li.resource:last-child {
}
/* heading */
a.heading {
.heading {
border: 1px solid transparent;
float: none;
clear: both;
overflow: hidden;
display: block;
}
a.heading h2 {
.heading h2 {
color: #999999;
padding-left: 0;
display: block;
@ -204,7 +245,7 @@ a.heading h2 {
font-family: "Droid Sans", sans-serif;
font-weight: bold;
}
a.heading ul.options {
.heading ul.options {
overflow: hidden;
padding: 0;
display: block;
@ -212,7 +253,7 @@ a.heading ul.options {
float: right;
margin: 6px 10px 0 0;
}
a.heading ul.options li {
.heading ul.options li {
float: left;
clear: none;
margin: 0;
@ -221,12 +262,12 @@ a.heading ul.options li {
color: #666666;
font-size: 0.9em;
}
a.heading ul.options li:first-child,
a.heading ul.options li.first {
.heading ul.options li:first-child,
.heading ul.options li.first {
padding-left: 0;
}
a.heading ul.options li:last-child,
a.heading ul.options li.last {
.heading ul.options li:last-child,
.heading ul.options li.last {
padding-right: 0;
border-right: none;
}
@ -240,13 +281,13 @@ li.operation {
margin: 0 0 10px;
padding: 0 0 0 0;
}
li.operation a.heading {
li.operation .heading {
margin: 0 0 0 0;
padding: 0;
background-color: #f0f0f0;
border: 1px solid #ddd;
}
li.operation a.heading h3 {
li.operation .heading h3 {
display: block;
clear: none;
float: left;
@ -256,25 +297,25 @@ li.operation a.heading h3 {
line-height: 1.1em;
color: black;
}
li.operation a.heading h3 span {
li.operation .heading h3 span {
margin: 0;
padding: 0;
}
li.operation a.heading h3 span.icon {
li.operation .heading h3 span.icon {
display: inline-block;
height: 12px;
width: 12px;
margin-left: 3px;
background: no-repeat center center;
}
li.operation a.heading h3 span.lock {
li.operation .heading h3 span.lock {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAMCAYAAABbayygAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUHEisepJ6ljAAAAJ5JREFUGNNt0LEOAUEUheEPuwkFtSg0old4Eo/imbQKiULpBZQSCtFoaIjSktXMxpjsSW5xzvnnZmb4aYMymg9WEq1Decc1zCNkyxisoFGUTXDGEZpR8cIp8jccKiaLigwDdMP9hughr8ptALtYoB18C+Pgd5KXlrhgX5P/mSfmmKVgM/mmDP1qQ1rEyjFFkYKNmtMF3uikYFGzOdXnC5FWMZNd2GfvAAAAAElFTkSuQmCC");
}
li.operation a.heading h3 span.keys {
li.operation .heading h3 span.keys {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAMCAYAAAC0qUeeAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUHEisb1PRRAwAAAN9JREFUKM9lz71KQ0EQhuEnJ7EIKhZWaRULu4htLLwDvYRgZWmTWoI3kAuIlVik0U5Io4USRMFOUGwU7cQ/kAQMajMHDpuBZWd235lvPtjHDT4xxhuu0ZJEhhXU8YAG7rCKBWyn8EnkVSxjOuoPbKTT1/GXnCd0YqWt4uQrk3GLGcxiswgPcRgG4QsDzKMSKtUc/kUbFwEf4BlrUdeCk8WOj3jBO+5xhGOMwmQzh6Ec9zemwtgOLuN9D4tZYqwUSvnuu3jFHLpZASqHUqXQfIZe5PX8Y4RTLKGfqLVwjp9/HR4zOkGnnAoAAAAASUVORK5CYII=");
}
li.operation a.heading h3 span.http_method i, li.operation a.heading h3 span.deprecated i {
li.operation .heading h3 span.http_method i, li.operation .heading h3 span.deprecated i {
text-transform: uppercase;
text-decoration: none;
color: white;
@ -288,14 +329,26 @@ li.operation a.heading h3 span.http_method i, li.operation a.heading h3 span.dep
border-radius: 2px;
background-color: #ccc;
}
li.operation a.heading h3 span.deprecated i {
li.operation .heading h3 span.deprecated i {
width: 75px;
background-color: #F00;
}
li.operation a.heading h3 span.path {
li.operation .heading h3 span.path {
padding-left: 5px;
}
li.operation .heading h3 span.tag {
color: #FFFFFF;
font-size: 0.7em;
vertical-align: baseline;
background-color: #d9534f;
padding-bottom: 3px;
padding-left: 6px;
padding-right: 6px;
padding-top: 2px;
border-radius: 4px;
}
li.operation div.content {
border: 1px solid #ddd;
padding: 10px;
@ -321,14 +374,14 @@ li.operation div.content form input[type='text'].error {
}
/* GET operations */
li.operation.get a.heading {
li.operation.get .heading {
border-color: #c3d9ec;
background-color: #e7f0f7;
}
li.operation.get a.heading h3 span.http_method i {
li.operation.get .heading h3 span.http_method i {
background-color: #0f6ab4;
}
li.operation.get a.heading ul.options li {
li.operation.get .heading ul.options li {
border-right-color: #c3d9ec;
color: #0f6ab4;
}
@ -342,14 +395,14 @@ li.operation.get div.content h4 {
}
/* POST operations */
li.operation.post a.heading {
li.operation.post .heading {
border-color: #a7e1a1;
background-color: #d4f7cd;
}
li.operation.post a.heading h3 span.http_method i{
li.operation.post .heading h3 span.http_method i{
background-color: #10a54a;
}
li.operation.post a.heading ul.options li {
li.operation.post .heading ul.options li {
border-right-color: #c3e8d1;
color: #10a54a;
}
@ -363,14 +416,14 @@ li.operation.post div.content h4 {
}
/* ANY operations */
li.operation.any a.heading {
li.operation.any .heading {
background-color: lightgray;
border-color: gray;
}
li.operation.any a.heading h3 span.http_method i {
li.operation.any .heading h3 span.http_method i {
background-color: #000;
}
li.operation.any a.heading ul.options li {
li.operation.any .heading ul.options li {
color: #000;
border-right-color: gray;
}
@ -384,14 +437,14 @@ li.operation.any div.content h4 {
}
/* PUT operations */
li.operation.put a.heading {
li.operation.put .heading {
background-color: #f9f2e9;
border-color: #f0e0ca;
}
li.operation.put a.heading h3 span.http_method i {
li.operation.put .heading h3 span.http_method i {
background-color: #c5862b;
}
li.operation.put a.heading ul.options li {
li.operation.put .heading ul.options li {
border-right-color: #f0e0ca;
color: #c5862b;
}
@ -405,14 +458,14 @@ li.operation.put div.content h4 {
}
/* DELETE operations */
li.operation.delete a.heading {
li.operation.delete .heading {
background-color: #f5e8e8;
border-color: #e8c6c7;
}
li.operation.delete a.heading h3 span.http_method i {
li.operation.delete .heading h3 span.http_method i {
background-color: #a41e22;
}
li.operation.delete a.heading ul.options li {
li.operation.delete .heading ul.options li {
border-right-color: #e8c6c7;
color: #a41e22;
}
@ -426,14 +479,14 @@ li.operation.delete div.content h4 {
}
/* PATCH operations */
li.operation.patch a.heading {
li.operation.patch .heading {
background-color: #f5e8e8;
border-color: #e8c6e7;
}
li.operation.patch a.heading h3 span.http_method i {
li.operation.patch .heading h3 span.http_method i {
background-color: #a41ee2;
}
li.operation.patch a.heading ul.options li {
li.operation.patch .heading ul.options li {
border-right-color: #e8c6c7;
color: #a41ee2;
}
@ -447,13 +500,13 @@ li.operation.patch div.content h4 {
}
/* LINK operations */
li.operation.link a.heading {
li.operation.link .heading {
background-color: #F7F7D5;
}
li.operation.link a.heading h3 span.http_method i {
li.operation.link .heading h3 span.http_method i {
background-color: #C3D448;
}
li.operation.link a.heading ul.options li {
li.operation.link .heading ul.options li {
color: #C3D448;
}
@ -465,13 +518,13 @@ li.operation.link div.content h4 {
}
/* UNLINK operations */
li.operation.unlink a.heading {
li.operation.unlink .heading {
background-color: #FFEBDE;
}
li.operation.unlink a.heading h3 span.http_method i {
li.operation.unlink .heading h3 span.http_method i {
background-color: #FF8438;
}
li.operation.unlink a.heading ul.options li {
li.operation.unlink .heading ul.options li {
color: #FF8438;
}
@ -545,7 +598,7 @@ form .parameters {
width: 50%;
}
form .parameters .tuple input {
form .parameters .tuple input, form .parameters .tuple textarea {
width: 40%;
}
@ -577,3 +630,8 @@ form .request-content {
.motd {
padding:20px;
}
.json-collapse-section {
color: #660;
cursor: pointer;
}

View file

@ -17,24 +17,34 @@
<a href="{{ path('nelmio_api_doc_index') }}"><h1>{{ apiName }}</h1></a>
{% if enableSandbox %}
<div id="sandbox_configuration">
{% if bodyFormats|length > 0 %}
body format:
<select id="body_format">
<option value="x-www-form-urlencoded"{{ bodyFormat == 'form' ? ' selected' : '' }}>Form Data</option>
<option value="json"{{ bodyFormat == 'json' ? ' selected' : '' }}>JSON</option>
{% if 'form' in bodyFormats %}<option value="form"{{ defaultBodyFormat == 'form' ? ' selected' : '' }}>Form Data</option>{% endif %}
{% if 'json' in bodyFormats %}<option value="json"{{ defaultBodyFormat == 'json' ? ' selected' : '' }}>JSON</option>{% endif %}
</select>
{% endif %}
{% if requestFormats|length > 0 %}
request format:
<select id="request_format">
<option value="json"{{ defaultRequestFormat == 'json' ? ' selected' : '' }}>JSON</option>
<option value="xml"{{ defaultRequestFormat == 'xml' ? ' selected' : '' }}>XML</option>
{% for format, header in requestFormats %}
<option value="{{ header }}"{{ defaultRequestFormat == format ? ' selected' : '' }}>{{ format }}</option>
{% endfor %}
{% endif %}
</select>
{% if authentication and authentication.delivery in ['query', 'http_basic', 'header'] %}
api key: <input type="text" id="api_key" value=""/>
{% endif %}
{% if authentication and authentication.delivery in ['http_basic'] %}
api pass: <input type="text" id="api_pass" value=""/>
{% endif %}
{% if authentication and authentication.custom_endpoint %}
api endpoint: <input type="text" id="api_endpoint" value=""/>
{% if authentication %}
{% if authentication.delivery == 'http' and authentication.type == 'basic' %}
api login: <input type="text" id="api_login" value=""/>
api password: <input type="text" id="api_pass" value=""/>
{% elseif authentication.delivery in ['query', 'http', 'header'] %}
api key: <input type="text" id="api_key" value=""/>
{% endif %}
{% if authentication.custom_endpoint %}
api endpoint: <input type="text" id="api_endpoint" value=""/>
{% endif %}
<button id="save_api_auth" type="button">Save</button>
<button id="clear_api_auth" type="button">Clear</button>
{% endif %}
</div>
{% endif %}
@ -51,22 +61,161 @@
</p>
<script type="text/javascript">
var getHash = function() {
return window.location.hash || '';
};
var setHash = function(hash) {
window.location.hash = hash;
};
var clearHash = function() {
var scrollTop, scrollLeft;
if(typeof history === 'object' && typeof history.pushState === 'function') {
history.replaceState('', document.title, window.location.pathname + window.location.search);
} else {
scrollTop = document.body.scrollTop;
scrollLeft = document.body.scrollLeft;
setHash('');
document.body.scrollTop = scrollTop;
document.body.scrollLeft = scrollLeft;
}
};
$(window).load(function() {
var id = (window.location.hash || '').substr(1).replace( /([:\.\[\]\{\}])/g, "\\$1");
var id = getHash().substr(1).replace( /([:\.\[\]\{\}|])/g, "\\$1");
var elem = $('#' + id);
if (elem.length) {
setTimeout(function() {
$('body,html').scrollTop(elem.position().top);
});
elem.find('a.toggler').click();
elem.find('.toggler').click();
var section = elem.parents('.section').first();
if (section) {
section.addClass('active');
section.find('.section-list').slideDown('fast');
}
}
{% if enableSandbox %}
loadStoredAuthParams();
{% endif %}
});
$('.toggler').click(function() {
$(this).next().slideToggle('fast');
$('.toggler').click(function(event) {
var contentContainer = $(this).next();
if(contentContainer.is(':visible')) {
clearHash();
} else {
setHash($(this).data('href'));
}
contentContainer.slideToggle('fast');
return false;
});
$('.action-show-hide, .section > h1').on('click', function(){
var section = $(this).parents('.section').first();
if (section.hasClass('active')) {
section.removeClass('active');
section.find('.section-list').slideUp('fast');
} else {
section.addClass('active');
section.find('.section-list').slideDown('fast');
}
});
$('.action-list').on('click', function(){
var section = $(this).parents('.section').first();
if (!section.hasClass('active')) {
section.addClass('active');
}
section.find('.section-list').slideDown('fast');
section.find('.operation > .content').slideUp('fast');
});
$('.action-expand').on('click', function(){
var section = $(this).parents('.section').first();
if (!section.hasClass('active')) {
section.addClass('active');
}
$(section).find('ul').slideDown('fast');
$(section).find('.operation > .content').slideDown('fast');
});
{% if enableSandbox %}
var getStoredValue, storeValue, deleteStoredValue;
var apiAuthKeys = ['api_key', 'api_login', 'api_pass', 'api_endpoint'];
if ('localStorage' in window) {
var buildKey = function (key) {
return 'nelmio_' + key;
}
getStoredValue = function (key) {
return localStorage.getItem(buildKey(key));
}
storeValue = function (key, value) {
localStorage.setItem(buildKey(key), value);
}
deleteStoredValue = function (key) {
localStorage.removeItem(buildKey(key));
}
} else {
getStoredValue = storeValue = deleteStoredValue = function (){};
}
var loadStoredAuthParams = function() {
$.each(apiAuthKeys, function(_, value) {
var elm = $('#' + value);
if (elm.length) {
elm.val(getStoredValue(value));
}
});
}
var setParameterType = function ($context,setType) {
// no 2nd argument, use default from parameters
if (typeof setType == "undefined") {
setType = $context.parent().attr("data-dataType");
$context.val(setType);
}
$context.parent().find('.value').remove();
var placeholder = "";
if ($context.parent().attr("data-dataType") != "" && typeof $context.parent().attr("data-dataType") != "undefined") {
placeholder += "[" + $context.parent().attr("data-dataType") + "] ";
}
if ($context.parent().attr("data-format") != "" && typeof $context.parent().attr("data-format") != "undefined") {
placeholder += $context.parent().attr("data-format");
}
if ($context.parent().attr("data-description") != "" && typeof $context.parent().attr("data-description") != "undefined") {
placeholder += $context.parent().attr("data-description");
} else {
placeholder += "Value";
}
switch(setType) {
case "boolean":
$('<select class="value"><option value=""></option><option value="1">True</option><option value="0">False</option></select>').insertAfter($context);
break;
case "file":
$('<input type="file" class="value" placeholder="'+ placeholder +'">').insertAfter($context);
break;
case "textarea":
$('<textarea class="value" placeholder="'+ placeholder +'" />').insertAfter($context);
break;
default:
$('<input type="text" class="value" placeholder="'+ placeholder +'">').insertAfter($context);
}
};
var toggleButtonText = function ($btn) {
if ($btn.text() === 'Default') {
$btn.text('Raw');
@ -97,7 +246,7 @@
$btn = $container.parents('.pane').find('.to-prettify');
$container.removeClass('prettyprinted');
$container.html(prettifyResponse(rawData));
$container.html(attachCollapseMarker(prettifyResponse(rawData)));
prettyPrint && prettyPrint();
$btn.removeClass('to-prettify');
@ -129,7 +278,26 @@
}
}
return body;
}
};
$('#save_api_auth').click(function(event) {
$.each(apiAuthKeys, function(_, value) {
var elm = $('#' + value);
if (elm.length) {
storeValue(value, elm.val());
}
});
});
$('#clear_api_auth').click(function(event) {
$.each(apiAuthKeys, function(_, value) {
deleteStoredValue(value);
var elm = $('#' + value);
if (elm.length) {
elm.val('');
}
});
});
$('.tabs li').click(function() {
var contentGroup = $(this).parents('.content');
@ -141,6 +309,22 @@
$(this).addClass('selected');
});
var getJsonCollapseHtml = function(sectionOpenCharacter) {
var $toggler = $('<span>').addClass('json-collapse-section').
attr('data-section-open-character', sectionOpenCharacter).
append($('<span>').addClass('json-collapse-marker')
.html('&#9663;')
).append(sectionOpenCharacter);
return $('<div>').append($toggler).html();
};
var attachCollapseMarker = function (prettifiedJsonString) {
prettifiedJsonString = prettifiedJsonString.replace(/(\{|\[)\n/g, function(match, sectionOpenCharacter) {
return getJsonCollapseHtml(sectionOpenCharacter) + '<span class="json-collapse-content">\n';
});
return prettifiedJsonString.replace(/([^\[][\}\]],?)\n/g, '$1</span>\n');
};
var prettifyResponse = function(text) {
try {
var data = typeof text === 'string' ? JSON.parse(text) : text;
@ -152,8 +336,34 @@
return $('<div>').text(text).html();
};
var displayFinalUrl = function(xhr, method, url, container) {
container.text(method + ' ' + url);
var displayFinalUrl = function(xhr, method, url, data, container) {
container.text(method + ' ' + getFinalUrl(method, url, data));
};
var displayRequestBody = function(method, data, container, header) {
if ('GET' != method && !jQuery.isEmptyObject(data) && data !== "" && data !== undefined) {
if (jQuery.type(data) !== 'string') {
data = decodeURIComponent(jQuery.param(data));
}
container.text(data);
container.show();
header.show();
} else {
container.hide();
header.hide();
}
};
var displayProfilerUrl = function(xhr, link, container) {
var profilerUrl = xhr.getResponseHeader('X-Debug-Token-Link');
if (profilerUrl) {
link.attr('href', profilerUrl);
container.show();
} else {
link.attr('href', '');
container.hide();
}
};
var displayResponseData = function(xhr, container) {
@ -174,45 +384,131 @@
container.text(text);
};
var displayResponse = function(xhr, method, url, result_container) {
displayFinalUrl(xhr, method, url, $('.url', result_container));
var displayCurl = function(method, url, headers, data, result_container) {
var escapeShell = function(param) {
param = "" + param;
return '"' + param.replace(/(["\s'$`\\])/g,'\\$1') + '"';
};
url = getFinalUrl(method, url, data);
var command = "curl";
command += " -X " + escapeShell(method);
if (method != "GET" && !jQuery.isEmptyObject(data) && data !== "" && data !== undefined) {
if (jQuery.type(data) !== 'string') {
data = decodeURIComponent(jQuery.param(data));
}
command += " -d " + escapeShell(data);
}
for (headerKey in headers) {
if (headers.hasOwnProperty(headerKey)) {
command += " -H " + escapeShell(headerKey + ': ' + headers[headerKey]);
}
}
command += " " + url;
result_container.text(command);
};
var getFinalUrl = function(method, url, data) {
if ('GET' == method && !jQuery.isEmptyObject(data)) {
var separator = url.indexOf('?') >= 0 ? '&' : '?';
url = url + separator + decodeURIComponent(jQuery.param(data));
}
return url;
};
var displayResponse = function(xhr, method, url, headers, data, result_container) {
displayFinalUrl(xhr, method, url, data, $('.url', result_container));
displayRequestBody(method, data, $('.request-body', result_container), $('.request-body-header', result_container));
displayProfilerUrl(xhr, $('.profiler-link', result_container), $('.profiler', result_container));
displayResponseData(xhr, $('.response', result_container));
displayResponseHeaders(xhr, $('.headers', result_container));
displayCurl(method, url, headers, data, $('.curl-command', result_container));
result_container.show();
};
$('.pane.sandbox form').submit(function() {
var url = $(this).attr('action'),
method = $(this).attr('method'),
method = $('[name="header_method"]', this).val(),
self = this,
params = {},
filters = {},
formData = new FormData(),
doubledParams = {},
doubledFilters = {},
headers = {},
content = $(this).find('textarea.content').val(),
result_container = $('.result', $(this).parent());
if (method === 'ANY') {
method = 'POST';
} else if (method.indexOf('|') !== -1) {
method = method.split('|').sort().pop();
}
// set default requestFormat
var requestFormat = $('#request_format').val();
// set requestFormat
var requestFormatMethod = '{{ requestFormatMethod }}';
if (requestFormatMethod == 'format_param') {
params['_format'] = requestFormat;
params['_format'] = $('#request_format option:selected').text();
formData.append('_format',$('#request_format option:selected').text());
} else if (requestFormatMethod == 'accept_header') {
headers['Accept'] = 'application/' + requestFormat;
headers['Accept'] = $('#request_format').val();
}
// set default bodyFormat
var bodyFormat = $('#body_format').val();
var bodyFormat = $('#body_format').val() || '{{ defaultBodyFormat }}';
if(!('Content-type' in headers)) {
headers['Content-type'] = 'application/'+bodyFormat;
if (bodyFormat == 'form') {
headers['Content-type'] = 'application/x-www-form-urlencoded';
} else {
headers['Content-type'] = 'application/json';
}
}
var hasFileTypes = false;
$('.parameters .tuple_type', $(this)).each(function() {
if ($(this).val() == 'file') {
hasFileTypes = true;
}
});
if (hasFileTypes && method != 'POST') {
alert("Sorry, you can only submit files via POST.");
return false;
}
if (hasFileTypes && bodyFormat != 'form') {
alert("Body Format must be set to 'Form Data' when utilizing file upload type parameters.\nYour current bodyFormat is '" + bodyFormat + "'. Change your BodyFormat or do not use file type\nparameters.");
return false;
}
if (hasFileTypes) {
// retrieve all the parameters to send for file upload
$('.parameters .tuple', $(this)).each(function() {
var key, value;
key = $('.key', $(this)).val();
if ($('.value', $(this)).attr('type') === 'file' ) {
value = $('.value', $(this)).prop('files')[0];
if(!value) {
value = new File([], '');
}
} else {
value = $('.value', $(this)).val();
}
if (value) {
formData.append(key,value);
}
});
}
// retrieve all the parameters to send
$('.parameters .tuple', $(this)).each(function() {
var key, value;
@ -221,10 +517,43 @@
value = $('.value', $(this)).val();
if (value) {
params[key] = value;
// convert boolean values to boolean
if ('json' === bodyFormat && 'boolean' === $('.tuple_type', $(this)).val()) {
value = '1' === value;
}
// temporary save all additional/doubled parameters
if (key in params) {
doubledParams[key] = value;
} else {
params[key] = value;
}
}
});
// retrieve all the filters to send
$('.parameters .tuple.filter', $(this)).each(function() {
var key, value;
key = $('.key', $(this)).val();
value = $('.value', $(this)).val();
if (value) {
// temporary save all additional/doubled parameters
if (key in filters) {
doubledFilters[key] = value;
} else {
filters[key] = value;
}
}
});
// retrieve the additional headers to send
$('.headers .tuple', $(this)).each(function() {
var key, value;
@ -246,13 +575,19 @@
}
};
// merge additional params back to real params object
if (!$.isEmptyObject(doubledParams)) {
$.extend(params, doubledParams);
}
// disable all the fiels and buttons
$('input, button', $(this)).attr('disabled', 'disabled');
// append the query authentication
if (authentication_delivery == 'query') {
var api_key_val = $('#api_key').val();
if (authentication_delivery == 'query' && api_key_val.length>0) {
url += url.indexOf('?') > 0 ? '&' : '?';
url += api_key_parameter + '=' + $('#api_key').val();
url += api_key_parameter + '=' + api_key_val;
}
// prepare the api enpoint
@ -261,9 +596,23 @@
{% else -%}
var endpoint = '{{ endpoint }}';
{% endif -%}
if ($('#api_endpoint') && $('#api_endpoint').val() != null) {
{% if authentication and authentication.custom_endpoint %}
if ($('#api_endpoint') && typeof($('#api_endpoint').val()) != 'undefined') {
endpoint = $('#api_endpoint').val();
}
{% endif %}
//add filters as GET params and remove them from params
if(method != 'GET'){
for (var filterKey in $.extend({}, filters)){
url += url.indexOf('?') > 0 ? '&' : '?';
url += filterKey + '=' + filters[filterKey];
if (params.hasOwnProperty(filterKey)){
delete(params[filterKey]);
}
}
}
// prepare final parameters
var body = {};
@ -274,39 +623,78 @@
body = params;
}
var data = content.length ? content : body;
// and trigger the API call
$.ajax({
url: endpoint + url,
var ajaxOptions = {
url: (url.indexOf('http')!=0?endpoint:'') + url,
xhrFields: { withCredentials: true },
type: method,
data: data,
headers: headers,
crossDomain: true,
beforeSend: function (xhr) {
if (authentication_delivery == 'http_basic') {
xhr.setRequestHeader('Authorization', 'Basic ' + btoa($('#api_key').val() + ':' + $('#api_pass').val()));
}else if(authentication_delivery == 'header') {
xhr.setRequestHeader(api_key_parameter, $('#api_key').val());
if (authentication_delivery) {
var value;
if ('http' == authentication_delivery) {
if ('basic' == authentication_type) {
value = 'Basic ' + btoa($('#api_login').val() + ':' + $('#api_pass').val());
} else if ('bearer' == authentication_type) {
value = 'Bearer ' + $('#api_key').val();
}
} else if ('header' == authentication_delivery) {
value = $('#api_key').val();
}
xhr.setRequestHeader(api_key_parameter, value);
}
},
complete: function(xhr) {
displayResponse(xhr, method, url, result_container);
displayResponse(xhr, method, url, headers, data, result_container);
// and enable them back
$('input:not(.content-type), button', $(self)).removeAttr('disabled');
}
});
};
// overrides body format to send data properly
if (hasFileTypes) {
ajaxOptions.data = formData;
ajaxOptions.processData = false;
ajaxOptions.contentType = false;
delete(headers['Content-type']);
}
// and trigger the API call
$.ajax(ajaxOptions);
return false;
});
$('.operations').on('click', '.operation > a', function(e) {
$('.operations').on('click', '.operation > .heading', function(e) {
if (history.pushState) {
history.pushState(null, null, $(this).attr('href'));
history.pushState(null, null, $(this).data('href'));
e.preventDefault();
}
});
$(document).on('click', '.json-collapse-section', function() {
var openChar = $(this).data('section-open-character'),
closingChar = (openChar == '{' ? '}' : ']');
if ($(this).next('.json-collapse-content').is(':visible')) {
$(this).html('&oplus;' + openChar + '...' + closingChar);
} else {
$(this).html('&#9663;' + $(this).data('section-open-character'));
}
$(this).next('.json-collapse-content').toggle();
});
$(document).on('copy', '.prettyprinted', function () {
var $toggleMarkers = $(this).find('.json-collapse-marker');
$toggleMarkers.hide();
setTimeout(function () {
$toggleMarkers.show();
}, 100);
});
$('.pane.sandbox').on('click', '.to-raw', function(e) {
renderRawBody($(this).parents('.pane').find('.response'));
@ -336,8 +724,30 @@
e.preventDefault();
});
$('.pane.sandbox').on('click', '.add', function() {
var html = $(this).parents('.pane').find('.tuple_template').html();
// sets the correct parameter type on load
$('.pane.sandbox .tuple_type').each(function() {
setParameterType($(this));
});
// handles parameter type change
$('.pane.sandbox').on('change', '.tuple_type', function() {
setParameterType($(this),$(this).val());
});
$('.pane.sandbox').on('click', '.add_parameter', function() {
var html = $(this).parents('.pane').find('.parameters_tuple_template').html();
$(this).before(html);
return false;
});
$('.pane.sandbox').on('click', '.add_header', function() {
var html = $(this).parents('.pane').find('.headers_tuple_template').html();
$(this).before(html);
@ -378,8 +788,10 @@
});
{% if authentication and authentication.delivery == 'http_basic' %}
{% if authentication and authentication.delivery == 'http' %}
var authentication_delivery = '{{ authentication.delivery }}';
var api_key_parameter = '{{ authentication.name }}';
var authentication_type = '{{ authentication.type }}';
{% elseif authentication and authentication.delivery == 'query' %}
var authentication_delivery = '{{ authentication.delivery }}';
var api_key_parameter = '{{ authentication.name }}';

View file

@ -1,5 +1,5 @@
<li class="{{ data.method|lower }} operation" id="{{ data.id }}">
<a class="heading toggler{% if data.deprecated %} deprecated{% endif %}" href="#{{ data.id }}">
<div class="heading toggler{% if data.deprecated %} deprecated{% endif %}" data-href="#{{ data.id }}">
<h3>
<span class="http_method">
<i>{{ data.method|upper }}</i>
@ -11,13 +11,6 @@
</span>
{% endif %}
{% if data.https %}
<span class="icon lock" title="HTTPS"></span>
{% endif %}
{% if data.authentication %}
<span class="icon keys" title="Needs {{ data.authenticationRoles|length > 0 ? data.authenticationRoles|join(', ') : 'authentication' }}"></span>
{% endif %}
<span class="path">
{% if data.host is defined -%}
{{ data.https ? 'https://' : 'http://' -}}
@ -25,18 +18,23 @@
{% endif -%}
{{ data.uri }}
</span>
{% if data.tags is defined %}
{% for tag, color_code in data.tags %}
<span class="tag" {% if color_code is defined and color_code is not empty %}style="background-color:{{ color_code }};"{% endif %}>{{ tag }}</span>
{% endfor %}
{% endif %}
</h3>
<ul class="options">
{% if data.description is defined %}
<li>{{ data.description }}</li>
{% endif %}
</ul>
</a>
</div>
<div class="content" style="display: {% if displayContent is defined and displayContent == true %}display{% else %}none{% endif %};">
<ul class="tabs">
<li class="selected" data-pane="content">Documentation</li>
{% if enableSandbox %}
<li class="selected" data-pane="content">Documentation</li>
<li data-pane="sandbox">Sandbox</li>
{% endif %}
</ul>
@ -53,7 +51,7 @@
<div><a href="{{ data.link }}" target="_blank">{{ data.link }}</a></div>
{% endif %}
{% if data.requirements is defined and data.requirements is not empty %}
{% if data.requirements is defined and data.requirements is not empty %}
<h4>Requirements</h4>
<table class="fullwidth">
<thead>
@ -95,7 +93,7 @@
{% for key, value in infos %}
<tr>
<td>{{ key|title }}</td>
<td>{{ value|json_encode|replace({'\\\\': '\\'})|trim('"') }}</td>
<td>{{ value|json_encode(constant('JSON_UNESCAPED_UNICODE'))|replace({'\\\\': '\\'})|trim('"') }}</td>
</tr>
{% endfor %}
</table>
@ -125,8 +123,8 @@
<td>{{ name }}</td>
<td>{{ infos.dataType is defined ? infos.dataType : '' }}</td>
<td>{{ infos.required ? 'true' : 'false' }}</td>
<td>{{ infos.format }}</td>
<td>{{ infos.description is defined ? infos.description : '' }}</td>
<td class="format">{{ infos.format }}</td>
<td>{{ infos.description is defined ? infos.description|trans : '' }}</td>
</tr>
{% endif %}
{% endfor %}
@ -134,27 +132,62 @@
</table>
{% endif %}
{% if data.response is defined and data.response is not empty %}
{% if data.headers is defined and data.headers is not empty %}
<h4>Headers</h4>
<table class="fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Required?</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for name, infos in data.headers %}
<tr>
<td>{{ name }}</td>
<td>{{ infos.required is defined and infos.required == 'true' ? 'true' : 'false'}}</td>
<td>{{ infos.description is defined ? infos.description|trans : ''}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if data.parsedResponseMap is defined and data.parsedResponseMap is not empty %}
<h4>Return</h4>
<table class='fullwidth'>
<thead>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Versions</th>
<th>Description</th>
</tr>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Versions</th>
<th>Description</th>
</tr>
</thead>
{% for status_code, response in data.parsedResponseMap %}
<tbody>
{% for name, infos in data.response %}
<tr>
<td>{{ name }}</td>
<td>{{ infos.dataType }}</td>
<td>{% include 'NelmioApiDocBundle:Components:version.html.twig' with {'sinceVersion': infos.sinceVersion, 'untilVersion': infos.untilVersion} only %}</td>
<td>{{ infos.description }}</td>
</tr>
{% endfor %}
<tr>
<td>
<h4>
{{ status_code }}
{% if data.statusCodes is defined and data.statusCodes[status_code] is defined %}
- {{ data.statusCodes[status_code]|join(', ') }}
{% endif %}
</h4>
</td>
</tr>
{% for name, infos in response.model %}
<tr>
<td>{{ name }}</td>
<td>{{ infos.dataType }}</td>
<td>{% include '@NelmioApiDoc/Components/version.html.twig' with {'sinceVersion': infos.sinceVersion, 'untilVersion': infos.untilVersion} only %}</td>
<td>{{ infos.description }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
{% endif %}
@ -170,7 +203,7 @@
<tbody>
{% for status_code, descriptions in data.statusCodes %}
<tr>
<td><a href="http://en.wikipedia.org/wiki/HTTP_{{ status_code }}" target="_blank">{{ status_code }}<a/></td>
<td><a href="http://en.wikipedia.org/wiki/HTTP_{{ status_code }}" target="_blank">{{ status_code }}</a></td>
<td>
<ul>
{% for description in descriptions %}
@ -194,9 +227,9 @@
{% if enableSandbox %}
<div class="pane sandbox">
{% if app.request is not null and data.https and app.request.secure != data.https %}
Please reload the documentation using the scheme {% if data.https %}HTTPS{% else %}HTTP{% endif %} if you want to use the sandbox.
Please reload the documentation using the scheme HTTP if you want to use the sandbox.
{% else %}
<form method="{{ data.method|upper }}" action="{% if data.host is defined %}{{ data.https ? 'https://' : 'http://' }}{{ data.host }}{% endif %}{{ data.uri }}">
<form method="" action="{% if data.host is defined %}http://{{ data.host }}{% endif %}{{ data.uri }}">
<fieldset class="parameters">
<legend>Input</legend>
{% if data.requirements is defined %}
@ -212,7 +245,7 @@
{% if data.filters is defined %}
<h4>Filters</h4>
{% for name, infos in data.filters %}
<p class="tuple">
<p class="tuple filter">
<input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="{% if infos.description is defined %}{{ infos.description }}{% else %}Value{% endif %}" {% if infos.default is defined %} value="{{ infos.default }}" {% endif %}/> <span class="remove">-</span>
@ -223,19 +256,38 @@
<h4>Parameters</h4>
{% for name, infos in data.parameters %}
{% if not infos.readonly %}
<p class="tuple">
<p class="tuple" data-dataType="{% if infos.dataType %}{{ infos.dataType }}{% endif %}" data-format="{% if infos.format %}{{ infos.format }} {% endif %}" data-description="{% if infos.description %}{{ infos.description|trans }}{% endif %}">
<input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="{% if infos.dataType %}[{{ infos.dataType }}] {% endif %}{% if infos.format %}{{ infos.format }}{% endif %}{% if infos.description %}{{ infos.description }}{% else %}Value{% endif %}" {% if infos.default is defined %} value="{{ infos.default }}" {% endif %}/> <span class="remove">-</span>
<select class="tuple_type">
<option value="">Type</option>
<option value="string">String</option>
<option value="boolean">Boolean</option>
<option value="file">File</option>
<option value="textarea">Textarea</option>
</select>
<input type="text" class="value" placeholder="{% if infos.dataType %}[{{ infos.dataType }}] {% endif %}{% if infos.format %}{{ infos.format }}{% endif %}{% if infos.description %}{{ infos.description|trans }}{% else %}Value{% endif %}" {% if infos.default is defined %} value="{{ infos.default }}" {% endif %}/> <span class="remove">-</span>
</p>
{% endif %}
{% endfor %}
<button class="add">New parameter</button>
<button type="button" class="add_parameter">New parameter</button>
{% endif %}
</fieldset>
<fieldset class="headers">
{% set methods = data.method|upper|split('|') %}
{% if methods|length > 1 %}
<legend>Method</legend>
<select name="header_method">
{% for method in methods %}
<option value="{{ method }}">{{ method }}</option>
{% endfor %}
</select>
{% else %}
<input type="hidden" name="header_method" value="{{ methods|join }}" />
{% endif %}
<legend>Headers</legend>
{% if acceptType %}
@ -246,13 +298,25 @@
</p>
{% endif %}
{% if data.headers is defined %}
{% for name, infos in data.headers %}
<p class="tuple">
<input type="text" class="key" value="{{ name }}" />
<span>=</span>
<input type="text" class="value" value="{% if infos.default is defined %}{{ infos.default }}{% endif %}" placeholder="Value" /> <span class="remove">-</span>
</p>
{% endfor %}
{% endif %}
<p class="tuple">
<input type="text" class="key" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="Value" /> <span class="remove">-</span>
</p>
<button class="add">New header</button>
<button type="button" class="add_header">New header</button>
</fieldset>
<fieldset class="request-content">
@ -264,7 +328,7 @@
<input type="text" class="key content-type" value="Content-Type" disabled="disabled" />
<span>=</span>
<input type="text" class="value" placeholder="Value" />
<button class="set-content-type">Set header</button> <small>Replaces header if set</small>
<button type="button" class="set-content-type">Set header</button> <small>Replaces header if set</small>
</p>
</fieldset>
@ -273,7 +337,22 @@
</div>
</form>
<script type="text/x-tmpl" class="tuple_template">
<script type="text/x-tmpl" class="parameters_tuple_template">
<p class="tuple">
<input type="text" class="key" placeholder="Key" />
<span>=</span>
<select class="tuple_type">
<option value="">Type</option>
<option value="string">String</option>
<option value="boolean">Boolean</option>
<option value="file">File</option>
<option value="textarea">Textarea</option>
</select>
<input type="text" class="value" placeholder="Value" /> <span class="remove">-</span>
</p>
</script>
<script type="text/x-tmpl" class="headers_tuple_template">
<p class="tuple">
<input type="text" class="key" placeholder="Key" />
<span>=</span>
@ -281,15 +360,22 @@
</p>
</script>
<div class="result">
<h4>Request URL</h4>
<pre class="url"></pre>
<h4>Response Headers&nbsp;<small>[<a href="" class="to-expand">Expand</a>]</small></h4>
<h4 class="request-body-header">Request body</h4>
<pre class="request-body"></pre>
<h4>Response Headers&nbsp;<small>[<a href="" class="to-expand">Expand</a>]</small>&nbsp;<small class="profiler">[<a href="" class="profiler-link" target="_blank">Profiler</a>]</small></h4>
<pre class="headers to-expand"></pre>
<h4>Response Body&nbsp;<small>[<a href="" class="to-raw">Raw</a>]</small></h4>
<pre class="response prettyprint"></pre>
<h4>Curl Command Line</h4>
<pre class="curl-command"></pre>
</div>
{% endif %}
</div>

View file

@ -1,11 +1,11 @@
{% extends "NelmioApiDocBundle::layout.html.twig" %}
{% extends "@NelmioApiDoc/layout.html.twig" %}
{% block content %}
<li class="resource">
<ul class="endpoints">
<li class="endpoint">
<ul class="operations">
{% include 'NelmioApiDocBundle::method.html.twig' %}
{% include '@NelmioApiDoc/method.html.twig' %}
</ul>
</li>
</ul>

View file

@ -1,12 +1,26 @@
{% extends "NelmioApiDocBundle::layout.html.twig" %}
{% extends "@NelmioApiDoc/layout.html.twig" %}
{% block content %}
<div id="summary">
<ul>
{% for section, sections in resources %}
<li><a href="#section-{{ section }}">{{ section }}</a></li>
{% endfor %}
</ul>
</div>
{% for section, sections in resources %}
{% if section != '_others' %}
<div id="section">
<h1>{{ section }}</h1>
<li class="section{{ defaultSectionsOpened? ' active':'' }}">
<div class="actions">
<a class="action-show-hide">Show/hide</a>
<a class="action-list">List Operations</a>
<a class="action-expand">Expand Operations</a>
</div>
<h1>{{ section }}</h1>
<ul class="section-list" {% if not defaultSectionsOpened %}style="display: none"{% endif %}>
{% endif %}
{% for resource, methods in sections %}
<a id="section-{{ section }}"></a>
<li class="resource">
<div class="heading">
{% if section == '_others' and resource != 'others' %}
@ -19,7 +33,7 @@
<li class="endpoint">
<ul class="operations">
{% for data in methods %}
{% include 'NelmioApiDocBundle::method.html.twig' %}
{% include '@NelmioApiDoc/method.html.twig' %}
{% endfor %}
</ul>
</li>
@ -27,7 +41,8 @@
</li>
{% endfor %}
{% if section != '_others' %}
</div>
</ul>
</li>
{% endif %}
{% endfor %}
{% endblock content %}

236
Swagger/ModelRegistry.php Normal file
View file

@ -0,0 +1,236 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Swagger;
use Nelmio\ApiDocBundle\DataTypes;
/**
* Class ModelRegistry
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class ModelRegistry
{
/**
* @var array
*/
protected $namingStrategies = [
'dot_notation' => 'nameDotNotation',
'last_segment_only' => 'nameLastSegmentOnly',
];
/**
* @var array
*/
protected $models = [];
protected $classes = [];
/**
* @var callable
*/
protected $namingStategy;
protected $typeMap = [
DataTypes::INTEGER => 'integer',
DataTypes::FLOAT => 'number',
DataTypes::STRING => 'string',
DataTypes::BOOLEAN => 'boolean',
DataTypes::FILE => 'string',
DataTypes::DATE => 'string',
DataTypes::DATETIME => 'string',
];
protected $formatMap = [
DataTypes::INTEGER => 'int32',
DataTypes::FLOAT => 'float',
DataTypes::FILE => 'byte',
DataTypes::DATE => 'date',
DataTypes::DATETIME => 'date-time',
];
public function __construct($namingStrategy)
{
if (!isset($this->namingStrategies[$namingStrategy])) {
throw new \InvalidArgumentException(sprintf(
'Invalid naming strategy. Choose from: %s',
json_encode(array_keys($this->namingStrategies))
));
}
$this->namingStategy = [$this, $this->namingStrategies[$namingStrategy]];
}
public function register($className, ?array $parameters = null, $description = '')
{
if (!isset($this->classes[$className])) {
$this->classes[$className] = [];
}
$id = call_user_func_array($this->namingStategy, [$className]);
if (isset($this->models[$id])) {
return $id;
}
$this->classes[$className][] = $id;
$model = [
'id' => $id,
'description' => $description,
];
if (is_array($parameters)) {
$required = [];
$properties = [];
foreach ($parameters as $name => $prop) {
$subParam = [];
if (DataTypes::MODEL === $prop['actualType']) {
$subParam['$ref'] = $this->register(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
} else {
$type = null;
$format = null;
$items = null;
$enum = null;
$ref = null;
if (isset($this->typeMap[$prop['actualType']])) {
$type = $this->typeMap[$prop['actualType']];
} else {
switch ($prop['actualType']) {
case DataTypes::ENUM:
$type = 'string';
if (isset($prop['format'])) {
$enum = array_keys(json_decode($prop['format'], true));
}
break;
case DataTypes::COLLECTION:
$type = 'array';
if (null === $prop['subType']) {
$items = [
'type' => 'string',
];
} elseif (isset($this->typeMap[$prop['subType']])) {
$items = [
'type' => $this->typeMap[$prop['subType']],
];
} elseif (!isset($this->typeMap[$prop['subType']])) {
$items = [
'$ref' => $this->register(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
),
];
}
/* @TODO: Handle recursion if subtype is a model. */
break;
case DataTypes::MODEL:
$ref = $this->register(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
$type = $ref;
/* @TODO: Handle recursion. */
break;
}
}
if (isset($this->formatMap[$prop['actualType']])) {
$format = $this->formatMap[$prop['actualType']];
}
$subParam = [
'type' => $type,
'description' => false === empty($prop['description']) ? (string) $prop['description'] : $prop['dataType'],
];
if (null !== $format) {
$subParam['format'] = $format;
}
if (null !== $enum) {
$subParam['enum'] = $enum;
}
if (null !== $ref) {
$subParam['$ref'] = $ref;
}
if (null !== $items) {
$subParam['items'] = $items;
}
if ($prop['required']) {
$required[] = $name;
}
}
$properties[$name] = $subParam;
}
$model['properties'] = $properties;
$model['required'] = $required;
$this->models[$id] = $model;
}
return $id;
}
public function nameDotNotation($className)
{
/*
* Converts \Fully\Qualified\Class\Name to Fully.Qualified.Class.Name
* "[...]" in aliased and non-aliased collections preserved.
*/
$id = preg_replace('#(\\\|[^A-Za-z0-9\[\]])#', '.', $className);
// Replace duplicate dots.
$id = preg_replace('/\.+/', '.', $id);
// Replace trailing dots.
$id = preg_replace('/^\./', '', $id);
return $id;
}
public function nameLastSegmentOnly($className)
{
/*
* Converts \Fully\Qualified\ClassName to ClassName
*/
$segments = explode('\\', $className);
$id = end($segments);
return $id;
}
public function getModels()
{
return $this->models;
}
public function clear(): void
{
$this->models = [];
$this->classes = [];
}
}

View file

@ -11,38 +11,32 @@
namespace Nelmio\ApiDocBundle\Tests\Annotation;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Tests\TestCase;
use Symfony\Component\Routing\Route;
class ApiDocTest extends TestCase
{
public function testConstructWithoutData()
public function testConstructWithoutData(): void
{
$data = array();
$annot = new ApiDoc($data);
$annot = new ApiDoc();
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource());
$this->assertEmpty($annot->getViews());
$this->assertFalse($annot->getDeprecated());
$this->assertFalse(isset($array['description']));
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertNull($annot->getInput());
$this->assertFalse($array['authentication']);
$this->assertTrue(is_array($array['authenticationRoles']));
$this->assertFalse(isset($array['headers']));
}
public function testConstructWithInvalidData()
public function testConstructWithInvalidData(): void
{
$data = array(
'unknown' => 'foo',
'array' => array('bar' => 'bar'),
);
$annot = new ApiDoc($data);
$annot = new ApiDoc();
$array = $annot->toArray();
$this->assertTrue(is_array($array));
@ -55,13 +49,13 @@ class ApiDocTest extends TestCase
$this->assertNull($annot->getInput());
}
public function testConstruct()
public function testConstruct(): void
{
$data = array(
$data = [
'description' => 'Heya',
);
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(description: $data['description']);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
@ -74,14 +68,17 @@ class ApiDocTest extends TestCase
$this->assertNull($annot->getInput());
}
public function testConstructDefinesAFormType()
public function testConstructDefinesAFormType(): void
{
$data = array(
'description' => 'Heya',
'input' => 'My\Form\Type',
);
$data = [
'description' => 'Heya',
'input' => 'My\Form\Type',
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(
description: $data['description'],
input: $data['input']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
@ -94,16 +91,21 @@ class ApiDocTest extends TestCase
$this->assertEquals($data['input'], $annot->getInput());
}
public function testConstructMethodIsResource()
public function testConstructMethodIsResource(): void
{
$data = array(
'resource' => true,
'description' => 'Heya',
'deprecated' => true,
'input' => 'My\Form\Type',
);
$data = [
'resource' => true,
'description' => 'Heya',
'deprecated' => true,
'input' => 'My\Form\Type',
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(
resource: $data['resource'],
description: $data['description'],
deprecated: $data['deprecated'],
input: $data['input']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
@ -116,16 +118,21 @@ class ApiDocTest extends TestCase
$this->assertEquals($data['input'], $annot->getInput());
}
public function testConstructMethodResourceIsFalse()
public function testConstructMethodResourceIsFalse(): void
{
$data = array(
'resource' => false,
'description' => 'Heya',
'deprecated' => false,
'input' => 'My\Form\Type',
);
$data = [
'resource' => false,
'description' => 'Heya',
'deprecated' => false,
'input' => 'My\Form\Type',
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(
resource: $data['resource'],
description: $data['description'],
deprecated: $data['deprecated'],
input: $data['input']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
@ -138,24 +145,29 @@ class ApiDocTest extends TestCase
$this->assertEquals($data['input'], $annot->getInput());
}
public function testConstructMethodHasFilters()
public function testConstructMethodHasFilters(): void
{
$data = array(
'resource' => true,
'deprecated' => false,
'description' => 'Heya',
'filters' => array(
array('name' => 'a-filter'),
),
);
$data = [
'resource' => true,
'deprecated' => false,
'description' => 'Heya',
'filters' => [
['name' => 'a-filter'],
],
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(
resource: $data['resource'],
description: $data['description'],
deprecated: $data['deprecated'],
filters: $data['filters']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['filters']));
$this->assertCount(1, $array['filters']);
$this->assertEquals(array('a-filter' => array()), $array['filters']);
$this->assertEquals(['a-filter' => []], $array['filters']);
$this->assertTrue($annot->isResource());
$this->assertEquals($data['description'], $array['description']);
$this->assertFalse(isset($array['requirements']));
@ -164,104 +176,66 @@ class ApiDocTest extends TestCase
$this->assertNull($annot->getInput());
}
/**
* @expectedException \InvalidArgumentException
*/
public function testConstructMethodHasFiltersWithoutName()
public function testConstructMethodHasFiltersWithoutName(): void
{
$data = array(
'description' => 'Heya',
'filters' => array(
array('parameter' => 'foo'),
),
);
$this->expectException(\InvalidArgumentException::class);
$annot = new ApiDoc($data);
}
public function testConstructNoFiltersIfFormTypeDefined()
{
$data = array(
'resource' => true,
'description' => 'Heya',
'input' => 'My\Form\Type',
'filters' => array(
array('name' => 'a-filter'),
),
);
$annot = new ApiDoc($data);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters']));
$this->assertTrue($annot->isResource());
$this->assertEquals($data['description'], $array['description']);
$this->assertEquals($data['input'], $annot->getInput());
}
public function testConstructWithStatusCodes()
{
$data = array(
$data = [
'description' => 'Heya',
'statusCodes' => array(
200 => "Returned when successful",
403 => "Returned when the user is not authorized",
404 => array(
"Returned when the user is not found",
"Returned when when something else is not found"
)
)
);
'filters' => [
['parameter' => 'foo'],
],
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(
description: $data['description'],
filters: $data['filters']
);
}
public function testConstructWithStatusCodes(): void
{
$data = [
'description' => 'Heya',
'statusCodes' => [
200 => 'Returned when successful',
403 => 'Returned when the user is not authorized',
404 => [
'Returned when the user is not found',
'Returned when when something else is not found',
],
],
];
$annot = new ApiDoc(
description: $data['description'],
statusCodes: $data['statusCodes']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['statusCodes']));
foreach ($data['statusCodes'] as $code => $message) {
$this->assertEquals($array['statusCodes'][$code], !is_array($message) ? array($message) : $message);
$this->assertEquals($array['statusCodes'][$code], !is_array($message) ? [$message] : $message);
}
}
public function testConstructWithAuthentication()
public function testConstructWithRequirements(): void
{
$data = array(
'authentication' => true
);
$annot = new ApiDoc($data);
$array = $annot->toArray();
$this->assertTrue($array['authentication']);
}
public function testConstructWithCache()
{
$data = array(
'cache' => '60'
);
$annot = new ApiDoc($data);
$array = $annot->toArray();
$this->assertEquals($data['cache'], $array['cache']);
}
public function testConstructWithRequirements()
{
$data = array(
'requirements' => array(
array(
$data = [
'requirements' => [
[
'name' => 'fooId',
'requirement' => '\d+',
'dataType' => 'integer',
'description' => 'This requirement might be used withing action method directly from Request object'
)
)
);
'description' => 'This requirement might be used withing action method directly from Request object',
],
],
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(
requirements: $data['requirements']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
@ -269,23 +243,168 @@ class ApiDocTest extends TestCase
$this->assertTrue(isset($array['requirements']['fooId']['dataType']));
}
public function testConstructWithParameters()
public function testConstructWithParameters(): void
{
$data = array(
'parameters' => array(
array(
$data = [
'parameters' => [
[
'name' => 'fooId',
'dataType' => 'integer',
'description' => 'Some description'
)
)
);
'description' => 'Some description',
],
],
];
$annot = new ApiDoc($data);
$annot = new ApiDoc(
parameters: $data['parameters']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(isset($array['parameters']['fooId']));
$this->assertTrue(isset($array['parameters']['fooId']['dataType']));
}
public function testConstructWithHeaders(): void
{
$data = [
'headers' => [
[
'name' => 'headerName',
'description' => 'Some description',
],
],
];
$annot = new ApiDoc(
headers: $data['headers']
);
$array = $annot->toArray();
$this->assertArrayHasKey('headerName', $array['headers']);
$this->assertNotEmpty($array['headers']['headerName']);
$keys = array_keys($array['headers']);
$this->assertEquals($data['headers'][0]['name'], $keys[0]);
$this->assertEquals($data['headers'][0]['description'], $array['headers']['headerName']['description']);
}
public function testConstructWithOneTag(): void
{
$data = [
'tags' => 'beta',
];
$annot = new ApiDoc(
tags: $data['tags']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['tags']), 'Single tag should be put in array');
$this->assertEquals(['beta'], $array['tags']);
}
public function testConstructWithOneTagAndColorCode(): void
{
$data = [
'tags' => [
'beta' => '#ff0000',
],
];
$annot = new ApiDoc(
tags: $data['tags']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['tags']), 'Single tag should be put in array');
$this->assertEquals(['beta' => '#ff0000'], $array['tags']);
}
public function testConstructWithMultipleTags(): void
{
$data = [
'tags' => [
'experimental' => '#0000ff',
'beta' => '#0000ff',
],
];
$annot = new ApiDoc(
tags: $data['tags']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['tags']), 'Tags should be in array');
$this->assertEquals($data['tags'], $array['tags']);
}
public function testAlignmentOfOutputAndResponseModels(): void
{
$data = [
'output' => 'FooBar',
'responseMap' => [
400 => 'Foo\\ValidationErrorCollection',
],
];
$apiDoc = new ApiDoc(
output: $data['output'],
responseMap: $data['responseMap']
);
$map = $apiDoc->getResponseMap();
$this->assertCount(2, $map);
$this->assertArrayHasKey(200, $map);
$this->assertArrayHasKey(400, $map);
$this->assertEquals($data['output'], $map[200]);
}
public function testAlignmentOfOutputAndResponseModels2(): void
{
$data = [
'responseMap' => [
200 => 'FooBar',
400 => 'Foo\\ValidationErrorCollection',
],
];
$apiDoc = new ApiDoc(
responseMap: $data['responseMap']
);
$map = $apiDoc->getResponseMap();
$this->assertCount(2, $map);
$this->assertArrayHasKey(200, $map);
$this->assertArrayHasKey(400, $map);
$this->assertEquals($apiDoc->getOutput(), $map[200]);
}
public function testSetRoute(): void
{
$route = new Route(
'/path/{foo}',
[
'foo' => 'bar',
'nested' => [
'key1' => 'value1',
'key2' => 'value2',
],
],
[],
[],
'{foo}.awesome_host.com'
);
$apiDoc = new ApiDoc();
$apiDoc->setRoute($route);
$this->assertSame($route, $apiDoc->getRoute());
$this->assertEquals('bar.awesome_host.com', $apiDoc->getHost());
$this->assertEquals('ANY', $apiDoc->getMethod());
}
}

View file

@ -0,0 +1,108 @@
<?php
/*
* 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.
*/
namespace NelmioApiDocBundle\Tests\Command;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\ApplicationTester;
use Symfony\Component\PropertyAccess\PropertyAccess;
class DumpCommandTest extends WebTestCase
{
/**
* @dataProvider viewProvider
*
* @param string $view Command view option value
* @param array $expectedMethodsCount Expected resource methods count
* @param array $expectedMethodValues Expected resource method values
*/
public function testDumpWithViewOption($view, array $expectedMethodsCount, array $expectedMethodValues): void
{
$this->getContainer();
$application = new Application(static::$kernel);
$application->setCatchExceptions(false);
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$input = [
'command' => 'api:doc:dump',
'--view' => $view,
'--format' => 'json',
];
$tester->run($input);
$display = $tester->getDisplay();
$this->assertJson($display);
$json = json_decode($display);
$accessor = PropertyAccess::createPropertyAccessor();
foreach ($expectedMethodsCount as $propertyPath => $expectedCount) {
$this->assertCount($expectedCount, $accessor->getValue($json, $propertyPath));
}
foreach ($expectedMethodValues as $propertyPath => $expectedValue) {
$this->assertEquals($expectedValue, $accessor->getValue($json, $propertyPath));
}
}
/**
* @return array
*/
public static function viewProvider()
{
return [
'test' => [
'test',
[
'/api/resources' => 1,
],
[
'/api/resources[0].method' => 'GET',
'/api/resources[0].uri' => '/api/resources.{_format}',
],
],
'premium' => [
'premium',
[
'/api/resources' => 2,
],
[
'/api/resources[0].method' => 'GET',
'/api/resources[0].uri' => '/api/resources.{_format}',
'/api/resources[1].method' => 'POST',
'/api/resources[1].uri' => '/api/resources.{_format}',
],
],
'default' => [
'default',
[
'/api/resources' => 4,
],
[
'/api/resources[0].method' => 'GET',
'/api/resources[0].uri' => '/api/resources.{_format}',
'/api/resources[1].method' => 'POST',
'/api/resources[1].uri' => '/api/resources.{_format}',
'/api/resources[2].method' => 'DELETE',
'/api/resources[2].uri' => '/api/resources/{id}.{_format}',
'/api/resources[3].method' => 'GET',
'/api/resources[3].uri' => '/api/resources/{id}.{_format}',
],
],
];
}
}

View file

@ -0,0 +1,66 @@
<?php
/*
* 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.
*/
namespace NelmioApiDocBundle\Tests\Controller;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
/**
* Class ApiDocControllerTest
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class ApiDocControllerTest extends WebTestCase
{
public function testSwaggerDocResourceListRoute(): void
{
$client = static::createClient();
$client->request('GET', '/api-docs');
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-type'));
}
public function dataTestApiDeclarations()
{
return [
['resources'],
['tests'],
['tests2'],
['TestResource'],
];
}
/**
* @dataProvider dataTestApiDeclarations
*/
public function testApiDeclarationRoutes($resource): void
{
$client = static::createClient();
$client->request('GET', '/api-docs/' . $resource);
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('application/json', $response->headers->get('Content-type'));
}
public function testNonExistingApiDeclaration(): void
{
$client = static::createClient();
$client->request('GET', '/api-docs/santa');
$response = $client->getResponse();
$this->assertEquals(404, $response->getStatusCode());
}
}

View file

@ -15,14 +15,14 @@ use Nelmio\ApiDocBundle\Tests\WebTestCase;
class RequestListenerTest extends WebTestCase
{
public function testDocQueryArg()
public function testDocQueryArg(): void
{
$client = $this->createClient();
$client->request('GET', '/tests?_doc=1');
$content = $client->getResponse()->getContent();
$this->assertTrue(0 !== strpos($content, '<h1>API documentation</h1>'), 'Event listener should capture ?_doc=1 requests');
$this->assertTrue(0 !== strpos($content, '/tests.{_format}'), 'Event listener should capture ?_doc=1 requests');
$this->assertTrue(!str_starts_with($content, '<h1>API documentation</h1>'), 'Event listener should capture ?_doc=1 requests');
$this->assertTrue(!str_starts_with($content, '/tests.{_format}'), 'Event listener should capture ?_doc=1 requests');
$client->request('GET', '/tests');
$this->assertEquals('tests', $client->getResponse()->getContent(), 'Event listener should let normal requests through');

View file

@ -11,77 +11,61 @@
namespace Nelmio\ApiDocBundle\Tests\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
class ApiDocExtractorTest extends WebTestCase
{
const ROUTES_QUANTITY = 24;
private static $ROUTES_QUANTITY_DEFAULT = 26; // Routes in the default view
private static $ROUTES_QUANTITY_PREMIUM = 5; // Routes in the premium view
private static $ROUTES_QUANTITY_TEST = 2; // Routes in the test view
public function testAll()
public function testAll(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler(array($this, 'handleDeprecation'));
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all();
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount(self::ROUTES_QUANTITY, $data);
$this->assertCount(self::$ROUTES_QUANTITY_DEFAULT, $data);
foreach ($data as $d) {
$cacheFile = $container->getParameter('kernel.cache_dir') . '/api-doc.cache.' . ApiDoc::DEFAULT_VIEW;
$this->assertFileExists($cacheFile);
$this->assertStringEqualsFile($cacheFile, serialize($data));
foreach ($data as $key => $d) {
$this->assertTrue(is_array($d));
$this->assertArrayHasKey('annotation', $d);
$this->assertArrayHasKey('resource', $d);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Annotation\ApiDoc', $d['annotation']);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $d['annotation']);
$this->assertInstanceOf('Symfony\Component\Routing\Route', $d['annotation']->getRoute());
$this->assertNotNull($d['resource']);
}
$a1 = $data[0]['annotation'];
$array1 = $a1->toArray();
$this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getInput());
$a1 = $data[1]['annotation'];
$array1 = $a1->toArray();
$this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getInput());
$a2 = $data[2]['annotation'];
$array2 = $a2->toArray();
$this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
$a2 = $data[3]['annotation'];
$array2 = $a2->toArray();
$this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
$a4 = $data[5]['annotation'];
$this->assertTrue($a4->isResource());
$this->assertEquals('TestResource', $a4->getResource());
$a3 = $data['14']['annotation'];
$this->assertTrue($a3->getHttps());
}
public function testGet()
public function testRouteVersionChecking(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->allForVersion('1.5');
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_DEFAULT, $data);
$data = $extractor->allForVersion('1.4');
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_DEFAULT - 1, $data);
}
public function testGet(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::indexAction', 'test_route_1');
$this->assertInstanceOf('Nelmio\ApiDocBundle\Annotation\ApiDoc', $annotation);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $annotation);
$this->assertTrue($annotation->isResource());
$this->assertEquals('index action', $annotation->getDescription());
@ -90,14 +74,15 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertTrue(is_array($array['filters']));
$this->assertNull($annotation->getInput());
$annotation2 = $extractor->get('nemlio.test.controller:indexAction', 'test_service_route_1');
$annotation2 = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::indexAction', 'test_service_route_1');
$annotation2->getRoute()
->setDefault('_controller', $annotation->getRoute()->getDefault('_controller'))
->compile(); // compile as we changed a default value
->compile() // compile as we changed a default value
;
$this->assertEquals($annotation, $annotation2);
}
public function testGetWithBadController()
public function testGetWithBadController(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
@ -110,7 +95,7 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertNull($data);
}
public function testGetWithBadRoute()
public function testGetWithBadRoute(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
@ -118,12 +103,12 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertNull($data);
$data = $extractor->get('nemlio.test.controller:indexAction', 'invalid_route');
$data = $extractor->get('nelmio.test.controller:indexAction', 'invalid_route');
$this->assertNull($data);
}
public function testGetWithInvalidPattern()
public function testGetWithInvalidPath(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
@ -131,12 +116,12 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertNull($data);
$data = $extractor->get('nemlio.test.controller', 'test_service_route_1');
$data = $extractor->get('nelmio.test.controller', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithMethodWithoutApiDocAnnotation()
public function testGetWithMethodWithoutApiDocAnnotation(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
@ -144,20 +129,20 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertNull($data);
$data = $extractor->get('nemlio.test.controller:anotherAction', 'test_service_route_1');
$data = $extractor->get('nelmio.test.controller:anotherAction', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithDocComment()
public function testGetWithDocComment(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::myCommentedAction', 'test_route_5');
$this->assertNotNull($annotation);
$this->assertEquals(
"This method is useful to test if the getDocComment works.",
'This method is useful to test if the getDocComment works.',
$annotation->getDescription()
);
@ -176,38 +161,10 @@ class ApiDocExtractorTest extends WebTestCase
);
}
public function testGetWithAuthentication()
public function testGetWithDeprecated(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::AuthenticatedAction', 'test_route_13');
$this->assertNotNull($annotation);
$this->assertTrue(
$annotation->getAuthentication()
);
$this->assertContains('ROLE_USER', $annotation->getAuthenticationRoles());
$this->assertContains('ROLE_FOOBAR', $annotation->getAuthenticationRoles());
$this->assertCount(2, $annotation->getAuthenticationRoles());
}
public function testGetWithCache()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::CachedAction', 'test_route_14');
$this->assertNotNull($annotation);
$this->assertEquals(
60,
$annotation->getCache()
);
}
public function testGetWithDeprecated()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::DeprecatedAction', 'test_route_14');
$this->assertNotNull($annotation);
@ -216,39 +173,233 @@ class ApiDocExtractorTest extends WebTestCase
);
}
public function testOutputWithSelectedParsers()
public function testOutputWithSelectedParsers(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::zReturnSelectedParsersOutputAction', 'test_route_19');
$this->assertNotNull($annotation);
$output = $annotation->getOutput();
$parsers = $output['parsers'];
$this->assertEquals(
"Nelmio\\ApiDocBundle\\Parser\\JmsMetadataParser",
'Nelmio\\ApiDocBundle\\Parser\\JmsMetadataParser',
$parsers[0]
);
$this->assertEquals(
"Nelmio\\ApiDocBundle\\Parser\\ValidationParser",
'Nelmio\\ApiDocBundle\\Parser\\ValidationParser',
$parsers[1]
);
$this->assertCount(2, $parsers);
}
public function testInputWithSelectedParsers()
public function testInputWithSelectedParsers(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::zReturnSelectedParsersInputAction', 'test_route_20');
$this->assertNotNull($annotation);
$input = $annotation->getInput();
$parsers = $input['parsers'];
$this->assertEquals(
"Nelmio\\ApiDocBundle\\Parser\\FormTypeParser",
'Nelmio\\ApiDocBundle\\Parser\\FormTypeParser',
$parsers[0]
);
$this->assertCount(1, $parsers);
}
public function testPostRequestDoesRequireParametersWhenMarkedAsSuch(): void
{
$container = $this->getContainer();
/** @var ApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
/** @var ApiDoc $annotation */
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::requiredParametersAction', 'test_required_parameters');
$parameters = $annotation->getParameters();
$this->assertTrue($parameters['required_field']['required']);
}
public function testPatchRequestDoesNeverRequireParameters(): void
{
$container = $this->getContainer();
/** @var ApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
/** @var ApiDoc $annotation */
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::requiredParametersAction', 'test_patch_disables_required_parameters');
$parameters = $annotation->getParameters();
$this->assertFalse($parameters['required_field']['required']);
}
public static function dataProviderForViews(): array
{
$offset = 0;
return [
['default', self::$ROUTES_QUANTITY_DEFAULT + $offset],
['premium', self::$ROUTES_QUANTITY_PREMIUM + $offset],
['test', self::$ROUTES_QUANTITY_TEST + $offset],
['foobar', $offset],
['', $offset],
[null, $offset],
];
}
public function testViewNamedTest(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all('test');
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_TEST, $data);
$a1 = $data[0]['annotation'];
$this->assertCount(3, $a1->getViews());
$this->assertEquals('List resources.', $a1->getDescription());
$a2 = $data[1]['annotation'];
$this->assertCount(2, $a2->getViews());
$this->assertEquals('create another test', $a2->getDescription());
}
public function testViewNamedPremium(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all('premium');
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_PREMIUM, $data);
$a1 = $data[0]['annotation'];
$this->assertCount(2, $a1->getViews());
$this->assertEquals('List another resource.', $a1->getDescription());
$a2 = $data[1]['annotation'];
$this->assertCount(3, $a2->getViews());
$this->assertEquals('List resources.', $a2->getDescription());
}
/**
* @dataProvider dataProviderForViews
*/
public function testForViews($view, $count): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all($view);
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount($count, $data);
}
public function testOverrideJmsAnnotationWithApiDocParameters(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get(
'Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::overrideJmsAnnotationWithApiDocParametersAction',
'test_route_27'
);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $annotation);
$array = $annotation->toArray();
$this->assertTrue(is_array($array['parameters']));
$this->assertEquals('string', $array['parameters']['foo']['dataType']);
$this->assertEquals('DateTime', $array['parameters']['bar']['dataType']);
$this->assertEquals('integer', $array['parameters']['number']['dataType']);
$this->assertEquals('string', $array['parameters']['number']['actualType']);
$this->assertNull($array['parameters']['number']['subType']);
$this->assertTrue($array['parameters']['number']['required']);
$this->assertEquals('This is the new description', $array['parameters']['number']['description']);
$this->assertFalse($array['parameters']['number']['readonly']);
$this->assertEquals('v3.0', $array['parameters']['number']['sinceVersion']);
$this->assertEquals('v4.0', $array['parameters']['number']['untilVersion']);
$this->assertEquals('object (ArrayCollection)', $array['parameters']['arr']['dataType']);
$this->assertEquals('object (JmsNested)', $array['parameters']['nested']['dataType']);
$this->assertEquals('integer', $array['parameters']['nested']['children']['bar']['dataType']);
$this->assertEquals('d+', $array['parameters']['nested']['children']['bar']['format']);
}
public function testJmsAnnotation(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get(
'Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::defaultJmsAnnotations',
'test_route_27'
);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $annotation);
$array = $annotation->toArray();
$this->assertTrue(is_array($array['parameters']));
$this->assertEquals('string', $array['parameters']['foo']['dataType']);
$this->assertEquals('DateTime', $array['parameters']['bar']['dataType']);
$this->assertEquals('double', $array['parameters']['number']['dataType']);
$this->assertEquals('float', $array['parameters']['number']['actualType']);
$this->assertNull($array['parameters']['number']['subType']);
$this->assertFalse($array['parameters']['number']['required']);
$this->assertEquals('', $array['parameters']['number']['description']);
$this->assertFalse($array['parameters']['number']['readonly']);
$this->assertNull($array['parameters']['number']['sinceVersion']);
$this->assertNull($array['parameters']['number']['untilVersion']);
$this->assertEquals('array', $array['parameters']['arr']['dataType']);
$this->assertEquals('object (JmsNested)', $array['parameters']['nested']['dataType']);
$this->assertEquals('string', $array['parameters']['nested']['children']['bar']['dataType']);
}
public function testMergeParametersDefaultKeyNotExistingInFirstArray(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$mergeMethod = new \ReflectionMethod('Nelmio\ApiDocBundle\Extractor\ApiDocExtractor', 'mergeParameters');
$mergeMethod->setAccessible(true);
$p1 = [
'myPropName' => [
'dataType' => 'string',
'actualType' => 'string',
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null,
],
];
$p2 = [
'myPropName' => [
'dataType' => 'string',
'actualType' => 'string',
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null,
'default' => '',
],
];
$mergedResult = $mergeMethod->invokeArgs($extractor, [$p1, $p2]);
$this->assertEquals($p2, $mergedResult);
}
}

View file

@ -0,0 +1,94 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
class CachingApiDocExtractorTest extends WebTestCase
{
/**
* @return array
*/
public static function viewsWithoutDefaultProvider()
{
$data = ApiDocExtractorTest::dataProviderForViews();
// remove default view data from provider
array_shift($data);
return $data;
}
/**
* Test that every view cache is saved in its own cache file
*
* @dataProvider viewsWithoutDefaultProvider
*
* @param string $view View name
*/
public function testDifferentCacheFilesAreCreatedForDifferentViews($view): void
{
$container = $this->getContainer();
/* @var CachingApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$this->assertInstanceOf('\Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor', $extractor);
set_error_handler([$this, 'handleDeprecation']);
$defaultData = $extractor->all(ApiDoc::DEFAULT_VIEW);
$data = $extractor->all($view);
restore_error_handler();
$this->assertIsArray($data);
$this->assertNotSameSize($defaultData, $data);
$this->assertNotEquals($defaultData, $data);
$cacheFile = $container->getParameter('kernel.cache_dir') . '/api-doc.cache';
$expectedDefaultViewCacheFile = $cacheFile . '.' . ApiDoc::DEFAULT_VIEW;
$expectedViewCacheFile = $cacheFile . '.' . $view;
$this->assertFileExists($expectedDefaultViewCacheFile);
$this->assertFileExists($expectedViewCacheFile);
$this->assertFileNotEquals($expectedDefaultViewCacheFile, $expectedViewCacheFile);
}
/**
* @dataProvider \Nelmio\ApiDocBundle\Tests\Extractor\ApiDocExtractorTest::dataProviderForViews
*
* @param string $view View name to test
*/
public function testCachedResultSameAsGenerated($view): void
{
$container = $this->getContainer();
/* @var CachingApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$this->assertInstanceOf('\Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor', $extractor);
$cacheFile = $container->getParameter('kernel.cache_dir') . '/api-doc.cache';
$expectedViewCacheFile = $cacheFile . '.' . $view;
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all($view);
$this->assertFileExists($expectedViewCacheFile);
$cachedData = $extractor->all($view);
restore_error_handler();
$this->assertIsArray($data);
$this->assertIsArray($cachedData);
$this->assertSameSize($data, $cachedData);
$this->assertEquals($data, $cachedData);
}
}

View file

@ -0,0 +1,116 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Extractor;
use PHPUnit\Framework\TestCase;
class CollectionDirectiveTest extends TestCase
{
/**
* @var TestExtractor
*/
private $testExtractor;
protected function setUp(): void
{
$this->testExtractor = new TestExtractor();
}
private function normalize($input)
{
return $this->testExtractor->getNormalization($input);
}
/**
* @dataProvider dataNormalizationTests
*/
public function testNormalizations($input, callable $callable): void
{
call_user_func($callable, $this->normalize($input), $this);
}
public function dataNormalizationTests()
{
return [
'test_simple_notation' => [
'array<User>',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('', $actual['collectionName']);
$case->assertEquals('User', $actual['class']);
},
],
'test_simple_notation_with_namespaces' => [
'array<Vendor0_2\\_Namespace1\\Namespace_2\\User>',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('', $actual['collectionName']);
$case->assertEquals('Vendor0_2\\_Namespace1\\Namespace_2\\User', $actual['class']);
},
],
'test_simple_named_collections' => [
'array<Group> as groups',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('groups', $actual['collectionName']);
$case->assertEquals('Group', $actual['class']);
},
],
'test_namespaced_named_collections' => [
'array<_Vendor\\Namespace0\\Namespace_2F3\\Group> as groups',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('groups', $actual['collectionName']);
$case->assertEquals('_Vendor\\Namespace0\\Namespace_2F3\\Group', $actual['class']);
},
],
];
}
/**
* @dataProvider dataInvalidDirectives
*/
public function testInvalidDirectives($input): void
{
$this->expectException(\InvalidArgumentException::class);
$this->normalize($input);
}
public function dataInvalidDirectives()
{
return [
['array<>'],
['array<Vendor\\>'],
['array<2Vendor\\>'],
['array<Vendor\\2Class>'],
['array<User> as'],
['array<User> as '],
];
}
}

View file

@ -1,90 +0,0 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Extractor;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
class FosRestHandlerTest extends WebTestCase
{
public function testGetWithQueryParamStrict()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::zActionWithQueryParamStrictAction', 'test_route_15');
$this->assertNotNull($annotation);
$requirements = $annotation->getRequirements();
$this->assertCount(1, $requirements);
$this->assertArrayHasKey('page', $requirements);
$requirement = $requirements['page'];
$this->assertArrayHasKey('requirement', $requirement);
$this->assertEquals($requirement['requirement'], '\d+');
$this->assertArrayHasKey('description', $requirement);
$this->assertEquals($requirement['description'], 'Page of the overview.');
$this->assertArrayHasKey('dataType', $requirement);
$this->assertArrayNotHasKey('default', $requirement);
}
public function testGetWithQueryParam()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::zActionWithQueryParamAction', 'test_route_8');
$this->assertNotNull($annotation);
$filters = $annotation->getFilters();
$this->assertCount(1, $filters);
$this->assertArrayHasKey('page', $filters);
$filter = $filters['page'];
$this->assertArrayHasKey('requirement', $filter);
$this->assertEquals($filter['requirement'], '\d+');
$this->assertArrayHasKey('description', $filter);
$this->assertEquals($filter['description'], 'Page of the overview.');
$this->assertArrayHasKey('default', $filter);
$this->assertEquals($filter['default'], '1');
}
public function testGetWithQueryParamNoDefault()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::zActionWithQueryParamNoDefaultAction', 'test_route_16');
$this->assertNotNull($annotation);
$filters = $annotation->getFilters();
$this->assertCount(1, $filters);
$this->assertArrayHasKey('page', $filters);
$filter = $filters['page'];
$this->assertArrayHasKey('requirement', $filter);
$this->assertEquals($filter['requirement'], '\d+');
$this->assertArrayHasKey('description', $filter);
$this->assertEquals($filter['description'], 'Page of the overview.');
$this->assertArrayNotHasKey('default', $filter);
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Extractor;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
class TestExtractor extends ApiDocExtractor
{
public function __construct()
{
}
public function getNormalization($input)
{
return $this->normalizeClassParameter($input);
}
}

View file

@ -0,0 +1,73 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Controller;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
class ResourceController
{
#[ApiDoc(
resource: true,
views: ['test', 'premium', 'default'],
resourceDescription: 'Operations on resource.',
description: 'List resources.',
output: "array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test> as tests",
statusCodes: [200 => 'Returned on success.', 404 => 'Returned if resource cannot be found.']
)]
public function listResourcesAction(): void
{
}
#[ApiDoc(description: 'Retrieve a resource by ID.')]
public function getResourceAction(): void
{
}
#[ApiDoc(description: 'Delete a resource by ID.')]
public function deleteResourceAction(): void
{
}
#[ApiDoc(
description: 'Create a new resource.',
views: ['default', 'premium'],
input: ['class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", 'name' => ''],
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested",
responseMap: [
400 => ['class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", 'form_errors' => true],
]
)]
public function createResourceAction(): void
{
}
#[ApiDoc(
resource: true,
views: ['default', 'premium'],
description: 'List another resource.',
resourceDescription: 'Operations on another resource.',
output: "array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest>"
)]
public function listAnotherResourcesAction(): void
{
}
#[ApiDoc(description: 'Retrieve another resource by ID.')]
public function getAnotherResourceAction(): void
{
}
#[ApiDoc(description: 'Update a resource bu ID.')]
public function updateAnotherResourceAction(): void
{
}
}

View file

@ -11,66 +11,72 @@
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Controller;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Tests\Fixtures\DependencyTypePath;
use Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
class TestController
{
/**
* @ApiDoc(
* resource="TestResource"
* )
*/
public function namedResourceAction()
#[ApiDoc(
resource: 'TestResource',
views: 'default'
)]
public function namedResourceAction(): void
{
}
/**
* @ApiDoc(
* resource=true,
* description="index action",
* filters={
* {"name"="a", "dataType"="integer"},
* {"name"="b", "dataType"="string", "arbitrary"={"arg1", "arg2"}}
* }
* )
*/
#[ApiDoc(
resource: true,
description: 'index action',
filters: [
['name' => 'a', 'dataType' => 'integer'],
['name' => 'b', 'dataType' => 'string', 'arbitrary' => ['arg1', 'arg2']],
],
)]
public function indexAction()
{
return new Response('tests');
}
/**
* @ApiDoc(
* description="create test",
* input="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType"
* )
*/
public function postTestAction()
#[ApiDoc(
resource: true,
description: 'create test',
views: ['default', 'premium'],
input: TestType::class
)]
public function postTestAction(): void
{
}
/**
* @ApiDoc(
* description="post test 2",
* resource=true
* )
*/
public function postTest2Action()
#[ApiDoc(
description: 'post test 2',
views: ['default', 'premium'],
resource: true
)]
public function postTest2Action(): void
{
}
public function anotherAction()
#[ApiDoc(
description: 'Action with required parameters',
input: "Nelmio\ApiDocBundle\Tests\Fixtures\Form\RequiredType"
)]
public function requiredParametersAction(): void
{
}
/**
* @ApiDoc(description="Action without HTTP verb")
*/
public function anyAction()
public function anotherAction(): void
{
}
#[ApiDoc]
public function routeVersionAction(): void
{
}
#[ApiDoc(description: 'Action without HTTP verb')]
public function anyAction(): void
{
}
@ -78,205 +84,193 @@ class TestController
* This method is useful to test if the getDocComment works.
* And, it supports multilines until the first '@' char.
*
* @ApiDoc()
*
* @param int $id A nice comment
* @param int $page
* @param int $paramType The param type
* @param int $param The param id
*/
public function myCommentedAction()
#[ApiDoc]
public function myCommentedAction($id, $page, int $paramType, int $param): void
{
}
#[ApiDoc]
public function yetAnotherAction(): void
{
}
#[ApiDoc(
views: ['default', 'test'],
description: 'create another test',
input: DependencyTypePath::TYPE
)]
public function anotherPostAction(): void
{
}
#[ApiDoc(
description: 'Testing JMS',
input: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
)]
public function jmsInputTestAction(): void
{
}
#[ApiDoc(
description: 'Testing return',
output: DependencyTypePath::TYPE
)]
public function jmsReturnTestAction(): void
{
}
#[ApiDoc]
public function zCachedAction(): void
{
}
#[ApiDoc]
public function zSecuredAction(): void
{
}
/**
* @ApiDoc()
*/
public function yetAnotherAction()
{
}
/**
* @ApiDoc(
* description="create another test",
* input="dependency_type"
* )
*/
public function anotherPostAction()
{
}
/**
* @ApiDoc()
* @QueryParam(strict=true, name="page", requirements="\d+", description="Page of the overview.")
*/
public function zActionWithQueryParamStrictAction()
{
}
/**
* @ApiDoc()
* @QueryParam(name="page", requirements="\d+", default="1", description="Page of the overview.")
*/
public function zActionWithQueryParamAction()
{
}
/**
* @ApiDoc()
* @QueryParam(name="page", requirements="\d+", description="Page of the overview.")
*/
public function zActionWithQueryParamNoDefaultAction()
{
}
/**
* @ApiDoc(
* description="Testing JMS",
* input="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
* )
*/
public function jmsInputTestAction()
{
}
/**
* @ApiDoc(
* description="Testing return",
* output="dependency_type"
* )
*/
public function jmsReturnTestAction()
{
}
/**
* @ApiDoc()
* @RequestParam(name="param1", requirements="string", description="Param1 description.")
*/
public function zActionWithRequestParamAction()
{
}
/**
* @ApiDoc()
*/
public function secureRouteAction()
{
}
/**
* @ApiDoc(
* authentication=true,
* authenticationRoles={"ROLE_USER","ROLE_FOOBAR"}
* )
*/
public function authenticatedAction()
{
}
/**
* @ApiDoc()
* @Cache(maxage=60, public=1)
*/
public function cachedAction()
{
}
/**
* @ApiDoc()
* @deprecated
*/
public function deprecatedAction()
#[ApiDoc]
public function deprecatedAction(): void
{
}
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
)]
public function jmsReturnNestedOutputAction(): void
{
}
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsChild"
)]
public function jmsReturnNestedExtendedOutputAction(): void
{
}
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\MultipleTest"
)]
public function zReturnJmsAndValidationOutputAction(): void
{
}
#[ApiDoc(
description: 'Returns a collection of Object',
requirements: [
['name' => 'limit', 'dataType' => 'integer', 'requirement' => "\d+", 'description' => 'how many objects to return'],
],
parameters: [
['name' => 'categoryId', 'dataType' => 'integer', 'required' => true, 'description' => 'category id'],
]
)]
public function cgetAction($id): void
{
}
#[ApiDoc(
input: [
'class' => TestType::class,
'parsers' => ["Nelmio\ApiDocBundle\Parser\FormTypeParser"],
]
)]
public function zReturnSelectedParsersInputAction(): void
{
}
#[ApiDoc(
output: [
'class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Model\MultipleTest",
'parsers' => [
"Nelmio\ApiDocBundle\Parser\JmsMetadataParser",
"Nelmio\ApiDocBundle\Parser\ValidationParser",
],
]
)]
public function zReturnSelectedParsersOutputAction(): void
{
}
#[ApiDoc(
section: 'private'
)]
public function privateAction(): void
{
}
#[ApiDoc(
section: 'exclusive'
)]
public function exclusiveAction(): void
{
}
/**
* @ApiDoc(
* output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
* )
* @see http://symfony.com
*/
public function jmsReturnNestedOutputAction()
#[ApiDoc]
public function withLinkAction(): void
{
}
/**
* @ApiDoc(
* output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsChild"
* )
*/
public function jmsReturnNestedExtendedOutputAction()
#[ApiDoc(
input: ['class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"],
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest",
parameters: [
[
'name' => 'number',
'dataType' => 'integer',
'actualType' => 'string',
'subType' => null,
'required' => true,
'description' => 'This is the new description',
'readonly' => false,
'sinceVersion' => 'v3.0',
'untilVersion' => 'v4.0',
],
[
'name' => 'arr',
'dataType' => 'object (ArrayCollection)',
],
[
'name' => 'nested',
'dataType' => 'object (JmsNested)',
'children' => [
'bar' => [
'dataType' => 'integer',
'format' => 'd+',
],
],
],
]
)]
public function overrideJmsAnnotationWithApiDocParametersAction(): void
{
}
/**
* @ApiDoc(
* output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\MultipleTest"
* )
*/
public function zReturnJmsAndValidationOutputAction()
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest",
input: [
'class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest",
],
)]
public function defaultJmsAnnotations(): void
{
}
/**
* @ApiDoc(
* description="Returns a collection of Object",
* requirements={
* {"name"="limit", "dataType"="integer", "requirement"="\d+", "description"="how many objects to return"}
* },
* parameters={
* {"name"="categoryId", "dataType"="integer", "required"=true, "description"="category id"}
* }
* )
*/
public function cgetAction($id)
{
}
/**
* @ApiDoc(
* input={
* "class"="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType",
* "parsers"={
* "Nelmio\ApiDocBundle\Parser\FormTypeParser",
* }
* }
* )
*/
public function zReturnSelectedParsersInputAction()
{
}
/**
* @ApiDoc(
* output={
* "class"="Nelmio\ApiDocBundle\Tests\Fixtures\Model\MultipleTest",
* "parsers"={
* "Nelmio\ApiDocBundle\Parser\JmsMetadataParser",
* "Nelmio\ApiDocBundle\Parser\ValidationParser"
* }
* }
* )
*/
public function zReturnSelectedParsersOutputAction()
{
}
/**
* @ApiDoc(
* section="private"
* )
*/
public function privateAction()
{
}
/**
* @ApiDoc(
* section="exclusive"
* )
*/
public function exclusiveAction()
#[ApiDoc(
description: 'Route with host placeholder',
views: ['default']
)]
public function routeWithHostAction(): void
{
}
}

View file

@ -0,0 +1,32 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
/*
* This class is used to have dynamic annotations for BC.
* {@see Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController}
*
* @author Ener-Getick <egetick@gmail.com>
*/
if (LegacyFormHelper::isLegacy()) {
class DependencyTypePath
{
public const TYPE = 'dependency_type';
}
} else {
class DependencyTypePath
{
public const TYPE = 'Nelmio\ApiDocBundle\Tests\Fixtures\Form\DependencyType';
}
}

View file

@ -11,24 +11,35 @@
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CollectionType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$collectionType = 'Symfony\Component\Form\Extension\Core\Type\CollectionType';
$builder
->add('a', 'collection', array('type' => 'text'))
->add('b', 'collection', array('type' => new TestType()))
->add('a', LegacyFormHelper::getType($collectionType), [
LegacyFormHelper::hasBCBreaks() ? 'entry_type' : 'type' => LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'),
])
->add('b', LegacyFormHelper::getType($collectionType), [
LegacyFormHelper::hasBCBreaks() ? 'entry_type' : 'type' => LegacyFormHelper::isLegacy() ? new TestType() : __NAMESPACE__ . '\TestType',
])
;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'collection_type';
}

View file

@ -0,0 +1,46 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class CompoundType
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class CompoundType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('sub_form', LegacyFormHelper::isLegacy() ? new SimpleType() : __NAMESPACE__ . '\SimpleType')
->add('a', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\NumberType'))
;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return '';
}
}

View file

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class DependencyType extends AbstractType
@ -21,29 +22,40 @@ class DependencyType extends AbstractType
{
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('a', null, array('description' => 'A nice description'))
->add('a', null, ['description' => 'A nice description'])
;
}
/**
* {@inheritdoc}
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(array(
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
));
]);
return;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'dependency_type';
}

View file

@ -0,0 +1,56 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EntityType extends AbstractType
{
/**
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\EntityTest',
]);
return;
}
public function getParent()
{
return LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\ChoiceType');
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'entity';
}
}

View file

@ -1,53 +1,78 @@
<?php
/**
* Created by PhpStorm.
* User: Maxim_Romanovsky
* Date: 4/4/14
* Time: 11:00 AM
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ImprovedTestType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choiceType = LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\ChoiceType');
$datetimeType = LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateTimeType');
$dateType = LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateType');
$builder
->add('dt1', 'datetime', array('widget' => 'single_text', 'description' => 'A nice description'))
->add('dt2', 'datetime', array('date_format' => 'M/d/y'))
->add('dt3', 'datetime', array('widget' => 'single_text', 'format' => 'M/d/y H:i:s'))
->add('dt4', 'datetime', array('date_format' => \IntlDateFormatter::MEDIUM))
->add('dt5', 'datetime', array('format' => 'M/d/y H:i:s'))
->add('d1', 'date', array('format' => \IntlDateFormatter::MEDIUM))
->add('d2', 'date', array('format' => 'd-M-y'))
->add('c1', 'choice', array('choices' => array('m' => 'Male', 'f' => 'Female')))
->add('c2', 'choice', array('choices' => array('m' => 'Male', 'f' => 'Female'), 'multiple' => true))
->add('c3', 'choice', array('choices' => array()))
->add('c4', 'choice', array('choice_list' => new SimpleChoiceList(array('foo' => 'bar', 'bazgroup' => array('baz' => 'Buzz')))))
->add('dt1', $datetimeType, ['widget' => 'single_text', 'description' => 'A nice description'])
->add('dt2', $datetimeType, ['date_format' => 'M/d/y', 'html5' => false])
->add('dt3', $datetimeType, ['widget' => 'single_text', 'format' => 'M/d/y H:i:s', 'html5' => false])
->add('dt4', $datetimeType, ['date_format' => \IntlDateFormatter::MEDIUM])
->add('dt5', $datetimeType, ['format' => 'M/d/y H:i:s', 'html5' => false])
->add('d1', $dateType, ['format' => \IntlDateFormatter::MEDIUM])
->add('d2', $dateType, ['format' => 'd-M-y'])
->add('c1', $choiceType, ['choices' => ['Male' => 'm', 'Female' => 'f']])
->add('c2', $choiceType, ['choices' => ['Male' => 'm', 'Female' => 'f'], 'multiple' => true])
->add('c3', $choiceType, ['choices' => []])
->add('c4', $choiceType, ['choices' => ['bar' => 'foo', 'bazgroup' => ['Buzz' => 'baz']]])
->add('e1', LegacyFormHelper::isLegacy() ? new EntityType() : __NAMESPACE__ . '\EntityType',
LegacyFormHelper::isLegacy()
? ['choice_list' => new SimpleChoiceList(['bar' => 'foo', 'bazgroup' => ['Buzz' => 'baz']])]
: ['choices' => ['bar' => 'foo', 'bazgroup' => ['Buzz' => 'baz']]]
)
;
}
/**
* {@inheritdoc}
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(array(
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\ImprovedTest',
));
]);
return;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return '';
}

View file

@ -0,0 +1,69 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RequireConstructionType extends AbstractType
{
private $noThrow;
public function __construct($optionalArgs = null)
{
$this->noThrow = true;
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (true !== $this->noThrow) {
throw new \RuntimeException(__CLASS__ . ' require contruction');
}
$builder
->add('a', null, ['description' => 'A nice description'])
;
}
/**
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
]);
return;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'require_construction_type';
}
}

View file

@ -0,0 +1,43 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class SimpleType
*
* @author Lucas van Lierop <lucas@vanlierop.org>
*/
class RequiredType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('required_field', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'), ['required' => true]);
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return '';
}
}

View file

@ -0,0 +1,53 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class SimpleType
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class SimpleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('a', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'), [
'description' => 'Something that describes A.',
])
->add('b', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\NumberType'))
->add('c', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\ChoiceType'),
['choices' => ['X' => 'x', 'Y' => 'y', 'Z' => 'z']]
)
->add('d', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateTimeType'))
->add('e', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateType'))
->add('g', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextareaType'))
;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'simple';
}
}

View file

@ -11,37 +11,51 @@
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TestType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('a', null, array('description' => 'A nice description'))
->add('a', null, ['description' => 'A nice description'])
->add('b')
->add($builder->create('c', 'checkbox'))
->add($builder->create('c', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\CheckboxType')))
->add('d', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'), ['data' => 'DefaultTest'])
;
}
/**
* {@inheritdoc}
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(array(
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
));
]);
return;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return '';
}

View file

@ -0,0 +1,16 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
class EntityTest
{
}

View file

@ -11,8 +11,6 @@
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
use Symfony\Component\Validator\Constraints as Assert;
class ImprovedTest
{
public $dt1;
@ -26,4 +24,5 @@ class ImprovedTest
public $c2;
public $c3;
public $c4;
public $e1;
}

View file

@ -1,4 +1,14 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
use JMS\Serializer\Annotation as JMS;
@ -9,5 +19,4 @@ class JmsChild extends JmsTest
* @JMS\Type("string");
*/
public $child;
}

View file

@ -0,0 +1,27 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
use JMS\Serializer\Annotation as JMS;
class JmsInline
{
/**
* @JMS\Type("string");
*/
public $foo;
/**
* @JMS\Inline
*/
public $inline;
}

View file

@ -1,21 +1,31 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
use JMS\Serializer\Annotation as JMS;
class JmsNested
{
/**
* @JMS\Type("DateTime");
* @JMS\ReadOnly
*
* @JMS\ReadOnlyProperty
*/
public $foo;
/**
* @JMS\Type("string");
*/
public $bar;
public $bar = 'baz';
/**
* Epic description.
@ -38,19 +48,23 @@ class JmsNested
/**
* @Jms\Type("string")
*
* @Jms\Since("0.2")
*/
public $since;
/**
* @Jms\Type("string")
*
* @Jms\Until("0.3")
*/
public $until;
/**
* @Jms\Type("string")
*
* @Jms\Since("0.4")
*
* @Jms\Until("0.5")
*/
public $sinceAndUntil;

View file

@ -1,5 +1,14 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
use JMS\Serializer\Annotation as JMS;
@ -15,12 +24,14 @@ class JmsTest
/**
* @JMS\Type("DateTime");
* @JMS\ReadOnly
*
* @JMS\ReadOnlyProperty
*/
public $bar;
/**
* @JMS\Type("double");
*
* @JMS\SerializedName("number");
*/
public $baz;
@ -40,4 +51,8 @@ class JmsTest
*/
public $nestedArray;
/**
* @JMS\Groups("hidden")
*/
public $hidden;
}

View file

@ -0,0 +1,24 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
class JsonSerializableOptionalConstructorTest implements \JsonSerializable
{
public function __construct($optional = null)
{
}
public function jsonSerialize(): mixed
{
return [];
}
}

View file

@ -0,0 +1,24 @@
<?php
/*
* 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.
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model;
class JsonSerializableRequiredConstructorTest implements \JsonSerializable
{
public function __construct($required)
{
}
public function jsonSerialize(): mixed
{
return [];
}
}

Some files were not shown because too many files have changed in this diff Show more