Commit a8127491 authored by Sergey Shadrin's avatar Sergey Shadrin

[#124455] Update module patch, git removed composer packages to install drupal

parent d5d3a9a4
......@@ -5,8 +5,16 @@ sites/*/files/*
sites/*/private
sites/*/settings.php
# Exclude update module
!core/modules/update/update.info.yml
core/modules/update
# Exclude IDE specific directories.
.idea
.vscode
# Exclude composer plugin vendor packages.
vendor/drupal/core-composer-scaffold
vendor/drupal/core-project-message
vendor/oomphinc/composer-installers-extender
vendor/cweagans/composer-patches
vendor/wikimedia/composer-merge-plugin
name: 'Update Manager'
type: module
description: 'Не поддерживается в ДАР CMS'
version: VERSION
package: Core
......@@ -41,3 +41,15 @@ index 2de1283198..208fc8125d 100644
return $output;
case 'system.modules_uninstall':
diff --git a/core/modules/update/update.info.yml b/core/modules/update/update.info.yml
index 684c544b39..8321d08e93 100644
--- a/core/modules/update/update.info.yml
+++ b/core/modules/update/update.info.yml
@@ -1,6 +1,5 @@
name: 'Update Manager'
type: module
-description: 'Checks for updates and allows users to manage them through a user interface.'
+description: 'Не поддерживается в ДАР CMS'
version: VERSION
package: Core
-configure: update.settings
# Russian translation of CKEditor 5 Plugin Pack (1.0.1)
# Copyright (c) 2024 by the Russian translation team
#
msgid ""
msgstr ""
"Project-Id-Version: CKEditor 5 Plugin Pack (1.0.1)\n"
"POT-Creation-Date: 2024-04-30 08:46+0000\n"
"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
"Language-Team: Russian\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=((((n%10)==1)&&((n%100)!=11))?(0):(((((n%10)>=2)&&((n%10)<=4))&&(((n%100)<10)||((n%100)>=20)))?(1):2));\n"
msgid "Status"
msgstr "Статус"
msgid "Type"
msgstr "Тип"
msgid "Remove"
msgstr "Удалить"
msgid "Description"
msgstr "Описание"
msgid "Disabled"
msgstr "Отключено"
msgid "Enabled"
msgstr "Включено"
msgid "Label"
msgstr "Метка"
msgid "Font Size"
msgstr "Размер шрифта"
msgid "Background Color"
msgstr "Цвет фона"
msgid "Options"
msgstr "Настройки"
msgid "Colors"
msgstr "Цвета"
msgid "Color"
msgstr "Цвет"
msgid "Marker"
msgstr "Маркер"
msgid "Font Family"
msgstr "Семейство шрифтов"
msgid "Formats"
msgstr "Форматы"
msgid "Machine name"
msgstr "Машинное имя"
msgid "Icon preview"
msgstr "Пред просмотр иконки"
msgid "Font Color"
msgstr "Цвет шрифта"
msgid "HTML Code"
msgstr "HTML Code"
msgid "Custom classes"
msgstr "Пользовательские классы"
......@@ -3,7 +3,7 @@
'name' => 'drupal/recommended-project',
'pretty_version' => '10.3.x-dev',
'version' => '10.3.9999999.9999999-dev',
'reference' => '69c1608e100aa2f0d6f1e5e9fa488312ccfedbfc',
'reference' => 'd5d3a9a4c04c7d1e169cf2acf18008a3e4c73fc6',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
......@@ -1102,7 +1102,7 @@
'drupal/recommended-project' => array(
'pretty_version' => '10.3.x-dev',
'version' => '10.3.9999999.9999999-dev',
'reference' => '69c1608e100aa2f0d6f1e5e9fa488312ccfedbfc',
'reference' => 'd5d3a9a4c04c7d1e169cf2acf18008a3e4c73fc6',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
......
# This is the top-most .editorconfig file; do not search in parent directories.
root = true
# All files.
[*]
end_of_line = LF
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
Copyright 2013 Cameron Eagans
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# composer-patches
Simple patches plugin for Composer. Applies a patch from a local or remote file to any package required with composer.
Note that the 1.x versions of Composer Patches are supported on a best-effort
basis due to the imminent release of 2.0.0. You may still be interested in
using 1.x if you need Composer to cooperate with earlier PHP versions. No new
features will be added to 1.x releases, but any security or bug fixes will
still be accepted.
## Usage
Example composer.json:
```json
{
"require": {
"cweagans/composer-patches": "~1.0",
"drupal/drupal": "~8.2"
},
"config": {
"preferred-install": "source"
},
"extra": {
"patches": {
"drupal/drupal": {
"Add startup configuration for PHP server": "https://www.drupal.org/files/issues/add_a_startup-1543858-30.patch"
}
}
}
}
```
## Using an external patch file
Instead of a patches key in your root composer.json, use a patches-file key.
```json
{
"require": {
"cweagans/composer-patches": "~1.0",
"drupal/drupal": "~8.2"
},
"config": {
"preferred-install": "source"
},
"extra": {
"patches-file": "local/path/to/your/composer.patches.json"
}
}
```
Then your `composer.patches.json` should look like this:
```
{
"patches": {
"vendor/project": {
"Patch title": "http://example.com/url/to/patch.patch"
}
}
}
```
## Allowing patches to be applied from dependencies
If your project doesn't supply any patches of its own, but you still want to accept patches from dependencies, you must have the following in your composer file:
```json
{
"require": {
"cweagans/composer-patches": "^1.5.0"
},
"extra": {
"enable-patching": true
}
}
```
If you do have a `patches` section in your composer file that defines your own set of patches then the `enable-patching` setting will be ignored and patches from dependencies will always be applied.
## Ignoring patches
There may be situations in which you want to ignore a patch supplied by a dependency. For example:
- You use a different more recent version of a dependency, and now a patch isn't applying.
- You have a more up to date patch than the dependency, and want to use yours instead of theirs.
- A dependency's patch adds a feature to a project that you don't need.
- Your patches conflict with a dependency's patches.
```json
{
"require": {
"cweagans/composer-patches": "~1.0",
"drupal/drupal": "~8.2",
"drupal/lightning": "~8.1"
},
"config": {
"preferred-install": "source"
},
"extra": {
"patches": {
"drupal/drupal": {
"Add startup configuration for PHP server": "https://www.drupal.org/files/issues/add_a_startup-1543858-30.patch"
}
},
"patches-ignore": {
"drupal/lightning": {
"drupal/panelizer": {
"This patch has known conflicts with our Quick Edit integration": "https://www.drupal.org/files/issues/2664682-49.patch"
}
}
}
}
}
```
## Allowing to force the patch level (-pX)
Some situations require to force the patchLevel used to apply patches on a particular package.
Its useful for packages like drupal/core which packages only a subdir of the original upstream project on which patches are based.
```json
{
"extra": {
"patchLevel": {
"drupal/core": "-p2"
}
}
}
```
## Using patches from HTTP URLs
Composer [blocks](https://getcomposer.org/doc/06-config.md#secure-http) you from downloading anything from HTTP URLs, you can disable this for your project by adding a `secure-http` setting in the config section of your `composer.json`. Note that the `config` section should be under the root of your `composer.json`.
```json
{
"config": {
"secure-http": false
}
}
```
However, it's always advised to setup HTTPS to prevent MITM code injection.
## Patches containing modifications to composer.json files
Because patching occurs _after_ Composer calculates dependencies and installs packages, changes to an underlying dependency's `composer.json` file introduced in a patch will have _no effect_ on installed packages.
If you need to modify a dependency's `composer.json` or its underlying dependencies, you cannot use this plugin. Instead, you must do one of the following:
- Work to get the underlying issue resolved in the upstream package.
- Fork the package and [specify your fork as the package repository](https://getcomposer.org/doc/05-repositories.md#vcs) in your root `composer.json`
- Specify compatible package version requirements in your root `composer.json`
## Error handling
If a patch cannot be applied (hunk failed, different line endings, etc.) a message will be shown and the patch will be skipped.
To enforce throwing an error and stopping package installation/update immediately, you have two available options:
1. Add `"composer-exit-on-patch-failure": true` option to the `extra` section of your composer.json file.
1. Export `COMPOSER_EXIT_ON_PATCH_FAILURE=1`
By default, failed patches are skipped.
## Patches reporting
When a patch is applied, the plugin writes a report-file `PATCHES.txt` to a patching directory (e.g. `./patch-me/PATCHES.txt`),
which contains a list of applied patches.
If you want to avoid this behavior, add a specific key to the `extra` section:
```json
"extra": {
"composer-patches-skip-reporting": true
}
```
Or provide an environment variable `COMPOSER_PATCHES_SKIP_REPORTING` with a config.
## Patching composer.json in dependencies
This doesn't work like you'd want. By the time you're running `composer install`,
the metadata from your dependencies' composer.json has already been aggregated by
packagist (or whatever metadata repo you're using). Unfortunately, this means that
you cannot e.g. patch a dependency to be compatible with an earlier version of PHP
or change the framework version that a plugin depends on.
@anotherjames over at @computerminds wrote an article about how to work around
that particular problem for a Drupal 8 -> Drupal 9 upgrade:
[Apply Drupal 9 compatibility patches with Composer](https://www.computerminds.co.uk/articles/apply-drupal-9-compatibility-patches-composer) ([archive](https://web.archive.org/web/20210124171010/https://www.computerminds.co.uk/articles/apply-drupal-9-compatibility-patches-composer))
## Difference between this and netresearch/composer-patches-plugin
- This plugin is much more simple to use and maintain
- This plugin doesn't require you to specify which package version you're patching
- This plugin is easy to use with Drupal modules (which don't use semantic versioning).
- This plugin will gather patches from all dependencies and apply them as if they were in the root composer.json
## Credits
A ton of this code is adapted or taken straight from https://github.com/jpstacey/composer-patcher, which is abandoned in favor of https://github.com/netresearch/composer-patches-plugin, which is (IMHO) overly complex and difficult to use.
{
"name": "cweagans/composer-patches",
"description": "Provides a way to patch Composer packages.",
"minimum-stability": "dev",
"license": "BSD-3-Clause",
"type": "composer-plugin",
"extra": {
"class": "cweagans\\Composer\\Patches"
},
"authors": [
{
"name": "Cameron Eagans",
"email": "me@cweagans.net"
}
],
"require": {
"php": ">=5.3.0",
"composer-plugin-api": "^1.0 || ^2.0"
},
"require-dev": {
"composer/composer": "~1.0 || ~2.0",
"phpunit/phpunit": "~4.6"
},
"autoload": {
"psr-4": {"cweagans\\Composer\\": "src"}
},
"autoload-dev": {
"psr-4": {"cweagans\\Composer\\Tests\\": "tests"}
}
}
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4a5c841252204815536a37cad51d347b",
"packages": [],
"packages-dev": [
{
"name": "composer/ca-bundle",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
"reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd",
"reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-pcre": "*",
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
"psr/log": "^1.0",
"symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\CaBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
"keywords": [
"cabundle",
"cacert",
"certificate",
"ssl",
"tls"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-04-08T08:27:21+00:00"
},
{
"name": "composer/composer",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
"reference": "870fdc59dfcffe0bd2d43ca2de4235761d0dec7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/composer/zipball/870fdc59dfcffe0bd2d43ca2de4235761d0dec7a",
"reference": "870fdc59dfcffe0bd2d43ca2de4235761d0dec7a",
"shasum": ""
},
"require": {
"composer/ca-bundle": "^1.0",
"composer/semver": "^3.0",
"composer/spdx-licenses": "^1.2",
"composer/xdebug-handler": "^1.1",
"justinrainbow/json-schema": "^5.2.10",
"php": "^5.3.2 || ^7.0 || ^8.0",
"psr/log": "^1.0",
"react/promise": "^1.2 || ^2.7",
"seld/jsonlint": "^1.4",
"seld/phar-utils": "^1.0",
"symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
"symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
"symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
"symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0"
},
"require-dev": {
"phpspec/prophecy": "^1.10",
"symfony/phpunit-bridge": "^4.2 || ^5.0"
},
"suggest": {
"ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
"ext-zip": "Enabling the zip extension allows you to unzip archives",
"ext-zlib": "Allow gzip compression of HTTP requests"
},
"bin": [
"bin/composer"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\": "src/Composer"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "https://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
"homepage": "https://getcomposer.org/",
"keywords": [
"autoload",
"dependency",
"package"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-07-15T15:02:16+00:00"
},
{
"name": "composer/semver",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "00915994bb1de62e750ae279669c9c5a57379957"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/00915994bb1de62e750ae279669c9c5a57379957",
"reference": "00915994bb1de62e750ae279669c9c5a57379957",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.19",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-05-31T11:44:06+00:00"
},
{
"name": "composer/spdx-licenses",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/composer/spdx-licenses.git",
"reference": "6946f785871e2314c60b4524851f3702ea4f2223"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/spdx-licenses/zipball/6946f785871e2314c60b4524851f3702ea4f2223",
"reference": "6946f785871e2314c60b4524851f3702ea4f2223",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Spdx\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "SPDX licenses list and validation library.",
"keywords": [
"license",
"spdx",
"validator"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-07-15T15:35:07+00:00"
},
{
"name": "composer/xdebug-handler",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
"reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51",
"reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0",
"psr/log": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8"
},
"type": "library",
"autoload": {
"psr-4": {
"Composer\\XdebugHandler\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "John Stevenson",
"email": "john-stevenson@blueyonder.co.uk"
}
],
"description": "Restarts a process without Xdebug.",
"keywords": [
"Xdebug",
"performance"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-06-04T11:16:35+00:00"
},
{
"name": "doctrine/instantiator",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
"shasum": ""
},
"require": {
"php": ">=5.3,<8.0-DEV"
},
"require-dev": {
"athletic/athletic": "~0.1.8",
"ext-pdo": "*",
"ext-phar": "*",
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"homepage": "http://ocramius.github.com/"
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
"homepage": "https://github.com/doctrine/instantiator",
"keywords": [
"constructor",
"instantiate"
],
"time": "2015-06-14T21:17:01+00:00"
},
{
"name": "justinrainbow/json-schema",
"version": "5.x-dev",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
"reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
"bin": [
"bin/validate-json"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0.x-dev"
}
},
"autoload": {
"psr-4": {
"JsonSchema\\": "src/JsonSchema/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bruno Prieto Reis",
"email": "bruno.p.reis@gmail.com"
},
{
"name": "Justin Rainbow",
"email": "justin.rainbow@gmail.com"
},
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
},
{
"name": "Robert Schönthal",
"email": "seroscho@googlemail.com"
}
],
"description": "A library to validate a json schema.",
"homepage": "https://github.com/justinrainbow/json-schema",
"keywords": [
"json",
"schema"
],
"time": "2020-05-27T16:41:55+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"suggest": {
"dflydev/markdown": "~1.0",
"erusev/parsedown": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-0": {
"phpDocumentor": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"time": "2015-02-03T12:10:50+00:00"
},
{
"name": "phpspec/prophecy",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "4f9b1eaf0a7da77c362f8d91cbc68ab1f4718d62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/4f9b1eaf0a7da77c362f8d91cbc68ab1f4718d62",
"reference": "4f9b1eaf0a7da77c362f8d91cbc68ab1f4718d62",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"phpdocumentor/reflection-docblock": "~2.0",
"sebastian/comparator": "~1.1"
},
"require-dev": {
"phpspec/phpspec": "~2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.5.x-dev"
}
},
"autoload": {
"psr-0": {
"Prophecy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
},
{
"name": "Marcello Duarte",
"email": "marcello.duarte@gmail.com"
}
],
"description": "Highly opinionated mocking framework for PHP 5.3+",
"homepage": "https://github.com/phpspec/prophecy",
"keywords": [
"Double",
"Dummy",
"fake",
"mock",
"spy",
"stub"
],
"time": "2015-09-22T14:49:23+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "2.2.x-dev",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979",
"reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"phpunit/php-file-iterator": "~1.3",
"phpunit/php-text-template": "~1.2",
"phpunit/php-token-stream": "~1.3",
"sebastian/environment": "^1.3.2",
"sebastian/version": "~1.0"
},
"require-dev": {
"ext-xdebug": ">=2.1.4",
"phpunit/phpunit": "~4"
},
"suggest": {
"ext-dom": "*",
"ext-xdebug": ">=2.2.1",
"ext-xmlwriter": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
"homepage": "https://github.com/sebastianbergmann/php-code-coverage",
"keywords": [
"coverage",
"testing",
"xunit"
],
"time": "2015-10-06T15:47:00+00:00"
},
{
"name": "phpunit/php-file-iterator",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
"reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "FilterIterator implementation that filters files based on a list of suffixes.",
"homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
"keywords": [
"filesystem",
"iterator"
],
"time": "2015-06-21T13:08:43+00:00"
},
{
"name": "phpunit/php-text-template",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-text-template.git",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Simple template engine.",
"homepage": "https://github.com/sebastianbergmann/php-text-template/",
"keywords": [
"template"
],
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "phpunit/php-timer",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
"reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
"reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Utility class for timing",
"homepage": "https://github.com/sebastianbergmann/php-timer/",
"keywords": [
"timer"
],
"time": "2015-06-21T08:01:12+00:00"
},
{
"name": "phpunit/php-token-stream",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cab6c6fefee93d7b7c3a01292a0fe0884ea66644",
"reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Wrapper around PHP's tokenizer extension.",
"homepage": "https://github.com/sebastianbergmann/php-token-stream/",
"keywords": [
"tokenizer"
],
"time": "2015-09-23T14:46:55+00:00"
},
{
"name": "phpunit/phpunit",
"version": "4.8.x-dev",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "be067d6105286b74272facefc2697038f8807b77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/264188ddf4d3586c80ea615f8ec8eaea34e652a1",
"reference": "be067d6105286b74272facefc2697038f8807b77",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-json": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-spl": "*",
"php": ">=5.3.3",
"phpspec/prophecy": "^1.3.1",
"phpunit/php-code-coverage": "~2.1",
"phpunit/php-file-iterator": "~1.4",
"phpunit/php-text-template": "~1.2",
"phpunit/php-timer": ">=1.0.6",
"phpunit/phpunit-mock-objects": "~2.3",
"sebastian/comparator": "~1.1",
"sebastian/diff": "~1.2",
"sebastian/environment": "~1.3",
"sebastian/exporter": "~1.2",
"sebastian/global-state": "~1.0",
"sebastian/version": "~1.0",
"symfony/yaml": "~2.1|~3.0"
},
"suggest": {
"phpunit/php-invoker": "~1.1"
},
"bin": [
"phpunit"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.8.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "The PHP Unit Testing framework.",
"homepage": "https://phpunit.de/",
"keywords": [
"phpunit",
"testing",
"xunit"
],
"time": "2015-10-14T13:49:40+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "2.3.x-dev",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983",
"reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": ">=5.3.3",
"phpunit/php-text-template": "~1.2",
"sebastian/exporter": "~1.2"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"suggest": {
"ext-soap": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
"description": "Mock Object library for PHPUnit",
"homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
"keywords": [
"mock",
"xunit"
],
"abandoned": true,
"time": "2015-10-02T06:51:40+00:00"
},
{
"name": "psr/container",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "fc1bc363ecf887921e3897c7b1dad3587ae154eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/fc1bc363ecf887921e3897c7b1dad3587ae154eb",
"reference": "fc1bc363ecf887921e3897c7b1dad3587ae154eb",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"time": "2019-10-04T14:07:35+00:00"
},
{
"name": "psr/log",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2020-03-23T09:12:05+00:00"
},
{
"name": "react/promise",
"version": "2.x-dev",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4",
"reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"keywords": [
"promise",
"promises"
],
"time": "2020-05-12T15:16:56+00:00"
},
{
"name": "sebastian/comparator",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "937efb279bd37a375bcadf584dec0726f84dbf22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22",
"reference": "937efb279bd37a375bcadf584dec0726f84dbf22",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"sebastian/diff": "~1.2",
"sebastian/exporter": "~1.2"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Provides the functionality to compare PHP values for equality",
"homepage": "http://www.github.com/sebastianbergmann/comparator",
"keywords": [
"comparator",
"compare",
"equality"
],
"time": "2015-07-26T15:48:44+00:00"
},
{
"name": "sebastian/diff",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "6899b3e33bfbd386d88b5eea5f65f563e8793051"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6899b3e33bfbd386d88b5eea5f65f563e8793051",
"reference": "6899b3e33bfbd386d88b5eea5f65f563e8793051",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Diff implementation",
"homepage": "http://www.github.com/sebastianbergmann/diff",
"keywords": [
"diff"
],
"time": "2015-06-22T14:15:55+00:00"
},
{
"name": "sebastian/environment",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "6324c907ce7a52478eeeaede764f48733ef5ae44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
"reference": "6324c907ce7a52478eeeaede764f48733ef5ae44",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Provides functionality to handle HHVM/PHP environments",
"homepage": "http://www.github.com/sebastianbergmann/environment",
"keywords": [
"Xdebug",
"environment",
"hhvm"
],
"time": "2015-08-03T06:14:51+00:00"
},
{
"name": "sebastian/exporter",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "f88f8936517d54ae6d589166810877fb2015d0a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f88f8936517d54ae6d589166810877fb2015d0a2",
"reference": "f88f8936517d54ae6d589166810877fb2015d0a2",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"sebastian/recursion-context": "~1.0"
},
"require-dev": {
"ext-mbstring": "*",
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
}
],
"description": "Provides the functionality to export PHP variables for visualization",
"homepage": "http://www.github.com/sebastianbergmann/exporter",
"keywords": [
"export",
"exporter"
],
"time": "2015-08-09T04:23:41+00:00"
},
{
"name": "sebastian/global-state",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
"reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
},
"suggest": {
"ext-uopz": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Snapshotting of global state",
"homepage": "http://www.github.com/sebastianbergmann/global-state",
"keywords": [
"global state"
],
"time": "2015-10-12T03:26:01+00:00"
},
{
"name": "sebastian/recursion-context",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "994d4a811bafe801fb06dccbee797863ba2792ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba",
"reference": "994d4a811bafe801fb06dccbee797863ba2792ba",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phpunit/phpunit": "~4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
}
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"time": "2015-06-21T08:04:50+00:00"
},
{
"name": "sebastian/version",
"version": "1.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
"reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
"shasum": ""
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2015-06-21T13:59:46+00:00"
},
{
"name": "seld/jsonlint",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/jsonlint.git",
"reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1",
"reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1",
"shasum": ""
},
"require": {
"php": "^5.3 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
},
"bin": [
"bin/jsonlint"
],
"type": "library",
"autoload": {
"psr-4": {
"Seld\\JsonLint\\": "src/Seld/JsonLint/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "JSON Linter",
"keywords": [
"json",
"linter",
"parser",
"validator"
],
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/seld/jsonlint",
"type": "tidelift"
}
],
"time": "2020-04-30T19:05:18+00:00"
},
{
"name": "seld/phar-utils",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/phar-utils.git",
"reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8674b1d84ffb47cc59a101f5d5a3b61e87d23796",
"reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Seld\\PharUtils\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be"
}
],
"description": "PHAR file format utilities, for when PHP phars you up",
"keywords": [
"phar"
],
"time": "2020-07-07T18:42:57+00:00"
},
{
"name": "symfony/console",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "e4a70bd8c5a4382630197b7b87910b3fc0e6b526"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/e4a70bd8c5a4382630197b7b87910b3fc0e6b526",
"reference": "e4a70bd8c5a4382630197b7b87910b3fc0e6b526",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.8",
"symfony/polyfill-php80": "^1.15",
"symfony/service-contracts": "^1.1|^2",
"symfony/string": "^5.1"
},
"conflict": {
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
"symfony/lock": "<4.4",
"symfony/process": "<4.4"
},
"provide": {
"psr/log-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "^4.4|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/event-dispatcher": "^4.4|^5.0",
"symfony/lock": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0",
"symfony/var-dumper": "^4.4|^5.0"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command line",
"console",
"terminal"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-06T13:25:45+00:00"
},
{
"name": "symfony/filesystem",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "e7550993849f986f01a9161b302d4aed8d4aab0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/e7550993849f986f01a9161b302d4aed8d4aab0a",
"reference": "e7550993849f986f01a9161b302d4aed8d4aab0a",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-05-30T20:38:10+00:00"
},
{
"name": "symfony/finder",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "2a63a45741144325f84d28ea1e67bc1b669b1748"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/2a63a45741144325f84d28ea1e67bc1b669b1748",
"reference": "2a63a45741144325f84d28ea1e67bc1b669b1748",
"shasum": ""
},
"require": {
"php": ">=7.2.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-05-20T17:44:07+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
"reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.18-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-14T12:35:20+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5",
"reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.18-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's grapheme_* functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"grapheme",
"intl",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-14T12:35:20+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
"reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.18-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-14T12:35:20+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a",
"reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.18-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-14T12:35:20+00:00"
},
{
"name": "symfony/polyfill-php73",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
"reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
"reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.18-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php73\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-14T12:35:20+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981",
"reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981",
"shasum": ""
},
"require": {
"php": ">=7.0.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.18-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-14T12:35:20+00:00"
},
{
"name": "symfony/process",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "d158a452d952049e0e55b7cfe5f360c973edc57c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/d158a452d952049e0e55b7cfe5f360c973edc57c",
"reference": "d158a452d952049e0e55b7cfe5f360c973edc57c",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.15"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-06T13:25:45+00:00"
},
{
"name": "symfony/service-contracts",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "58c7475e5457c5492c26cc740cc0ad7464be9442"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442",
"reference": "58c7475e5457c5492c26cc740cc0ad7464be9442",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/container": "^1.0"
},
"suggest": {
"symfony/service-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-06T13:23:11+00:00"
},
{
"name": "symfony/string",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "b7914561c03f8d78f83eec3ec4502adbdc343c48"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/b7914561c03f8d78f83eec3ec4502adbdc343c48",
"reference": "b7914561c03f8d78f83eec3ec4502adbdc343c48",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "~1.15"
},
"require-dev": {
"symfony/error-handler": "^4.4|^5.0",
"symfony/http-client": "^4.4|^5.0",
"symfony/translation-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.4|^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"files": [
"Resources/functions.php"
],
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony String component",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
"i18n",
"string",
"unicode",
"utf-8",
"utf8"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-08T08:28:10+00:00"
},
{
"name": "symfony/yaml",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "8d32eb597b531eb915b4fee3dc582ade5ae1fe6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ab0314f7544d600ea7917f02cdad774358b81113",
"reference": "8d32eb597b531eb915b4fee3dc582ade5ae1fe6a",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2015-10-13T16:01:35+00:00"
}
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.3.0",
"composer-plugin-api": "^1.0 || ^2.0"
},
"platform-dev": [],
"plugin-api-version": "1.1.0"
}
<!--?xml version="1.0" encoding="UTF-8"?-->
<phpunit colors="true" bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="composer-patches">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<!-- Filter for coverage reports. -->
<filter>
<whitelist>
<directory>src/</directory>
</whitelist>
<blacklist>
<directory>vendor/</directory>
</blacklist>
</filter>
</phpunit>
<?php
/**
* @file
* Dispatch events when patches are applied.
*/
namespace cweagans\Composer;
use Composer\EventDispatcher\Event;
use Composer\Package\PackageInterface;
class PatchEvent extends Event {
/**
* @var PackageInterface $package
*/
protected $package;
/**
* @var string $url
*/
protected $url;
/**
* @var string $description
*/
protected $description;
/**
* Constructs a PatchEvent object.
*
* @param string $eventName
* @param PackageInterface $package
* @param string $url
* @param string $description
*/
public function __construct($eventName, PackageInterface $package, $url, $description) {
parent::__construct($eventName);
$this->package = $package;
$this->url = $url;
$this->description = $description;
}
/**
* Returns the package that is patched.
*
* @return PackageInterface
*/
public function getPackage() {
return $this->package;
}
/**
* Returns the url of the patch.
*
* @return string
*/
public function getUrl() {
return $this->url;
}
/**
* Returns the description of the patch.
*
* @return string
*/
public function getDescription() {
return $this->description;
}
}
<?php
/**
* @file
* Dispatch events when patches are applied.
*/
namespace cweagans\Composer;
class PatchEvents {
/**
* The PRE_PATCH_APPLY event occurs before a patch is applied.
*
* The event listener method receives a cweagans\Composer\PatchEvent instance.
*
* @var string
*/
const PRE_PATCH_APPLY = 'pre-patch-apply';
/**
* The POST_PATCH_APPLY event occurs after a patch is applied.
*
* The event listener method receives a cweagans\Composer\PatchEvent instance.
*
* @var string
*/
const POST_PATCH_APPLY = 'post-patch-apply';
}
<?php
/**
* @file
* Provides a way to patch Composer packages after installation.
*/
namespace cweagans\Composer;
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\PackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Installer\PackageEvents;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Installer\PackageEvent;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Symfony\Component\Process\Process;
class Patches implements PluginInterface, EventSubscriberInterface {
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var IOInterface $io
*/
protected $io;
/**
* @var EventDispatcher $eventDispatcher
*/
protected $eventDispatcher;
/**
* @var ProcessExecutor $executor
*/
protected $executor;
/**
* @var array $patches
*/
protected $patches;
/**
* @var array $installedPatches
*/
protected $installedPatches;
/**
* Apply plugin modifications to composer
*
* @param Composer $composer
* @param IOInterface $io
*/
public function activate(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->eventDispatcher = $composer->getEventDispatcher();
$this->executor = new ProcessExecutor($this->io);
$this->patches = array();
$this->installedPatches = array();
}
/**
* Returns an array of event names this subscriber wants to listen to.
*/
public static function getSubscribedEvents() {
return array(
ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'),
ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'),
PackageEvents::PRE_PACKAGE_INSTALL => array('gatherPatches'),
PackageEvents::PRE_PACKAGE_UPDATE => array('gatherPatches'),
// The following is a higher weight for compatibility with
// https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with
// every Composer plugin which deploys downloaded packages to other locations.
// In such cases you want that those plugins deploy patched files so they have to run after
// the "composer-patches" plugin.
// @see: https://github.com/cweagans/composer-patches/pull/153
PackageEvents::POST_PACKAGE_INSTALL => array('postInstall', 10),
PackageEvents::POST_PACKAGE_UPDATE => array('postInstall', 10),
);
}
/**
* Before running composer install,
* @param Event $event
*/
public function checkPatches(Event $event) {
if (!$this->isPatchingEnabled()) {
return;
}
try {
$repositoryManager = $this->composer->getRepositoryManager();
$localRepository = $repositoryManager->getLocalRepository();
$installationManager = $this->composer->getInstallationManager();
$packages = $localRepository->getPackages();
$extra = $this->composer->getPackage()->getExtra();
$patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array();
$tmp_patches = $this->grabPatches();
foreach ($packages as $package) {
$extra = $package->getExtra();
if (isset($extra['patches'])) {
if (isset($patches_ignore[$package->getName()])) {
foreach ($patches_ignore[$package->getName()] as $package_name => $patches) {
if (isset($extra['patches'][$package_name])) {
$extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches);
}
}
}
$this->installedPatches[$package->getName()] = $extra['patches'];
}
$patches = isset($extra['patches']) ? $extra['patches'] : array();
$tmp_patches = $this->arrayMergeRecursiveDistinct($tmp_patches, $patches);
}
if ($tmp_patches == FALSE) {
$this->io->write('<info>No patches supplied.</info>');
return;
}
// Remove packages for which the patch set has changed.
$promises = array();
foreach ($packages as $package) {
if (!($package instanceof AliasPackage)) {
$package_name = $package->getName();
$extra = $package->getExtra();
$has_patches = isset($tmp_patches[$package_name]);
$has_applied_patches = isset($extra['patches_applied']) && count($extra['patches_applied']) > 0;
if (($has_patches && !$has_applied_patches)
|| (!$has_patches && $has_applied_patches)
|| ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) {
$uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.');
$this->io->write('<info>Removing package ' . $package_name . ' so that it can be re-installed and re-patched.</info>');
$promises[] = $installationManager->uninstall($localRepository, $uninstallOperation);
}
}
}
$promises = array_filter($promises);
if ($promises) {
$this->composer->getLoop()->wait($promises);
}
}
// If the Locker isn't available, then we don't need to do this.
// It's the first time packages have been installed.
catch (\LogicException $e) {
return;
}
}
/**
* Gather patches from dependencies and store them for later use.
*
* @param PackageEvent $event
*/
public function gatherPatches(PackageEvent $event) {
// If we've already done this, then don't do it again.
if (isset($this->patches['_patchesGathered'])) {
$this->io->write('<info>Patches already gathered. Skipping</info>', TRUE, IOInterface::VERBOSE);
return;
}
// If patching has been disabled, bail out here.
elseif (!$this->isPatchingEnabled()) {
$this->io->write('<info>Patching is disabled. Skipping.</info>', TRUE, IOInterface::VERBOSE);
return;
}
$this->patches = $this->grabPatches();
if (empty($this->patches)) {
$this->io->write('<info>No patches supplied.</info>');
}
$extra = $this->composer->getPackage()->getExtra();
$patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array();
// Now add all the patches from dependencies that will be installed.
$operations = $event->getOperations();
$this->io->write('<info>Gathering patches for dependencies. This might take a minute.</info>');
foreach ($operations as $operation) {
if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) {
$package = $this->getPackageFromOperation($operation);
$extra = $package->getExtra();
if (isset($extra['patches'])) {
if (isset($patches_ignore[$package->getName()])) {
foreach ($patches_ignore[$package->getName()] as $package_name => $patches) {
if (isset($extra['patches'][$package_name])) {
$extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches);
}
}
}
$this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $extra['patches']);
}
// Unset installed patches for this package
if(isset($this->installedPatches[$package->getName()])) {
unset($this->installedPatches[$package->getName()]);
}
}
}
// Merge installed patches from dependencies that did not receive an update.
foreach ($this->installedPatches as $patches) {
$this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $patches);
}
// If we're in verbose mode, list the projects we're going to patch.
if ($this->io->isVerbose()) {
foreach ($this->patches as $package => $patches) {
$number = count($patches);
$this->io->write('<info>Found ' . $number . ' patches for ' . $package . '.</info>');
}
}
// Make sure we don't gather patches again. Extra keys in $this->patches
// won't hurt anything, so we'll just stash it there.
$this->patches['_patchesGathered'] = TRUE;
}
/**
* Get the patches from root composer or external file
* @return Patches
* @throws \Exception
*/
public function grabPatches() {
// First, try to get the patches from the root composer.json.
$extra = $this->composer->getPackage()->getExtra();
if (isset($extra['patches'])) {
$this->io->write('<info>Gathering patches for root package.</info>');
$patches = $extra['patches'];
return $patches;
}
// If it's not specified there, look for a patches-file definition.
elseif (isset($extra['patches-file'])) {
$this->io->write('<info>Gathering patches from patch file.</info>');
$patches = file_get_contents($extra['patches-file']);
$patches = json_decode($patches, TRUE);
$error = json_last_error();
if ($error != 0) {
switch ($error) {
case JSON_ERROR_DEPTH:
$msg = ' - Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = ' - Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = ' - Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$msg = ' - Syntax error, malformed JSON';
break;
case JSON_ERROR_UTF8:
$msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$msg = ' - Unknown error';
break;
}
throw new \Exception('There was an error in the supplied patches file:' . $msg);
}
if (isset($patches['patches'])) {
$patches = $patches['patches'];
return $patches;
}
elseif(!$patches) {
throw new \Exception('There was an error in the supplied patch file');
}
}
else {
return array();
}
}
/**
* @param PackageEvent $event
* @throws \Exception
*/
public function postInstall(PackageEvent $event) {
// Check if we should exit in failure.
$extra = $this->composer->getPackage()->getExtra();
$exitOnFailure = getenv('COMPOSER_EXIT_ON_PATCH_FAILURE') || !empty($extra['composer-exit-on-patch-failure']);
$skipReporting = getenv('COMPOSER_PATCHES_SKIP_REPORTING') || !empty($extra['composer-patches-skip-reporting']);
// Get the package object for the current operation.
$operation = $event->getOperation();
/** @var PackageInterface $package */
$package = $this->getPackageFromOperation($operation);
$package_name = $package->getName();
if (!isset($this->patches[$package_name])) {
if ($this->io->isVerbose()) {
$this->io->write('<info>No patches found for ' . $package_name . '.</info>');
}
return;
}
$this->io->write(' - Applying patches for <info>' . $package_name . '</info>');
// Get the install path from the package object.
$manager = $event->getComposer()->getInstallationManager();
$install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
// Set up a downloader.
$downloader = new RemoteFilesystem($this->io, $this->composer->getConfig());
// Track applied patches in the package info in installed.json
$localRepository = $this->composer->getRepositoryManager()->getLocalRepository();
$localPackage = $localRepository->findPackage($package_name, $package->getVersion());
$extra = $localPackage->getExtra();
$extra['patches_applied'] = array();
foreach ($this->patches[$package_name] as $description => $url) {
$this->io->write(' <info>' . $url . '</info> (<comment>' . $description. '</comment>)');
try {
$this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description));
$this->getAndApplyPatch($downloader, $install_path, $url, $package);
$this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description));
$extra['patches_applied'][$description] = $url;
}
catch (\Exception $e) {
$this->io->write(' <error>Could not apply patch! Skipping. The error was: ' . $e->getMessage() . '</error>');
if ($exitOnFailure) {
throw new \Exception("Cannot apply patch $description ($url)!");
}
}
}
$localPackage->setExtra($extra);
$this->io->write('');
if (true !== $skipReporting) {
$this->writePatchReport($this->patches[$package_name], $install_path);
}
}
/**
* Get a Package object from an OperationInterface object.
*
* @param OperationInterface $operation
* @return PackageInterface
* @throws \Exception
*/
protected function getPackageFromOperation(OperationInterface $operation) {
if ($operation instanceof InstallOperation) {
$package = $operation->getPackage();
}
elseif ($operation instanceof UpdateOperation) {
$package = $operation->getTargetPackage();
}
else {
throw new \Exception('Unknown operation: ' . get_class($operation));
}
return $package;
}
/**
* Apply a patch on code in the specified directory.
*
* @param RemoteFilesystem $downloader
* @param $install_path
* @param $patch_url
* @param PackageInterface $package
* @throws \Exception
*/
protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url, PackageInterface $package) {
// Local patch file.
if (file_exists($patch_url)) {
$filename = realpath($patch_url);
}
else {
// Generate random (but not cryptographically so) filename.
$filename = uniqid(sys_get_temp_dir().'/') . ".patch";
// Download file from remote filesystem to this location.
$hostname = parse_url($patch_url, PHP_URL_HOST);
try {
$downloader->copy($hostname, $patch_url, $filename, false);
} catch (\Exception $e) {
// In case of an exception, retry once as the download might
// have failed due to intermittent network issues.
$downloader->copy($hostname, $patch_url, $filename, false);
}
}
// The order here is intentional. p1 is most likely to apply with git apply.
// p0 is next likely. p2 is extremely unlikely, but for some special cases,
// it might be useful. p4 is useful for Magento 2 patches
$patch_levels = array('-p1', '-p0', '-p2', '-p4');
// Check for specified patch level for this package.
$extra = $this->composer->getPackage()->getExtra();
if (!empty($extra['patchLevel'][$package->getName()])){
$patch_levels = array($extra['patchLevel'][$package->getName()]);
}
// Attempt to apply with git apply.
$patched = $this->applyPatchWithGit($install_path, $patch_levels, $filename);
// In some rare cases, git will fail to apply a patch, fallback to using
// the 'patch' command.
if (!$patched) {
foreach ($patch_levels as $patch_level) {
// --no-backup-if-mismatch here is a hack that fixes some
// differences between how patch works on windows and unix.
if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) {
break;
}
}
}
// Clean up the temporary patch file.
if (isset($hostname)) {
unlink($filename);
}
// If the patch *still* isn't applied, then give up and throw an Exception.
// Otherwise, let the user know it worked.
if (!$patched) {
throw new \Exception("Cannot apply patch $patch_url");
}
}
/**
* Checks if the root package enables patching.
*
* @return bool
* Whether patching is enabled. Defaults to TRUE.
*/
protected function isPatchingEnabled() {
$extra = $this->composer->getPackage()->getExtra();
if (empty($extra['patches']) && empty($extra['patches-ignore']) && !isset($extra['patches-file'])) {
// The root package has no patches of its own, so only allow patching if
// it has specifically opted in.
return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE;
}
else {
return TRUE;
}
}
/**
* Writes a patch report to the target directory.
*
* @param array $patches
* @param string $directory
*/
protected function writePatchReport($patches, $directory) {
$output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n";
$output .= "Patches applied to this directory:\n\n";
foreach ($patches as $description => $url) {
$output .= $description . "\n";
$output .= 'Source: ' . $url . "\n\n\n";
}
file_put_contents($directory . "/PATCHES.txt", $output);
}
/**
* Executes a shell command with escaping.
*
* @param string $cmd
* @return bool
*/
protected function executeCommand($cmd) {
// Shell-escape all arguments except the command.
$args = func_get_args();
foreach ($args as $index => $arg) {
if ($index !== 0) {
$args[$index] = escapeshellarg($arg);
}
}
// And replace the arguments.
$command = call_user_func_array('sprintf', $args);
$output = '';
if ($this->io->isVerbose()) {
$this->io->write('<comment>' . $command . '</comment>');
$io = $this->io;
$output = function ($type, $data) use ($io) {
if ($type == Process::ERR) {
$io->write('<error>' . $data . '</error>');
}
else {
$io->write('<comment>' . $data . '</comment>');
}
};
}
return ($this->executor->execute($command, $output) == 0);
}
/**
* Recursively merge arrays without changing data types of values.
*
* Does not change the data types of the values in the arrays. Matching keys'
* values in the second array overwrite those in the first array, as is the
* case with array_merge.
*
* @param array $array1
* The first array.
* @param array $array2
* The second array.
* @return array
* The merged array.
*
* @see http://php.net/manual/en/function.array-merge-recursive.php#92195
*/
protected function arrayMergeRecursiveDistinct(array $array1, array $array2) {
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value);
}
else {
$merged[$key] = $value;
}
}
return $merged;
}
/**
* Attempts to apply a patch with git apply.
*
* @param $install_path
* @param $patch_levels
* @param $filename
*
* @return bool
* TRUE if patch was applied, FALSE otherwise.
*/
protected function applyPatchWithGit($install_path, $patch_levels, $filename) {
// Do not use git apply unless the install path is itself a git repo
// @see https://stackoverflow.com/a/27283285
if (!is_dir($install_path . '/.git')) {
return FALSE;
}
$patched = FALSE;
foreach ($patch_levels as $patch_level) {
if ($this->io->isVerbose()) {
$comment = 'Testing ability to patch with git apply.';
$comment .= ' This command may produce errors that can be safely ignored.';
$this->io->write('<comment>' . $comment . '</comment>');
}
$checked = $this->executeCommand('git -C %s apply --check -v %s %s', $install_path, $patch_level, $filename);
$output = $this->executor->getErrorOutput();
if (substr($output, 0, 7) == 'Skipped') {
// Git will indicate success but silently skip patches in some scenarios.
//
// @see https://github.com/cweagans/composer-patches/pull/165
$checked = FALSE;
}
if ($checked) {
// Apply the first successful style.
$patched = $this->executeCommand('git -C %s apply %s %s', $install_path, $patch_level, $filename);
break;
}
}
return $patched;
}
/**
* Indicates if a package has been patched.
*
* @param \Composer\Package\PackageInterface $package
* The package to check.
*
* @return bool
* TRUE if the package has been patched.
*/
public static function isPackagePatched(PackageInterface $package) {
return array_key_exists('patches_applied', $package->getExtra());
}
/**
* {@inheritDoc}
*/
public function deactivate(Composer $composer, IOInterface $io)
{
}
/**
* {@inheritDoc}
*/
public function uninstall(Composer $composer, IOInterface $io)
{
}
}
<?php
/**
* @file
* Tests event dispatching.
*/
namespace cweagans\Composer\Tests;
use cweagans\Composer\PatchEvent;
use cweagans\Composer\PatchEvents;
use Composer\Package\PackageInterface;
class PatchEventTest extends \PHPUnit_Framework_TestCase {
/**
* Tests all the getters.
*
* @dataProvider patchEventDataProvider
*/
public function testGetters($event_name, PackageInterface $package, $url, $description) {
$patch_event = new PatchEvent($event_name, $package, $url, $description);
$this->assertEquals($event_name, $patch_event->getName());
$this->assertEquals($package, $patch_event->getPackage());
$this->assertEquals($url, $patch_event->getUrl());
$this->assertEquals($description, $patch_event->getDescription());
}
public function patchEventDataProvider() {
$prophecy = $this->prophesize('Composer\Package\PackageInterface');
$package = $prophecy->reveal();
return array(
array(PatchEvents::PRE_PATCH_APPLY, $package, 'https://www.drupal.org', 'A test patch'),
array(PatchEvents::POST_PATCH_APPLY, $package, 'https://www.drupal.org', 'A test patch'),
);
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\Installer\PackageEvent;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
/**
* Determine recursively which packages have been allowed to scaffold files.
*
* If the root-level composer.json allows drupal/core, and drupal/core allows
* drupal/assets, then the later package will also implicitly be allowed.
*
* @internal
*/
class AllowedPackages implements PostPackageEventListenerInterface {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* Manager of the options in the top-level composer.json's 'extra' section.
*
* @var \Drupal\Composer\Plugin\Scaffold\ManageOptions
*/
protected $manageOptions;
/**
* The list of new packages added by this Composer command.
*
* @var array
*/
protected $newPackages = [];
/**
* AllowedPackages constructor.
*
* @param \Composer\Composer $composer
* The composer object.
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param \Drupal\Composer\Plugin\Scaffold\ManageOptions $manage_options
* Manager of the options in the top-level composer.json's 'extra' section.
*/
public function __construct(Composer $composer, IOInterface $io, ManageOptions $manage_options) {
$this->composer = $composer;
$this->io = $io;
$this->manageOptions = $manage_options;
}
/**
* Gets a list of all packages that are allowed to copy scaffold files.
*
* We will implicitly allow the projects 'drupal/legacy-scaffold-assets'
* and 'drupal/core' to scaffold files, if they are present. Any other
* project must be explicitly whitelisted in the top-level composer.json
* file in order to be allowed to override scaffold files.
* Configuration for packages specified later will override configuration
* specified by packages listed earlier. In other words, the last listed
* package has the highest priority. The root package will always be returned
* at the end of the list.
*
* @return \Composer\Package\PackageInterface[]
* An array of allowed Composer packages.
*/
public function getAllowedPackages() {
$top_level_packages = $this->getTopLevelAllowedPackages();
$allowed_packages = $this->recursiveGetAllowedPackages($top_level_packages);
// If the root package defines any file mappings, then implicitly add it
// to the list of allowed packages. Add it at the end so that it overrides
// all the preceding packages.
if ($this->manageOptions->getOptions()->hasFileMapping()) {
$root_package = $this->composer->getPackage();
unset($allowed_packages[$root_package->getName()]);
$allowed_packages[$root_package->getName()] = $root_package;
}
// Handle any newly-added packages that are not already allowed.
return $this->evaluateNewPackages($allowed_packages);
}
/**
* {@inheritdoc}
*/
public function event(PackageEvent $event) {
$operation = $event->getOperation();
// Determine the package. Later, in evaluateNewPackages(), we will report
// which of the newly-installed packages have scaffold operations, and
// whether or not they are allowed to scaffold by the allowed-packages
// option in the root-level composer.json file.
$package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage();
if (ScaffoldOptions::hasOptions($package->getExtra())) {
$this->newPackages[$package->getName()] = $package;
}
}
/**
* Gets all packages that are allowed in the top-level composer.json.
*
* We will implicitly allow the projects 'drupal/legacy-scaffold-assets'
* and 'drupal/core' to scaffold files, if they are present. Any other
* project must be explicitly whitelisted in the top-level composer.json
* file in order to be allowed to override scaffold files.
*
* @return array
* An array of allowed Composer package names.
*/
protected function getTopLevelAllowedPackages() {
$implicit_packages = [
'drupal/legacy-scaffold-assets',
'drupal/core',
];
$top_level_packages = $this->manageOptions->getOptions()->allowedPackages();
return array_merge($implicit_packages, $top_level_packages);
}
/**
* Builds a name-to-package mapping from a list of package names.
*
* @param string[] $packages_to_allow
* List of package names to allow.
* @param array $allowed_packages
* Mapping of package names to PackageInterface of packages already
* accumulated.
*
* @return \Composer\Package\PackageInterface[]
* Mapping of package names to PackageInterface in priority order.
*/
protected function recursiveGetAllowedPackages(array $packages_to_allow, array $allowed_packages = []) {
foreach ($packages_to_allow as $name) {
$package = $this->getPackage($name);
if ($package instanceof PackageInterface && !isset($allowed_packages[$name])) {
$allowed_packages[$name] = $package;
$package_options = $this->manageOptions->packageOptions($package);
$allowed_packages = $this->recursiveGetAllowedPackages($package_options->allowedPackages(), $allowed_packages);
}
}
return $allowed_packages;
}
/**
* Evaluates newly-added packages and see if they are already allowed.
*
* For now we will only emit warnings if they are not.
*
* @param array $allowed_packages
* Mapping of package names to PackageInterface of packages already
* accumulated.
*
* @return \Composer\Package\PackageInterface[]
* Mapping of package names to PackageInterface in priority order.
*/
protected function evaluateNewPackages(array $allowed_packages) {
foreach ($this->newPackages as $name => $newPackage) {
if (!array_key_exists($name, $allowed_packages)) {
$this->io->write("Not scaffolding files for <comment>{$name}</comment>, because it is not listed in the element 'extra.drupal-scaffold.allowed-packages' in the root-level composer.json file.");
}
else {
$this->io->write("Package <comment>{$name}</comment> has scaffold operations, and is already allowed in the root-level composer.json file.");
}
}
// @todo We could prompt the user and ask if they wish to allow a
// newly-added package. This might be useful if, for example, the user
// might wish to require an installation profile that contains scaffolded
// assets. For more information, see:
// https://www.drupal.org/project/drupal/issues/3064990
return $allowed_packages;
}
/**
* Retrieves a package from the current composer process.
*
* @param string $name
* Name of the package to get from the current composer installation.
*
* @return \Composer\Package\PackageInterface|null
* The Composer package.
*/
protected function getPackage($name) {
return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*');
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
/**
* List of all commands provided by this package.
*
* @internal
*/
class CommandProvider implements CommandProviderCapability {
/**
* {@inheritdoc}
*/
public function getCommands() {
return [new ComposerScaffoldCommand()];
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Command\BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* The "drupal:scaffold" command class.
*
* Manually run the scaffold operation that normally happens after
* 'composer install'.
*
* @internal
*/
class ComposerScaffoldCommand extends BaseCommand {
/**
* {@inheritdoc}
*/
protected function configure() {
$this
->setName('drupal:scaffold')
->setAliases(['scaffold'])
->setDescription('Update the Drupal scaffold files.')
->setHelp(
<<<EOT
The <info>drupal:scaffold</info> command places the scaffold files in their
respective locations according to the layout stipulated in the composer.json
file.
<info>php composer.phar drupal:scaffold</info>
It is usually not necessary to call <info>drupal:scaffold</info> manually,
because it is called automatically as needed, e.g. after an <info>install</info>
or <info>update</info> command. Note, though, that only packages explicitly
allowed to scaffold in the top-level composer.json will be processed by this
command.
For more information, see https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold.
EOT
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$handler = new Handler($this->requireComposer(), $this->getIO());
$handler->scaffold();
return 0;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult;
/**
* Generates an 'autoload.php' that includes the autoloader created by Composer.
*
* @internal
*/
final class GenerateAutoloadReferenceFile {
/**
* This class provides only static methods.
*/
private function __construct() {
}
/**
* Generates the autoload file at the specified location.
*
* This only writes a bit of PHP that includes the autoload file that
* Composer generated. Drupal does this so that it can guarantee that there
* will always be an `autoload.php` file in a well-known location.
*
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
* @param string $vendor
* The path to the vendor directory.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The result of the autoload file generation.
*/
public static function generateAutoload(IOInterface $io, $package_name, $web_root, $vendor) {
$autoload_path = static::autoloadPath($package_name, $web_root);
// Calculate the relative path from the webroot (location of the project
// autoload.php) to the vendor directory.
$fs = new Filesystem();
$relative_autoload_path = $fs->findShortestPath($autoload_path->fullPath(), "$vendor/autoload.php");
file_put_contents($autoload_path->fullPath(), static::autoLoadContents($relative_autoload_path));
return new ScaffoldResult($autoload_path, TRUE);
}
/**
* Determines whether or not the autoload file has been committed.
*
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
*
* @return bool
* True if autoload.php file exists and has been committed to the repository
*/
public static function autoloadFileCommitted(IOInterface $io, $package_name, $web_root) {
$autoload_path = static::autoloadPath($package_name, $web_root);
$autoload_file = $autoload_path->fullPath();
$location = dirname($autoload_file);
if (!file_exists($autoload_file)) {
return FALSE;
}
return Git::checkTracked($io, $autoload_file, $location);
}
/**
* Generates a scaffold file path object for the autoload file.
*
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* Object wrapping the relative and absolute path to the destination file.
*/
protected static function autoloadPath($package_name, $web_root) {
$rel_path = 'autoload.php';
$dest_rel_path = '[web-root]/' . $rel_path;
$dest_full_path = $web_root . '/' . $rel_path;
return new ScaffoldFilePath('autoload', $package_name, $dest_rel_path, $dest_full_path);
}
/**
* Builds the contents of the autoload file.
*
* @param string $relative_autoload_path
* The relative path to the autoloader in vendor.
*
* @return string
* Return the contents for the autoload.php.
*/
protected static function autoLoadContents($relative_autoload_path) {
$relative_autoload_path = preg_replace('#^\./#', '', $relative_autoload_path);
return <<<EOF
<?php
/**
* @file
* Includes the autoloader created by Composer.
*
* This file was generated by drupal-scaffold.
*
* @see composer.json
* @see index.php
* @see core/install.php
* @see core/rebuild.php
*/
return require __DIR__ . '/{$relative_autoload_path}';
EOF;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
// cspell:ignore unmatch
use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor;
/**
* Provide some Git utility operations.
*
* @internal
*/
class Git {
/**
* This class provides only static methods.
*/
private function __construct() {
}
/**
* Determines whether the specified scaffold file is already ignored.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $path
* Path to scaffold file to check.
* @param string $dir
* Base directory for git process.
*
* @return bool
* Whether the specified file is already ignored or not (TRUE if ignored).
*/
public static function checkIgnore(IOInterface $io, $path, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git check-ignore ' . $process->escape($path), $output, $dir);
return $exitCode == 0;
}
/**
* Determines whether the specified scaffold file is tracked by git.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $path
* Path to scaffold file to check.
* @param string $dir
* Base directory for git process.
*
* @return bool
* Whether the specified file is already tracked or not (TRUE if tracked).
*/
public static function checkTracked(IOInterface $io, $path, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git ls-files --error-unmatch ' . $process->escape($path), $output, $dir);
return $exitCode == 0;
}
/**
* Checks to see if the project root dir is in a git repository.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $dir
* Base directory for git process.
*
* @return bool
* True if this is a repository.
*/
public static function isRepository(IOInterface $io, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git rev-parse --show-toplevel', $output, $dir);
return $exitCode == 0;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\Installer\PackageEvent;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
use Drupal\Composer\Plugin\Scaffold\Operations\OperationData;
use Drupal\Composer\Plugin\Scaffold\Operations\OperationFactory;
use Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection;
/**
* Core class of the plugin.
*
* Contains the primary logic which determines the files to be fetched and
* processed.
*
* @internal
*/
class Handler {
/**
* Composer hook called before scaffolding begins.
*/
const PRE_DRUPAL_SCAFFOLD_CMD = 'pre-drupal-scaffold-cmd';
/**
* Composer hook called after scaffolding completes.
*/
const POST_DRUPAL_SCAFFOLD_CMD = 'post-drupal-scaffold-cmd';
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The scaffold options in the top-level composer.json's 'extra' section.
*
* @var \Drupal\Composer\Plugin\Scaffold\ManageOptions
*/
protected $manageOptions;
/**
* The manager that keeps track of which packages are allowed to scaffold.
*
* @var \Drupal\Composer\Plugin\Scaffold\AllowedPackages
*/
protected $manageAllowedPackages;
/**
* The list of listeners that are notified after a package event.
*
* @var \Drupal\Composer\Plugin\Scaffold\PostPackageEventListenerInterface[]
*/
protected $postPackageListeners = [];
/**
* Handler constructor.
*
* @param \Composer\Composer $composer
* The Composer service.
* @param \Composer\IO\IOInterface $io
* The Composer I/O service.
*/
public function __construct(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->manageOptions = new ManageOptions($composer);
$this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions);
}
/**
* Registers post-package events if the 'require' command was called.
*/
public function requireWasCalled() {
// In order to differentiate between post-package events called after
// 'composer require' vs. the same events called at other times, we will
// only install our handler when a 'require' event is detected.
$this->postPackageListeners[] = $this->manageAllowedPackages;
}
/**
* Posts package command event.
*
* We want to detect packages 'require'd that have scaffold files, but are not
* yet allowed in the top-level composer.json file.
*
* @param \Composer\Installer\PackageEvent $event
* Composer package event sent on install/update/remove.
*/
public function onPostPackageEvent(PackageEvent $event) {
foreach ($this->postPackageListeners as $listener) {
$listener->event($event);
}
}
/**
* Creates scaffold operation objects for all items in the file mappings.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param array $package_file_mappings
* The package file mappings array keyed by destination path and the values
* are operation metadata arrays.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[]
* A list of scaffolding operation objects
*/
protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) {
$scaffold_op_factory = new OperationFactory($this->composer);
$scaffold_ops = [];
foreach ($package_file_mappings as $dest_rel_path => $data) {
$operation_data = new OperationData($dest_rel_path, $data);
$scaffold_ops[$dest_rel_path] = $scaffold_op_factory->create($package, $operation_data);
}
return $scaffold_ops;
}
/**
* Copies all scaffold files from source to destination.
*/
public function scaffold() {
// Recursively get the list of allowed packages. Only allowed packages
// may declare scaffold files. Note that the top-level composer.json file
// is implicitly allowed.
$allowed_packages = $this->manageAllowedPackages->getAllowedPackages();
if (empty($allowed_packages)) {
$this->io->write("Nothing scaffolded because no packages are allowed in the top-level composer.json file.");
return;
}
// Call any pre-scaffold scripts that may be defined.
$dispatcher = $this->composer->getEventDispatcher();
$dispatcher->dispatchScript(self::PRE_DRUPAL_SCAFFOLD_CMD);
// Fetch the list of file mappings from each allowed package and normalize
// them.
$file_mappings = $this->getFileMappingsFromPackages($allowed_packages);
$location_replacements = $this->manageOptions->getLocationReplacements();
$scaffold_options = $this->manageOptions->getOptions();
// Create a collection of scaffolded files to process. This determines which
// take priority and which are combined.
$scaffold_files = new ScaffoldFileCollection($file_mappings, $location_replacements);
// Get the scaffold files whose contents on disk match what we are about to
// write. We can remove these from consideration, as rewriting would be a
// no-op.
$unchanged = $scaffold_files->checkUnchanged();
$scaffold_files->filterFiles($unchanged);
// Process the list of scaffolded files.
$scaffold_results = $scaffold_files->processScaffoldFiles($this->io, $scaffold_options);
// Generate an autoload file in the document root that includes the
// autoload.php file in the vendor directory, wherever that is. Drupal
// requires this in order to easily locate relocated vendor dirs.
$web_root = $this->manageOptions->getOptions()->getLocation('web-root');
if (!GenerateAutoloadReferenceFile::autoloadFileCommitted($this->io, $this->rootPackageName(), $web_root)) {
$scaffold_results[] = GenerateAutoloadReferenceFile::generateAutoload($this->io, $this->rootPackageName(), $web_root, $this->getVendorPath());
}
// Add the managed scaffold files to .gitignore if applicable.
$gitIgnoreManager = new ManageGitIgnore($this->io, getcwd());
$gitIgnoreManager->manageIgnored($scaffold_results, $scaffold_options);
// Call post-scaffold scripts.
$dispatcher->dispatchScript(self::POST_DRUPAL_SCAFFOLD_CMD);
}
/**
* Gets the path to the 'vendor' directory.
*
* @return string
* The file path of the vendor directory.
*/
protected function getVendorPath() {
$vendor_dir = $this->composer->getConfig()->get('vendor-dir');
$filesystem = new Filesystem();
return $filesystem->normalizePath(realpath($vendor_dir));
}
/**
* Gets a consolidated list of file mappings from all allowed packages.
*
* @param \Composer\Package\Package[] $allowed_packages
* A multidimensional array of file mappings, as returned by
* self::getAllowedPackages().
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[][]
* An array of destination paths => scaffold operation objects.
*/
protected function getFileMappingsFromPackages(array $allowed_packages) {
$file_mappings = [];
foreach ($allowed_packages as $package_name => $package) {
$file_mappings[$package_name] = $this->getPackageFileMappings($package);
}
return $file_mappings;
}
/**
* Gets the array of file mappings provided by a given package.
*
* @param \Composer\Package\PackageInterface $package
* The Composer package from which to get the file mappings.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[]
* An array of destination paths => scaffold operation objects.
*/
protected function getPackageFileMappings(PackageInterface $package) {
$options = $this->manageOptions->packageOptions($package);
if ($options->hasFileMapping()) {
return $this->createScaffoldOperations($package, $options->fileMapping());
}
// Warn the user if they allow a package that does not have any scaffold
// files. We will ignore drupal/core, though, as it is implicitly allowed,
// but might not have scaffold files (version 8.7.x and earlier).
if (!$options->hasAllowedPackages() && ($package->getName() != 'drupal/core')) {
$this->io->writeError("The allowed package {$package->getName()} does not provide a file mapping for Composer Scaffold.");
}
return [];
}
/**
* Gets the root package name.
*
* @return string
* The package name of the root project
*/
protected function rootPackageName() {
$root_package = $this->composer->getPackage();
return $root_package->getName();
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
/**
* Injects config values from an associative array into a string.
*
* @internal
*/
class Interpolator {
/**
* The character sequence that identifies the start of a token.
*
* @var string
*/
protected $startToken;
/**
* The character sequence that identifies the end of a token.
*
* @var string
*/
protected $endToken;
/**
* The associative array of replacements.
*
* @var array
*/
protected $data = [];
/**
* Interpolator constructor.
*
* @param string $start_token
* The start marker for a token, e.g. '['.
* @param string $end_token
* The end marker for a token, e.g. ']'.
*/
public function __construct($start_token = '\\[', $end_token = '\\]') {
$this->startToken = $start_token;
$this->endToken = $end_token;
}
/**
* Sets the data set to use when interpolating.
*
* @param array $data
* The key:value pairs to use when interpolating.
*
* @return $this
*/
public function setData(array $data) {
$this->data = $data;
return $this;
}
/**
* Adds to the data set to use when interpolating.
*
* @param array $data
* The key:value pairs to use when interpolating.
*
* @return $this
*/
public function addData(array $data) {
$this->data = array_merge($this->data, $data);
return $this;
}
/**
* Replaces tokens in a string with values from an associative array.
*
* Tokens are surrounded by delimiters, e.g. square brackets "[key]". The
* characters that surround the key may be defined when the Interpolator is
* constructed.
*
* Example:
* If the message is 'Hello, [user.name]', then the value of the user.name
* item is fetched from the array, and the token [user.name] is replaced with
* the result.
*
* @param string $message
* Message containing tokens to be replaced.
* @param array $extra
* Data to use for interpolation in addition to whatever was provided to
* self::setData().
* @param string|bool $default
* (optional) The value to substitute for tokens that are not found in the
* data. If FALSE, then missing tokens are not replaced. Defaults to an
* empty string.
*
* @return string
* The message after replacements have been made.
*/
public function interpolate($message, array $extra = [], $default = '') {
$data = $extra + $this->data;
$replacements = $this->replacements($message, $data, $default);
return strtr($message, $replacements);
}
/**
* Finds the tokens that exist in a message and builds a replacement array.
*
* All of the replacements in the data array are looked up given the token
* keys from the provided message. Keys that do not exist in the configuration
* are replaced with the default value.
*
* @param string $message
* String with tokens.
* @param array $data
* Data to use for interpolation.
* @param string $default
* (optional) The value to substitute for tokens that are not found in the
* data. If FALSE, then missing tokens are not replaced. Defaults to an
* empty string.
*
* @return string[]
* An array of replacements to make. Keyed by tokens and the replacements
* are the values.
*/
protected function replacements($message, array $data, $default = '') {
$tokens = $this->findTokens($message);
$replacements = [];
foreach ($tokens as $sourceText => $key) {
$replacement_text = array_key_exists($key, $data) ? $data[$key] : $default;
if ($replacement_text !== FALSE) {
$replacements[$sourceText] = $replacement_text;
}
}
return $replacements;
}
/**
* Finds all of the tokens in the provided message.
*
* @param string $message
* String with tokens.
*
* @return string[]
* map of token to key, e.g. {{key}} => key
*/
protected function findTokens($message) {
$reg_ex = '#' . $this->startToken . '([a-zA-Z0-9._-]+)' . $this->endToken . '#';
if (!preg_match_all($reg_ex, $message, $matches, PREG_SET_ORDER)) {
return [];
}
$tokens = [];
foreach ($matches as $matchSet) {
[$sourceText, $key] = $matchSet;
$tokens[$sourceText] = $key;
}
return $tokens;
}
}
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\IO\IOInterface;
/**
* Manage the .gitignore file.
*
* @internal
*/
class ManageGitIgnore {
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The directory where the project is located.
*
* @var string
*/
protected $dir;
/**
* ManageGitIgnore constructor.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO interface.
* @param string $dir
* The directory where the project is located.
*/
public function __construct(IOInterface $io, $dir) {
$this->io = $io;
$this->dir = $dir;
}
/**
* Manages gitignore files.
*
* @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[] $files
* A list of scaffold results, each of which holds a path and whether
* or not that file is managed.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Configuration options from the composer.json extras section.
*/
public function manageIgnored(array $files, ScaffoldOptions $options) {
if (!$this->managementOfGitIgnoreEnabled($options)) {
return;
}
// Accumulate entries to add to .gitignore, sorted into buckets based on the
// location of the .gitignore file the entry should be added to.
$add_to_git_ignore = [];
foreach ($files as $scaffoldResult) {
$path = $scaffoldResult->destination()->fullPath();
$is_ignored = Git::checkIgnore($this->io, $path, $this->dir);
if (!$is_ignored) {
$is_tracked = Git::checkTracked($this->io, $path, $this->dir);
if (!$is_tracked && $scaffoldResult->isManaged()) {
$dir = realpath(dirname($path));
$name = basename($path);
$add_to_git_ignore[$dir][] = '/' . $name;
}
}
}
// Write out the .gitignore files one at a time.
foreach ($add_to_git_ignore as $dir => $entries) {
$this->addToGitIgnore($dir, $entries);
}
}
/**
* Determines whether we should manage gitignore files.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Configuration options from the composer.json extras section.
*
* @return bool
* Whether or not gitignore files should be managed.
*/
protected function managementOfGitIgnoreEnabled(ScaffoldOptions $options) {
// If the composer.json stipulates whether gitignore is managed or not, then
// follow its recommendation.
if ($options->hasGitIgnore()) {
return $options->gitIgnore();
}
// Do not manage .gitignore if there is no repository here.
if (!Git::isRepository($this->io, $this->dir)) {
return FALSE;
}
// If the composer.json did not specify whether or not .gitignore files
// should be managed, then manage them if the vendor directory is ignored.
return Git::checkIgnore($this->io, 'vendor', $this->dir);
}
/**
* Adds a set of entries to the specified .gitignore file.
*
* @param string $dir
* Path to directory where gitignore should be written.
* @param string[] $entries
* Entries to write to .gitignore file.
*/
protected function addToGitIgnore($dir, array $entries) {
sort($entries);
$git_ignore_path = $dir . '/.gitignore';
$contents = '';
// Appending to existing .gitignore files.
if (file_exists($git_ignore_path)) {
$contents = file_get_contents($git_ignore_path);
if (!empty($contents) && !str_ends_with($contents, "\n")) {
$contents .= "\n";
}
}
$contents .= implode("\n", $entries);
file_put_contents($git_ignore_path, $contents);
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
/**
* Per-project options from the 'extras' section of the composer.json file.
*
* Projects that describe scaffold files do so via their scaffold options.
* This data is pulled from the 'drupal-scaffold' portion of the extras
* section of the project data.
*
* @internal
*/
class ManageOptions {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* ManageOptions constructor.
*
* @param \Composer\Composer $composer
* The Composer service.
*/
public function __construct(Composer $composer) {
$this->composer = $composer;
}
/**
* Gets the root-level scaffold options for this project.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions
* The scaffold options object.
*/
public function getOptions() {
return $this->packageOptions($this->composer->getPackage());
}
/**
* Gets the scaffold options for the stipulated project.
*
* @param \Composer\Package\PackageInterface $package
* The package to fetch the scaffold options from.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions
* The scaffold options object.
*/
public function packageOptions(PackageInterface $package) {
return ScaffoldOptions::create($package->getExtra());
}
/**
* Creates an interpolator for the 'locations' element.
*
* The interpolator returned will replace a path string with the tokens
* defined in the 'locations' element.
*
* Note that only the root package may define locations.
*
* @return \Drupal\Composer\Plugin\Scaffold\Interpolator
* Interpolator that will do replacements in a string using tokens in
* 'locations' element.
*/
public function getLocationReplacements() {
return (new Interpolator())->setData($this->ensureLocations());
}
/**
* Ensures that all of the locations defined in the scaffold files exist.
*
* Create them on the filesystem if they do not.
*/
protected function ensureLocations() {
$fs = new Filesystem();
$locations = $this->getOptions()->locations() + ['web_root' => './'];
$locations = array_map(function ($location) use ($fs) {
$fs->ensureDirectoryExists($location);
$location = realpath($location);
return $location;
}, $locations);
return $locations;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
/**
* Provides default behaviors for operations.
*
* @internal
*/
abstract class AbstractOperation implements OperationInterface {
/**
* Cached contents of scaffold file to be written to disk.
*
* @var string
*/
protected $contents;
/**
* {@inheritdoc}
*/
final public function contents() {
if (!isset($this->contents)) {
$this->contents = $this->generateContents();
}
return $this->contents;
}
/**
* Load the scaffold contents or otherwise generate what is needed.
*
* @return string
* The contents of the scaffold file.
*/
abstract protected function generateContents();
/**
* {@inheritdoc}
*/
public function scaffoldOverExistingTarget(OperationInterface $existing_target) {
return $this;
}
/**
* {@inheritdoc}
*/
public function scaffoldAtNewLocation(ScaffoldFilePath $destination) {
return $this;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to add to the beginning and/or end of a scaffold file.
*
* @internal
*/
class AppendOp extends AbstractOperation {
/**
* Identifies Append operations.
*/
const ID = 'append';
/**
* Path to the source file to prepend, if any.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $prepend;
/**
* Path to the source file to append, if any.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $append;
/**
* Path to the default data to use when appending to an empty file.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $default;
/**
* An indicator of whether the file we are appending to is managed or not.
*/
protected $managed;
/**
* An indicator of whether we are allowed to append to a non-scaffolded file.
*/
protected $forceAppend;
/**
* The contents from the file that we are prepending / appending to.
*
* @var string
*/
protected $originalContents;
/**
* Constructs an AppendOp.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath|null $prepend_path
* (optional) The relative path to the prepend file.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath|null $append_path
* (optional) The relative path to the append file.
* @param bool $force_append
* (optional) TRUE if is okay to append to a file that was not scaffolded.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath|null $default_path
* (optional) The relative path to the default data.
*/
public function __construct(
?ScaffoldFilePath $prepend_path = NULL,
?ScaffoldFilePath $append_path = NULL,
$force_append = FALSE,
?ScaffoldFilePath $default_path = NULL,
) {
$this->forceAppend = $force_append;
$this->prepend = $prepend_path;
$this->append = $append_path;
$this->default = $default_path;
$this->managed = TRUE;
}
/**
* {@inheritdoc}
*/
protected function generateContents() {
// Fetch the prepend contents, if provided.
$prepend_contents = '';
if (!empty($this->prepend)) {
$prepend_contents = file_get_contents($this->prepend->fullPath()) . "\n";
}
// Fetch the append contents, if provided.
$append_contents = '';
if (!empty($this->append)) {
$append_contents = "\n" . file_get_contents($this->append->fullPath());
}
// Get the original contents, or the default data if the original is empty.
$original_contents = $this->originalContents;
if (empty($original_contents) && !empty($this->default)) {
$original_contents = file_get_contents($this->default->fullPath());
}
// Attach it all together.
return $prepend_contents . $original_contents . $append_contents;
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$destination_path = $destination->fullPath();
$interpolator = $destination->getInterpolator();
// Be extra-noisy of creating a new file or appending to a non-scaffold
// file. Note that if the file already has the append contents, then the
// OperationFactory will make a SkipOp instead, and we will not get here.
if (!$this->managed) {
$message = ' - <info>NOTICE</info> Modifying existing file at <info>[dest-rel-path]</info>.';
if (!file_exists($destination_path)) {
$message = ' - <info>NOTICE</info> Creating a new file at <info>[dest-rel-path]</info>.';
}
$message .= ' Examine the contents and ensure that it came out correctly.';
$io->write($interpolator->interpolate($message));
}
// Notify that we are prepending, if there is prepend data.
if (!empty($this->prepend)) {
$this->prepend->addInterpolationData($interpolator, 'prepend');
$io->write($interpolator->interpolate(" - Prepend to <info>[dest-rel-path]</info> from <info>[prepend-rel-path]</info>"));
}
// Notify that we are appending, if there is append data.
if (!empty($this->append)) {
$this->append->addInterpolationData($interpolator, 'append');
$io->write($interpolator->interpolate(" - Append to <info>[dest-rel-path]</info> from <info>[append-rel-path]</info>"));
}
// Write the resulting data
file_put_contents($destination_path, $this->contents());
// Return a ScaffoldResult with knowledge of whether this file is managed.
return new ScaffoldResult($destination, $this->managed);
}
/**
* {@inheritdoc}
*/
public function scaffoldOverExistingTarget(OperationInterface $existing_target) {
$this->originalContents = $existing_target->contents();
return $this;
}
/**
* {@inheritdoc}
*/
public function scaffoldAtNewLocation(ScaffoldFilePath $destination) {
// If there is no existing scaffold file at the target location, then any
// append we do will be to an unmanaged file.
$this->managed = FALSE;
// Default: do not allow an append over a file that was not scaffolded.
if (!$this->forceAppend) {
$message = " - Skip <info>[dest-rel-path]</info>: cannot append to a path that was not scaffolded unless 'force-append' property is set.";
return new SkipOp($message);
}
// If the target file does not exist, then we will allow the append to
// happen if we have default data to provide for it.
if (!file_exists($destination->fullPath())) {
if (!empty($this->default)) {
return $this;
}
$message = " - Skip <info>[dest-rel-path]</info>: no file exists at the target path, and no default data provided.";
return new SkipOp($message);
}
// If the target file DOES exist, and it already contains the append/prepend
// data, then we will skip the operation.
$existingData = file_get_contents($destination->fullPath());
if ($this->existingFileHasData($existingData, $this->append) || $this->existingFileHasData($existingData, $this->prepend)) {
$message = " - Skip <info>[dest-rel-path]</info>: the file already has the append/prepend data.";
return new SkipOp($message);
}
// Cache the original data to use during append.
$this->originalContents = $existingData;
return $this;
}
/**
* Check to see if the append/prepend data has already been applied.
*
* @param string $contents
* The contents of the target file.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $data_path
* The path to the data to append or prepend
*
* @return bool
* 'TRUE' if the append/prepend data already exists in contents.
*/
protected function existingFileHasData($contents, $data_path) {
if (empty($data_path)) {
return FALSE;
}
$data = file_get_contents($data_path->fullPath());
return str_contains($contents, $data);
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
/**
* Holds parameter data for operation objects during operation creation only.
*
* @internal
*/
class OperationData {
const MODE = 'mode';
const PATH = 'path';
const OVERWRITE = 'overwrite';
const PREPEND = 'prepend';
const APPEND = 'append';
const DEFAULT = 'default';
const FORCE_APPEND = 'force-append';
/**
* The parameter data.
*
* @var array
*/
protected $data;
/**
* The destination path.
*
* @var string
*/
protected $destination;
/**
* OperationData constructor.
*
* @param string $destination
* The destination path.
* @param mixed $data
* The raw data array to wrap.
*/
public function __construct($destination, $data) {
$this->destination = $destination;
$this->data = $this->normalizeScaffoldMetadata($destination, $data);
}
/**
* Gets the destination path that this operation data is associated with.
*
* @return string
* The destination path for the scaffold result.
*/
public function destination() {
return $this->destination;
}
/**
* Gets operation mode.
*
* @return string
* Operation mode.
*/
public function mode() {
return $this->data[self::MODE];
}
/**
* Checks if path exists.
*
* @return bool
* Returns true if path exists
*/
public function hasPath() {
return isset($this->data[self::PATH]);
}
/**
* Gets path.
*
* @return string
* The path.
*/
public function path() {
return $this->data[self::PATH];
}
/**
* Determines overwrite.
*
* @return bool
* Returns true if overwrite mode was selected.
*/
public function overwrite() {
return !empty($this->data[self::OVERWRITE]);
}
/**
* Determines whether 'force-append' has been set.
*
* @return bool
* Returns true if 'force-append' mode was selected.
*/
public function forceAppend() {
if ($this->hasDefault()) {
return TRUE;
}
return !empty($this->data[self::FORCE_APPEND]);
}
/**
* Checks if prepend path exists.
*
* @return bool
* Returns true if prepend exists.
*/
public function hasPrepend() {
return isset($this->data[self::PREPEND]);
}
/**
* Gets prepend path.
*
* @return string
* Path to prepend data
*/
public function prepend() {
return $this->data[self::PREPEND];
}
/**
* Checks if append path exists.
*
* @return bool
* Returns true if prepend exists.
*/
public function hasAppend() {
return isset($this->data[self::APPEND]);
}
/**
* Gets append path.
*
* @return string
* Path to append data
*/
public function append() {
return $this->data[self::APPEND];
}
/**
* Checks if default path exists.
*
* @return bool
* Returns true if there is default data available.
*/
public function hasDefault() {
return isset($this->data[self::DEFAULT]);
}
/**
* Gets default path.
*
* @return string
* Path to default data
*/
public function default() {
return $this->data[self::DEFAULT];
}
/**
* Normalizes metadata by converting literal values into arrays.
*
* Conversions performed include:
* - Boolean 'false' means "skip".
* - A string means "replace", with the string value becoming the path.
*
* @param string $destination
* The destination path for the scaffold file.
* @param mixed $value
* The metadata for this operation object, which varies by operation type.
*
* @return array
* Normalized scaffold metadata with default values.
*/
protected function normalizeScaffoldMetadata($destination, $value) {
$defaultScaffoldMetadata = [
self::MODE => ReplaceOp::ID,
self::PREPEND => NULL,
self::APPEND => NULL,
self::DEFAULT => NULL,
self::OVERWRITE => TRUE,
];
return $this->convertScaffoldMetadata($destination, $value) + $defaultScaffoldMetadata;
}
/**
* Performs the conversion-to-array step in normalizeScaffoldMetadata.
*
* @param string $destination
* The destination path for the scaffold file.
* @param mixed $value
* The metadata for this operation object, which varies by operation type.
*
* @return array
* Normalized scaffold metadata.
*/
protected function convertScaffoldMetadata($destination, $value) {
if (is_bool($value)) {
if (!$value) {
return [self::MODE => SkipOp::ID];
}
throw new \RuntimeException("File mapping {$destination} cannot be given the value 'true'.");
}
if (empty($value)) {
throw new \RuntimeException("File mapping {$destination} cannot be empty.");
}
if (is_string($value)) {
$value = [self::PATH => $value];
}
// If there is no 'mode', but there is an 'append' or a 'prepend' path,
// then the mode is 'append' (append + prepend).
if (!isset($value[self::MODE]) && (isset($value[self::APPEND]) || isset($value[self::PREPEND]))) {
$value[self::MODE] = AppendOp::ID;
}
return $value;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\Composer;
use Composer\Package\PackageInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
/**
* Create Scaffold operation objects based on provided metadata.
*
* @internal
*/
class OperationFactory {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* OperationFactory constructor.
*
* @param \Composer\Composer $composer
* Reference to the 'Composer' object, since the Scaffold Operation Factory
* is also responsible for evaluating relative package paths as it creates
* scaffold operations.
*/
public function __construct(Composer $composer) {
$this->composer = $composer;
}
/**
* Creates a scaffolding operation object as determined by the metadata.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object; varies by operation type.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* The scaffolding operation object (skip, replace, etc.)
*
* @throws \RuntimeException
* Exception thrown when parameter data does not identify a known scaffold
* operation.
*/
public function create(PackageInterface $package, OperationData $operation_data) {
switch ($operation_data->mode()) {
case SkipOp::ID:
return new SkipOp();
case ReplaceOp::ID:
return $this->createReplaceOp($package, $operation_data);
case AppendOp::ID:
return $this->createAppendOp($package, $operation_data);
}
throw new \RuntimeException("Unknown scaffold operation mode <comment>{$operation_data->mode()}</comment>.");
}
/**
* Creates a 'replace' scaffold op.
*
* Replace ops may copy or symlink, depending on settings.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object, i.e. the relative 'path'.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* A scaffold replace operation object.
*/
protected function createReplaceOp(PackageInterface $package, OperationData $operation_data) {
if (!$operation_data->hasPath()) {
throw new \RuntimeException("'path' component required for 'replace' operations.");
}
$package_name = $package->getName();
$package_path = $this->getPackagePath($package);
$source = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->path());
$op = new ReplaceOp($source, $operation_data->overwrite());
return $op;
}
/**
* Creates an 'append' (or 'prepend') scaffold op.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object, i.e. the relative 'path'.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* A scaffold replace operation object.
*/
protected function createAppendOp(PackageInterface $package, OperationData $operation_data) {
$package_name = $package->getName();
$package_path = $this->getPackagePath($package);
$prepend_source_file = NULL;
$append_source_file = NULL;
$default_data_file = NULL;
if ($operation_data->hasPrepend()) {
$prepend_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->prepend());
}
if ($operation_data->hasAppend()) {
$append_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->append());
}
if ($operation_data->hasDefault()) {
$default_data_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->default());
}
if (!$this->hasContent($prepend_source_file) && !$this->hasContent($append_source_file)) {
$message = ' - Keep <info>[dest-rel-path]</info> unchanged: no content to prepend / append was provided.';
return new SkipOp($message);
}
return new AppendOp($prepend_source_file, $append_source_file, $operation_data->forceAppend(), $default_data_file);
}
/**
* Checks to see if the specified scaffold file exists and has content.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath|null $file
* (optional) Scaffold file to check.
*
* @return bool
* True if the file exists and has content.
*/
protected function hasContent(?ScaffoldFilePath $file = NULL) {
if (!$file) {
return FALSE;
}
$path = $file->fullPath();
return is_file($path) && (filesize($path) > 0);
}
/**
* Gets the file path of a package.
*
* Note that if we call getInstallPath on the root package, we get the
* wrong answer (the installation manager thinks our package is in
* vendor). We therefore add special checking for this case.
*
* @param \Composer\Package\PackageInterface $package
* The package.
*
* @return string
* The file path.
*/
protected function getPackagePath(PackageInterface $package) {
if ($package->getName() == $this->composer->getPackage()->getName()) {
// This will respect the --working-dir option if Composer is invoked with
// it. There is no API or method to determine the filesystem path of
// a package's composer.json file.
return getcwd();
}
return $this->composer->getInstallationManager()->getInstallPath($package);
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Interface for scaffold operation objects.
*
* @internal
*/
interface OperationInterface {
/**
* Returns the exact data that will be written to the scaffold files.
*
* @return string
* Data to be written to the scaffold location.
*/
public function contents();
/**
* Process this scaffold operation.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file's destination path.
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Various options that may alter the behavior of the operation.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* Result of the scaffolding operation.
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options);
/**
* Determines what to do if operation is used at same path as a previous op.
*
* Default behavior is to scaffold this operation at the specified
* destination, ignoring whatever was there before.
*
* @param OperationInterface $existing_target
* Existing file at the destination path that we should combine with.
*
* @return OperationInterface
* The op to use at this destination.
*/
public function scaffoldOverExistingTarget(OperationInterface $existing_target);
/**
* Determines what to do if operation is used without a previous operation.
*
* Default behavior is to scaffold this operation at the specified
* destination. Most operations overwrite rather than modify existing files,
* and therefore do not need to do anything special when there is no existing
* file.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file's destination path.
*
* @return OperationInterface
* The op to use at this destination.
*/
public function scaffoldAtNewLocation(ScaffoldFilePath $destination);
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to copy or symlink from source to destination.
*
* @internal
*/
class ReplaceOp extends AbstractOperation {
/**
* Identifies Replace operations.
*/
const ID = 'replace';
/**
* The relative path to the source file.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $source;
/**
* Whether to overwrite existing files.
*
* @var bool
*/
protected $overwrite;
/**
* Constructs a ReplaceOp.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $sourcePath
* The relative path to the source file.
* @param bool $overwrite
* Whether to allow this scaffold file to overwrite files already at
* the destination. Defaults to TRUE.
*/
public function __construct(ScaffoldFilePath $sourcePath, $overwrite = TRUE) {
$this->source = $sourcePath;
$this->overwrite = $overwrite;
}
/**
* {@inheritdoc}
*/
protected function generateContents() {
return file_get_contents($this->source->fullPath());
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$fs = new Filesystem();
$destination_path = $destination->fullPath();
// Do nothing if overwrite is 'false' and a file already exists at the
// destination.
if ($this->overwrite === FALSE && file_exists($destination_path)) {
$interpolator = $destination->getInterpolator();
$io->write($interpolator->interpolate(" - Skip <info>[dest-rel-path]</info> because it already exists and overwrite is <comment>false</comment>."));
return new ScaffoldResult($destination, FALSE);
}
// Get rid of the destination if it exists, and make sure that
// the directory where it's going to be placed exists.
$fs->remove($destination_path);
$fs->ensureDirectoryExists(dirname($destination_path));
if ($options->symlink()) {
return $this->symlinkScaffold($destination, $io);
}
return $this->copyScaffold($destination, $io);
}
/**
* Copies the scaffold file.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file to process.
* @param \Composer\IO\IOInterface $io
* IOInterface to writing to.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
protected function copyScaffold(ScaffoldFilePath $destination, IOInterface $io) {
$interpolator = $destination->getInterpolator();
$this->source->addInterpolationData($interpolator);
if (file_put_contents($destination->fullPath(), $this->contents()) === FALSE) {
throw new \RuntimeException($interpolator->interpolate("Could not copy source file <info>[src-rel-path]</info> to <info>[dest-rel-path]</info>!"));
}
$io->write($interpolator->interpolate(" - Copy <info>[dest-rel-path]</info> from <info>[src-rel-path]</info>"));
return new ScaffoldResult($destination, $this->overwrite);
}
/**
* Symlinks the scaffold file.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* Scaffold file to process.
* @param \Composer\IO\IOInterface $io
* IOInterface to writing to.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
protected function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io) {
$interpolator = $destination->getInterpolator();
try {
$fs = new Filesystem();
$fs->relativeSymlink($this->source->fullPath(), $destination->fullPath());
}
catch (\Exception $e) {
throw new \RuntimeException($interpolator->interpolate("Could not symlink source file <info>[src-rel-path]</info> to <info>[dest-rel-path]</info>!"), [], $e);
}
$io->write($interpolator->interpolate(" - Link <info>[dest-rel-path]</info> from <info>[src-rel-path]</info>"));
return new ScaffoldResult($destination, $this->overwrite);
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\Interpolator;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFileInfo;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Collection of scaffold files.
*
* @internal
*/
class ScaffoldFileCollection implements \IteratorAggregate {
/**
* Nested list of all scaffold files.
*
* The top level array maps from the package name to the collection of
* scaffold files provided by that package. Each collection of scaffold files
* is keyed by destination path.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFileInfo[][]
*/
protected $scaffoldFilesByProject = [];
/**
* ScaffoldFileCollection constructor.
*
* @param \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface[][] $file_mappings
* A multidimensional array of file mappings.
* @param \Drupal\Composer\Plugin\Scaffold\Interpolator $location_replacements
* An object with the location mappings (e.g. [web-root]).
*/
public function __construct(array $file_mappings, Interpolator $location_replacements) {
// Collection of all destination paths to be scaffolded. Used to determine
// when two projects scaffold the same file and we have to either replace or
// combine them together.
// @see OperationInterface::scaffoldOverExistingTarget().
$scaffoldFiles = [];
// Build the list of ScaffoldFileInfo objects by project.
foreach ($file_mappings as $package_name => $package_file_mappings) {
foreach ($package_file_mappings as $destination_rel_path => $op) {
$destination = ScaffoldFilePath::destinationPath($package_name, $destination_rel_path, $location_replacements);
// If there was already a scaffolding operation happening at this path,
// allow the new operation to decide how to handle the override.
// Usually, the new operation will replace whatever was there before.
if (isset($scaffoldFiles[$destination_rel_path])) {
$previous_scaffold_file = $scaffoldFiles[$destination_rel_path];
$op = $op->scaffoldOverExistingTarget($previous_scaffold_file->op());
// Remove the previous op so we only touch the destination once.
$message = " - Skip <info>[dest-rel-path]</info>: overridden in <comment>{$package_name}</comment>";
$this->scaffoldFilesByProject[$previous_scaffold_file->packageName()][$destination_rel_path] = new ScaffoldFileInfo($destination, new SkipOp($message));
}
// If there is NOT already a scaffolding operation happening at this
// path, notify the scaffold operation of this fact.
else {
$op = $op->scaffoldAtNewLocation($destination);
}
// Combine the scaffold operation with the destination and record it.
$scaffold_file = new ScaffoldFileInfo($destination, $op);
$scaffoldFiles[$destination_rel_path] = $scaffold_file;
$this->scaffoldFilesByProject[$package_name][$destination_rel_path] = $scaffold_file;
}
}
}
/**
* Removes any item that has a path matching any path in the provided list.
*
* Matching is done via destination path.
*
* @param string[] $files_to_filter
* List of destination paths
*/
public function filterFiles(array $files_to_filter) {
foreach ($this->scaffoldFilesByProject as $project_name => $scaffold_files) {
foreach ($scaffold_files as $destination_rel_path => $scaffold_file) {
if (in_array($destination_rel_path, $files_to_filter, TRUE)) {
unset($scaffold_files[$destination_rel_path]);
}
}
$this->scaffoldFilesByProject[$project_name] = $scaffold_files;
if (!$this->checkListHasItemWithContent($scaffold_files)) {
unset($this->scaffoldFilesByProject[$project_name]);
}
}
}
/**
* Scans through a list of scaffold files and determines if any has contents.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFileInfo[] $scaffold_files
* List of scaffold files, path: ScaffoldFileInfo
*
* @return bool
* TRUE if at least one item in the list has content
*/
protected function checkListHasItemWithContent(array $scaffold_files) {
foreach ($scaffold_files as $scaffold_file) {
$contents = $scaffold_file->op()->contents();
if (!empty($contents)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function getIterator() {
return new \ArrayIterator($this->scaffoldFilesByProject);
}
/**
* Processes the files in our collection.
*
* @param \Composer\IO\IOInterface $io
* The Composer IO object.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $scaffold_options
* The scaffold options.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[]
* The results array.
*/
public function processScaffoldFiles(IOInterface $io, ScaffoldOptions $scaffold_options) {
$results = [];
foreach ($this as $project_name => $scaffold_files) {
$io->write("Scaffolding files for <comment>{$project_name}</comment>:");
foreach ($scaffold_files as $scaffold_file) {
$results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options);
}
}
return $results;
}
/**
* Processes the iterator created by ScaffoldFileCollection::create().
*
* @param \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldFileCollection $collection
* The iterator to process.
* @param \Composer\IO\IOInterface $io
* The Composer IO object.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $scaffold_options
* The scaffold options.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult[]
* The results array.
*
* @deprecated. Called when upgrading from the Core Composer Scaffold plugin
* version 8.8.x due to a bug in the plugin and handler classes. Do not use
* in 8.9.x or 9.x, and remove in Drupal 10.x.
*/
public static function process(ScaffoldFileCollection $collection, IOInterface $io, ScaffoldOptions $scaffold_options) {
$results = [];
foreach ($collection as $project_name => $scaffold_files) {
$io->write("Scaffolding files for <comment>{$project_name}</comment>:");
foreach ($scaffold_files as $scaffold_file) {
$results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options);
}
}
return $results;
}
/**
* Returns the list of files that have not changed since they were scaffolded.
*
* Note that there are two reasons a file may have changed:
* - The user modified it after it was scaffolded.
* - The package the file came to was updated, and the file is different in
* the new version.
*
* With the current scaffold code, we cannot tell the difference between the
* two. @see https://www.drupal.org/project/drupal/issues/3092563
*
* @return string[]
* List of relative paths to unchanged files on disk.
*/
public function checkUnchanged() {
$results = [];
foreach ($this as $scaffold_files) {
foreach ($scaffold_files as $scaffold_file) {
if (!$scaffold_file->hasChanged()) {
$results[] = $scaffold_file->destination()->relativePath();
}
}
}
return $results;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
/**
* Record the result of a scaffold operation.
*
* @internal
*/
class ScaffoldResult {
/**
* The path to the scaffold file that was processed.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $destination;
/**
* Indicates if this scaffold file is managed by the scaffold command.
*
* @var bool
*/
protected $managed;
/**
* ScaffoldResult constructor.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* The path to the scaffold file that was processed.
* @param bool $isManaged
* (optional) Whether this result is managed. Defaults to FALSE.
*/
public function __construct(ScaffoldFilePath $destination, $isManaged = FALSE) {
$this->destination = $destination;
$this->managed = $isManaged;
}
/**
* Determines whether this scaffold file is managed.
*
* @return bool
* TRUE if this scaffold file is managed, FALSE if not.
*/
public function isManaged() {
return $this->managed;
}
/**
* Gets the destination scaffold file that this result refers to.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* The destination path for the scaffold result.
*/
public function destination() {
return $this->destination;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
use Drupal\Composer\Plugin\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to skip a scaffold file (do nothing).
*
* @internal
*/
class SkipOp extends AbstractOperation {
/**
* Identifies Skip operations.
*/
const ID = 'skip';
/**
* The message to output while processing.
*
* @var string
*/
protected $message;
/**
* SkipOp constructor.
*
* @param string $message
* (optional) A custom message to output while skipping.
*/
public function __construct($message = " - Skip <info>[dest-rel-path]</info>: disabled") {
$this->message = $message;
}
/**
* {@inheritdoc}
*/
protected function generateContents() {
return '';
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$interpolator = $destination->getInterpolator();
$io->write($interpolator->interpolate($this->message));
return new ScaffoldResult($destination, FALSE);
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\Plugin\Capability\CommandProvider;
use Composer\Plugin\Capable;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Drupal\Composer\Plugin\Scaffold\CommandProvider as ScaffoldCommandProvider;
/**
* Composer plugin for handling drupal scaffold.
*
* @internal
*/
class Plugin implements PluginInterface, EventSubscriberInterface, Capable {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The Composer Scaffold handler.
*
* @var \Drupal\Composer\Plugin\Scaffold\Handler
*/
protected $handler;
/**
* Record whether the 'require' command was called.
*
* @param bool
*/
protected $requireWasCalled;
/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->requireWasCalled = FALSE;
}
/**
* {@inheritdoc}
*/
public function deactivate(Composer $composer, IOInterface $io) {
}
/**
* {@inheritdoc}
*/
public function uninstall(Composer $composer, IOInterface $io) {
}
/**
* {@inheritdoc}
*/
public function getCapabilities() {
return [CommandProvider::class => ScaffoldCommandProvider::class];
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Important note: We only instantiate our handler on "post" events.
return [
ScriptEvents::POST_UPDATE_CMD => 'postCmd',
ScriptEvents::POST_INSTALL_CMD => 'postCmd',
PackageEvents::POST_PACKAGE_INSTALL => 'postPackage',
PluginEvents::COMMAND => 'onCommand',
];
}
/**
* Post command event callback.
*
* @param \Composer\Script\Event $event
* The Composer event.
*/
public function postCmd(Event $event) {
$this->handler()->scaffold();
}
/**
* Post package event behavior.
*
* @param \Composer\Installer\PackageEvent $event
* Composer package event sent on install/update/remove.
*/
public function postPackage(PackageEvent $event) {
$this->handler()->onPostPackageEvent($event);
}
/**
* Pre command event callback.
*
* @param \Composer\Plugin\CommandEvent $event
* The Composer command event.
*/
public function onCommand(CommandEvent $event) {
if ($event->getCommandName() == 'require') {
if ($this->handler) {
throw new \Error('Core Scaffold Plugin handler instantiated too early. See https://www.drupal.org/project/drupal/issues/3104922');
}
$this->requireWasCalled = TRUE;
}
}
/**
* Instantiates the handler object upon demand.
*
* It is dangerous to update a Composer plugin if it loads any classes prior
* to the `composer update` operation, and later tries to use them in a
* post-update hook.
*/
protected function handler() {
if (!$this->handler) {
$this->handler = new Handler($this->composer, $this->io);
// On instantiation of our handler, notify it if the 'require' command
// was executed.
if ($this->requireWasCalled) {
$this->handler->requireWasCalled();
}
}
return $this->handler;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Installer\PackageEvent;
/**
* Interface for post package event listeners.
*
* @see \Drupal\Composer\Plugin\Scaffold\Handler::onPostPackageEvent
*
* @internal
*/
interface PostPackageEventListenerInterface {
/**
* Handles package events during a 'composer require' operation.
*
* @param \Composer\Installer\PackageEvent $event
* Composer package event sent on install/update/remove.
*/
public function event(PackageEvent $event);
}
# Drupal Composer Scaffold
This project provides a composer plugin making the Drupal core Composer package
work correctly in a Composer project.
This takes care of:
- Placing scaffold files (like `index.php`, `update.php`, …) from the
`drupal/core` project into their desired location inside the web root. Only
individual files may be scaffolded with this plugin.
- Writing an autoload.php file to the web root, which includes the Composer
autoload.php file.
The purpose of scaffolding files is to allow Drupal sites to be fully managed by
Composer, and still allow individual asset files to be placed in arbitrary
locations. The goal of doing this is to enable a properly configured composer
template to produce a file layout that exactly matches the file layout of a
Drupal 8.7.x and earlier tarball distribution. Other file layouts will also be
possible; for example, a project layout very similar to the current
[drupal-composer/drupal-project](https://github.com/drupal-composer/drupal-scaffold)
template will also be provided. When one of these projects is used, the user
should be able to use `composer require` and `composer update` on a Drupal site
immediately after extracting the downloaded archive.
Note that the dependencies of a Drupal site are only able to scaffold files if
explicitly granted that right in the top-level composer.json file. See
[allowed packages](#allowed-packages), below.
## Usage
Drupal Composer Scaffold is used by requiring `drupal/core-composer-scaffold` in your
project, and providing configuration settings in the `extra` section of your
project's composer.json file. Additional configuration from the composer.json
file of your project's dependencies is also consulted in order to scaffold the
files a project needs. Additional information may be added to the beginning or
end of scaffold files, as is commonly done to `.htaccess` and `robots.txt`
files. See [altering scaffold files](#altering-scaffold-files) for more
information.
Typically, the scaffold operations run automatically as needed, e.g. after
`composer install`, so it is usually not necessary to do anything different
to scaffold a project once the configuration is set up in the project
composer.json file, as described below. To scaffold files directly, run:
```
composer drupal:scaffold
```
### Allowed Packages
Scaffold files are stored inside of projects that are required from the main
project's composer.json file as usual. The scaffolding operation happens after
`composer install`, and involves copying or symlinking the desired assets to
their destination location. In order to prevent arbitrary dependencies from
copying files via the scaffold mechanism, only those projects that are
specifically permitted by the top-level project will be used to scaffold files.
Example: Permit scaffolding from the project `upstream/project`
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"upstream/project"
],
...
}
}
```
Allowing a package to scaffold files also permits it to delegate permission to
scaffold to any project that it requires itself. This allows a package to
organize its scaffold assets as it sees fit. For example, if `upstream/project`
stores its assets in a subproject `upstream/assets`, `upstream/assets` would
implicitly be allowed to scaffold files.
It is possible for a project to obtain scaffold files from multiple projects.
For example, a Drupal project using a distribution, and installing on a specific
web hosting service provider might take its scaffold files from:
- Drupal core
- Its distribution
- A project provided by the hosting provider
- The project itself
Each project allowed to scaffold by the top-level project will be used in turn,
with projects declared later in the `allowed-packages` list taking precedence
over the projects named before. `drupal/core` is implicitly allowed and will be
placed at the top of the list. The top-level composer.json itself is also
implicitly allowed to scaffold files, and its scaffold files have highest
priority.
### Defining Project Locations
The top-level project in turn must define where the web root is located. It does
so via the `locations` mapping, as shown below:
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "./docroot"
},
...
}
}
```
This makes it possible to configure a project with different file layouts; for
example, either the `drupal/drupal` file layout or the
`drupal-composer/drupal-project` file layout could be used to set up a project.
If a web-root is not explicitly defined, then it will default to `.`, the same
directory as the composer.json file.
### Altering Scaffold Files
Sometimes, a project might wish to use a scaffold file provided by a dependency,
but alter it in some way. Two forms of alteration are supported: appending and
patching.
The example below shows a project that appends additional entries onto the end
of the `robots.txt` file provided by `drupal/core`:
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": {
"append": "assets/my-robots-additions.txt",
}
}
}
}
```
It is also possible to prepend to a scaffold file instead of, or in addition to
appending by including a "prepend" entry that provides the relative path to the
file to prepend to the scaffold file.
The example below demonstrates the use of the `post-drupal-scaffold-cmd` hook
to patch the `.htaccess` file using a patch.
```
"name": "my/project",
...
"scripts": {
"post-drupal-scaffold-cmd": [
"cd docroot && patch -p1 <../patches/htaccess-ssl.patch"
]
}
```
### Defining Scaffold Files
The placement of scaffold assets is under the control of the project that
provides them, but the location is always relative to some directory defined by
the root project -- usually the web root. For example, the scaffold file
`robots.txt` is copied from its source location, `assets/robots.txt` into the
web root in the snippet below.
```
{
"name": "drupal/assets",
...
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": "assets/robots.txt",
...
}
}
}
}
```
### Excluding Scaffold Files
Sometimes, a project might prefer to entirely replace a scaffold file provided
by a dependency, and receive no further updates for it. This can be done by
setting the value for the scaffold file to exclude to `false`:
```
"name": "my/project",
...
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": false
}
}
}
```
If possible, use the `append` and `prepend` directives as explained in [altering
scaffold files](#altering-scaffold-files), above. Excluding a file means that
your project will not get any bug fixes or other updates to files that are
modified locally.
### Overwrite
By default, scaffold files overwrite whatever content exists at the target
location. Sometimes a project may wish to provide the initial contents for a
file that will not be changed in subsequent updates. This can be done by setting
the `overwrite` flag to `false`, as shown in the example below:
```
{
"name": "service-provider/d8-scaffold-files",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/sites/default/settings.php": {
"mode": "replace",
"path": "assets/sites/default/settings.php",
"overwrite": false
}
}
}
}
}
```
Note that the `overwrite` directive is intended to be used by starter kits,
service providers, and so on. Individual Drupal sites should exclude the file
by setting its value to false instead.
### Autoload File
The scaffold tool automatically creates the required `autoload.php` file at the
Drupal root as part of the scaffolding operation. This file should not be
modified or customized in any way. If it is committed to the repository, though,
then the scaffold tool will stop managing it. If the location of the `vendor`
directory is changed for any reason, and the `autoload.php` file has been
committed to the repository, manually delete it and then run `composer install`
to update it.
## Specifications
Reference section for the configuration directives for the "drupal-scaffold"
section of the "extra" section of a `composer.json` file appear below.
### allowed-packages
The `allowed-packages` configuration setting contains an ordered list of package
names that will be used during the scaffolding phase.
```
"allowed-packages": [
"example/assets",
],
```
### file-mapping
The `file-mapping` configuration setting consists of a map from the destination
path of the file to scaffold to a set of properties that control how the file
should be scaffolded.
The available properties are as follows:
- mode: One of "replace", "append" or "skip".
- path: The path to the source file to write over the destination file.
- prepend: The path to the source file to prepend to the destination file, which
must always be a scaffold file provided by some other project.
- append: Like `prepend`, but appends content rather than prepends.
- overwrite: If `false`, prevents a `replace` from happening if the destination
already exists.
The mode may be inferred from the other properties. If the mode is not
specified, then the following defaults will be supplied:
- replace: Selected if a `path` property is present, or if the entry's value is
a string rather than a property set.
- append: Selected if a `prepend` or `append` property is present.
- skip: Selected if the entry's value is a boolean `false`.
Examples:
```
"file-mapping": {
"[web-root]/sites/default/default.settings.php": {
"mode": "replace",
"path": "assets/sites/default/default.settings.php",
"overwrite": true
},
"[web-root]/sites/default/settings.php": {
"mode": "replace",
"path": "assets/sites/default/settings.php",
"overwrite": false
},
"[web-root]/robots.txt": {
"mode": "append",
"prepend": "assets/robots-prequel.txt",
"append": "assets/robots-append.txt"
},
"[web-root]/.htaccess": {
"mode": "skip",
}
}
```
The short-form of the above example would be:
```
"file-mapping": {
"[web-root]/sites/default/default.settings.php": "assets/sites/default/default.settings.php",
"[web-root]/sites/default/settings.php": {
"path": "assets/sites/default/settings.php",
"overwrite": false
},
"[web-root]/robots.txt": {
"prepend": "assets/robots-prequel.txt",
"append": "assets/robots-append.txt"
},
"[web-root]/.htaccess": false
}
```
Note that there is no distinct "prepend" mode; "append" mode is used to both
append and prepend to scaffold files. The reason for this is that scaffold file
entries are identified in the file-mapping section keyed by their destination
path, and it is not possible for multiple entries to have the same key. If
"prepend" were a separate mode, then it would not be possible to both prepend
and append to the same file.
By default, append operations may only be applied to files that were scaffolded
by a previously evaluated project. If the `force-append` attribute is added to
an `append` operation, though, then the append will be made to non-scaffolded
files if and only if the append text does not already appear in the file. When
using this mode, it is also possible to provide default contents to use in the
event that the destination file is entirely missing.
The example below demonstrates scaffolding a settings-custom.php file, and
including it from the existing `settings.php` file.
```
"file-mapping": {
"[web-root]/sites/default/settings-custom.php": "assets/settings-custom.php",
"[web-root]/sites/default/settings.php": {
"append": "assets/include-settings-custom.txt",
"force-append": true,
"default": "assets/initial-default-settings.txt"
}
}
```
Note that the example above still works if used with a project that scaffolds
the settings.php file.
### gitignore
The `gitignore` configuration setting controls whether or not this plugin will
manage `.gitignore` files for files written during the scaffold operation.
- true: `.gitignore` files will be updated when scaffold files are written.
- false: `.gitignore` files will never be modified.
- Not set: `.gitignore` files will be updated if the target directory is a local
working copy of a git repository, and the `vendor` directory is ignored
in that repository.
### locations
The `locations` configuration setting contains a list of named locations that
may be used in placing scaffold files. The only required location is `web-root`.
Other locations may also be defined if desired.
```
"locations": {
"web-root": "./docroot"
},
```
### symlink
The `symlink` property causes `replace` operations to make a symlink to the
source file rather than copying it. This is useful when doing core development,
as the symlink files themselves should not be edited. Note that `append`
operations override the `symlink` option, to prevent the original scaffold
assets from being altered.
```
"symlink": true,
```
## Managing Scaffold Files
Scaffold files should be treated the same way that the `vendor` directory is
handled. If you need to commit `vendor` (e.g. in order to deploy your site),
then you should also commit your scaffold files. You should not commit your
`vendor` directory or scaffold files unless it is necessary.
If a dependency provides a scaffold file with `overwrite` set to `false`, that
file should be committed to your repository.
By default, `.gitignore` files will be automatically updated if needed when
scaffold files are written. See the `gitignore` setting in the Specifications
section above.
## Examples
Some full-length examples appear below.
Sample composer.json for a project that relies on packages that use composer-scaffold:
```
{
"name": "my/project",
"require": {
"drupal/core-composer-scaffold": "*",
"composer/installers": "^2.0",
"cweagans/composer-patches": "^1.6.5",
"drupal/core": "^8.8.x-dev",
"service-provider/d8-scaffold-files": "^1"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "./docroot"
},
"symlink": true,
"file-mapping": {
"[web-root]/.htaccess": false,
"[web-root]/robots.txt": "assets/robots-default.txt"
}
}
}
}
```
Sample composer.json for drupal/core, with assets placed in a different project:
```
{
"name": "drupal/core",
"extra": {
"drupal-scaffold": {
"allowed-packages": [
"drupal/assets",
]
}
}
}
```
Sample composer.json for composer-scaffold files in drupal/assets:
```
{
"name": "drupal/assets",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/.csslintrc": "assets/.csslintrc",
"[web-root]/.editorconfig": "assets/.editorconfig",
"[web-root]/.eslintignore": "assets/.eslintignore",
"[web-root]/.eslintrc.json": "assets/.eslintrc.json",
"[web-root]/.gitattributes": "assets/.gitattributes",
"[web-root]/.ht.router.php": "assets/.ht.router.php",
"[web-root]/.htaccess": "assets/.htaccess",
"[web-root]/sites/default/default.services.yml": "assets/default.services.yml",
"[web-root]/sites/default/default.settings.php": "assets/default.settings.php",
"[web-root]/sites/example.settings.local.php": "assets/example.settings.local.php",
"[web-root]/sites/example.sites.php": "assets/example.sites.php",
"[web-root]/index.php": "assets/index.php",
"[web-root]/robots.txt": "assets/robots.txt",
"[web-root]/update.php": "assets/update.php",
"[web-root]/web.config": "assets/web.config"
}
}
}
}
```
Sample composer.json for a library that implements composer-scaffold:
```
{
"name": "service-provider/d8-scaffold-files",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/sites/default/settings.php": "assets/sites/default/settings.php"
}
}
}
}
```
Append to robots.txt:
```
{
"name": "service-provider/d8-scaffold-files",
"extra": {
"drupal-scaffold": {
"file-mapping": {
"[web-root]/robots.txt": {
"append": "assets/my-robots-additions.txt",
}
}
}
}
}
```
Patch a file after it's copied:
```
"post-drupal-scaffold-cmd": [
"cd docroot && patch -p1 <../patches/htaccess-ssl.patch"
]
```
## Related Plugins
### drupal-composer/drupal-scaffold
Previous versions of Drupal Composer Scaffold (see community project,
[drupal-composer/drupal-scaffold](https://github.com/drupal-composer/drupal-project))
downloaded each scaffold file directly from its distribution server (e.g.
`https://git.drupalcode.org`) to the desired destination directory. This was
necessary, because there was no subtree split of the scaffold files available.
Copying the scaffold assets from projects already downloaded by Composer is more
effective, as downloading and unpacking archive files is more efficient than
downloading each scaffold file individually.
### composer/installers
The [composer/installers](https://github.com/composer/installers) plugin is
similar to this plugin in that it allows dependencies to be installed in
locations other than the `vendor` directory. However, Composer and the
`composer/installers` plugin have a limitation that one project cannot be moved
inside of another project. Therefore, if you use `composer/installers` to place
Drupal modules inside the directory `web/modules/contrib`, then you cannot also
use `composer/installers` to place files such as `index.php` and `robots.txt`
into the `web` directory. The drupal-scaffold plugin was created to work around
this limitation.
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\IO\IOInterface;
use Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface;
/**
* Data object that keeps track of one scaffold file.
*
* Scaffold files are identified primarily by their destination path. Each
* scaffold file also has an 'operation' object that controls how the scaffold
* file will be placed (e.g. via copy or symlink, or maybe by appending multiple
* files together). The operation may have one or more source files.
*
* @internal
*/
class ScaffoldFileInfo {
/**
* The path to the destination.
*
* @var \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
*/
protected $destination;
/**
* The operation used to create the destination.
*
* @var \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
*/
protected $op;
/**
* Constructs a ScaffoldFileInfo object.
*
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath $destination
* The full and relative paths to the destination file and the package
* defining it.
* @param \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface $op
* Operations object that will handle scaffolding operations.
*/
public function __construct(ScaffoldFilePath $destination, OperationInterface $op) {
$this->destination = $destination;
$this->op = $op;
}
/**
* Gets the Scaffold operation.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\OperationInterface
* Operations object that handles scaffolding (copy, make symlink, etc).
*/
public function op() {
return $this->op;
}
/**
* Gets the package name.
*
* @return string
* The name of the package this scaffold file info was collected from.
*/
public function packageName() {
return $this->destination->packageName();
}
/**
* Gets the destination.
*
* @return \Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath
* The scaffold path to the destination file.
*/
public function destination() {
return $this->destination;
}
/**
* Determines if this scaffold file has been overridden by another package.
*
* @param string $providing_package
* The name of the package that provides the scaffold file at this location,
* as returned by self::findProvidingPackage()
*
* @return bool
* Whether this scaffold file if overridden or removed.
*/
public function overridden($providing_package) {
return $this->packageName() !== $providing_package;
}
/**
* Replaces placeholders in a message.
*
* @param string $message
* Message with placeholders to fill in.
* @param array $extra
* Additional data to merge with the interpolator.
* @param mixed $default
* Default value to use for missing placeholders, or FALSE to keep them.
*
* @return string
* Interpolated string with placeholders replaced.
*/
public function interpolate($message, array $extra = [], $default = FALSE) {
$interpolator = $this->destination->getInterpolator();
return $interpolator->interpolate($message, $extra, $default);
}
/**
* Moves a single scaffold file from source to destination.
*
* @param \Composer\IO\IOInterface $io
* The scaffold file to be processed.
* @param \Drupal\Composer\Plugin\Scaffold\ScaffoldOptions $options
* Assorted operational options, e.g. whether the destination should be a
* symlink.
*
* @return \Drupal\Composer\Plugin\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
public function process(IOInterface $io, ScaffoldOptions $options) {
return $this->op()->process($this->destination, $io, $options);
}
/**
* Returns TRUE if the target does not exist or has changed.
*
* @return bool
*/
final public function hasChanged() {
$path = $this->destination()->fullPath();
if (!file_exists($path)) {
return TRUE;
}
return $this->op()->contents() !== file_get_contents($path);
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
use Composer\Util\Filesystem;
/**
* Manage the path to a file to scaffold.
*
* Both the relative and full path to the file is maintained so that the shorter
* name may be used in progress and error messages, as needed. The name of the
* package that provided the file path is also recorded for the same reason.
*
* ScaffoldFilePaths may be used to represent destination scaffold files, or the
* source files used to create them. Static factory methods named
* destinationPath and sourcePath, respectively, are provided to create
* ScaffoldFilePath objects.
*
* @internal
*/
class ScaffoldFilePath {
/**
* The type of scaffold file this is,'autoload', 'dest' or 'src'.
*
* @var string
*/
protected $type;
/**
* The name of the package containing the file.
*
* @var string
*/
protected $packageName;
/**
* The relative path to the file.
*
* @var string
*/
protected $relativePath;
/**
* The full path to the file.
*
* @var string
*/
protected $fullPath;
/**
* ScaffoldFilePath constructor.
*
* @param string $path_type
* The type of scaffold file this is,'autoload', 'dest' or 'src'.
* @param string $package_name
* The name of the package containing the file.
* @param string $rel_path
* The relative path to the file.
* @param string $full_path
* The full path to the file.
*/
public function __construct($path_type, $package_name, $rel_path, $full_path) {
$this->type = $path_type;
$this->packageName = $package_name;
$this->relativePath = $rel_path;
$this->fullPath = $full_path;
// Ensure that the full path really is a full path. We do not use
// 'realpath' here because the file specified by the full path might
// not exist yet.
$fs = new Filesystem();
if (!$fs->isAbsolutePath($this->fullPath)) {
$this->fullPath = getcwd() . '/' . $this->fullPath;
}
}
/**
* Gets the name of the package this source file was pulled from.
*
* @return string
* Name of package.
*/
public function packageName() {
return $this->packageName;
}
/**
* Gets the relative path to the source file (best to use in messages).
*
* @return string
* Relative path to file.
*/
public function relativePath() {
return $this->relativePath;
}
/**
* Gets the full path to the source file.
*
* @return string
* Full path to file.
*/
public function fullPath() {
return $this->fullPath;
}
/**
* Converts the relative source path into an absolute path.
*
* The path returned will be relative to the package installation location.
*
* @param string $package_name
* The name of the package containing the source file. Only used for error
* messages.
* @param string $package_path
* The installation path of the package containing the source file.
* @param string $destination
* Destination location provided as a relative path. Only used for error
* messages.
* @param string $source
* Source location provided as a relative path.
*
* @return self
* Object wrapping the relative and absolute path to the source file.
*/
public static function sourcePath($package_name, $package_path, $destination, $source) {
// Complain if there is no source path.
if (empty($source)) {
throw new \RuntimeException("No scaffold file path given for {$destination} in package {$package_name}.");
}
// Calculate the full path to the source scaffold file.
$source_full_path = $package_path . '/' . $source;
if (!file_exists($source_full_path)) {
throw new \RuntimeException("Scaffold file {$source} not found in package {$package_name}.");
}
if (is_dir($source_full_path)) {
throw new \RuntimeException("Scaffold file {$source} in package {$package_name} is a directory; only files may be scaffolded.");
}
return new self('src', $package_name, $source, $source_full_path);
}
/**
* Converts the relative destination path into an absolute path.
*
* Any placeholders in the destination path, e.g. '[web-root]', will be
* replaced using the provided location replacements interpolator.
*
* @param string $package_name
* The name of the package defining the destination path.
* @param string $destination
* The relative path to the destination file being scaffolded.
* @param \Drupal\Composer\Plugin\Scaffold\Interpolator $location_replacements
* Interpolator that includes the [web-root] and any other available
* placeholder replacements.
*
* @return self
* Object wrapping the relative and absolute path to the destination file.
*/
public static function destinationPath($package_name, $destination, Interpolator $location_replacements) {
$dest_full_path = $location_replacements->interpolate($destination);
return new self('dest', $package_name, $destination, $dest_full_path);
}
/**
* Adds data about the relative and full path to the provided interpolator.
*
* @param \Drupal\Composer\Plugin\Scaffold\Interpolator $interpolator
* Interpolator to add data to.
* @param string $name_prefix
* (optional) Prefix to add before -rel-path and -full-path item names.
* Defaults to path type provided when constructing this object.
*/
public function addInterpolationData(Interpolator $interpolator, $name_prefix = '') {
if (empty($name_prefix)) {
$name_prefix = $this->type;
}
$data = [
'package-name' => $this->packageName(),
"{$name_prefix}-rel-path" => $this->relativePath(),
"{$name_prefix}-full-path" => $this->fullPath(),
];
$interpolator->addData($data);
}
/**
* Interpolate a string using the data from this scaffold file info.
*
* @param string $name_prefix
* (optional) Prefix to add before -rel-path and -full-path item names.
* Defaults to path type provided when constructing this object.
*
* @return \Drupal\Composer\Plugin\Scaffold\Interpolator
* An interpolator for making string replacements.
*/
public function getInterpolator($name_prefix = '') {
$interpolator = new Interpolator();
$this->addInterpolationData($interpolator, $name_prefix);
return $interpolator;
}
}
<?php
namespace Drupal\Composer\Plugin\Scaffold;
/**
* Per-project options from the 'extras' section of the composer.json file.
*
* Projects that describe scaffold files do so via their scaffold options. This
* data is pulled from the 'drupal-scaffold' portion of the extras section of
* the project data.
*
* @internal
*/
class ScaffoldOptions {
/**
* The raw data from the 'extras' section of the top-level composer.json file.
*
* @var array
*/
protected $options = [];
/**
* ScaffoldOptions constructor.
*
* @param array $options
* The scaffold options taken from the 'drupal-scaffold' section.
*/
protected function __construct(array $options) {
$this->options = $options + [
"allowed-packages" => [],
"locations" => [],
"symlink" => FALSE,
"file-mapping" => [],
];
// Define any default locations.
$this->options['locations'] += [
'project-root' => '.',
'web-root' => '.',
];
}
/**
* Determines if the provided 'extras' section has scaffold options.
*
* @param array $extras
* The contents of the 'extras' section.
*
* @return bool
* True if scaffold options have been declared
*/
public static function hasOptions(array $extras) {
return array_key_exists('drupal-scaffold', $extras);
}
/**
* Creates a scaffold options object.
*
* @param array $extras
* The contents of the 'extras' section.
*
* @return self
* The scaffold options object representing the provided scaffold options
*/
public static function create(array $extras) {
$options = static::hasOptions($extras) ? $extras['drupal-scaffold'] : [];
return new self($options);
}
/**
* Creates a new scaffold options object with some values overridden.
*
* @param array $options
* Override values.
*
* @return self
* The scaffold options object representing the provided scaffold options
*/
protected function override(array $options) {
return new self($options + $this->options);
}
/**
* Creates a new scaffold options object with an overridden 'symlink' value.
*
* @param bool $symlink
* Whether symlinking should be enabled or not.
*
* @return self
* The scaffold options object representing the provided scaffold options
*/
public function overrideSymlink($symlink) {
return $this->override(['symlink' => $symlink]);
}
/**
* Determines whether any allowed packages were defined.
*
* @return bool
* Whether there are allowed packages
*/
public function hasAllowedPackages() {
return !empty($this->allowedPackages());
}
/**
* Gets allowed packages from these options.
*
* @return array
* The list of allowed packages
*/
public function allowedPackages() {
return $this->options['allowed-packages'];
}
/**
* Gets the location mapping table, e.g. 'webroot' => './'.
*
* @return array
* A map of name : location values
*/
public function locations() {
return $this->options['locations'];
}
/**
* Determines whether a given named location is defined.
*
* @param string $name
* The location name to search for.
*
* @return bool
* True if the specified named location exist.
*/
protected function hasLocation($name) {
return array_key_exists($name, $this->locations());
}
/**
* Gets a specific named location.
*
* @param string $name
* The name of the location to fetch.
*
* @return string
* The value of the provided named location
*/
public function getLocation($name) {
return $this->hasLocation($name) ? $this->locations()[$name] : FALSE;
}
/**
* Determines if symlink mode is set.
*
* @return bool
* Whether or not 'symlink' mode
*/
public function symlink() {
return $this->options['symlink'];
}
/**
* Determines if there are file mappings.
*
* @return bool
* Whether or not the scaffold options contain any file mappings
*/
public function hasFileMapping() {
return !empty($this->fileMapping());
}
/**
* Returns the actual file mappings.
*
* @return array
* File mappings for just this config type.
*/
public function fileMapping() {
return $this->options['file-mapping'];
}
/**
* Determines if there is defined a value for the 'gitignore' option.
*
* @return bool
* Whether or not there is a 'gitignore' option setting
*/
public function hasGitIgnore() {
return isset($this->options['gitignore']);
}
/**
* Gets the value of the 'gitignore' option.
*
* @return bool
* The 'gitignore' option, or TRUE if undefined.
*/
public function gitIgnore() {
return $this->hasGitIgnore() ? $this->options['gitignore'] : TRUE;
}
}
HOW-TO: Test this Drupal composer plugin
In order to test this plugin, you'll need to get the entire Drupal repo and
run the tests there.
You'll find the tests under core/tests/Drupal/Tests/Composer/Plugin.
You can get the full Drupal repo here:
https://www.drupal.org/project/drupal/git-instructions
You can find more information about running PHPUnit tests with Drupal here:
https://www.drupal.org/node/2116263
Each component in the Drupal\Composer\Plugin namespace has its own annotated test
group. You can use this group to run only the tests for this component. Like
this:
$ ./vendor/bin/phpunit -c core --group Scaffold
{
"name": "drupal/core-composer-scaffold",
"description": "A flexible Composer project scaffold builder.",
"type": "composer-plugin",
"keywords": ["drupal"],
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0-or-later",
"require": {
"composer-plugin-api": "^2",
"php": ">=7.3.0"
},
"conflict": {
"drupal-composer/drupal-scaffold": "*"
},
"autoload": {
"psr-4": {
"Drupal\\Composer\\Plugin\\Scaffold\\": ""
}
},
"extra": {
"class": "Drupal\\Composer\\Plugin\\Scaffold\\Plugin",
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"config": {
"sort-packages": true
},
"require-dev": {
"composer/composer": "^1.8@stable"
}
}
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
<?php
namespace Drupal\Composer\Plugin\ProjectMessage;
use Composer\Package\RootPackageInterface;
/**
* Determine configuration.
*
* @internal
*/
class Message {
/**
* The root package.
*
* @var \Composer\Package\RootPackageInterface
*/
protected $rootPackage;
/**
* The name of the event.
*
* @var string
*/
protected $eventName;
/**
* The message to display.
*
* @var string[]
*/
protected $messageText = [];
/**
* Construct a Config object.
*
* @param \Composer\Package\RootPackageInterface $root_package
* Composer package object for the root package.
* @param string $event_name
* The event name.
*/
public function __construct(RootPackageInterface $root_package, $event_name) {
$this->rootPackage = $root_package;
$this->eventName = $event_name;
}
public function getText() {
if ($this->messageText) {
return $this->messageText;
}
$package_config = $this->rootPackage->getExtra();
$file = $this->eventName . '-message.txt';
if ($config_file = $package_config['drupal-core-project-message'][$this->eventName . '-file'] ?? FALSE) {
$file = $config_file;
}
$message = $package_config['drupal-core-project-message'][$this->eventName . '-message'] ?? [];
if ($message) {
$this->messageText = $message;
}
else {
$this->messageText = $this->getMessageFromFile($file);
}
// Include structured support info from composer.json.
if ($config_keys = $package_config['drupal-core-project-message']['include-keys'] ?? FALSE) {
foreach ($config_keys as $config_key) {
switch ($config_key) {
case 'name':
if ($homepage = $this->rootPackage->getName()) {
$this->messageText[] = ' * Name: ' . $homepage;
}
break;
case 'description':
if ($homepage = $this->rootPackage->getDescription()) {
$this->messageText[] = ' * Description: ' . $homepage;
}
break;
case 'homepage':
if ($homepage = $this->rootPackage->getHomepage()) {
$this->messageText[] = ' * Homepage: ' . $homepage;
}
break;
case 'support':
if ($support = $this->rootPackage->getSupport()) {
$this->messageText[] = ' * Support:';
foreach ($support as $support_key => $support_value) {
$this->messageText[] = ' * ' . $support_key . ': ' . $support_value;
}
}
break;
}
}
}
return $this->messageText;
}
/**
* Reads the message file as an array.
*
* @param string $file
* The file to read. Relative paths are relative to the project directory.
*
* @return string[]
*/
protected function getMessageFromFile($file) {
return file_exists($file) ? file($file, FILE_IGNORE_NEW_LINES) : [];
}
}
<?php
namespace Drupal\Composer\Plugin\ProjectMessage;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
/**
* A Composer plugin to display a message after creating a project.
*
* @internal
*/
class MessagePlugin implements PluginInterface, EventSubscriberInterface {
/**
* Composer object.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* IO object.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* Configuration.
*
* @var \Drupal\Composer\Plugin\VendorHardening\Config
*/
protected $config;
/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
}
/**
* {@inheritdoc}
*/
public function deactivate(Composer $composer, IOInterface $io) {
}
/**
* {@inheritdoc}
*/
public function uninstall(Composer $composer, IOInterface $io) {
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
ScriptEvents::POST_CREATE_PROJECT_CMD => 'displayPostCreateMessage',
ScriptEvents::POST_INSTALL_CMD => 'displayPostCreateMessage',
];
}
public function displayPostCreateMessage(Event $event) {
$message = new Message($this->composer->getPackage(), $event->getName());
if ($message = $message->getText()) {
$this->io->write($message);
}
}
}
The Drupal Project Message Plugin
=================================
Thanks for using this Drupal component.
You can participate in its development on Drupal.org, through our issue system:
https://www.drupal.org/project/issues/drupal
You can get the full Drupal repo here:
https://www.drupal.org/project/drupal/git-instructions
You can browse the full Drupal repo here:
https://git.drupalcode.org/project/drupal
What does it do?
----------------
This Composer plugin displays a configurable message after Composer installation
processes have finished.
This is ideal for a 'next steps' type prompt to help get the user oriented.
Currently only two Composer events are supported:
- post-create-project-cmd, when a `composer create-project` command has
finished.
- post-install-cmd, when a `composer install` command has finished.
How do I set it up?
-------------------
Require this Composer plugin in your project template composer.json file:
"require": {
"drupal/core-project-message": "^8.8"
}
### Configuration
There are three ways to configure this plugin to output information:
- Using a text file.
- Using composer.json schema keys.
- Embedding the information in the extra section of the composer.json file.
### Using a text file
By default, the plugin will respond to `post-install-cmd` or
`post-create-project-cmd` Composer events by looking for a similarly-named file
in the root of the project. For instance, if the user issues a `composer
create-project` command, when that command is finished, the plugin will look for
a file named `post-create-project-cmd-message.txt` and then display it on the
command line.
The file should be plain text, with markup suitable for Symfony's
`OutputInterface::writeln()` method. See documentation here:
https://symfony.com/doc/3.4/console/coloring.html
You can also configure your own file(s), using the `extra` section of your
composer.json file:
"extra": {
"drupal-core-project-message": {
"post-create-project-cmd-file": "bespoke/special_file.txt"
}
}
### Using composer.json schema keys
You can tell the plugin to output the structured support information from the
composer.json file by telling it the keys you wish to display.
Currently, only `name`, `description`, `homepage` and `support` are supported.
"extra": {
"drupal-core-project-message": {
"include-keys": ["homepage", "support"],
}
}
Then you can include this information in your composer.json file, which you
should probably be doing anyway.
### Embedding the information in the extra section
You can specify text directly within the `extra` section by using the
`[event-name]-message` key. This message should be an array, with one string for
each line:
"extra": {
"drupal-core-project-message": {
"post-create-project-cmd-message": [
"Thanks for installing this project.",
"Read our documentation here: http://example.com/docs"
]
}
}
These strings should be plain text, with markup suitable for Symfony's
`OutputInterface::writeln()` method. See documentation here:
https://symfony.com/doc/3.4/console/coloring.html
The `-message` section will always override `-file` for a given event.
HOW-TO: Test this Drupal component
In order to test this component, you'll need to get the entire Drupal repo and
run the tests there.
You'll find the tests under core/tests/Drupal/Tests/Plugin.
You can get the full Drupal repo here:
https://www.drupal.org/project/drupal/git-instructions
You can find more information about running PHPUnit tests with Drupal here:
https://www.drupal.org/node/2116263
Each component in the Drupal\Composer namespace has its own annotated test
group. You can use this group to run only the tests for this component. Like
this:
$ ./vendor/bin/phpunit -c core --group ProjectMessage
{
"name": "drupal/core-project-message",
"description": "Adds a message after Composer installation.",
"keywords": ["drupal"],
"homepage": "https://www.drupal.org/project/drupal",
"license": "GPL-2.0-or-later",
"type": "composer-plugin",
"autoload": {
"psr-4": {
"Drupal\\Composer\\Plugin\\ProjectMessage\\": "."
}
},
"extra": {
"class": "Drupal\\Composer\\Plugin\\ProjectMessage\\MessagePlugin"
},
"require": {
"php": ">=7.3.0",
"composer-plugin-api": "^2"
}
}
# This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
# PHP PSR-2 Coding Standards
# http://www.php-fig.org/psr/psr-2/
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{xml,yml}]
indent_size = 2
name: composer-installers-extender
services:
php:
type: php:7.1
via: cli
xdebug: true
tooling:
composer:
service: php
description: Run composer commands
cmd:
- composer
- --ansi
php:
service: php
language: php
php:
- 5.6
- 7.0
- 7.1
- 7.2
- nightly
matrix:
allow_failures:
- php: 5.6
- php: 7.0
fast_finish: true
install: composer install
script:
- composer lint
- composer test
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog][] and this project adheres to the
[Semantic Versioning][] scheme.
[Keep a Changelog]: http://keepachangelog.com
[Semantic Versioning]: https://semver.org
## [Unreleased]
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security
## [2.0.1] - 2021-12-15
### Changed
- Add support for composer/installers 2.x
## [2.0.0] - 2020-08-11
### Added
- Add `.editorconfig`, `.gitignore`, `.lando.yml`, `phpcs.xml` and `phpunit.xml`
files to support local development
- Add `LICENSE` and `CHANGELOG.md` files
- Add `phpunit/phpunit` and `squizlabs/php_codesniffer` as development
dependencies
- Add requirement for PHP 7.1
- Add support for Composer 2
### Changed
- Move `OomphInc\ComposerInstallersExtender\Installer` to
`OomphInc\ComposerInstallersExtender\Installers\Installer`
- Move `OomphInc\ComposerInstallersExtender\InstallerHelper` to
`OomphInc\ComposerInstallersExtender\Installers\CustomInstaller`
- Implement PSR-2 standards and PHP 7.1 syntax
- Update project `README.md` file
## [1.1.2] - 2017-03-31
### Changed
- Minor syntax update to provide compatibility with PHP 5.3
## [1.1.1] - 2016-07-05
### Changed
- Update composer/installers version requirement
## [1.1.0] - 2016-03-02
### Changed
- Update package requirements to be less restrictive
## [1.0.0] - 2016-01-07
### Added
- Initial release
MIT License
Copyright (c) 2018 Oomph, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Composer Installers Extender
The `composer-installers-extender` is a plugin for [Composer][] that allows
any package to be installed to a directory other than the default `vendor`
directory within a project on a package-by-package basis. This plugin extends
the [`composer/installers`][] plugin to allow any arbitrary package type to be
handled by their custom installer.
The [`composer/installers`][] plugin has a finite set of supported package types
and we recognize the need for any arbitrary package type to be installed to a
specific directory other than `vendor`. This plugin allows additional package
types to be handled by the [`composer/installers`][] plugin, benefiting from
their explicit install path mapping and token replacement of package properties.
## How to Install
Add `oomphinc/composer-installers-extender` as a dependency of your project:
```bash
$ composer require oomphinc/composer-installers-extender
```
This plugin requires at least PHP 7.1. If you're using a lower version of PHP
use the latest stable 1.x release:
```bash
$ composer require oomphinc/composer-installers-extender:^1.1
```
## How to Use
The [`composer/installers`][] plugin is a dependency of this plugin and will be
automatically required as well if not already required.
To support additional package types, add an array of these types in the
`extra` property in your `composer.json`:
with [`composer/installers`][] < v1.0.13:
```json
{
"extra": {
"installer-types": ["library"]
}
}
```
with [`composer/installers`][] >= v1.0.13:
```json
{
"extra": {
"installer-types": ["drupal-library"]
}
}
```
Then refer to that type when adding to `installer-paths`:
with [`composer/installers`][] < v1.0.13:
```json
{
"extra": {
"installer-types": ["library"],
"installer-paths": {
"special/package/": ["my/package"],
"path/to/libraries/{$name}/": ["type:library"]
}
}
}
```
with [`composer/installers`][] >= v1.0.13:
```json
{
"extra": {
"installer-types": ["drupal-library"],
"installer-paths": {
"special/package/": ["my/package"],
"path/to/libraries/{$name}/": ["type:drupal-library"]
}
}
}
```
By default, packages that do not specify a `type` will be considered the type
`library`. Adding support for this type allows any of these packages to be
placed in a different install path.
If a type has been added to `installer-types`, the plugin will attempt to find
an explicit installer path in the mapping. If there is no match either by name
or by type, the default installer path for all packages will be used instead.
**Please see the README for [`composer/installers`][] to see the supported syntax
for package and type matching as well as the supported replacement tokens in
the path (e.g. `{$name}`).**
## License
[MIT License][]
[Composer]: https://getcomposer.org
[`composer/installers`]: https://github.com/composer/installers
[MIT License]: LICENSE
{
"name": "oomphinc/composer-installers-extender",
"description": "Extend the composer/installers plugin to accept any arbitrary package type.",
"homepage": "http://www.oomphinc.com/",
"type": "composer-plugin",
"license": "MIT",
"authors": [
{
"name": "Stephen Beemsterboer",
"email": "stephen@oomphinc.com",
"homepage": "https://github.com/balbuf"
},
{
"name": "Nathan Dentzau",
"email": "nate@oomphinc.com",
"homepage": "http://oomph.is/ndentzau"
}
],
"support": {
"issues": "https://github.com/oomphinc/composer-installers-extender/issues"
},
"require": {
"php": ">=7.1",
"composer-plugin-api": "^1.1 || ^2.0",
"composer/installers": "^1.0 || ^2.0"
},
"require-dev": {
"composer/composer": "^2.0",
"phpunit/phpunit": "^7.2",
"squizlabs/php_codesniffer": "^3.3"
},
"autoload":{
"psr-4": {
"OomphInc\\ComposerInstallersExtender\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"OomphInc\\ComposerInstallerExtender\\Tests\\": "tests/src/"
}
},
"scripts": {
"fix": "vendor/bin/phpcbf",
"lint": "vendor/bin/phpcs",
"test": "vendor/bin/phpunit"
},
"config": {
"sort-packages": true
},
"extra": {
"class": "OomphInc\\ComposerInstallersExtender\\Plugin"
},
"minimum-stability": "dev",
"prefer-stable": true
}
<?xml version="1.0"?>
<ruleset name="Composer Installers Extender">
<description>
Coding standards for the Composer Installers Extender project using PSR-2
with PHP 7 syntax.
</description>
<!-- Directories to scan -->
<file>src/</file>
<!-- PHPCS argument overrides -->
<arg name="colors" />
<!-- PHP configuration overrides -->
<ini name="memory_limit" value="-1" />
<!-- Project autoloader -->
<autoload>vendor/autoload.php</autoload>
<!-- Import rules -->
<rule ref="PSR2" />
</ruleset>
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
beStrictAboutChangesToGlobalState="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
bootstrap="vendor/autoload.php">
<php>
<ini name="error_reporting" value="32767" />
<ini name="memory_limit" value="-1" />
</php>
<testsuites>
<testsuite name="unit">
<directory>./tests/src</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="tests/results/html"/>
<log type="coverage-clover" target="tests/results/coverage.xml"/>
<log type="coverage-text" target="tests/results/coverage.txt"/>
</logging>
</phpunit>
<?php
declare(strict_types = 1);
namespace OomphInc\ComposerInstallersExtender\Installers;
use Composer\Installers\BaseInstaller;
/**
* Provides a custom installer class for custom installer types.
*
* By default, the parent class has no specified locations. By not providing an
* array of locations we are forcing the installer to use custom installer
* paths.
*/
class CustomInstaller extends BaseInstaller
{
}
<?php
declare(strict_types = 1);
namespace OomphInc\ComposerInstallersExtender\Installers;
use Composer\Package\PackageInterface;
use Composer\Installer\LibraryInstaller;
use Composer\Installers\Installer as InstallerBase;
class Installer extends InstallerBase
{
/**
* A list of installer types.
*
* @var array
*/
protected $installerTypes;
/**
* {@inheritDoc}
*/
public function getInstallPath(PackageInterface $package): string
{
$installer = new CustomInstaller($package, $this->composer, $this->io);
$path = $installer->getInstallPath($package, $package->getType());
return $path ?: LibraryInstaller::getInstallPath($package);
}
/**
* {@inheritDoc}
*/
public function supports($packageType): bool
{
return in_array($packageType, $this->getInstallerTypes());
}
/**
* Get a list of custom installer types.
*
* @return array
*/
public function getInstallerTypes(): array
{
if (!$this->installerTypes) {
$extra = $this->composer->getPackage()->getExtra();
$this->installerTypes = $extra['installer-types'] ?? [];
}
return $this->installerTypes;
}
}
<?php
declare(strict_types = 1);
namespace OomphInc\ComposerInstallersExtender;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use OomphInc\ComposerInstallersExtender\Installers\Installer;
class Plugin implements PluginInterface
{
/**
* {@inheritDoc}
*/
public function activate(Composer $composer, IOInterface $io): void
{
$installer = new Installer($io, $composer);
$composer->getInstallationManager()->addInstaller($installer);
}
/**
* {@inheritDoc}
*/
public function deactivate(Composer $composer, IOInterface $io): void
{
}
/**
* {@inheritDoc}
*/
public function uninstall(Composer $composer, IOInterface $io): void
{
}
}
<?php
declare(strict_types = 1);
namespace OomphInc\ComposerInstallersExtender\Tests\Installers;
use PHPUnit\Framework\TestCase;
use OomphInc\ComposerInstallersExtender\Installers\CustomInstaller;
class CustomInstallerTest extends TestCase
{
public function testLocations(): void
{
$installer = (new \ReflectionClass(CustomInstaller::class))
->newInstanceWithoutConstructor();
$this->assertEmpty($installer->getLocations());
}
}
<?php
declare(strict_types = 1);
namespace OomphInc\ComposerInstallersExtender\Installers;
use Composer\Composer;
use Composer\IO\IOInterface;
use PHPUnit\Framework\TestCase;
use Composer\Package\Package;
class InstallerTest extends TestCase
{
protected $composer;
protected $io;
public function setUp(): void
{
parent::setUp();
$this->composer = $this->createMock(Composer::class);
$this->composer
->method('getConfig')
->willReturn(new class {
public function get($name) {
return null;
}
});
$this->io = $this->createMock(IOInterface::class);
}
public function testGetInstallPath(): void
{
$this->composer
->method('getPackage')
->willReturn(new class {
public function getExtra()
{
return [
'installer-types' => ['custom-type'],
'installer-paths' => [
'custom/path/{$name}' => ['type:custom-type'],
],
];
}
});
$installer = new Installer($this->io, $this->composer);
$package = new Package('oomphinc/test', '1.0.0', '1.0.0');
$package->setType('custom-type');
$this->assertEquals(
'custom/path/test',
$installer->getInstallPath($package)
);
}
public function testSupports(): void
{
$installer = new class extends Installer {
public function __construct() {}
public function getInstallerTypes(): array
{
return ['custom-type'];
}
};
$this->assertTrue($installer->supports('custom-type'));
$this->assertFalse($installer->supports('oomph'));
}
/**
* @dataProvider installerTypesDataProvider
*/
public function testGetInstallerTypes($package, array $expected): void
{
$this->composer
->method('getPackage')
->willReturn($package);
$installer = new Installer($this->io, $this->composer);
$this->assertEquals($expected, $installer->getInstallerTypes());
}
public function installerTypesDataProvider(): array
{
return [
[
new class {
public function getExtra(): array
{
return [
'installer-types' => ['custom-type'],
'installer-paths' => [
'custom/path/{$name}' => ['type:custom-type'],
],
];
}
},
['custom-type'],
],
[
new class {
public function getExtra(): array
{
return [];
}
},
[],
],
];
}
}
<?php
declare(strict_types = 1);
namespace OomphInc\ComposerInstallersExtender\Tests;
use Composer\Composer;
use Composer\IO\IOInterface;
use PHPUnit\Framework\TestCase;
use Composer\Installer\InstallationManager;
use OomphInc\ComposerInstallersExtender\Installers\Installer;
use OomphInc\ComposerInstallersExtender\Plugin;
class PluginTest extends TestCase
{
protected $composer;
protected $io;
public function setUp(): void
{
parent::setUp();
$this->composer = $this->createMock(Composer::class);
$this->composer
->method('getConfig')
->willReturn(new class{
public function get($name)
{
return null;
}
});
$this->io = $this->createMock(IOInterface::class);
}
public function testActive(): void
{
$installationManager = $this->createMock(InstallationManager::class);
$installationManager
->expects($this->once())
->method('addInstaller')
->with(new Installer($this->io, $this->composer));
$this->composer
->expects($this->once())
->method('getInstallationManager')
->willReturn($installationManager);
// There is no output to test from the activate method. Only test for
// method call expectations.
$plugin = new Plugin();
$plugin->activate($this->composer, $this->io);
}
}
Copyright (c) 2015 Bryan Davis, Wikimedia Foundation, and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
[![Latest Stable Version]](https://packagist.org/packages/wikimedia/composer-merge-plugin) [![License]](https://github.com/wikimedia/composer-merge-plugin/blob/master/LICENSE)
[![Build Status]](https://github.com/wikimedia/composer-merge-plugin/actions/workflows/CI.yaml)
[![Code Coverage]](https://scrutinizer-ci.com/g/wikimedia/composer-merge-plugin/?branch=master)
Composer Merge Plugin
=====================
Merge multiple composer.json files at [Composer] runtime.
Composer Merge Plugin is intended to allow easier dependency management for
applications which ship a composer.json file and expect some deployments to
install additional Composer managed libraries. It does this by allowing the
application's top level `composer.json` file to provide a list of optional
additional configuration files. When Composer is run it will parse these files
and merge their configuration settings into the base configuration. This
combined configuration will then be used when downloading additional libraries
and generating the autoloader.
Composer Merge Plugin was created to help with installation of [MediaWiki]
which has core library requirements as well as optional libraries and
extensions which may be managed via Composer.
Installation
------------
Composer Merge Plugin 1.4.x (and older) requires Composer 1.x.
Composer Merge Plugin 2.0.x (and newer) is compatible with both Composer 2.x and 1.x.
```
$ composer require wikimedia/composer-merge-plugin
```
### Upgrading from Composer 1 to 2
If you are already using Composer Merge Plugin 1.4 (or older) and you are updating the plugin to 2.0 (or newer), it is recommended that you update the plugin first using Composer 1.
If you update the incompatible plugin using Composer 2, the plugin will be ignored:
> The "wikimedia/composer-merge-plugin" plugin was skipped because it requires a Plugin API version ("^1.0") that does not match your Composer installation ("2.0.0"). You may need to run composer update with the "--no-plugins" option.
Consequently, Composer will be unaware of the merged dependencies and will remove them requiring you to run `composer update` again to reinstall merged dependencies.
Usage
-----
```json
{
"require": {
"wikimedia/composer-merge-plugin": "dev-master"
},
"extra": {
"merge-plugin": {
"include": [
"composer.local.json",
"extensions/*/composer.json"
],
"require": [
"submodule/composer.json"
],
"recurse": true,
"replace": false,
"ignore-duplicates": false,
"merge-dev": true,
"merge-extra": false,
"merge-extra-deep": false,
"merge-replace": true,
"merge-scripts": false
}
}
}
```
### Updating sub-levels `composer.json` files
In order for Composer Merge Plugin to install dependencies from updated or newly created sub-level `composer.json` files in your project you need to run the command:
```
$ composer update
```
This will [instruct Composer to recalculate the file hash](https://getcomposer.org/doc/03-cli.md#update) for the top-level `composer.json` thus triggering Composer Merge Plugin to look for the sub-level configuration files and update your dependencies.
Plugin configuration
--------------------
The plugin reads its configuration from the `merge-plugin` section of your
composer.json's `extra` section. An `include` setting is required to tell
Composer Merge Plugin which file(s) to merge.
### include
The `include` setting can specify either a single value or an array of values.
Each value is treated as a PHP `glob()` pattern identifying additional
composer.json style configuration files to merge into the root package
configuration for the current Composer execution.
The following sections of the found configuration files will be merged into
the Composer root package configuration as though they were directly included
in the top-level composer.json file:
* [autoload](https://getcomposer.org/doc/04-schema.md#autoload)
* [autoload-dev](https://getcomposer.org/doc/04-schema.md#autoload-dev)
(optional, see [merge-dev](#merge-dev) below)
* [conflict](https://getcomposer.org/doc/04-schema.md#conflict)
* [provide](https://getcomposer.org/doc/04-schema.md#provide)
* [replace](https://getcomposer.org/doc/04-schema.md#replace)
(optional, see [merge-replace](#merge-replace) below)
* [repositories](https://getcomposer.org/doc/04-schema.md#repositories)
* [require](https://getcomposer.org/doc/04-schema.md#require)
* [require-dev](https://getcomposer.org/doc/04-schema.md#require-dev)
(optional, see [merge-dev](#merge-dev) below)
* [suggest](https://getcomposer.org/doc/04-schema.md#suggest)
* [extra](https://getcomposer.org/doc/04-schema.md#extra)
(optional, see [merge-extra](#merge-extra) below)
* [scripts](https://getcomposer.org/doc/04-schema.md#scripts)
(optional, see [merge-scripts](#merge-scripts) below)
### require
The `require` setting is identical to [`include`](#include) except when
a pattern fails to match at least one file then it will cause an error.
### recurse
By default the merge plugin is recursive; if an included file has
a `merge-plugin` section it will also be processed. This functionality can be
disabled by adding a `"recurse": false` setting.
### replace
By default, Composer's conflict resolution engine is used to determine which
version of a package should be installed when multiple files specify the same
package. A `"replace": true` setting can be provided to change to a "last
version specified wins" conflict resolution strategy. In this mode, duplicate
package declarations found in merged files will overwrite the declarations
made by earlier files. Files are loaded in the order specified by the
`include` setting with globbed files being processed in alphabetical order.
### ignore-duplicates
By default, Composer's conflict resolution engine is used to determine which
version of a package should be installed when multiple files specify the same
package. An `"ignore-duplicates": true` setting can be provided to change to
a "first version specified wins" conflict resolution strategy. In this mode,
duplicate package declarations found in merged files will be ignored in favor
of the declarations made by earlier files. Files are loaded in the order
specified by the `include` setting with globbed files being processed in
alphabetical order.
Note: `"replace": true` and `"ignore-duplicates": true` modes are mutually
exclusive. If both are set, `"ignore-duplicates": true` will be used.
### merge-dev
By default, `autoload-dev` and `require-dev` sections of included files are
merged. A `"merge-dev": false` setting will disable this behavior.
### merge-extra
A `"merge-extra": true` setting enables the merging the contents of the
`extra` section of included files as well. The normal merge mode for the extra
section is to accept the first version of any key found (e.g. a key in the
master config wins over the version found in any imported config). If
`replace` mode is active ([see above](#replace)) then this behavior changes
and the last key found will win (e.g. the key in the master config is replaced
by the key in the imported config). If `"merge-extra-deep": true` is specified
then, the sections are merged similar to array_merge_recursive() - however
duplicate string array keys are replaced instead of merged, while numeric
array keys are merged as usual. The usefulness of merging the extra section
will vary depending on the Composer plugins being used and the order in which
they are processed by Composer.
Note that `merge-plugin` sections are excluded from the merge process, but are
always processed by the plugin unless [recursion](#recurse) is disabled.
### merge-replace
By default, the `replace` section of included files are merged.
A `"merge-replace": false` setting will disable this behavior.
### merge-scripts
A `"merge-scripts": true` setting enables merging the contents of the
`scripts` section of included files as well. The normal merge mode for the
scripts section is to accept the first version of any key found (e.g. a key in
the master config wins over the version found in any imported config). If
`replace` mode is active ([see above](#replace)) then this behavior changes
and the last key found will win (e.g. the key in the master config is replaced
by the key in the imported config).
Note: [custom commands][] added by merged configuration will work when invoked
as `composer run-script my-cool-command` but will not be available using the
normal `composer my-cool-command` shortcut.
Running tests
-------------
```
$ composer install
$ composer test
```
Contributing
------------
Bug, feature requests and other issues should be reported to the [GitHub
project]. We accept code and documentation contributions via Pull Requests on
GitHub as well.
- [PSR-2 Coding Standard][] is used by the project. The included test
configuration uses [PHP_CodeSniffer][] to validate the conventions.
- Tests are encouraged. Our test coverage isn't perfect but we'd like it to
get better rather than worse, so please try to include tests with your
changes.
- Keep the documentation up to date. Make sure `README.md` and other
relevant documentation is kept up to date with your changes.
- One pull request per feature. Try to keep your changes focused on solving
a single problem. This will make it easier for us to review the change and
easier for you to make sure you have updated the necessary tests and
documentation.
License
-------
Composer Merge plugin is licensed under the MIT license. See the
[`LICENSE`](LICENSE) file for more details.
---
[Composer]: https://getcomposer.org/
[MediaWiki]: https://www.mediawiki.org/wiki/MediaWiki
[GitHub project]: https://github.com/wikimedia/composer-merge-plugin
[PSR-2 Coding Standard]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
[PHP_CodeSniffer]: http://pear.php.net/package/PHP_CodeSniffer
[Latest Stable Version]: https://img.shields.io/packagist/v/wikimedia/composer-merge-plugin.svg?style=flat
[License]: https://img.shields.io/packagist/l/wikimedia/composer-merge-plugin.svg?style=flat
[Build Status]: https://github.com/wikimedia/composer-merge-plugin/actions/workflows/CI.yaml/badge.svg
[Code Coverage]: https://img.shields.io/scrutinizer/coverage/g/wikimedia/composer-merge-plugin/master.svg?style=flat
[custom commands]: https://getcomposer.org/doc/articles/scripts.md#writing-custom-commands
{
"name": "wikimedia/composer-merge-plugin",
"description": "Composer plugin to merge multiple composer.json files",
"type": "composer-plugin",
"license": "MIT",
"authors": [
{
"name": "Bryan Davis",
"email": "bd808@wikimedia.org"
}
],
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=7.2.0",
"composer-plugin-api": "^1.1||^2.0"
},
"require-dev": {
"ext-json": "*",
"composer/composer": "^1.1||^2.0",
"mediawiki/mediawiki-phan-config": "0.11.1",
"php-parallel-lint/php-parallel-lint": "~1.3.1",
"phpspec/prophecy": "~1.15.0",
"phpunit/phpunit": "^8.5||^9.0",
"squizlabs/php_codesniffer": "~3.7.1"
},
"autoload": {
"psr-4": {
"Wikimedia\\Composer\\Merge\\V2\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
},
"class": "Wikimedia\\Composer\\Merge\\V2\\MergePlugin"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"scripts": {
"coverage": [
"phpunit --log-junit=reports/unitreport.xml --coverage-text --coverage-html=reports/coverage --coverage-clover=reports/coverage.xml",
"phpcs --encoding=utf-8 --standard=PSR2 --report-checkstyle=reports/checkstyle-phpcs.xml --report-full --extensions=php src/* tests/phpunit/*"
],
"phan": "phan -d . --long-progress-bar --allow-polyfill-parser",
"phpcs": "phpcs --encoding=utf-8 --standard=PSR2 --extensions=php src/* tests/phpunit/*",
"phpunit": "phpunit",
"test": [
"composer validate --no-interaction",
"parallel-lint src tests",
"@phpunit",
"@phpcs"
]
}
}
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Composer;
use Composer\Json\JsonFile;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Package\Loader\ArrayLoader;
use Composer\Package\RootAliasPackage;
use Composer\Package\RootPackage;
use Composer\Package\RootPackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\Semver\Intervals;
use UnexpectedValueException;
/**
* Processing for a composer.json file that will be merged into
* a RootPackageInterface
*
* @author Bryan Davis <bd808@bd808.com>
*/
class ExtraPackage
{
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var Logger $logger
*/
protected $logger;
/**
* @var string $path
*/
protected $path;
/**
* @var array $json
*/
protected $json;
/**
* @var CompletePackage $package
*/
protected $package;
/**
* @var array<string, bool> $mergedRequirements
*/
protected $mergedRequirements = [];
/**
* @var VersionParser $versionParser
*/
protected $versionParser;
/**
* @param string $path Path to composer.json file
* @param Composer $composer
* @param Logger $logger
*/
public function __construct($path, Composer $composer, Logger $logger)
{
$this->path = $path;
$this->composer = $composer;
$this->logger = $logger;
$this->json = $this->readPackageJson($path);
$this->package = $this->loadPackage($this->json);
$this->versionParser = new VersionParser();
}
/**
* Get list of additional packages to include if precessing recursively.
*
* @return array
*/
public function getIncludes()
{
return isset($this->json['extra']['merge-plugin']['include']) ?
$this->fixRelativePaths($this->json['extra']['merge-plugin']['include']) : [];
}
/**
* Get list of additional packages to require if precessing recursively.
*
* @return array
*/
public function getRequires()
{
return isset($this->json['extra']['merge-plugin']['require']) ?
$this->fixRelativePaths($this->json['extra']['merge-plugin']['require']) : [];
}
/**
* Get list of merged requirements from this package.
*
* @return string[]
*/
public function getMergedRequirements()
{
return array_keys($this->mergedRequirements);
}
/**
* Read the contents of a composer.json style file into an array.
*
* The package contents are fixed up to be usable to create a Package
* object by providing dummy "name" and "version" values if they have not
* been provided in the file. This is consistent with the default root
* package loading behavior of Composer.
*
* @param string $path
* @return array
*/
protected function readPackageJson($path)
{
$file = new JsonFile($path);
$json = $file->read();
if (!isset($json['name'])) {
$json['name'] = 'merge-plugin/' .
strtr($path, DIRECTORY_SEPARATOR, '-');
}
if (!isset($json['version'])) {
$json['version'] = '1.0.0';
}
return $json;
}
/**
* @param array $json
* @return CompletePackage
*/
protected function loadPackage(array $json)
{
$loader = new ArrayLoader();
$package = $loader->load($json);
// @codeCoverageIgnoreStart
if (!$package instanceof CompletePackage) {
throw new UnexpectedValueException(
'Expected instance of CompletePackage, got ' .
get_class($package)
);
}
// @codeCoverageIgnoreEnd
return $package;
}
/**
* Merge this package into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeInto(RootPackageInterface $root, PluginState $state)
{
$this->prependRepositories($root);
$this->mergeRequires('require', $root, $state);
$this->mergePackageLinks('conflict', $root);
if ($state->shouldMergeReplace()) {
$this->mergePackageLinks('replace', $root);
}
$this->mergePackageLinks('provide', $root);
$this->mergeSuggests($root);
$this->mergeAutoload('autoload', $root);
$this->mergeExtra($root, $state);
$this->mergeScripts($root, $state);
if ($state->isDevMode()) {
$this->mergeDevInto($root, $state);
} else {
$this->mergeReferences($root);
$this->mergeAliases($root);
}
}
/**
* Merge just the dev portion into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeDevInto(RootPackageInterface $root, PluginState $state)
{
$this->mergeRequires('require-dev', $root, $state);
$this->mergeAutoload('devAutoload', $root);
$this->mergeReferences($root);
$this->mergeAliases($root);
}
/**
* Add a collection of repositories described by the given configuration
* to the given package and the global repository manager.
*
* @param RootPackageInterface $root
*/
protected function prependRepositories(RootPackageInterface $root)
{
if (!isset($this->json['repositories'])) {
return;
}
$repoManager = $this->composer->getRepositoryManager();
$newRepos = [];
foreach ($this->json['repositories'] as $repoJson) {
if (!isset($repoJson['type'])) {
continue;
}
if ($repoJson['type'] === 'path' && isset($repoJson['url'])) {
$repoJson['url'] = $this->fixRelativePaths(array($repoJson['url']))[0];
}
$this->logger->info("Prepending {$repoJson['type']} repository");
$repo = $repoManager->createRepository(
$repoJson['type'],
$repoJson
);
$repoManager->prependRepository($repo);
$newRepos[] = $repo;
}
$unwrapped = self::unwrapIfNeeded($root, 'setRepositories');
$unwrapped->setRepositories(array_merge(
$newRepos,
$root->getRepositories()
));
}
/**
* Merge require or require-dev into a RootPackageInterface
*
* @param string $type 'require' or 'require-dev'
* @param RootPackageInterface $root
* @param PluginState $state
*/
protected function mergeRequires(
$type,
RootPackageInterface $root,
PluginState $state
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$requires = $this->package->{$getter}();
if (empty($requires)) {
return;
}
$this->mergeStabilityFlags($root, $requires);
$requires = $this->replaceSelfVersionDependencies(
$type,
$requires,
$root
);
$root->{$setter}($this->mergeOrDefer(
$type,
$root->{$getter}(),
$requires,
$state
));
}
/**
* Merge two collections of package links and collect duplicates for
* subsequent processing.
*
* @param string $type 'require' or 'require-dev'
* @param array $origin Primary collection
* @param array $merge Additional collection
* @param PluginState $state
* @return array Merged collection
*/
protected function mergeOrDefer(
$type,
array $origin,
array $merge,
PluginState $state
) {
if ($state->ignoreDuplicateLinks() && $state->replaceDuplicateLinks()) {
$this->logger->warning("Both replace and ignore-duplicates are true. These are mutually exclusive.");
$this->logger->warning("Duplicate packages will be ignored.");
}
foreach ($merge as $name => $link) {
if (isset($origin[$name])) {
if ($state->ignoreDuplicateLinks()) {
$this->logger->info("Ignoring duplicate <comment>{$name}</comment>");
continue;
}
if ($state->replaceDuplicateLinks()) {
$this->logger->info("Replacing <comment>{$name}</comment>");
$this->mergedRequirements[$name] = true;
$origin[$name] = $link;
} else {
$this->logger->info("Merging <comment>{$name}</comment>");
$this->mergedRequirements[$name] = true;
$origin[$name] = $this->mergeConstraints($origin[$name], $link, $state);
}
} else {
$this->logger->info("Adding <comment>{$name}</comment>");
$this->mergedRequirements[$name] = true;
$origin[$name] = $link;
}
}
if (!$state->isComposer1()) {
Intervals::clear();
}
return $origin;
}
/**
* Merge package constraints.
*
* Adapted from Composer's UpdateCommand::appendConstraintToLink
*
* @param Link $origin The base package link.
* @param Link $merge The related package link to merge.
* @param PluginState $state
* @return Link Merged link.
*/
protected function mergeConstraints(Link $origin, Link $merge, PluginState $state)
{
$oldPrettyString = $origin->getConstraint()->getPrettyString();
$newPrettyString = $merge->getConstraint()->getPrettyString();
if ($state->isComposer1()) {
$constraintClass = MultiConstraint::class;
} else {
$constraintClass = \Composer\Semver\Constraint\MultiConstraint::class;
if (Intervals::isSubsetOf($origin->getConstraint(), $merge->getConstraint())) {
return $origin;
}
if (Intervals::isSubsetOf($merge->getConstraint(), $origin->getConstraint())) {
return $merge;
}
}
$newConstraint = $constraintClass::create([
$origin->getConstraint(),
$merge->getConstraint()
], true);
$newConstraint->setPrettyString($oldPrettyString.', '.$newPrettyString);
return new Link(
$origin->getSource(),
$origin->getTarget(),
$newConstraint,
$origin->getDescription(),
$origin->getPrettyConstraint() . ', ' . $newPrettyString
);
}
/**
* Merge autoload or autoload-dev into a RootPackageInterface
*
* @param string $type 'autoload' or 'devAutoload'
* @param RootPackageInterface $root
*/
protected function mergeAutoload($type, RootPackageInterface $root)
{
$getter = 'get' . ucfirst($type);
$setter = 'set' . ucfirst($type);
$autoload = $this->package->{$getter}();
if (empty($autoload)) {
return;
}
$unwrapped = self::unwrapIfNeeded($root, $setter);
$unwrapped->{$setter}(array_merge_recursive(
$root->{$getter}(),
$this->fixRelativePaths($autoload)
));
}
/**
* Fix a collection of paths that are relative to this package to be
* relative to the base package.
*
* @param array $paths
* @return array
*/
protected function fixRelativePaths(array $paths)
{
$base = dirname($this->path);
$base = ($base === '.') ? '' : "{$base}/";
array_walk_recursive(
$paths,
function (&$path) use ($base) {
$path = "{$base}{$path}";
}
);
return $paths;
}
/**
* Extract and merge stability flags from the given collection of
* requires and merge them into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param array $requires
*/
protected function mergeStabilityFlags(
RootPackageInterface $root,
array $requires
) {
$flags = $root->getStabilityFlags();
$sf = new StabilityFlags($flags, $root->getMinimumStability());
$unwrapped = self::unwrapIfNeeded($root, 'setStabilityFlags');
$unwrapped->setStabilityFlags(array_merge(
$flags,
$sf->extractAll($requires)
));
}
/**
* Merge package links of the given type into a RootPackageInterface
*
* @param string $type 'conflict', 'replace' or 'provide'
* @param RootPackageInterface $root
*/
protected function mergePackageLinks($type, RootPackageInterface $root)
{
$linkType = BasePackage::$supportedLinkTypes[$type];
$getter = 'get' . ucfirst($linkType['method']);
$setter = 'set' . ucfirst($linkType['method']);
$links = $this->package->{$getter}();
if (!empty($links)) {
$unwrapped = self::unwrapIfNeeded($root, $setter);
// @codeCoverageIgnoreStart
if ($root !== $unwrapped) {
$this->logger->warning(
'This Composer version does not support ' .
"'{$type}' merging for aliased packages."
);
}
// @codeCoverageIgnoreEnd
$unwrapped->{$setter}(array_merge(
$root->{$getter}(),
$this->replaceSelfVersionDependencies($type, $links, $root)
));
}
}
/**
* Merge suggested packages into a RootPackageInterface
*
* @param RootPackageInterface $root
*/
protected function mergeSuggests(RootPackageInterface $root)
{
$suggests = $this->package->getSuggests();
if (!empty($suggests)) {
$unwrapped = self::unwrapIfNeeded($root, 'setSuggests');
$unwrapped->setSuggests(array_merge(
$root->getSuggests(),
$suggests
));
}
}
/**
* Merge extra config into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeExtra(RootPackageInterface $root, PluginState $state)
{
$extra = $this->package->getExtra();
unset($extra['merge-plugin']);
if (!$state->shouldMergeExtra() || empty($extra)) {
return;
}
$rootExtra = $root->getExtra();
$unwrapped = self::unwrapIfNeeded($root, 'setExtra');
if ($state->replaceDuplicateLinks()) {
$unwrapped->setExtra(
self::mergeExtraArray($state->shouldMergeExtraDeep(), $rootExtra, $extra)
);
} else {
if (!$state->shouldMergeExtraDeep()) {
foreach (array_intersect(
array_keys($extra),
array_keys($rootExtra)
) as $key) {
$this->logger->info(
"Ignoring duplicate <comment>{$key}</comment> in ".
"<comment>{$this->path}</comment> extra config."
);
}
}
$unwrapped->setExtra(
self::mergeExtraArray($state->shouldMergeExtraDeep(), $extra, $rootExtra)
);
}
}
/**
* Merge scripts config into a RootPackageInterface
*
* @param RootPackageInterface $root
* @param PluginState $state
*/
public function mergeScripts(RootPackageInterface $root, PluginState $state)
{
$scripts = $this->package->getScripts();
if (!$state->shouldMergeScripts() || empty($scripts)) {
return;
}
$rootScripts = $root->getScripts();
$unwrapped = self::unwrapIfNeeded($root, 'setScripts');
if ($state->replaceDuplicateLinks()) {
$unwrapped->setScripts(
array_merge($rootScripts, $scripts)
);
} else {
$unwrapped->setScripts(
array_merge($scripts, $rootScripts)
);
}
}
/**
* Merges two arrays either via arrayMergeDeep or via array_merge.
*
* @param bool $mergeDeep
* @param array $array1
* @param array $array2
* @return array
*/
public static function mergeExtraArray($mergeDeep, $array1, $array2)
{
if ($mergeDeep) {
return NestedArray::mergeDeep($array1, $array2);
}
return array_merge($array1, $array2);
}
/**
* Update Links with a 'self.version' constraint with the root package's
* version.
*
* @param string $type Link type
* @param array $links
* @param RootPackageInterface $root
* @return array
*/
protected function replaceSelfVersionDependencies(
$type,
array $links,
RootPackageInterface $root
) {
$linkType = BasePackage::$supportedLinkTypes[$type];
$version = $root->getVersion();
$prettyVersion = $root->getPrettyVersion();
$vp = $this->versionParser;
$method = 'get' . ucfirst($linkType['method']);
$packages = $root->$method();
return array_map(
static function ($link) use ($linkType, $version, $prettyVersion, $vp, $packages) {
if ('self.version' === $link->getPrettyConstraint()) {
if (isset($packages[$link->getSource()])) {
/** @var Link $package */
$package = $packages[$link->getSource()];
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($package->getConstraint()->getPrettyString()),
$linkType['description'],
$package->getPrettyConstraint()
);
}
return new Link(
$link->getSource(),
$link->getTarget(),
$vp->parseConstraints($version),
$linkType['description'],
$prettyVersion
);
}
return $link;
},
$links
);
}
/**
* Get a full featured Package from a RootPackageInterface.
*
* In Composer versions before 599ad77 the RootPackageInterface only
* defines a sub-set of operations needed by composer-merge-plugin and
* RootAliasPackage only implemented those methods defined by the
* interface. Most of the unimplemented methods in RootAliasPackage can be
* worked around because the getter methods that are implemented proxy to
* the aliased package which we can modify by unwrapping. The exception
* being modifying the 'conflicts', 'provides' and 'replaces' collections.
* We have no way to actually modify those collections unfortunately in
* older versions of Composer.
*
* @param RootPackageInterface $root
* @param string $method Method needed
* @return RootPackageInterface|RootPackage
*/
public static function unwrapIfNeeded(
RootPackageInterface $root,
$method = 'setExtra'
) {
// @codeCoverageIgnoreStart
if ($root instanceof RootAliasPackage &&
!method_exists($root, $method)
) {
// Unwrap and return the aliased RootPackage.
$root = $root->getAliasOf();
}
// @codeCoverageIgnoreEnd
return $root;
}
protected function mergeAliases(RootPackageInterface $root)
{
$aliases = [];
$unwrapped = self::unwrapIfNeeded($root, 'setAliases');
foreach (array('require', 'require-dev') as $linkType) {
$linkInfo = BasePackage::$supportedLinkTypes[$linkType];
$method = 'get'.ucfirst($linkInfo['method']);
$links = [];
foreach ($unwrapped->$method() as $link) {
$links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
}
$aliases = $this->extractAliases($links, $aliases);
}
$unwrapped->setAliases($aliases);
}
/**
* Extract aliases from version constraints (dev-branch as 1.0.0).
*
* @param array $requires
* @param array $aliases
* @return array
* @see RootPackageLoader::extractAliases()
*/
protected function extractAliases(array $requires, array $aliases)
{
foreach ($requires as $reqName => $reqVersion) {
if (preg_match('{^([^,\s#]+)(?:#[^ ]+)? +as +([^,\s]+)$}', $reqVersion, $match)) {
$aliases[] = [
'package' => strtolower($reqName),
'version' => $this->versionParser->normalize($match[1], $reqVersion),
'alias' => $match[2],
'alias_normalized' => $this->versionParser->normalize($match[2], $reqVersion),
];
} elseif (strpos($reqVersion, ' as ') !== false) {
throw new UnexpectedValueException(
'Invalid alias definition in "'.$reqName.'": "'.$reqVersion.'". '
. 'Aliases should be in the form "exact-version as other-exact-version".'
);
}
}
return $aliases;
}
/**
* Update the root packages reference information.
*
* @param RootPackageInterface $root
*/
protected function mergeReferences(RootPackageInterface $root)
{
// Merge source reference information for merged packages.
// @see RootPackageLoader::load
$references = [];
$unwrapped = self::unwrapIfNeeded($root, 'setReferences');
foreach (['require', 'require-dev'] as $linkType) {
$linkInfo = BasePackage::$supportedLinkTypes[$linkType];
$method = 'get'.ucfirst($linkInfo['method']);
$links = [];
foreach ($unwrapped->$method() as $link) {
$links[$link->getTarget()] = $link->getConstraint()->getPrettyString();
}
$references = $this->extractReferences($links, $references);
}
$unwrapped->setReferences($references);
}
/**
* Extract vcs revision from version constraint (dev-master#abc123.
*
* @param array $requires
* @param array $references
* @return array
* @see RootPackageLoader::extractReferences()
*/
protected function extractReferences(array $requires, array $references)
{
foreach ($requires as $reqName => $reqVersion) {
$reqVersion = preg_replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion);
$stabilityName = VersionParser::parseStability($reqVersion);
if ($stabilityName === 'dev' &&
preg_match('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match)
) {
$name = strtolower($reqName);
$references[$name] = $match[1];
}
}
return $references;
}
}
// vim:sw=4:ts=4:sts=4:et:
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\IO\IOInterface;
/**
* Simple logging wrapper for Composer\IO\IOInterface
*
* @author Bryan Davis <bd808@bd808.com>
*/
class Logger
{
/**
* @var string $name
*/
protected $name;
/**
* @var IOInterface $inputOutput
*/
protected $inputOutput;
/**
* @param string $name
* @param IOInterface $io
*/
public function __construct($name, IOInterface $io)
{
$this->name = $name;
$this->inputOutput = $io;
}
/**
* Log a debug message
*
* Messages will be output at the "very verbose" logging level (eg `-vv`
* needed on the Composer command).
*
* @param string $message
*/
public function debug($message)
{
if ($this->inputOutput->isVeryVerbose()) {
$message = " <info>[{$this->name}]</info> {$message}";
$this->log($message);
}
}
/**
* Log an informative message
*
* Messages will be output at the "verbose" logging level (eg `-v` needed
* on the Composer command).
*
* @param string $message
*/
public function info($message)
{
if ($this->inputOutput->isVerbose()) {
$message = " <info>[{$this->name}]</info> {$message}";
$this->log($message);
}
}
/**
* Log a warning message
*
* @param string $message
*/
public function warning($message)
{
$message = " <error>[{$this->name}]</error> {$message}";
$this->log($message);
}
/**
* Write a message
*
* @param string $message
*/
public function log($message)
{
if (method_exists($this->inputOutput, 'writeError')) {
$this->inputOutput->writeError($message);
} else {
// @codeCoverageIgnoreStart
// Backwards compatibility for Composer before cb336a5
$this->inputOutput->write($message);
// @codeCoverageIgnoreEnd
}
}
}
// vim:sw=4:ts=4:sts=4:et:
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\EventDispatcher\Event as BaseEvent;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Factory;
use Composer\Installer;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Package\RootPackageInterface;
use Composer\Plugin\PluginEvents;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event as ScriptEvent;
use Composer\Script\ScriptEvents;
/**
* Composer plugin that allows merging multiple composer.json files.
*
* When installed, this plugin will look for a "merge-plugin" key in the
* composer configuration's "extra" section. The value for this key is
* a set of options configuring the plugin.
*
* An "include" setting is required. The value of this setting can be either
* a single value or an array of values. Each value is treated as a glob()
* pattern identifying additional composer.json style configuration files to
* merge into the configuration for the current compser execution.
*
* The "autoload", "autoload-dev", "conflict", "provide", "replace",
* "repositories", "require", "require-dev", and "suggest" sections of the
* found configuration files will be merged into the root package
* configuration as though they were directly included in the top-level
* composer.json file.
*
* If included files specify conflicting package versions for "require" or
* "require-dev", the normal Composer dependency solver process will be used
* to attempt to resolve the conflict. Specifying the 'replace' key as true will
* change this default behaviour so that the last-defined version of a package
* will win, allowing for force-overrides of package defines.
*
* By default the "extra" section is not merged. This can be enabled by
* setitng the 'merge-extra' key to true. In normal mode, when the same key is
* found in both the original and the imported extra section, the version in
* the original config is used and the imported version is skipped. If
* 'replace' mode is active, this behaviour changes so the imported version of
* the key is used, replacing the version in the original config.
*
*
* @code
* {
* "require": {
* "wikimedia/composer-merge-plugin": "dev-master"
* },
* "extra": {
* "merge-plugin": {
* "include": [
* "composer.local.json"
* ]
* }
* }
* }
* @endcode
*
* @author Bryan Davis <bd808@bd808.com>
*/
class MergePlugin implements PluginInterface, EventSubscriberInterface
{
/**
* Official package name
*/
public const PACKAGE_NAME = 'wikimedia/composer-merge-plugin';
/**
* Priority that plugin uses to register callbacks.
*/
private const CALLBACK_PRIORITY = 50000;
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var PluginState $state
*/
protected $state;
/**
* @var Logger $logger
*/
protected $logger;
/**
* Files that have already been fully processed
*
* @var array<string, bool> $loaded
*/
protected $loaded = [];
/**
* Files that have already been partially processed
*
* @var array<string, bool> $loadedNoDev
*/
protected $loadedNoDev = [];
/**
* Nested packages to restrict update operations.
*
* @var array<string, bool> $updateAllowList
*/
protected $updateAllowList = [];
/**
* {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io)
{
$this->composer = $composer;
$this->state = new PluginState($this->composer);
$this->logger = new Logger('merge-plugin', $io);
}
/**
* {@inheritdoc}
*/
public function deactivate(Composer $composer, IOInterface $io)
{
}
/**
* {@inheritdoc}
*/
public function uninstall(Composer $composer, IOInterface $io)
{
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
PluginEvents::INIT =>
['onInit', self::CALLBACK_PRIORITY],
PackageEvents::POST_PACKAGE_INSTALL =>
['onPostPackageInstall', self::CALLBACK_PRIORITY],
ScriptEvents::POST_INSTALL_CMD =>
['onPostInstallOrUpdate', self::CALLBACK_PRIORITY],
ScriptEvents::POST_UPDATE_CMD =>
['onPostInstallOrUpdate', self::CALLBACK_PRIORITY],
ScriptEvents::PRE_AUTOLOAD_DUMP =>
['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
ScriptEvents::PRE_INSTALL_CMD =>
['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
ScriptEvents::PRE_UPDATE_CMD =>
['onInstallUpdateOrDump', self::CALLBACK_PRIORITY],
];
}
/**
* Get list of packages to restrict update operations.
*
* @return string[]
* @see \Composer\Installer::setUpdateAllowList()
*/
public function getUpdateAllowList()
{
return array_keys($this->updateAllowList);
}
/**
* Handle an event callback for initialization.
*
* @param BaseEvent $event
*/
public function onInit(BaseEvent $event)
{
$this->state->loadSettings();
// It is not possible to know if the user specified --dev or --no-dev
// so assume it is false. The dev section will be merged later when
// the other events fire.
$this->state->setDevMode(false);
$this->mergeFiles($this->state->getIncludes(), false);
$this->mergeFiles($this->state->getRequires(), true);
}
/**
* Handle an event callback for an install, update or dump command by
* checking for "merge-plugin" in the "extra" data and merging package
* contents if found.
*
* @param ScriptEvent $event
*/
public function onInstallUpdateOrDump(ScriptEvent $event)
{
$this->state->loadSettings();
$this->state->setDevMode($event->isDevMode());
$this->mergeFiles($this->state->getIncludes(), false);
$this->mergeFiles($this->state->getRequires(), true);
if ($event->getName() === ScriptEvents::PRE_AUTOLOAD_DUMP) {
$this->state->setDumpAutoloader(true);
$flags = $event->getFlags();
if (isset($flags['optimize'])) {
$this->state->setOptimizeAutoloader($flags['optimize']);
}
}
}
/**
* Find configuration files matching the configured glob patterns and
* merge their contents with the master package.
*
* @param array $patterns List of files/glob patterns
* @param bool $required Are the patterns required to match files?
* @throws MissingFileException when required and a pattern returns no
* results
*/
protected function mergeFiles(array $patterns, $required = false)
{
$root = $this->composer->getPackage();
$files = array_map(
static function ($files, $pattern) use ($required) {
if ($required && !$files) {
throw new MissingFileException(
"merge-plugin: No files matched required '{$pattern}'"
);
}
return $files;
},
array_map('glob', $patterns),
$patterns
);
foreach (array_reduce($files, 'array_merge', []) as $path) {
$this->mergeFile($root, $path);
}
}
/**
* Read a JSON file and merge its contents
*
* @param RootPackageInterface $root
* @param string $path
*/
protected function mergeFile(RootPackageInterface $root, $path)
{
if (isset($this->loaded[$path]) ||
(isset($this->loadedNoDev[$path]) && !$this->state->isDevMode())
) {
$this->logger->debug(
"Already merged <comment>$path</comment> completely"
);
return;
}
$package = new ExtraPackage($path, $this->composer, $this->logger);
if (isset($this->loadedNoDev[$path])) {
$this->logger->info(
"Loading -dev sections of <comment>{$path}</comment>..."
);
$package->mergeDevInto($root, $this->state);
} else {
$this->logger->info("Loading <comment>{$path}</comment>...");
$package->mergeInto($root, $this->state);
}
$requirements = $package->getMergedRequirements();
if (!empty($requirements)) {
$this->updateAllowList = array_replace(
$this->updateAllowList,
array_fill_keys($requirements, true)
);
}
if ($this->state->isDevMode()) {
$this->loaded[$path] = true;
} else {
$this->loadedNoDev[$path] = true;
}
if ($this->state->recurseIncludes()) {
$this->mergeFiles($package->getIncludes(), false);
$this->mergeFiles($package->getRequires(), true);
}
}
/**
* Handle an event callback following installation of a new package by
* checking to see if the package that was installed was our plugin.
*
* @param PackageEvent $event
*/
public function onPostPackageInstall(PackageEvent $event)
{
$op = $event->getOperation();
if ($op instanceof InstallOperation) {
$package = $op->getPackage()->getName();
if ($package === self::PACKAGE_NAME) {
$this->logger->info('composer-merge-plugin installed');
$this->state->setFirstInstall(true);
$this->state->setLocked(
$event->getComposer()->getLocker()->isLocked()
);
}
}
}
/**
* Handle an event callback following an install or update command. If our
* plugin was installed during the run then trigger an update command to
* process any merge-patterns in the current config.
*
* @param ScriptEvent $event
*/
public function onPostInstallOrUpdate(ScriptEvent $event)
{
// @codeCoverageIgnoreStart
if ($this->state->isFirstInstall()) {
$this->state->setFirstInstall(false);
$requirements = $this->getUpdateAllowList();
if (empty($requirements)) {
return;
}
$this->logger->log("\n".'<info>Running composer update to apply merge settings</info>');
$lockBackup = null;
$lock = null;
if (!$this->state->isComposer1()) {
$file = Factory::getComposerFile();
$lock = Factory::getLockFile($file);
if (file_exists($lock)) {
$lockBackup = file_get_contents($lock);
}
}
$config = $this->composer->getConfig();
$preferSource = $config->get('preferred-install') === 'source';
$preferDist = $config->get('preferred-install') === 'dist';
$installer = Installer::create(
$event->getIO(),
// Create a new Composer instance to ensure full processing of
// the merged files.
Factory::create($event->getIO(), null, false)
);
$installer->setPreferSource($preferSource);
$installer->setPreferDist($preferDist);
$installer->setDevMode($event->isDevMode());
$installer->setDumpAutoloader($this->state->shouldDumpAutoloader());
$installer->setOptimizeAutoloader(
$this->state->shouldOptimizeAutoloader()
);
$installer->setUpdate(true);
if ($this->state->isComposer1()) {
// setUpdateWhitelist() only exists in composer 1.x. Configure as to run phan against composer 2.x
// @phan-suppress-next-line PhanUndeclaredMethod
$installer->setUpdateWhitelist($requirements);
} else {
$installer->setUpdateAllowList($requirements);
}
$status = $installer->run();
if (( $status !== 0 ) && $lockBackup && $lock && !$this->state->isComposer1()) {
$this->logger->log(
"\n".'<error>'.
'Update to apply merge settings failed, reverting '.$lock.' to its original content.'.
'</error>'
);
file_put_contents($lock, $lockBackup);
}
}
// @codeCoverageIgnoreEnd
}
}
// vim:sw=4:ts=4:sts=4:et:
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use RuntimeException;
/**
* @author Bryan Davis <bd808@bd808.com>
*/
class MissingFileException extends RuntimeException
{
}
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2021 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\EmptyConstraint;
use Composer\Semver\Constraint\MultiConstraint as SemverMultiConstraint;
use function count;
/**
* Adapted from Composer's v2 MultiConstraint::create for Composer v1
* @link https://github.com/composer/semver/blob/3.2.4/src/Constraint/MultiConstraint.php
* @author Chauncey McAskill <chauncey@mcaskill.ca>
*/
class MultiConstraint extends SemverMultiConstraint
{
/**
* Tries to optimize the constraints as much as possible, meaning
* reducing/collapsing congruent constraints etc.
* Does not necessarily return a MultiConstraint instance if
* things can be reduced to a simple constraint
*
* @param ConstraintInterface[] $constraints A set of constraints
* @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
*
* @return ConstraintInterface
*/
public static function create(array $constraints, $conjunctive = true)
{
if (count($constraints) === 0) {
// EmptyConstraint only exists in composer 1.x. Configure as to run phan against composer 2.x
// @phan-suppress-next-line PhanTypeMismatchReturn, PhanUndeclaredClassMethod
return new EmptyConstraint();
}
if (count($constraints) === 1) {
return $constraints[0];
}
$optimized = self::optimizeConstraints($constraints, $conjunctive);
if ($optimized !== null) {
list($constraints, $conjunctive) = $optimized;
if (count($constraints) === 1) {
return $constraints[0];
}
}
return new self($constraints, $conjunctive);
}
/**
* @return array|null
*/
private static function optimizeConstraints(array $constraints, $conjunctive)
{
// parse the two OR groups and if they are contiguous we collapse
// them into one constraint
// [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4]
if (!$conjunctive) {
$left = $constraints[0];
$mergedConstraints = [];
$optimized = false;
for ($i = 1, $l = count($constraints); $i < $l; $i++) {
$right = $constraints[$i];
if ($left instanceof SemverMultiConstraint
&& $left->conjunctive
&& $right instanceof SemverMultiConstraint
&& $right->conjunctive
&& count($left->constraints) === 2
&& count($right->constraints) === 2
&& ($left0 = (string) $left->constraints[0])
&& $left0[0] === '>' && $left0[1] === '='
&& ($left1 = (string) $left->constraints[1])
&& $left1[0] === '<'
&& ($right0 = (string) $right->constraints[0])
&& $right0[0] === '>' && $right0[1] === '='
&& ($right1 = (string) $right->constraints[1])
&& $right1[0] === '<'
&& substr($left1, 2) === substr($right0, 3)
) {
$optimized = true;
$left = new MultiConstraint(
[
$left->constraints[0],
$right->constraints[1],
],
true
);
} else {
$mergedConstraints[] = $left;
$left = $right;
}
}
if ($optimized) {
$mergedConstraints[] = $left;
return [$mergedConstraints, false];
}
}
// TODO: Here's the place to put more optimizations
return null;
}
}
// vim:sw=4:ts=4:sts=4:et:
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
/**
* Adapted from
* http://cgit.drupalcode.org/drupal/tree/core/lib/Drupal/Component/Utility/NestedArray.php
* @ f86a4d650d5af0b82a3981e09977055fa63f6f2e
*/
class NestedArray
{
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is similar to PHP's array_merge_recursive() function, but
* it handles non-array values differently. When merging values that are
* not both arrays, the latter value replaces the former rather than
* merging with it.
*
* Example:
*
* @code
* $link_options_1 = ['fragment' => 'x', 'attributes' => ['title' => t('X'), 'class' => ['a', 'b']]];
* $link_options_2 = ['fragment' => 'y', 'attributes' => ['title' => t('Y'), 'class' => ['c', 'd']]];
*
* // This results in ['fragment' => ['x', 'y'], 'attributes' =>
* // ['title' => [t('X'), t('Y')], 'class' => ['a', 'b',
* // 'c', 'd']]].
* $incorrect = array_merge_recursive($link_options_1, $link_options_2);
*
* // This results in ['fragment' => 'y', 'attributes' =>
* // ['title' => t('Y'), 'class' => ['a', 'b', 'c', 'd']]].
* $correct = NestedArray::mergeDeep($link_options_1, $link_options_2);
* @endcode
*
* @param mixed ...$params Arrays to merge.
*
* @return array The merged array.
*
* @see NestedArray::mergeDeepArray()
*/
public static function mergeDeep(...$params)
{
return self::mergeDeepArray($params);
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is equivalent to NestedArray::mergeDeep(), except the
* input arrays are passed as a single array parameter rather than
* a variable parameter list.
*
* The following are equivalent:
* - NestedArray::mergeDeep($a, $b);
* - NestedArray::mergeDeepArray([$a, $b]);
*
* The following are also equivalent:
* - call_user_func_array('NestedArray::mergeDeep', $arrays_to_merge);
* - NestedArray::mergeDeepArray($arrays_to_merge);
*
* @param array $arrays
* An arrays of arrays to merge.
* @param bool $preserveIntegerKeys
* (optional) If given, integer keys will be preserved and merged
* instead of appended. Defaults to false.
*
* @return array
* The merged array.
*
* @see NestedArray::mergeDeep()
*/
public static function mergeDeepArray(
array $arrays,
$preserveIntegerKeys = false
) {
$result = [];
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// Renumber integer keys as array_merge_recursive() does
// unless $preserveIntegerKeys is set to TRUE. Note that PHP
// automatically converts array keys that are integer strings
// (e.g., '1') to integers.
if (is_int($key) && !$preserveIntegerKeys) {
$result[] = $value;
} elseif (isset($result[$key]) &&
is_array($result[$key]) &&
is_array($value)
) {
// Recurse when both values are arrays.
$result[$key] = self::mergeDeepArray(
[$result[$key], $value],
$preserveIntegerKeys
);
} else {
// Otherwise, use the latter value, overriding any
// previous value.
$result[$key] = $value;
}
}
}
return $result;
}
}
// vim:sw=4:ts=4:sts=4:et:
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Composer;
use Composer\Plugin\PluginInterface;
/**
* Mutable plugin state
*
* @author Bryan Davis <bd808@bd808.com>
*/
class PluginState
{
/**
* @var Composer $composer
*/
protected $composer;
/**
* @var bool $isComposer1
*/
protected $isComposer1;
/**
* @var array $includes
*/
protected $includes = [];
/**
* @var array $requires
*/
protected $requires = [];
/**
* @var bool $devMode
*/
protected $devMode = false;
/**
* @var bool $recurse
*/
protected $recurse = true;
/**
* @var bool $replace
*/
protected $replace = false;
/**
* @var bool $ignore
*/
protected $ignore = false;
/**
* Whether to merge the -dev sections.
* @var bool $mergeDev
*/
protected $mergeDev = true;
/**
* Whether to merge the extra section.
*
* By default, the extra section is not merged and there will be many
* cases where the merge of the extra section is performed too late
* to be of use to other plugins. When enabled, merging uses one of
* two strategies - either 'first wins' or 'last wins'. When enabled,
* 'first wins' is the default behaviour. If Replace mode is activated
* then 'last wins' is used.
*
* @var bool $mergeExtra
*/
protected $mergeExtra = false;
/**
* Whether to merge the extra section in a deep / recursive way.
*
* By default the extra section is merged with array_merge() and duplicate
* keys are ignored. When enabled this allows to merge the arrays recursively
* using the following rule: Integer keys are merged, while array values are
* replaced where the later values overwrite the former.
*
* This is useful especially for the extra section when plugins use larger
* structures like a 'patches' key with the packages as sub-keys and the
* patches as values.
*
* When 'replace' mode is activated the order of array merges is exchanged.
*
* @var bool $mergeExtraDeep
*/
protected $mergeExtraDeep = false;
/**
* Whether to merge the replace section.
*
* @var bool $mergeReplace
*/
protected $mergeReplace = true;
/**
* Whether to merge the scripts section.
*
* @var bool $mergeScripts
*/
protected $mergeScripts = false;
/**
* @var bool $firstInstall
*/
protected $firstInstall = false;
/**
* @var bool $locked
*/
protected $locked = false;
/**
* @var bool $dumpAutoloader
*/
protected $dumpAutoloader = false;
/**
* @var bool $optimizeAutoloader
*/
protected $optimizeAutoloader = false;
/**
* @param Composer $composer
*/
public function __construct(Composer $composer)
{
$this->composer = $composer;
$this->isComposer1 = version_compare(PluginInterface::PLUGIN_API_VERSION, '2.0.0', '<');
}
/**
* Test if this plugin runs within Composer 1.
*
* @return bool
*/
public function isComposer1()
{
return $this->isComposer1;
}
/**
* Load plugin settings
*/
public function loadSettings()
{
$extra = $this->composer->getPackage()->getExtra();
$config = array_merge(
[
'include' => [],
'require' => [],
'recurse' => true,
'replace' => false,
'ignore-duplicates' => false,
'merge-dev' => true,
'merge-extra' => false,
'merge-extra-deep' => false,
'merge-replace' => true,
'merge-scripts' => false,
],
$extra['merge-plugin'] ?? []
);
$this->includes = (is_array($config['include'])) ?
$config['include'] : [$config['include']];
$this->requires = (is_array($config['require'])) ?
$config['require'] : [$config['require']];
$this->recurse = (bool)$config['recurse'];
$this->replace = (bool)$config['replace'];
$this->ignore = (bool)$config['ignore-duplicates'];
$this->mergeDev = (bool)$config['merge-dev'];
$this->mergeExtra = (bool)$config['merge-extra'];
$this->mergeExtraDeep = (bool)$config['merge-extra-deep'];
$this->mergeReplace = (bool)$config['merge-replace'];
$this->mergeScripts = (bool)$config['merge-scripts'];
}
/**
* Get list of filenames and/or glob patterns to include
*
* @return array
*/
public function getIncludes()
{
return $this->includes;
}
/**
* Get list of filenames and/or glob patterns to require
*
* @return array
*/
public function getRequires()
{
return $this->requires;
}
/**
* Set the first install flag
*
* @param bool $flag
*/
public function setFirstInstall($flag)
{
$this->firstInstall = (bool)$flag;
}
/**
* Is this the first time that the plugin has been installed?
*
* @return bool
*/
public function isFirstInstall()
{
return $this->firstInstall;
}
/**
* Set the locked flag
*
* @param bool $flag
*/
public function setLocked($flag)
{
$this->locked = (bool)$flag;
}
/**
* Was a lockfile present when the plugin was installed?
*
* @return bool
*/
public function isLocked()
{
return $this->locked;
}
/**
* Should an update be forced?
*
* @return true If packages are not locked
*/
public function forceUpdate()
{
return !$this->locked;
}
/**
* Set the devMode flag
*
* @param bool $flag
*/
public function setDevMode($flag)
{
$this->devMode = (bool)$flag;
}
/**
* Should devMode settings be processed?
*
* @return bool
*/
public function isDevMode()
{
return $this->shouldMergeDev() && $this->devMode;
}
/**
* Should devMode settings be merged?
*
* @return bool
*/
public function shouldMergeDev()
{
return $this->mergeDev;
}
/**
* Set the dumpAutoloader flag
*
* @param bool $flag
*/
public function setDumpAutoloader($flag)
{
$this->dumpAutoloader = (bool)$flag;
}
/**
* Is the autoloader file supposed to be written out?
*
* @return bool
*/
public function shouldDumpAutoloader()
{
return $this->dumpAutoloader;
}
/**
* Set the optimizeAutoloader flag
*
* @param bool $flag
*/
public function setOptimizeAutoloader($flag)
{
$this->optimizeAutoloader = (bool)$flag;
}
/**
* Should the autoloader be optimized?
*
* @return bool
*/
public function shouldOptimizeAutoloader()
{
return $this->optimizeAutoloader;
}
/**
* Should includes be recursively processed?
*
* @return bool
*/
public function recurseIncludes()
{
return $this->recurse;
}
/**
* Should duplicate links be replaced in a 'last definition wins' order?
*
* @return bool
*/
public function replaceDuplicateLinks()
{
return $this->replace;
}
/**
* Should duplicate links be ignored?
*
* @return bool
*/
public function ignoreDuplicateLinks()
{
return $this->ignore;
}
/**
* Should the extra section be merged?
*
* By default, the extra section is not merged and there will be many
* cases where the merge of the extra section is performed too late
* to be of use to other plugins. When enabled, merging uses one of
* two strategies - either 'first wins' or 'last wins'. When enabled,
* 'first wins' is the default behaviour. If Replace mode is activated
* then 'last wins' is used.
*
* @return bool
*/
public function shouldMergeExtra()
{
return $this->mergeExtra;
}
/**
* Should the extra section be merged deep / recursively?
*
* By default the extra section is merged with array_merge() and duplicate
* keys are ignored. When enabled this allows to merge the arrays recursively
* using the following rule: Integer keys are merged, while array values are
* replaced where the later values overwrite the former.
*
* This is useful especially for the extra section when plugins use larger
* structures like a 'patches' key with the packages as sub-keys and the
* patches as values.
*
* When 'replace' mode is activated the order of array merges is exchanged.
*
* @return bool
*/
public function shouldMergeExtraDeep()
{
return $this->mergeExtraDeep;
}
/**
* Should the replace section be merged?
*
* By default, the replace section is merged.
*
* @return bool
*/
public function shouldMergeReplace()
{
return $this->mergeReplace;
}
/**
* Should the scripts section be merged?
*
* By default, the scripts section is not merged.
*
* @return bool
*/
public function shouldMergeScripts()
{
return $this->mergeScripts;
}
}
// vim:sw=4:ts=4:sts=4:et:
<?php
/**
* This file is part of the Composer Merge plugin.
*
* Copyright (C) 2015 Bryan Davis, Wikimedia Foundation, and contributors
*
* This software may be modified and distributed under the terms of the MIT
* license. See the LICENSE file for details.
*/
namespace Wikimedia\Composer\Merge\V2;
use Composer\Package\BasePackage;
use Composer\Package\Version\VersionParser;
/**
* Adapted from Composer's RootPackageLoader::extractStabilityFlags
* @author Bryan Davis <bd808@bd808.com>
*/
class StabilityFlags
{
/**
* @var array Current package name => stability mappings
*/
protected $stabilityFlags;
/**
* @var int Current default minimum stability
*/
protected $minimumStability;
/**
* @var string Regex to extract an explicit stability flag (eg '@dev')
*/
protected $explicitStabilityRe;
/**
* @param array $stabilityFlags Current package name => stability mappings
* @param int|string $minimumStability Current default minimum stability
*/
public function __construct(
array $stabilityFlags = [],
$minimumStability = BasePackage::STABILITY_STABLE
) {
$this->stabilityFlags = $stabilityFlags;
$this->minimumStability = $this->getStabilityInt((string)$minimumStability);
$this->explicitStabilityRe = '/^[^@]*?@(' .
implode('|', array_keys(BasePackage::$stabilities)) .
')$/i';
}
/**
* Get the stability value for a given string.
*
* @param string $name Stability name
* @return int Stability value
*/
protected function getStabilityInt($name)
{
$name = VersionParser::normalizeStability($name);
return BasePackage::$stabilities[$name] ?? BasePackage::STABILITY_STABLE;
}
/**
* Extract and merge stability flags from the given collection of
* requires with another collection of stability flags.
*
* @param array $requires New package name => link mappings
* @return array Unified package name => stability mappings
*/
public function extractAll(array $requires)
{
$flags = [];
foreach ($requires as $name => $link) {
$name = strtolower($name);
$version = $link->getPrettyConstraint();
$stability = $this->getExplicitStability($version);
if ($stability === null) {
$stability = $this->getParsedStability($version);
}
$flags[$name] = max($stability, $this->getCurrentStability($name));
}
// Filter out null stability values
return array_filter($flags, function ($v) {
return $v !== null;
});
}
/**
* Extract the most unstable explicit stability (eg '@dev') from a version
* specification.
*
* @param string $version
* @return int|null Stability or null if no explict stability found
*/
protected function getExplicitStability($version)
{
$found = null;
$constraints = $this->splitConstraints($version);
foreach ($constraints as $constraint) {
if (preg_match($this->explicitStabilityRe, $constraint, $match)) {
$stability = $this->getStabilityInt($match[1]);
$found = max($stability, $found);
}
}
return $found;
}
/**
* Split a version specification into a list of version constraints.
*
* @param string $version
* @return array
*/
protected function splitConstraints($version)
{
$found = [];
$orConstraints = preg_split('/\s*\|\|?\s*/', trim($version));
foreach ($orConstraints as $constraints) {
$andConstraints = preg_split(
'/(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)/',
$constraints
);
foreach ($andConstraints as $constraint) {
$found[] = $constraint;
}
}
return $found;
}
/**
* Get the stability of a version
*
* @param string $version
* @return int|null Stability or null if STABLE or less than minimum
*/
protected function getParsedStability($version)
{
// Drop aliasing if used
$version = preg_replace('/^([^,\s@]+) as .+$/', '$1', $version);
$stability = $this->getStabilityInt(
VersionParser::parseStability($version)
);
if ($stability === BasePackage::STABILITY_STABLE ||
$this->minimumStability > $stability
) {
// Ignore if 'stable' or more stable than the global
// minimum
$stability = null;
}
return $stability;
}
/**
* Get the current stability of a given package.
*
* @param string $name
* @return int|null Stability of null if not set
*/
protected function getCurrentStability($name)
{
return $this->stabilityFlags[$name] ?? null;
}
}
// vim:sw=4:ts=4:sts=4:et:
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment