---
title: 'Eloquent et gros volumes : chunk, lazy et cursor | DevSense'
description: 'Traiter et exporter des centaines de milliers de lignes dans Laravel sans saturer la mémoire : N+1 et preventLazyLoading, chunk contre chunkById, lazy et cursor, buffering PDO, streaming CSV, Query Builder et SQL brut pour les rapports.'
faq:
    - { question: 'Quelle est la différence entre chunk() et cursor() dans Laravel ?', answer: "chunk() exécute de nombreuses requêtes avec LIMIT et OFFSET et passe à la closure une collection de N modèles : il prend donc en charge l'eager loading. cursor() exécute une seule requête et crée les modèles un par un via un générateur, mais ne gère pas l'eager loading, et le driver PDO conserve malgré tout par défaut l'intégralité du résultat brut de la requête en mémoire." }
    - { question: 'Pourquoi chunk() saute-t-il des enregistrements quand on les modifie dans la boucle ?', answer: "chunk() parcourt le résultat via OFFSET. Si la boucle modifie la colonne sur laquelle la requête filtre (par exemple processed = false → true), les lignes traitées sortent de la sélection, et l'OFFSET suivant saute par-dessus des lignes non traitées. chunkById() pagine sur la clé primaire (WHERE id > dernier) et n'a pas ce problème." }
    - { question: 'Pourquoi cursor() finit-il quand même par saturer la mémoire ?', answer: "Les modèles sont créés un par un, mais PDO récupère par défaut l'intégralité du résultat depuis le serveur et le conserve dans un buffer côté client : c'est le cas de pdo_mysql (requêtes bufferisées) comme de pdo_pgsql. Sur des millions de lignes, ce buffer dépasse le memory_limit. Pour de tels volumes, utilisez lazyById() ou un curseur côté serveur de la base de données." }
    - { question: 'Quand faut-il abandonner Eloquent au profit du Query Builder ou du SQL brut ?', answer: 'Quand les modèles sont inutiles : agrégats et rapports (GROUP BY, fonctions de fenêtrage), UPDATE et INSERT ... SELECT en masse, export de données à plat. Eloquent crée un objet par ligne et applique casts et événements : sur des centaines de milliers de lignes, cela représente des secondes et des centaines de mégaoctets superflus. Attention : les événements de modèle et les observers ne sont pas déclenchés lors des opérations en masse.' }
published: '2026-10-03'
---
# Eloquent et gros volumes : N+1, chunk, lazy et cursor sans saturer la mémoire

Le rapport « toutes les commandes de l'année en CSV » fonctionne en staging, où il y a cinq mille commandes, et plante en production avec `Allowed memory size of 536870912 bytes exhausted`. La commande nocturne de recalcul des bonus traite la moitié des clients et se termine silencieusement, et l'on découvre le lendemain qu'un client sur deux a été ignoré. Une page qui liste cinquante articles envoie deux cents requêtes à la base.

Ces trois histoires relèvent d'une même compétence : comprendre ce qu'Eloquent fait à la base et à la mémoire, et choisir le bon outil selon le volume de données. Au programme de cet article : le N+1, la différence entre `chunk`, `chunkById`, `lazy` et `cursor`, pourquoi `cursor` ne protège pas de la saturation mémoire, comment streamer un export d'un million de lignes et quand il est plus honnête d'écrire du SQL.

**Voir aussi :** [Optimisation des requêtes](database-query-optimization) · [Index de bases de données](database-indexes-deep-dive) · [PostgreSQL pour les dashboards](postgresql-for-dashboards) · [Files d'attente Laravel en production](laravel-queues-production)

## Sommaire

* [D'où vient la saturation mémoire](#why-memory)
* [N+1 : des centaines de requêtes au lieu de deux](#n-plus-one)
* [Carte des méthodes pour les grandes sélections](#methods)
* [chunk() et le piège de l'OFFSET](#chunk)
* [lazy() et lazyById() : un flux plutôt que des lots](#lazy)
* [cursor() et le buffering PDO](#cursor)
* [Export CSV d'un million de lignes](#export)
* [Quand passer au Query Builder ou au SQL brut](#query-builder)
* [Comment mesurer](#measure)
* [Erreurs fréquentes](#common-mistakes)
* [Checklist](#checklist)
* [Quiz d'auto-évaluation](#self-test-quiz)

---

<a id="why-memory"></a>
## D'où vient la saturation mémoire

`Order::where('year', 2026)->get()` fait trois choses :

1. Exécute la requête et charge **toutes les lignes** du résultat dans la mémoire de PHP.
2. Crée **un objet modèle par ligne** : attributs, copie des valeurs d'origine pour le suivi des modifications, casts, relations.
3. Range les modèles dans une **collection**, qui vit aussi longtemps que la variable.

Un modèle Eloquent occupe en mémoire plusieurs fois plus de place que la ligne de table elle-même. Dix mille modèles ne posent généralement pas de problème ; un million, c'est le dépassement garanti du `memory_limit`. Pour les gros volumes, l'objectif est donc unique : **ne jamais garder tout le résultat en mémoire d'un coup**, ni les modèles, ni les lignes brutes.

---

<a id="n-plus-one"></a>
## N+1 : des centaines de requêtes au lieu de deux

Le grand classique : une liste d'articles avec le nom de leurs auteurs.

```php
$articles = Article::latest()->take(50)->get();

foreach ($articles as $article) {
    echo $article->author->name; // one extra query per article
}
```

Une requête pour les articles, plus une requête par auteur pour chaque article : 51 requêtes. Ajoutez les tags et le nombre de commentaires, et la page interroge la base deux cents fois. La solution, c'est l'**eager loading** :

```php
$articles = Article::latest()
    ->with(['author:id,name', 'tags'])
    ->withCount('comments')
    ->take(50)
    ->get();
```

Il n'y a plus que quatre requêtes : les articles, les auteurs, les tags (avec la table pivot) et le comptage des commentaires via une sous-requête. `author:id,name` ne charge que les colonnes utiles ; la clé `id` doit obligatoirement figurer dans la liste.

Pour que le N+1 ne revienne pas en douce, interdisez le chargement paresseux en dehors de la production :

```php
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}
```

En développement et dans les tests, l'accès à une relation non chargée lèvera une exception, tandis qu'en production le code continuera de fonctionner. Les versions récentes de Laravel proposent aussi l'approche inverse, `Model::automaticallyEagerLoadRelationships()` : au premier accès à une relation, celle-ci est chargée d'un coup pour tous les modèles de la collection. C'est un filet de sécurité pratique, mais un `with()` explicite reste plus lisible : le code montre quelles données la page utilise.

---

<a id="methods"></a>
## Carte des méthodes pour les grandes sélections

| Méthode | Requêtes | Simultanément en mémoire | Eager loading | Sûr si le filtre change |
|-------|----------|-----------------------|---------------|---------------------------------|
| `get()` | 1 | Tous les modèles | Oui | — |
| `chunk(N)` | Nombreuses (`LIMIT/OFFSET`) | N modèles | Oui | **Non** |
| `chunkById(N)` | Nombreuses (`WHERE id > ?`) | N modèles | Oui | Oui |
| `lazy(N)` | Nombreuses (`LIMIT/OFFSET`) | N modèles, fournis un par un | Oui | **Non** |
| `lazyById(N)` | Nombreuses (`WHERE id > ?`) | N modèles, fournis un par un | Oui | Oui |
| `cursor()` | 1 | 1 modèle + tout le résultat brut dans le buffer PDO | **Non** | Oui |

Règle par défaut pour les jobs et les commandes : **`lazyById()`** ou **`chunkById()`**. Ils bornent la mémoire, prennent en charge `with()` et ne sautent aucun enregistrement.

---

<a id="chunk"></a>
## chunk() et le piège de l'OFFSET

`chunk()` découpe la sélection en pages via `LIMIT` et `OFFSET` :

```php
Customer::where('bonus_recalculated', false)
    ->chunk(1000, function (Collection $customers) {
        foreach ($customers as $customer) {
            $customer->recalculateBonus(); // sets bonus_recalculated = true
        }
    });
```

Ce code traitera environ la moitié des clients. Premier lot : les lignes 1 à 1000 ; une fois traitées, elles ne correspondent plus à `bonus_recalculated = false`. La deuxième requête demande `OFFSET 1000`, mais la sélection s'est déjà décalée de mille lignes, et les clients 1001 à 2000 sont ignorés. Aucune erreur : la commande se termine avec succès.

`chunkById()` pagine sur la clé primaire : chaque requête suivante est un `WHERE id > :last_id ORDER BY id LIMIT 1000`. Le décalage de la sélection ne l'affecte pas :

```php
Customer::where('bonus_recalculated', false)
    ->chunkById(1000, function (Collection $customers) {
        foreach ($customers as $customer) {
            $customer->recalculateBonus();
        }
    });
```

L'`OFFSET` pose un second problème : la performance. Pour renvoyer `OFFSET 900000 LIMIT 1000`, la base lit malgré tout 900 000 lignes avant de les jeter. Les derniers lots s'exécutent plusieurs fois plus lentement que les premiers. La pagination par clé (keyset) s'appuie sur l'index de la clé primaire et coûte la même chose quelle que soit la profondeur.

> [!WARNING]
> **Regroupez les conditions contenant `orWhere`.** `chunkById()` et `lazyById()` ajoutent leur propre condition `id > ?`. Si votre requête contient un `orWhere`, sans parenthèses vous obtiendrez `a = 1 OR b = 2 AND id > 100`, et la pagination sera cassée. Enveloppez vos conditions dans une closure :
>
> ```php
> Customer::where(function ($query) {
>     $query->where('tier', 'gold')->orWhere('lifetime_cents', '>', 1_000_000);
> })->chunkById(1000, fn (Collection $customers) => /* ... */);
> ```

---

<a id="lazy"></a>
## lazy() et lazyById() : un flux plutôt que des lots

En interne, `lazy()` fait la même chose que `chunk()`, mais renvoie une `LazyCollection` : un flux de modèles que l'on parcourt avec un simple `foreach` et sur lequel on applique les méthodes de collection :

```php
Customer::where('newsletter', true)
    ->with('subscription')
    ->lazyById(1000)
    ->filter(fn (Customer $customer) => $customer->subscription?->isActive())
    ->each(fn (Customer $customer) => SendDigest::dispatch($customer->id));
```

Les méthodes de `LazyCollection` s'exécutent paresseusement : `filter` et `each` traitent les modèles au fil de leur arrivée, et seul le lot courant de 1000 éléments réside en mémoire. `with()` fonctionne : les relations sont chargées pour chaque lot par une requête distincte. Pour un parcours en ordre inverse, il existe `lazyByIdDesc()`.

Le code avec `lazyById()` se lit comme une boucle ordinaire ; dans les nouveaux jobs, il est donc plus pratique que `chunkById()` avec sa closure.

---

<a id="cursor"></a>
## cursor() et le buffering PDO

`cursor()` exécute **une seule** requête et crée les modèles un par un via un générateur :

```php
foreach (Order::where('status', 'paid')->cursor() as $order) {
    // only one Order model is hydrated at a time
}
```

Cela semble idéal, mais il y a deux limites.

**Pas d'eager loading.** Un seul modèle est en mémoire, donc `with()` ne s'applique pas. Accéder à `$order->customer` dans la boucle, c'est de nouveau du N+1, et sur toute la sélection.

**Tout le résultat brut reste en mémoire.** Par défaut, PDO récupère l'intégralité du résultat de la requête depuis le serveur et le conserve dans un buffer côté client :

* **pdo_mysql** fonctionne en mode requêtes bufferisées (`PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = true`) ;
* **pdo_pgsql** reçoit de libpq la totalité du résultat d'un coup.

Les modèles sont créés un par un, mais le tableau des lignes brutes d'un million d'enregistrements occupe la mémoire du processus et finit tôt ou tard par heurter le `memory_limit`. La documentation de Laravel recommande d'ailleurs explicitement `lazy()` plutôt que `cursor()` pour les très gros volumes.

Si vous avez réellement besoin d'un seul passage sans pagination, il existe deux façons honnêtes de streamer.

**MySQL : une requête non bufferisée sur une connexion dédiée.**

```php
// config/database.php — a dedicated connection for exports
'mysql_unbuffered' => array_merge(config('database.connections.mysql'), [
    'options' => [PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false],
]),
```

Tant que le résultat n'a pas été lu jusqu'au bout, la connexion est occupée : impossible d'y exécuter une autre requête. Une telle connexion sert donc uniquement à lire le flux, et les écritures passent par la connexion principale.

**PostgreSQL : un curseur côté serveur.**

```php
DB::transaction(function () {
    DB::statement(
        'DECLARE export_cursor NO SCROLL CURSOR FOR
         SELECT id, customer_id, total_cents, created_at FROM orders WHERE created_at >= ?',
        [now()->startOfYear()],
    );

    while ($rows = DB::select('FETCH 5000 FROM export_cursor')) {
        foreach ($rows as $row) {
            // stream $row somewhere
        }
    }
});
```

Le curseur vit sur le serveur, et PHP reçoit les données par portions de 5000 lignes. Un curseur sans `WITH HOLD` n'existe qu'à l'intérieur de la transaction. Mieux vaut ne pas la garder ouverte pendant des heures : une transaction longue empêche VACUUM de nettoyer les anciennes versions de lignes.

En pratique, `lazyById()` couvre 95 % des besoins ; le curseur côté serveur se justifie quand le tri ne porte pas sur la clé primaire, ou quand la requête est complexe et coûteuse à réexécuter pour chaque lot.

---

<a id="export"></a>
## Export CSV d'un million de lignes

Un export typique sans saturation mémoire repose sur trois techniques : ne sélectionner que les colonnes utiles, ne pas créer de modèles, et écrire le résultat dans un flux plutôt que dans une chaîne.

```php
// app/Http/Controllers/OrderExportController.php
use Symfony\Component\HttpFoundation\StreamedResponse;

public function __invoke(Request $request): StreamedResponse
{
    $year = (int) $request->validate(['year' => ['required', 'integer', 'min:2020']])['year'];

    return response()->streamDownload(function () use ($year) {
        $out = fopen('php://output', 'w');
        fputcsv($out, ['id', 'customer_id', 'total', 'created_at']);

        DB::table('orders')
            ->select(['id', 'customer_id', 'total_cents', 'created_at'])
            ->whereYear('created_at', $year)
            ->lazyById(5000)
            ->each(function (object $row) use ($out) {
                fputcsv($out, [
                    $row->id,
                    $row->customer_id,
                    number_format($row->total_cents / 100, 2, '.', ''),
                    $row->created_at,
                ]);
            });

        fclose($out);
    }, "orders-{$year}.csv", ['Content-Type' => 'text/csv']);
}
```

* `DB::table()` plutôt qu'un modèle : on obtient des objets `stdClass` légers, sans casts ni suivi des modifications. Pour une requête construite sur un modèle, `->toBase()` donne le même résultat.
* `select()` limite les colonnes : `SELECT *` ramène des champs `TEXT` dont le CSV n'a pas besoin.
* `streamDownload()` envoie les données au client au fur et à mesure de leur génération ; la réponse n'est pas assemblée en mémoire.

> [!NOTE]
> **Export de plus de 30 secondes : direction la file d'attente.** La requête HTTP butera sur les timeouts de PHP-FPM, de nginx ou du load balancer. Mieux vaut générer un gros export dans un job, vers un fichier sur disque ou dans S3, et envoyer un lien à l'utilisateur. Les détails sur les timeouts et la mémoire des workers se trouvent dans l'article sur les [files d'attente en production](laravel-queues-production#heavy-jobs).

Attention à `whereYear()` : une fonction appliquée à la colonne empêche d'utiliser l'index sur `created_at`. Sur les grosses tables, un intervalle est plus fiable : `whereBetween('created_at', [$from, $to])` (plus de détails dans l'article sur les [index](database-indexes-deep-dive)).

---

<a id="query-builder"></a>
## Quand passer au Query Builder ou au SQL brut

Eloquent est pratique quand on a besoin de modèles : logique métier, relations, événements. Pour manipuler des données « en masse », il est souvent superflu.

**Calculez les agrégats dans la base, pas en PHP.**

```php
// Bad: loads every order into PHP to sum one column
$total = Order::where('status', 'paid')->get()->sum('total_cents');

// Good: the database returns one number
$total = Order::where('status', 'paid')->sum('total_cents');

// Reports: grouping and window functions belong in SQL
$daily = DB::table('orders')
    ->selectRaw('date(created_at) as day, count(*) as orders, sum(total_cents) as revenue')
    ->where('created_at', '>=', now()->subDays(30))
    ->groupByRaw('date(created_at)')
    ->orderBy('day')
    ->get();
```

**Les modifications en masse : en une seule requête.**

```php
// One UPDATE instead of loading and saving 200 000 models
Order::where('status', 'pending')
    ->where('created_at', '<', now()->subDays(30))
    ->update(['status' => 'expired']);

// Batch upsert for imports: one statement per batch of rows
DB::table('product_prices')->upsert(
    $rows,                       // array of ['sku' => ..., 'price_cents' => ..., 'updated_at' => ...]
    uniqueBy: ['sku'],
    update: ['price_cents', 'updated_at'],
);
```

Un `update()` en masse via le builder Eloquent renseignera `updated_at`, mais **ne déclenchera ni les événements de modèle ni les observers**, n'appliquera pas les mutateurs et ne vérifiera pas `$fillable`. Si de la logique est accrochée à l'événement `updated` (invalidation du cache, audit), il faut l'exécuter explicitement.

**Les requêtes complexes : en SQL franc, avec des paramètres liés.** `INSERT ... SELECT`, CTE et fonctions de fenêtrage via `DB::select()` ou `selectRaw()` se lisent mieux qu'une chaîne de vingt méthodes du builder. Règle d'or : les valeurs passent uniquement par des placeholders :

```php
// Safe: values are bound, never concatenated into SQL
$rows = DB::select(
    'SELECT customer_id, sum(total_cents) AS spent
     FROM orders WHERE created_at >= ? GROUP BY customer_id HAVING sum(total_cents) > ?',
    [$from, 100_000],
);
```

Insérer une saisie utilisateur dans une chaîne SQL mène tout droit à l'injection SQL, même dans un rapport « interne » (plus de détails dans l'article sur les [attaques web](web-attacks-and-prevention)).

---

<a id="measure"></a>
## Comment mesurer

Ne devinez pas, mesurez sur un volume de données réaliste :

```php
$start = hrtime(true);
DB::enableQueryLog();

// ... code under test ...

logger()->info('export stats', [
    'queries' => count(DB::getQueryLog()),
    'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 1),
    'ms' => (int) ((hrtime(true) - $start) / 1_000_000),
]);
```

* `memory_get_peak_usage(true)` indique le pic et non la valeur courante ; or c'est précisément le pic qui heurte le `memory_limit`.
* N'activez le journal des requêtes que le temps de la mesure. Dans les commandes longues, il devient lui-même une fuite : chaque requête est conservée dans un tableau. Pour la même raison, Telescope et Debugbar, qui collectent les requêtes, font gonfler la mémoire des workers de longue durée.
* Pour les requêtes restées lentes après la correction du N+1, examinez le plan d'exécution avec `EXPLAIN ANALYZE` (en détail dans la [masterclass sur l'optimisation des requêtes](database-query-optimization)).

---

<a id="common-mistakes"></a>
## Erreurs fréquentes

**1. `chunk()` en modifiant une colonne de la condition.**
La moitié des enregistrements est ignorée sans bruit. Utilisez `chunkById()` ou `lazyById()`.

**2. `cursor()` comme remède à la saturation mémoire.**
Les modèles sont créés un par un, mais le résultat brut est bufferisé par PDO. Sur des millions de lignes, c'est le même OOM, juste un peu plus tard.

**3. Accéder aux relations à l'intérieur de `cursor()`.**
L'eager loading ne fonctionne pas : on obtient un N+1 sur toute la sélection.

**4. `get()->sum()`, `get()->count()`, `all()->filter()`.**
Les agrégats et filtres calculés en PHP chargent toute la table en mémoire. Calculez dans la base.

**5. Un `orWhere` non regroupé avec `chunkById()`.**
La condition `id > ?` se colle à votre `OR`, et la pagination est cassée.

**6. Un `update()` en masse là où les événements de modèle sont nécessaires.**
Observers et événements ne sont pas déclenchés : le cache n'est pas invalidé, l'audit n'est pas écrit.

**7. Le journal des requêtes activé dans une commande longue.**
Chaque requête s'accumule en mémoire. N'activez le journal que pour les mesures.

---

<a id="checklist"></a>
## Checklist

1. Les listes avec relations sont chargées via `with()` et `withCount()` ; `preventLazyLoading()` est activé hors production.
2. Les sélections de plus de quelques milliers de lignes sont traitées via `lazyById()` ou `chunkById()`, et non `get()`.
3. `chunk()` et `lazy()` ne sont pas utilisés si la boucle modifie des colonnes de la condition de la requête.
4. `cursor()` est employé en connaissance de cause : sans relations et en comprenant le buffering PDO.
5. Les exports ne sélectionnent que les colonnes utiles, se passent de modèles et écrivent dans un flux.
6. Les exports et imports longs s'exécutent dans une file d'attente, pas dans une requête HTTP.
7. Les agrégats et modifications en masse s'exécutent dans la base, en une seule requête.
8. Le SQL brut utilise exclusivement des paramètres liés.
9. La mémoire de pic et le nombre de requêtes ont été vérifiés sur un volume de données réaliste.

---

## En résumé

Eloquent n'est pas lent : il fait exactement ce qu'on lui demande, c'est-à-dire charger tout ce que renvoie la requête et créer un objet par ligne. Les gros volumes imposent une autre question : « quelle quantité de tout cela se retrouvera simultanément en mémoire ? » Pour le traitement, `lazyById()` ; pour les rapports, des agrégats dans la base ; pour l'export, un flux sans modèles ; pour les modifications en masse, une seule requête. Quant à `chunk()` avec une condition modifiée et à `cursor()` sur des millions de lignes, qu'ils restent dans la liste des erreurs fréquentes plutôt que dans votre production.

---

<a id="self-test-quiz"></a>
## Quiz d'auto-évaluation

### Question 1 : Une commande passe `processed = true` sur des enregistrements sélectionnés par la condition `processed = false`, via `chunk(500)`. Que se passe-t-il ?
- A) Tous les enregistrements sont traités, mais plus lentement qu'avec `chunkById()`.
- B) Environ la moitié des enregistrements est ignorée : la sélection se décale alors que l'`OFFSET` augmente.
- C) Laravel lève une exception de modification concurrente des données.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
Une fois le premier lot traité, ces lignes ne correspondent plus à la condition, et la page suivante avec `OFFSET 500` saute par-dessus des enregistrements pas encore traités. `chunkById()` pagine sur `id > dernier` et ne dépend pas du décalage de la sélection.
</details>

### Question 2 : Pourquoi `cursor()` peut-il épuiser la mémoire sur une sélection de cinq millions de lignes, alors que les modèles sont créés un par un ?
- A) Les générateurs PHP conservent toutes les valeurs déjà produites.
- B) Le driver PDO récupère par défaut tout le résultat de la requête et le conserve dans un buffer côté client.
- C) `cursor()` charge automatiquement toutes les relations des modèles.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
pdo_mysql en mode bufferisé comme pdo_pgsql récupèrent la totalité du résultat de la requête. Pour les très gros volumes, on utilise `lazyById()`, une connexion MySQL non bufferisée ou un curseur côté serveur PostgreSQL.
</details>

### Question 3 : Il faut passer 300 000 commandes expirées au statut `expired`. Une invalidation de cache est accrochée à l'événement `updated` du modèle `Order`. Quelle approche est correcte ?
- A) Une seule requête `Order::where(...)->update(['status' => 'expired'])` : les événements se déclencheront automatiquement.
- B) Un `update()` en masse suivi d'une invalidation explicite du cache, ou bien `lazyById()` avec enregistrement des modèles si la logique de l'événement est nécessaire pour chaque enregistrement.
- C) `Order::where(...)->get()->each->update(...)` : c'est la variante la plus rapide.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
Un `update()` en masse ne déclenche ni les événements de modèle ni les observers. Si leur logique est nécessaire, on l'exécute explicitement après la requête, ou l'on traite les modèles en flux via `lazyById()`. La variante C charge les 300 000 modèles en mémoire.
</details>