Skip to content

Commit 0e9e6a3

Browse files
committed
[WIP] Hydra ApiDocumentation
1 parent 38f70d5 commit 0e9e6a3

File tree

12 files changed

+361
-55
lines changed

12 files changed

+361
-55
lines changed

ApiDocumentation.php

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,178 @@
1111

1212
namespace Dunglas\JsonLdApiBundle;
1313

14+
use Dunglas\JsonLdApiBundle\Mapping\ClassMetadataFactory;
15+
use Symfony\Component\Routing\RouterInterface;
16+
1417
/**
1518
* ApiDocumentation.
1619
*
1720
* @author Kévin Dunglas <[email protected]>
1821
*/
1922
class ApiDocumentation
2023
{
24+
/**
25+
* @var string
26+
*/
27+
const HYDRA_NS = 'http://www.w3.org/ns/hydra/core#';
28+
29+
/**
30+
* @var Resources
31+
*/
32+
private $resources;
33+
/**
34+
* @var RouterInterface
35+
*/
36+
private $router;
37+
/**
38+
* @var ClassMetadataFactory
39+
*/
40+
private $classMetadataFactory;
41+
/**
42+
* @var string
43+
*/
2144
private $title;
45+
/**
46+
* @var string
47+
*/
2248
private $description;
23-
private $entrypoint;
24-
private $supportedClass;
25-
private $statusCodes;
49+
50+
/**
51+
* @param Resources $resources
52+
* @param RouterInterface $router
53+
* @param ClassMetadataFactory $classMetadataFactory
54+
* @param string $title
55+
* @param string $description
56+
*/
57+
public function __construct(
58+
Resources $resources,
59+
RouterInterface $router,
60+
ClassMetadataFactory $classMetadataFactory,
61+
$title,
62+
$description
63+
) {
64+
$this->resources = $resources;
65+
$this->router = $router;
66+
$this->classMetadataFactory = $classMetadataFactory;
67+
$this->title = $title;
68+
$this->description = $description;
69+
}
70+
71+
public function getDocumentation()
72+
{
73+
$doc = [
74+
'@context' => self::HYDRA_NS,
75+
'@id' => $this->router->generate('json_ld_api_vocab'),
76+
'hydra:title' => $this->title,
77+
'hydra:description' => $this->description,
78+
'hydra:entrypoint' => $this->router->generate('json_ld_api_entrypoint'),
79+
'hydra:supportedClass' => [],
80+
];
81+
82+
foreach ($this->resources as $resource) {
83+
$metadata = $this->classMetadataFactory->getMetadataFor($resource->getEntityClass());
84+
$shortName = $resource->getShortName();
85+
86+
$supportedClass = [
87+
'@id' => $shortName,
88+
'@type' => 'hydra:Class',
89+
'hydra:title' => $resource->getTitle() ?: $resource->getShortName(),
90+
];
91+
92+
$description = $resource->getDescription() ?: $metadata->getDescription();
93+
if ($description) {
94+
$supportedClass['hydra:description'] = $description;
95+
}
96+
97+
$attributes = $metadata->getAttributes(
98+
$resource->getNormalizationGroups(),
99+
$resource->getDenormalizationGroups(),
100+
$resource->getValidationGroups()
101+
);
102+
103+
$supportedClass['hydra:supportedProperty'] = [];
104+
foreach ($attributes as $name => $details) {
105+
$supportedProperty = [
106+
'@type' => 'hydra:SupportedProperty',
107+
'hydra:property' => sprintf('%s/%s', $shortName, $name),
108+
'hydra:title' => $name,
109+
'hydra:required' => $details['required'],
110+
'hydra:readable' => $details['readable'],
111+
'hydra:writable' => $details['writable'],
112+
];
113+
114+
if ($details['description']) {
115+
$supportedProperty['hydra:description'] = $details['description'];
116+
}
117+
118+
$supportedClass['hydra:supportedProperty'][] = $supportedProperty;
119+
}
120+
121+
$supportedClass['hydra:supportedOperation'] = [];
122+
foreach ($resource->getCollectionOperations() as $operation) {
123+
$supportedOperation = [];
124+
125+
if('POST' === $operation['hydra:method']) {
126+
$supportedOperation['@type'] = 'hydra:CreateResourceOperation';
127+
$supportedOperation['hydra:title'] = sprintf('Creates a %s resource.', $shortName);
128+
$supportedOperation['hydra:expects'] = $shortName;
129+
$supportedOperation['hydra:returns'] = $shortName;
130+
} else {
131+
$supportedOperation['@type'] = 'hydra:Operation';
132+
if ('GET' === $operation['hydra:method']) {
133+
$supportedOperation['hydra:title'] = sprintf('Retrieves the collection of %s resources.', $shortName);
134+
$supportedOperation['hydra:returns'] = 'hydra:PagedCollection';
135+
}
136+
}
137+
138+
$this->populateSupportedOperation($supportedOperation, $operation);
139+
140+
$supportedClass['hydra:supportedOperation'][] = $supportedOperation;
141+
}
142+
143+
foreach ($resource->getItemOperations() as $operation) {
144+
$supportedOperation = [];
145+
146+
if ('PUT' === $operation['hydra:method']) {
147+
$supportedOperation['@type'] = 'hydra:ReplaceResourceOperation';
148+
$supportedOperation['hydra:title'] = sprintf('Replaces the %s resource.', $shortName);
149+
$supportedOperation['hydra:expects'] = $shortName;
150+
$supportedOperation['hydra:returns'] = $shortName;
151+
} elseif ('DELETE' === $operation['hydra:method']) {
152+
$supportedOperation['@type'] = 'hydra:Operation';
153+
$supportedOperation['hydra:title'] = sprintf('Deletes the %s resource.', $shortName);
154+
$supportedOperation['hydra:expects'] = $shortName;
155+
} else {
156+
if ('GET' === $operation['hydra:method']) {
157+
$supportedOperation['@type'] = 'hydra:Operation';
158+
$supportedOperation['hydra:title'] = sprintf('Retrieves %s resource.', $shortName);
159+
$supportedOperation['hydra:returns'] = $shortName;
160+
}
161+
}
162+
163+
$this->populateSupportedOperation($supportedOperation, $operation);
164+
165+
$supportedClass['hydra:supportedOperation'][] = $supportedOperation;
166+
}
167+
168+
$doc['hydra:supportedClass'][] = $supportedClass;
169+
}
170+
171+
return $doc;
172+
}
173+
174+
/**
175+
* Copy data from $operation to $supportedOperation except when the key start with "!".
176+
*
177+
* @param array $supportedOperation
178+
* @param array $operation
179+
*/
180+
private function populateSupportedOperation(array &$supportedOperation, array $operation)
181+
{
182+
foreach ($operation as $key => $value) {
183+
if (isset($key[0]) && '!' !== $key[0]) {
184+
$supportedOperation[$key] = $value;
185+
}
186+
}
187+
}
26188
}

