Управление своими модулями для CMS c использованием composer +12


Здравствуйте, друзья! Появление популярного менеджера зависимостей в PHP — является одним из важнейших событий в жизни PHP-сообщества. Composer разделил время на "до" — когда наши самописные библиотеки лежали в zip-архивах или просто разбросаны по папкам и копировались "ручками", лишь в единичных случаях, использовался git или PEAR, и "после" — когда мы начали использовать composer. Но что делать если вы работаете с определенной CMS, а она не "знает" что такое composer?


Предисловие


Я работаю тимлидом в некрупной web-студии и около года назад мне пришлось пересмотреть подход к использованию наработок в моей команде. Для разработки проектов, более 70% функционала которых можно назвать стандартным (сайт, интернет-магазин), мы используем 1C-Битрикс. Как и все популярные CMS 1C-Битрикс имеет модульную структуру и поддерживает установку модулей сторонних разработчиков. Звучит хорошо, но на деле всё немного иначе. Вы можете разрабатывать свои модули и хранить их в архиве копируя исходные коды в нужные папки и устанавливая модули по-одному в панеле управления, а можете публиковать модули на предусмотренной для этого площадке “1С-Битрикс: Маркетплейс”. Для публикации модулей вам нужно всего-лишь заключить партнерский договор и подписать соглашение, в котором будет описана ваша ответственность. И ни о каких зависимостях модулей и установки цепочки зависимостей речи не идет.


Данная проблема в той или иной степени присуща всем CMS разработанным “до”. Она демотивирует разрабатывать мелкие модули (нам кажется что не стоит “этот простой функционал” оборачивать в модуль), и требует отдельного ведения документации.


В этой статье я расскажу как организовать хранение и использование наработок в команде/компании. Это будет интересно тем, кто сталкивался с подобной проблемой, тем, кто разрабатывает сайты с использованием CMS, тем кто разрабатывает свою CMS и тем кто просто разрабатывает проекты на PHP. Поехали!


Часть первая. Публичное размещение модулей


Первая задача, которую нужно решить, это где хранить модули. Исходные коды модулей можно хранить в любом git, mercurial или svn репозитории. Для публичных модулей я рекомендую GitHub. На GitHub у вас будет возможность легко просматривать исходные коды и поддерживать документацию в формате Markdown. Для использования composer Вам необходимо создать и наполнить информацией файл composer.json. Это можно сделать в редакторе, в вашей IDE или вызовом команды composer init. Здесь и далее я не буду углубляться в базовые возможности и команды composer, если Вы плохо знакомы с composer, прочитайте эту статью.


После того как Вы создали свой модуль (для примера пока пустой) и разместили его код на GitHub, Вам необходимо зарегистрировать модуль на сайте packagist.org. Packagist предложит Вам настроить хуки GitHub так, чтобы при попадании изменений в репозиторий, на packagist.org обновлялись сведения о модуле.


Часть вторая. Установка. Самое интересное тут


Вы, точно, очень хорошо знакомы с той CMS с которой работаете, а значит знаете все тонкости установки модулей в ней. В 1С-Битрикс установка модулей проходит в 2 этапа:


  • размещение исходного кода модуля в определенной директории <project_dir>/local/modules/<company_name>.<mod_mame>
  • Вызов функции RegisterModule(<company_name>.<mod_mame>). Как правило все действия установки модуля описываются в методе DoInstall класса, отвечающего за установку и удаления модуля. <project_dir>/local/modules/<company_name>.<mod_mame>/install/index.php

Часть два один. Прячем пакеты в надежное место


По-умолчанию composer устанавливает все пакеты в каталог <project_dir>/vendor если композер размещен в корне проекта, и не выполняет никаких хуков в Ваших пакетах. Но это легко изменить.


Нам необходимо в корне проекта разместить файл composer.json:


{
    "name": "sites/<sitename>",
    "description": "<SiteName>",
    "authors": [
        {
            "name": "<developerName>",
            "email": "<developerEmail>"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "config": {
        "vendor-dir": "local/vendor"
    }
}

В 1C-Битрикс код, написанный разработчиками, обычно, размещают в директории <project_dir>/local. Поэтому мы перенесли туда папку vendor записью в секции config. Теперь все пакеты сторонних разработчиков будут размещаться там. Но наши модули необходимо размещать в директории <project_dir>/local/modules/<company_name>.<mod_mame>, что-же делать?


Часть два два. Плагин установки модулей


У composer есть несколько типов пакетов, один из которых composer-plugin — это расширения для самого композера. Чтобы наши модули устанавливались так, как этого требует CMS нам нужно написать свой плагин. Для этого создаем отдельный проект и в его корне размещаем composer.json:


{
  "name": "<my_name>/installer",
  "description": "Plugin for custom installing",
  "type": "composer-plugin",
  "license": "MIT",
  "homepage": "<link to homepage github>",
  "version": "0.0.1",
  "authors": [
    {
      "name": "<name>",
      "email": "<email>"
    }
  ],
  "require": {
    "composer-plugin-api": "^1.0"
  },
  "require-dev": {
    "composer/composer": "^1.0"
  },
  "autoload": {
    "psr-4": { "<my_name>\\installer\\": "" }
  },
  "extra": {
    "class": "<my_name>\\installer\\Plugin"
  }
}

В этом файле есть 3 ключевых момента:


  • "type": "composer-plugin" — говорит композеру, что это плагин
  • autoload — описывает правила автозагрузки классов
  • extra — указывает какой класс является плагином

Плагин будет состоять из двух классов:


  • непосредственно сам плагин. Он будет добавлять в composer собственный инсталлятор
  • инсталлятор, который и будет заниматься установкой модулей

Плагин выполняет просто добавление инсталлятора (файл: Plugin.php)


namespace company_name\installer;

use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;

class Plugin implements PluginInterface
{
    public function activate(Composer $composer, IOInterface $io)
    {
        $composer->getInstallationManager()->addInstaller(new Bitrix($io, $composer));
    }
}

Далее сам инсталятор (класс company_name\installer\Bitrix). Класс должен наследоваться от Composer\Installer\LibraryInstaller и содержать следующие методы:


  • supports — возвращает true если инсталлятор поддерживает данный тип пакетов
  • getInstallPath — возвращает путь, по которому необходимо разместить исходный код пакета
  • install/uninstall/update — хуки установки/удаления/обновления пакета

Все наши модули будут иметь тип bitrix-module и именно с ними должен работать инсталятор.


public function supports($packageType)
{
    return $packageType === 'bitrix-module';
}

Я решил сохранить целостность имени модуля (оно состоит из company_name и mod_name разделенных точкой) и именую пакеты <my_name>/<company_name>.<mod_mame> или <company_name>/<company_name>.<mod_mame>. Если мы возьмем имя пакета и разобьем по слешу, то вторая часть и будет именем модуля


public function getInstallPath(PackageInterface $package)
{
    $name = explode("/", $package->getName());
    return "local/modules/{$name[1]}/";
}

Методы initBitrix и getModule реализуют работу с API 1C-Битрикс для установки модуля. Метод update реализуется исходя из того, какая у Вас CMS и как вы выпускаете обновления модулей и как планируете их выполнять (файл: Bitrix.php).


namespace company_name\installer;

use Composer\Installer\LibraryInstaller;
use Composer\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface;

class Bitrix extends LibraryInstaller
{

    public function supports($packageType)
    {
        return $packageType === 'bitrix-module';
    }

    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
    {
        parent::install($repo, $package);
        $name = explode("/", $package->getName());
        $this->initBitrix();
        $module = $this->getModule($name[1]);
        $module->DoInstall();
    }

    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
    {
        $name = explode("/", $package->getName());
        $this->initBitrix();
        $module = $this->getModule($name[1]);
        $module->DoUninstall();
        parent::uninstall($repo, $package);
    }

    public function getInstallPath(PackageInterface $package)
    {
        $name = explode("/", $package->getName());
        return "local/modules/{$name[1]}/";
    }

    protected function initBitrix()
    {
        $_SERVER['DOCUMENT_ROOT'] = __DIR__ . "/../../../../";
        define('STOP_STATISTICS', true);
        define("NO_KEEP_STATISTIC", "Y");
        define("NO_AGENT_STATISTIC","Y");
        define("NOT_CHECK_PERMISSIONS", true);
        require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
        $GLOBALS['APPLICATION']->RestartBuffer();
    }

    protected function getModule($module)
    {
        include_once $_SERVER['DOCUMENT_ROOT'] . "/local/modules/" . $module . "/install/index.php";
        $class = str_replace(".", "_", $module);
        $module = new $class();
        return $module;
    }

}

После того, как вы проверили работу плагина, код можно заливать на GitHub и регистрировать в Packagist.


Часть два три. Модуль


Вернемся к самому модулю, который мы упомянули в первой части. а точнее к его composer.json.


{
    "name": "<my_name>/<company_name>.<mod_mame>",
    "type": "bitrix-module",
    "description": "Описание самого модуля",
    "version": "1.0.0",
    "time": "11.09.2018",
    "minimum-stability": "dev",
    "license": "MIT",
    "homepage": "<link to homepage github>",
    "authors": [
        {
            "name": "<name>",
            "email": "<email>"
        }
    ],
    "require": {
        "<my_name>/installer": "*"
    }
}

Имя модуля должно соответствовать требованиям CMS, тип должен быть указан тот, с которым работает инсталлятор (в нашем случае bitrix-module) и в зависимостях у модуля должен быть плагин (секция require). После создания самого модуля и проверки его работы заливаем его код на GitHub и регистрируем в Packagist.


Часть два четыре. использование


Напомню, что сам проект (сайт) имеет примерно следующий composer.json


{
    "name": "sites/<sitename>",
    "description": "<SiteName>",
    "authors": [
        {
            "name": "<developerName>",
            "email": "<developerEmail>"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "config": {
        "vendor-dir": "local/vendor"
    }
}

теперь мы можем перечислить в секции require все необходимые нам модули или вызвать команду


composer require "<my_name>/<company_name>.<mod_mame>" "*"

В полной мере ощутить полезность проделанной работы можно, если Вам предстоит, например, добавить в проект модуль авторизации с отправкой пароля в SMS


image


Сам модуль содержит код, который отвечает за логику авторизации, включать в него код отправки SMS не стоит, ведь отправку SMS выполняют и другие модули, например модуль уведомлений, значит SMS лучше сделать отдельным модулем, чтобы его код не дублировать. Так же и REST сервис. Его тоже могут использовать другие модули. И при всей этой непростой схеме, когда ваш модуль тянет за собой еще четыре, его установка остаётся всё такой же простой. Просто выполните одну команду:


composer require "<my_name>/<company_name>.<mod_mame>" "*"

А что и в какой последовательности скачивать и устанавливать позвольте решить композеру.


Часть третья. Приватные модули


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


  • GitLab — это аналог GitHub который Вы можете скачать и установить на свои сервера.
  • Satis — это генератор репозитория, с которым сможет работать композер.

Для начала установите GitLab и перенесите в него исходные коды Ваших модулей. после установите Satis и опишите все ваши модули в satis.json


{
    "name": "ropoName",
    "homepage": "https://composer.<company_name>.ru/",
    "repositories": [
        { 
            "type": "vcs",  
            "url": "https://gitlab.<company_name>.ru/<my_name>/installer"
        },
        { 
            "type": "vcs",  
            "url": "https://gitlab.<company_name>.ru/<my_name>/<company_name>.<mod_name>"
        }
    ],
    "config": {
        "gitlab-domains": [
            "gitlab.<company_name>.ru"
        ],
        "gitlab-token": {
            "gitlab.<company_name>.ru": "GitLab Token"
        }
    },
    "require-all": true
}

В GitLab нужно создать токен, которому будет доступно api и указать его в satis.json. После всех этих манипуляций выполните команду:


php bin/satis build satis.json ./web

И в папке web получите статический репозиторий, который можно опубликовать по адресу https://composer.<company_name>.ru/.


composer.json сайта будет отличатся лишь тем, что в нем будет присутствовать секция repositories


{
    "name": "sites/<sitename>",
    "description": "<SiteName>",
    "authors": [
        {
            "name": "<developerName>",
            "email": "<developerEmail>"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "config": {
        "vendor-dir": "local/vendor"
    },
    "repositories": [
        {
            "type": "composer",
            "url": "https://composer.<company_name>.ru/"
        }
    ]
}

Послесловие


Друзья, я очень надеюсь, что эта статья оказалась для Вас полезной. И, не важно, какой CMS Вы пользуетесь, Вы сможете грамотно организовать хранение Вашего кода, будете строго его документировать, сможете разделить "толстые" модули на множество "тонких" оформив зависимости, и перестанете испытывать сложности с установкой или обновлением модулей. Удачи.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (10):

  1. Jouretz
    /#19338726

    Вот так с помощью десятка костылей мы можем подружить битрикс с технологиями 2018 года.
    Мне интересно, они отдел разработки полностью сократили и оставили только отдел маркетинга?
    Система требующая выставлять mbstring.func_overload = 2 на php7.2 вызывает уважение.
    А обещанные третий и четвёртый этап сертификации разработчиков не выходят уже лет пять, как и «новое ядро» D7. С — стабильность.

    • KrisSnow
      /#19338768

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

  2. ErnestMiller
    /#19341822 / +1

    Недавно (то ли в сентябре, то ли в октябре) 1С-Битрикс обещали выкатить поддержку composer. Не знаю в чем она будет заключаться, просто сам факт.

    • mmjurov
      /#19347126

      А они и выкатили. Получилось — как всегда. Теперь в битриксе три или четыре автозагрузчика :) А composer работает ради одной зависимости — symfony/console, на которой написана всего одна команда сомнительного назначения, без которой можно было бы обойтись. Чтобы использовать composer, встроенный в битрикс, для своих нужд, предлагается юзать merge-plugin
      dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&LESSON_ID=4637

  3. serginhold
    /#19343716 / -2

    всё как обычно в мире битрикса, все давно написано, но мы навелосипедим свой плагин-установщик


    https://github.com/composer/installers
    https://github.com/composer/installers/blob/master/src/Composer/Installers/BitrixInstaller.php


    {
      "name": "vendor/packageName",
      "type": "bitrix-d7-module",
      "extra": {
        "installer-name": "packageName"
      },
      "require": {
        "composer/installers": "~1"
      }
    }

    в проекте для /local:


    "extra": {
      "installer-paths": {
        "local/modules/{$vendor}.{$name}/": ["type:bitrix-d7-module"]
      }
    }

    • KrisSnow
      /#19344784 / +1

      Данный инсталлятор позволяет только разместить файлы в нужной директории.
      github.com/composer/installers/blob/master/src/Composer/Installers/BitrixInstaller.php

      Если каждый написанный Вами модуль не имеет зависимостей от других модулей, то можете пользоваться этим инсталлятором. Но после выполнения composer require Вам нужно будет зайти в панель управления и установить модуль вручную.

      Если ваш модуль зависит от других, как в моём примере с СМС авторизацией, то после выполнения composer require Вам нужно будет зайти в панель управления и установить 5 модулей в нужной последовательности!

      • serginhold
        /#19344868

        пакеты в composer при загрузке не должны заниматься изменениями в базе данных, такие вещи делаются миграциями или командами, если логика сложнее чем кнопку нажать

        • KrisSnow
          /#19344942

          Composer этого и не делает. Он просто запускает процесс установки в самом модуле. Конечно composer не должен сам лезть в базу. Но выполнять хуки при установке пакета он должен. А то, что он не делает это по умолчанию, большой плюс к безопасности.

  4. mmjurov
    /#19347160

    Не стоит хранить vendor-dir в local.

    В local должен находиться версионируемый код проекта, а вы храните там библиотеки, которые подгружаются автоматически с помощью composer. Понятно, что можно исключить с gitignore, но зачем, если можно его хранить за пределами document_root? Это более надежно

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

    И еще. В модулях битрикса есть такая «фича», как пошаговая инсталляция/деинсталляция. В вашем случае она работать не будет. Использовать методы DoInstall и DoUninstall не является правильным. Сам битрикс при автоматической инсталляции (без участия пользователя) своих же модулей не вызывает эти методы. Подробнее можно посмотреть в этом MR — github.com/notamedia/console-jedi/issues/4

    • KrisSnow
      /#19347690

      Спасибо за комментарий! Мы просто отказались от пошаговой установки, а в глобальном смысле нужно это учитывать.
      За пределами document_root у меня не было возможность что-либо хранить т.к. document_root — "/" (после выполнения chroot, это один их способов изоляции у нас).
      Спасибо, что упомянули про настройку вебсервере. Я не написал это в статье, но к local/vendor, к composer.json, к composer.json модулей нужно закрывать доступ.