company_banner

Управление зависимостями в PHP

https://medium.com/@tfidry/managing-your-dependencies-in-php-321d584441ab
  • Перевод

При создании PHP-приложения или библиотеки обычно у вас есть три вида зависимостей:


  • Жёсткие зависимости: необходимые для запуска вашего приложения/библиотеки.
  • Опциональные зависимости: например, PHP-библиотека может предоставлять мост для разных фреймворков.
  • Зависимости, связанные с разработкой: инструменты отладки, фреймворки для тестов...

Как управлять этими зависимостями?


Жёсткие зависимости:


{
    "require": {
        "acme/foo": "^1.0"
    }
}

Опциональные зависимости:


{
    "suggest": {
        "monolog/monolog": "Advanced logging library",
        "ext-xml": "Required to support XML"
    }
}

Зависимости опциональные и связанные с разработкой:


{
    "require-dev": {
      "monolog/monolog": "^1.0",
      "phpunit/phpunit": "^6.0"
    }
}

И так далее. Что может случиться плохого? Всё дело в ограничениях, присущих require-dev.


Проблемы и ограничения


Слишком много зависимостей


Зависимости с менеджером пакетов — это прекрасно. Это замечательный механизм для повторного использования кода и лёгкого обновления. Но вы отвечаете за то, какие зависимости и как вы включаете. Вы вносите код, который может содержать ошибки или уязвимости. Вы начинаете зависеть от того, что написано кем-то другим и чем вы можете даже не управлять. Не говоря уж о том, что вы рискуете стать жертвой сторонних проблем. Packagist и GitHub позволяют очень сильно снизить такие риски, но не избавляют от них совсем. Фиаско с left-pad в JavaScript-сообществе — хороший пример ситуации, когда всё может пойти наперекосяк, так что добавление пакетов иногда приводит к неприятным последствиям.


Второй недостаток зависимостей заключается в том, что они должны быть совместимы. Это задача для Composer. Но как бы ни был хорош Composer, встречаются зависимости, которые нельзя использовать совместно, и чем больше вы добавляете зависимостей, тем вероятнее возникновение конфликта.


Резюме


Выбирайте зависимости с умом и старайтесь ограничить их количество.


Жёсткий конфликт


Рассмотрим пример:


{
    "require-dev": {
        "phpstan/phpstan": "^1.0@dev",
        "phpmetrics/phpmetrics": "^2.0@dev"
    }
}

Эти два пакета — инструменты статичного анализа, при совместной установке они иногда конфликтуют, поскольку могут зависеть от разных и несовместимых версий PHP-Parser.


Это вариант «глупого» конфликта: он возникает лишь тогда, когда пытаешься включить зависимость, несовместимую с твоим приложением. Пакеты не обязаны быть совместимыми, ваше приложение не использует их напрямую, к тому же они не исполняют ваш код.


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


{
    "require-dev": {
        "symfony/framework-bundle": "^4.0",
        "laravel/framework": "~5.5.0" # gentle reminder that Laravel
                                      # packages are not semver
    }
}

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


Непроверяемые зависимости


Посмотрите на этот composer.json:


{
    "require": {
        "symfony/yaml": "^2.8 || ^3.0"
    },
    "require-dev": {
        "symfony/yaml": "^3.0"
    }
}

