--- title: Kata-API tags: Présentation, Marche à suivre --- # Kata-API marche à suivre ## Pré-requis : - git - Linux - docker, docker-compose ## Docker-compose Avant toute chose, on stoppe tous les containers docker en cours d'execution pour éviter les conflits `docker stop $(docker ps -q)` Ensuite on récupère le code du Kata-API en clonant le repo : ``` git clone git@github.com:epfl-dojo/Kata-API.git kata_api_speedrun && cd kata_api_speedrun ``` Afin d'avoir les outils nécessaires, nous utilisons `docker-compose`. Les services utilisés sont : - **php**: l'intérpréteur php, sa seule fonction est d'interpréter le code PHP. - **nginx**: le serveur web. redirige les requêtes vers le container php - **db**: la base de donnée. Ici, le système de gestion de base de donnée [MariDB](https://mariadb.org/) est utilisé. - **adminer**: une interface d'administration de la base de donnée, pour le confort. On modifie le fichier `docker-compose.yml` à la base du répertoire et on y ajoute le contenu suivant : ``` version: "3" services: php: container_name: php_kata_api build: context: ./docker args: USER_ID: 1000 GROUP_ID: 1000 volumes: # Dossier du projet Laravel - ./src:/srv ports: - 9000:9000 nginx: container_name: nginx_kata_api image: nginx:stable volumes: - ./src:/srv # montage du nginx.conf personnalisé à l'intérieur du container nginx - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf ports: - 8081:80 depends_on: - php db: container_name: db_kata_api image: mariadb environment: MYSQL_ROOT_PASSWORD: Super MYSQL_DATABASE: kata-api volumes: # le docker-entrypoint-initdb.d est le dossier DB par défaut de l'image docker mariadb - ./initSQL:/docker-entrypoint-initdb.d ports: - 3306:3306 adminer: container_name: adminer_kata_api image: adminer ports: - 8888:8080 ``` ## Dockerfile Les services nginx, db et adminer utilisent des images directement pullées depuis dockerhub.com. Le service php, lui doit être basé sur un Dockerfile car on doit installer Laravel dessus. On l'a référencé comme `./docker/Dockerfile` dans le docker-compose, donc on va créer un dossier docker avec un dockerfille dedans : ``` mkdir docker touch docker/Dockerfile ``` On colle le contenu suivant dans le dockerfile : ``` FROM php:7.4-fpm ARG USER_ID=1000 ARG GROUP_ID=1000 RUN apt-get update; apt-get install -yq git vim zip npm; RUN curl -sS https://getcomposer.org/installer | php RUN mv composer.phar /usr/local/bin/composer RUN composer global require laravel/installer RUN export PATH=$PATH:/root/.composer/vendor/bin RUN docker-php-ext-install pdo pdo_mysql # https://jtreminio.com/blog/running-docker-containers-as-current-host-user/ RUN echo ${USER_ID:-0} RUN echo ${GROUP_ID:-0} RUN set -e -x; if [ ${USER_ID:-0} -ne 0 ] && [ ${GROUP_ID:-0} -ne 0 ]; then \ userdel -f www-data &&\ if getent group www-data ; then groupdel www-data; fi &&\ groupadd -g ${GROUP_ID} www-data &&\ useradd -l -u ${USER_ID} -g www-data www-data &&\ install -d -m 0755 -o www-data -g www-data /home/www-data &&\ chown --changes --no-dereference --recursive \ --from=33:33 ${USER_ID}:${GROUP_ID} \ /home/www-data \ /srv \ /var/www/html \ ;fi USER www-data WORKDIR /srv ``` Note: on utilise ici l'image php-fpm, installons composer (gestionnaire de packages qui est à php ce que gem est à ruby et npm à nodejs), installons Laravel puis les extensions PHP PDO (pour la connection à la base de donnée). Finalement, nous nous assurons que l'utilisateur du conteneur est celui avec l'ID 1000 (parce qu'à priori cela "matchera" l'utilisateur local et évitera des problèmes de permissions). ## Configuration serveur Nginx Notre serveur web, [nginx](https://www.nginx.com/), nécessite d'être configuré pour passer les requêtes destinées à PHP dans l'interpréteur php-fpm. Pour cela nous créons un `fastcgi_pass` pour tous les appels sur des fichiers `*.php`. Comme spécifié dans le docker-compose.yml, ` - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf`, il est donc nécessaire de créer un fichier `nginx.conf` dans le répertoire `docker`. Le contenu de `nginx.conf` est le suivant : ``` server { listen 80; index index.php index.html; server_name localhost; rewrite_log on; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; root /srv/speedrun/public; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php_kata_api:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } } ``` ## Lancement des services Docker Avant de démarrer les containers, créer le répertoire `src` à la racine de notre projet : `mkdir src` Une fois ces préparations initiales effectuées, on peut démarrer les containers avec `docker-compose` : ``` docker-compose up --build ``` Note: le `--build` n'est pas nécessaire la première fois mais permet de forcer la reconstruction des images si nécessaire. Observer les logs de `docker-compose` : ``` Starting db_kata_api ... done Starting adminer_kata_api ... done Starting php_kata_api ... done Creating nginx_kata_api ... done Attaching to php_kata_api, db_kata_api, adminer_kata_api, nginx_kata_api ``` → docker-compose démarre les containers. ``` db_kata_api | 2021-02-04 16:21:34+00:00 [Note] [Entrypoint]: Creating database kata-api db_kata_api | db_kata_api | 2021-02-04 16:21:34+00:00 [Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01-create-db.sql db_kata_api | 2021-02-04 16:21:34+00:00 [Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/beers.sql db_kata_api | 2021-02-04 16:21:34+00:00 [Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/breweries.sql db_kata_api | 2021-02-04 16:21:34+00:00 [Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/categories.sql db_kata_api | 2021-02-04 16:21:34+00:00 [Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/styles.sql ``` → On voit ici que nos fichiers d'initialisation de la base de données ont correctement été exécutés. ``` db_kata_api | 2021-02-04 16:21:35 0 [Note] mysqld: ready for connections. ``` → la base de données est prête. →→ A partir de ce point, notre environnement de dévelopement est prêt, nous pouvons installer les librairies et dépendances nécessaires au bon déroulement du Kata. ## Installation de Laravel Avant toute chose, il est nécessaire de créer le projet Laravel : ``` docker exec -it php_kata_api composer create-project laravel/laravel speedrun ``` Note qu'on peut "entrer" dans le container avec la commande : ``` docker exec -it php_kata_api bash ``` On peut ensuite vérifier si l'installation s'est bien déroulée en se rendant sur http://localhost:8081 On tombe alors sur la page d'accueil par défaut de Laravel : ![](https://i.imgur.com/nizMJq9.png) **Laravel est à présent opérationnel !** ![](https://media.giphy.com/media/uTuLngvL9p0Xe/giphy.gif) ## Laravel Maintenant que Laravel est fonctionnel, nous pouvons passer aux choses sérieuses. Quelques rappels : Laravel est un framework PHP MVC (Modèle Vue Controller). Par conséquent, l'organisation des différents fichiers suit une organisation précise, bien que modifiable. Il est important de connaître les choses suivantes: - `public/index.php` → le point d'entrée de toutes les requêtes HTTP passées au Framework. - `routes` → le dossier contenant les routes de l'application, par exemple `web.php` ou `api.php` - `config` → les différents fichiers de configuration - `ressources` → docuemnts, images, javascript et css - `app/Models/` → **M** les modèles - `resources/views/` → **V** les vues - `app/Http/Controllers/` → **C** les controlleurs Concernant les fichiers de configuration, il est possible de les éditer, mais on peut observer l'utilisation de la fonction `env`. Par conséquent, il est plutôt conseillé de laisser la configuration par défaut et d'utiliser les `.env` pour modifier l'application. C'est ce que nous allons faire dans ce "speedrun". ### Schéma explicatif ![](https://i.imgur.com/tXUWUcT.jpg) (source: https://selftaughtcoders.com/from-idea-to-launch/lesson-17/laravel-5-mvc-application-in-10-minutes/) ### Objectifs L'objectif qui est fixé pour ce speedrun est de créer une API dans les règles de l'art en un minimum de temps. Elle doit fournir les accesseurs nécessaires au CRUD sur la base de donnée. ### Tester la connection à la base de donnée En premier lieu il est confortable de s'assurer que nous avons bien accès à la base de données. Le code suivant, placé dans `web.php`, permet de le vérifier : ``` // Comfort route to test DB and PDO // https://github.com/laravel/framework/issues/30737 Route::get('/test-db', function() { try { DB::connection()->getPdo(); echo "<h1> It works! 🤓 </h1>"; } catch (\Exception $e) { die("<pre>Could not connect to the database. Please check your configuration. error:" . $e ); } }); ``` En se rendant sur http://localhost:8081/test-db, on obtient l'erreur suivante: `Could not connect to the database. Please check your configuration. error:PDOException: SQLSTATE[HY000] [2002] Connection refused in /srv/speedrun/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70` On édite donc le fichier `src/speedrun/.env` et on s'assure que la partie "DATABASE" est en adéquation avec nos besoins (et accessoirement le `docker-compose.yml`) : ``` DB_CONNECTION=mysql DB_HOST=db DB_PORT=3306 DB_DATABASE=kata-api DB_USERNAME=root DB_PASSWORD=Super ``` http://localhost:8081/test-db afficher maintenant une page blanche, indiquant que la connection à la base de données a bien pu se faire. ## Reliese Reliese est un ensemble de composants de Laravel qui permettent de gagner du temps en générant automatiquement du code. Nous allons l'utiliser pour effectuer des migrations depuis notre DB Pour l'installer, utiliser la commande : ``` docker exec -it php_kata_api bash -c "cd speedrun; composer require reliese/laravel" ``` On fait ensuite la migration : ``` docker exec -it php_kata_api bash -c "cd speedrun; php artisan vendor:publish --tag=reliese-models" ``` > Copied File [/vendor/reliese/laravel/config/models.php] To [/config/models.php] > Publishing complete. Une fois que c'est fait, il ne nous reste plus qu'a scaffolder le modèle de chaque table de la db : ``` docker exec -it php_kata_api bash -c "cd speedrun; php artisan code:models --table=beers"; docker exec -it php_kata_api bash -c "cd speedrun; php artisan code:models --table=breweries"; docker exec -it php_kata_api bash -c "cd speedrun; php artisan code:models --table=categories"; docker exec -it php_kata_api bash -c "cd speedrun; php artisan code:models --table=styles"; ``` > Check out your models for beers > Check out your models for breweries > Check out your models for categories > Check out your models for styles ### Les routes Il s'agit maintenant de créer les routes de l'API. Pour l'exemple on ajoute `/beers` ``` use App\Models\Beer; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; Route::get('beers', function() { // If the Content-Type and Accept headers are set to 'application/json' // this will return a JSON structure. This will be cleaned up later. return Beer::paginate(); }); ``` On peut tester la fonctionnalité sur http://localhost:8081/api/beers ... ## Controlleurs Lorsqu'on décide d'effectuer des opération plus complexes sur les routes de l'API, on se rend compte que la visibilité est mauvaise. On va donc utiliser un controlleur dédié pour chaque table. Pour ce faire, on va en créer un pour chaque tables de la db avec la syntaxe suivante : `<NomDeLaTable>Controller.php` ``` docker exec -it php_kata_api bash -c "cd speedrun; php artisan make:controller BeerController --resource --model=Beer" docker exec -it php_kata_api bash -c "cd speedrun; php artisan make:controller BreweryController --resource --model=Brewery" docker exec -it php_kata_api bash -c "cd speedrun; php artisan make:controller CategoryController --resource --model=Category" docker exec -it php_kata_api bash -c "cd speedrun; php artisan make:controller StyleController --resource --model=Style" ``` Si tout s'est bien passé, on retrouve nos différents fichiers controlleurs dans le projet : ![](https://i.imgur.com/YAJA4tI.png) On place les fonctions qui permettent d'effectuer les différentes opérations CRUD de la table Beers dans le controlleur BeerController : ``` <?php namespace App\Http\Controllers; use App\Models\Beer; use Illuminate\Http\Request; use \Illuminate\Support\Facades\Validator; use Log; use DateTime; class BeerController extends Controller { public static function getBeers(){ return Beer::paginate(); } public static function getBeer($id){ return Beer::find($id); } public static function postBeer($request){ $date = new DateTime(); $formatedDate = $date->format('Y-m-d H:i:s'); $data = $request->json()->all(); $data['last_mod'] = $formatedDate; $validator = Validator::make($data, [ 'name' => ['required', 'string'], 'brewery_id' => ['required', 'integer'], 'cat_id' => ['required', 'integer'], 'style_id' => ['required', 'integer'], 'abv' => ['required', 'numeric'], 'ibu' => ['required', 'numeric'], 'srm' => ['required', 'numeric'], 'upc' => ['required', 'integer'], 'filepath' => ['required', "string"], 'descript' => ['required', "string"], 'add_user' => ['required', 'integer'], 'last_mod' => ['required', 'string'], ]); if($validator->fails()){ Log::error('fail'); return 400; } else { Log::info('success'); Beer::create([ 'name' => $request->input('name'), 'brewery_id' => $request->input('brewery_id'), 'cat_id' => $request->input('cat_id'), 'style_id' => $request->input('style_id'), 'abv' => $request->input('abv'), 'ibu' => $request->input('ibu'), 'srm' => $request->input('srm'), 'upc' => $request->input('upc'), 'filepath' => $request->input('filepath'), 'descript' => $request->input('descript'), 'add_user' => $request->input('add_user'), 'last_mod' => $formatedDate] ); return 204; } } public static function putBeer($request, $id){ $date = new DateTime(); $formatedDate = $date->format('Y-m-d H:i:s'); $data = $request->json()->all(); $data['last_mod'] = $formatedDate; $validator = Validator::make($data, [ 'name' => ['required', 'string'], 'brewery_id' => ['required', 'integer'], 'cat_id' => ['required', 'integer'], 'style_id' => ['required', 'integer'], 'abv' => ['required', 'numeric'], 'ibu' => ['required', 'numeric'], 'srm' => ['required', 'numeric'], 'upc' => ['required', 'integer'], 'filepath' => ['required', "string"], 'descript' => ['required', "string"], 'add_user' => ['required', 'integer'], 'last_mod' => ['required', 'string'], ]); if($validator->fails()){ Log::error('fail'); return 400; } else { Log::info('success'); $beer = Beer::find($id); $beer->fill($data); $beer->save(); return 204; } } public static function patchBeer($request, $id){ $date = new DateTime(); $formatedDate = $date->format('Y-m-d H:i:s'); $data = $request->json()->all(); $data['last_mod'] = $formatedDate; $validator = Validator::make($data, [ 'name' => ['string'], 'brewery_id' => ['integer'], 'cat_id' => ['integer'], 'style_id' => ['integer'], 'abv' => ['numeric'], 'ibu' => ['numeric'], 'srm' => ['numeric'], 'upc' => ['integer'], 'filepath' => ['string'], 'descript' => ['string'], 'add_user' => ['integer'], ]); if($validator->fails()){ Log::error('fail'); return 400; } else { Log::info('success'); $beer = Beer::find($id); $beer->fill($data); $beer->save(); return 204; } } public static function deleteBeer($request, $id){ Beer::find($id)->delete(); return 204; } } ``` Je ne mets pas les fonctions des autres controlleurs car il se limite à `return nomDuModele::all();` et `return nomDuModele::find($id);` Une fois que c'est fait, on peut passer à la documentation. ## Documentation ### Installation Swagger [Swagger](swagger.io) est un langage de description d'API créé par l'[OAS](https://www.openapis.org/) Pour se faciliter la tâche, nous allons utiliser un autre package, `L5-Swagger`: https://github.com/DarkaOnLine/L5-Swagger ``` docker exec -it php_kata_api bash -c "cd speedrun; composer require darkaonline/l5-swagger" ``` Il faut ensuite publier le provider, ``` docker exec -it php_kata_api bash -c 'cd speedrun; php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"' ``` Swagger est maintenant installé, mais pour fonctionner correctement il se base sur les commentaires des routes et modèles selon les spécification de l'OAS (https://swagger.io/specification/). Dans le controlleur global (`app/Http/Controllers/Controllers.php`), ajouter les commentaires suivants : ``` /** * @OA\Info( * version="0.0.1", * title="Swagger Kata-API", * description="Beers Kata-API" * ) * */ ``` Afin de générer le fichier de configuration de swagger, utiliser la commande : ``` docker exec -it php_kata_api bash -c "cd speedrun; php artisan l5-swagger:generate" ``` On peut alors accéder à la documentation de swagger en utilisant la route par défaut : ``` http://localhost:8081/api/documentation ``` ### Documentation des Endpoints Nous allons documenter les endpoints, les routes et les modèles swagger en rajoutant chaque commentaires avant la fonction qui lui correspond dans le BeerController : ``` /** * @OA\Get( * path="/api/beers", * description="Get all beers", * tags={"Beer"}, * @OA\Response(response="default", description="List of all beers") * ) */ /** * @OA\Get( * path="/api/beer/{id}", * operationId="getBeerByID", * tags={"Beer"}, * summary="Get a beer by ID", * description="Return a beer by id", * @OA\Parameter( * name="id", * description="Beer ID", * required=true, * in="path", * @OA\Schema( * type="integer" * ) * ), * @OA\Response( * response=200, * description="Successful operation", * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ) * ) */ /** * @OA\Post( * path="/api/beer", * description="Create a beer", * tags={"Beer"}, * @OA\Response(response="default", description="Confirmation") * ) */ /** * @OA\Delete( * path="/api/beer/:id", * description="Delete a beer by id", * tags={"Beer"}, * @OA\Response(response="default", description="Confirmation") * ) */ /** * @OA\Put( * path="/api/beer/:id", * description="Modify a beer by id", * tags={"Beer"}, * @OA\Response(response="default", description="Confirmation") * ) */ /** * @OA\Patch( * path="/api/beer/:id", * description="Partially modify a beer by id", * tags={"Beer"}, * @OA\Response(response="default", description="Confirmation") * ) */ ``` Ensuite on répète le même schéma dans les autres controlleurs. BreweryController : ``` /** * @OA\Get( * path="/api/breweries", * description="Get all breweries", * tags={"Brewery"}, * @OA\Response(response="default", description="List of all breweries") * ) */ /** * @OA\Get( * path="/api/brewery/{id}", * operationId="getBreweryByID", * tags={"Brewery"}, * summary="Get a brewery by ID", * description="Return a brewery by id", * @OA\Parameter( * name="id", * description="Brewery ID", * required=true, * in="path", * @OA\Schema( * type="integer" * ) * ), * @OA\Response( * response=200, * description="Successful operation" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ) * ) */ ``` CategoryController ``` /** * @OA\Get( * path="/api/categories", * description="Get all categories", * tags={"Category"}, * @OA\Response(response="default", description="List of all categories") * ) */ /** * @OA\Get( * path="/api/category/{id}", * operationId="getCategoryByID", * tags={"Category"}, * summary="Get a category by ID", * description="Return a category by id", * @OA\Parameter( * name="id", * description="Category ID", * required=true, * in="path", * @OA\Schema( * type="integer" * ) * ), * @OA\Response( * response=200, * description="Successful operation" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ) * ) */ ``` StyleController : ``` /** * @OA\Get( * path="/api/styles", * description="Get all styles", * tags={"Style"}, * @OA\Response(response="default", description="List of all styles") * ) */ /** * @OA\Get( * path="/api/style/{id}", * operationId="getStyleByID", * tags={"Style"}, * summary="Get a style by ID", * description="Return a style by id", * @OA\Parameter( * name="id", * description="Style ID", * required=true, * in="path", * @OA\Schema( * type="integer" * ) * ), * @OA\Response( * response=200, * description="Successful operation" * ), * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" * ) * ) */ ``` On s'occupe maintenant des models. Swagger pour le modèle Beer : Avant la classe : ``` /** * @OA\Schema( * title="Beer", * description="Beer model", * @OA\Xml( * name="Beer" * ) * ) */ ``` Dans la classe : ``` /** * @OA\Property( * title="ID", * description="ID", * format="int64", * example=1 * ) * * @var integer */ private $id; /** * @OA\Property( * title="Name", * description="Name of the beer", * example="Edel Weissen" * ) * * @var string */ public $name; /** * @OA\Property( * title="Description", * description="Description of the beer", * example="This is beer description" * ) * * @var string */ public $descript; /** * @var Category * @OA\Property() */ private $cat_id; /** * @var Brewery * @OA\Property() */ private $brewery_id; /** * @OA\Property( * title="Created at", * description="Created at", * example="2020-01-27 17:50:45", * format="datetime", * type="string" * ) * * @var \DateTime */ private $created_at; /** * @OA\Property( * title="last_mod", * description="last_mod", * example="2020-01-27 17:50:45", * format="datetime", * type="string" * ) * * @var \DateTime */ private $last_mod; ``` Swagger pour le modèle Brewery : Avant la classe : ``` /** * @OA\Schema( * title="Brewery", * description="Brewery model", * @OA\Xml( * name="Brewery" * ) * ) */ ``` Dans la classe : ``` /** * @OA\Property( * title="ID", * description="ID", * format="int64", * example=1 * ) * * @var integer */ private $id; /** * @OA\Property( * title="Brewery", * description="Name of the brewery", * example="Alaskan Brewing" * ) * * @var string */ public $cat_name; ``` Swagger pour le modèle Category : Avant la classe : ``` /** * @OA\Schema( * title="Category", * description="Category model", * @OA\Xml( * name="Category" * ) * ) */ ``` Dans la classe : ``` use HasFactory; /** * @OA\Property( * title="ID", * description="ID", * format="int64", * example=1 * ) * * @var integer */ private $id; /** * @OA\Property( * title="Category", * description="Name of the category", * example="IPA" * ) * * @var string */ public $cat_name; ``` Voilà, il nous reste plus qu'à regénérer le fichier de configuration de Swagger : `docker exec -it php_kata_api bash -c "cd speedrun; php artisan l5-swagger:generate"` Et voilà, nous avons notre jolie API ! ### Webographie https://dev.to/avsecdongol/laravel-api-documentation-with-swagger-and-passport-3ec0 https://medium.com/@ivankolodiy/how-to-write-swagger-documentation-for-laravel-api-tips-examples-5510fb392a94 https://quickadminpanel.com/blog/laravel-api-documentation-with-openapiswagger/