Test de Api-platform et admin

Soumis par GoZ le dim 24/12/2017 - 11:27

Test de Api-platform et admin

Après avoir vu des présentations et lu des articles sur api-platform et son bundle admin, j'ai voulu tester voir ce que cela donnait.

Bonne nouvelle déjà, il y a de la documentation sur la manière de l'installer avec un exemple sur la manière de l'utiliser. C'est plutôt didactique et bien fait.

https://api-platform.com/docs/admin/getting-started

Autant la documentation de api-platform ne souffre d'aucun reproche, autant il en est autrement pour admin.
La documentation n'est pas forcément à jour, ainsi si l'on suit scrupuleusement les instructions, on rencontre tout de même des problèmes.

Pour mes tests, j'ai utilisé docker sur mac avec les données par défaut.

Tout d'abord, commençons par vider les caches de api-platform en version de production pour ne plus voir foo mais nos entités books et reviews.

docker-compose exec app bin/console cache:clear --env=prod

## Création de l'admin - Creating the admin

[Documentation originale sur api-platform](https://api-platform.com/docs/admin/getting-started#creating-the-admin)

Cela peut aller de soi pour la majorité, mais histoire de ne laisser planer aucun doute, la commande create-react-app my-admin est à lancer à la racine du projet, puis toutes les commandes yarn (ou npm) sont à lancer depuis le répertoire nouvellement créé: `my-admin`. C'est également dans ce répertoire que l'on trouvera le fichier `package.json` où retirer les 2 dépendances pour `react` et `react-dom`.

Avec les données par défaut on arrive donc à :

import React from 'react';
import { HydraAdmin } from '@api-platform/admin';

export default () => <HydraAdmin entrypoint="http://localhost"/>; // Replace with your own API entrypoint

Premier problème rencontré en lançant `yarn start`, la page http://localhost:3000 s'ouvre et j'ai le message `Unable to retrieve API documentation.`

En regardant de plus près dans la console, j'ai l'erreur suivante :

Blocage d’une requête multiorigines (Cross-Origin Request) : la politique « Same Origin » ne permet pas de consulter la ressource distante située sur http://localhost/. Raison : l’en-tête CORS « Access-Control-Allow-Origin » ne correspond pas à « null ».

Une piste donc.

Allons faire un tour dans le fichier `app/config/parameters.yml` et remplaçons la valeur de `cors_allow_origin` par `http://localhost:3000`.

On re-vide les caches:

docker-compose exec app bin/console cache:clear --env=prod

Et le résultat est là! Une administration propre et réactive pour notre API.

## Customizing the Admin

### Using Custom Components

Dans cette partie, on cherche à utiliser des composants personnalisés tel qu'un texte riche.
Lorsque l'on copie et colle le contenu fourni par la documentation, on obtient l'erreur:

Failed to compile.

./src/App.js
Module not found: Can't resolve 'api-doc-parser/lib/hydra/parseHydraDocumentation' in 'api-platform-2.1.4/my-admin/src'

Le fichier original tel que décrit dans la documentation:

import React from 'react';
import { RichTextField } from 'admin-on-rest';
import RichTextInput from 'aor-rich-text-input';
import HydraAdmin from 'api-platform-admin/lib/hydra/HydraAdmin';
import parseHydraDocumentation from 'api-doc-parser/lib/hydra/parseHydraDocumentation';

const entrypoint = 'http://localhost';

const apiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
.then(api => {
api.resources.map(resource => {
const books = api.resources.find(r => 'books' === r.name);
books.fields.find(f => 'description' === f.name).fieldComponent = <RichTextField source="description" key="description"/>;
books.fields.find(f => 'description' === f.name).inputComponent = <RichTextInput source="description" key="description"/>;

return resource;
});

return api;
})
;

export default (props) => (
<HydraAdmin apiDocumentationParser={apiDocumentationParser} entrypoint={entrypoint}/>
);

Et le fichier corrigé:

import React from 'react';
import { RichTextField } from 'admin-on-rest';
import RichTextInput from 'aor-rich-text-input';
import { HydraAdmin } from '@api-platform/admin';
import parseHydraDocumentation from '@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation';

const entrypoint = 'http://localhost';

const apiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
.then( ({ api }) => {
const books = api.resources.find(({ name }) => 'books' === name);
const description = books.fields.find(f => 'description' === f.name);

description.input = props => (
<RichTextInput {...props} source="description" />
);

description.input.defaultProps = {
addField: true,
addLabel: true,
};

return { api };
})
;

export default (props) => (
<HydraAdmin apiDocumentationParser={apiDocumentationParser} entrypoint={entrypoint}/>
);

Chouette, une zone de text riche!

## Managing Files and Images

Même chose pour gérer les images et les fichiers. Voici la configuration de la documentation:

import { FunctionField, ImageField, ImageInput } from 'admin-on-rest/lib/mui';
import React from 'react';
import HydraAdmin from 'api-platform-admin/lib/hydra/HydraAdmin';
import parseHydraDocumentation from 'api-doc-parser/lib/hydra/parseHydraDocumentation';

const entrypoint = 'http://localhost';

const apiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
.then(api => {
api.resources.map(resource => {
if ('http://schema.org/ImageObject' === resource.id) {
resource.fields.map(field => {
if ('http://schema.org/contentUrl' === field.id) {
field.denormalizeData = value => ({
src: value
});

field.fieldComponent = (
<FunctionField
key={field.name}
render={
record => (
<ImageField key={field.name} record={record} source={`${field.name}.src`}/>
)
}
source={field.name}
/>
);

field.inputComponent = (
<ImageInput accept="image/*" key={field.name} multiple={false} source={field.name}>
<ImageField source="src"/>
</ImageInput>
);

field.normalizeData = value => {
if (value[0] && value[0].rawFile instanceof File) {
const body = new FormData();
body.append('file', value[0].rawFile);

return fetch(`${entrypoint}/images/upload`, { body, method: 'POST' })
.then(response => response.json());
}

return value.src;
};
}

return field;
});
}

return resource;
});

return api;
})
;

export default (props) => (
<HydraAdmin apiDocumentationParser={apiDocumentationParser} entrypoint={entrypoint}/>
);

Et voici une configuration qui fonctionne avec la version 2.1.4:

import { FunctionField, ImageField, ImageInput } from 'admin-on-rest/lib/mui';
import React from 'react';
import { RichTextField } from 'admin-on-rest';
import RichTextInput from 'aor-rich-text-input';
import { HydraAdmin } from '@api-platform/admin';
import parseHydraDocumentation from '@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation';

const entrypoint = 'http://localhost';

const myApiDocumentationParser = entrypoint => parseHydraDocumentation(entrypoint)
.then( ({ api }) => {

const books = api.resources.find(({ name }) => 'books' === name);
const description = books.fields.find(f => 'description' === f.name);

description.input = props => (
<RichTextInput {...props} source="description" />
);

description.input.defaultProps = {
addField: true,
addLabel: true,
};

api.resources.map(resource => {
if ('http://schema.org/ImageObject' === resource.id) {
resource.fields.map(field => {
if ('http://schema.org/contentUrl' === field.id) {
field.denormalizeData = value => ({
src: value
});

field.fieldComponent = (
<FunctionField
key={field.name}
render={
record => (
<ImageField key={field.name} record={record} source={`${field.name}.src`}/>
)
}
source={field.name}
/>
);

field.inputComponent = (
<ImageInput accept="image/*" key={field.name} multiple={false} source={field.name}>
<ImageField source="src"/>
</ImageInput>
);

field.normalizeData = value => {
if (value[0] && value[0].rawFile instanceof File) {
const body = new FormData();
body.append('file', value[0].rawFile);

return fetch(`${entrypoint}/images/upload`, { body, method: 'POST' })
.then(response => response.json());
}

return value.src;
};
}

return field;
});
}

return resource;
});

return { api };
})
;

export default (props) => (
<HydraAdmin apiDocumentationParser={myApiDocumentationParser} entrypoint={entrypoint} />
);

L'application se lance à ce stade mais il nous manque une définition d'image du côté de nos entités pour pouvoir tester. Cela mérite un article a part entière. Affaire à suivre donc.

## Using a Custom Validation Function or Inject Custom Props

Idem pour cette partie, il faut vraiment que j'approfondisse React.js pour pouvoir maitriser et comprendre ce que sont censés faire ces composants et comment le corriger.

## Conclusion

C'est simple à mettre en place, et permet non seulement de créer rapidement une API, mais en plus de fournir les outils utiles aux administrateurs et consommateurs.

Rien à dire sur la partie Symfony/PHP claire, lisible et dans la lignée de de ce que l'on a l'habitude de développer.

Pour la partie administration en revanche, il faudra alors se faire à React dans le cas où le back-office auto-généré ne se suffit pas. Et là, c'est une autre histoire pour quelqu'un qui n'a jamais touché à du React...