DEV Community

Estéban
Estéban

Posted on

Création et visualisation des articles - Créer un blog avec Adonis

Bonjour,

Bienvenue dans ce tutoriel pour apprendre à utiliser le framework web Adonis ! Si tu souhaites en savoir plus sur Adonis en 1 coup œil, je t'invite à lire cette page.

Dans cette partie, on va voir ensemble comment créer un article, le lier à un utilisateur et l'afficher sur une page !

Rappel

Ce tutoriel est la partie 4 d'une série de tutoriels qui ont pour objectif de te faire découvrir Adonis au travers la création d'un blog.

Pour lire la partie précédente, c'est par là Création de l'authentification pour l'utilisateur - Créer un blog avec Adonis

Tu trouveras aussi sur GiHub l'ensemble du code source du projet !

Sommaire

Ce tutoriel est découpé en différente partie pour t'aider et pour éviter d'avoir des articles trop longs où l'on pourrait se perdre !

Nous allons donc voir ensemble :

Finalement, tu auras un blog fonctionnel !

Création et visualisation des articles

On va faire ce chapitre sous la forme d'un petit exercice. En effet, on a, dans les parties précédentes notamment Création d'un utilisateur - Créer un blog avec Adonis, presque tout vu pour te permettre d'être autonome sur cette partie. Je vais t'expliquer ce qu'il faut faire, te donner quelques indices puis on corrigera pas à pas ensuite !

Ce qu'il faut faire

La liste suivante est l'ensemble des tâches ordonnées t'indiquant ce qu'il y a à faire pour arriver au bout de cette partie :

  1. Créer un modèle pour nos articles
    • Un article contient les propriétés suivantes :
      • un id
      • un titre
      • du contenu
      • un propriétaire
      • une date de création
      • une date de mise à jour
    • Pour le propriétaire, on peut s'aider de ce lien
  2. Créer une migration pour les articles
    • On fixera la limite du contenu à 1024 caractères
    • On s'assurera du lien entre le propriétaire et la table des utilisateurs
    • Aide ici et
  3. Créer un seeder pour les articles et éviter de devoir les créer à la main lors de la phase de développement de l'application
  4. Créer la route permettant de visualiser un ensemble d'articles et la route permettant de visualiser 1 article
    • GET /articles, pas d'authentification requise
    • GET /articles/:id, pas d'authentification requise
    • Aide ici
  5. Créer le controller et les vues en charge de la gestion de ses 2 routes
    • Dans le controller de la première route, on ira chercher l'ensemble des articles présents en base de données en les paginant et on affichera le tout dans une vue. On s'assurera qu'ils soient ordonnés par date de création dans l'ordre décroissant !
      • Un peu de lecture ici et !
    • Dans le controller de la seconde route, on ira chercher un article spécifique dépendant de l'id présent dans l'url, on y chargera l'utilisateur et on affichera le tout dans une vue

Et voilà ! Si tu as réussi à faire tout ça, bravo ! Je t'invite à bien lire les différents liens, à chercher, à essayer et à te tromper !

Création du modèle

Pour le modèle, c'est assez simple. On commence par la commande pour créer un nouveau modèle :

node ace make:model article
Enter fullscreen mode Exit fullscreen mode

Ensuite, on complète notre modèle avec les informations souhaitées :

import { DateTime } from 'luxon'
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
import User from './User'

export default class Article extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public title: string

  @column()
  public content: string

  @column()
  public ownerId: number

  @belongsTo(() => User, { localKey: 'id', foreignKey: 'ownerId' })
  public owner: BelongsTo<typeof User>

  @column.dateTime({
    autoCreate: true,
  })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}
Enter fullscreen mode Exit fullscreen mode

La nouveauté concerne cette partie :

@belongsTo(() => User, { localKey: 'id', foreignKey: 'ownerId' })
public owner: BelongsTo<typeof User>
Enter fullscreen mode Exit fullscreen mode

On dit simplement ici qu'un article appartient à un utilisateur et que pour faire le lien, on utilise la clé ownerId de l'article et la clé id de l'utilisateur. Ensuite, cela ajoutera une propriété owner à l'article du type User.

Pour en savoir plus : belongsTo

Création de la migration

Pour la création de la migration, on va aussi utiliser une commande pour la générer :

node ace make:migration article
Enter fullscreen mode Exit fullscreen mode

