> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Étendre et résoudre à nouveau les canaux de log d'un package

> Choisissez entre custom/via et LogManager::extend() de Laravel 13, et testez la journalisation en tenant compte de la conservation des canaux, de forgetChannel() et du repli.

Lorsqu'un package doit fournir sa propre sortie de logs, séparez **le mécanisme qui construit le logger** de **la configuration de canal choisie par l'application consommatrice**. Enregistrer un driver ne reconstruit pas les canaux existants, et d'anciens Handlers peuvent continuer à être utilisés même après une modification de la configuration.

Cette page a été vérifiée sur l'implémentation de Laravel Framework `v13.39.0`. Pour les opérations de base, consultez [Journalisation](https://kawax.biz/fr/logging.md), et pour préparer le provider, consultez [Développement de packages](https://kawax.biz/fr/advanced/package-development.md).

## Choisir le point d'extension nécessaire

Si un driver existant suffit, il n'est pas nécessaire d'en ajouter un nouveau.

| Objectif | Option | Ce qui est étendu |
| - | - | - |
| Ajuster le formateur d'un canal existant, etc. | `tap` | Le `Illuminate\Log\Logger` généré |
| Choisir un Handler Monolog par configuration | `driver: monolog` | Les options `handler`, `handler_with`, etc. |
| Construire le logger avec une factory du package | `driver: custom` et `via` | Une classe possédant `__invoke(array $config)` |
| Exposer un nom de driver propre au package | `LogManager::extend()` | Une closure recevant l'application et le tableau de configuration |

Les classes `via` et `tap` sont résolues par le conteneur, l'injection par constructeur est donc disponible. `tap` est un ajustement effectué lors de la création du canal, et non un hook exécuté à chaque écriture d'une entrée de log.

> [!NOTE]
>
> Dans `logging.channels.audit`, `audit` est le **nom du canal**. Le `driver` qu'il contient est le **nom de la méthode de création**. `Log::channel('audit')` sélectionne un nom de canal : vous pouvez donc changer de driver plus tard tout en conservant le nom utilisé par l'appelant.

## Exposer d'abord une factory avec custom et via

Dans un package fictif `Acme\Audit`, nous fournissons une factory pour la sortie vers un fichier. Placez-la dans `src/Logging/CreateAuditLogger.php`, chargeable via PSR-4.

```php theme={null}
<?php

namespace Acme\Audit\Logging;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;

final class CreateAuditLogger
{
    public function __invoke(array $config): Logger
    {
        return new Logger(
            $config['name'] ?? 'acme-audit',
            [new StreamHandler($config['path'], $config['level'] ?? 'info')],
        );
    }
}
```

Dans l'application consommatrice, ajoutez l'élément suivant au tableau `channels` existant de `config/logging.php`. Conservez les réglages existants de `default` et de `stack`.

```php theme={null}
'audit' => [
    'driver' => 'custom',
    'via' => \Acme\Audit\Logging\CreateAuditLogger::class,
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

`LogManager::createCustomDriver()` récupère la classe `via` depuis le conteneur et l'appelle avec le tableau de configuration du canal comme unique argument. Avec cette seule approche, aucun provider d'enregistrement de driver n'est nécessaire.

```php theme={null}
use Illuminate\Support\Facades\Log;

Log::channel('audit')->info('Commande confirmée.', ['order_id' => 42]);
```

Cette factory transmet explicitement `level` au StreamHandler. **LogManager n'applique pas uniformément le `level` de la configuration au logger renvoyé par une factory `custom`**. Si vous ajoutez vos propres options de configuration, appliquez-les également dans la factory.

> [!TIP]
>
> Pour simplement changer la destination du fichier, le driver `single` suffit. Cet exemple est la configuration minimale permettant d'intégrer un Handler ou une initialisation propres au package. Utilisez pour `via` un nom de classe pouvant être stocké dans le cache de configuration, et n'enregistrez ni closure ni instance de logger dans le tableau de configuration.

## Utiliser extend pour fournir un nom de driver propre

Si vous souhaitez exposer une configuration `driver: acme-audit` à l'application consommatrice, enregistrez un driver utilisant la même factory. Étendez le service `log` de Laravel dans la méthode `boot()` de `src/AuditServiceProvider.php`.

```php theme={null}
<?php

namespace Acme\Audit;

use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\LogManager;
use Illuminate\Support\ServiceProvider;
use Monolog\Logger as Monolog;

class AuditServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('log', static function (LogManager $manager): void {
            $manager->extend('acme-audit', static function (Application $app, array $config): Monolog {
                return $app->make(CreateAuditLogger::class)($config);
            });
        });
    }
}
```

Le provider est chargé, par exemple, par la [découverte automatique de packages](https://kawax.biz/fr/advanced/package-discovery.md). Côté application, remplacez la configuration `channels.audit` précédente par la suivante. `via` n'est plus nécessaire.

```php theme={null}
'audit' => [
    'driver' => 'acme-audit',
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

* Le `LogServiceProvider` de Laravel enregistre `log` en singleton. `callAfterResolving()` attend si le service n'est pas encore résolu, et applique aussi l'enregistrement à l'instance s'il l'est déjà. Pour plus de détails, consultez [Hooks après résolution de service](https://kawax.biz/fr/advanced/package-service-resolution.md).
* `extend()` ne fait qu'enregistrer la closure de création. Ni la création du canal ni l'écriture de logs n'ont lieu au moment de l'enregistrement de ce callback.
* `callCustomCreator()` transmet `$app` et le tableau de configuration du canal à la closure. Ces arguments diffèrent de ceux transmis à une factory `via`.
* Enregistrer à nouveau le même nom de driver écrase la closure de création. N'utilisez pas de noms intégrés comme `single`, ni de noms susceptibles d'entrer en conflit avec d'autres packages.

> [!WARNING]
>
> Dans l'implémentation vérifiée, une closure anonyme passée à `extend()` est re-liée à la portée de LogManager. Dans une closure non statique, `$this` n'est pas le service provider. Comme dans l'exemple, utilisez une closure statique avec l'argument `$app`, et ne dépendez pas du `$this` du provider.

Le package fournit le driver, et c'est l'application consommatrice qui choisit dans quel canal ou quelle stack l'utiliser. Concevez le provider de manière à ne pas réécrire `logging.default` ni l'ensemble des `logging.channels` existants.

## Les canaux sont conservés par nom

`LogManager::get()` stocke les canaux résolus dans `$channels`. Lorsqu'un canal du même nom est demandé, l'objet conservé est renvoyé sans relire le tableau de configuration.

```mermaid theme={null}
flowchart TD
    A["Log::channel('audit')"] --> B{"audit déjà résolu ?"}
    B -->|Yes| C["Renvoyer le Logger conservé"]
    B -->|No| D["Sélectionner le driver de channels.audit"]
    D --> E["Factory via ou extend"]
    E --> F["Envelopper dans un Illuminate Logger<br>appliquer tap et le contexte partagé"]
    F --> G["Conserver sous le nom audit"]
```

Il s'agit d'une **conservation d'objets au sein du LogManager courant**, distincte du cache de configuration stocké dans un fichier. Modifier `config()` ou ré-enregistrer `extend()` ne suffit pas à remplacer les Handlers déjà créés.

```php theme={null}
use Illuminate\Support\Facades\Log;

$before = Log::channel('audit');

config(['logging.channels.audit.level' => 'error']);

$same = Log::channel('audit');

Log::forgetChannel('audit');

$after = Log::channel('audit');
```

Dans cet exemple, `$same` est le même objet que `$before`. Une fois que `forgetChannel('audit')` a libéré la conservation dans le Manager, la récupération suivante génère `$after` à partir de la nouvelle configuration.

### forgetChannel ne met pas à jour les références existantes

`forgetChannel()` se contente de supprimer l'entrée dans le Manager.

* Un service qui conserve `$before` peut continuer à utiliser l'ancien logger. Ce n'est pas une opération qui remplace les références existantes par le nouvel objet.
* Une stack déjà générée, par exemple via `Log::channel('stack')`, possède son propre Monolog indépendant qui regroupe les Handlers et Processors des canaux enfants. Appeler `forgetChannel()` uniquement sur un enfant ne modifie pas la composition de la stack existante. Si nécessaire, libérez également la conservation du nom de canal de cette stack, puis récupérez-la à nouveau.
* `forgetChannel()` n'appelle pas lui-même `close()` sur les Handlers. Cette API ne garantit pas la fermeture des connexions ni le vidage des buffers.

Lors d'un déploiement classique, intégrez à vos opérations la [mise à jour du cache de configuration](https://kawax.biz/fr/advanced/package-config-merging.md) et le redémarrage des workers longue durée utilisés. Ne vous reposez pas sur un mécanisme qui réécrit la configuration et résout à nouveau les canaux à chaque requête, et tenez aussi compte du [cycle de vie des services](https://kawax.biz/fr/advanced/package-service-lifetimes.md).

### L'intégration dans une stack nécessite aussi des opérations compatibles Monolog

Un driver personnalisé peut renvoyer une implémentation de `Psr\Log\LoggerInterface`, mais `createStackDriver()` de Laravel appelle `getHandlers()` et `getProcessors()` sur les canaux enfants. Ces méthodes ne font pas partie de l'interface PSR.

Renvoyer n'importe quel logger PSR ne garantit pas le fonctionnement dans une stack. Un package qui prend aussi en charge l'intégration dans une stack doit donc, comme dans l'exemple, renvoyer un Monolog, ou bien documenter explicitement le périmètre d'utilisation et le tester.

## Vérifier la résolution réelle des canaux, pas seulement des mocks

`Log::shouldReceive('channel')` seul ne permet pas de vérifier l'enregistrement du provider, la sélection de la factory ni l'application de la configuration. L'exemple suivant fait passer les appels par le véritable Manager et la véritable factory dans [l'environnement de test du package](https://kawax.biz/fr/advanced/package-testing.md).

Utilisez `php://memory` comme destination de test. Ici, on vérifie la construction du canal et la configuration du Handler, sans écrire de log.

```php theme={null}
<?php

namespace Acme\Audit\Tests;

use Acme\Audit\AuditServiceProvider;
use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger as Monolog;
use Orchestra\Testbench\TestCase;

class AuditLoggingTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [AuditServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('logging.channels.audit', [
            'driver' => 'acme-audit',
            'path' => 'php://memory',
            'level' => 'warning',
        ]);

        $app['config']->set('logging.channels.emergency.path', 'php://memory');
    }

    public function test_the_package_driver_creates_the_configured_logger(): void
    {
        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    public function test_configuration_changes_require_a_new_channel(): void
    {
        $before = Log::channel('audit');

        config(['logging.channels.audit.level' => 'error']);

        $this->assertSame($before, Log::channel('audit'));
        $this->assertAuditChannel($before, Level::Warning);

        Log::forgetChannel('audit');

        $after = Log::channel('audit');

        $this->assertNotSame($before, $after);
        $this->assertAuditChannel($after, Level::Error);
        $this->assertAuditChannel($before, Level::Warning);
    }

    public function test_the_same_factory_can_be_used_with_via(): void
    {
        config([
            'logging.channels.audit.driver' => 'custom',
            'logging.channels.audit.via' => CreateAuditLogger::class,
        ]);

        Log::forgetChannel('audit');

        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    private function assertAuditChannel(Logger $channel, Level $level): void
    {
        $logger = $channel->getLogger();

        $this->assertInstanceOf(Monolog::class, $logger);
        $this->assertSame('acme-audit', $logger->getName());
        $this->assertCount(1, $logger->getHandlers());

        $handler = $logger->getHandlers()[0];

        $this->assertInstanceOf(StreamHandler::class, $handler);
        $this->assertSame($level, $handler->getLevel());
    }
}
```

> [!WARNING]
>
> `LogManager::get()` peut intercepter une exception survenue pendant la création d'un canal et renvoyer l'emergency logger. Constater qu'« aucune exception n'a été levée » ou que « c'était un LoggerInterface » ne prouve pas que le canal visé a bien été construit. Le test ci-dessus vérifie aussi le nom du Monolog, le Handler et le niveau de log, afin de ne pas confondre un repli avec un succès. Les erreurs d'écriture survenant après la création ne sont pas forcément interceptées par le même mécanisme.

## Vérifications à ajouter pour la maintenance à long terme

| Modification ou usage | Ce qu'il faut vérifier |
| - | - |
| Ajout de `tap` | L'ajustement attendu est appliqué au logger renvoyé par la véritable factory |
| Intégration dans une stack | Les Handlers et Processors des canaux enfants sont intégrés, et la composition reste correcte après une nouvelle résolution |
| Changement de destination ou de format | Vérifier le contenu réellement écrit, les permissions, la rotation, etc. dans un environnement de vérification séparé |
| Changement du moment d'enregistrement | Le driver est enregistré et le canal peut être créé même si `log` est déjà résolu |
| Mise à jour de Laravel | Continuer à vérifier les arguments et la portée du callback, la libération de la conservation et le repli vers l'emergency logger |

## Sources primaires consultées

* [Documentation officielle de Laravel : Logging](https://github.com/laravel/docs/blob/13.x/logging.md)
* [Framework v13.39.0 : création, extension et libération dans LogManager](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogManager.php)
* [Framework v13.39.0 : enveloppement et transfert des méthodes dans Logger](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/Logger.php)
* [Framework v13.39.0 : enregistrement singleton dans LogServiceProvider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogServiceProvider.php)
* [Framework v13.39.0 : ServiceProvider::callAfterResolving](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/ServiceProvider.php)
* [Framework v13.39.0 : RebindsCallbacksToSelf](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/RebindsCallbacksToSelf.php)
* [Tests officiels de Laravel : LogManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Integration/Log/LogManagerTest.php)


## Related topics

- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Enregistrer des drivers Storage dans un package et les résoudre à nouveau](https://kawax.biz/fr/advanced/package-filesystem-drivers.md)
- [Logging](https://kawax.biz/fr/logging.md)
- [Canaux de notification personnalisés dans un package et vérification de l'envoi](https://kawax.biz/fr/advanced/package-notification-channels.md)
- [Enregistrement et portée des middlewares d'un package](https://kawax.biz/fr/advanced/package-middleware.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.