Здесь кое-что происходит… Можно будет установить компонент Symfony YAML (пакет symfony/yaml) только версий [3.0.0, 4.0.0[.


В приложении вам наверняка не будет до этого дела. А вот в библиотеке это может привести к проблеме, потому что у вас никогда не получится протестировать свою библиотеку с symfony/yaml [2.8.0, 3.0.0[.


Станет ли это настоящей проблемой — во многом зависит от конкретной ситуации. Нужно иметь в виду, что подобное ограничение может встать поперёк, и выявить это будет не так просто. Показан простой пример, но если требование symfony/yaml: ^3.0 спрятать поглубже в дерево зависимостей, например:


{
    "require": {
        "symfony/yaml": "^2.8 || ^3.0"
    },
    "require-dev": {
        "acme/foo": "^1.0"  # requires symfony/yaml ^3.0
    }
}

вы об этом никак не узнаете, по крайней мере сейчас.


Решения


Не использовать пакеты


KISS. Всё нормально, на самом деле вам этот пакет не нужен!


PHAR’ы


PHAR’ы (PHP-архивы) — способ упаковки приложения в один файл. Подробнее можно почитать об этом в официальной PHP-документации.


Пример использования с PhpMetrics, инструментом статичного анализа:


$ wget https://url/to/download/phpmetrics/phar-file -o phpmetrics.phar
$ chmod +x phpmetrics.phar
$ mv phpmetrics.phar /usr/local/bin/phpmetrics
$ phpmetrics --version
PhpMetrics, version 1.9.0
# or if you want to keep the PHAR close and do not mind the .phar
# extension:
$ phpmetrics.phar --version
PhpMetrics, version 1.9.0

Внимание: упакованный в PHAR код не изолируется, в отличие от, например, JAR’ов в Java.


Наглядно проиллюстрируем проблему. Вы сделали консольное приложение myapp.phar, полагающееся на Symfony YAML 2.8.0, который исполняет PHP-скрипт:


$ myapp.phar myscript.php

Ваш скрипт myscript.php применяет Composer для использования Symfony YAML 4.0.0.


Что может случиться, если PHAR загружает класс Symfony YAML, например Symfony\Yaml\Yaml, а потом исполняет ваш скрипт? Он тоже использует Symfony\Yaml\Yaml, но ведь класс уже загружен! Причём загружен из пакета symfony/yaml 2.8.0, а не из 4.0.0, как нужно вашему скрипту. И если API различаются, всё ломается напрочь.


Резюме


PHAR’ы замечательно подходят для инструментов статичного анализа вроде PhpStan или PhpMetrics, но ненадёжны (как минимум сейчас), поскольку исполняют код в зависимости от коллизий зависимостей (на данный момент!).


Нужно помнить о PHAR’ах ещё кое-что:


  • Их труднее отслеживать, потому что они не поддерживаются нативно в Composer. Однако есть несколько решений вроде Composer-плагина tooly-composer-script или PhiVe, установщика PHAR’ов.
  • Управление версиями во многом зависит от проекта. В одних проектах используется команда self-update а-ля Composer с разными каналами стабильности. В других проектах предоставляется уникальная конечная точка скачивания с последним релизом. В третьих проектах используется функция GitHub-релиза с поставкой каждого релиза в виде PHAR и т. д.

Использование нескольких репозиториев


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


Возьмём предыдущий пример с библиотекой. Назовём её acme/foo, затем создадим пакеты acme/foo-bundle для Symfony и acme/foo-provider для Laravel.


Обратите внимание, мы пока ещё можем всё поместить в один репозиторий, а для других пакетов вроде Symfony использовать репозитории только для чтения.


Главное преимущество этого подхода в том, что он относительно прост и не требует дополнительных инструментов, за исключением разделителя по репозиториям вроде splitsh, используемого для Symfony, Laravel и PhpBB. А недостаток в том, что теперь вместо одного пакета вам нужно поддерживать несколько.


Настройка конфигурации


Можно пойти другим путём и выбрать более продвинутый скрипт установки и тестирования. Для нашего предыдущего примера можно использовать подобное:


#!/usr/bin/env bash
# bin/tests.sh
# Test the core library
vendor/bin/phpunit --exclude-group=laravel,symfony
# Test the Symfony bridge
composer require symfony/framework-bundle:^4.0
vendor/bin/phpunit --group=symfony
composer remove symfony/framework-bundle
# Test the Laravel bridge
composer require laravel/framework:~5.5.0
vendor/bin/phpunit --group=symfony
composer remove laravel/framework

Работать будет, но, по моему опыту, тестовые скрипты получатся раздутыми и относительно медленными, трудными в поддержке и не слишком простыми в понимании сторонними программистами.


Использование нескольких composer.json


Этот подход довольно свежий (в PHP), в основном потому, что раньше не было нужных инструментов, так что я расскажу чуть подробнее.


Идея проста. Вместо


{
    "autoload": {...},
    "autoload-dev": {...},
    "require": {...},
    "require-dev": {
        "phpunit/phpunit": "^6.0",
        "phpstan/phpstan": "^1.0@dev",
        "phpmetrics/phpmetrics": "^2.0@dev"
    }
}

мы установим phpstan/phpstan и phpmetrics/phpmetrics в разные файлы composer.json. Но тут возникает первая сложность: куда их класть? Какую создавать структуру?


Здесь поможет composer-bin-plugin. Это очень простой плагин для Composer, позволяющий взаимодействовать с composer.json в разных папках. Допустим, есть корневой файл composer.json:


{
    "autoload": {...},
    "autoload-dev": {...},
    "require": {...},
    "require-dev": {
        "phpunit/phpunit": "^6.0"
    }
}

Установим плагин:


$ composer require --dev bamarni/composer-bin-plugin

После этого, если выполнить composer bin acme smth, то команда composer smth будет выполнена в поддиректории vendor-bin/acme. Теперь установим PhpStan и PhpMetrics:


$ composer bin phpstan require phpstan/phpstan:^1.0@dev
$ composer bin phpmetrics require phpmetrics/phpmetrics:^2.0@dev

Будет создана такая структура директорий:


... # projects files/directories
composer.json
composer.lock
vendor/
vendor-bin/
    phpstan/
        composer.json
        composer.lock
        vendor/
    phpmetrics/
        composer.json
        composer.lock
        vendor/

Здесь vendor-bin/phpstan/composer.json выглядит так:


{
    "require": {
        "phpstan/phpstan": "^1.0"
    }
}

А vendor-bin/phpmetrics/composer.json выглядит так:


{
    "require": {
        "phpmetrics/phpmetrics": "^2.0"
    }
}

Теперь можно использовать PhpStan и PhpMetrics, просто вызвав vendor-bin/phpstan/vendor/bin/phpstan и vendor-bin/phpmetrics/vendor/bin/phpstan.


Пойдём дальше. Возьмём пример с библиотекой с мостами для разных фреймворков:


{
    "autoload": {...},
    "autoload-dev": {...},
    "require": {...},
    "require-dev": {
        "phpunit/phpunit": "^6.0",
        "symfony/framework-bundle": "^4.0",
        "laravel/framework": "~5.5.0"
    }
}

Применим тот же подход и получим файл vendor-bin/symfony/composer.json для моста Symfony:


{
    "autoload": {...},
    "autoload-dev": {...},
    "require": {...},
    "require-dev": {
        "phpunit/phpunit": "^6.0",
        "symfony/framework-bundle": "^4.0"
    }
}

И файл vendor-bin/laravel/composer.json для моста Laravel:


{
    "autoload": {...},
    "autoload-dev": {...},
    "require": {...},
    "require-dev": {
        "phpunit/phpunit": "^6.0",
        "laravel/framework": "~5.5.0"
    }
} 

Наш корневой файл composer.json будет выглядеть так:


{
    "autoload": {...},
    "autoload-dev": {...},
    "require": {...},
    "require-dev": {
        "bamarni/composer-bin-plugin": "^1.0"
        "phpunit/phpunit": "^6.0"
    }
}

Для тестирования основной библиотеки и мостов теперь нужно создать три разных PHPUnit-файла, каждый с соответствующим файлом автозагрузки (например, vendor-bin/symfony/vendor/autoload.php для моста Symfony).


Если вы попробуете сами, то заметите главный недостаток подхода: избыточность конфигурирования. Вам придётся дублировать конфигурацию корневого composer.json в другие два vendor-bin/{symfony,laravel/composer.json, настраивать разделы autoload, поскольку пути к файлам могут измениться, и когда вам потребуется новая зависимость, то придётся прописать её и в других файлах composer.json. Получается неудобно, но на помощь приходит плагин composer-inheritance-plugin.


Это маленькая обёртка вокруг composer-merge-plugin, позволяющая объединять контент vendor-bin/symfony/composer.json с корневым composer.json. Вместо


{
    "autoload": {...},
    "autoload-dev": {...},
    "require": {...},
    "require-dev": {
        "phpunit/phpunit": "^6.0",
        "symfony/framework-bundle": "^4.0"
    }
}

получится


{
    "require-dev": {
        "symfony/framework-bundle": "^4.0",
        "theofidry/composer-inheritance-plugin": "^1.0"
    }
}

Сюда будет включена оставшаяся часть конфигурации, автозагрузки и зависимостей корневого composer.json. Ничего конфигурировать не нужно, composer-inheritance-plugin — лишь тонкая обёртка вокруг composer-merge-plugin для предварительного конфигурирования, чтобы можно было использовать с composer-bin-plugin.


Если хотите, можете изучить установленные зависимости с помощью


$ composer bin symfony show

Я применял этот подход в разных проектах, например в alice, для разных инструментов вроде PhpStan или PHP-CS-Fixer и мостов для фреймворков. Другой пример — alice-data-fixtures, где используется много разных ORM-мостов для уровня хранения данных (Doctrine ORM, Doctrine ODM, Eloquent ORM и т. д.) и интеграций фреймворков.


Также я применял этот подход в нескольких частных проектах как альтернативу PHAR’ам в приложениях с разными инструментами, и он прекрасно себя зарекомендовал.


Заключение


Уверен, кто-то сочтёт некоторые методики странными или нерекомендуемыми. Я не собирался давать оценку или советовать что-то конкретное, а хотел лишь описать возможные способы управления зависимостями, их достоинства и недостатки. Выберите, что вам больше подходит, ориентируясь на свои задачи и личные предпочтения. Как кто-то сказал, не существует решений, есть только компромиссы.

Mail.Ru Group 562,83
Строим Интернет
Поделиться публикацией
Похожие публикации
Комментарии 50
  • –10
    Лично мне кажется, что copy-paste кода библиотеки лучше работает, чем включение этой библиотеки через Composer. Может быть, я неправильно что-то считаю, но у меня (пример ситуации) выходит так, что использовать код StringHelper из Yii Framework напрямую (удалив из него код, зависимый от HTMLPurifier) выходит выгоднее по времени, чем использование композера. На удаление лишних методов и зависимостей из кода я потрачу один раз несколько минут, и код раз за разом будет нормально работать. Но если я буду что-то включать через Composer, то через какое-то время получится, что на операции обновления зависимостей через него у меня затрачивается больше времени — оно [время] просто накапливается. Сегодня ушло 20 секунд на то, чтобы Composer прочекал все зависимости, завтра ушло ещё 20 секунд. Послезавтра ещё 30 (в новой версии билиотеки, в методе, который я никогда не буду использовать, появилась зависимость от другой библиотеки, которая, конечно же, загрузится и потянет за собой ещё какие-нибудь зависимости). В какой-то момент получится, что я мог потратить с самого начала 2 минуты на copy-paste и удаление ненужного кода, но, в итоге, я потратил за 2 месяца 30 минут только на ожидание, пока композер всё прочекает и обновит. (В итоге, за всю карьеру накапливается куча бесполезно проведённого времени, которое можно было бы потратить гораздо веселее — например, сметнуться в Новую Зеландию за презиками.)

    (Теги, конечно же, не читал, потому что их никто не читает)
    • 0
      Ну это пока пакет не обновляется а коллеги берут код с FTP, если же вы хотите обновляемый пакет, то математика работает в обратную сторону — 10 минут ручной работы против 20 секунд композера.
      Про надёжность того что вы там на удаляли я вообще молчу — зачем перекладывать бремя тестирования пакета на себя? Да и пулл реквест сделать уже будет проще.
      Кстати, если композер у вас долго работает — возможно его стоит обновить? У меня он весьма быстр.
      Если у вас проблема с композером — обновляйтесь пока ходите за чаем.
      • 0
        если же вы хотите обновляемый пакет

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

        К слову, мой подход приводит к тому, что я иногда в оригинал этого же пакета пулл-реквесты отправляю — из-за того, что внимательно смотрю, что именно принимаю в кодовую базу и как оно работает. По-моему, всем от этого польза. :)

        зачем перекладывать бремя тестирования пакета на себя?

        Это вопрос надёжности ПО. У каждого свои критерии надёжности и того, сколько внимания он готов этому уделять. Есть примеры того, как откровенно вредный код может без вашего ведома подтянуться вместе с зависимостями. В общем-то, чем больше экономишь время, полагаясь на чужой код, который полагается на библиотеку, которая зависит от четвёртой библиотеки, тем выше риск получить нерабочий или уязвимый код.

        Это всё не аксиома, и я не призываю фанатично чему-то следовать. Просто я вижу, что использование зависимостей может помогать тактически, но вредить стратегически.

        Я просто верю, что стремление к надёжному и безопасному коду приводит в компанию людей, которым нужен надёжный и безопасный код, и которые готовы платить больше за твою внимательность в разработке.

        Ну это пока пакет не обновляется а коллеги берут код с FTP

        Я не совсем понимаю, причём тут FTP, если подключаемый код будет лежать в том же самом репозитории VCS. Я всегда считал, что FTP вместе с мамонтами вымер.
        • 0
          Я обычно хочу не обновляемый пакет, а надёжный — который с самого начала работал и будет и дальше предсказуемо работать, потому что в нём ничего не поменяется.

          Зачем тогда вам "операции обновления зависимостей" при использовании composer? Загружайте точные версии, не запускайте composer update, и всё.

          • 0
            При таком подходе, ИМХО, придётся следить за развитием билиотеки в обход композера, потому что иначе можно будет пропустить какое-то обновление безопасности (или просто фикс какой-то ошибки). Это кучу времени будет отнимать.

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

            Лично для меня моя концепция лучше работает. Я её никому не навязываю. Она мне даёт больше плюсов, чем минусов.
            • 0
              придётся следить за развитием библиотеки в обход композера

              А в чем разница с вашим подходом?

              • 0
                В том, что я не слежу за развитием библиотеки и вытаскиваю из неё, скажем, 5% кода, который мне реально потребуется, а остальное выкидываю из кодовой базы. А то, что осталось, я дополнительно тестирую самостоятельно. И если вижу ошибки, то я их могу мгновенно исправить, а не отправлять в цепочку: «создать issue на github» → «дождаться фикса» → «загрузить себе через композер», сидя на некорректно работающей библиотеке всё время, пока эта цепочка не завершится.
                • +1

                  Но ведь и в вашем подходе можно пропустить какое-то обновление безопасности.


                  И если вижу ошибки, то я их могу мгновенно исправить

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


                  Я, собственно, не вас пытаюсь переубедить, а скорее тех, кто прочитает диалог и узнает себя. В своих проектах можно делать как угодно, а рабочие потом придется кому-то поддерживать.

                  • 0
                    Я, собственно, не вас пытаюсь переубедить, а скорее тех, кто прочитает диалог и узнает себя. В своих проектах можно делать как угодно, а рабочие потом придется кому-то поддерживать.

                    Я, в общем-то, особенно тоже не убеждаю. Просто я вижу минусы и их озвучиваю. Кто-то видит минусы в моём подходе и тоже их озвучивает. Лично меня такие диалоги хорошо развивают и дают возможность переосмысливать собственные подходы к разработке.

                    Просто хорошо, когда есть разные точки зрения, и хорошо, когда видно и минусы и плюсы обеих этих точек зрения. Кто-то (включая меня) может увидеть все эти минусы и плюсы и изобрести третий вариант, который унаследует все плюсы.

                    Если встретилась какая-то редкая специфичная ошибка, и неохота ждать, можно унаследоваться и переопределить нужный метод. Вы будете «мгновенно» исправлять каждую ошибку в каждой библиотеке, и совокупное время уже сложно будет назвать мгновенным, а с возможностью обновления ошибки будут исправлены вообще без вашего участия

                    Конечно, можно отнаследоваться, но может получиться так, что, например, в классе, от которого я наследуюсь, статичный метод, который я хочу заменить, вызывается как self::method(), а не как static::method(), и в результате вызовы пойдут мимо метода, который я пропатчил в дочернем классе. Придётся тогда и те куски кода тоже в дочернем классе переопределять. А это приведёт к тому, что я больше не буду вызывать часть оригинального кода. Если в эти части придут какие-то изменения, я ими не буду пользоваться, а они могут быть полезными, либо могут как-то значительно влиять на полезность других методов, которые я использую. Мне в таком случае придётся дополнительно следить за кодом, который приходит при обновлениях, чтобы соответственно реагировать в моём дочернем классе. Это снижает ценность подключения и обновления библиотеки в автоматическом режиме — тратится больше времени.
                    • 0
                      Если в эти части придут какие-то изменения, я ими не буду пользоваться, а они могут быть полезными, либо могут как-то значительно влиять на полезность других методов, которые я использую. Мне в таком случае придётся дополнительно следить за кодом, который приходит при обновлениях

                      Зачем? Вот есть код, вы его скопировали и не обновляете. Вот есть тот же код, вы зафиксировали нужную версию в управлении пакетами и тоже не обновляете. Разницы нет, но во втором случае есть возможность обновить и проверить код автоматически, а в первом нет. Запустили тесты, все работает значит все ок, следить не надо. Тесты упали, значит либо откатили либо поправили пару мест, следить тоже не надо. А много мест быть не должно, потому что semver.

                      • 0
                        Я на примере кода поясню, что я имел ввиду, потому что мне кажется, что вы уже немного о другом говорите.

                        Пример кода
                        class ComposerLibDependency {
                            public static function usefulMethod($a, $b) {
                                return floor($a * $b);
                            }
                        }
                        
                        class ComposerLib extends ComposerLibDependency {
                            public static function useMoreA($a, $b) {
                                return self::usefulMethod($a * 3, $b);
                            }
                        
                            public static function useMoreB($a, $b) {
                                return self::usefulMethod($a, $b * 3);
                            }
                        }
                        
                        class MyLibPatch extends ComposerLib {
                            public static function usefulMethod($a, $b) {
                                if (!is_numeric($a) || !is_numeric($b)) {
                                    throw new Exception('Incorrect input');
                                }
                        
                                return intval(parent::usefulMethod($a, $b));
                            }
                        }
                        
                        var_dump(MyLibPatch::useMoreA(2, 3.5));
                        var_dump(MyLibPatch::useMoreB(2, 3.5));
                        



                        Метод MyLibPatch::usefulMethod() здесь в итоге не используется. Чтобы этот мой фикс использовать, нужно заодно переопределить методы useMoreA и useMoreB, по сути, заменяя в них только self на static. Если потом в них придут изменения из ComposerLib, они не отразятся, потому что я эти методы переопределил. Получается, мне нужно будет мониторить внешние зависимости, чтобы в эти переопределённые методы своевременно вносить изменения. Или, даже, если не вносить, то просто удостоверяться, что такое переопределение ничего не ломает в новых версиях билиотеки из композера.

                        Это просто к вопросу об отнаследовании.
                        • 0
                          Если потом в них придут изменения из ComposerLib

                          Откуда там появятся изменения? Ни в скопированном коде, ни в фиксированной версии никаких изменений нет.

                          • 0
                            Я вас же процитирую:

                            Если встретилась какая-то редкая специфичная ошибка, и неохота ждать, можно унаследоваться и переопределить нужный метод. Вы будете «мгновенно» исправлять каждую ошибку в каждой библиотеке, и совокупное время уже сложно будет назвать мгновенным, а с возможностью обновления ошибки будут исправлены вообще без вашего участия

                            Ключевые слова здесь: «а с возможностью обновления ошибки будут исправлены вообще без вашего участия».

                            Вы ничего про фиксированную версию не говорили в тот момент. Вы говорили про то, что подразумевается возможность обновления. То есть, вы советовали унаследоваться и переопределить метод, а потом спокойно дожидаться, пока придёт обновление. Я просто показываю минусы такого подхода.
                            • 0
                              Нет. Я предлагал не копировать код в репозиторий, а просто зафиксировать версию, потому что в результате будет то же самое, но без ручной работы. «Возможность» обновления на то и возможность, захотели — сделали. Раз в полгода решили выделить время, обновили и проверили, получили исправления ошибок и безопасности, убрали свои исправления. Постоянно следить за новым сторонним кодом не нужно. И с форком то же самое, только без наследования.
                              • 0
                                Ясно. Я, в основном всё-таки про отнаследование и переопределение метода говорил и про то, что одним переопределением метода это может не ограничиться, и поддержка этого кода в соответствие с кодом оригинальной библиотеки может требовать больше времени и внимания, чем кажется на первый взгляд.
                                • +2
                                  Пусть мои 5 лет в пхп не так уж и много, но за 5 лет не наследовался от вендор-либ ни одного раза. Всегда вопрос решался композицией. А это значит, что всегда можно напилить адаптер для другого вендор-решения или самописной либы.
                  • 0

                    При вашем подходе все равно придется следить за обновлением безопасности и фиксами ошибок (никто ведь не может быть уверен, что вы выкинули именно ту часть, которая эти ошибки содержит)


                    Ну и при подходе с композером никто не заставляет ждать цепочку "issue — fix — tag", всегда можете форкнуть и использовать форк до исправления ошибки.

                    • 0
                      Я в моём подходе бремя тестирования беру на себя. Некоторое время назад меня впечатлила книга Г.Майерса «Надёжность программного обеспечения», особенно идея, что тесты нужны для поиска ошибок, а не для того, чтобы просто покрывать ими код или использовать их в качестве двигателя функционала (когда используется TDD). Так что, мне удобнее брать минимально возможное количества кода со стороны, чтобы потом приходилось меньше писать тестов, которые его пытаются сломать.

                      Ну и при подходе с композером никто не заставляет ждать цепочку «issue — fix — tag», всегда можете форкнуть и использовать форк до исправления ошибки.

                      Минус в том, что форк придётся тоже поддерживать в актуальном состоянии. Нужно будет принимать из оригинала обновления. Иногда эти обновления будут затрагивать лично ваши изменения, не исправляя при этом ошибок, ради которых форк создавался, и придётся делать сложные мёржи.
                      • 0
                        А возможно и не придется.
                        Вот ради интереса вопрос, сколько раз вы пофиксили или просто столкнулись с критикал багами в сторонних либах за последний год?
                        • 0
                          Критикал — ни разу в этом году. Из просто ошибок, которыми могу поделиться: этот и этот (плюс пулл-реквест, который приняли) репорты в Yii Framework.

                          Из того, всеми деталями чего не могу поделиться: фиксы в cordova-plugin-file-transfer, cordova-plugin-statusbar, react-dom.js (планирую попробовать отправить репорт и фикс, когда появится время, про ReactDOM.render, который возвращает не то, что должен, если вызывается внутри коллбека setState — нужно писать синтетический код для демонстрации — тот, который в проекте используется, показывать не могу), react-select.js (вообще выкинул после пары правок — проще и быстрее оказалось свою обёртку для <select> написать, чем натыкаться на постоянные грабли), sprintf.js (понимает %e, но не понимает %E + ещё несколько моментов, когда она работает не так, как в PHP).

                          Просто по поводу криткал-багов у меня такой подход, что проще бывает вообще не использовать библиотеку, чем заниматься правками критикал багов в ней. Поэтому я их не фиксю в других библиотеках и не могу похвалиться подобными правками. Мне хватило одного раза, когда я несколько лет назад работал с ics-parser на его начальных этапах жизни — правда, то, что я фиксил тогда, наружу никуда не выходило. Пишу свою библиотеку на эту тему, но она два года никуда не двигается, и я, наверное, не буду её писать всё-таки.
        • 0
          Сегодня ушло 20 секунд на то, чтобы Composer прочекал все зависимости, завтра ушло ещё 20 секунд.

          А зачем вы вообще запускаете composer update, если вас устраивает версия, которая указана в composer.lock?

          • –3
            У меня иногда бывали моменты, когда в начале проекта корневые неймспейсы часто появляются/удаляются, пока не устаканится какая-то определённая структура. Они, в общем-то, в composer.json прописываются, и чтобы autoload.php и прочие сопутствующие загрузчики перегенерировались, нужно запускать composer update
            • +1

              Не нужно. Достаточно запустить composer dump-autoload.

          • 0
            Мне кажется что если уж сильно хочется использовать какую-то сильно изменённую под свои нужды библиотеку, то лучше оформлять её в отдельный репозиторий (хоть на своём сервере, хоть на гитхаб, без разницы) и ставить тем-же композером. Сильно поддержку упрощает, особенно если она используется в нескольких проектах, достаточно исправить ошибку в одном месте.
          • 0
            Столкнулся давеча с таким подходом в процессе перевода приложения с 5 на 7. Не понравилось.
            • 0

              Я видимо что-то не понимаю.


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


              В проекте же, в require, можно вообще любые зависимости держать. За них отвечает команда разработчиков.


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


              А если хочется использовать фичу из последней версии пакета, которая не совместима с другими пакетами, то просто не используйте эту фичу. Подождите пока исправят совместить или сделайте PR.


              Не стоит гнаться за последними релизами.


              Мне бывает приходится использовать фиксированную версию пакетов, но только потому, что разработки ломают обратную совместимость.


              Решение vendor in vendor мне видится странным и наверняка добавит проблем.

              • 0

                Не всегда всё так просто. Например, всё отлично работает на PHP5.6, приходит задача перевести всё на PHP7.1 по требованию аудитора/регулятора (альтернатива — закрыть бизнес) и выясняется, что две зависимости, которые отлично работали вместе под 5.6, несовместимы теми версиями с 7.1, а новые совместимые с 7.1 версии не работают вместе. Ну и PR могут не принимать месяцами или закрыть его вовсе с wantfix

                • 0
                  Всегда можно форкнуть и пофиксить проблему в своем форке.
                  • 0

                    Вариант в целом, более того лично так делал. Но не всегда легко взять и пофиксить, даже если в новой версии проблемы нет. очень сильно вникать в код библиотеки может понадобиться, особенно если это не лефтпад какой-нибудь, а ОРМ, например, или поддержка какого-то зубодробительного формата типа xls

                    • 0
                      Ну и как вам поможет копипаста либы, если либа сложная и багу фиксить всеравно надо?
                      • 0

                        Взять текущую версию либы, в которой уже нет проблемы и изолировать её от несовместимых с ней других версий.

                  • 0

                    Я сейчас перевожу один проект на PHP 7. Одн из пакетов оказался не совместим с PHP 7.
                    Так как проект заброшен, я просто взял и заменил его на другой. Переписал пару адаптеров и все.
                    Ну ещё немножко смазал маслицем чтоб лучше зашёл

                  • 0
                    Очевидно же, что в библиотеке require желательно оставить пустым, а в require-dev пишем, что хотим, так как эти пакеты используются только для разработки и тестирования и к клиенту они не попадут.


                    я правильно понимаю, что вы заставляете пользователя искать транзитивные зависимости вручную? ну типа у вас есть какой-нибудь api клиент с использованием guzzle, а guzzle нужной версии вы заставляете людей ставить?

                    А если ваша библиотека написана с использованием nullable, а у человека 7.0, он тоже об этом только опытным путем узнает?
                    • 0
                      Требования платформы естественно указываются.
                      Либа зависит от пср-овского хттп клиента. В suggest-ах может предложить что-то. Но зачем тащить guzzle, если приложение уже что-то использует? Пользователь вашей либы просто наконфигурит какой клиент юзать.
                      • +1
                        в таком случае
                        1. у вас в src нигде не будет зависимости от guzzle, только от psr
                        2. в require у вас будет какой нибудь psr/http-message все равно, т.к он требуется для вашей библиотеки
                        • 0
                          C газлом возможно неудачный пример, надо было брать какой-нибудь соап клиент, на которых нормальных стандартов не понаписали, а наследоваться от \SoapClient все не хотят
                          • +1
                            Можно разделить либу на 2 пакета. В первом объявить интерфейс, который должен быть реализован (аналог пср-а) и завязать всю логику на интрфейс.
                            Во втором пакете можно предоставить реализацию, которая с требованиями к платформе и потенциально конфликтующей реализацией.

                            Второй пакет в саггестах у первого.
                            Нету конфликтов и лень чето выдумывать? Ставь пакет из саггеста.
                            Не устраивает пакет из саггеста? Напили свою реализацию или найди подходящую и сделай адаптер.
                            • 0

                              Обычно я так и делаю. Один пакет чистый без зависимостей, а второй bundle для Symfony с конфигами и всеми делами.
                              Хочешь юзать в Symfony ставь бандл. Если у тебя не Symfony, юзай чистый пакет.

                              • +1
                                саджест все равно в таких случаях некорректен. саджест — это предложение, а в вашем случае без установки такого «предложения» пакет не будет работать. в таких случаях делают по-другому:

                                реквестят (мета)-пакет «some-vendor/my-super-interface-implementation»
                                этот пакет не зарегистрирован в packagist, но любой другой пакет (например из вашего списка саджестов) может прописать у себя provides some-vendor/my-super-interface-implementation

                                таким образом при установке будут разрешены все зависимости, не будет лишних реквайров, но при этом будет явное требование установить что-то что имплементит ваш интерфейс

                                Вот например так работают с тем же PSR-3 логгером

                                packagist.org/providers/psr/log-implementation
                      • 0
                        Использую composer совместно со своим gitlab сервером, все legacy-библиотеки перевёл в отдельные репозитории, на остальные сделал форки, опять же в своём gitlab сервере.
                        • +1
                          А какой смысл в тотальных форках пакетов? Если вы боитесь, что репы пропадут, то проще использовать кэширующий прокси для пакетов
                          • 0

                            Прокси дело такое, сегодня он есть, а завтра его кто-то снёс, не поняв, что его основное назначение не ускорение доступа и т. п., а постоянное хранение. Или сам прокси оставят, а вот кэш почистят, когда место на диске кончится. Кэш же, не хранилище, прогреется со временем.

                            • 0
                              А форки также случайно не почистят? подумав что это ненужная фигня — вот же оригинальный обновляемый проект в публичном доступе.
                              • 0

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

                                • 0
                                  я просто грустнею даже от самой мысли, что мне бы пришлось поддерживать сотни три форков (да пусть даже и автоматикой, но поддерживать) с полной синхронизацией всех коммитов
                                  • 0

                                    Ну я сам не практикую тотальное форканье, но если апстрим не обновлялся годами, особенно с висящими пулл-реквестами от разных людей, то предпочитаю форкнуть сразу, будучи практически уверенным, что рано или поздно столкнусь с необходимостью патчить.

                                    • 0
                                      Ну вот это примерно тот подход, который практиковали и мы. Обычно форк предварялся игрой «найди более поддерживаемый форк»
                                      • 0

                                        Именно. Но на самом деле мы лишь субъективно оценивали вероятности того, что в интересующей перспективе репозиторий может вообще исчезнуть. Если цена исчезновения очень высока, то нужно форкать всё, к какой бы бесконечно малой величине не стремилась оценка исчезновения или потери возможности доступа. Потери на поддержку форка без своих патчей (причём не обязательно до уровня каждого коммита или тега оперативно синкать, достаточно актуализировать на момент принятия решения об обновлении) могут оказаться пренебрежимо малы по сравнению с потенциальными потерями при недоступности.

                                        • 0
                                          Ну, все таки, при обычной недоступности спасет кэш-прокси (маловероятно что вам почистят кэши именно в день, когда гитхаб опять забанят).

                                          От внезапного удаления пакета помогает во-первых все тот же кэш прокси (они все таки жалуются, когда пакет пропал при синке), а во вторых --prefer-source при разработке — хоть у кого то из разработчиков да останется пакет в исходном виде (если учесть, что он не обновляется).

                                          В самом крайнем случае код довольно легко извлечь с любого из стендов и уже в момент потери положить в репку, которую подключить в satis\toran\etc.

                                          Мне все это кажется менее геморным, чем постоянно поддерживать несколько сотен форков, все таки пакеты не каждый день пропадают
                                          • 0

                                            Ключевое слово "кажется". Кому-то менее геморным кажется, кому-то более. Кто-то вообще не форкает без принятия решения развития своего форка, кто-то избранно, кто-то всё. В принципе, если создана масштабируемая инфраструктура для автоматического обновления хотя бы избранных, то цена форканья всех не столь уже высока.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое