Impostazione di uno specchio locale per pacchetti Composer con Satis

Installare tutte le tue librerie PHP con Composer è un ottimo modo per risparmiare tempo. Ma i progetti più grandi testati automaticamente ed eseguiti ad ogni commit al tuo sistema di controllo della versione del software (SVC) impiegheranno molto tempo per installare tutti i pacchetti richiesti da Internet. Si desidera eseguire i test il più presto possibile attraverso il sistema di integrazione continua (CI) in modo da ottenere feedback rapidi e reazioni rapide in caso di errore. In questo tutorial creeremo un mirror locale per proxy tutti i pacchetti richiesti nei tuoi progetti composer.json file. Questo farà sì che i nostri CI funzionino molto più velocemente, installino i pacchetti sulla rete locale o siano anche ospitati sulla stessa macchina, e assicurati che le versioni specifiche dei pacchetti siano sempre disponibili.


Cos'è Satis?

Satis è il nome dell'applicazione che useremo per rispecchiare i vari repository per il nostro progetto. Si trova come un proxy tra Internet e il tuo compositore. La nostra soluzione creerà uno specchio locale di alcuni pacchetti e istruirà il nostro compositore a utilizzarlo al posto delle fonti trovate su Internet.

Ecco un'immagine che dice più di mille parole.


Il nostro progetto userà il compositore come al solito. Sarà configurato per utilizzare il server Satis locale come fonte principale. Se viene trovato un pacchetto, verrà installato da lì. In caso contrario, lasceremo che il compositore usi il pacchetto predefinito di packagist.org per recuperare il pacchetto.


Ottenere Satis

Satis è disponibile tramite il compositore, quindi l'installazione è molto semplice. Nell'archivio del codice sorgente allegato, troverai Satis installato nel Fonti / Satis cartella. Per prima cosa installeremo il compositore stesso.

$ curl -sS https://getcomposer.org/installer | php #! / usr / bin / env php Tutte le impostazioni corrette per l'utilizzo di Composer Downloading ... Composer installato con successo su: / home / csaba / Personal / Programming / NetTuts / Impostazione di un mirror locale per i pacchetti Composer con Satis / Sources / Satis / compositore .phar Usalo: php composer.phar

Quindi installeremo Satis.

$ php composer.phar crea-progetto compositore / satis --stability = dev --keep-vcs Installazione di compositore / satis (dev-master eddb78d52e8f7ea772436f2320d6625e18d5daf5) - Installazione di compositore / satis (master di sviluppo master) Master di clonazione Progetto creato in / home / csaba / Personal / Programmazione / NetTuts / Configurare un mirror locale per i pacchetti Composer con Satis / Sources / Satis / satis Caricamento dei repository di composer con informazioni sul pacchetto Installazione delle dipendenze (incluso require-dev) dal file di blocco - Installazione symfony / process (dev-master 27b0fc6) Clonazione 27b0fc645a557b2fc7bc7735cfb05505de9351be - Installazione symfony / finder (v2.4.0-BETA1) Download: 100% - Installazione symfony / console (dev-master f44cc6f) Clonazione f44cc6fabdaa853335d7f54f1b86c99622db518a - Installazione seld / jsonlint (1.1.1) Download: 100% - Installazione di justinrainbow / json-schema (1.1.0) Download: 100% - Installazione di compositore / compositore (dev-master f8be812) Clonazione f8be812a496886c84918d6dd1b50db5c16da3cc3 - Installazione di twig / twig (v1.14.1) Download: 100% symfony / console suggerisce l'installazione di symfony / event-dispatcher () Generazione di file autoload

Configurazione di Satis

Satis è configurato da un file JSON molto simile al compositore. È possibile utilizzare qualsiasi nome che si desidera per il file e specificarlo per l'utilizzo in un secondo momento. Noi useremo "mirroring-packages.conf".

"nome": "NetTuts Composer Mirror", "homepage": "http: // localhost: 4680", "repository": ["type": "vcs", "url": "https: // github. com / SynetoNet / monolog ", " tipo ":" compositore "," url ":" https://packagist.org "]," require ": " monolog / monolog ":" syneto-dev ", "mockery / mockery": "*", "phpunit / phpunit": "*", "require-dependencies": true

Analizziamo questo file di configurazione.

  • nome - rappresenta una stringa che verrà mostrata nell'interfaccia web del nostro mirror.
  • homepage - è l'indirizzo web in cui saranno conservati i nostri pacchetti. Questo non dice al nostro server web di usare quell'indirizzo e quella porta, è piuttosto solo un'informazione di una configurazione funzionante. Configureremo l'accesso ad esso su quel indirizzo e porta in seguito.
  • repository - un elenco di repository ordinati per preferenza. Nel nostro esempio, il primo repository è una fork Github delle librerie di log monolog. Ha alcune modifiche e vogliamo usare quella forcella specifica quando installiamo monolog. Il tipo di questo repository è "VCS"Il secondo repository è di tipo"compositore". Il suo URL è il sito predefinito di packagist.org.
  • richiedere - elenca i pacchetti che vogliamo specchiare. Può rappresentare un pacchetto specifico con una versione o una diramazione specifica o qualsiasi versione per quella materia. Utilizza la stessa sintassi del tuo "richiedere" o "require-dev" nel tuo composer.json.
  • richiedono dipendenze - è l'ultima opzione nel nostro esempio. Dirà a Satis di rispecchiare non solo i pacchetti che abbiamo specificato in "richiedere"sezione ma anche tutte le loro dipendenze.

Per provare rapidamente le nostre impostazioni, dobbiamo prima dire a Satis di creare gli specchi. Esegui questo comando nella cartella in cui hai installato Satis.

$ php ./satis/bin/satis build ./mirrored-packages.conf ./packages-mirror Pacchetti di scansione Scrittura di pacchetti.json Scrittura di visualizzazione Web

Mentre il processo è in corso, vedrai come Satis esegue il mirroring di ciascuna versione trovata dei pacchetti richiesti. Sii paziente potrebbe volerci un po 'di tempo per costruire tutti quei pacchetti.

Satis lo richiede date.timezone da specificare nel php.ini file, quindi assicurati che sia impostato sul fuso orario locale. Altrimenti apparirà un errore.

[Twig_Error_Runtime] È stata generata un'eccezione durante il rendering di un modello ("date_default_timezone_get (): non è sicuro fare affidamento sulle impostazioni del fuso orario del sistema. È * obbligatorio * utilizzare l'impostazione date.timezone o la funzione date_default_timezone_set).

Quindi possiamo eseguire un'istanza del server PHP nella nostra console che punta al repository creato di recente. È richiesto PHP 5.4 o più recente.

$ php -S localhost: 4680 -t ./packages-mirror/ PHP 5.4.22-pl0-Gentoo Development Server avviato a Dom Dic 8 14:47:48 2013 Ascolto su http: // localhost: 4680 Document root is / home / csaba / Personal / Programming / NetTuts / Configurare un mirror locale per i pacchetti Composer con Satis / Sources / Satis / packages-mirror Premere Ctrl-C per uscire. [Dom Dec 8 14:48:09 2013] 127.0.0.1:56999 [200]: / [Dom Dec 8 14:48:09 2013] 127.0.0.1:57000 [404]: /favicon.ico - Nessun file o elenco

E ora possiamo sfogliare i nostri pacchetti con mirroring e persino cercare quelli specifici puntando il nostro browser web su http: // localhost: 4680:



Facciamolo ospitare su Apache

Se si dispone di un Apache in esecuzione, la creazione di un host virtuale per Satis sarà piuttosto semplice.

Ascolta 4680  Opzioni -Indice FollowSymLinks AllowOverride all Consenti l'ordine, nega Consenti da tutti   DocumentRoot "/ percorso / per / tuo / pacchetti-mirror" ServerName 127.0.0.1:4680 ServerAdmin [email protected] ErrorLog syslog: user 

Usiamo solo a .conf file come questo, inserire in Apache conf.d cartella, di solito /etc/apache2/conf.d. Crea un host virtuale sulla porta 4680 e lo indirizza alla nostra cartella. Ovviamente puoi usare qualunque porta tu voglia.


Aggiornamento dei nostri mirror

Satis non può aggiornare automaticamente i mirror a meno che non lo diciamo. Quindi il modo più semplice, su qualsiasi sistema simile a UNIX, è aggiungere semplicemente un cron job al tuo sistema. Sarebbe molto facile, e solo un semplice script per eseguire il nostro comando di aggiornamento.

#! / bin / bash php / full / percorso / su / satis / bin / satis build \ /full/path/to/mirrored-packages.conf \ / full / path / to / packages-mirror

Lo svantaggio di questa soluzione è che è statico. Dobbiamo aggiornare manualmente il mirroring-packages.conf ogni volta aggiungiamo un altro pacchetto ai nostri progetti composer.json. Se fai parte di un team in un'azienda con un grande progetto e un server di integrazione continua, non puoi fare affidamento sulle persone che ricordano di aggiungere i pacchetti sul server. Potrebbero anche non disporre delle autorizzazioni per accedere all'infrastruttura dell'elemento della configurazione.


Aggiornamento dinamico della configurazione Satis

È tempo per un esercizio PHP TDD. Se vuoi solo che il tuo codice sia pronto e funzionante, controlla il codice sorgente allegato a questo tutorial.

require_once __DIR__. '/ ... / ... / ... / ... /vendor/autoload.php'; class SatisUpdaterTest estende PHPUnit_Framework_TestCase function testBehavior () $ this-> assertTrue (true); 

Come al solito iniziamo con un test degenerativo, quel tanto che basta per assicurarci di avere una struttura di testing funzionante. Si può notare che ho una linea require_once piuttosto strana, questo perché voglio evitare di dover reinstallare PHPUnit e Mockery per ogni piccolo progetto. Quindi li ho in un venditore cartella nel mio Nettuts'root. Dovresti semplicemente installarli con il compositore e rilasciare il require_once linea del tutto.

class SatisUpdaterTest estende PHPUnit_Framework_TestCase function testDefaultConfigFile () $ expected = '"name": "NetTuts Composer Mirror", "homepage": "http: // localhost: 4680", "repository": ["type": " vcs "," url ":" https://github.com/SynetoNet/monolog ", " type ":" compositore "," url ":" https://packagist.org "]," require " : , "require-dependencies": true '; $ actual = $ this-> parseComposerConf ("); $ this-> assertEquals ($ expected, $ actual);

Sembra giusto. Tutti i campi tranne "richiedere"Sono statici, dobbiamo generare solo i pacchetti, i repository puntano ai nostri cloni git privati ​​e ai packagist se necessario. Gestire quelli è più un lavoro di amministratore di sistema che di uno sviluppatore di software.

Ovviamente questo non funziona con:

Errore irreversibile di PHP: chiamata al metodo non definito SatisUpdaterTest :: parseComposerConf ()

Riparare è facile.

funzione privata parseComposerConf ($ string) 

Ho appena aggiunto un metodo vuoto con il nome richiesto, come privato, alla nostra classe di test. Fantastico, ma ora abbiamo un altro errore.

PHPUnit_Framework_ExpectationFailedException: impossibile affermare che le corrispondenze null siano previste '...'

Quindi null non corrisponde alla nostra stringa che contiene tutta la configurazione predefinita.

funzione privata parseComposerConf ($ stringa) return '"nome": "NetTuts Composer Mirror", "homepage": "http: // localhost: 4680", "repository": ["type": "vcs", " url ":" https://github.com/SynetoNet/monolog ", " type ":" compositore "," url ":" https://packagist.org "]," require ": , "require-dependencies": true '; 

OK, funziona. Tutti i test stanno passando.

PHPUnit 3.7.28 di Sebastian Bergmann. Tempo: 15 ms, Memoria: 2,50 MB OK (1 test, 1 asserzione)

Ma abbiamo introdotto una duplicazione orribile. Tutto quel testo statico in due punti, scritto carattere per carattere in due luoghi diversi. Risolviamolo:

class SatisUpdaterTest estende PHPUnit_Framework_TestCase static $ DEFAULT_CONFIG = '"name": "NetTuts Composer Mirror", "homepage": "http: // localhost: 4680", "repository": ["type": "vcs", " url ":" https://github.com/SynetoNet/monolog ", " type ":" compositore "," url ":" https://packagist.org "]," require ": , "require-dependencies": true '; function testDefaultConfigFile () $ expected = self :: $ DEFAULT_CONFIG; $ actual = $ this-> parseComposerConf ("); $ this-> assertEquals ($ expected, $ actual); funzione privata parseComposerConf ($ string) return self :: $ DEFAULT_CONFIG;

Ahhh! Così va meglio.

 function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration () $ expected = self :: $ DEFAULT_CONFIG; $ actual = $ this-> parseComposerConf ('"require": '); $ this-> assertEquals ($ expected, $ actual); 

Bene. Anche questo passa. Ma evidenzia anche alcune duplicazioni e un inutile compito.

 function testDefaultConfigFile () $ actual = $ this-> parseComposerConf ("); $ this-> assertEquals (self :: $ DEFAULT_CONFIG, $ actual); function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration () $ actual = $ this-> parseComposerConf (' "require":  '); $ this-> assertEquals (self :: $ DEFAULT_CONFIG, $ actual);

Abbiamo delineato il $ previsto variabile. $ reale potrebbe anche essere in linea, ma a me piace di più in questo modo. Mantiene l'attenzione su ciò che viene testato.

Ora abbiamo un altro problema. Il prossimo test che voglio scrivere sarà simile a questo:

function testARequiredPackageInComposerWillBeInSatisAlso () $ actual = $ this-> parseComposerConf ('"require": "Mockery / Mockery": "> = 0.7.2"'); $ this-> assertContains ('"Mockery / Mockery": "> = 0.7.2"', $ actual); 

Ma dopo aver scritto la semplice implementazione, noteremo che richiede json_decode () e json_encode (). E naturalmente queste funzioni riformattano la nostra stringa e le stringhe corrispondenti saranno al massimo difficili. Dobbiamo fare un passo indietro.

function testDefaultConfigFile () $ actual = $ this-> parseComposerConf ("); $ this-> assertJsonStringEqualsJsonString ($ this-> jsonRecode (self :: $ DEFAULT_CONFIG), $ actual); function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration () $ actual = $ this-> parseComposerConf ('"require": '); $ this-> assertJsonStringEqualsJsonString ($ this-> jsonRecode (self :: $ DEFAULT_CONFIG), $ actual); private function parseComposerConf ($ jsonConfig) return $ this-> jsonRecode (self :: $ DEFAULT_CONFIG); funzione privata jsonRecode ($ json) return json_encode (json_decode ($ json, true));

Abbiamo modificato il nostro metodo di asserzione per confrontare le stringhe JSON e abbiamo anche ricodificato la nostra $ reale variabile. ParseComposerConf () è stato modificato anche per utilizzare questo metodo. Vedrai in un attimo come ci aiuta. Il nostro prossimo test diventa più specifico per JSON.

function testARequiredPackageInComposerWillBeInSatisAlso () $ actual = $ this-> parseComposerConf ('"require": "Mockery / Mockery": "> = 0.7.2"'); $ this-> assertEquals ('> = 0.7.2', json_decode ($ actual, true) ['require'] ['Mockery / Mockery']); 

E far passare questo test, insieme al resto dei test, è abbastanza facile, di nuovo.

funzione privata parseComposerConf ($ jsonConfig) $ addedConfig = json_decode ($ jsonConfig, true); $ config = json_decode (self :: $ DEFAULT_CONFIG, true); if (isset ($ addedConfig ['require'])) $ config ['require'] = $ addedConfig ['require'];  restituisce json_encode ($ config); 

Prendiamo la stringa JSON di input, la decodifichiamo e se contiene una "richiedere"il campo lo utilizziamo nel nostro file di configurazione Satis, ma potremmo voler rispecchiare tutte le versioni di un pacchetto, non solo l'ultimo. Quindi forse vogliamo modificare il nostro test per verificare che la versione sia" * "in Satis, indipendentemente da quale sia la versione esatta composer.json.

function testARequiredPackageInComposerWillBeInSatisAlso () $ actual = $ this-> parseComposerConf ('"require": "Mockery / Mockery": "> = 0.7.2"'); $ this-> assertEquals ('*', json_decode ($ actual, true) ['require'] ['Mockery / Mockery']); 

Questo ovviamente fallisce con un messaggio interessante:

PHPUnit_Framework_ExpectationFailedException: impossibile affermare che due stringhe siano uguali. Previsto: * Effettivo:> = 0.7.2

Ora, dobbiamo effettivamente modificare il nostro JSON prima di ricodificarlo.

funzione privata parseComposerConf ($ jsonConfig) $ addedConfig = json_decode ($ jsonConfig, true); $ config = json_decode (self :: $ DEFAULT_CONFIG, true); $ config = $ this-> addNewRequires ($ addedConfig, $ config); ritorna json_encode ($ config);  private function toAllVersions ($ config) foreach ($ config ['require'] come $ package => $ version) $ config ['require'] [$ package] = '*';  return $ config;  funzione privata addNewRequires ($ addedConfig, $ config) if (isset ($ addedConfig ['require'])) $ config ['require'] = $ addedConfig ['require']; $ config = $ this-> toAllVersions ($ config);  return $ config; 

Per effettuare il test pass dobbiamo iterare su ogni elemento dell'array di pacchetti richiesto e impostare la loro versione su "*". Vedi metodo toAllVersion () per ulteriori dettagli. E per accelerare un po 'questo tutorial, abbiamo anche estratto alcuni metodi privati ​​nello stesso passo. Per di qua, parseComoserConf () diventa molto descrittivo e facile da capire. Potremmo anche inline $ config negli argomenti di addNewRequires (), ma per ragioni estetiche l'ho lasciato su due righe.

Ma per quanto riguarda "require-dev" nel composer.json?

function testARquiredDevPackageInComposerWillBeInSatisAlso () $ actual = $ this-> parseComposerConf ('"require-dev": "Mockery / Mockery": "> = 0.7.2", "phpunit / phpunit": "3.7.28" '); $ this-> assertEquals ('*', json_decode ($ actual, true) ['require'] ['Mockery / Mockery']); $ this-> assertEquals ('*', json_decode ($ actual, true) ['require'] ['phpunit / phpunit']); 

Questo ovviamente fallisce. Possiamo farlo passare solo con copia / incolla della nostra condizione in addNewRequires ():

funzione privata addNewRequires ($ addedConfig, $ config) if (isset ($ addedConfig ['require'])) $ config ['require'] = $ addedConfig ['require']; $ config = $ this-> toAllVersions ($ config);  if (isset ($ addedConfig ['require-dev'])) $ config ['require'] = $ addedConfig ['require-dev']; $ config = $ this-> toAllVersions ($ config);  return $ config; 

Sì, questo lo fa passare, ma quelli duplicati se le affermazioni sono cattive. Affrontiamoli con loro.

funzione privata addNewRequires ($ addedConfig, $ config) $ config = $ this-> addRequire ($ addedConfig, 'require', $ config); $ config = $ this-> addRequire ($ addedConfig, 'require-dev', $ config); restituire $ config;  private function addRequire ($ addedConfig, $ string, $ config) if (isset ($ addedConfig [$ string])) $ config ['require'] = $ addedConfig [$ string]; $ config = $ this-> toAllVersions ($ config);  return $ config; 

Possiamo essere di nuovo felici, i test sono ecologici e abbiamo rifattorizzato il nostro codice. Penso che sia stato scritto un solo test. Cosa succede se abbiamo entrambi "richiedere" e "require-dev"sezioni in composer.json?

function testItCanParseComposerJsonWithBothSections () $ actual = $ this-> parseComposerConf ('"require": "Mockery / Mockery": "> = 0.7.2", "require-dev": "phpunit / phpunit": " 3.7.28 " '); $ this-> assertEquals ('*', json_decode ($ actual, true) ['require'] ['Mockery / Mockery']); $ this-> assertEquals ('*', json_decode ($ actual, true) ['require'] ['phpunit / phpunit']); 

Questo fallisce perché i pacchetti impostati da "require-dev"sovrascriverà quelli di"richiedere"e avremo un errore:

Indice indefinito: Mockery / Mockery

Basta aggiungere un segno più per unire gli array, e abbiamo finito.

funzione privata addRequire ($ addedConfig, $ string, $ config) if (isset ($ addedConfig [$ string])) $ config ['require'] + = $ addedConfig [$ string]; $ config = $ this-> toAllVersions ($ config);  return $ config; 

I test stanno passando. La nostra logica è finita. Tutto ciò che restiamo da fare è estrarre i metodi nei propri file e classi. La versione finale dei test e il SatisUpdater la classe può essere trovata nel codice sorgente allegato.

Ora possiamo modificare il nostro script cron per caricare il nostro parser ed eseguirlo sul nostro composer.json. Questo sarà specifico per le cartelle particolari dei tuoi progetti. Ecco un esempio che puoi adattare al tuo sistema.

#! / Usr / local / bin / php parseComposerConf (file_get_contents ($ composerJsonFile)); file_put_contents ($ satisConf, $ conf); system (sprintf ('/ percorso / per / satis / bin / satis build% s% s', $ satisConf, $ outputDir), $ retval); exit ($ retval);

Realizza il tuo progetto Usa lo specchio

Abbiamo parlato di molte cose in questo articolo, ma non abbiamo menzionato come istruiremo il nostro progetto a utilizzare lo specchio anziché Internet. Sai, il valore predefinito è packagist.org? A meno che non facciamo qualcosa del genere:

 "repository": ["type": "compositore", "url": "http: // your-mirror-server: 4680"],

Questo renderà il tuo specchio la prima scelta per il compositore. Ma aggiungendo solo quello nel composer.json del tuo progetto non disabiliterà l'accesso a packagist.org. Se non è possibile trovare un pacchetto sul mirror locale, verrà scaricato da Internet. Se desideri bloccare questa funzione, puoi anche aggiungere la seguente riga alla sezione repository sopra:

"packagist": falso

Pensieri finali

Questo è tutto. Mirror locale, con pacchetti di adattamento e aggiornamento automatici. I tuoi colleghi non dovranno mai aspettare a lungo mentre loro o il server CI installano tutti i requisiti dei tuoi progetti. Divertiti.