Статьи

Настройка локального зеркала для пакетов Composer с Satis

Установка всех ваших библиотек PHP с Composer — отличный способ сэкономить время. Но более крупные проекты, автоматически проверяемые и запускаемые при каждой фиксации в вашей системе контроля версий программного обеспечения (SVC), потребуют много времени для установки всех необходимых пакетов из Интернета. Вы хотите как можно быстрее выполнить свои тесты через систему непрерывной интеграции (CI), чтобы иметь быструю обратную связь и быструю реакцию на сбой. В этом уроке мы настроим локальное зеркало для прокси всех ваших пакетов, необходимых в файле composer.json вашего проекта. Это заставит наш CI работать намного быстрее, установить пакеты в локальной сети или даже на одном и том же компьютере, а также обеспечить постоянную доступность определенных версий пакетов.


Satis — это название приложения, которое мы будем использовать для зеркалирования различных репозиториев для нашего проекта. Он сидит как посредник между Интернетом и вашим композитором. Наше решение создаст локальное зеркало из нескольких пакетов и проинструктирует нашего композитора использовать его вместо источников, найденных в Интернете.

Вот изображение, которое говорит более тысячи слов.

Архитектура

Наш проект будет использовать композитор как обычно. Он будет настроен на использование локального сервера Satis в качестве основного источника. Если пакет найден там, он будет установлен оттуда. Если нет, мы позволим composer использовать пакет по умолчанию packagist.org для получения пакета.


Satis доступен через composer, поэтому его установка очень проста. В прилагаемом архиве исходного кода вы найдете Satis, установленный в папке Sources/Satis . Сначала мы установим сам композитор.

1
2
3
$ curl -sS https://getcomposer.org/installer |
#!/usr/bin/env php All settings correct for using Composer Downloading…
 Composer successfully installed to: /home/csaba/Personal/Programming/NetTuts/Setting up a local mirror for Composer packages with Satis/Sources/Satis/composer.phar Use it: php composer.phar

Затем мы установим Satis.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ php composer.phar create-project composer/satis —stability=dev —keep-vcs Installing composer/satis (dev-master eddb78d52e8f7ea772436f2320d6625e18d5daf5)
 — Installing composer/satis (dev-master master)
   Cloning master
Created project in /home/csaba/Personal/Programming/NetTuts/Setting up a local mirror for Composer packages with Satis/Sources/Satis/satis Loading composer repositories with package information Installing dependencies (including require-dev) from lock file
 — Installing symfony/process (dev-master 27b0fc6)
   Cloning 27b0fc645a557b2fc7bc7735cfb05505de9351be
 
 — Installing symfony/finder (v2.4.0-BETA1)
   Downloading: 100%
 
 — Installing symfony/console (dev-master f44cc6f)
   Cloning f44cc6fabdaa853335d7f54f1b86c99622db518a
 
 — Installing seld/jsonlint (1.1.1)
   Downloading: 100%
 
 — Installing justinrainbow/json-schema (1.1.0)
   Downloading: 100%
 
 — Installing composer/composer (dev-master f8be812)
   Cloning f8be812a496886c84918d6dd1b50db5c16da3cc3
 
 — Installing twig/twig (v1.14.1)
   Downloading: 100%
symfony/console suggests installing symfony/event-dispatcher () Generating autoload files

