Vai al contenuto principale

Cos’è un pacchetto

In Laravel un pacchetto è un pacchetto Composer che aggiunge funzionalità all’applicazione. Ne esistono principalmente due tipi.
  • Pacchetto standalone — libreria PHP generica non dipendente da Laravel (es. Carbon, Pest)
  • Pacchetto Laravel — pacchetto integrato con Laravel che offre rotte, controller, view, configurazione, ecc.
Questa guida tratta il secondo caso: lo sviluppo di pacchetti specifici per Laravel. Serve una conoscenza approfondita della struttura interna di Laravel — service provider, facade, pubblicazione della configurazione.
Per scrivere i test di un pacchetto usi Orchestra Testbench. Ti permette di scrivere i test del pacchetto come faresti in una normale applicazione Laravel.

Auto-discovery dei pacchetti

Quando installi un pacchetto, Laravel legge la sezione extra.laravel del composer.json e registra automaticamente service provider e facade.
"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
Con questa configurazione, l’utente non deve modificare manualmente bootstrap/providers.php: il pacchetto viene caricato automaticamente.

Disabilitare l’auto-discovery

Se l’utente vuole disabilitare l’auto-discovery per un pacchetto specifico, imposta il composer.json dell’applicazione.
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}

Ruolo del service provider

Il service provider è il punto di ingresso del pacchetto. Concentra qui la registrazione delle risorse — view, config, migrazioni, rotte — in Laravel. Estende Illuminate\Support\ServiceProvider e ha due metodi: register e boot.
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    /**
     * Registra i servizi del pacchetto
     */
    public function register(): void
    {
        // qui esegui il binding nel service container
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );

        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * Bootstrap dei servizi del pacchetto
     */
    public function boot(): void
    {
        // qui registri le risorse
        $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
        $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

        $this->publishesMigrations([
            __DIR__.'/../database/migrations' => database_path('migrations'),
        ]);

        $this->publishes([
            __DIR__.'/../config/courier.php' => config_path('courier.php'),
        ], 'courier-config');

        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
        ], 'courier-views');
    }
}
In register non registrare listener di eventi, rotte o view. Rischi di usare per errore servizi di un altro service provider non ancora caricato. Le operazioni diverse dai binding vanno sempre in boot.

Pubblicare i file di configurazione

publishes() — pubblicare i file

Chiamando publishes() in boot, l’utente può copiare i file di configurazione nella propria app con vendor:publish.
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
Dopo la pubblicazione, i valori si leggono con la normale API config.
$value = config('courier.option');

mergeConfigFrom() — fondere con i valori di default

Con mergeConfigFrom() in register, i default del pacchetto si applicano anche se l’utente non ha pubblicato il file.
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
mergeConfigFrom() non fonde a livelli profondi degli array annidati. In configurazioni con array multidimensionali, se l’utente definisce solo una parte, il resto delle opzioni potrebbe non essere fuso.

Separare i gruppi di pubblicazione con i tag

Passando un tag come secondo argomento di publishes(), l’utente pubblica solo le risorse desiderate.
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ], 'courier-config');

    $this->publishesMigrations([
        __DIR__.'/../database/migrations/' => database_path('migrations'),
    ], 'courier-migrations');
}
# pubblica solo i file di configurazione
php artisan vendor:publish --tag=courier-config

# pubblica tutte le risorse offerte dal provider
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"

Registrare rotte

Carica i file delle rotte con loadRoutesFrom(). Se la cache delle rotte dell’app è attiva, viene automaticamente saltato.
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
Nel file delle rotte indichi i controller del pacchetto.
// routes/web.php
use Acme\Courier\Http\Controllers\TrackingController;
use Illuminate\Support\Facades\Route;

Route::prefix('courier')->group(function () {
    Route::get('/track/{id}', [TrackingController::class, 'show'])
        ->name('courier.track');
});

Pubblicare le migrazioni

Con publishesMigrations() puoi pubblicare i file di migrazione. Al momento della pubblicazione Laravel aggiorna automaticamente il timestamp.
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}

Pubblicare le view

loadViewsFrom() — registrare le view

Con loadViewsFrom() registri la directory delle view. Usa il namespace del secondo argomento per riferirti alle view come package::view.
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}
Dopo la registrazione, riferisci le view con il namespace del pacchetto.
Route::get('/dashboard', function () {
    return view('courier::dashboard');
});
Laravel cerca le view in due posti: prima in resources/views/vendor/courier dell’app, poi nella directory view del pacchetto. Questo permette all’utente di personalizzarle.

Pubblicare le view

public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');

    $this->publishes([
        __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
    ], 'courier-views');
}

Registrare componenti Blade

Se includi componenti nel pacchetto li registri in boot.
use Illuminate\Support\Facades\Blade;
use Acme\Courier\View\Components\AlertComponent;

public function boot(): void
{
    Blade::component('courier-alert', AlertComponent::class);
}
Puoi anche registrarli in blocco con un namespace di componenti.
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');
}
{{-- registrazione singola --}}
<x-courier-alert />

{{-- registrazione con namespace --}}
<x-courier::alert />

Pubblicare i file di traduzione

Registri i file di traduzione con loadTranslationsFrom(). Le traduzioni si referenziano nella forma package::file.key.
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
// uso della traduzione
echo trans('courier::messages.welcome');
Per file di traduzione JSON usi loadJsonTranslationsFrom().
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}

Registrare comandi

I comandi Artisan del pacchetto si registrano con il metodo commands(). Di solito solo in ambiente console.
use Acme\Courier\Console\Commands\InstallCommand;
use Acme\Courier\Console\Commands\SyncCommand;

public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            InstallCommand::class,
            SyncCommand::class,
        ]);
    }
}

Integrazione con il comando optimize

