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

# Logchannels voor packages uitbreiden en opnieuw resolven

> Kies tussen custom en via en LogManager::extend() in Laravel 13, en test logfunctionaliteit met oog voor het vasthouden van channels, forgetChannel() en fallbacks.

Wanneer je vanuit een package eigen logoutput wilt aanbieden, scheid je **het mechanisme dat de logger aanmaakt** van **de channelconfiguratie die de gebruikende applicatie kiest**. Alleen een driver registreren bouwt bestaande channels niet opnieuw op, en ook na een configuratiewijziging kan een oude Handler nog in gebruik zijn.

Deze pagina is gecontroleerd aan de hand van de implementatie van Laravel Framework `v13.39.0`. Zie [Logging](https://kawax.biz/nl/logging.md) voor de basisbediening en [Packageontwikkeling](https://kawax.biz/nl/advanced/package-development.md) voor het voorbereiden van de provider.

## Kies het juiste uitbreidingspunt

Als het met een bestaande driver kan, hoef je geen eigen driver toe te voegen.

| Wat je wilt doen | Optie | Waar je uitbreidt |
| - | - | - |
| De formatter en dergelijke van een bestaand channel aanpassen | `tap` | De aangemaakte `Illuminate\Log\Logger` |
| Een Monolog-Handler via de configuratie kiezen | `driver: monolog` | Instellingen zoals `handler` en `handler_with` |
| De logger opbouwen met een factory van het package | `driver: custom` en `via` | Een klasse met `__invoke(array $config)` |
| Een packagespecifieke drivernaam publiceren | `LogManager::extend()` | Een closure die de applicatie en de configuratie-array ontvangt |

De klassen voor `via` en `tap` worden via de container geresolved, dus je kunt ook constructor-injectie gebruiken. `tap` is een aanpassing bij het aanmaken van het channel, geen hook die bij elke geschreven logregel wordt uitgevoerd.

> [!NOTE]
>
> De `audit` in `logging.channels.audit` is de **channelnaam**. De `driver` daarin is de **naam van de aanmaakmethode**. Met `Log::channel('audit')` kies je de channelnaam, zodat de aanroepende kant dezelfde naam kan blijven gebruiken, ook als je later de driver wijzigt.

## Publiceer eerst een factory met custom en via

In het fictieve package `Acme\Audit` bieden we een factory voor bestandsoutput aan. Plaats die in `src/Logging/CreateAuditLogger.php`, zodat hij via PSR-4 kan worden geladen.

```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')],
        );
    }
}
```

Voeg in de gebruikende applicatie het volgende element toe aan de bestaande `channels`-array in `config/logging.php`. Laat bestaande instellingen zoals `default` en `stack` staan.

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

`LogManager::createCustomDriver()` haalt de `via`-klasse uit de container en roept die aan met de configuratie-array van het channel als enige argument. Met alleen deze aanpak heb je geen provider nodig om een driver te registreren.

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

Log::channel('audit')->info('Bestelling bevestigd.', ['order_id' => 42]);
```

Deze factory geeft `level` expliciet door aan de StreamHandler. **LogManager past de `level` uit de configuratie niet automatisch toe op de logger die een `custom`-factory teruggeeft**. Ook als je eigen configuratie-opties toevoegt, verwerk je die in de factory.

> [!TIP]
>
> Als je alleen de bestemming van het logbestand wilt wijzigen, volstaat de `single`-driver. Dit voorbeeld is de minimale opzet om een eigen Handler of initialisatie van het package in te bouwen. Gebruik voor `via` een klassenaam die in de configuratiecache kan worden opgeslagen, en sla geen closures of logger-instanties op in de configuratie-array.

## Gebruik extend om een eigen drivernaam aan te bieden

Als je de gebruikende kant een instelling als `driver: acme-audit` wilt bieden, registreer je een driver die dezelfde factory gebruikt. Breid in `boot()` van `src/AuditServiceProvider.php` de `log`-service van Laravel uit.

```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);
            });
        });
    }
}
```

Laad de provider bijvoorbeeld via [automatische packagedetectie](https://kawax.biz/nl/advanced/package-discovery.md). Vervang aan de gebruikende kant `channels.audit` door de volgende configuratie in plaats van de vorige. `via` is dan niet meer nodig.

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

* De `LogServiceProvider` van Laravel registreert `log` als singleton. `callAfterResolving()` wacht als de service nog niet is geresolved, en past de registratie ook toe op de instantie als die al is geresolved. Zie [Hooks na het resolven van services](https://kawax.biz/nl/advanced/package-service-resolution.md) voor details.
* `extend()` registreert alleen de closure die de logger aanmaakt. Bij het registreren van deze callback wordt er nog geen channel aangemaakt of log geschreven.
* `callCustomCreator()` geeft `$app` en de configuratie-array van het channel door aan de closure. Dat zijn andere argumenten dan die aan de `via`-factory worden doorgegeven.
* Een driver met dezelfde naam opnieuw registreren overschrijft de aanmaakclosure. Gebruik geen ingebouwde namen zoals `single` of namen die met andere packages botsen.

> [!WARNING]
>
> In de gecontroleerde implementatie wordt een anonieme closure die aan `extend()` wordt doorgegeven opnieuw gebonden aan de scope van LogManager. `$this` in een niet-statische closure is dus niet de service provider. Gebruik zoals in het voorbeeld een statische closure met het argument `$app`, en maak de implementatie niet afhankelijk van `$this` van de provider.

Het package levert de driver, en de gebruikende kant kiest in welk channel of welke stack die wordt gebruikt. Ontwerp de provider zo dat hij `logging.default` of de volledige bestaande `logging.channels` niet overschrijft.

## Channels worden per naam vastgehouden

`LogManager::get()` slaat geresolvede channels op in `$channels`. Als je een channel met dezelfde naam opnieuw ophaalt, wordt de configuratie-array niet opnieuw gelezen, maar krijg je het vastgehouden object terug.

```mermaid theme={null}
flowchart TD
    A["Log::channel('audit')"] --> B{"Is audit al geresolved?"}
    B -->|Yes| C["Vastgehouden Logger teruggeven"]
    B -->|No| D["driver van channels.audit kiezen"]
    D --> E["Factory van via of extend"]
    E --> F["Inpakken in Illuminate Logger<br>tap en gedeelde context toepassen"]
    F --> G["Vasthouden onder de naam audit"]
```

Dit staat los van de configuratiecache die in een bestand wordt opgeslagen: het gaat om **het vasthouden van objecten binnen de huidige LogManager**. Alleen `config()` wijzigen of `extend()` opnieuw registreren vervangt reeds aangemaakte Handlers niet.

```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');
```

In dit voorbeeld is `$same` hetzelfde object als `$before`. Zodra `forgetChannel('audit')` het vasthouden in de Manager opheft, wordt bij de volgende aanroep `$after` aangemaakt op basis van de nieuwe configuratie.

### forgetChannel werkt bestaande referenties niet bij

`forgetChannel()` verwijdert alleen de entry in de Manager.

* Services die `$before` vasthouden, kunnen daarna de oude logger blijven gebruiken. Het is geen bewerking die bestaande referenties vervangt door het nieuwe object.
* Een stack die al is aangemaakt, bijvoorbeeld met `Log::channel('stack')`, heeft een eigen Monolog-instantie waarin de Handlers en Processors van de onderliggende channels zijn verzameld. Als je alleen een onderliggend channel met `forgetChannel()` vergeet, verandert de opbouw van de bestaande stack niet. Hef zo nodig ook het vasthouden voor de channelnaam van die stack op en haal hem opnieuw op.
* `forgetChannel()` roept zelf geen `close()` aan op de Handler. Het is geen API die het afsluiten van verbindingen of buffers garandeert.

Neem bij een normale deploy [het bijwerken van de configuratiecache](https://kawax.biz/nl/advanced/package-config-merging.md) en het herstarten van gebruikte langlopende workers op in je werkwijze. Vertrouw niet op een mechanisme dat per request de configuratie wijzigt en opnieuw resolvet, en houd ook rekening met [de levenscyclus van services](https://kawax.biz/nl/advanced/package-service-lifetimes.md).

### Voor gebruik in een stack zijn ook Monolog-compatibele methoden nodig

Een eigen driver kan een implementatie van `Psr\Log\LoggerInterface` teruggeven, maar `createStackDriver()` van Laravel roept `getHandlers()` en `getProcessors()` aan op de onderliggende channels. Die maken geen deel uit van de PSR-interface.

Een willekeurige PSR-logger teruggeven betekent dus niet automatisch dat hij ook in een stack werkt. Packages die ook gebruik in een stack ondersteunen, geven zoals in het voorbeeld een Monolog-instantie terug, of documenteren en testen expliciet het ondersteunde gebruik.

## Verifieer niet alleen met mocks, maar ook de echte channelresolutie

Met alleen `Log::shouldReceive('channel')` kun je de registratie van de provider, de keuze van de factory en het toepassen van de configuratie niet controleren. Het volgende voorbeeld laat in [de testomgeving voor packages](https://kawax.biz/nl/advanced/package-testing.md) de echte Manager en factory doorlopen.

Gebruik `php://memory` als bestemming voor de testoutput. Hier verifiëren we de opbouw van het channel en de instellingen van de Handler, zonder daadwerkelijk logs te schrijven.

```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()` vangt exceptions op die tijdens het aanmaken van een channel optreden en kan dan de emergency logger teruggeven. Controleren dat "er geen exception optrad" of dat "het een LoggerInterface was" bewijst dus niet dat het bedoelde channel is opgebouwd. De test hierboven controleert ook de naam, de Handler en het logniveau van de Monolog-instantie, zodat een fallback niet ten onrechte als succes wordt gezien. Schrijffouten na het aanmaken worden niet noodzakelijk via hetzelfde mechanisme opgevangen.

## Extra verificaties voor langdurig onderhoud

| Wijziging of gebruik | Wat je controleert |
| - | - |
| `tap` toevoegen | De verwachte aanpassing wordt toegepast op de logger die de echte factory teruggeeft |
| Gebruik in een stack | De Handlers en Processors van de onderliggende channels worden ingebouwd, en ook na opnieuw resolven klopt de opbouw |
| Bestemming of formaat wijzigen | Controleer de daadwerkelijk geschreven inhoud, rechten, rotatie en dergelijke in een aparte verificatieomgeving |
| Moment van registratie wijzigen | De driver wordt geregistreerd en kan channels aanmaken, ook als `log` al is geresolved |
| Laravel-updates | Blijf de argumenten en scope van de callback, het opheffen van vasthouden en de fallback naar de emergency logger controleren |

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: Logging](https://github.com/laravel/docs/blob/13.x/logging.md)
* [Framework v13.39.0: aanmaken, uitbreiden en vergeten van channels in LogManager](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogManager.php)
* [Framework v13.39.0: inpakken en doorsturen van methoden in Logger](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/Logger.php)
* [Framework v13.39.0: singletonregistratie in 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)
* [Officiële Laravel-tests: LogManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Integration/Log/LogManagerTest.php)


## Related topics

- [Geavanceerde onderwerpen](https://kawax.biz/nl/advanced/index.md)
- [Storage-drivers registreren in een package en opnieuw resolven](https://kawax.biz/nl/advanced/package-filesystem-drivers.md)
- [Logging](https://kawax.biz/nl/logging.md)
- [Hooks na het resolven van packageservices](https://kawax.biz/nl/advanced/package-service-resolution.md)
- [Containertags in packages en het bundelen van plugins](https://kawax.biz/nl/advanced/package-container-tags.md)


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