ContextBuilder.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,9 @@ public function __construct(RouterInterface $router)
3535
/**
3636
* Builds the JSON-LD context for the given resource.
3737
*
38-
* @param Resource $resource
39-
*
4038
* @return array
4139
*/
42-
public function buildContext(Resource $resource)
40+
public function buildContext()
4341
{
4442
$context = [];
4543
$context['@vocab'] = $this->router->generate('json_ld_api_vocab', [], RouterInterface::ABSOLUTE_URL).'#';

Controller/DocumentationController.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
*/
2424
class DocumentationController extends Controller
2525
{
26+
/**
27+
* Serves the entrypoint of the API.
28+
*
29+
* @return JsonLdResponse
30+
*
31+
* @Route(name="json_ld_api_entrypoint", path="/")
32+
*/
33+
public function entrypointAction()
34+
{
35+
return new JsonLdResponse([]);
36+
}
37+
2638
/**
2739
* Namespace of types specific to the current API.
2840
*
@@ -32,11 +44,11 @@ class DocumentationController extends Controller
3244
*/
3345
public function vocabAction()
3446
{
35-
return new Response('The app vocab.');
47+
return new JsonLdResponse($this->get('dunglas_json_ld_api.api_documentation')->getDocumentation());
3648
}
3749

3850
/**
39-
* JSON-LD context for a given type
51+
* JSON-LD context for a given type.
4052
*
4153
* @param string $shortName
4254
*

Controller/ResourceController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ protected function findOrThrowNotFound(Resource $resource, $id)
112112
* @param Resource $resource
113113
* @param Request $request
114114
*
115-
* @return array|\Traversable
115+
* @return Paginator
116116
*/
117117
protected function getCollectionData(Resource $resource, Request $request)
118118
{

DependencyInjection/Configuration.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public function getConfigTreeBuilder()
3131

3232
$rootNode
3333
->children()
34+
->scalarNode('title')->cannotBeEmpty()->isRequired()->info('API\'s title.')->end()
35+
->scalarNode('description')->cannotBeEmpty()->isRequired()->info('API\'s description.')->end()
3436
->integerNode('elements_by_page')->min(1)->defaultValue(100)->cannotBeEmpty()->info('The number of elements by page in collections.')->end()
3537
->end()
3638
;

DependencyInjection/DunglasJsonLdApiExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public function load(array $configs, ContainerBuilder $container)
3131
$configuration = new Configuration();
3232
$config = $this->processConfiguration($configuration, $configs);
3333

34+
$container->setParameter('dunglas_json_ld_api.title', $config['title']);
35+
$container->setParameter('dunglas_json_ld_api.description', $config['description']);
3436
$container->setParameter('dunglas_json_ld_api.elements_by_page', $config['elements_by_page']);
3537

3638
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));

0 commit comments

Comments
 (0)