Come creare un package Laravel

by NewPortal SuperAdmin on 04/03/2018

In questo articolo vedremo come creare un pacchetto per Newportal, installabile con Composer e distribuibile attraverso Packagist. Composer è un dependecy manager che ci permette di installare e aggiornare le librerie esterne in modo molto semplice.
D'ora innanzi indicheremo con <newportal> il progetto root di laravel che si troverà nella cartella "newportal" e con <todolist> il percorso del pacchetto in costruzione ovvero "packages/lfgscavelli/todolist". La dir "packages" conterrà tutti i pacchetti che andremo a realizzare e sarà posizionata allo stesso livello gerarchico di <newportal>. Procediamo, quindi, con la creazione delle cartelle e del file di configurazione del pacchetto (composer.json).

# creazione della cartella src del pacchetto <todolist>
mkdir -p packages/lfgscavelli/todolist/src

# creazione della cartella tests
cd packages/lfgscavelli/todolist
mkdir tests

# creazione del file composer.json
composer init

Il comando "composer init" genererà interattivamente il file "composer.json", chiedendo alcune informazioni circa la configurazione del pacchetto. Riportiamo nel file <todolist>/composer.json, appena creato, quanto segue:

...
"require": {
		"php": ">=7.1.3",
        "laravel/framework": "5.7.*"
},
"require-dev": {
		"phpunit/phpunit": "~7.0",
        "orchestra/testbench": "~3.0"
},
"autoload": {
        "psr-4": {
            "Lfgscavelli\\Todolist\\": "src/",
            "Lfgscavelli\\Todolist\\Seeds\\": "database/seeds/"
        }
},
"autoload-dev": {
        "psr-4": {
            "Lfgscavelli\\Todolist\\Test\\": "tests/"
        }
},
"extra": {
        "laravel": {
            "providers": [
                "Lfgscavelli\\Todolist\\TodolistServiceProvider"
            ],
            "aliases": {
                "Todolist": "Lfgscavelli\\Todolist\\Facades\\TodolistFacade"
            }
        }
},
"minimum-stability": "dev",
"prefer-stable" : true
...

Tutti i file e le directory che non seguono la specifica PSR-0/4, ma che necessitano di un caricamento automatico delle classi, possono essere inseriti nella direttiva "classmap" di autoload es.

// a titolo di esempio
"autoload": {
     "classmap": ["lib/", "Something.php"]
}

# al termine delle modifiche ricordiamoci di effettuare la ricostruzione del file autoload.php
composer dump-autoload

Poichè lavoriamo in locale su un progetto in fase di sviluppo, per l'attivazione del pacchetto si potrà procedere in due differenti modi. In entrambi i casi sarà necessario creare un provider di servizi in modo da poter registrare le rotte e le viste del nostro pacchetto. Per cui spostiamoci su <newportal> e creiamo il file TodolistServiceProvider.

# creiamo il Server Provider e modifichiamo il namespace in "Lfgscavelli\Todolist";
php artisan make:provider TodolistServiceProvider

# spostiamo il file appena creato all'interno della src di <todolist>
mv providers/TodolistServiceProvider.php packages/lfgscavelli/todolist/src/


Attivazione del pacchetto - Prima procedura

La procedura è veloce e ci consente con due semplici passi di mettere in funzione il ns. package all'interno di un progetto laravel. Inseriamo all'interno del file <newportal>/composer.json, in corrispondenza della direttiva psr-4 di autoload, la specifica del nostro pacchetto avente come valore il path dei sorgenti:
 

"autoload": {
        "classmap": [
          ...
        ],
        "psr-4": {
            "App\\": "app/",
            "Lfgscavelli\\Todolist\\": "../packages/lfgscavelli/todolist/src"
        }
},

dopodichè carichiamo il provider dei servizi, inserendo il riferimento alla classe all'interno del file  <newportal>config/app.php. Al termine avviamo il comando di ricostruzione del file autoload.php, ovvero composer dump-autoload

'providers' => [
    ...
    /*
     * Package Service Providers...
     */
     Lfgscavelli\Todolist\TodolistServiceProvider::class
],


Attivazione del pacchetto - Seconda procedura

