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

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.

Capture d'écran Api-platform admin

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!

API Platform Admin zone de texte 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...