La migration ressemblera à cela :

import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class Articles extends BaseSchema {
  protected tableName = 'articles'

  public async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string('title').notNullable().unique()
      table.string('content', 1024).notNullable()
      table.integer('owner_id').references('id').inTable('users')

      table.timestamp('created_at', { useTz: true })
      table.timestamp('updated_at', { useTz: true })
    })
  }

  public async down() {
    this.schema.dropTable(this.tableName)
  }
}
Enter fullscreen mode Exit fullscreen mode

On remarque la création d'une clé secondaire sur owner_id en lien avec la table users. Cela permet de mettre sous contrainte la base de données pour s'assurer que l'id donné dans cette colonne soit effectivement existant dans la table des utilisateurs.

Aussi, on met la colonne title avec unique. Ainsi, on s'assure que tous les titres soient uniques directement dans les contraintes de la base de données.

Pour en savoir plus : Table

Création du seeder

Afin d'avoir des données dans notre base de données et pour éviter de devoir les entrer à la main à chaque fois, nous allons les inscrire une fois dans un fichier puis faire tourner ce fichier afin qu'il rentre les données dans la base de données.

Commençons par la commande suite :

node ace make:seeder article
Enter fullscreen mode Exit fullscreen mode

Dans le dossier database/seeders, tu trouveras notre seeder. Dans ce fichier, nous allons utiliser les méthodes de notre modèle pour insérer de la donnée dans la base de données. Nous allons aussi récupérer notre utilisateur pour le lier à nos articles.

Dans un premier temps, on récupère notre utilisateur :

const owner = await User.firstOrFail()
Enter fullscreen mode Exit fullscreen mode

Ensuite, on utilise la méthode updateOrCreateMany qui permet de mettre à jour des articles dépendant d'une clé ou de les créer s'ils ne sont pas trouvés :

await Article.updateOrCreateMany(uniqueKey, [])
Enter fullscreen mode Exit fullscreen mode

Dans notre cas, le premier sera le titre puisqu'on a défini qu'il est unique selon la migration d'un article. Aussi, entre les crochets, on va y mettre l'ensemble des données que l'on va insérer ou mettre à jour. Finalement, notre fichier ressemble à cela :

import BaseSeeder from '@ioc:Adonis/Lucid/Seeder'
import Article from 'App/Models/Article'
import User from 'App/Models/User'

export default class ArticleSeeder extends BaseSeeder {
  public static developmentOnly = true

  public async run() {
    const uniqueKey = 'title'

    const owner = await User.firstOrFail()

    await Article.updateOrCreateMany(uniqueKey, [
      {
        title: 'Article 1',
        content:
          'Nulla quis ipsum sed augue laoreet imperdiet. Fusce dapibus, lorem quis convallis fringilla, sem est maximus nulla, id egestas orci libero eget est. In maximus vestibulum nisi, dignissim aliquam orci dictum id.',
        ownerId: owner!.id,
      },
      {
        title: 'Article 2',
        content:
          'Suspendisse est mi, ultrices sit amet ullamcorper sed, semper non ipsum. Vestibulum at nisl sed purus luctus sodales. Nunc lectus lorem, vehicula in dolor pharetra, pulvinar convallis libero. Maecenas iaculis porta nibh in hendrerit. Suspendisse gravida leo non orci facilisis placerat.',
        ownerId: owner!.id,
      },
      {
        title: 'Article 3',
        content:
          'Curabitur vitae mi aliquam, pretium velit id, varius lacus. Duis id tellus nec eros semper elementum et et lectus. Phasellus eros justo, eleifend eget tellus quis, accumsan sollicitudin ex. ',
        ownerId: owner!.id,
      },
      {
        title: 'Article 4',
        content:
          'Sed id eleifend lacus. Cras est diam, commodo et erat ac, elementum volutpat dolor. Donec auctor, lorem vitae luctus aliquet, mi mi rhoncus nunc, vel vestibulum felis justo sit amet felis. Donec eleifend rhoncus nisi id pretium. Morbi sit amet auctor enim, sit amet finibus velit. In hac habitasse platea dictumst.',
        ownerId: owner!.id,
      },
      {
        title: 'Article 5',
        content:
          'Morbi eget porttitor turpis. Fusce venenatis tortor lacus, eget interdum augue pellentesque id. Vestibulum elit lorem, gravida at elit vel, molestie suscipit mauris. ',
        ownerId: owner!.id,
      },
      {
        title: 'Article 6',
        content:
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce congue nec tortor ut congue. Aenean a nunc nec felis sagittis auctor non a metus. Aenean euismod ligula eros, eu tempor turpis molestie sit amet. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. ',
        ownerId: owner!.id,
      },
    ])
  }
}
Enter fullscreen mode Exit fullscreen mode

