Cos'è Composer?
Proprio come NPM è per JavaScript, Composer è lo strumento dedicato per PHP che ti consente di gestire le dipendenze delle librerie per le tue applicazioni. Se hai utilizzato un framework come Symfony, sicuramente lo hai usato. In realtà, ogni sviluppatore PHP lo utilizza quotidianamente. Se sei su questa pagina, probabilmente ti stai chiedendo come creare una libreria facilmente redistribuibile e condivisibile con la comunità. Ecco cosa cercherò di mostrarti.
Per il bene del nostro tutorial, prenderò l'esempio concreto di una correzione che ho dovuto applicare di recente a un progetto. Mi sono veramente chiesto, "Perché non esiste già uno strumento per risolvere questo facilmente?". Dunque, trasformerò questa piccola correzione in una libreria di Composer e forse, oltre a permettermi di scrivere questo articolo, questa libreria sarà utile a qualcun altro.
Contesto: Ho ereditato un progetto con un problema di deserializzazione dei dati inseriti in un database. Lo sviluppatore ha avuto la buona idea di usare serialize su un array. Beh, non è quello che avrei fatto io, ma così è fatta l'app e di per sé non è troppo fastidioso. Tranne che, apparentemente, i dati sono andati da un DB all'altro (mysql->mssql->mysql) generando problemi di codifica. Alla fine ci ritroviamo con un bel errore di unserialize a offset. L'errore è piuttosto verboso. Il formato serialize di PHP, che indica il tipo seguito dal numero di caratteri, ci consente di diagnosticare rapidamente che il numero di caratteri calcolato non è corretto. È solo questione di ricalcolarli e riscriverli nel file con una regex e un callback.
Per un promemoria, ecco la doc sul formato https://en.wikipedia.org/wiki/PHP_serialization_format
Quindi il nostro errore proviene dal processo di deserializzazione quando il numero di caratteri specificato per una stringa non corrisponde a ciò che viene decodificato dalla funzione unserialize. Quindi è necessario ricalcolare. Ad esempio:
s:6:"apple";
dovrebbe essere:
s:5:"apple";
È abbastanza semplice, ma perfetto per il nostro esempio!
Cominciamo! Mettiamoci all'opera.
Prerequisiti
Averemo bisogno di:
– PHP installato in CLI sulla nostra macchina,
– un account su github,
– un account su packagist
– Composer installato sulla nostra macchina.
Presumibilmente, se sei qui è perché hai già installato PHP e Composer. Se non ancora, puoi trovare molte risorse su Internet per spiegare come fare.
Allora cominciamo creando il nostro progetto nel nostro editor preferito, per me è PhpStorm. A questo punto, dovresti avere una directory vuota.
Il seguente comando ti consentirà di inizializzare il tuo progetto Composer e arricchirlo con informazioni di base come la Licenza, il nome, l'indirizzo del repository e una breve descrizione.
composer init -q -a src \
--name partitech/fix-serialize \
--description "Fix Php unserialize error at offset" \
--license MIT
Passerai da una directory vuota a un'architettura progettata per Composer. È abbastanza magico.
{
"name": "partitech/fix-serialize",
"description": "Fix Php unserialize error at offset",
"license": "MIT",
"autoload": {
"psr-4": {
"Partitech\\FixSerialize\\": "src"
}
},
"require": {}
}
src: Contiene il tuo codice
vendor: Contiene tutte le dipendenze. Sì, la tua libreria potrebbe aver bisogno di altre librerie di Composer per funzionare.
composer.json e composer.lock sono file con cui sicuramente sei familiare 😉
Come ogni buona libreria, testeremo il nostro codice ad ogni fase del nostro sviluppo e per fare questo installeremo, indovina un po'... PhpUnit.
composer require --dev phpunit/phpunit ^9.5
Configureremo PhpUnit con il famoso phpunit.xml e aggiungeremo i giusti riferimenti in composer.json.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="common tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
Il tuo file composer dovrebbe più o meno somigliare a questo.
{
"name": "partitech/fix-unserialize",
"description": "Fix Php unserialize error at offset",
"license": "MIT",
"autoload": {
"psr-4": {
"Partitech\\FixUnSerialize\\": "src"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"scripts": {
"test": "phpunit"
}
}
Puoi creare la directory tests ed eseguire il comando composer test. Al momento non ci sono test, ma questo servirà a convalidare che tutto sia configurato correttamente.
mkdir tests
composer test
È finalmente il momento di creare il nostro primo test fittizio che ci consentirà di creare i file di cui abbiamo bisogno.
<?php
namespace Partitech\FixUnSerialize\Tests;
use Partitech\FixUnSerialize\UnSerializeService;
class FixUnSerializeTest extends \PHPUnit\Framework\TestCase
{
public function testSayHelloWorld()
{
$helloWorld = new UnSerializeService();
$this->assertEquals("Hello World!", $helloWorld->unserialize('Hello'));
}
}
<?php
namespace Partitech\FixUnSerialize;
class UnSerializeService
{
/**
* @param string $data
* @return string|null
*/
public function unserialize(string $data): ?string
{
return $data . ' World!';
}
}
A questo punto, hai tutto il necessario per iniziare a codificare la tua propria libreria. Per quanto mi riguarda, creerò i metodi più piccoli e semplici possibili in modo da poterli testare nel modo più semplice possibile. Mi piace abbastanza il principio "un metodo, una responsabilità", che mi consente di dividere chiaramente quello che voglio fare. Inoltre, assegnando una semplice responsabilità ad ognuno dei miei metodi mi permette di includere il contratto/documentazione direttamente nel nome. Così, un metodo "isValid" avrà solo la responsabilità di testare se una voce è valida, ecc. Ecco lo schema della classe che utilizzeremo. Ancora una volta, è solo per illustrare il nostro tutorial.
public isValid(string $data): bool
public fixIfInvalid(string $data):? string
public fix(string $data): string
public fixLength(array $values): string
public unserialize(string $data)
Quindi in ordine, chiamiamo unserialize -> testiamo se è non valido -> correggiamo -> deserializziamo. Molto semplice davvero. Sto fornendo il codice per l'esempio, e andiamo avanti.
<?php
namespace Partitech\FixUnSerialize;
class UnSerializeService
{
/**
* @param string $data
* @return mixed
*/
public function unserialize(string $data)
{
$data = $this->fixIfInvalid($data);
return unserialize($data);
}
/**
* @param string $data
* @return string|null
*/
public function fixIfInvalid(string $data): ?string
{
if (!$this->isValid($data)) {
$data = $this->fix($data);
}
return $data;
}
/**
* @param string $data
* @return bool
*/
public function isValid(string $data): bool
{
if (!@unserialize($data)) {
return false;
}
return true;
}
/**
* @param string $data
* @return string
*/
public function fix(string $data): string
{
$pattern = '/s\:(\d+)\:\"(.*?)\";/s';
return preg_replace_callback($pattern, [$this, 'fixLength'], $data);
}
/**
* @param array $values
* @return string
*/
public function fixLength(array $values): string
{
$string = $values[2];
$length = strlen($string);
return 's:' . $length . ':"' . $string . '";';
}
}
<?php
namespace Partitech\FixUnSerialize\Tests;
use Partitech\FixUnSerialize\UnSerializeService;
use PHPUnit\Framework\TestCase;
class FixUnSerializeTest extends TestCase
{
const VALID_STRING = 'a:2:{s:4:"test";s:4:"test";s:5:"test2";s:5:"test2";}';
const INVALID_STRING = 'a:2:{s:123456:"test";s:4:"test";s:5:"test2";s:5:"test2";}';
private UnSerializeService $unserializeService;
public function __construct(?string $name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
$this->unserializeService = new UnSerializeService();
}
public function testIsValidTrue()
{
$this->assertTrue($this->unserializeService->isValid(self::VALID_STRING));
}
public function testIsValidFalse()
{
$this->assertNotTrue($this->unserializeService->isValid(self::INVALID_STRING));
}
public function testFixIfInvalidWithValidString()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fixIfInvalid(self::VALID_STRING)
);
}
public function testFixIfInvalidWithInvalidString()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fixIfInvalid(self::INVALID_STRING)
);
}
public function testFixLength()
{
$data = [
's:5000:"test2";',
5,
'test2'
];
$expected = 's:5:"test2";';
$this->assertEquals(
$expected,
$this->unserializeService->fixLength($data)
);
}
public function testFixValid()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fix(self::VALID_STRING)
);
}
public function testFixInvalid()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fix(self::INVALID_STRING)
);
}
public function testUnserializeValid()
{
$this->assertEquals(
unserialize(self::VALID_STRING),
$this->unserializeService->unserialize(self::VALID_STRING)
);
}
public function testUnserializeInvalid()
{
$this->assertEquals(
unserialize(self::VALID_STRING),
$this->unserializeService->unserialize(self::INVALID_STRING)
);
}
}
Ora che abbiamo il nostro codice, lo spingeremo su Github. Iniziamo a versionare il nostro progetto e aggiungendo la lista dei file che non vogliamo includere.
git init
touch .gitignore
echo "vendor/" >> .gitignore
echo ".phpunit.result.cache" >> .gitignore
echo ".idea/" >> .gitignore
git add .
git commit -m "First commit"
Inizializza un repository vuoto su Github. Ora possiamo spingere il nostro codice sul nostro repository.
git remote add origin ssh://git@github.com/partitech/fix-serialize.git
git branch -M main
git push -f -u origin main
Ora che il tuo codice è pubblicato su Github possiamo andare avanti.
Crea la tua prima release.
Nella pagina di creazione release, seleziona "Scegli un tag" e inserisci "v0.0.1" e clicca su "crea nuovo tag", lo stesso per il nome della tua release. Inserisci "Primo rilascio" come descrizione e clicca su "Pubblica release".
Una volta creata, sarai sulla pagina della tua prima release.
Il nostro ultimo passo consisterà nel referenziare la nostra versione su Packagist in modo che sia direttamente accessibile tramite Composer.
Nella pagina https://packagist.org/login/ usa l'opzione "accedi con Github".
Invia il tuo repository:
Ora hai accesso al tuo pacchetto direttamente tramite:
composer require partitech/fix-unserialize
Fantastico 😉
Ora configureremo Github per notificare Packagist di qualsiasi cambiamento nel repository.
Recupera il tuo API Token su Packagist https://packagist.org/profile/. Clicca su Mostra API Token e copia la tua chiave.
In GitHub, vai su Impostazioni > Webhooks > Modifica.
Cambia il Secret incollando la tua chiave API di Packagist.
E ecco, puoi fare un push sul tuo repository e verificare i log Impostazioni > Webhooks > Consegne recenti
Assicurati che tutto sia a posto su Packagist.
Ora sai come creare una libreria di Composer e come consegnarla al resto del mondo 😊