Skip to content

Commit a970921

Browse files
committed
Merge branch '2.1'
2 parents f113a46 + 74ebd27 commit a970921

File tree

14 files changed

+201
-49
lines changed

14 files changed

+201
-49
lines changed

.travis.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
language: node
2+
3+
cache:
4+
yarn: true
5+
6+
env:
7+
global:
8+
secure: gjiWHIgNjLXuEFmLiepiOTHOuauHfeKHutrE0sBwFZUQzP9FoGTZzJub1z8/vm+hhygA+TZbB0iclygHyNrXCkZyNdnZXChXl4iPdYqY3OARPOAbQff16+/XSUDsZ/Ok1etdb3kKor1R4rBt/WywBvXmmdggumTA3yT5ExI+dyomdONo4yMUZ7g1la0ehMEzGqyVjt0nUW31PN3l6dI1qgigHCuotSrrpWP6fTXuUh5l6YA7KXb/V1hJxaGENLz1Cdfk0sF66e4KsV/DX6JZSqpvdqVB8OTPU+si511yJtGS8OeuLs4RqmXMLrY/ChCodlYnfCE+NleBFpUnCVqth/RDRh7LvplUJlpsXNcfyoA3mOFmZa0euOMCqJ3qwESz802Y9c5oN63hn2OUF/raFDc3SMMC86FFHxwyYjz5+yzXYupnFNj39NKdQ1v1KBbY8BD8UT8RU3mlu4/3LRz0tSamREHj3pGgBmvgUZfUE1dMngeaZjOmBaIZH8TnKQ75CvfnoT+LJnZo6g9g4uwz6jtaIziSMZ0OW/95nF8yx+xONbHEtt5ex5M09NOFsN2vB2bcUAgIjyGrmmNLQadwJyQv557IGPEE5CyGhJpXh9XZ+WMw2vO45MOw4sQPkARF6OJkMteqFc2NLXMOQJ07EScbgEKR/9VOcyxIk/7a7nc=
9+
10+
install:
11+
- cd ..
12+
- git clone https://github.com/api-platform/website.git
13+
- cd website
14+
- yarn install
15+
- ln -s $TRAVIS_BUILD_DIR src/pages/docs
16+
17+
script:
18+
- cd $TRAVIS_BUILD_DIR/../website
19+
- bin/generate-nav
20+
- yarn gatsby build
21+
22+
deploy:
23+
provider: pages
24+
skip_cleanup: true
25+
github_token: $GITHUB_TOKEN
26+
local_dir: $TRAVIS_BUILD_DIR/../website/public
27+
fqdn: api-platform.com
28+
on:
29+
branch: 2.1

admin/getting-started.md

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Then, create a new React application for your admin:
1111

1212
$ create-react-app my-admin
1313

14+
Now, go to the newly created `my-admin` directory:
15+
16+
$ cd my-admin
17+
1418
React and React DOM will be directly provided as dependencies of Admin On REST. As having different versions of React
1519
causes issues, remove `react` and `react-dom` from the `dependencies` section of the generated `package.json` file:
1620

@@ -34,6 +38,12 @@ import { HydraAdmin } from '@api-platform/admin';
3438
export default () => <HydraAdmin entrypoint="https://demo.api-platform.com"/>; // Replace with your own API entrypoint
3539
```
3640

41+
Be sure to make your API send proper [CORS HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to allow the admin's domain to access it. To do so, update the value of the `cors_allow_origin` parameter in `app/config/parameters.yml` (it will be `http://localhost:3000` by default).
42+
43+
Clear the cache to apply this change:
44+
45+
$ docker-compose exec app bin/console cache:clear --env=prod
46+
3747
Your new administration interface is ready! Type `yarn start` to try it!
3848

3949
Note: if you don't want to hardcode the API URL, you can [use an environment variable](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-custom-environment-variables).
@@ -51,26 +61,30 @@ In the following example, we change components used for the `description` proper
5161
import React from 'react';
5262
import { RichTextField } from 'admin-on-rest';
5363
import RichTextInput from 'aor-rich-text-input';
54-
import HydraAdmin from 'api-platform-admin/lib/hydra/HydraAdmin';
55-
import parseHydraDocumentation from 'api-doc-parser/lib/hydra/parseHydraDocumentation';
64+
import { HydraAdmin } from '@api-platform/admin';
65+
import parseHydraDocumentation from '@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation';
5666

5767
const entrypoint = 'https://demo.api-platform.com';
5868

59-
const apiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
60-
.then(api => {
61-
api.resources.map(resource => {
62-
const books = api.resources.find(r => 'books' === r.name);
63-
books.fields.find(f => 'description' === f.name).fieldComponent = <RichTextField source="description" key="description"/>;
64-
books.fields.find(f => 'description' === f.name).inputComponent = <RichTextInput source="description" key="description"/>;
69+
const myApiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
70+
.then( ({ api }) => {
71+
const books = api.resources.find(({ name }) => 'books' === name);
72+
const description = books.fields.find(f => 'description' === f.name);
6573

66-
return resource;
67-
});
74+
description.input = props => (
75+
<RichTextInput {...props} source="description" />
76+
);
6877

69-
return api;
78+
description.input.defaultProps = {
79+
addField: true,
80+
addLabel: true
81+
};
82+
83+
return { api };
7084
})
7185
;
7286

73-
export default (props) => <HydraAdmin apiDocumentationParser={apiDocumentationParser} entrypoint={entrypoint}/>;
87+
export default (props) => <HydraAdmin apiDocumentationParser={myApiDocumentationParser} entrypoint={entrypoint}/>;
7488
```
7589

7690
The `fieldComponent` property of the `Field` class allows to set the component used to render a property in list and show screens.
@@ -90,13 +104,28 @@ In the following example, we will:
90104
```javascript
91105
import { FunctionField, ImageField, ImageInput } from 'admin-on-rest/lib/mui';
92106
import React from 'react';
93-
import HydraAdmin from 'api-platform-admin/lib/hydra/HydraAdmin';
94-
import parseHydraDocumentation from 'api-doc-parser/lib/hydra/parseHydraDocumentation';
107+
import { RichTextField } from 'admin-on-rest';
108+
import RichTextInput from 'aor-rich-text-input';
109+
import { HydraAdmin } from '@api-platform/admin';
110+
import parseHydraDocumentation from '@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation';
95111

96112
const entrypoint = 'https://demo.api-platform.com';
97113

98-
const apiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
99-
.then(api => {
114+
const myApiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
115+
.then( ({ api }) => {
116+
117+
const books = api.resources.find(({ name }) => 'books' === name);
118+
const description = books.fields.find(f => 'description' === f.name);
119+
120+
description.input = props => (
121+
<RichTextInput {...props} source="description" />
122+
);
123+
124+
description.input.defaultProps = {
125+
addField: true,
126+
addLabel: true,
127+
};
128+
100129
api.resources.map(resource => {
101130
if ('http://schema.org/ImageObject' === resource.id) {
102131
resource.fields.map(field => {
@@ -143,11 +172,11 @@ const apiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
143172
return resource;
144173
});
145174

146-
return api;
175+
return { api };
147176
})
148177
;
149178

150-
export default (props) => <HydraAdmin apiDocumentationParser={apiDocumentationParser} entrypoint={entrypoint}/>;
179+
export default (props) => <HydraAdmin apiDocumentationParser={myApiDocumentationParser} entrypoint={entrypoint}/>;
151180
```
152181

153182
__Note__: In this example, we choose to send the file via a multi-part form data, but you are totally free to use another solution (like `base64`). But keep in mind that multi-part form data is the most efficient solution.
@@ -168,15 +197,17 @@ export default class extends Component {
168197
state = {api: null};
169198

170199
componentDidMount() {
171-
parseHydraDocumentation(entrypoint).then(api => {
200+
parseHydraDocumentation(entrypoint).then( ({ api }) => => {
172201
const books = api.resources.find(r => 'books' === r.name);
173202

174203
books.writableFields.find(f => 'description' === f.name).inputProps = {
175204
validate: value => value.length >= 30 ? undefined : 'Minimum length: 30';
176205
};
177206

178207
this.setState({api: api});
179-
})
208+
209+
return { api };
210+
});
180211
}
181212

182213
render() {

core/configuration.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ api_platform:
2121
# Specify a path name generator to use.
2222
path_segment_name_generator: 'api_platform.path_segment_name_generator.underscore'
2323

24-
# Specify a name for the folder within bundle that contain api resources.
25-
api_resources_directory: 'Entity'
26-
2724
eager_loading:
2825
# To enable or disable eager loading.
2926
enabled: true

core/extensions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,14 @@ use AppBundle\Entity\Offer;
6969
use AppBundle\Entity\User;
7070
use Doctrine\ORM\QueryBuilder;
7171
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
72-
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
72+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
7373

7474
final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
7575
{
7676
private $tokenStorage;
7777
private $authorizationChecker;
7878

79-
public function __construct(TokenStorageInterface $tokenStorage, AuthorizationChecker $checker)
79+
public function __construct(TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $checker)
8080
{
8181
$this->tokenStorage = $tokenStorage;
8282
$this->authorizationChecker = $checker;

core/filters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ For instance, treat entries with a property value of `null` as the smallest, wit
330330
# app/config/api_filters.yml
331331
services:
332332
offer.order_filter:
333-
parent: 'api_platform.doctrine.orm.date_filter'
333+
parent: 'api_platform.doctrine.orm.order_filter'
334334
arguments: [ { validFrom: { nulls_comparison: 'nulls_smallest' } } ]
335335
tags: [ 'api_platform.filter' ]
336336
```

core/getting-started.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ Alternatively, you can use [Composer](http://getcomposer.org) to install the sta
1212

1313
`composer require api-platform/core`
1414

15-
Then, update your `app/config/AppKernel.php` file:
15+
Then, update your `app/AppKernel.php` file:
1616

1717
```php
1818
<?php
19-
// app/config/AppKernel.php
19+
// app/AppKernel.php
2020

2121
public function registerBundles()
2222
{
@@ -106,7 +106,7 @@ class Product // The class name will be used to name exposed resources
106106
$this->offers->add($offer);
107107
}
108108

109-
public function removeGreeting(Offer $offer): void
109+
public function removeOffer(Offer $offer): void
110110
{
111111
$offer->product = null;
112112
$this->offers->removeElement($offer);

core/operation-path-naming.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ Pre-registered resolvers are available and can easily be overridden.
77

88
There are two pre-registered operation path naming services:
99

10-
Service name | Entity name | Path result
11-
--------------------------------------------------|--------------|----------------
12-
`api_platform.operation_path_resolver.underscore` | `MyResource` | `/my_resources`
13-
`api_platform.operation_path_resolver.dash` | `MyResource` | `/my-resources`
10+
Service name | Entity name | Path result
11+
------------------------------------------------------|--------------|----------------
12+
`api_platform.path_segment_name_generator.underscore` | `MyResource` | `/my_resources`
13+
`api_platform.path_segment_name_generator.dash` | `MyResource` | `/my-resources`
1414

15-
The default resolver is `api_platform.operation_path_resolver.underscore`.
15+
The default resolver is `api_platform.path_segment_name_generator.underscore`.
1616
To change it to the dash resolver, add the following lines to `app/config/config.yml`:
1717

1818
```yaml
1919
# app/config/config.yml
2020

2121
api_platform:
22-
default_operation_path_resolver: 'api_platform.operation_path_resolver.dash'
22+
path_segment_name_generator: api_platform.path_segment_name_generator.dash
2323
```
2424
2525
## Create a Custom Operation Path Resolver
@@ -75,5 +75,5 @@ services:
7575
# app/config/config.yml
7676
7777
api_platform:
78-
default_operation_path_resolver: 'AppBundle\PathResolver\NoSeparatorsOperationPathResolver'
78+
path_segment_name_generator: 'AppBundle\PathResolver\NoSeparatorsOperationPathResolver'
7979
```

core/operations.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,19 @@ class Question
287287
}
288288
```
289289

290+
Alternatively, you can use the YAML configuration format:
291+
292+
```yaml
293+
AppBundle\Entity\Answer: ~
294+
AppBundle\Entity\Question:
295+
properties:
296+
answer:
297+
subresource:
298+
resourceClass: 'AppBundle\Entity\Answer'
299+
collection: false
300+
```
301+
302+
290303
Note that all we had to do is to set up `@ApiSubresource` on the `Question::answer` relation. Because the `answer` is a to-one relation, we know that this subresource is an item. Therefore the response will look like this:
291304

292305
```json
@@ -360,6 +373,75 @@ Note that the operation name, here `api_questions_answer_get_subresource`, is th
360373
It'll be automatically set to `$resources_$subresource(s)_get_subresource`. To find the correct operation name you
361374
may use `bin/console debug:router`.
362375

376+
### Control the Path of Subresources
377+
378+
You can control the path of subresources with the `path` option of the `subresourceOperations` parameter:
379+
380+
```php
381+
<?php
382+
// src/AppBundle/Entity/Question.php
383+
384+
/**
385+
* @ORM\Entity()
386+
* @ApiResource(
387+
* subresourceOperations={
388+
* "answer_get_subresource"= {
389+
* "method"="GET",
390+
* "path"="/questions/{id}/all-answers",
391+
* },
392+
* },
393+
* )
394+
*/
395+
class Question
396+
{
397+
}
398+
```
399+
400+
### Control the Depth of Subresources
401+
402+
You can control depth of subresources with the parameter `maxDepth`. For example, if `Answer` entity also have subresource such as `comments`and you don't want the route `api/questions/{id}/answers/{id}/comments` to be generated. You can do this by adding the parameter maxDepth in ApiSubresource annotation or yml/xml file configuration.
403+
404+
```php
405+
<?php
406+
// src/AppBundle/Entity/Question.php
407+
408+
use ApiPlatform\Core\Annotation\ApiProperty;
409+
use ApiPlatform\Core\Annotation\ApiResource;
410+
use ApiPlatform\Core\Annotation\ApiSubresource;
411+
use Doctrine\ORM\Mapping as ORM;
412+
413+
/**
414+
* @ORM\Entity
415+
* @ApiResource
416+
*/
417+
class Question
418+
{
419+
/**
420+
* @ORM\Column(type="integer")
421+
* @ORM\Id
422+
* @ORM\GeneratedValue(strategy="AUTO")
423+
*/
424+
private $id;
425+
426+
/**
427+
* @ORM\Column
428+
*/
429+
public $content;
430+
431+
/**
432+
* @ORM\OneToOne(targetEntity="Answer", inversedBy="question")
433+
* @ORM\JoinColumn(referencedColumnName="id", unique=true)
434+
* @ApiSubresource(maxDepth=1)
435+
*/
436+
public $answer;
437+
438+
public function getId()
439+
{
440+
return $this->id;
441+
}
442+
}
443+
```
444+
363445
## Creating Custom Operations and Controllers
364446

365447
API Platform can leverage the Symfony routing system to register custom operation related to custom controllers. Such custom

core/serialization.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,19 +417,25 @@ services:
417417

418418
```php
419419
<?php
420-
// src/Appbundle/Serializer/ApiSerializer
420+
// src/Appbundle/Serializer/ApiNormalizer
421421
422422
namespace AppBundle\Serializer;
423423
424-
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
424+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
425425
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
426+
use Symfony\Component\Serializer\SerializerAwareInterface;
427+
use Symfony\Component\Serializer\SerializerInterface;
426428
427-
final class ApiNormalizer extends AbstractItemNormalizer
429+
final class ApiNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
428430
{
429431
private $decorated;
430432
431433
public function __construct(NormalizerInterface $decorated)
432434
{
435+
if (!$decorated instanceof DenormalizerInterface) {
436+
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
437+
}
438+
433439
$this->decorated = $decorated;
434440
}
435441
@@ -457,6 +463,13 @@ final class ApiNormalizer extends AbstractItemNormalizer
457463
{
458464
return $this->decorated->denormalize($data, $class, $format, $context);
459465
}
466+
467+
public function setSerializer(SerializerInterface $serializer)
468+
{
469+
if($this->decorated instanceof SerializerAwareInterface) {
470+
$this->decorated->setSerializer($serializer);
471+
}
472+
}
460473
}
461474
```
462475

0 commit comments

Comments
 (0)