Satis настроен с помощью файла JSON, очень похожего на composer. Вы можете использовать любое имя для своего файла и указать его для использования позже. Мы будем использовать « mirrored-packages.conf ».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
{
    «name»: «NetTuts Composer Mirror»,
    «homepage»: «http://localhost:4680»,
 
    «repositories»: [
        { «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
}

Давайте проанализируем этот файл конфигурации.

  • name — представляет строку, которая будет отображаться в веб-интерфейсе нашего зеркала.
  • homepage — это веб-адрес, где будут храниться наши посылки. Это не говорит нашему веб-серверу об использовании этого адреса и порта, это скорее просто информация о рабочей конфигурации. Мы настроим доступ к нему по этому адресу и порту позже.
  • repositories — список репозиториев, упорядоченных по предпочтению. В нашем примере первый репозиторий — это Github-форк из библиотек журналов монологов. Он имеет некоторые модификации, и мы хотим использовать этот конкретный форк при установке монолога. Тип этого хранилища — « vcs ». Второй репозиторий имеет тип « composer ». Его URL-адрес является сайтом packagist.org по умолчанию.
  • require — перечисляет пакеты, которые мы хотим отразить Он может представлять определенный пакет с определенной версией или веткой или любой версией в этом отношении. Он использует тот же синтаксис, что и ваш « require » или « require-dev » в вашем composer.json .
  • require-dependencies — это последний вариант в нашем примере. Он скажет Satis отразить не только пакеты, которые мы указали в разделе « require », но и все их зависимости.

Чтобы быстро опробовать наши настройки, сначала нужно сказать Satis создать зеркала. Запустите эту команду в папке, где вы установили Satis.

1
$ php ./satis/bin/satis build ./mirrored-packages.conf ./packages-mirror Scanning packages Writing packages.json Writing web view

Пока идет процесс, вы увидите, как Satis отражает каждую найденную версию требуемых пакетов. Будьте терпеливы, это может занять некоторое время, чтобы собрать все эти пакеты.

Satis требует, чтобы этот date.timezone был указан в файле php.ini , поэтому убедитесь, что он установлен и установлен в ваш местный часовой пояс. В противном случае появится ошибка.

1
2
3
[Twig_Error_Runtime]
  An exception has been thrown during the rendering of a template
(«date_default_timezone_get(): It is not safe to rely on the system’s timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set) function.

Затем мы можем запустить экземпляр PHP-сервера в нашей консоли, указывая на недавно созданный репозиторий. Требуется PHP 5.4 или новее.

1
2
3
$ php -S localhost:4680 -t ./packages-mirror/ PHP 5.4.22-pl0-gentoo Development Server started at Sun Dec 8 14:47:48 2013 Listening on http://localhost:4680 Document root is /home/csaba/Personal/Programming/NetTuts/Setting up a local mirror for Composer packages with Satis/Sources/Satis/packages-mirror Press Ctrl-C to quit.
[Sun Dec 8 14:48:09 2013] 127.0.0.1:56999 [200]: /
[Sun Dec 8 14:48:09 2013] 127.0.0.1:57000 [404]: /favicon.ico — No such file or directory

И теперь мы можем просматривать наши зеркальные пакеты и даже искать конкретные, указав в нашем веб-браузере http://localhost:4680 :

MirrorWebpage

Если у вас под рукой работает Apache, создать виртуальный хост для Satis будет довольно просто.

1
2
3
4
5
6
7
8
9
Listen 4680
 
<Directory «/path/to/your/packages-mirror»> Options -Indexes FollowSymLinks AllowOverride all Order allow,deny Allow from all
</Directory>
 
<VirtualHost *:4680> DocumentRoot «/path/to/your/packages-mirror» ServerName 127.0.0.1:4680 ServerAdmin [email protected]
 ErrorLog syslog:user
 
</VirtualHost>

Мы просто используем такой файл .conf , который помещается в папку Apache conf.d , обычно это /etc/apache2/conf.d . Он создает виртуальный хост на порту 4680 и указывает его на нашу папку. Конечно, вы можете использовать любой порт, который хотите.


Satis не может автоматически обновлять зеркала, если мы не скажем это. Поэтому в любой UNIX-подобной системе проще всего добавить в систему задание cron. Это было бы очень просто, и это просто сценарий для выполнения нашей команды обновления.

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

Недостаток этого решения в том, что оно статично. Мы должны вручную обновлять mirrored-packages.conf каждый раз, когда добавляем еще один пакет в наш проект composer.json . Если вы являетесь частью команды в компании с большим проектом и сервером непрерывной интеграции, вы не можете полагаться на людей, которые не хотят добавлять пакеты на сервер. Они могут даже не иметь прав доступа к инфраструктуре CI.


Пришло время для упражнения PHP TDD. Если вы просто хотите, чтобы ваш код был готов и запущен, ознакомьтесь с исходным кодом, прилагаемым к этому руководству.

1
2
3
4
5
6
require_once __DIR__ .
 class SatisUpdaterTest extends PHPUnit_Framework_TestCase {
    function testBehavior() {
        $this->assertTrue(true);
    }
}

Как обычно, мы начинаем с дегенеративного теста, которого достаточно, чтобы убедиться, что у нас есть работающая среда тестирования. Вы можете заметить, что у меня довольно странно выглядящая строка require_once, потому что я хочу избежать переустановки PHPUnit и Mockery для каждого небольшого проекта. Так что они у меня в папке vendor в NetTuts моего NetTuts . Вы должны просто установить их с помощью composer и вообще удалить строку require_once .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class SatisUpdaterTest extends 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);
    }
}

Это выглядит правильно. Все поля, кроме « require », являются статическими. Нам нужно генерировать только пакеты. Репозитории указывают на наши частные git-клоны и упаковщик по мере необходимости. Управление ими — скорее работа системного администратора, чем разработчика программного обеспечения.

Конечно, это не так с:

1
PHP Fatal error: Call to undefined method SatisUpdaterTest::parseComposerConf()

Исправить это легко.

