Репозитории Composer. Хранилища программ на PHP в Composer.
Хранилища программ на PHP в Composer. Репозитории Composer
  1. Концепции
    1. Пакет (Package)
    2. Репозиторий (Repository)
  2. Типы репозиториев в Composer
    1. Composer
      1. packages
      2. notify-batch
      3. metadata-url, available-packages и available-package-patterns
      4. providers-api
      5. list
      6. provider-includes и providers-url
      7. cURL или stream options
    2. VCS (version control system)
      1. Загрузка пакета из репозитория VCS
      2. Использование частных репозиториев
      3. Варианты Git
      4. Настройка Bitbucket Driver
      5. Настройка Subversion
    3. Package
  3. Размещение собственного пакета Composer
    1. Private Packagist
    2. Satis
    3. Artifact
    4. Маршрут до пакета Composer
  4. Отключение Packagist.org в composer.json


Концепции

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

Пакет (Package)

Composer - это менеджер зависимостей. Он устанавливает пакеты локально. Пакет - это, по сути, каталог (директория), содержащий что-либо. В данном случае это код PHP, но теоретически это может быть что угодно. Он содержит описание пакета, в котором есть имя и версия. Имя и версия используются для идентификации пакета.

Фактически, внутренне Composer рассматривает каждую версию как отдельный пакет. Хотя это различие не имеет значения, при использовании Composer, оно довольно важно, когда требуется его изменить.

Помимо имени и версии, существуют полезные метаданные. Информация, наиболее значимая для установки, - это определение источника, которое описывает, где взять содержимое пакета. Данные пакета указывают на содержимое пакета. Здесь есть два варианта: dist и source.

  • Dist:
    • Дистрибутив - это упакованная версия данных пакета. Обычно это готовая версия, обычно стабильный релиз.
  • Source:
    • Исходный код используется для разработки. Обычно он берется из репозитория исходного кода, например, git. Его можно получить, когда требуется внести изменения в загруженный пакет.

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

Репозиторий (Repository)

Репозиторий (хранилище) - это источник пакетов. Это список пакетов/версий. Composer будет искать во всех доступных вам репозиториях пакеты, необходимые вашему проекту.

По умолчанию в Composer зарегистрирован только репозиторий Packagist.org. Можно добавить больше репозиториев в проект, объявив их в composer.json.

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

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

Типы репозиториев в Composer

Composer

Основным типом репозитория является репозиторий composer. Он использует единственный файл packages.json, содержащий все метаданные пакета.

Это также тип репозитория, который использует packagist. Чтобы сослаться на репозиторий composer, укажите путь до файла packages.json. В случае с packagist этот файл находится по адресу /packages.json, поэтому URL репозитория будет repo.packagist.org. Для example.org/packages.json URL репозитория будет example.org.

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://example.org"
        }
    ]
}

packages

Единственное обязательное поле - это packages. Структура JSON выглядит следующим образом:

{
    "packages": {
        "vendor/package-name": {
            "dev-master": { @composer.json },
            "1.0.x-dev": { @composer.json },
            "0.0.1": { @composer.json },
            "1.0.0": { @composer.json }
        }
    }
}

Маркер @composer.json будет представлять собой содержимое composer.json данной версии пакета, включая, как минимум:

  • name
  • version
  • dist или source

Вот минимальное определение пакета:

{
    "name": "smarty/smarty",
    "version": "3.1.7",
    "dist": {
        "url": "https://www.smarty.net/files/Smarty-3.1.7.zip",
        "type": "zip"
    }
}

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

notify-batch

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

Пример значения:

{
    "notify-batch": "/downloads/"
}

Для example.org/packages.json, содержащего пакет monolog/monolog, будет отправлен POST-запрос на example.org/downloads/ со следующим телом запроса JSON:

{
    "downloads": [
        {"name": "monolog/monolog", "version": "1.2.1.0"}
    ]
}

Поле version будет содержать нормализованное представление номера версии.

Это поле является необязательным.

metadata-url, available-packages и available-package-patterns

Поле metadata-url позволяет указать шаблон URL для обслуживания всех пакетов, находящихся в репозитории. Оно должно содержать вставку %package%.

Это поле является новым в Composer v2 и имеет приоритет над полями provider-includes и providers-url, если они присутствуют. Для совместимости с Composer v1 и v2 рекомендуется указывать оба поля. Однако новым реализациям репозиториев может потребоваться поддержка только v2.

Пример:

{
    "metadata-url": "/p2/%package%.json"
}

Когда Composer ищет пакет, он заменит %package% на имя пакета и получит этот URL. Если для пакета предусмотрена dev-стабильность, он также загрузит URL снова с $packageName~dev (например, /p2/foo/bar~dev.json для поиска dev-версий foo/bar).

Файлы foo/bar.json и foo/bar~dev.json, содержащие версии пакетов, ДОЛЖНЫ содержать только версии для пакета foo/bar, как {"packages":{"foo/bar":[ ... versions here ... ]}}.

Кэширование осуществляется с помощью заголовка If-Modified-Since, поэтому убедитесь, что вы возвращаете заголовки Last-Modified и что они точны.

Массив версий также может быть минифицирован с помощью Composer\MetadataMinifier\MetadataMinifier::minify() из composer/metadata-minifier. Если вы это сделаете, вам следует добавить ключ "minified": "composer/2.0" на верхнем уровне, чтобы указать Composer, что он должен развернуть список версий обратно в исходные данные. Пример смотрите на https://repo.packagist.org/p2/monolog/monolog.json.

Любой запрошенный пакет, который не существует, ДОЛЖЕН вернуть код состояния 404, который укажет Composer, что этот пакет не существует в вашем хранилище. Убедитесь, что ответ 404 быстрый, чтобы избежать блокировки Composer. Избегайте перенаправлений на альтернативные страницы 404.

Если в вашем хранилище только небольшое количество пакетов, и вы хотите избежать 404-запросов, вы также можете указать ключ "available-packages" в packages.json, который должен представлять собой массив со всеми именами пакетов, которые содержит ваше хранилище. Также вы можете указать ключ "available-package-patterns", который представляет собой массив шаблонов имен пакетов (при этом * соответствует любой строке, например, vendor/* заставит Composer искать все подходящие имена пакетов в этом хранилище).

Это поле является необязательным.

providers-api

Поле providers-api позволяет указать шаблон URL для обслуживания всех пакетов, предоставляющих заданное имя пакета, но не самого пакета с таким именем. Он должен содержать вставку %package%.

Например, https://packagist.org/providers/monolog/monolog.json перечисляет некоторые пакеты, которые имеют правило "provide" для monolog/monolog, но не перечисляет сам monolog/monolog.

{
    "providers-api": "https://packagist.org/providers/%package%.json",
}

Это поле является необязательным.

list

Поле list позволяет вернуть имена пакетов, которые соответствуют заданному полю (или все имена, если filter не задан). Оно должно принимать необязательный параметр запроса ?filter=xx, который может содержать * как подстановочные знаки, соответствующие любой подстроке.

Правила замены/подстановки здесь не рассматриваются.

Он должен возвращать массив имен пакетов:

{
    "packageNames": [
        "a/b",
        "c/d"
    ]
}

См. пример https://packagist.org/packages/list.json?filter=composer/*.

Это поле является необязательным.

provider-includes и providers-url

Поле provider-includes позволяет перечислить набор файлов, в которых перечислены имена пакетов, предоставляемых данным хранилищем. Хэш файлов в этом случае должен быть sha256.

Поле providers-url указывает, как найти файлы провайдеров на сервере. Это абсолютный путь от корня хранилища. Он должен содержать подстановки %package% и %hash%.

Эти поля используются в Composer v1, или если в вашем хранилище не установлено поле metadata-url.

Пример:

{
    "provider-includes": {
        "providers-a.json": {
            "sha256": "f5b4bc0b354108ef08614e569c1ed01a2782e67641744864a74e788982886f4c"
        },
        "providers-b.json": {
            "sha256": "b38372163fac0573053536f5b8ef11b86f804ea8b016d239e706191203f6efac"
        }
    },
    "providers-url": "/p/%package%$%hash%.json"
}

Эти файлы содержат списки имен пакетов и хэшей для проверки целостности файла, например:

{
    "providers": {
        "acme/foo": {
            "sha256": "38968de1305c2e17f4de33aea164515bc787c42c7e2d6e25948539a14268bb82"
        },
        "acme/bar": {
            "sha256": "4dd24c930bd6e1103251306d6336ac813b563a220d9ca14f4743c032fb047233"
        }
    }
}

Приведенный выше файл объявляет, что acme/foo и acme/bar можно найти в этом хранилище, загрузив файл, на который ссылается providers-url, заменив %package% на имя пакета с именем поставщика и %hash% на поле sha256. Сами эти файлы содержат определения пакетов, как описано выше.

Эти поля необязательны. Возможно, они не понадобятся для вашего собственного репозитория.

cURL или stream options

Доступ к хранилищу осуществляется либо с помощью cURL (Composer 2 с включенным ext-curl), либо с помощью PHP-потоков. При помощи параметра options можно задать дополнительные опции. Для PHP-потоков можно задать любой допустимый параметр контекста PHP-потока. Дополнительную информацию смотрите в разделе Опции и параметры контекста. При использовании cURL можно настроить только ограниченный набор параметров http и ssl.

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://example.org",
            "options": {
                "http": {
                    "timeout": 60
                }
            }
        }
    ],
    "require": {
        "acme/package": "^1.0"
    }
}

VCS (version control system)

VCS - это система контроля версий. Сюда входят такие системы управления версиями, как git, svn, fossil или hg. Composer поддерживает тип хранилища для установки пакетов из этих систем.

Загрузка пакета из репозитория VCS

Для этого есть несколько вариантов использования. Самый распространенный из них - поддержка собственного расширения сторонней библиотеки. Если вы используете определенную библиотеку для своего проекта и решили что-то изменить в ней, вы захотите, чтобы ваш проект использовал исправленную версию. Если библиотека находится на GitHub (так чаще всего и бывает), вы можете форкнуть ее там и перенести изменения в свой форк. После этого вы обновите composer.json проекта. Все, что вам нужно сделать, это добавить ваш форк в качестве репозитория и обновить ограничение версии, чтобы оно указывало на вашу пользовательскую ветку. Только в файле composer.json имя вашей пользовательской ветки должно иметь префикс "dev-" (не делая его частью фактического имени ветки). Соглашения об именовании ограничений версий смотрите в разделе Библиотеки для получения дополнительной информации.

Пример, если вы исправили ошибку в monolog в ветке bugfix:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/igorw/monolog"
        }
    ],
    "require": {
        "monolog/monolog": "dev-bugfix"
    }
}

После запуска php composer.phar update вы должны получить модифицированную версию monolog/monolog вместо версии из packagist.

Обратите внимание, что вам не следует переименовывать пакет, если только вы не собираетесь использовать его в долгосрочной перспективе и полностью отказаться от оригинального пакета. Composer правильно выберет ваш пакет вместо оригинального, поскольку пользовательский репозиторий имеет приоритет над packagist. Если вы хотите переименовать пакет, вам следует сделать это в ветке по умолчанию (часто master), а не в ветке feature, поскольку имя пакета берется из ветки по умолчанию.

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

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

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

Точно такое же решение позволяет работать с частными репозиториями на GitHub и Bitbucket:

{
    "repositories": [
        {
            "type": "vcs",
            "url":  "[email protected]:vendor/my-private-repo.git"
        }
    ],
    "require": {
        "vendor/my-private-repo": "dev-master"
    }
}

Единственное требование - это установка SSH-ключей для git-клиента.

Варианты Git

Git - не единственная система контроля версий, поддерживаемая репозиторием VCS. Поддерживаются следующие:

Чтобы получить пакеты из этих систем, необходимо установить соответствующие клиенты. Это может быть неудобно. По этой причине существует специальная поддержка GitHub и Bitbucket, которая использует API, предоставляемые этими сайтами, для получения пакетов без необходимости установки системы контроля версий. Репозиторий VCS предоставляет для них дистрибутивы, которые извлекают пакеты в виде zip-архивов.

Используемый драйвер VCS определяется автоматически на основе URL. Однако если по каким-то причинам вам необходимо указать его, вы можете использовать bitbucket, github, gitlab, perforce, fossil, git, svn или hg в качестве типа репозитория вместо vcs.

Если вы установите ключ no-api в true для репозитория github, он будет клонировать репозиторий, как и любой другой репозиторий git, вместо того, чтобы использовать API GitHub. Но в отличие от прямого использования драйвера git, Composer все равно попытается использовать zip-файлы github.

Обратите внимание:

  • Чтобы Composer мог выбирать, какой драйвер использовать, "type" репозитория должен быть определен как "vcs".
  • Если вы уже использовали частный репозиторий, это означает, что Composer должен был клонировать его в кэш. Если вы хотите установить тот же пакет с драйверами, не забудьте запустить команду composer clearcache, а затем команду composer update для обновления кэша Composer и установки пакета из dist.
  • Драйвер VCS git-bitbucket устарел в пользу bitbucket

Настройка Bitbucket Driver

Обратите внимание, что конечная точка репозитория для Bitbucket должна быть https, а не git.

После настройки репозитория bitbucket вам также нужно будет настроить аутентификацию.

Настройка Subversion

Поскольку Subversion не имеет собственной концепции ветвей и тегов, Composer по умолчанию предполагает, что код расположен в $url/trunk, $url/branches и $url/tags. Если ваше хранилище имеет другую структуру, вы можете изменить эти значения. Например, если вы используете имена с заглавными буквами, то можно настроить хранилище следующим образом:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "http://svn.example.org/projectA/",
            "trunk-path": "Trunk",
            "branches-path": "Branches",
            "tags-path": "Tags"
        }
    ]
}

Если у вас нет каталога branches или tags, вы можете полностью отключить их, установив для branches-path или tags-path значение false.

Если пакет находится в подкаталоге, например, /trunk/foo/bar/composer.json и /tags/1.0/foo/bar/composer.json, то можно заставить Composer получить к нему доступ, установив параметр "package-path" в подкаталог, в данном примере это будет "package-path": "foo/bar/".

Если у вас есть частный репозиторий Subversion, вы можете сохранить учетные данные в разделе http-basic вашего конфига (см. Схема):

{
    "http-basic": {
        "svn.example.org": {
            "username": "username",
            "password": "password"
        }
    }
}

Если ваш клиент Subversion настроен на хранение учетных данных по умолчанию, эти учетные данные будут сохранены для текущего пользователя, а существующие сохраненные учетные данные для этого сервера будут перезаписаны. Чтобы изменить это поведение, установите опцию "svn-cache-credentials" в конфигурации вашего репозитория:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "http://svn.example.org/projectA/",
            "svn-cache-credentials": false
        }
    ]
}

Package

Если вы хотите использовать проект, который не поддерживает Composer ни одним из вышеперечисленных способов, вы все равно можете определить пакет самостоятельно, используя репозиторий package.

По сути, вы определяете ту же информацию, которая включена в packages.json репозитория composer, но только для одного пакета. Опять же, минимально необходимыми полями являются name, version, и либо dist, либо source.

Вот пример для шаблонизатора smarty:

{
    "repositories": [
        {
            "type": "package",
            "package": {
                "name": "smarty/smarty",
                "version": "3.1.7",
                "dist": {
                    "url": "https://www.smarty.net/files/Smarty-3.1.7.zip",
                    "type": "zip"
                },
                "source": {
                    "url": "http://smarty-php.googlecode.com/svn/",
                    "type": "svn",
                    "reference": "tags/Smarty_3_1_7/distribution/"
                },
                "autoload": {
                    "classmap": ["libs/"]
                }
            }
        }
    ],
    "require": {
        "smarty/smarty": "3.1.*"
    }
}

Обычно вы не указываете часть с исходным кодом, поскольку она вам не нужна.

Примечание: Этот тип хранилища имеет несколько ограничений, и его следует избегать, когда это возможно:

  • Composer не будет обновлять пакет, если вы не измените поле version.
  • Composer не будет обновлять ссылки на коммит, поэтому если вы используете master в качестве ссылки, вам придётся удалить пакет, чтобы принудительно обновить его, и вам придётся иметь дело с нестабильным файлом блокировки.

Ключ "package" в пакете package может быть задан в виде массива для определения нескольких версий пакета:

{
    "repositories": [
        {
            "type": "package",
            "package": [
                {
                    "name": "foo/bar",
                    "version": "1.0.0",
                    ...
                },
                {
                    "name": "foo/bar",
                    "version": "2.0.0",
                    ...
                }
            ]
        }
    ]
}

Размещение собственного пакета Composer

Хотя вы, вероятно, захотите размещать свои пакеты на packagist большую часть времени, есть несколько вариантов использования собственного хранилища.

  • Пакеты для частных компаний:
    • Если вы являетесь частью компании, которая использует Composer для своих пакетов внутри компании, вы можете захотеть разместить эти пакеты у себя в закрытом хранилище.
  • Отдельная экосистема:
    • Если у вас есть проект, который имеет свою собственную экосистему, и пакеты не очень пригодны для повторного использования большим сообществом PHP, вы можете захотеть сохранить их отдельно в packagist. Примером могут служить плагины WordPress.

Для размещения собственных пакетов рекомендуется использовать нативный тип composer, который обеспечивает наилучшую производительность.

Существует несколько инструментов, которые могут помочь вам создать репозиторий composer.

Private Packagist

Private Packagist — это приложение, размещаемое на хостинге или самостоятельно, обеспечивающее частный хостинг пакетов, а также зеркалирование GitHub, Packagist.org и других репозиториев пакетов.

Для получения дополнительной информации посетите Packagist.com.

Satis

Satis - это генератор статических репозиториев composer. Это немного похоже на сверхлегкую, основанную на статических файлах версию packagist.

Вы передаете ему composer.json, содержащий репозитории, как правило, VCS и определения репозиториев пакетов. Он получит все пакеты, которые необходимы (require), и выгрузит packages.json, который и будет вашим репозиторием composer.

Для получения дополнительной информации ознакомьтесь с репозиторием satis GitHub и статьей о работе с приватными пакетами.

Artifact

Бывают случаи, когда нет возможности иметь один из ранее упомянутых типов репозиториев онлайн, даже VCS. Типичным примером может быть межорганизационный обмен библиотеками через артефакты сборки. Конечно, в большинстве случаев они являются приватными. Чтобы использовать эти архивы как есть, можно использовать репозиторий типа artifact с папкой, содержащей ZIP или TAR архивы этих частных пакетов:

{
    "repositories": [
        {
            "type": "artifact",
            "url": "path/to/directory/with/zips/"
        }
    ],
    "require": {
        "private-vendor-one/core": "15.6.2",
        "private-vendor-two/connectivity": "*",
        "acme-corp/parser": "10.3.5"
    }
}

Каждый zip artifact представляет собой ZIP-архив с composer.json в корневой папке:

unzip -l acme-corp-parser-10.3.5.zip
composer.json
...

Если есть два архива с разными версиями пакета, они импортируются оба. Если в папку с артефактами добавлен архив с более новой версией и запущен update, эта версия также будет импортирована, и Composer обновится до последней версии.

Маршрут до пакета Composer

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

Например, если в вашем репозитории имеется следующая структура каталогов:

...
├── apps
│   └── my-app
│       └── composer.json
├── packages
│   └── my-package
│       └── composer.json
...

Тогда, чтобы добавить пакет my/package в качестве зависимости в файл apps/my-app/composer.json, вы можете использовать следующую конфигурацию:

{
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package"
        }
    ],
    "require": {
        "my/package": "*"
    }
}

Если пакет является локальным VCS-репозиторием, версия может быть определена по ветке или метке, которая в данный момент проверяется. В противном случае версия должна быть явно определена в файле composer.json пакета. Если версия не может быть определена этими способами, предполагается, что это dev-master.

Если версия не может быть определена из локального VCS-репозитория или если вы хотите переопределить версию, вы можете использовать опцию versions при объявлении репозитория:

{
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package",
            "options": {
                "versions": {
                    "my/package": "4.2-dev"
                }
            }
        }
    ]
}

Локальный пакет будет присоединен по симлинку, если это возможно, в этом случае в консоли будет написано Symlinking from ../../packages/my-package. Если привязка по симлинку невозможна, пакет будет скопирован. В этом случае в консоли будет выведено Mirrored from ../../packages/my-package.

Вместо стратегии отката по умолчанию вы можете принудительно использовать симлинк с помощью опции "symlink": true или зеркалирование с помощью опции "symlink": false. Принудительное зеркалирование может быть полезно при развертывании или генерации пакета из монолитного репозитория.

Примечание: В Windows симлинки каталогов реализованы с использованием NTFS-переходов, поскольку их могут создавать пользователи, не являющиеся администраторами. Зеркалирование всегда будет использоваться в версиях ниже Windows 7 или если proc_open был отключен.

{
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package",
            "options": {
                "symlink": false
            }
        }
    ]
}

Ведущие тильды расширяются до домашней папки текущего пользователя, а переменные окружения анализируются в нотациях Windows и Linux/Mac. Например, ~/git/mypackage автоматически загрузит клон mypackage из /home/<username>/git/mypackage, что эквивалентно $HOME/git/mypackage или %USERPROFILE%/git/mypackage.

Примечание: Пути к репозиториям также могут содержать подстановочные знаки, такие как * и ? Подробнее об этом смотрите в функции PHP glob{}.

Вы можете настроить способ создания dist-ссылки пакета (которая появляется в файле composer.lock).

Существуют следующие режимы:

  • none - ссылка всегда будет нулевой. Это может помочь уменьшить конфликты в файле блокировки, но снижает ясность относительно того, когда произошло последнее обновление и находится ли пакет в актуальном состоянии.
  • config - ссылка строится на основе хэша пакета composer.json и конфига repo.
  • auto (используется по умолчанию) - ссылка строится на основе хэша, как и в случае с config, но если папка пакета содержит git-репозиторий, то в качестве ссылки используется хэш HEAD-коммита.
{
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package",
            "options": {
                "reference": "config"
            }
        }
    ]
}

Отключение Packagist.org в composer.json

Вы можете отключить репозиторий Packagist.org по умолчанию, добавив это в composer.json:

{
    "repositories": [
        {
            "packagist.org": false
        }
    ]
}

Вы можете отключить Packagist.org глобально, используя флаг глобальной конфигурации:

php composer.phar config -g repo.packagist false

Перевод с английского официальной документации Composer:
https://getcomposer.org/doc/05-repositories.md

Заберите ссылку на статью к себе, чтобы потом легко её найти!
Раз уж досюда дочитали, то может может есть желание рассказать об этом месте своим друзьям, знакомым и просто мимо проходящим?
Не надо себя сдерживать! ;)

Старт! Горячий старт на просторы интернета
Старт! Горячий старт на просторы интернета
Старт! Меню