On remarque la clé developmentOnly à true qui permet de s'assurer que le seeder ne tourne qu'en développement. On ne souhaite pas de faux articles en production !

Et pour insérer nos données, il nous faut utiliser la commande suivante :

node ace db:seed
Enter fullscreen mode Exit fullscreen mode

Et voilà, nos données sont prêtes ! Pour les visualiser, tu peux te rendre dans pgAdmin sur la table des articles !

Pour en savoir plus : seeder, updateOrCreateMany

Création des routes

La création de la route tient en unique ligne :

Route.resource('articles', 'ArticlesController')
Enter fullscreen mode Exit fullscreen mode

La méthode resource permet de générer l'ensemble des routes dont nous allons avoir besoin ! Ainsi, pas besoin de se casser la tête ! Ainsi, on s'assure de rester dans les conventions d'Adonis tout en restant concis mais efficace dans notre code. Je te conseille d'aller lire la documentation à ce sujet qui explique et illustre très bien l'objectif d'utiliser cette méthode.

Pour en savoir plus : resource

Création du controller

Récupération de l'ensemble des articles

Pour cela, on va utiliser le query builder de Lucid. Ainsi, pour récupérer l'ensemble des articles de manière paginé et décroissant selon la date de création, la query ressemble à cela :

const articles = await Article.query().orderBy('created_at', 'desc').paginate(page, 3)
Enter fullscreen mode Exit fullscreen mode

Bien sûr, il nous faudra pouvoir changer de page. Par conséquent, nous allons devoir récupérer une entrée utilisateur nommée page comme cela :

const page = request.input('page', 1)
Enter fullscreen mode Exit fullscreen mode

Finalement, notre fonction index ressemble à cela :

public async index({ request }: HttpContextContract) {
    const page = request.input('page', 1)

    const articles = await Article.query().orderBy('created_at', 'desc').paginate(page, 3)

    return articles
  }
Enter fullscreen mode Exit fullscreen mode

Rendons-nous sur cette adresse. On peut ainsi observer l'ensemble de nos articles. Mais je dois bien dire que ça n'est ni très beau, ni très pratique. Mais pas de panique, la création de la vue nous permettra d'exploiter l'ensemble de ses données pour tout bien afficher !

Récupération d'un unique article

Pour faire cela, on commence par extraire notre id des paramètres de l'url :

const { id } = params
Enter fullscreen mode Exit fullscreen mode

Ensuite, on effectue une simple recherche en demandant à la recherche d'échouer si l'article n'est pas trouvé. En effet, cela permet de s'assurer que l'article demandé existe et de contrôler la réponse à cet échec :

let article: Article
try {
  article = await Article.findOrFail(id)
} catch (error) {
  console.error(error)
}
Enter fullscreen mode Exit fullscreen mode

Sur la gestion de l'erreur, on va simplement afficher l'erreur dans la console pour le moment.

Puis à la fin, on retourne l'article :

return article
Enter fullscreen mode Exit fullscreen mode

Allons voir le résultat ici ! Ce n'est pas mal, mais on ne voie pas les informations de l'utilisateur alors qu'on avait indiqué un lien entre les 2 lors de la création de notre modèle.

Pour lier les 2, il faut utiliser la méthode asynchrone load après la récupération de l'article :

article = await Article.findOrFail(id)
await article.load('owner')
Enter fullscreen mode Exit fullscreen mode

Si l'on retourne observer le résultat, on voit bien apparaitre le propriétaire sous la clé owner.

Pour en savoir plus : load, findOrFail

Création des vues

Dans cette section, nous allons devoir créer la vue puis modifier nos controllers pour qu'il génère la vue en fonction des données.

Pour stocker l'ensemble de nos routes, nous allons commencer par créer un dossier nommé articles dans resources/views

Visionnage de l'ensemble des articles

Dans le dossier précédemment créer, ajoutons un fichier nommé index.edge.