Se il pacchetto ha una sua cache, con il metodo optimizes() puoi integrarti con php artisan optimize e php artisan optimize:clear.
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'courier:cache',
            clear: 'courier:clear-cache',
        );
    }
}

Aggiungere informazioni al comando about

Per aggiungere informazioni del pacchetto all’output di php artisan about usa AboutCommand::add().
use Illuminate\Foundation\Console\AboutCommand;

public function boot(): void
{
    AboutCommand::add('Courier Package', fn () => ['Version' => '1.0.0']);
}

Creare un facade

Un facade permette di richiamare i binding del service container come metodi statici.
1

Creare la classe di servizio

<?php

namespace Acme\Courier;

class CourierManager
{
    public function __construct(
        protected array $config,
    ) {}

    public function send(string $to, string $message): bool
    {
        // logica di invio messaggio
        return true;
    }

    public function track(string $id): array
    {
        // logica di recupero delle informazioni di tracking
        return ['status' => 'delivered'];
    }
}
2

Creare la classe facade

Estendi Illuminate\Support\Facades\Facade e restituisci in getFacadeAccessor() la chiave di binding del container.
<?php

namespace Acme\Courier\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @method static bool send(string $to, string $message)
 * @method static array track(string $id)
 *
 * @see \Acme\Courier\CourierManager
 */
class Courier extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return \Acme\Courier\CourierManager::class;
    }
}
3

Fai il binding nel service provider

public function register(): void
{
    $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) {
        return new \Acme\Courier\CourierManager($app['config']['courier']);
    });
}
4

Registrare in composer.json

"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
Aggiungendo le annotazioni PHPDoc @method ai metodi del facade abiliti l’autocompletamento dell’IDE.
// invocazione del servizio tramite facade
use Acme\Courier\Facades\Courier;

Courier::send('[email protected]', 'Il tuo pacco è arrivato');
$status = Courier::track('ABC-123');

DeferrableProvider — implementare il caricamento differito

I provider che eseguono solo binding nel container possono realizzare il caricamento differito implementando l’interfaccia DeferrableProvider. Il provider non viene caricato finché il servizio non è effettivamente necessario: le prestazioni dell’app migliorano.
<?php

namespace Acme\Courier;

use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * Restituisce la lista dei servizi forniti da questo provider
     *
     * @return array<int, string>
     */
    public function provides(): array
    {
        return [CourierManager::class];
    }
}
Laravel compila e memorizza la lista dei servizi forniti dai deferred provider. Il provider viene caricato solo quando uno dei servizi elencati in provides() viene risolto.
Non usare DeferrableProvider in provider che devono registrare risorse (view, rotte, listener di eventi, ecc.). Con il caricamento differito, quelle risorse resterebbero non registrate.

Test del pacchetto

Per testare il pacchetto in isolamento usa Orchestra Testbench. Ti permette di scrivere i test come se fossi in un’app Laravel.
composer require --dev orchestra/testbench
Nel test case fai override di getPackageProviders() per registrare il service provider del pacchetto.
<?php

namespace Acme\Courier\Tests;

use Acme\Courier\CourierServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;

class TestCase extends BaseTestCase
{
    /**
     * Registra i service provider del pacchetto
     */
    protected function getPackageProviders($app): array
    {
        return [
            CourierServiceProvider::class,
        ];
    }

    /**
     * Registra gli alias dei facade del pacchetto
     */
    protected function getPackageAliases($app): array
    {
        return [
            'Courier' => \Acme\Courier\Facades\Courier::class,
        ];
    }

    /**
     * Configurazione d'ambiente per i test
     */
    protected function defineEnvironment($app): void
    {
        $app['config']->set('courier.api_key', 'test-key');
    }
}
<?php

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Facades\Courier;
use Acme\Courier\Tests\TestCase;

class CourierTest extends TestCase
{
    public function test_can_send_message(): void
    {
        $result = Courier::send('[email protected]', 'messaggio di test');

        $this->assertTrue($result);
    }
}

Pubblicazione su Composer

Best practice per pubblicare il pacchetto su Packagist. Impostazioni base di composer.json
{
    "name": "acme/courier",
    "description": "A Laravel courier package",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.2",
        "illuminate/support": "^11.0||^12.0||^13.0"
    },
    "require-dev": {
        "orchestra/testbench": "^9.0||^10.0",
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Courier\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Acme\\Courier\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Courier\\CourierServiceProvider"
            ],
            "aliases": {
                "Courier": "Acme\\Courier\\Facades\\Courier"
            }
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}
Dipendere da illuminate/support invece che da tutto illuminate/framework ti fa dipendere solo dai componenti necessari. Mantieni piccolo l’albero delle dipendenze del pacchetto.
Esempio di struttura delle directory
acme/courier/
├── config/
│   └── courier.php
├── database/
│   └── migrations/
│       └── 2024_01_01_000000_create_courier_logs_table.php
├── lang/
│   └── it/
│       └── messages.php
├── resources/
│   └── views/
│       └── dashboard.blade.php
├── routes/
│   └── web.php
├── src/
│   ├── Console/
│   │   └── Commands/
│   │       └── InstallCommand.php
│   ├── Facades/
│   │   └── Courier.php
│   ├── Http/
│   │   └── Controllers/
│   │       └── TrackingController.php
│   ├── CourierManager.php
│   └── CourierServiceProvider.php
├── tests/
│   ├── Feature/
│   └── TestCase.php
├── composer.json
└── README.md

Pagine correlate

Service provider

Rivedi i metodi register e boot dei service provider e i dettagli dei deferred provider.

Gestione della compatibilità tra versioni

Strategie per adattarsi agli upgrade major di Laravel e PHP e configurazione della matrix di test in GitHub Actions.
Ultima modifica il 13 luglio 2026