Meno veloce della prima, ma con un maggior controllo sul pacchetto attraverso i comandi composer. Procediamo con la creazione di un collegamento simbolico "<newportal>/packages" che punterà alla cartella esterna "packages", dove realmente si troverà il ns. pacchetto basato su composer. Questo perchè vogliamo gestire separatamente i packages dai vari progetti.

# su windows
mklink /D "C:\program\newportal\packages" "C:\program\packages"

# su homestead (da verificare)
ln -ls  /home/vagrant/code/newportal/packages /home/vagrant/code/packages

Apriamo il file <newportal>/composer.json della root laravel e inseriamo quanto segue:

# in fase di installazione del pacchetto sarà considerata come repository la ns. cartella packages
...
"repositories": [
        {
            "type": "path",
            "url": "./packages/lfgscavelli/todolist/",
            "options": {
                "symlink": true
            }
        }
]
...

La chiave symlink impostata a "true", indicherà a composer, in fase di installazione del pacchetto, di voler creare un link simbolico alla cartella del pacchetto "<newportal>/packages/lfgscavelli/todolist/", il cui percorso risulta associato alla chiave "url". Il repository principale dal quale Composer scarica i suoi pacchetti è Packagist. In questo caso stiamo dicendo a composer di cercare la libreria lfgscavelli/todolist in un archivio specifico.

Siamo, dunque, pronti per vedere in azione il ns. pacchetto. Carichiamolo nel progetto laravel attraverso "composer require". Attenzione, come vedremo più avanti, in questa demo abbiamo deciso di non installare, con "composer install", le dipendenze nel pacchetto <todolist>  e, quindi, di non includere il file "/vendor/autoload.php" nel pacchetto, ma di installare le dipendenze direttamente nel progetto <newportal>. In pratica non renderemo il pacchetto autoconsistente, ma strettamente legato a <newportal>.

# Il ns. pacchetto sarà inserito nel progetto laravel:
composer require lfgscavelli/todolist @dev

# in alternativa al comando "Composer require" avremmo potuto:
# 1) inserire nel file <newportal>/composer.json quanto segue:
"require": {
        "lfgscavelli/todolist": "dev-master"
}
# 2) ed eseguire il comando "composer update" per l'aggiornamento delle dipendenze.
composer update;

# ricordiamo che per rimuovere il pacchetto sarà necessario eseguire:
composer remove lfgscavelli/todolist


Creazione dei file necessari

Procediamo con la creazione dei file necessari al buon funzionamento del pacchetto, es. Todolist, Facade e il file per le rotte, web.php:

# Creiamo le cartelle e i file delle rotte e della Facade
mkdir packages/lfgscavelli/todolist/src/routes packages/lfgscavelli/todolist/src/Facades
touch 
packages/lfgscavelli/todolist/src/routes/web.php
packages/lfgscavelli/todolist/src/Facades/TodolistFacade.php
packages/lfgscavelli/todolist/src/Todolist.php

Nella classe Todolist riportiamo un semplice metodo d'esempio. Questa sarà la classe che sarà collegata alla facade e risulterà accessibile anche attraverso i metodi statici:

namespace Lfgscavelli\Todolist;

class Todolist {
    public function hello() {
        return "Hello package";
    }
}

Nel file TodolistServiceProvider.php riporteremo semplicemente quanto segue:

namespace Lfgscavelli\Todolist;
use Illuminate\Support\ServiceProvider;

class TodolistServiceProvider extends ServiceProvider {

    public function boot()  {
        if (!$this->app->routesAreCached()) {$this->loadRoutesFrom(__DIR__.'/routes/web.php');}
    }

    public function register() {
        $this->app->bind(Todolist::class, function() {
            return new Todolist;
        });

        $this->app->alias(Todolist::class, 'todo-list');
    }
}

Nel fornitore di servizio sarà anche possibile registrare altre risorse del pacchetto, quali rotte, migrazioni, configurazioni, etc... Vedremo pertanto, di seguito, alcuni esempi:

// caricamento e pubblicazione delle view
$this->loadViewsFrom(__DIR__ . '/resources/views', 'todolist');
$this->publishes([__DIR__.'/resources/views' =>
base_path('resources/views/vendor/lfgscavelli/todolist')]);

// caricamento e pubblicazione delle migrazioni
$this->loadMigrationsFrom(__DIR__ . '/database/migrations');
$this->publishes([__DIR__ .'/database/migrations' => database_path('migrations')], 'migrations');

// pubblicazione dei file di configurazione
$this->publishes([__DIR__ .'/config/todolist.php' => config_path('todolist.php')], 'config');
// per l'utilizzo config('todolist.option')

// file di traduzione
$this->loadTranslationsFrom(__DIR__.'/resources/lang/translations', 'todolist'); 
// per l'utilizzo trans('todolist::messages.welcome')

// pubblicazione dei file di traduzione
$this->publishes([__DIR__.'/resources/lang/translations' =>
resource_path('lang/vendor/lfgscavelli/todolist')]);

// pubblicazione degli asset pubblici
$this->publishes([__DIR__.'/resources/assets' =>
public_path('vendor/lfgscavelli/todolist')], 'public');

etc...

# le migrations devono essere installate
php artisan migrate --path=vendor/lfgscavelli/todolist/src/database/migrations

# se abbiamo pubblicato i file nella migrations dell'app <newportal>
php artisan migrate

# se abbiamo precisato nel composer.json Lfgscavelli\\Todolist\\Seeds\\
php artisan db:seed --class=Lfgscavelli\\Todolist\\Seeds\\DatabaseSeeder

# Pubblica tutti i file caricati con ->publishes
php artisan vendor:publish [--force] [--tag] # --tag=config

Riportiamo nel file TodolistFacade.php quanto segue.

namespace Lfgscavelli\Todolist\Facades;
use Illuminate\Support\Facades\Facade;

class TodolistFacade extends Facade {
    protected static function getFacadeAccessor() {
        return 'todo-list';
    }
}

A pacchetto caricato, potremo accedere ai metodi della classe Todolist anche nel seguente modo: Todolist::hello(). Ora gestiamo alcune rotte nel file web.php:

Route::get('/hello', function () {
    return Todolist::hello();
});

Route::get('/testhello', function () {
    return app('todo-list')->hello();
});

/*
# es. di rotta verso un controller
Route::group(['namespace' => 'Lfgscavelli\Todolist\Http\Controllers'], function () {
    Route::get('/admin/tasks', 'TaskController@list');
});
*/

Possiamo, quindi, passare alla creazione dei file di test, utilizzando il pacchetto orchesta/testbench. Creiamo il file "<todolist>/tests/TestCase.php" e riportiamo quanto segue:

namespace Lfgscavelli\Todolist\Test;
use Lfgscavelli\Todolist\Facades\TodolistFacade;
use Lfgscavelli\Todolist\TodolistServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;
class TestCase extends Orchestra {
    protected function getPackageProviders($app) {
        return [TodolistServiceProvider::class,];
    }
    protected function getEnvironmentSetUp($app) {
        // ...
    }
    protected function getPackageAliases($app)   {
        return ['Todolist' => TodolistFacade::class];
    }
}

Creiamo il file <todolist>/tests/Unit/TestTodolist.php ed inseriamo la classe TestTodolist come estensione di TestCase.

namespace Lfgscavelli\Todolist\Test\Unit;
use Lfgscavelli\Todolist\Todolist;
use Lfgscavelli\Todolist\Test\TestCase;
class TestTodolist extends TestCase {
    public function testExamplePackage() {
        $this->assertTrue(true);
    }
}

Per poter eseguire i test direttamente nel ns. pacchetto, abbiamo bisogno di creare anche il file di configurazione di Phpunit <todolist>/phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="../../../newportal/vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         syntaxCheck="false"
         stopOnFailure="false">
    <testsuites>
        <testsuites>
            <testsuite name="Unit">
                <directory suffix="Test.php">./tests</directory>
            </testsuite>
        </testsuites>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./src</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_URL" value="app.test"/>
    </php>
</phpunit>

Notiamo che il valore del parametro bootstrap è stato impostato a "../../../newportal/vendor/autoload.php". Questo perchè vogliamo utilizzare l'autoload di <newportal> in quanto le dipendenze, come detto pocanzi, le installeremo direttamente nella root di laravel <newportal>. Nel nostro caso caricheremo soltanto orchestral/testbench in quanto le altre dipendenze risulteranno già presenti.

# installiamo il pacchetto orchestra/testbench su <newportal>
composer require orchestra/testbench:"^3.6"

Per eseguire i test del pacchetto eseguiremo phpunit con l'uso di alcune opzioni:

​​​​​​​# eseguiamo i tests
cd /c/program/newportal/packages/lfgscavelli/todolist
../../../vendor/bin/phpunit tests

# per agevolarci la vita, potremmo anche creare un alias
alias runtest=../../../vendor/bin/phpunit

# e chiamare direttamente l'alias
runtest tests

# per eseguire solo i test di alcune cartelle etc...
runtest tests/Unit

In alternativa, se il pacchetto non fosse legato a newportal, avremmo potuto anche:
1) porre il valore di bootstrap a "vendor/autoload.php", esempio:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
...

2) installare le dipendenze nella cartella <todolist>, esempio:

# tutte le dipendenze saranno installate all'interno di <todolist>/vendor
cd /c/program/newportal/packages/lfgscavelli/todolist
composer install

# per cui sotto <todolist> avere le dir: src, tests e vendor

3) effettuare i test nel seguente modo

# avendo tutte le dipendenze sotto vendor eseguire direttamente il comando
cd /c/program/newportal/packages/lfgscavelli/todolist
vendor/bin/phpunit tests

4) includere, al di la dei test, l'autoload nel Service Provider del pacchetto per renderlo autoconsistente es. require_once __DIR__ . '/vendor/autoload.php'

Il nostro pacchetto è pronto per poter essere arricchito di altri file quali <todolist>/README.md<todolist>/LICENSE.md, <todolist>/CONTRIBUTING.md, <todolist>/.gitignore, <todolist>/.gitattributes etc...
​​​​​​​

​​​​​​​Caricamento su GitHub

Trasferiamo, dunque, il nostro pacchetto su un repository Github. Dal ns. account creiamo un nuovo repository di nome "todolist". Localmente eseguiamo quanto segue:

​​​​​​​cd /c/program/www/packages
git init 
git add .
git commit -m "first commit"
git remote add origin https://github.com/lfgscavelli/todolist.git
git push -u origin master


​​​​​​​Distribuzione del Pacchetto su Packagist

Prima di distribuire il progetto su packagist.org, dobbiamo creare una versione stabile del pacchetto ed inviarla su github.

git tag -a 1.0.0 -m 'First version'
git push origin 1.0.0

Accediamo al sito packagist.org e, dopo esserci autenticati, clicchiamo sul link "Submit" posto nel menu in alto a destra. Dalla sezione "Submit package" inseriremo l'URL del repository pubblico GitHub nell'unico campo presente e avvieremo il check. Il pacchetto verrà automaticamente sottoposto a scansione periodica. Bisogna solo assicurarsi di mantenere aggiornato il file composer.json.

Utilizzo del ns. package pubblico

Quando il ns. package sarà terminato e pubblicato su packagist.org, tutto quello che ci resterà da fare è rimuovere quanto aggiunto nei file <newportal>/composer.json o nel file <newportal>/app.php a seconda della procedura di attivazione scelta, e aggiungere fra le dipendenze del progetto il nostro nuovo pacchetto (lfgscavelli/todolist).

# se si è adottata la seconda procedura, rimuoviamo la seguente sezione 
# ...
"repositories": [
        {
            "type": "path",
            "url": "./packages/lfgscavelli/todolist/",
            "options": {
                "symlink": true
            }
        }
]
# ...

# se si è adottata la prima procedura, rimuoviamo dal file <newportal>/composer.json quanto segue
# ...
    "Lfgscavelli\\Todolist\\": "../packages/lfgscavelli/todolist/src"
# ...
# e dal file <newportal>/config/app.php quanto segue 
# ...
    Lfgscavelli\Todolist\TodolistServiceProvider::class
# ...
# inseriamo come  dipendenza il ns. pacchetto
"require": {
        "lfgscavelli/todolist": ">=1.0.0"
}

Da <newportal> eseguiamo il comando "composer update" per caricare il tutto e lavorare con il ns. nuovissimo package todolist.

Send Comment