1
2
private function parseComposerConf($string) {
}

Я просто добавил в наш тестовый класс пустой метод с обязательным именем private. Круто, но теперь у нас есть другая ошибка.

1
PHPUnit_Framework_ExpectationFailedException : Failed asserting that null matches expected ‘{ … }’

Итак, null не соответствует нашей строке, содержащей все эти настройки по умолчанию.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private function 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
}’;
}

ОК, это работает. Все тесты проходят.

1
PHPUnit 3.7.28 by Sebastian Bergmann.

Но мы ввели ужасное дублирование. Весь этот статический текст в двух местах, написанный символ за символом в двух разных местах. Давайте исправим это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SatisUpdaterTest extends 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);
    }
 
    private function parseComposerConf($string) {
        return self::$DEFAULT_CONFIG;
    }
}

Ааа! Так-то лучше.

1
2
3
4
5
6
function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration() {
    $expected = self::$DEFAULT_CONFIG;
 
    $actual = $this->parseComposerConf(‘{«require»: {}}’);
    $this->assertEquals($expected, $actual);
}

Что ж. Это также проходит. Но это также подчеркивает некоторое дублирование и бесполезное назначение.

1
2
3
4
5
6
7
8
function testDefaultConfigFile() {
    $actual = $this->parseComposerConf(»);
    $this->assertEquals(self::$DEFAULT_CONFIG, $actual);
}
 function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration() {
    $actual = $this->parseComposerConf(‘{«require»: {}}’);
    $this->assertEquals(self::$DEFAULT_CONFIG, $actual);
}

Мы указали переменную $expected . $actual также может быть встроен, но мне больше нравится этот способ. Он держит акцент на том, что тестируется.

Теперь у нас есть другая проблема. Следующий тест, который я хочу написать, будет выглядеть так:

1
2
3
4
5
6
7
function testARequiredPackageInComposerWillBeInSatisAlso() {
    $actual = $this->parseComposerConf(
    ‘{«require»: {
        «Mockery/Mockery»: «>=0.7.2»
    }}’);
    $this->assertContains(‘»Mockery/Mockery»: «>=0.7.2″‘, $actual);
}

Но после написания простой реализации мы заметим, что для этого требуются json_decode() и json_encode() . И, конечно, эти функции переформатируют нашу строку, и сопоставление строк будет в лучшем случае трудным. Мы должны сделать шаг назад.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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);
}
 private function jsonRecode($json) {
    return json_encode(json_decode($json, true));
}

Мы изменили наш метод утверждения для сравнения строк JSON, а также перекодировали нашу переменную $actual . ParseComposerConf() также был изменен для использования этого метода. Через мгновение вы увидите, как это помогает нам. Наш следующий тест становится более специфичным для JSON.

1
2
3
4
5
6
7
function testARequiredPackageInComposerWillBeInSatisAlso() {
    $actual = $this->parseComposerConf(
        ‘{«require»: {
            «Mockery/Mockery»: «>=0.7.2»
        }}’);
    $this->assertEquals(‘>=0.7.2’, json_decode($actual, true)[‘require’][‘Mockery/Mockery’]);
}

И сделать этот тест, наряду с остальными тестами, довольно легко, опять же.

1
2
3
4
5
6
7
8
private function parseComposerConf($jsonConfig) {
    $addedConfig = json_decode($jsonConfig, true);
    $config = json_decode(self::$DEFAULT_CONFIG, true);
    if (isset($addedConfig[‘require’])) {
        $config[‘require’] = $addedConfig[‘require’];
    }
    return json_encode($config);
}

Мы берем входную строку JSON, декодируем ее, и если она содержит поле « require », мы используем его в нашем конфигурационном файле Satis. Но мы можем захотеть отразить все версии пакета, а не только последнюю. Поэтому, возможно, мы захотим изменить наш тест, чтобы проверить, что в Satis версия «*», независимо от того, какая именно версия находится в composer.json .

1
2
3
4
5
6
7
function testARequiredPackageInComposerWillBeInSatisAlso() {
    $actual = $this->parseComposerConf(
        ‘{«require»: {
            «Mockery/Mockery»: «>=0.7.2»
        }}’);
    $this->assertEquals(‘*’, json_decode($actual, true)[‘require’][‘Mockery/Mockery’]);
}

Это, очевидно, не удается с классным сообщением:

1
PHPUnit_Framework_ExpectationFailedException : Failed asserting that two strings are equal.

Теперь нам нужно отредактировать наш JSON, прежде чем перекодировать его.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private function parseComposerConf($jsonConfig) {
    $addedConfig = json_decode($jsonConfig, true);
    $config = json_decode(self::$DEFAULT_CONFIG, true);
    $config = $this->addNewRequires($addedConfig, $config);
    return json_encode($config);
}
 private function toAllVersions($config) {
    foreach ($config[‘require’] as $package => $version) {
        $config[‘require’][$package] = ‘*’;
    }
    return $config;
}
 private function addNewRequires($addedConfig, $config) {
    if (isset($addedConfig[‘require’])) {
        $config[‘require’] = $addedConfig[‘require’];
        $config = $this->toAllVersions($config);
    }
    return $config;
}

Чтобы пройти тест, мы должны выполнить итерацию по каждому элементу требуемого массива пакетов и установить для их версии значение *. Смотрите метод toAllVersion() для более подробной информации. И чтобы немного ускорить этот урок, мы также извлекли некоторые частные методы на том же шаге. Таким образом, parseComoserConf() становится очень наглядным и простым для понимания. Мы могли бы также вставить $config в аргументы addNewRequires() , но по эстетическим соображениям я оставил его в двух строках.

Но как насчет » require-dev » в composer.json ?

1
2
3
4
5
6
7
8
9
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’]);
}

Это, очевидно, не удается. Мы можем сделать это, просто скопировав / вставив условие if в addNewRequires() :

01
02
03
04
05
06
07
08
09
10
11
private function 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;
}

Да, это проходит мимо, но они дублируются, если заявления выглядят неприятно. Давайте разберемся с ними.

01
02
03
04
05
06
07
08
09
10
11
12
private function addNewRequires($addedConfig, $config) {
    $config = $this->addRequire($addedConfig, ‘require’, $config);
    $config = $this->addRequire($addedConfig, ‘require-dev’, $config);
    return $config;
}
 private function addRequire($addedConfig, $string, $config) {
    if (isset($addedConfig[$string])) {
        $config[‘require’] = $addedConfig[$string];
        $config = $this->toAllVersions($config);
    }
    return $config;
}

Мы снова можем быть счастливы, тесты зеленые, и мы реорганизовали наш код. Я думаю, что осталось написать только один тест. Что если в composer.json разделы « require » и « require-dev »?

01
02
03
04
05
06
07
08
09
10
11
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’]);
}

Это терпит неудачу, потому что пакеты, установленные » require-dev «, будут перезаписывать пакеты » require » и у нас будет ошибка

1
Undefined index: Mockery/Mockery

Просто добавьте знак плюс, чтобы объединить массивы, и все готово.

1
2
3
4
5
6
7
private function addRequire($addedConfig, $string, $config) {
    if (isset($addedConfig[$string])) {
        $config[‘require’] += $addedConfig[$string];
        $config = $this->toAllVersions($config);
    }
    return $config;
}

Тесты проходят. Наша логика закончена. Все, что нам осталось сделать, — это извлечь методы в их собственный файл и класс. Окончательную версию тестов и класс SatisUpdater можно найти в прилагаемом исходном коде.

Теперь мы можем изменить наш скрипт cron, чтобы загрузить наш парсер и запустить его на нашем composer.json . Это будет характерно для конкретных папок ваших проектов. Вот пример, который вы можете адаптировать к вашей системе.

01
02
03
04
05
06
07
08
09
10
11
#!/usr/local/bin/php
 
<?php require_once __DIR__ .
 
$outputDir = ‘/path/to/your/packages-mirror’;
$composerJsonFile = ‘/path/to/your/projects/composer.json’;
$satisConf = ‘/path/to/your/mirrored-packages.conf’;
 
$satisUpdater = new SatisUpdater();
$conf = $satisUpdater->parseComposerConf(file_get_contents($composerJsonFile));
 system(sprintf(‘/path/to/satis/bin/satis build %s %s’, $satisConf, $outputDir), $retval);

В этой статье мы говорили о многих вещах, но не упомянули, как мы проинструктируем наш проект использовать зеркало вместо Интернета. Вы знаете, по умолчанию это packagist.org? Если мы не сделаем что-то вроде этого:

1
2
3
4
5
6
«repositories»: [
       {
           «type»: «composer»,
           «url»: «http://your-mirror-server:4680»
       }
   ],

Это сделает ваше зеркало первым выбором для композитора. Но добавление только этого в composer.json вашего проекта не заблокирует доступ к packagist.org. Если пакет не может быть найден на локальном зеркале, он будет загружен из Интернета. Если вы хотите заблокировать эту функцию, вы также можете добавить следующую строку в раздел репозитории выше:

1
«packagist»: false

Вот и все. Локальное зеркало, с автоматической адаптацией и обновлением пакетов. Вашим коллегам никогда не придется долго ждать, пока они или сервер CI установят все требования ваших проектов. Веселиться.