Configuración de un espejo local para paquetes de compositores con Satis

Instalar todas tus bibliotecas PHP con Composer es una excelente manera de ahorrar tiempo. Pero los proyectos más grandes que se prueban y ejecutan automáticamente en cada compromiso con su sistema de control de versión de software (SVC) tomarán mucho tiempo para instalar todos los paquetes requeridos desde Internet. Desea ejecutar sus pruebas lo antes posible a través de su sistema de integración continua (CI) para que tenga una respuesta rápida y reacciones rápidas en caso de fallo. En este tutorial, configuraremos una réplica local para representar a todos sus paquetes requeridos en los proyectos de su proyecto. compositor.json expediente. Esto hará que nuestro CI trabaje mucho más rápido, instale los paquetes a través de la red local o incluso alojados en la misma máquina, y asegúrese de tener las versiones específicas de los paquetes siempre disponibles..


Lo que es satisfactorio?

Satis es el nombre de la aplicación que utilizaremos para reflejar los distintos repositorios de nuestro proyecto. Se asienta como un proxy entre Internet y su compositor. Nuestra solución creará un espejo local de algunos paquetes e instruirá a nuestro compositor para que lo use en lugar de las fuentes que se encuentran en Internet..

Aquí hay una imagen que dice más de mil palabras..


Nuestro proyecto utilizará compositor como de costumbre. Se configurará para utilizar el servidor local de Satis como la fuente principal. Si se encuentra un paquete allí, se instalará desde allí. Si no es así, dejaremos que el compositor use el packagist.org predeterminado para recuperar el paquete.


Obteniendo satisfacción

Satis está disponible a través del compositor, por lo que su instalación es muy simple. En el archivo de código fuente adjunto, encontrará Satis instalado en el Fuentes / Satis carpeta. Primero instalaremos el propio compositor..

$ curl -sS https://getcomposer.org/installer | php #! / usr / bin / env php Todas las configuraciones correctas para usar Composer Downloading ... Composer se instaló correctamente en: / home / csaba / Personal / Programming / NetTuts / Configuración de un espejo local para los paquetes de Composer con Satis / Sources / Satis / composer .phar Úsalo: php composer.phar

Luego instalaremos Satis..

$ php composer.phar create-project composer / satis --stability = dev --keep-vcs Instalación de composer / satis (dev-master eddb78d52e8f7ea772436f2320d6625e18d5daf5) - Instalación de composer / satis (dev-master master) Master de clonación Proyecto creado en / home / csaba / Personal / Programming / NetTuts / Configuración de un espejo local para los paquetes de Composer con Satis / Sources / Satis / satis Carga de repositorios del compositor con información del paquete Instalación de dependencias (incluido require-dev) del archivo de bloqueo - Instalación de symfony / process (dev-master 27b0fc6) Clonación 27b0fc645a557b2f2bc7735cfb05505de9351be - Instalación de symfony / finder / json-schema (1.1.0) Descargando: 100% - Instalando compositor / compositor (dev-master f8be812) Clonando f8be812a496886c84918d6dd1b50db5c16da3cc3 - Instalando ramita / ramita (v1.14.1) Descargando: 100% symfony / console sugiere instalar symfony / event-dispatcher () Generando archivos de carga automática

Configurando Satis

Satis es configurado por un archivo JSON muy similar al compositor. Puede usar el nombre que desee para su archivo y especificarlo para su uso más adelante. Usaremos "mirrored-packages.conf".

"nombre": "NetTuts Composer Mirror", "página de inicio": "http: // localhost: 4680", "repositorios": ["type": "vcs", "url": "https: // github. com / SynetoNet / monolog ", " type ":" composer "," url ":" https://packagist.org "]," require ": " monolog / monolog ":" syneto-dev ", "mockery / mockery": "*", "phpunit / phpunit": "*", "require-dependencies": true

Analicemos este archivo de configuración..

  • nombre - representa una cadena que se mostrará en la interfaz web de nuestro espejo.
  • página principal - Es la dirección web donde se guardarán nuestros paquetes. Esto no le dice a nuestro servidor web que use esa dirección y puerto, es más bien información de una configuración que funciona. Configuraremos el acceso a él en esa dirección y puerto más adelante..
  • repositorios - Una lista de repositorios ordenados por preferencia. En nuestro ejemplo, el primer repositorio es una bifurcación Github de las bibliotecas de registro monolog. Tiene algunas modificaciones y queremos usar ese fork específico al instalar monolog. El tipo de este repositorio es "vcs". El segundo repositorio es de tipo"compositor". Su URL es el sitio predeterminado de packagist.org.
  • exigir - Enumera los paquetes que queremos reflejar. Puede representar un paquete específico con una versión o rama específica, o cualquier versión para esa materia. Utiliza la misma sintaxis que tu "exigir"o"require-dev" en tus compositor.json.
  • dependencias requeridas - Es la opción final en nuestro ejemplo. Le dirá a Satis que refleje no solo los paquetes especificados en "exigir"sección pero también todas sus dependencias.

Para probar rápidamente nuestra configuración, primero debemos decirle a Satis que cree los espejos. Ejecute este comando en la carpeta donde instaló Satis.

$ php ./satis/bin/satis build ./mirrored-packages.conf ./packages-mirror Paquetes de escaneo Escribiendo paquetes.json Escribiendo vista web

Mientras se lleva a cabo el proceso, verá cómo Satis duplica cada versión encontrada de los paquetes requeridos. Sea paciente, puede llevar un tiempo construir todos esos paquetes..

Satis requiere que date.timezone para ser especificado en el php.ini archivo, así que asegúrese de que sea así y configúrelo en su zona horaria local. De lo contrario aparecerá un error..

[Twig_Error_Runtime] Se ha lanzado una excepción durante la representación de una plantilla ("date_default_timezone_get (): no es seguro confiar en la configuración de zona horaria del sistema. Es necesario * utilizar * la configuración de date.timezone o la función date_default_timezone_set).

Luego podemos ejecutar una instancia de servidor PHP en nuestra consola que apunta al repositorio creado recientemente. Se requiere PHP 5.4 o más nuevo.

$ php -S localhost: 4680 -t ./packages-mirror/ PHP 5.4.22-pl0-gentoo Development Server se inició en Sun Dec 8 14:47:48 2013 Cómo escuchar en http: // localhost: 4680 La raíz del documento es / home / csaba / Personal / Programming / NetTuts / Configuración de un espejo local para los paquetes de Composer con Satis / Sources / Satis / packages-mirror Presione Ctrl-C para salir. [Dom 8 de diciembre 14:48:09 2013] 127.0.0.1:56999 [200]: / [Dom 8 de diciembre 14:48:09 2013] 127.0.0.1:57000 [404]: /favicon.ico - No hay tal archivo o directorio

Y ahora podemos navegar por nuestros paquetes duplicados e incluso buscar paquetes específicos apuntando nuestro navegador web a http: // localhost: 4680:



Vamos a alojarlo en Apache

Si tiene un Apache en ejecución, crear un host virtual para Satis será bastante simple.

Escuchar 4680  Opciones -Indexes FollowSymLinks AllowOverride all Order allow, deny Allow from all   DocumentRoot "/ path / to / your / packages-mirror" ServerName 127.0.0.1:4680 ServerAdmin [email protected] ErrorLog syslog: usuario 

Solo usamos un .conf archivo como este, poner en Apache conf.d carpeta, generalmente /etc/apache2/conf.d. Crea un host virtual en el puerto 4680 y lo apunta a nuestra carpeta. Por supuesto que puedes usar el puerto que quieras.


Actualizando nuestros espejos

Satis no puede actualizar automáticamente los espejos a menos que se lo digamos. Así que la forma más fácil, en cualquier sistema similar a UNIX, es simplemente agregar un trabajo cron a su sistema. Eso sería muy fácil, y solo un simple script para ejecutar nuestro comando de actualización.

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

El inconveniente de esta solución es que es estática. Tenemos que actualizar manualmente el mirrored-packages.conf Cada vez que agregamos otro paquete a nuestro proyecto. compositor.json. Si forma parte de un equipo en una empresa con un gran proyecto y un servidor de integración continua, no puede confiar en que las personas recuerden agregar los paquetes en el servidor. Es posible que ni siquiera tengan permisos para acceder a la infraestructura de CI..


Actualizando dinámicamente la configuración de satisfacción

Es hora de un ejercicio PHP TDD. Si solo desea que su código esté listo y en funcionamiento, consulte el código fuente adjunto a este tutorial.

require_once __DIR__. '/… /… /… /… /Vendor/autoload.php'; la clase SatisUpdaterTest extiende PHPUnit_Framework_TestCase function testBehavior () $ this-> assertTrue (true); 

Como de costumbre, comenzamos con una prueba degenerativa, lo suficiente para asegurarnos de que tenemos un marco de trabajo que funcione. Puede notar que tengo una línea de requerimiento una vez bastante extraña, esto es porque quiero evitar tener que reinstalar PHPUnit y Mockery para cada proyecto pequeño. Así que los tengo en una vendedor carpeta en mi NetTutsla raíz Solo debes instalarlos con el compositor y soltar el requerir una vez línea en conjunto.

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

Eso se ve bien. Todos los campos excepto "exigir"son estáticos. Necesitamos generar solo los paquetes. Los repositorios están apuntando a nuestros clones git privados y a packagist según sea necesario. Administrarlos es más un trabajo de administrador de sistemas que un desarrollador de software..

Por supuesto esto falla con:

Error fatal de PHP: llamada a un método indefinido SatisUpdaterTest :: parseComposerConf ()

Arreglar eso es fácil..

función privada parseComposerConf ($ string) 

Acabo de agregar un método vacío con el nombre requerido, como privado, a nuestra clase de prueba. Genial, pero ahora tenemos otro error..

PHPUnit_Framework_ExpectationFailedException: Falló al afirmar que las coincidencias nulas esperadas '…'

Entonces, null no coincide con nuestra cadena que contiene toda la configuración predeterminada.

función privada parseComposerConf ($ string) return '"name": "NetTuts Composer Mirror", "homepage": "http: // localhost: 4680", "repositories": ["type": "vcs", " url ":" https://github.com/SynetoNet/monolog ", " type ":" composer "," url ":" https://packagist.org "]," require ": , "require-dependencies": true '; 

Ok eso funciona Todas las pruebas estan pasando.

PHPUnit 3.7.28 por Sebastian Bergmann. Tiempo: 15 ms, Memoria: 2.50Mb OK (1 prueba, 1 aserción)

Pero introdujimos una horrible duplicación. Todo ese texto estático en dos lugares, escrito carácter por carácter en dos lugares diferentes. Vamos a arreglarlo:

la clase SatisUpdaterTest extiende PHPUnit_Framework_TestCase static $ DEFAULT_CONFIG = '"name": "NetTuts Composer Mirror", "homepage": "http: // localhost: 4680", "repositories": ["type": "vcs", " url ":" https://github.com/SynetoNet/monolog ", " type ":" composer "," url ":" https://packagist.org "]," require ": , "require-dependencies": true '; function testDefaultConfigFile () $ expected = self :: $ DEFAULT_CONFIG; $ actual = $ this-> parseComposerConf ("); $ this-> assertEquals ($ expected, $ actual); función privada parseComposerConf ($ string) return self :: $ DEFAULT_CONFIG;

Ahhh Eso es mejor.

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

Bien. Eso también pasa. Pero también destaca alguna duplicación y asignación inútil..

 function testDefaultConfigFile () $ actual = $ this-> parseComposerConf ("); $ this-> assertEquals (self :: $ DEFAULT_CONFIG, $ actual); function testEmptyRequiredpermos en los últimos casos en los últimos casos en los que se encuentran en este país. "require":  '); $ this-> assertEquals (self :: $ DEFAULT_CONFIG, $ actual);

Nosotros inline el $ esperado variable. $ actual También podría estar en línea, pero me gusta más así. Mantiene el foco en lo que se prueba..

Ahora tenemos otro problema. La próxima prueba que quiero escribir se vería así:

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

Pero después de escribir la implementación simple, notaremos que requiere json_decode () y json_encode (). Y, por supuesto, estas funciones reformatear nuestra cadena y las cadenas coincidentes serán difíciles en el mejor de los casos. Tenemos que dar un paso atrás..

function testDefaultConfigFile () $ actual = $ this-> parseComposerConf ("); $ this-> assertJsonStringEqualsJsonString ($ this-> jsonRecode.) this-> parseComposerConf ('"require": '); $ this-> assertJsonStringEqualsJsonString ($ this-> jsonRecode (self :: $ DEFAULT_CONFIG), $ actual); función privada parseComposerConf ($ jsonConfig) return $ this-> jsonRecode (self :: $ DEFAULT_CONFIG); función privada jsonRecode ($ json) return json_encode (json_decode ($ json, true));

Cambiamos nuestro método de afirmación para comparar cadenas JSON y también recodificamos nuestro $ actual variable. ParseComposerConf () También se modificó para utilizar este método. Verás en un momento cómo nos ayuda. Nuestra próxima prueba se vuelve más específica para JSON.

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

Y hacer que esta prueba sea aprobada, junto con el resto de las pruebas, es bastante fácil, una vez más.

función privada parseComposerConf ($ jsonConfig) $ addedConfig = json_decode ($ jsonConfig, true); $ config = json_decode (self :: $ DEFAULT_CONFIG, true); if (isset ($ addedConfig ['require'])) $ config ['require'] = $ addedConfig ['require'];  devolver json_encode ($ config); 

Tomamos la cadena JSON de entrada, la decodificamos, y si contiene una "exigir"lo utilizamos en nuestro archivo de configuración Satis. Sin embargo, es posible que deseamos reflejar todas las versiones de un paquete, no solo la última. Por lo tanto, tal vez queramos modificar nuestra prueba para verificar que la versión esté" * "en Satis, independientemente de la versión exacta en compositor.json.

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

Eso obviamente falla con un mensaje genial:

PHPUnit_Framework_ExpectationFailedException: Error al afirmar que dos cadenas son iguales. Esperado: * real:> = 0.7.2

Ahora, necesitamos editar nuestro JSON antes de volver a codificarlo.

función privada parseComposerConf ($ jsonConfig) $ addedConfig = json_decode ($ jsonConfig, true); $ config = json_decode (self :: $ DEFAULT_CONFIG, true); $ config = $ this-> addNewRequires ($ addedConfig, $ config); devuelve json_encode ($ config);  función privada a AllVersions ($ config) foreach ($ config ['require'] como $ package => $ version) $ config ['require'] [$ package] = '*';  devuelve $ config;  función privada addNewRequires ($ addedConfig, $ config) if (isset ($ addedConfig ['require'])) $ config ['require'] = $ addedConfig ['require']; $ config = $ this-> toAllVersions ($ config);  devuelve $ config; 

Para hacer que la prueba pase, tenemos que iterar sobre cada elemento de la matriz de paquetes necesarios y establecer su versión en '*'. Ver metodo toAllVersion () para más detalles. Y para acelerar un poco este tutorial, también extrajimos algunos métodos privados en el mismo paso. De esta manera, parseComoserConf () Se vuelve muy descriptivo y fácil de entender. También podríamos en línea $ config en los argumentos de addNewRequires (), Pero por razones estéticas lo dejé en dos líneas..

Pero que pasa "require-deven compositor.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']); 

Eso obviamente falla. Podemos hacerlo pasar con solo copiar / pegar nuestra condición if en addNewRequires ():

función privada 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);  devuelve $ config; 

Sí, eso lo hace pasar, pero esos duplicados si las declaraciones son desagradables. Vamos a tratar con ellos.

función privada addNewRequires ($ addedConfig, $ config) $ config = $ this-> addRequire ($ addedConfig, 'require', $ config); $ config = $ this-> addRequire ($ addedConfig, 'require-dev', $ config); devuelve $ config;  función privada addRequire ($ addedConfig, $ string, $ config) if (isset ($ addedConfig [$ string])) $ config ['require'] = $ addedConfig [$ string]; $ config = $ this-> toAllVersions ($ config);  devuelve $ config; 

Podemos volver a ser felices, las pruebas son verdes y hemos refactorizado nuestro código. Creo que solo queda una prueba por escribir. ¿Y si tenemos ambos?exigir"y"require-dev"secciones en compositor.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']); 

Eso falla porque los paquetes establecidos por "require-dev"sobrescribirá los de"exigir"y tendremos un error:

Índice indefinido: Mockery / Mockery

Simplemente agregue un signo más para combinar los arreglos, y listo..

función privada addRequire ($ addedConfig, $ string, $ config) if (isset ($ addedConfig [$ string])) $ config ['require'] + = $ addedConfig [$ string]; $ config = $ this-> toAllVersions ($ config);  devuelve $ config; 

Las pruebas están pasando. Nuestra lógica está terminada. Todo lo que nos queda por hacer es extraer los métodos en su propio archivo y clase. La versión final de las pruebas y el SatisUpdater clase se puede encontrar en el código fuente adjunto.

Ahora podemos modificar nuestro script cron para cargar nuestro analizador y ejecutarlo en nuestro compositor.json. Esto será específico para las carpetas particulares de sus proyectos. Aquí hay un ejemplo que puedes adaptar a tu sistema..

#! / usr / local / bin / php parseComposerConf (file_get_contents ($ composerJsonFile)); file_put_contents ($ satisConf, $ conf); sistema (sprintf ('/ path / to / satis / bin / satis compilación% s% s', $ satisConf, $ outputDir), $ retval); salida ($ retval);

Haciendo que tu proyecto use el espejo

Hablamos de muchas cosas en este artículo, pero no mencionamos cómo instruiremos a nuestro proyecto para que use el espejo en lugar de Internet. Ya sabes, el valor predeterminado es packagist.org? A menos que hagamos algo como esto:

 "repositorios": ["tipo": "compositor", "url": "http: // su-servidor-espejo: 4680"],

Eso hará que tu espejo sea la primera elección para el compositor. Pero añadiendo eso a la compositor.json de su proyecto no deshabilitará el acceso a packagist.org. Si no se puede encontrar un paquete en el espejo local, se descargará de Internet. Si desea bloquear esta función, también puede agregar la siguiente línea a la sección de repositorios anterior:

"packagist": falso

Pensamientos finales

Eso es. Espejo local, con adaptación automática y actualización de paquetes. Sus colegas nunca tendrán que esperar mucho tiempo mientras ellos o el servidor de CI instalan todos los requisitos de sus proyectos. Que te diviertas.