Crea un Package per Laravel


crea-un-package-per-laravel

Creare un package per Laravel è un ottimo modo per estenderne le funzionalità, e non è così difficile come potresti pensare.
In questo tutorial ti racconto come realizzarne uno molto semplice e, volendo, come condividerlo su Github e Packagist.

Il progetto in sintesi

L'obiettivo è quello di realizzare una libreria per la generazione in tempo reale di una sitemap.
Si chiamerà Sitemapper e sarà una libreria per siti di poche pagine, come questo, e, alla richiesta dell'url /sitemap.xml, si occuperà di generare l'albero xml secondo le indicazioni fornite da codice.

La preparazione dell'ambiente di lavoro

Per lo sviluppo del package ti suggerisco di dedicare un'installazione di Laravel a questo scopo.
Il vantaggio è che puoi raccogliere, testare e gestire tutti i vari packages in un unico posto.

Crea quindi una cartella packages nella root dell'installazione di Laravel, allo stesso livello delle cartelle app, bootstrap, config, etc.

Anatomia di un package

Per creare un package devi seguire alcune linee guida:

  • il package è identificato da un'accoppiata di nomi, solitamente composta da nome_autore e nome_pacchetto e la si usa per nominare le cartelle: crea, perciò, dentro packages, la cartella tuonome che conterrà la cartella sitemapper ( entrambi i nomi minuscoli ).
  • all'interno di sitemapper crea la cartella src, in cui inserirai i file che compongono il package;
  • sempre dentro sitemapper, crea anche questi due files:
    • readme.md: con le indicazioni per l'installazione e, soprattutto, le informazioni sulla funzione del package; puoi seguire questo esempio per farti un'idea di come scrivere una buona readme;
    • composer.json: serve ad indicare autore, tipo, nome, homepage del package, eventuali dipendenze e le informazioni per l'autoloading. Per crearlo, sfrutta la procedura di composer, come indicato nel prossimo paragrafo. Se hai come obiettivo quello di caricare il package su Packagist, segui le indicazioni fornite direttamente sul sito.
  • infine, per poter poi distribuire il package su packagist o su github, devi creare il file in cui specifichi la licenza con cui rilasci il codice:
    • LICENSE: contiene il testo della licenza. La licenza MIT è quella meno restrittiva. Il sito choosealicense.com ti può aiutare a scegliere quella più opportuna.

La gerarchia dei file di Sitemapper è la seguente:
gerarchia delle cartelle di Sitemapper

Composer e GIT

Come accennato poco fa, apri il terminale e posizionati nella cartella tuonome/sitemapper e crea il file composer.json tramite il comando composer init.
Questo comando ti guida nella creazione chiedendoti le informazioni per le varie voci del file.
Qui di seguito trovi quello creato per Sitemapper:

{
    "name": "tuonome/sitemapper",
    "type": "library",
    "keywords": ["laravel", "sitemap","generator"],
    "homepage": "", 
    "description": "Ridiculously small package for manual sitemap generation in small websites",
    "authors": [
        {
            "name": "Nome Cognome",
            "email": "info@example.com",
            "role": "Developer"
        }
    ],
    "license": "MIT",
    "require": {
        "php": ">=5.5.9"
    },
    "autoload": {
        "classmap": [
            "src"
        ],
        "psr-4": {
            "Tuonome\\Sitemapper\\": "src/"
        }
    },
    "minimum-stability": "",
    "require": {}
}

L'altra operazione che ti rimane da fare, a questo punto, è creare un nuovo repository GIT dentro la cartella tuonome/sitemapper eseguendo git init e facendo poi il first commit di rito.

L'autoload

Per far sì che il package venga caricato da Laravel, devi indicare il percorso nel composer.json nella root del sito.
Dentro alla voce psr-4, sotto la riga già presente, inserisci: "Tuonome\\Sitemapper\\": "packages/tuonome/sitemapper/src".
Indicando che il namespace Tuonome\Sitemapper si trova in packages/tuonome/sitemapper/src, l'autoloader sa dove andare a cercare i file.

Il service provider

Se vuoi lasciare le cose ad un livello base, non ti serve implementare nessun Service Provider.
Ma se hai un file di configurazione, una route, un controller o altri elementi che devono essere utilizzati da Laravel, diventa indispensabile.
Poniamo caso che vuoi aggiungere un file di configurazione per modificare una preferenza di Sitemapper: crea, allora, il SitemapperServiceProvider, digitando
artisan make:provider SitemapperServiceProvider
Questo comando si occupa di creare il file all'interno della cartella app/providers: prendilo e spostalo dentro la cartella src del package, aggiornando, ovviamente, il namespace.

Il service provider estende la classe astratta ServiceProvider e richiede la presenza di due metodi: boot e register.
Ecco il codice:

/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
    // pubblica il file di ocnfigurazione nella cartella config/
    $this->publishes([__DIR__.'/config/sitemapper.php' => config_path('sitemapper.php')]);
}

/**
* Register the application services.
*
* @return void
*/
public function register()
{
    //qui nulla
}

Quello che serve è che nel metodo boot() venga invocato il metodo publishes() con i due percorsi, quello del file dentro il package e quello del file dentro config.
Nel metodo register() non inserire nulla: per lo scopo del tutorial, puoi lasciarlo vuoto.

Manca ancora un passaggio: il caricamento del service provider durante l'avvio di Laravel.
Inserisci all'interno di config/app.php, nell'array providers : Tuonome\Sitemapper\SitemapperServiceProvider::class

Dopo aver eseguito questa operazione, digita artisan vendor:publish e il file di configurazione verrà spostato nella cartella config.

Il codice

Sitemapper.php

Il codice di Sitemapper è molto semplice: permette di aggiungere nodi alla sitemap tramite il metodo addUrl() e creare la sitemap richiamando il metodo render().
Ha un po' di variabili:

/**
* @var string
*/
protected $header = '<?xml version="1.0" encoding="UTF-8"?>';

/**
* @var string
*/
protected $open_urlset = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

/**
* @var string
*/
protected $end_urlset = '</urlset>';

/**
* @var string
*/
protected $compositing = '';

/**
* @var string
*/
protected $sitemap_file_name_to_verify;

che si occupano di indicare l'header del file xml ($header), l'elemento contenitore urlset ($open_urlset e $end_urlset), inizializzare una variabile in cui inserire di volta in volta i vari nodi ($compositing) e definire una variabile col nome del file su cui viene fatta una verifica ($sitemap_file_name_to_verify): Sitemapper, infatti, verifica che non sia già presente un file fisico nella root del sito.
Nel caso ci sia, restituisce quel file, altrimenti genera la sitemap: in $sitemap_file_name_to_verify indichiamo appunto il nome del file da verificare.

Il codice della libreria vera e proprira è il seguente:

/**
* Sitemapper constructor.
*/
function __construct()
{
    if (!is_null(config('sitemapper.sitemap_file_name_to_verify'))) {
        $this->sitemap_file_name_to_verify = config('sitemapper.sitemap_file_name_to_verify');
    } else {
        $config_package = include(__DIR__ . '/config/sitemapper.php');
        $this->sitemap_file_name_to_verify = $config_package['sitemap_file_name_to_verify'];
    }
}

/**
* Aggiunge un nodo nell'albero xml
* @param $location
* @param string $lastmod
* @param string $changefreq
* @param string $priority
*/
public function addUrl($location, $lastmod = null, $changefreq = null, $priority = null) {
    $output = '<url>';
        $output .= '<loc>' . url($location) . '</loc>';
        if (!is_null($lastmod)) {
            $datetime = new \DateTime($lastmod);
            $output .= '<lastmod>' . $datetime->format('Y-m-d\TH:i:sP') . '</lastmod>';
        }
        if (!is_null($changefreq)) $output .= '<changefreq>' . $changefreq . '</changefreq>';
        if (!is_null($priority)) $output .= '<priority>' . $priority . '</priority>';
    $output .= '</url>';
    $this->compositing .= $output;
}

/**
* Restituisce l'intero albero xml
* @return string
*/
public function render() {
    if (is_file( public_path( $this->sitemap_file_name_to_verify ) )) {
        response()->file( public_path( $this->sitemap_file_name_to_verify ) );
    } else {
        $sitemap = $this->header;
        $sitemap .= $this->open_urlset;
        $sitemap .= $this->compositing;
        $sitemap .= $this->end_urlset;

        return $sitemap;
}

Questo è quello che fa: il __construct() si preoccupa di verificare se il file di configurazione è stato pubblicato nella cartella config/. Se non lo trova, carica il valore di default dal file di configurazione all'interno della libreria.

Il metodo addUrl() accetta 4 parametri:

  • $location: obbligatorio, la url del sito da aggiungere;
  • $lastmod: opzionale, la data di ultima modifica, nel formato AAAA-MM-GG;
  • $changefreq: opzionale, la frequenza di aggiornamento della pagina (always, hourly, daily, weekly, monthly, yearly, never);
  • $priority: opzionale, la priorità di questa URL rispetto ad altre URL del sito valori compresi tra 0,0 e 1,0.

Il file di configurazione sitemapper.php

Il file di configurazione rispetta la sintassi presente negli altri file di configurazione di Laravel, ritorna, cioè, un array associativo:

return[
    'sitemap_file_name_to_verify' => 'sitemap.xml',
];

Di default il nome del file da verificare è sitemap.xml, ma può essere cambiato una volta pubblicato.
In realtà, non ci sono molti motivi per modificare il nome del file, ma ho incluso questa parte per mostrarti come creare un file di configurazione e come usarlo.

Be an open source dependency...

Per poter includere facilmente Sitemapper come dipendenza in eventuali prossimi progetti, lo devi dapprima caricare su Github e poi registrarlo su Packagist.
Crea un nuovo repository su Github e, dato che in locale avevi inizializzato il repository nella cartella sitemapper, aggiungi come remote origin il repository remoto tremite git remote add origin https://github.com/user/repo.git, sostituendo, ovviamente i tuoi parametri al posto di user e repo.git.
Infine, con git push, carica tutto.

Una volta fatta questa operazione, crea un account su packagist e registra un nuovo package:

  • clicca su Submit;
  • incolla il link alla pagina di Github con il tuo repository.

Tutto fatto.
L'unico inconveniente è che l'update del package è manuale: questo vuol dire che ogni volta che fai un push su github, dovresti andare anche su packagist, dentro al package e cliccare update. Scomodo, vero?

La soluzione è creare un webhook su Github:

  • su packagist copia il tuo API Token dalla pagina profile;
  • dentro al progetto su github, clicca su Settings e poi Webhooks & services;
  • dal selettore Add service seleziona packagist e, in basso, compila i campi incollando il token di packagist. Da questo momento, ad ogni push su github, il progetto viene aggiornato anche su packagist.

Ora tutto è pronto perchè il tuo package possa essere incluso in altri progetti, inserendo nel composer.json, tra i require, "tuonome/sitemapper": "*" ( al posto dell'asterisco puoi indicare una versione specifica, creata con git tag - approfondisci git tag nella documentazione ufficiale di GIT ).

... or be a private dependency

Nel caso la dipendenza debba rimanere privata o tu non abbia necessità o volontà di condividerla, è possibile riutilizzare il package su tutti i sistemi che vuoi semplicemente creando una cartella package (o altro nome di tua preferenza) e inserendo i parametri autoload nel composer.json della root. A Laravel non interessa molto dove metti il package, ma gli interessa sapere dove lo deve trovare se lo usi.

Conclusioni

Nel caso di Sitemapper, siamo di fronte a qualcosa di veramente banale, ma è perfetto per capire le basi dello sviluppo di un package: basta comprendere quali sono i passaggi da fare e le regole da rispettare.
Se poi vuoi contribuire a migliorare Sitemapper, sei il benvenuto: puoi trovare il mio Sitemapper a questa pagina di GitHub.


Fonti e approfondimenti

documentazione ufficiale Laravel
tutorial in inglese per lo sviluppo di un package


Blog tags


Cerca un post


Ultimi tweet


pubblicato il {{ tweet.created_at | formattaData }}