Dedans, nous y trouverons cela :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Les articles</title>
</head>
<body>
  <h1>Les articles</h1>
  <section>
    @each(article in articles)
    <article>
      <a href="{{ route('ArticlesController.show', { id: article.id }) }}">
        <h2>
          {{ article.title }}
        </h2>
      </a>
      <p>
        {{ excerpt(article.content, 100) }}
      </p>
    </article>
    @endeach
  </section>
  <div>
  @each(anchor in articles.getUrlsForRange(1, articles.lastPage))
    <a href="{{ anchor.url }}">
      {{ anchor.page }}
    </a>
  @endeach
</div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

C'est un fichier le plus simple possible car nous ne nous concentrons pas sur le design du site mais sur ses fonctionnalités et la découverte d'Adonis.

On trouve une boucle @each qui permet d'itérer au travers l'ensemble des articles et de générer le code html correspondant. La fonction route permet de générer l'url de manière dynamique en fonction de son nom et d'un paramètre. Dans notre cas, la paramètre et l'id de l'article. Il est possible de faire cela car l'id d'un article est sa clé primaire qui est par définition unique.

Dans le bas du fichier, on observe la fonction getUrlsForRange qui permet d'obtenir ensemble des urls et son numéro entre 2 valeurs. Cela est très pratique pour générer la pagination de notre site.

Il existe d'autres propriétés très utiles permettant de savoir si le lien est actif ou non, d'avoir le total des pages...

Ensuite, dans notre controller, nous devons indiquer que nous souhaitons rendre la page index des articles avec le paramètre articles, qui représente l'ensemble des articles :

public async index({ request, view }: HttpContextContract) {
    const page = request.input('page', 1)

    const articles = await Article.query().orderBy('created_at', 'desc').paginate(page, 3)

+    articles.baseUrl('/articles')
-    return articles
+    return view.render('articles/index', {
+      articles,
+    })
   }
Enter fullscreen mode Exit fullscreen mode

La fonction baseUrl permet de s'assurer d'obtenir la bonne url sur la génération des urls dans le template lors de l'utilisation de getUrlsForRange par exemple.

Nous pouvons maintenant nous rendre ici pour admirer le résultat ! Il t'est possible de jouer avec la navigation en toute simplicité !

Visionnage d'un unique article

Rien de sorcier ici. On commence par créer la vue, show.edge dans le fichier resources/views :

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
  </head>
  <body>
    <a href="{{ route('ArticlesController.index') }}">Voir les articles</a>

    <section>
      <h1>{{ article.title }}</h1>
      <p>{{ article.content }}</p>
      <div>par <strong> {{article.owner.pseudo}} </strong> le {{ article.createdAt.toLocaleString({ locale: 'fr-FR'}) }}, mis à jour le {{ article.updatedAt.toLocaleString({ locale: 'fr-FR'}) }}</div>
    </section>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

On retrouve en haut de la page, un lien permettant de retourner voir l'ensemble des articles. Dessous, on affiche l'article, son auteur et on adapte les dates à notre région !

Dans le controller, comme précédemment, on ajoute le fait de vouloir générer la route avec l'article que l'on a sorti de la base de données :

 public async show({ view, params }: HttpContextContract) {
   const { id } = params

   let article: Article
   try {
     article = await Article.findOrFail(id)
     await article.load('owner')
   } catch (error) {
     console.error(error)
+    return view.render('errors/not-found')
   }

-  return article
+  return view.render('articles/show', {
+     article,
+  })
}
Enter fullscreen mode Exit fullscreen mode

Aussi, on a ajouté la génération de la route not-found si jamais on ne trouve pas l'article !

En savoir plus : each, getUrlsForRange, render, ,toLocalString

Conclusion

Et voilà pour cette quatrième partie. On a vu plus en détail la création d'un modèle et de la manière dont on peut aller chercher des données dans la base de données. On a aussi vu comment lier nos données à nos pages.

Dans la suite, on va créer les pages de création et d'édition de nos articles et les sécuriser grâce à un middleware !

N'hésite pas à commenter si tu as des questions, si ça t'a plus ou même pour me faire des retours !

Et tu peux aussi me retrouver sur Twitter ou sur LinkedIn !

On se donne rendez-vous ici, Gestion des articles - Créer un blog avec Adonis pour la suite du tutoriel et gérer nos articles !

Discussion (0)