diff --git a/composer.lock b/composer.lock
index 05ca71eec5..3bd285bef6 100644
--- a/composer.lock
+++ b/composer.lock
@@ -346,33 +346,38 @@
},
{
"name": "doctrine/cache",
- "version": "v1.6.2",
+ "version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
- "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b"
+ "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b",
- "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b",
+ "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57",
+ "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57",
"shasum": ""
},
"require": {
- "php": "~5.5|~7.0"
+ "php": "~7.1"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
- "phpunit/phpunit": "~4.8|~5.0",
- "predis/predis": "~1.0",
- "satooshi/php-coveralls": "~0.6"
+ "alcaeus/mongo-php-adapter": "^1.1",
+ "doctrine/coding-standard": "^4.0",
+ "mongodb/mongodb": "^1.1",
+ "phpunit/phpunit": "^7.0",
+ "predis/predis": "~1.0"
+ },
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6.x-dev"
+ "dev-master": "1.8.x-dev"
}
},
"autoload": {
@@ -407,42 +412,45 @@
}
],
"description": "Caching library offering an object-oriented API for many cache backends",
- "homepage": "http://www.doctrine-project.org",
+ "homepage": "https://www.doctrine-project.org",
"keywords": [
"cache",
"caching"
],
- "time": "2017-07-22T12:49:21+00:00"
+ "time": "2018-08-21T18:01:43+00:00"
},
{
"name": "doctrine/collections",
- "version": "v1.3.0",
+ "version": "v1.6.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
- "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a"
+ "reference": "d2ae4ef05e25197343b6a39bae1d3c427a2f6956"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a",
- "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/d2ae4ef05e25197343b6a39bae1d3c427a2f6956",
+ "reference": "d2ae4ef05e25197343b6a39bae1d3c427a2f6956",
"shasum": ""
},
"require": {
- "php": ">=5.3.2"
+ "php": "^7.1.3"
},
"require-dev": {
- "phpunit/phpunit": "~4.0"
+ "doctrine/coding-standard": "^6.0",
+ "phpstan/phpstan-shim": "^0.9.2",
+ "phpunit/phpunit": "^7.0",
+ "vimeo/psalm": "^3.2.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2.x-dev"
+ "dev-master": "1.6.x-dev"
}
},
"autoload": {
- "psr-0": {
- "Doctrine\\Common\\Collections\\": "lib/"
+ "psr-4": {
+ "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -471,44 +479,51 @@
"email": "schmittjoh@gmail.com"
}
],
- "description": "Collections Abstraction library",
- "homepage": "http://www.doctrine-project.org",
+ "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.",
+ "homepage": "https://www.doctrine-project.org/projects/collections.html",
"keywords": [
"array",
"collections",
- "iterator"
+ "iterators",
+ "php"
],
- "time": "2015-04-14T22:21:58+00:00"
+ "time": "2019-03-25T19:03:48+00:00"
},
{
"name": "doctrine/common",
- "version": "v2.6.2",
+ "version": "v2.10.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/common.git",
- "reference": "7bce00698899aa2c06fe7365c76e4d78ddb15fa3"
+ "reference": "30e33f60f64deec87df728c02b107f82cdafad9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/common/zipball/7bce00698899aa2c06fe7365c76e4d78ddb15fa3",
- "reference": "7bce00698899aa2c06fe7365c76e4d78ddb15fa3",
+ "url": "https://api.github.com/repos/doctrine/common/zipball/30e33f60f64deec87df728c02b107f82cdafad9d",
+ "reference": "30e33f60f64deec87df728c02b107f82cdafad9d",
"shasum": ""
},
"require": {
- "doctrine/annotations": "1.*",
- "doctrine/cache": "1.*",
- "doctrine/collections": "1.*",
- "doctrine/inflector": "1.*",
- "doctrine/lexer": "1.*",
- "php": "~5.5|~7.0"
+ "doctrine/annotations": "^1.0",
+ "doctrine/cache": "^1.0",
+ "doctrine/collections": "^1.0",
+ "doctrine/event-manager": "^1.0",
+ "doctrine/inflector": "^1.0",
+ "doctrine/lexer": "^1.0",
+ "doctrine/persistence": "^1.1",
+ "doctrine/reflection": "^1.0",
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "~4.8|~5.0"
+ "doctrine/coding-standard": "^1.0",
+ "phpunit/phpunit": "^6.3",
+ "squizlabs/php_codesniffer": "^3.0",
+ "symfony/phpunit-bridge": "^4.0.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7.x-dev"
+ "dev-master": "2.10.x-dev"
}
},
"autoload": {
@@ -540,48 +555,124 @@
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
}
],
- "description": "Common Library for Doctrine projects",
- "homepage": "http://www.doctrine-project.org",
+ "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, persistence interfaces, proxies, event system and much more.",
+ "homepage": "https://www.doctrine-project.org/projects/common.html",
"keywords": [
- "annotations",
- "collections",
- "eventmanager",
- "persistence",
- "spl"
+ "common",
+ "doctrine",
+ "php"
+ ],
+ "time": "2018-11-21T01:24:55+00:00"
+ },
+ {
+ "name": "doctrine/event-manager",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/event-manager.git",
+ "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3",
+ "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "conflict": {
+ "doctrine/common": "<2.9@dev"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^4.0",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\": "lib/Doctrine/Common"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "Doctrine Event Manager component",
+ "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
+ "keywords": [
+ "event",
+ "eventdispatcher",
+ "eventmanager"
],
- "time": "2016-11-30T16:50:46+00:00"
+ "time": "2018-06-11T11:59:03+00:00"
},
{
"name": "doctrine/inflector",
- "version": "v1.1.0",
+ "version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
- "reference": "90b2128806bfde671b6952ab8bea493942c1fdae"
+ "reference": "5527a48b7313d15261292c149e55e26eae771b0a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae",
- "reference": "90b2128806bfde671b6952ab8bea493942c1fdae",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a",
+ "reference": "5527a48b7313d15261292c149e55e26eae771b0a",
"shasum": ""
},
"require": {
- "php": ">=5.3.2"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "4.*"
+ "phpunit/phpunit": "^6.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1.x-dev"
+ "dev-master": "1.3.x-dev"
}
},
"autoload": {
- "psr-0": {
- "Doctrine\\Common\\Inflector\\": "lib/"
+ "psr-4": {
+ "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -618,7 +709,7 @@
"singularize",
"string"
],
- "time": "2015-11-06T14:35:42+00:00"
+ "time": "2018-01-09T20:05:19+00:00"
},
{
"name": "doctrine/lexer",
@@ -674,6 +765,163 @@
],
"time": "2014-09-09T13:34:57+00:00"
},
+ {
+ "name": "doctrine/persistence",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/persistence.git",
+ "reference": "3da7c9d125591ca83944f477e65ed3d7b4617c48"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/persistence/zipball/3da7c9d125591ca83944f477e65ed3d7b4617c48",
+ "reference": "3da7c9d125591ca83944f477e65ed3d7b4617c48",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/annotations": "^1.0",
+ "doctrine/cache": "^1.0",
+ "doctrine/collections": "^1.0",
+ "doctrine/event-manager": "^1.0",
+ "doctrine/reflection": "^1.0",
+ "php": "^7.1"
+ },
+ "conflict": {
+ "doctrine/common": "<2.10@dev"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^5.0",
+ "phpstan/phpstan": "^0.8",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\": "lib/Doctrine/Common"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.",
+ "homepage": "https://doctrine-project.org/projects/persistence.html",
+ "keywords": [
+ "mapper",
+ "object",
+ "odm",
+ "orm",
+ "persistence"
+ ],
+ "time": "2019-04-23T08:28:24+00:00"
+ },
+ {
+ "name": "doctrine/reflection",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/reflection.git",
+ "reference": "02538d3f95e88eb397a5f86274deb2c6175c2ab6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/reflection/zipball/02538d3f95e88eb397a5f86274deb2c6175c2ab6",
+ "reference": "02538d3f95e88eb397a5f86274deb2c6175c2ab6",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/annotations": "^1.0",
+ "ext-tokenizer": "*",
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^4.0",
+ "doctrine/common": "^2.8",
+ "phpstan/phpstan": "^0.9.2",
+ "phpstan/phpstan-phpunit": "^0.9.4",
+ "phpunit/phpunit": "^7.0",
+ "squizlabs/php_codesniffer": "^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\": "lib/Doctrine/Common"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "Doctrine Reflection component",
+ "homepage": "https://www.doctrine-project.org/projects/reflection.html",
+ "keywords": [
+ "reflection"
+ ],
+ "time": "2018-06-14T14:45:07+00:00"
+ },
{
"name": "easyrdf/easyrdf",
"version": "0.9.1",
@@ -738,16 +986,16 @@
},
{
"name": "egulias/email-validator",
- "version": "2.1.7",
+ "version": "2.1.8",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
- "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
+ "reference": "c26463ff9241f27907112fbcd0c86fa670cfef98"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
- "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
+ "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/c26463ff9241f27907112fbcd0c86fa670cfef98",
+ "reference": "c26463ff9241f27907112fbcd0c86fa670cfef98",
"shasum": ""
},
"require": {
@@ -791,7 +1039,7 @@
"validation",
"validator"
],
- "time": "2018-12-04T22:38:24+00:00"
+ "time": "2019-05-16T22:02:54+00:00"
},
{
"name": "guzzlehttp/guzzle",
@@ -911,32 +1159,33 @@
},
{
"name": "guzzlehttp/psr7",
- "version": "1.4.2",
+ "version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
+ "reference": "9f83dded91781a01c63574e387eaa769be769115"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
- "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115",
+ "reference": "9f83dded91781a01c63574e387eaa769be769115",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
- "psr/http-message": "~1.0"
+ "psr/http-message": "~1.0",
+ "ralouphie/getallheaders": "^2.0.5"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.0"
+ "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4-dev"
+ "dev-master": "1.5-dev"
}
},
"autoload": {
@@ -966,41 +1215,44 @@
"keywords": [
"http",
"message",
+ "psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
- "time": "2017-03-20T17:10:46+00:00"
+ "time": "2018-12-04T20:46:45+00:00"
},
{
"name": "masterminds/html5",
- "version": "2.3.0",
+ "version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
- "reference": "2c37c6c520b995b761674de3be8455a381679067"
+ "reference": "c961ca6a0a81dc6b55b6859b3f9ea7f402edf9ad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/2c37c6c520b995b761674de3be8455a381679067",
- "reference": "2c37c6c520b995b761674de3be8455a381679067",
+ "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/c961ca6a0a81dc6b55b6859b3f9ea7f402edf9ad",
+ "reference": "c961ca6a0a81dc6b55b6859b3f9ea7f402edf9ad",
"shasum": ""
},
"require": {
+ "ext-ctype": "*",
+ "ext-dom": "*",
"ext-libxml": "*",
"php": ">=5.3.0"
},
"require-dev": {
- "phpunit/phpunit": "4.*",
+ "phpunit/phpunit": "^4.8.35",
"sami/sami": "~2.0",
"satooshi/php-coveralls": "1.0.*"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.2-dev"
+ "dev-master": "2.6-dev"
}
},
"autoload": {
@@ -1037,7 +1289,7 @@
"serializer",
"xml"
],
- "time": "2017-09-04T12:26:28+00:00"
+ "time": "2019-03-10T11:41:28+00:00"
},
{
"name": "paragonie/random_compat",
@@ -1090,16 +1342,16 @@
},
{
"name": "pear/archive_tar",
- "version": "1.4.6",
+ "version": "1.4.7",
"source": {
"type": "git",
"url": "https://github.com/pear/Archive_Tar.git",
- "reference": "b8e33f9063a7cd1d20f079014f8382b3a7aee47e"
+ "reference": "7e48add6f8edc3027dd98ad15964b1a28fd0c845"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/b8e33f9063a7cd1d20f079014f8382b3a7aee47e",
- "reference": "b8e33f9063a7cd1d20f079014f8382b3a7aee47e",
+ "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/7e48add6f8edc3027dd98ad15964b1a28fd0c845",
+ "reference": "7e48add6f8edc3027dd98ad15964b1a28fd0c845",
"shasum": ""
},
"require": {
@@ -1152,20 +1404,20 @@
"archive",
"tar"
],
- "time": "2019-02-01T11:10:38+00:00"
+ "time": "2019-04-08T13:15:55+00:00"
},
{
"name": "pear/console_getopt",
- "version": "v1.4.1",
+ "version": "v1.4.2",
"source": {
"type": "git",
"url": "https://github.com/pear/Console_Getopt.git",
- "reference": "82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f"
+ "reference": "6c77aeb625b32bd752e89ee17972d103588b90c0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pear/Console_Getopt/zipball/82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f",
- "reference": "82f05cd1aa3edf34e19aa7c8ca312ce13a6a577f",
+ "url": "https://api.github.com/repos/pear/Console_Getopt/zipball/6c77aeb625b32bd752e89ee17972d103588b90c0",
+ "reference": "6c77aeb625b32bd752e89ee17972d103588b90c0",
"shasum": ""
},
"type": "library",
@@ -1199,20 +1451,20 @@
}
],
"description": "More info available on: http://pear.php.net/package/Console_Getopt",
- "time": "2015-07-20T20:28:12+00:00"
+ "time": "2019-02-06T16:52:33+00:00"
},
{
"name": "pear/pear-core-minimal",
- "version": "v1.10.7",
+ "version": "v1.10.9",
"source": {
"type": "git",
"url": "https://github.com/pear/pear-core-minimal.git",
- "reference": "19a3e0fcd50492c4357372f623f55f1b144346da"
+ "reference": "742be8dd68c746a01e4b0a422258e9c9cae1c37f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/19a3e0fcd50492c4357372f623f55f1b144346da",
- "reference": "19a3e0fcd50492c4357372f623f55f1b144346da",
+ "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/742be8dd68c746a01e4b0a422258e9c9cae1c37f",
+ "reference": "742be8dd68c746a01e4b0a422258e9c9cae1c37f",
"shasum": ""
},
"require": {
@@ -1243,7 +1495,7 @@
}
],
"description": "Minimal set of PEAR core files to be used as composer dependency",
- "time": "2018-12-05T20:03:52+00:00"
+ "time": "2019-03-13T18:15:44+00:00"
},
{
"name": "pear/pear_exception",
@@ -1401,16 +1653,16 @@
},
{
"name": "psr/log",
- "version": "1.0.2",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
- "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"shasum": ""
},
"require": {
@@ -1444,7 +1696,47 @@
"psr",
"psr-3"
],
- "time": "2016-10-10T12:19:37+00:00"
+ "time": "2018-11-20T15:27:04+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "2.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+ "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~3.7.0",
+ "satooshi/php-coveralls": ">=1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "time": "2016-02-11T07:05:27+00:00"
},
{
"name": "stack/builder",
@@ -1556,7 +1848,7 @@
},
{
"name": "symfony/class-loader",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/class-loader.git",
@@ -1612,7 +1904,7 @@
},
{
"name": "symfony/console",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
@@ -1684,7 +1976,7 @@
},
{
"name": "symfony/debug",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
@@ -1740,16 +2032,16 @@
},
{
"name": "symfony/dependency-injection",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
- "reference": "dee85a9148399cdb2731603802842bcfd8afe5ab"
+ "reference": "be0feb3fa202aedfd8d1956f2dafd563fb13acbf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/dee85a9148399cdb2731603802842bcfd8afe5ab",
- "reference": "dee85a9148399cdb2731603802842bcfd8afe5ab",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/be0feb3fa202aedfd8d1956f2dafd563fb13acbf",
+ "reference": "be0feb3fa202aedfd8d1956f2dafd563fb13acbf",
"shasum": ""
},
"require": {
@@ -1807,11 +2099,11 @@
],
"description": "Symfony DependencyInjection Component",
"homepage": "https://symfony.com",
- "time": "2019-04-16T11:13:42+00:00"
+ "time": "2019-04-20T15:32:49+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
@@ -1928,16 +2220,16 @@
},
{
"name": "symfony/http-kernel",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "14fa41ccd38570b5e3120a3754bbaa144a15f311"
+ "reference": "586046f5adc6a08eaebbe4519ef18ad52f54e453"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/14fa41ccd38570b5e3120a3754bbaa144a15f311",
- "reference": "14fa41ccd38570b5e3120a3754bbaa144a15f311",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/586046f5adc6a08eaebbe4519ef18ad52f54e453",
+ "reference": "586046f5adc6a08eaebbe4519ef18ad52f54e453",
"shasum": ""
},
"require": {
@@ -2013,7 +2305,7 @@
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
- "time": "2019-04-17T15:57:07+00:00"
+ "time": "2019-05-01T13:03:24+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -2252,7 +2544,7 @@
},
{
"name": "symfony/process",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
@@ -2301,25 +2593,27 @@
},
{
"name": "symfony/psr-http-message-bridge",
- "version": "v1.1.2",
+ "version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/psr-http-message-bridge.git",
- "reference": "a33352af16f78a5ff4f9d90811536abf210df12b"
+ "reference": "9ab9d71f97d5c7d35a121a7fb69f74fee95cd0ad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/a33352af16f78a5ff4f9d90811536abf210df12b",
- "reference": "a33352af16f78a5ff4f9d90811536abf210df12b",
+ "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/9ab9d71f97d5c7d35a121a7fb69f74fee95cd0ad",
+ "reference": "9ab9d71f97d5c7d35a121a7fb69f74fee95cd0ad",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0",
+ "php": "^7.1",
"psr/http-message": "^1.0",
- "symfony/http-foundation": "^2.3.42 || ^3.4 || ^4.0"
+ "symfony/http-foundation": "^3.4 || ^4.0"
},
"require-dev": {
- "symfony/phpunit-bridge": "^3.4 || ^4.0"
+ "nyholm/psr7": "^1.1",
+ "symfony/phpunit-bridge": "^3.4.20 || ^4.0",
+ "zendframework/zend-diactoros": "^1.4.1 || ^2.0"
},
"suggest": {
"nyholm/psr7": "For a super lightweight PSR-7/17 implementation"
@@ -2327,7 +2621,7 @@
"type": "symfony-bridge",
"extra": {
"branch-alias": {
- "dev-master": "1.1-dev"
+ "dev-master": "1.2-dev"
}
},
"autoload": {
@@ -2360,11 +2654,11 @@
"psr-17",
"psr-7"
],
- "time": "2019-04-03T17:09:40+00:00"
+ "time": "2019-03-11T18:22:33+00:00"
},
{
"name": "symfony/routing",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
@@ -2440,16 +2734,16 @@
},
{
"name": "symfony/serializer",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
- "reference": "14b3221cc41dcfef404205f0060cda873f43a534"
+ "reference": "99aceeb3e10852b951b9cab57a2b83062db09efb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/serializer/zipball/14b3221cc41dcfef404205f0060cda873f43a534",
- "reference": "14b3221cc41dcfef404205f0060cda873f43a534",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/99aceeb3e10852b951b9cab57a2b83062db09efb",
+ "reference": "99aceeb3e10852b951b9cab57a2b83062db09efb",
"shasum": ""
},
"require": {
@@ -2472,7 +2766,7 @@
"symfony/dependency-injection": "~3.2|~4.0",
"symfony/http-foundation": "~2.8|~3.0|~4.0",
"symfony/property-access": "~2.8|~3.0|~4.0",
- "symfony/property-info": "~3.1|~4.0",
+ "symfony/property-info": "^3.4.13|~4.0",
"symfony/yaml": "~3.4|~4.0"
},
"suggest": {
@@ -2515,20 +2809,20 @@
],
"description": "Symfony Serializer Component",
"homepage": "https://symfony.com",
- "time": "2019-04-11T05:44:34+00:00"
+ "time": "2019-04-27T21:20:35+00:00"
},
{
"name": "symfony/translation",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "aae26f143da71adc8707eb489f1dc86aef7d376b"
+ "reference": "301a5d627220a1c4ee522813b0028653af6c4f54"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/aae26f143da71adc8707eb489f1dc86aef7d376b",
- "reference": "aae26f143da71adc8707eb489f1dc86aef7d376b",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/301a5d627220a1c4ee522813b0028653af6c4f54",
+ "reference": "301a5d627220a1c4ee522813b0028653af6c4f54",
"shasum": ""
},
"require": {
@@ -2585,20 +2879,20 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
- "time": "2019-04-10T16:00:48+00:00"
+ "time": "2019-05-01T11:10:09+00:00"
},
{
"name": "symfony/validator",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/validator.git",
- "reference": "83da5259779aaf9dde220130e62b785f74e2ac49"
+ "reference": "cc3f577d8887737df4d77a4c0cc6e3c22164cea4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/validator/zipball/83da5259779aaf9dde220130e62b785f74e2ac49",
- "reference": "83da5259779aaf9dde220130e62b785f74e2ac49",
+ "url": "https://api.github.com/repos/symfony/validator/zipball/cc3f577d8887737df4d77a4c0cc6e3c22164cea4",
+ "reference": "cc3f577d8887737df4d77a4c0cc6e3c22164cea4",
"shasum": ""
},
"require": {
@@ -2670,11 +2964,11 @@
],
"description": "Symfony Validator Component",
"homepage": "https://symfony.com",
- "time": "2019-04-16T11:21:44+00:00"
+ "time": "2019-04-29T08:34:27+00:00"
},
{
"name": "symfony/yaml",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
@@ -2733,16 +3027,16 @@
},
{
"name": "twig/twig",
- "version": "v1.38.4",
+ "version": "v1.41.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "7732e9e7017d751313811bd118de61302e9c8b35"
+ "reference": "575cd5028362da591facde1ef5d7b94553c375c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/7732e9e7017d751313811bd118de61302e9c8b35",
- "reference": "7732e9e7017d751313811bd118de61302e9c8b35",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/575cd5028362da591facde1ef5d7b94553c375c9",
+ "reference": "575cd5028362da591facde1ef5d7b94553c375c9",
"shasum": ""
},
"require": {
@@ -2757,7 +3051,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.38-dev"
+ "dev-master": "1.41-dev"
}
},
"autoload": {
@@ -2795,7 +3089,7 @@
"keywords": [
"templating"
],
- "time": "2019-03-23T14:27:19+00:00"
+ "time": "2019-05-14T11:59:08+00:00"
},
{
"name": "typo3/phar-stream-wrapper",
@@ -2894,21 +3188,21 @@
},
{
"name": "zendframework/zend-diactoros",
- "version": "1.4.1",
+ "version": "1.8.6",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-diactoros.git",
- "reference": "424a840dc3bedcdeea510b42e056c77c2d6c4bef"
+ "reference": "20da13beba0dde8fb648be3cc19765732790f46e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/424a840dc3bedcdeea510b42e056c77c2d6c4bef",
- "reference": "424a840dc3bedcdeea510b42e056c77c2d6c4bef",
+ "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e",
+ "reference": "20da13beba0dde8fb648be3cc19765732790f46e",
"shasum": ""
},
"require": {
- "php": "^5.4 || ^7.0",
- "psr/http-message": "~1.0"
+ "php": "^5.6 || ^7.0",
+ "psr/http-message": "^1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
@@ -2916,17 +3210,29 @@
"require-dev": {
"ext-dom": "*",
"ext-libxml": "*",
- "phpunit/phpunit": "^4.6 || ^5.5",
- "zendframework/zend-coding-standard": "~1.0.0"
+ "php-http/psr7-integration-tests": "dev-master",
+ "phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7",
+ "zendframework/zend-coding-standard": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4-dev",
- "dev-develop": "1.5-dev"
+ "dev-master": "1.8.x-dev",
+ "dev-develop": "1.9.x-dev",
+ "dev-release-2.0": "2.0.x-dev"
}
},
"autoload": {
+ "files": [
+ "src/functions/create_uploaded_file.php",
+ "src/functions/marshal_headers_from_sapi.php",
+ "src/functions/marshal_method_from_sapi.php",
+ "src/functions/marshal_protocol_version_from_sapi.php",
+ "src/functions/marshal_uri_from_sapi.php",
+ "src/functions/normalize_server.php",
+ "src/functions/normalize_uploaded_files.php",
+ "src/functions/parse_cookie_header.php"
+ ],
"psr-4": {
"Zend\\Diactoros\\": "src/"
}
@@ -2942,34 +3248,34 @@
"psr",
"psr-7"
],
- "time": "2017-08-17T21:21:00+00:00"
+ "time": "2018-09-05T19:29:37+00:00"
},
{
"name": "zendframework/zend-escaper",
- "version": "2.5.2",
+ "version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-escaper.git",
- "reference": "2dcd14b61a72d8b8e27d579c6344e12c26141d4e"
+ "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/2dcd14b61a72d8b8e27d579c6344e12c26141d4e",
- "reference": "2dcd14b61a72d8b8e27d579c6344e12c26141d4e",
+ "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/31d8aafae982f9568287cb4dce987e6aff8fd074",
+ "reference": "31d8aafae982f9568287cb4dce987e6aff8fd074",
"shasum": ""
},
"require": {
- "php": ">=5.5"
+ "php": "^5.6 || ^7.0"
},
"require-dev": {
- "fabpot/php-cs-fixer": "1.7.*",
- "phpunit/phpunit": "~4.0"
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2",
+ "zendframework/zend-coding-standard": "~1.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.5-dev",
- "dev-develop": "2.6-dev"
+ "dev-master": "2.6.x-dev",
+ "dev-develop": "2.7.x-dev"
}
},
"autoload": {
@@ -2981,55 +3287,58 @@
"license": [
"BSD-3-Clause"
],
- "homepage": "https://github.com/zendframework/zend-escaper",
+ "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs",
"keywords": [
+ "ZendFramework",
"escaper",
- "zf2"
+ "zf"
],
- "time": "2016-06-30T19:48:38+00:00"
+ "time": "2018-04-25T15:48:53+00:00"
},
{
"name": "zendframework/zend-feed",
- "version": "2.7.0",
+ "version": "2.12.0",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-feed.git",
- "reference": "12b328d382aa5200f1de53d4147033b885776b67"
+ "reference": "d926c5af34b93a0121d5e2641af34ddb1533d733"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-feed/zipball/12b328d382aa5200f1de53d4147033b885776b67",
- "reference": "12b328d382aa5200f1de53d4147033b885776b67",
+ "url": "https://api.github.com/repos/zendframework/zend-feed/zipball/d926c5af34b93a0121d5e2641af34ddb1533d733",
+ "reference": "d926c5af34b93a0121d5e2641af34ddb1533d733",
"shasum": ""
},
"require": {
- "php": "^5.5 || ^7.0",
- "zendframework/zend-escaper": "^2.5",
- "zendframework/zend-stdlib": "^2.7 || ^3.0"
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-escaper": "^2.5.2",
+ "zendframework/zend-stdlib": "^3.2.1"
},
"require-dev": {
- "fabpot/php-cs-fixer": "1.7.*",
- "phpunit/phpunit": "~4.0",
- "psr/http-message": "^1.0",
- "zendframework/zend-cache": "^2.5",
- "zendframework/zend-db": "^2.5",
- "zendframework/zend-http": "^2.5",
- "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3",
- "zendframework/zend-validator": "^2.5"
+ "phpunit/phpunit": "^5.7.23 || ^6.4.3",
+ "psr/http-message": "^1.0.1",
+ "zendframework/zend-cache": "^2.7.2",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-db": "^2.8.2",
+ "zendframework/zend-http": "^2.7",
+ "zendframework/zend-servicemanager": "^2.7.8 || ^3.3",
+ "zendframework/zend-validator": "^2.10.1"
},
"suggest": {
- "psr/http-message": "PSR-7 ^1.0, if you wish to use Zend\\Feed\\Reader\\Http\\Psr7ResponseDecorator",
+ "psr/http-message": "PSR-7 ^1.0.1, if you wish to use Zend\\Feed\\Reader\\Http\\Psr7ResponseDecorator",
"zendframework/zend-cache": "Zend\\Cache component, for optionally caching feeds between requests",
"zendframework/zend-db": "Zend\\Db component, for use with PubSubHubbub",
"zendframework/zend-http": "Zend\\Http for PubSubHubbub, and optionally for use with Zend\\Feed\\Reader",
"zendframework/zend-servicemanager": "Zend\\ServiceManager component, for easily extending ExtensionManager implementations",
- "zendframework/zend-validator": "Zend\\Validator component, for validating email addresses used in Atom feeds and entries ehen using the Writer subcomponent"
+ "zendframework/zend-validator": "Zend\\Validator component, for validating email addresses used in Atom feeds and entries when using the Writer subcomponent"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7-dev",
- "dev-develop": "2.8-dev"
+ "dev-master": "2.12.x-dev",
+ "dev-develop": "2.13.x-dev"
}
},
"autoload": {
@@ -3042,40 +3351,40 @@
"BSD-3-Clause"
],
"description": "provides functionality for consuming RSS and Atom feeds",
- "homepage": "https://github.com/zendframework/zend-feed",
"keywords": [
+ "ZendFramework",
"feed",
- "zf2"
+ "zf"
],
- "time": "2016-02-11T18:54:29+00:00"
+ "time": "2019-03-05T20:08:49+00:00"
},
{
"name": "zendframework/zend-stdlib",
- "version": "3.0.1",
+ "version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-stdlib.git",
- "reference": "8bafa58574204bdff03c275d1d618aaa601588ae"
+ "reference": "66536006722aff9e62d1b331025089b7ec71c065"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/8bafa58574204bdff03c275d1d618aaa601588ae",
- "reference": "8bafa58574204bdff03c275d1d618aaa601588ae",
+ "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/66536006722aff9e62d1b331025089b7ec71c065",
+ "reference": "66536006722aff9e62d1b331025089b7ec71c065",
"shasum": ""
},
"require": {
- "php": "^5.5 || ^7.0"
+ "php": "^5.6 || ^7.0"
},
"require-dev": {
- "athletic/athletic": "~0.1",
- "fabpot/php-cs-fixer": "1.7.*",
- "phpunit/phpunit": "~4.0"
+ "phpbench/phpbench": "^0.13",
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2",
+ "zendframework/zend-coding-standard": "~1.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev",
- "dev-develop": "3.1-dev"
+ "dev-master": "3.2.x-dev",
+ "dev-develop": "3.3.x-dev"
}
},
"autoload": {
@@ -3087,12 +3396,13 @@
"license": [
"BSD-3-Clause"
],
- "homepage": "https://github.com/zendframework/zend-stdlib",
+ "description": "SPL extensions, array utilities, error handlers, and more",
"keywords": [
+ "ZendFramework",
"stdlib",
- "zf2"
+ "zf"
],
- "time": "2016-04-12T21:19:36+00:00"
+ "time": "2018-08-28T21:34:05+00:00"
}
],
"packages-dev": [
@@ -3102,26 +3412,27 @@
"source": {
"type": "git",
"url": "https://github.com/minkphp/Mink.git",
- "reference": "9ea1cebe3dc529ba3861d87c818f045362c40484"
+ "reference": "6d637f7af4816c26ad8a943da2e3f7eef1231bea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/minkphp/Mink/zipball/9ea1cebe3dc529ba3861d87c818f045362c40484",
- "reference": "9ea1cebe3dc529ba3861d87c818f045362c40484",
+ "url": "https://api.github.com/repos/minkphp/Mink/zipball/6d637f7af4816c26ad8a943da2e3f7eef1231bea",
+ "reference": "6d637f7af4816c26ad8a943da2e3f7eef1231bea",
"shasum": ""
},
"require": {
"php": ">=5.3.1",
- "symfony/css-selector": "~2.1|~3.0"
+ "symfony/css-selector": "^2.7|^3.0|^4.0"
},
"require-dev": {
- "symfony/phpunit-bridge": "~2.7|~3.0"
+ "symfony/phpunit-bridge": "^3.3|^4.0"
},
"suggest": {
"behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
"behat/mink-goutte-driver": "fast headless driver for any app without JS emulation",
"behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)",
- "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)"
+ "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)",
+ "dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)"
},
"type": "library",
"extra": {
@@ -3152,7 +3463,7 @@
"testing",
"web"
],
- "time": "2017-02-06T09:59:54+00:00"
+ "time": "2019-05-14T09:56:49+00:00"
},
{
"name": "behat/mink-browserkit-driver",
@@ -3271,12 +3582,12 @@
"source": {
"type": "git",
"url": "https://github.com/minkphp/MinkSelenium2Driver.git",
- "reference": "93474c65a2a7bf959200ab5f7a14cc450645c185"
+ "reference": "8684ee4e634db7abda9039ea53545f86fc1e105a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/93474c65a2a7bf959200ab5f7a14cc450645c185",
- "reference": "93474c65a2a7bf959200ab5f7a14cc450645c185",
+ "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/8684ee4e634db7abda9039ea53545f86fc1e105a",
+ "reference": "8684ee4e634db7abda9039ea53545f86fc1e105a",
"shasum": ""
},
"require": {
@@ -3324,36 +3635,278 @@
"testing",
"webdriver"
],
- "time": "2018-01-07T19:17:08+00:00"
+ "time": "2018-10-10T12:39:06+00:00"
+ },
+ {
+ "name": "composer/ca-bundle",
+ "version": "1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/ca-bundle.git",
+ "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/558f321c52faeb4828c03e7dc0cfe39a09e09a2d",
+ "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-pcre": "*",
+ "php": "^5.3.2 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
+ "psr/log": "^1.0",
+ "symfony/process": "^2.5 || ^3.0 || ^4.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"
+ ],
+ "time": "2019-01-28T09:30:10+00:00"
+ },
+ {
+ "name": "composer/composer",
+ "version": "1.8.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/composer.git",
+ "reference": "949b116f9e7d98d8d276594fed74b580d125c0e6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/composer/zipball/949b116f9e7d98d8d276594fed74b580d125c0e6",
+ "reference": "949b116f9e7d98d8d276594fed74b580d125c0e6",
+ "shasum": ""
+ },
+ "require": {
+ "composer/ca-bundle": "^1.0",
+ "composer/semver": "^1.0",
+ "composer/spdx-licenses": "^1.2",
+ "composer/xdebug-handler": "^1.1",
+ "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
+ "php": "^5.3.2 || ^7.0",
+ "psr/log": "^1.0",
+ "seld/jsonlint": "^1.4",
+ "seld/phar-utils": "^1.0",
+ "symfony/console": "^2.7 || ^3.0 || ^4.0",
+ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
+ "symfony/finder": "^2.7 || ^3.0 || ^4.0",
+ "symfony/process": "^2.7 || ^3.0 || ^4.0"
+ },
+ "conflict": {
+ "symfony/console": "2.8.38"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7",
+ "phpunit/phpunit-mock-objects": "^2.3 || ^3.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": "1.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\": "src/Composer"
+ }
+ },
+ "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"
+ }
+ ],
+ "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
+ "homepage": "https://getcomposer.org/",
+ "keywords": [
+ "autoload",
+ "dependency",
+ "package"
+ ],
+ "time": "2019-04-09T15:46:48+00:00"
+ },
+ {
+ "name": "composer/spdx-licenses",
+ "version": "1.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/spdx-licenses.git",
+ "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d",
+ "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d",
+ "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"
+ ],
+ "time": "2019-03-26T10:23:26+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "1.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "d17708133b6c276d6e42ef887a877866b909d892"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/d17708133b6c276d6e42ef887a877866b909d892",
+ "reference": "d17708133b6c276d6e42ef887a877866b909d892",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0",
+ "psr/log": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+ },
+ "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"
+ ],
+ "time": "2019-01-28T20:25:53+00:00"
},
{
"name": "doctrine/instantiator",
- "version": "1.0.5",
+ "version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a",
"shasum": ""
},
"require": {
- "php": ">=5.3,<8.0-DEV"
+ "php": "^7.1"
},
"require-dev": {
- "athletic/athletic": "~0.1.8",
+ "doctrine/coding-standard": "^6.0",
"ext-pdo": "*",
"ext-phar": "*",
- "phpunit/phpunit": "~4.0",
- "squizlabs/php_codesniffer": "~2.0"
+ "phpbench/phpbench": "^0.13",
+ "phpstan/phpstan-phpunit": "^0.11",
+ "phpstan/phpstan-shim": "^0.11",
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.2.x-dev"
}
},
"autoload": {
@@ -3373,20 +3926,20 @@
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://github.com/doctrine/instantiator",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
"keywords": [
"constructor",
"instantiate"
],
- "time": "2015-06-14T21:17:01+00:00"
+ "time": "2019-03-17T17:37:11+00:00"
},
{
"name": "drupal/coder",
- "version": "8.3.2",
+ "version": "8.3.3",
"source": {
"type": "git",
"url": "https://git.drupal.org/project/coder.git",
- "reference": "44c80c21074df43572652f35bec4f184f9eae5e7"
+ "reference": "a33d3388fb2e1d94bd2aee36a8ff79186e9d8f43"
},
"require": {
"ext-mbstring": "*",
@@ -3415,7 +3968,7 @@
"phpcs",
"standards"
],
- "time": "2019-04-14T17:56:07+00:00"
+ "time": "2019-04-16T18:56:06+00:00"
},
{
"name": "fabpot/goutte",
@@ -3531,60 +4084,18 @@
],
"time": "2017-06-30T04:02:48+00:00"
},
- {
- "name": "ircmaxell/password-compat",
- "version": "v1.0.4",
- "source": {
- "type": "git",
- "url": "https://github.com/ircmaxell/password_compat.git",
- "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c",
- "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c",
- "shasum": ""
- },
- "require-dev": {
- "phpunit/phpunit": "4.*"
- },
- "type": "library",
- "autoload": {
- "files": [
- "lib/password.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Anthony Ferrara",
- "email": "ircmaxell@php.net",
- "homepage": "http://blog.ircmaxell.com"
- }
- ],
- "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash",
- "homepage": "https://github.com/ircmaxell/password_compat",
- "keywords": [
- "hashing",
- "password"
- ],
- "time": "2014-11-20T16:49:30+00:00"
- },
{
"name": "jcalderonzumba/gastonjs",
- "version": "v1.0.2",
+ "version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/jcalderonzumba/gastonjs.git",
- "reference": "21bebb8ca03eb0f93ec2f3fad61192fb079e2622"
+ "reference": "575a9c18d8b87990c37252e8d9707b29f0a313f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/jcalderonzumba/gastonjs/zipball/21bebb8ca03eb0f93ec2f3fad61192fb079e2622",
- "reference": "21bebb8ca03eb0f93ec2f3fad61192fb079e2622",
+ "url": "https://api.github.com/repos/jcalderonzumba/gastonjs/zipball/575a9c18d8b87990c37252e8d9707b29f0a313f3",
+ "reference": "575a9c18d8b87990c37252e8d9707b29f0a313f3",
"shasum": ""
},
"require": {
@@ -3628,20 +4139,20 @@
"headless",
"phantomjs"
],
- "time": "2016-01-18T09:21:03+00:00"
+ "time": "2017-03-31T07:31:47+00:00"
},
{
"name": "jcalderonzumba/mink-phantomjs-driver",
- "version": "v0.3.2",
+ "version": "v0.3.3",
"source": {
"type": "git",
"url": "https://github.com/jcalderonzumba/MinkPhantomJSDriver.git",
- "reference": "194942e14557b86467bf31e313f1370645d6c828"
+ "reference": "008f43670e94acd39273d15add1e7348eb23848d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/jcalderonzumba/MinkPhantomJSDriver/zipball/194942e14557b86467bf31e313f1370645d6c828",
- "reference": "194942e14557b86467bf31e313f1370645d6c828",
+ "url": "https://api.github.com/repos/jcalderonzumba/MinkPhantomJSDriver/zipball/008f43670e94acd39273d15add1e7348eb23848d",
+ "reference": "008f43670e94acd39273d15add1e7348eb23848d",
"shasum": ""
},
"require": {
@@ -3686,7 +4197,7 @@
"phantomjs",
"testing"
],
- "time": "2016-10-04T09:27:04+00:00"
+ "time": "2016-12-01T10:57:30+00:00"
},
{
"name": "justinrainbow/json-schema",
@@ -3756,16 +4267,16 @@
},
{
"name": "mikey179/vfsStream",
- "version": "v1.6.5",
+ "version": "v1.6.6",
"source": {
"type": "git",
"url": "https://github.com/bovigo/vfsStream.git",
- "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145"
+ "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
- "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
+ "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/095238a0711c974ae5b4ebf4c4534a23f3f6c99d",
+ "reference": "095238a0711c974ae5b4ebf4c4534a23f3f6c99d",
"shasum": ""
},
"require": {
@@ -3798,29 +4309,32 @@
],
"description": "Virtual file system to mock the real file system in unit tests.",
"homepage": "http://vfs.bovigo.org/",
- "time": "2017-08-01T08:02:14+00:00"
+ "time": "2019-04-08T13:54:32+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.7.0",
+ "version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
+ "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
+ "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": "^7.1"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
},
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
- "phpunit/phpunit": "^4.1"
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
"autoload": {
@@ -3843,7 +4357,7 @@
"object",
"object graph"
],
- "time": "2017-10-19T19:58:43+00:00"
+ "time": "2019-04-07T13:18:21+00:00"
},
{
"name": "phar-io/manifest",
@@ -3947,39 +4461,142 @@
"description": "Library for handling version information and constraints",
"time": "2017-03-05T17:38:23+00:00"
},
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+ "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
+ "src"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "time": "2017-09-11T18:02:19+00:00"
+ },
{
"name": "phpdocumentor/reflection-docblock",
- "version": "2.0.4",
+ "version": "4.3.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
+ "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
- "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
+ "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": "^7.0",
+ "phpdocumentor/reflection-common": "^1.0.0",
+ "phpdocumentor/type-resolver": "^0.4.0",
+ "webmozart/assert": "^1.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.0"
+ "doctrine/instantiator": "~1.0.5",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^6.4"
},
- "suggest": {
- "dflydev/markdown": "~1.0",
- "erusev/parsedown": "~1.0"
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "time": "2019-04-30T17:48:53+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "0.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
+ "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5 || ^7.0",
+ "phpdocumentor/reflection-common": "^1.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^0.9.4",
+ "phpunit/phpunit": "^5.2||^4.8.24"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "1.0.x-dev"
}
},
"autoload": {
- "psr-0": {
- "phpDocumentor": [
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
"src/"
]
}
@@ -3991,40 +4608,40 @@
"authors": [
{
"name": "Mike van Riel",
- "email": "mike.vanriel@naenius.com"
+ "email": "me@mikevanriel.com"
}
],
- "time": "2015-02-03T12:10:50+00:00"
+ "time": "2017-07-14T14:27:02+00:00"
},
{
"name": "phpspec/prophecy",
- "version": "v1.7.0",
+ "version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "93d39f1f7f9326d746203c7c056f300f7f126073"
+ "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073",
- "reference": "93d39f1f7f9326d746203c7c056f300f7f126073",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+ "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
- "phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
- "sebastian/comparator": "^1.1|^2.0",
+ "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
+ "sebastian/comparator": "^1.1|^2.0|^3.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5|^3.2",
- "phpunit/phpunit": "^4.8 || ^5.6.5"
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6.x-dev"
+ "dev-master": "1.8.x-dev"
}
},
"autoload": {
@@ -4057,7 +4674,7 @@
"spy",
"stub"
],
- "time": "2017-03-02T20:05:34+00:00"
+ "time": "2018-08-05T17:53:17+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -5011,6 +5628,99 @@
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2016-10-03T07:35:21+00:00"
},
+ {
+ "name": "seld/jsonlint",
+ "version": "1.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/jsonlint.git",
+ "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38",
+ "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.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"
+ ],
+ "time": "2018-01-24T12:46:19+00:00"
+ },
+ {
+ "name": "seld/phar-utils",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/phar-utils.git",
+ "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a",
+ "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a",
+ "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": [
+ "phra"
+ ],
+ "time": "2015-10-13T18:44:15+00:00"
+ },
{
"name": "squizlabs/php_codesniffer",
"version": "3.4.2",
@@ -5064,25 +5774,25 @@
},
{
"name": "symfony/browser-kit",
- "version": "v3.4.26",
+ "version": "v4.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
- "reference": "7f2b0843d5045468225f9a9b27a0cb171ae81828"
+ "reference": "c09c18cca96d7067152f78956faf55346c338283"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/browser-kit/zipball/7f2b0843d5045468225f9a9b27a0cb171ae81828",
- "reference": "7f2b0843d5045468225f9a9b27a0cb171ae81828",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c09c18cca96d7067152f78956faf55346c338283",
+ "reference": "c09c18cca96d7067152f78956faf55346c338283",
"shasum": ""
},
"require": {
- "php": "^5.5.9|>=7.0.8",
- "symfony/dom-crawler": "~2.8|~3.0|~4.0"
+ "php": "^7.1.3",
+ "symfony/dom-crawler": "~3.4|~4.0"
},
"require-dev": {
- "symfony/css-selector": "~2.8|~3.0|~4.0",
- "symfony/process": "~2.8|~3.0|~4.0"
+ "symfony/css-selector": "~3.4|~4.0",
+ "symfony/process": "~3.4|~4.0"
},
"suggest": {
"symfony/process": ""
@@ -5090,7 +5800,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.4-dev"
+ "dev-master": "4.2-dev"
}
},
"autoload": {
@@ -5117,11 +5827,11 @@
],
"description": "Symfony BrowserKit Component",
"homepage": "https://symfony.com",
- "time": "2019-04-06T19:33:58+00:00"
+ "time": "2019-04-07T09:56:43+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@@ -5174,25 +5884,25 @@
},
{
"name": "symfony/dom-crawler",
- "version": "v3.4.26",
+ "version": "v4.2.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "d40023c057393fb25f7ca80af2a56ed948c45a09"
+ "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d40023c057393fb25f7ca80af2a56ed948c45a09",
- "reference": "d40023c057393fb25f7ca80af2a56ed948c45a09",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb",
+ "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb",
"shasum": ""
},
"require": {
- "php": "^5.5.9|>=7.0.8",
+ "php": "^7.1.3",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
- "symfony/css-selector": "~2.8|~3.0|~4.0"
+ "symfony/css-selector": "~3.4|~4.0"
},
"suggest": {
"symfony/css-selector": ""
@@ -5200,7 +5910,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.4-dev"
+ "dev-master": "4.2-dev"
}
},
"autoload": {
@@ -5227,11 +5937,110 @@
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
- "time": "2019-02-23T15:06:07+00:00"
+ "time": "2019-02-23T15:17:42+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v4.2.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601",
+ "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.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",
+ "time": "2019-02-07T11:40:08+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v4.2.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "e45135658bd6c14b61850bf131c4f09a55133f69"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/e45135658bd6c14b61850bf131c4f09a55133f69",
+ "reference": "e45135658bd6c14b61850bf131c4f09a55133f69",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.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",
+ "time": "2019-04-06T13:51:08+00:00"
},
{
"name": "symfony/phpunit-bridge",
- "version": "v3.4.26",
+ "version": "v3.4.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/phpunit-bridge.git",
@@ -5333,13 +6142,65 @@
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"time": "2019-04-04T09:56:43+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/assert.git",
+ "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
+ "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.6",
+ "sebastian/version": "^1.0.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "time": "2018-12-25T11:19:39+00:00"
}
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"behat/mink": 20,
- "behat/mink-selenium2-driver": 20
+ "behat/mink-selenium2-driver": 20,
+ "composer/composer": 0
},
"prefer-stable": true,
"prefer-lowest": false,
@@ -5357,7 +6218,8 @@
"ext-spl": "*",
"ext-tokenizer": "*",
"ext-xml": "*",
- "php": "^5.5.9|>=7.0.8"
+ "php": ">=7.0.8",
+ "composer-plugin-api": "^1.0.0"
},
"platform-dev": []
}
diff --git a/core/composer.json b/core/composer.json
index e5de4f6b2c..076685d661 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -110,6 +110,7 @@
"drupal/core-proxy-builder": "self.version",
"drupal/core-render": "self.version",
"drupal/core-serialization": "self.version",
+ "drupal/core-composer-scaffold": "self.version",
"drupal/core-transliteration": "self.version",
"drupal/core-utility": "self.version",
"drupal/core-uuid": "self.version",
@@ -200,6 +201,7 @@
"core/lib/Drupal/Component/ProxyBuilder/composer.json",
"core/lib/Drupal/Component/Render/composer.json",
"core/lib/Drupal/Component/Serialization/composer.json",
+ "core/lib/Drupal/Component/Scaffold/composer.json",
"core/lib/Drupal/Component/Transliteration/composer.json",
"core/lib/Drupal/Component/Utility/composer.json",
"core/lib/Drupal/Component/Uuid/composer.json",
diff --git a/core/lib/Drupal/Component/Scaffold/AllowedPackages.php b/core/lib/Drupal/Component/Scaffold/AllowedPackages.php
new file mode 100644
index 0000000000..5b0c25bb2d
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/AllowedPackages.php
@@ -0,0 +1,163 @@
+composer = $composer;
+ $this->io = $io;
+ $this->manageOptions = $manageOptions;
+ }
+
+ /**
+ * Gets a list of all packages that are allowed to copy 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() {
+ $options = $this->manageOptions->getOptions();
+ $allowed_packages = $this->recursiveGetAllowedPackages($options->allowedPackages());
+ // 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 ($options->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.
+ $allowed_packages = $this->evaluateNewPackages($allowed_packages);
+ return $allowed_packages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function event(PackageEvent $event) {
+ $operation = $event->getOperation();
+ // Determine the package.
+ $package = $operation->getJobType() == 'update' ? $operation->getTargetPackage() : $operation->getPackage();
+ if (ScaffoldOptions::hasOptions($package->getExtra())) {
+ $this->newPackages[$package->getName()] = $package;
+ }
+ }
+
+ /**
+ * 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 && $package instanceof PackageInterface && !array_key_exists($name, $allowed_packages)) {
+ $allowed_packages[$name] = $package;
+ $packageOptions = $this->manageOptions->packageOptions($package);
+ $allowed_packages = $this->recursiveGetAllowedPackages($packageOptions->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("Package {$name} has scaffold operations, but it is not allowed in the root-level composer.json file.");
+ }
+ else {
+ $this->io->write("Package {$name} 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.
+ 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, '*');
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/CommandProvider.php b/core/lib/Drupal/Component/Scaffold/CommandProvider.php
new file mode 100644
index 0000000000..9ec63b4a0d
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/CommandProvider.php
@@ -0,0 +1,19 @@
+setName('composer:scaffold')
+ ->setDescription('Update the Composer scaffold files.')
+ ->setHelp(
+ <<composer:scaffold command places the scaffold files in their
+respective locations according to the layout stipulated in the
+composer.json file.
+
+php composer.phar composer:scaffold
+
+It is usually not necessary to call composer:scaffold manually,
+because it is called automatically as needed, e.g. after an install
+or update 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) {
+ $handler = new Handler($this->getComposer(), $this->getIO());
+ $handler->scaffold();
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/GenerateAutoloadReferenceFile.php b/core/lib/Drupal/Component/Scaffold/GenerateAutoloadReferenceFile.php
new file mode 100644
index 0000000000..332dc12093
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/GenerateAutoloadReferenceFile.php
@@ -0,0 +1,89 @@
+fullPath());
+ // Calculate the relative path from the webroot (location of the project
+ // autoload.php) to the vendor directory.
+ $fs = new SymfonyFilesystem();
+ $relativeVendorPath = $fs->makePathRelative($vendorPath, realpath($location));
+ $fs->dumpFile($autoloadPath->fullPath(), static::autoLoadContents($relativeVendorPath));
+ return new ScaffoldResult($autoloadPath, TRUE);
+ }
+
+ /**
+ * 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\Component\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 $relativeVendorPath
+ * The relative path to vendor.
+ *
+ * @return string
+ * Return the contents for the autoload.php.
+ */
+ protected static function autoLoadContents($relativeVendorPath) {
+ $relativeVendorPath = rtrim($relativeVendorPath, '/');
+ return <<composer = $composer;
+ $this->io = $io;
+ $this->manageOptions = new ManageOptions($composer);
+ $this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions);
+ }
+
+ /**
+ * Listens to the post install command event to execute the scaffolding.
+ *
+ * @param \Composer\Script\Event $event
+ * The Composer event.
+ */
+ public function onPostCmdEvent(Event $event) {
+ $this->scaffold();
+ }
+
+ /**
+ * Registers post-package events before any 'require' event runs.
+ *
+ * This method is called by composer prior to doing a 'require' command.
+ *
+ * @param \Composer\Plugin\CommandEvent $event
+ * The Composer Command event.
+ */
+ public function beforeRequire(CommandEvent $event) {
+ // 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\Component\Scaffold\Operations\OperationInterface[]
+ * A list of scaffolding operation objects
+ */
+ protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) {
+ $scaffoldOpFactory = new OperationFactory($this->composer);
+ $scaffoldOps = [];
+ foreach ($package_file_mappings as $dest_rel_path => $metadata) {
+ $scaffoldOps[$dest_rel_path] = $scaffoldOpFactory->create($package, $dest_rel_path, $metadata);
+ }
+ return $scaffoldOps;
+ }
+
+ /**
+ * 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.
+ $allowedPackages = $this->manageAllowedPackages->getAllowedPackages();
+ if (empty($allowedPackages)) {
+ $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 = new EventDispatcher($this->composer, $this->io);
+ $dispatcher->dispatch(self::PRE_COMPOSER_SCAFFOLD_CMD);
+
+ // Fetch the list of file mappings from each allowed package and normalize
+ // them.
+ $file_mappings = $this->getFileMappingsFromPackages($allowedPackages);
+
+ // Analyze the list of file mappings, and determine which take priority.
+ $scaffoldCollection = new OperationCollection($this->io);
+ $locationReplacements = $this->manageOptions->getLocationReplacements();
+
+ // Write the collected scaffold files to the designated location on disk.
+ $scaffoldResults = $scaffoldCollection->process($file_mappings, $locationReplacements, $this->manageOptions->getOptions());
+
+ // 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.
+ $webRoot = $this->manageOptions->getOptions()->requiredLocation('web-root');
+ $scaffoldResults[] = GenerateAutoloadReferenceFile::generateAutoload($this->rootPackageName(), $webRoot, $this->getVendorPath());
+
+ // Add the managed scaffold files to .gitignore if applicable.
+ $gitIgnoreManager = new ManageGitIgnore(getcwd());
+ $gitIgnoreManager->manageIgnored($scaffoldResults, $this->manageOptions->getOptions());
+
+ // Call post-scaffold scripts.
+ $dispatcher->dispatch(self::POST_COMPOSER_SCAFFOLD_CMD);
+ }
+
+ /**
+ * Gets the path to the 'vendor' directory.
+ *
+ * @return string
+ * The file path of the vendor directory.
+ */
+ protected function getVendorPath() {
+ $vendorDir = $this->composer->getConfig()->get('vendor-dir');
+ $filesystem = new Filesystem();
+ return $filesystem->normalizePath(realpath($vendorDir));
+ }
+
+ /**
+ * 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\Component\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) {
+ $package_file_mappings = $this->getPackageFileMappings($package);
+ $file_mappings[$package_name] = $package_file_mappings;
+ }
+ 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\Component\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());
+ }
+ else {
+ if (!$options->hasAllowedPackages()) {
+ $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();
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Interpolator.php b/core/lib/Drupal/Component/Scaffold/Interpolator.php
new file mode 100644
index 0000000000..70577e26b5
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Interpolator.php
@@ -0,0 +1,155 @@
+startToken = $startToken;
+ $this->endToken = $endToken;
+ }
+
+ /**
+ * Sets the data set to use when interpolating.
+ *
+ * @param array $data
+ * Interpolation data 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
+ * Interpolation data 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) {
+ $replacementText = array_key_exists($key, $data) ? $data[$key] : $default;
+ if ($replacementText !== FALSE) {
+ $replacements[$sourceText] = $replacementText;
+ }
+ }
+ 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) {
+ $regEx = '#' . $this->startToken . '([a-zA-Z0-9._-]+)' . $this->endToken . '#';
+ if (!preg_match_all($regEx, $message, $matches, PREG_SET_ORDER)) {
+ return [];
+ }
+ $tokens = [];
+ foreach ($matches as $matchSet) {
+ list($sourceText, $key) = $matchSet;
+ $tokens[$sourceText] = $key;
+ }
+ return $tokens;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/LICENSE.txt b/core/lib/Drupal/Component/Scaffold/LICENSE.txt
new file mode 100644
index 0000000000..94fb84639c
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/LICENSE.txt
@@ -0,0 +1,339 @@
+ 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.
+
+
+ Copyright (C)
+
+ 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.
+
+ , 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.
diff --git a/core/lib/Drupal/Component/Scaffold/ManageGitIgnore.php b/core/lib/Drupal/Component/Scaffold/ManageGitIgnore.php
new file mode 100644
index 0000000000..28a37fb9f0
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/ManageGitIgnore.php
@@ -0,0 +1,171 @@
+dir = $dir;
+ }
+
+ /**
+ * Manages gitignore files.
+ *
+ * @param array $files
+ * A list of scaffold results, each of which holds a path and whether
+ * or not that file is managed.
+ * @param \Drupal\Component\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.
+ $addToGitIgnore = [];
+ foreach ($files as $scaffoldResult) {
+ $isIgnored = $this->checkIgnore($scaffoldResult->destination()->fullPath());
+ if (!$isIgnored) {
+ $isTracked = $this->checkTracked($scaffoldResult->destination()->fullPath());
+ if (!$isTracked && $scaffoldResult->isManaged()) {
+ $path = $scaffoldResult->destination()->fullPath();
+ $dir = realpath(dirname($path));
+ $name = basename($path);
+ $addToGitIgnore[$dir][] = $name;
+ }
+ }
+ }
+ // Write out the .gitignore files one at a time.
+ foreach ($addToGitIgnore as $dir => $entries) {
+ $this->addToGitIgnore($dir, $entries);
+ }
+ }
+
+ /**
+ * Determines whether the specified scaffold file is already ignored.
+ *
+ * @param string $path
+ * Path to scaffold file to check.
+ *
+ * @return bool
+ * Whether the specified file is already ignored or not (TRUE if ignored).
+ */
+ protected function checkIgnore($path) {
+ $process = new Process('git check-ignore ' . $path, $this->dir);
+ $process->run();
+ $isIgnored = $process->getExitCode() == 0;
+ return $isIgnored;
+ }
+
+ /**
+ * Determines whether the specified scaffold file is tracked by git.
+ *
+ * @param string $path
+ * Path to scaffold file to check.
+ *
+ * @return bool
+ * Whether the specified file is already tracked or not (TRUE if tracked).
+ */
+ protected function checkTracked($path) {
+ $process = new Process('git ls-files --error-unmatch ' . $path, $this->dir);
+ $process->run();
+ $isTracked = $process->getExitCode() == 0;
+ return $isTracked;
+ }
+
+ /**
+ * Checks to see if the project root dir is in a git repository.
+ *
+ * @return bool
+ * True if this is a repository.
+ */
+ protected function isRepository() {
+ $process = new Process('git rev-parse --show-toplevel', $this->dir);
+ $process->run();
+ $isRepository = $process->getExitCode() == 0;
+ return $isRepository;
+ }
+
+ /**
+ * Checks to see if the vendor directory is git ignored.
+ *
+ * @return bool
+ * True if 'vendor' is committed, or false if it is ignored.
+ */
+ protected function vendorCommitted() {
+ return $this->checkTracked('vendor');
+ }
+
+ /**
+ * Determines whether we should manage gitignore files.
+ *
+ * @param \Drupal\Component\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 (!$this->isRepository()) {
+ 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 not
+ // committed.
+ return !$this->vendorCommitted();
+ }
+
+ /**
+ * 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);
+ $gitIgnorePath = $dir . '/.gitignore';
+ $contents = '';
+
+ // Appending to existing .gitignore files.
+ if (file_exists($gitIgnorePath)) {
+ $contents = file_get_contents($gitIgnorePath);
+ if (!empty($contents) && substr($contents, -1) != "\n") {
+ $contents .= "\n";
+ }
+ }
+
+ $contents .= implode("\n", $entries);
+ file_put_contents($gitIgnorePath, $contents);
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/ManageOptions.php b/core/lib/Drupal/Component/Scaffold/ManageOptions.php
new file mode 100644
index 0000000000..be69cfb1a2
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/ManageOptions.php
@@ -0,0 +1,90 @@
+composer = $composer;
+ }
+
+ /**
+ * Gets the root-level scaffold options for this project.
+ *
+ * @return \Drupal\Component\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\Component\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\Component\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 filed 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;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Operations/AppendOp.php b/core/lib/Drupal/Component/Scaffold/Operations/AppendOp.php
new file mode 100644
index 0000000000..eeb47f0bd3
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Operations/AppendOp.php
@@ -0,0 +1,123 @@
+prepend = $prependPath;
+ $this->append = $appendPath;
+ }
+
+ /**
+ * Adds interpolation data for our append and prepend source files.
+ *
+ * @param \Drupal\Component\Scaffold\Interpolator $interpolator
+ * Interpolator to add data to.
+ */
+ protected function addInterpolationData(Interpolator $interpolator) {
+ if (isset($this->prepend)) {
+ $this->prepend->addInterpolationData($interpolator, 'prepend');
+ }
+ if (isset($this->append)) {
+ $this->append->addInterpolationData($interpolator, 'append');
+ }
+ }
+
+ /**
+ * Called if the append op does not have an original operation at the same destination path.
+ *
+ * {@inheritdoc}
+ */
+ public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
+ throw new \RuntimeException($destination->getInterpolator()->interpolate("Cannot append/prepend because no prior package provided a scaffold file at that [dest-rel-path]."));
+ }
+
+ /**
+ * Appends or prepends information to the overridden scaffold file.
+ *
+ * {@inheritdoc}
+ */
+ public function preprocess(OperationInterface $originalOp, ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
+ $interpolator = $destination->getInterpolator();
+ $this->addInterpolationData($interpolator);
+ $destination_path = $destination->fullPath();
+ // First, scaffold the original file. Disable symlinking, because we
+ // need a copy of the file if we're going to append / prepend to it.
+ @unlink($destination_path);
+ $originalOp->process($destination, $io, $options->overrideSymlink(FALSE));
+ // Fetch the prepend contents, if provided.
+ $prependContents = '';
+ if (!empty($this->prepend)) {
+ $prependContents = file_get_contents($this->prepend->fullPath()) . "\n";
+ $io->write($interpolator->interpolate(" - Prepend to [dest-rel-path] from [prepend-rel-path]"));
+ }
+ // Fetch the append contents, if provided.
+ $appendContents = '';
+ if (!empty($this->append)) {
+ $appendContents = "\n" . file_get_contents($this->append->fullPath());
+ $io->write($interpolator->interpolate(" - Append to [dest-rel-path] from [append-rel-path]"));
+ }
+ $this->append($destination, $prependContents, $appendContents, $io);
+ return new ScaffoldResult($destination, TRUE);
+ }
+
+ /**
+ * Performs the append and prepend operations on the provided scaffold file.
+ *
+ * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination
+ * The scaffold file to append and prepend to.
+ * @param string $prependContents
+ * The contents to add to the beginning of the file.
+ * @param string $appendContents
+ * The contents to add to the end of the file.
+ * @param \Composer\IO\IOInterface $io
+ * IOInterface to write to.
+ */
+ protected function append(ScaffoldFilePath $destination, $prependContents, $appendContents, IOInterface $io) {
+ $interpolator = $destination->getInterpolator();
+ $destination_path = $destination->fullPath();
+ // Exit early if there is no append / prepend data.
+ if (empty(trim($prependContents)) && empty(trim($appendContents))) {
+ $io->write($interpolator->interpolate(" - Keep [dest-rel-path] unchanged: no content to prepend / append was provided."));
+ return;
+ }
+ // Assume that none of these files is very large, so load them all into
+ // memory for now. Considering uses streams to scaffold large files.
+ $originalContents = file_get_contents($destination_path);
+ // Write the appended and prepended contents back to the file.
+ $alteredContents = $prependContents . $originalContents . $appendContents;
+ file_put_contents($destination_path, $alteredContents);
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php b/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php
new file mode 100644
index 0000000000..f5667c2c7e
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationCollection.php
@@ -0,0 +1,152 @@
+io = $io;
+ }
+
+ /**
+ * Finds the package name that provides the scaffold file.
+ *
+ * Given the list of all scaffold file info objects, return the package that
+ * provides the scaffold file info for the scaffold file that will be placed
+ * at the destination that this scaffold file would be placed at. Note that
+ * this will be the same as $scaffold_file->packageName() unless this scaffold
+ * file has been overridden or removed by some other package.
+ *
+ * @param array $list_of_scaffold_files
+ * Associative array containing destination => operation mappings.
+ * @param \Drupal\Component\Scaffold\ScaffoldFileInfo $scaffold_file
+ * The scaffold file to use to find a providing package name.
+ *
+ * @return string
+ * The name of the package that provided the scaffold file information.
+ */
+ protected function findProvidingPackage(array $list_of_scaffold_files, ScaffoldFileInfo $scaffold_file) {
+ // The scaffold file should always be in our list, but we will check
+ // just to be sure that it really is.
+ $dest_rel_path = $scaffold_file->destination()->relativePath();
+ if (!array_key_exists($dest_rel_path, $list_of_scaffold_files)) {
+ throw new \RuntimeException("Scaffold file not found in list of all scaffold files.");
+ }
+ $overridden_scaffold_file = $list_of_scaffold_files[$dest_rel_path];
+ return $overridden_scaffold_file->packageName();
+ }
+
+ /**
+ * Process all of the scaffold files listed in the provided file mappings.
+ * @param array $file_mappings
+ * An multidimensional array of file mappings, as returned by
+ * self::getFileMappingsFromPackages().
+ * @param \Drupal\Component\Scaffold\Interpolator $locationReplacements
+ * An object with the location mappings (e.g. [web-root]).
+ * @param \Drupal\Component\Scaffold\ScaffoldOptions $options
+ * Configuration options from the top-level composer.json file.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult[]
+ * Associative array keyed by destination path and values as the scaffold
+ * result for each scaffolded file.
+ */
+ public function process(array $file_mappings, Interpolator $locationReplacements, ScaffoldOptions $options) {
+ list($list_of_scaffold_files, $resolved_file_mappings) = $this->collateScaffoldFiles($file_mappings, $locationReplacements);
+ return $this->processScaffoldFiles($list_of_scaffold_files, $resolved_file_mappings, $options);
+ }
+
+ /**
+ * Organizes provided file mappings by destination and package.
+ *
+ * @param array $file_mappings
+ * An multidimensional array of file mappings, as returned by
+ * self::getFileMappingsFromPackages().
+ * @param \Drupal\Component\Scaffold\Interpolator $locationReplacements
+ * An object with the location mappings (e.g. [web-root]).
+ *
+ * @return array
+ * A list containing two lists:
+ * - Associative array containing destination => operation mappings.
+ * - Associative array containing package name => file mappings.
+ */
+ protected function collateScaffoldFiles(array $file_mappings, Interpolator $locationReplacements) {
+ $resolved_file_mappings = [];
+ $list_of_scaffold_files = [];
+ 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, $locationReplacements);
+ $scaffold_file = new ScaffoldFileInfo($destination, $op);
+ // If there was already a scaffolding operation happening at this
+ // path, then pass it along to the new scaffold op, if it cares.
+ if (isset($list_of_scaffold_files[$destination_rel_path]) && $op instanceof PreprocessOriginalOpInterface) {
+ $preprocessOp = new PreprocessOp($list_of_scaffold_files[$destination_rel_path]->op(), $op);
+ $scaffold_file = new ScaffoldFileInfo($destination, $preprocessOp);
+ }
+ $list_of_scaffold_files[$destination_rel_path] = $scaffold_file;
+ $resolved_file_mappings[$package_name][$destination_rel_path] = $scaffold_file;
+ }
+ }
+ return [$list_of_scaffold_files, $resolved_file_mappings];
+ }
+
+ /**
+ * Scaffolds the files in our scaffold collection, package-by-package.
+ *
+ * @param array $list_of_scaffold_files
+ * Associative array containing destination => operation mappings.
+ * @param array $resolved_file_mappings
+ * Associative array containing package name => file mappings.
+ * @param \Drupal\Component\Scaffold\ScaffoldOptions $options
+ * Configuration options from the top-level composer.json file.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult[]
+ * Associative array keyed by destination path and values as the scaffold
+ * result for each scaffolded file.
+ */
+ protected function processScaffoldFiles(array $list_of_scaffold_files, array $resolved_file_mappings, ScaffoldOptions $options) {
+ $result = [];
+ // We could simply scaffold all of the files from $list_of_scaffold_files,
+ // which contain only the list of files to be processed. We iterate over
+ // $resolved_file_mappings instead so that we can print out all of the
+ // scaffold files grouped by the package that provided them, including
+ // those not being scaffolded (because they were overridden or removed
+ // by some later package).
+ foreach ($resolved_file_mappings as $package_name => $package_scaffold_files) {
+ $this->io->write("Scaffolding files for {$package_name}:");
+ foreach ($package_scaffold_files as $dest_rel_path => $scaffold_file) {
+ $overriding_package = $this->findProvidingPackage($list_of_scaffold_files, $scaffold_file);
+ if ($scaffold_file->overridden($overriding_package)) {
+ $this->io->write($scaffold_file->interpolate(" - Skip [dest-rel-path]: overridden in {$overriding_package}"));
+ }
+ else {
+ $result[$dest_rel_path] = $scaffold_file->process($this->io, $options);
+ }
+ }
+ }
+ return $result;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Operations/OperationFactory.php b/core/lib/Drupal/Component/Scaffold/Operations/OperationFactory.php
new file mode 100644
index 0000000000..ad6f531837
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationFactory.php
@@ -0,0 +1,185 @@
+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 string $dest_rel_path
+ * The destination path for the scaffold file. Used only for error messages.
+ * @param mixed $metadata
+ * The metadata for this operation object, which varies by operation type.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\OperationInterface
+ * The scaffolding operation object (skip, replace, etc.)
+ *
+ * @throws \RuntimeException
+ * Exception thrown when $metadata can not be used to determine a scaffold
+ * operation.
+ */
+ public function create(PackageInterface $package, $dest_rel_path, $metadata) {
+ $metadata = $this->normalizeScaffoldMetadata($dest_rel_path, $metadata);
+ switch ($metadata['mode']) {
+ case 'skip':
+ return new SkipOp();
+
+ case 'replace':
+ return $this->createReplaceOp($package, $dest_rel_path, $metadata);
+
+ case 'append':
+ return $this->createAppendOp($package, $dest_rel_path, $metadata);
+ }
+ throw new \RuntimeException("Unknown scaffold operation mode {$metadata['mode']}.");
+ }
+
+ /**
+ * 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 string $dest_rel_path
+ * The destination path for the scaffold file. Used only for error messages.
+ * @param array $metadata
+ * The metadata for this operation object, i.e. the relative 'path'.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\OperationInterface
+ * A scaffold replace operation object.
+ */
+ protected function createReplaceOp(PackageInterface $package, $dest_rel_path, array $metadata) {
+ // If this op does not provide an 'overwrite' value, default it to true.
+ $metadata += ['overwrite' => TRUE];
+ if (!isset($metadata['path'])) {
+ 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, $dest_rel_path, $metadata['path']);
+ $op = new ReplaceOp($source, $metadata['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 string $dest_rel_path
+ * The destination path for the scaffold file. Used only for error messages.
+ * @param array $metadata
+ * The metadata for this operation object, i.e. the relative 'path'.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\OperationInterface
+ * A scaffold replace operation object.
+ */
+ protected function createAppendOp(PackageInterface $package, $dest_rel_path, array $metadata) {
+ $package_name = $package->getName();
+ $package_path = $this->getPackagePath($package);
+ $prepend_source_file = NULL;
+ $append_source_file = NULL;
+ if (isset($metadata['prepend'])) {
+ $prepend_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $dest_rel_path, $metadata['prepend']);
+ }
+ if (isset($metadata['append'])) {
+ $append_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $dest_rel_path, $metadata['append']);
+ }
+ $op = new AppendOp($prepend_source_file, $append_source_file);
+ return $op;
+ }
+
+ /**
+ * 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();
+ }
+ else {
+ return $this->composer->getInstallationManager()->getInstallPath($package);
+ }
+ }
+
+ /**
+ * 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 $dest_rel_path
+ * 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 normalizeScaffoldMetadata($dest_rel_path, $value) {
+ if (is_bool($value)) {
+ if (!$value) {
+ return ['mode' => 'skip'];
+ }
+ throw new \RuntimeException("File mapping {$dest_rel_path} cannot be given the value 'true'.");
+ }
+ if (empty($value)) {
+ throw new \RuntimeException("File mapping {$dest_rel_path} cannot be empty.");
+ }
+ if (is_string($value)) {
+ $value = ['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['mode']) && (isset($value['append']) || isset($value['prepend']))) {
+ $value['mode'] = 'append';
+ }
+ // If there is no 'mode', then the default is 'replace'.
+ if (!isset($value['mode'])) {
+ $value['mode'] = 'replace';
+ }
+ return $value;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Operations/OperationInterface.php b/core/lib/Drupal/Component/Scaffold/Operations/OperationInterface.php
new file mode 100644
index 0000000000..8fa0cee2be
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Operations/OperationInterface.php
@@ -0,0 +1,29 @@
+originalOp = $originalOp;
+ $this->op = $op;
+ }
+
+ /**
+ * Skip the specified scaffold file.
+ *
+ * {@inheritdoc}
+ */
+ public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
+ return $this->op->preprocess($this->originalOp, $destination, $io, $options);
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Operations/PreprocessOriginalOpInterface.php b/core/lib/Drupal/Component/Scaffold/Operations/PreprocessOriginalOpInterface.php
new file mode 100644
index 0000000000..e287fc21e2
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Operations/PreprocessOriginalOpInterface.php
@@ -0,0 +1,31 @@
+source = $sourcePath;
+ $this->overwrite = $overwrite;
+ }
+
+ /**
+ * Gets the source.
+ *
+ * @return \Drupal\Component\Scaffold\ScaffoldFilePath
+ * The source file reference object.
+ */
+ protected function getSource() {
+ return $this->source;
+ }
+
+ /**
+ * Determines whether scaffold file should overwrite files.
+ *
+ * @return bool
+ * Value of the 'overwrite' option.
+ */
+ protected function getOverwrite() {
+ return $this->overwrite;
+ }
+
+ /**
+ * Copy or Symlink the specified scaffold file.
+ *
+ * {@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->getOverwrite() === FALSE && file_exists($destination_path)) {
+ $interpolator = $destination->getInterpolator();
+ $io->write($interpolator->interpolate(" - Skip [dest-rel-path] because it already exists and overwrite is false."));
+ 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.
+ @unlink($destination_path);
+ $fs->ensureDirectoryExists(dirname($destination_path));
+ if ($options->symlink() == TRUE) {
+ return $this->symlinkScaffold($destination, $io);
+ }
+ return $this->copyScaffold($destination, $io);
+ }
+
+ /**
+ * Copies the scaffold file.
+ *
+ * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination
+ * Scaffold file to process.
+ * @param \Composer\IO\IOInterface $io
+ * IOInterface to writing to.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult
+ * The scaffold result.
+ */
+ protected function copyScaffold(ScaffoldFilePath $destination, IOInterface $io) {
+ $interpolator = $destination->getInterpolator();
+ $this->getSource()->addInterpolationData($interpolator);
+ $success = copy($this->getSource()->fullPath(), $destination->fullPath());
+ if (!$success) {
+ throw new \RuntimeException($interpolator->interpolate("Could not copy source file [src-rel-path] to [dest-rel-path]!"));
+ }
+ $io->write($interpolator->interpolate(" - Copy [dest-rel-path] from [src-rel-path]"));
+ return new ScaffoldResult($destination, $this->getOverwrite());
+ }
+
+ /**
+ * Symlinks the scaffold file.
+ *
+ * @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination
+ * Scaffold file to process.
+ * @param \Composer\IO\IOInterface $io
+ * IOInterface to writing to.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult
+ * The scaffold result.
+ */
+ protected function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io) {
+ $interpolator = $destination->getInterpolator();
+ try {
+ $fs = new Filesystem();
+ $fs->relativeSymlink($this->getSource()->fullPath(), $destination->fullPath());
+ }
+ catch (\Exception $e) {
+ throw new \RuntimeException($interpolator->interpolate("Could not symlink source file [src-rel-path] to [dest-rel-path]!"), [], $e);
+ }
+ $io->write($interpolator->interpolate(" - Link [dest-rel-path] from [src-rel-path]"));
+ return new ScaffoldResult($destination, $this->getOverwrite());
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldResult.php b/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldResult.php
new file mode 100644
index 0000000000..b542b1e1a3
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Operations/ScaffoldResult.php
@@ -0,0 +1,59 @@
+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\Component\Scaffold\ScaffoldFilePath
+ * The destination path for the scaffold result.
+ */
+ public function destination() {
+ return $this->destination;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php
new file mode 100644
index 0000000000..325737be0f
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Operations/SkipOp.php
@@ -0,0 +1,25 @@
+getInterpolator();
+ $io->write($interpolator->interpolate(" - Skip [dest-rel-path]: disabled"));
+ return new ScaffoldResult($destination, FALSE);
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/Plugin.php b/core/lib/Drupal/Component/Scaffold/Plugin.php
new file mode 100644
index 0000000000..61a39b9c6d
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/Plugin.php
@@ -0,0 +1,90 @@
+handler = new Handler($composer, $io);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCapabilities() {
+ return ['Composer\Plugin\Capability\CommandProvider' => ScaffoldCommandProvider::class];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ return [
+ ScriptEvents::POST_UPDATE_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->onPostCmdEvent($event);
+ }
+
+ /**
+ * Post package event behaviour.
+ *
+ * @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') {
+ $this->handler->beforeRequire($event);
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/PostPackageEventListenerInterface.php b/core/lib/Drupal/Component/Scaffold/PostPackageEventListenerInterface.php
new file mode 100644
index 0000000000..2285146f27
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/PostPackageEventListenerInterface.php
@@ -0,0 +1,22 @@
+destination = $destination;
+ $this->op = $op;
+ }
+
+ /**
+ * Gets the Scaffold operation.
+ *
+ * @return \Drupal\Component\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\Component\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->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\Component\Scaffold\ScaffoldOptions $options
+ * Assorted operational options, e.g. whether the destination should be a
+ * symlink.
+ *
+ * @return \Drupal\Component\Scaffold\Operations\ScaffoldResult
+ * The scaffold result.
+ */
+ public function process(IOInterface $io, ScaffoldOptions $options) {
+ return $this->op()->process($this->destination, $io, $options);
+ }
+
+ /**
+ * Interpolates a string using the data from this scaffold file info.
+ *
+ * @return \Drupal\Component\Scaffold\Interpolator
+ * An interpolator for making string replacements.
+ */
+ protected function getInterpolator() {
+ return $this->destination->getInterpolator();
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php b/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php
new file mode 100644
index 0000000000..35a1428fbe
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/ScaffoldFilePath.php
@@ -0,0 +1,190 @@
+type = $path_type;
+ $this->packageName = $package_name;
+ $this->relPath = $rel_path;
+ $this->fullPath = $full_path;
+ }
+
+ /**
+ * 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->relPath;
+ }
+
+ /**
+ * 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\Component\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\Component\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\Component\Scaffold\Interpolator
+ * An interpolator for making string replacements.
+ */
+ public function getInterpolator($name_prefix = '') {
+ $interpolator = new Interpolator();
+ $this->addInterpolationData($interpolator, $name_prefix);
+ return $interpolator;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/ScaffoldOptions.php b/core/lib/Drupal/Component/Scaffold/ScaffoldOptions.php
new file mode 100644
index 0000000000..24ea6ff410
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/ScaffoldOptions.php
@@ -0,0 +1,243 @@
+options = $options + [
+ "allowed-packages" => [],
+ "locations" => [],
+ "symlink" => FALSE,
+ "file-mapping" => [],
+ ];
+ }
+
+ /**
+ * 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('composer-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['composer-scaffold'] : [];
+ return new self($options);
+ }
+
+ /**
+ * Creates a scaffold option object with default values.
+ *
+ * @return self
+ * A scaffold options object with default values
+ */
+ public static function defaultOptions() {
+ return new self([]);
+ }
+
+ /**
+ * 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.
+ * @param string $default
+ * The value to return if the requested location is not defined.
+ *
+ * @return string
+ * The value of the provided named location
+ */
+ public function getLocation($name, $default = '') {
+ return $this->hasLocation($name) ? $this->locations()[$name] : $default;
+ }
+
+ /**
+ * Returns the value of a specific named location, or throw.
+ *
+ * @param string $name
+ * The name of the location to fetch.
+ * @param string $message
+ * The message to pass into the exception if the requested location
+ * does not exist. Optional; a default message is generated if needed.
+ *
+ * @return string
+ * The value of the provided named location
+ */
+ public function requiredLocation($name, $message = '') {
+ if (!$this->hasLocation($name)) {
+ throw new \RuntimeException($this->requiredLocationErrorMessage($name, $message));
+ }
+ return $this->getLocation($name);
+ }
+
+ /**
+ * Returns the error message to use when a required location was not specified.
+ *
+ * @param string $name
+ * The name of the location being fetched.
+ * @param string $message
+ * The caller-provided error message, or empty if none provided.
+ * @return string
+ * The error message to use when the location cannot be found.
+ */
+ protected function requiredLocationErrorMessage($name, $message) {
+ if (!empty($message)) {
+ return $message;
+ }
+ return "The extra.composer-scaffold.location.$name is not set in the project's composer.json.";
+ }
+
+ /**
+ * 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;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Scaffold/TESTING.txt b/core/lib/Drupal/Component/Scaffold/TESTING.txt
new file mode 100644
index 0000000000..3186f0f901
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/TESTING.txt
@@ -0,0 +1,18 @@
+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/Component.
+
+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\Component 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
diff --git a/core/lib/Drupal/Component/Scaffold/composer.json b/core/lib/Drupal/Component/Scaffold/composer.json
new file mode 100644
index 0000000000..1fd028e116
--- /dev/null
+++ b/core/lib/Drupal/Component/Scaffold/composer.json
@@ -0,0 +1,31 @@
+{
+ "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": "^1.0.0",
+ "php": ">=7.0.8"
+ },
+ "autoload": {
+ "psr-4": {
+ "Drupal\\Component\\Scaffold\\": ""
+ }
+ },
+ "extra": {
+ "class": "Drupal\\Component\\Scaffold\\Plugin",
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "require-dev": {
+ "composer/composer": "^1.8@stable",
+ "drupal/coder": "^8.3.2",
+ "phpunit/phpunit": "^4.8.35 || ^6.5"
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/AssertUtilsTrait.php b/core/tests/Drupal/Tests/Component/Scaffold/AssertUtilsTrait.php
new file mode 100644
index 0000000000..39d0165c5b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/AssertUtilsTrait.php
@@ -0,0 +1,20 @@
+assertFileExists($path);
+ $contents = file_get_contents($path);
+ $this->assertRegExp($contents_contains, basename($path) . ': ' . $contents);
+ $this->assertSame($is_link, is_link($path));
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/ExecTrait.php b/core/tests/Drupal/Tests/Component/Scaffold/ExecTrait.php
new file mode 100644
index 0000000000..14c44ee6f4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/ExecTrait.php
@@ -0,0 +1,36 @@
+ getenv('PATH'), 'HOME' => getenv('HOME')]);
+ $process->inheritEnvironmentVariables();
+ $process->setTimeout(300)->setIdleTimeout(300)->run();
+ $exitCode = $process->getExitCode();
+ if (0 != $exitCode) {
+ throw new \Exception("Exit code: {$exitCode}\n\n" . $process->getErrorOutput() . "\n\n" . $process->getOutput());
+ }
+ return [$process->getOutput(), $process->getErrorOutput()];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Fixtures.php b/core/tests/Drupal/Tests/Component/Scaffold/Fixtures.php
new file mode 100644
index 0000000000..86dab883e1
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Fixtures.php
@@ -0,0 +1,329 @@
+io) {
+ $this->io = new BufferIO();
+ }
+ return $this->io;
+ }
+
+ /**
+ * Get the Composer object.
+ *
+ * @return \Composer\Composer
+ * The main Composer object, needed by the scaffold Handler, etc.
+ */
+ public function getComposer() {
+ if (!$this->composer) {
+ $this->composer = Factory::create($this->io(), NULL, TRUE);
+ }
+ return $this->composer;
+ }
+
+ /**
+ * Get the output from our io() fixture.
+ *
+ * @return string
+ * Output captured from tests that write to Fixtures::io().
+ */
+ public function getOutput() {
+ return $this->io()->getOutput();
+ }
+
+ /**
+ * Return the path to this project so that it may be injected into composer.json files.
+ *
+ * @return string
+ * Path to the root of this project.
+ */
+ public function projectRoot() {
+ return realpath(__DIR__) . '/../../../../../../core/lib/Drupal/Component/Scaffold';
+ }
+
+ /**
+ * Return the path to the project fixtures.
+ *
+ * @return string
+ * Path to project fixtures
+ */
+ public function allFixturesDir() {
+ return realpath(__DIR__ . '/fixtures');
+ }
+
+ /**
+ * Return the path to one particular project fixture.
+ *
+ * @return string
+ * Path to project fixture
+ */
+ public function projectFixtureDir($project_name) {
+ $dir = $this->allFixturesDir() . '/' . $project_name;
+ if (!is_dir($dir)) {
+ throw new \Exception("Requested fixture project {$project_name} that does not exist.");
+ }
+ return $dir;
+ }
+
+ /**
+ * Return the path to one particular bin path.
+ *
+ * @return string
+ * Path to project fixture
+ */
+ public function binFixtureDir($bin_name) {
+ $dir = $this->allFixturesDir() . '/scripts/' . $bin_name;
+ if (!is_dir($dir)) {
+ throw new \Exception("Requested fixture bin dir {$bin_name} that does not exist.");
+ }
+ return $dir;
+ }
+
+ /**
+ * Use in place of ScaffoldFilePath::sourcePath to get a path to a source scaffold fixture.
+ *
+ * @param string $project_name
+ * The name of the project to fetch; $package_name is "fixtures/$project_name".
+ * @param string $source
+ * The name of the asset; path is "assets/$source".
+ * @param string $destination
+ * The path to the destination; only used in error messages, not needed for most tests.
+ *
+ * @return \Drupal\Component\Scaffold\ScaffoldFilePath
+ * The full and relative path to the desired asset
+ */
+ public function sourcePath($project_name, $source, $destination = 'unknown') {
+ $package_name = "fixtures/{$project_name}";
+ $source_rel_path = "assets/{$source}";
+ $package_path = $this->projectFixtureDir($project_name);
+ $destination = 'unknown';
+ return ScaffoldFilePath::sourcePath($package_name, $package_path, $destination, $source_rel_path);
+ }
+
+ /**
+ * Use in place of Handler::getLocationReplacements() to obtain a 'web-root'.
+ *
+ * @return \Drupal\Component\Scaffold\Interpolator
+ * An interpolator with location replacements, including 'web-root'.
+ */
+ public function getLocationReplacements() {
+ $destinationTmpDir = $this->mkTmpDir();
+ $interpolator = new Interpolator();
+ $interpolator->setData(['web-root' => $destinationTmpDir, 'package-name' => 'fixtures/tmp-destination']);
+ return $interpolator;
+ }
+
+ /**
+ * Use to create a ReplaceOp fixture.
+ *
+ * @param string $project_name
+ * The name of the project to fetch; $package_name is "fixtures/$project_name".
+ * @param string $source
+ * The name of the asset; path is "assets/$source".
+ *
+ * @return \Drupal\Component\Scaffold\Operations\ReplaceOp
+ * A replace operation object.
+ */
+ public function replaceOp($project_name, $source) {
+ $source_path = $this->sourcePath($project_name, $source);
+ return new ReplaceOp($source_path, TRUE);
+ }
+
+ /**
+ * Use to create an AppendOp fixture.
+ *
+ * @param string $project_name
+ * The name of the project to fetch; $package_name is "fixtures/$project_name".
+ * @param string $source
+ * The name of the asset; path is "assets/$source".
+ *
+ * @return \Drupal\Component\Scaffold\Operations\AppendOp
+ * An append opperation object.
+ */
+ public function appendOp($project_name, $source) {
+ $source_path = $this->sourcePath($project_name, $source);
+ return new AppendOp(NULL, $source_path);
+ }
+
+ /**
+ * Use in place of ScaffoldFilePath::destinationPath to get a destination path in a tmp dir.
+ *
+ * @param string $destination
+ * Destination path; should be in the form '[web-root]/robots.txt', where
+ * '[web-root]' is always literally '[web-root]', with any arbitrarily
+ * desired filename following.
+ * @param \Drupal\Component\Scaffold\Interpolator $interpolator
+ * Location replacements. Obtain via Fixtures::getLocationReplacements()
+ * when creating multiple scaffold destinations.
+ * @param string $package_name
+ * The name of the fixture package that this path came from. Optional;
+ * taken from interpolator if not provided.
+ *
+ * @return \Drupal\Component\Scaffold\ScaffoldFilePath
+ * A destination scaffold file backed by temporary storage.
+ */
+ public function destinationPath($destination, Interpolator $interpolator = NULL, $package_name = NULL) {
+ $interpolator = $interpolator ?: $this->getLocationReplacements();
+ $package_name = $package_name ?: $interpolator->interpolate('[package-name]');
+ return ScaffoldFilePath::destinationPath($package_name, $destination, $interpolator);
+ }
+
+ /**
+ * Generate a path to a temporary location, but do not create the directory.
+ *
+ * @param string $extraSalt
+ * Extra characters to throw into the md5 to add to name.
+ *
+ * @return string
+ * Path to temporary directory
+ */
+ public function tmpDir($extraSalt = '') {
+ $tmpDir = sys_get_temp_dir() . '/composer-scaffold-test-' . md5($extraSalt . microtime());
+ $this->tmpDirs[] = $tmpDir;
+ return $tmpDir;
+ }
+
+ /**
+ * Create a temporary directory.
+ *
+ * @param string $extraSalt
+ * Extra characters to throw into the md5 to add to name.
+ *
+ * @return string
+ * Path to temporary directory
+ */
+ public function mkTmpDir($extraSalt = '') {
+ $tmpDir = $this->tmpDir($extraSalt);
+ $filesystem = new Filesystem();
+ $filesystem->ensureDirectoryExists($tmpDir);
+ return $tmpDir;
+ }
+
+ /**
+ * Call 'tearDown' in any test that copies fixtures to transient locations.
+ */
+ public function tearDown() {
+ // Remove any temporary directories that were created.
+ $filesystem = new Filesystem();
+ foreach ($this->tmpDirs as $dir) {
+ $filesystem->remove($dir);
+ }
+ // Clear out variables from the previous pass.
+ $this->tmpDirs = [];
+ $this->fixturesDir = NULL;
+ $this->io = NULL;
+ }
+
+ /**
+ * Create a temporary copy of all of the fixtures projects into a temp dir.
+ *
+ * The fixtures remain dirty if they already exist. Individual tests should
+ * first delete any fixture directory that needs to remain pristine. Since
+ * all temporary directories are removed in tearDown, this is only an issue
+ * when a) the FIXTURE_DIR environment variable has been set, or b) tests
+ * are calling cloneFixtureProjects more than once per test method.
+ *
+ * @param string $fixturesDir
+ * The directory to place fixtures in.
+ * @param array $replacements
+ * Key : value mappings for placeholders to replace in composer.json templates.
+ */
+ public function cloneFixtureProjects($fixturesDir, array $replacements = []) {
+ $filesystem = new Filesystem();
+ $replacements += ['SYMLINK' => 'true'];
+ $interpolator = new Interpolator('__', '__', TRUE);
+ $interpolator->setData($replacements);
+ $filesystem->copy($this->allFixturesDir(), $fixturesDir);
+ $composer_json_templates = glob($fixturesDir . "/*/composer.json.tmpl");
+ foreach ($composer_json_templates as $composer_json_tmpl) {
+ // Inject replacements into composer.json.
+ if (file_exists($composer_json_tmpl)) {
+ $composer_json_contents = file_get_contents($composer_json_tmpl);
+ $composer_json_contents = $interpolator->interpolate($composer_json_contents, [], FALSE);
+ file_put_contents(dirname($composer_json_tmpl) . "/composer.json", $composer_json_contents);
+ @unlink($composer_json_tmpl);
+ }
+ }
+ }
+
+ /**
+ * Run the scaffold operation.
+ *
+ * This is equivalent to running `composer composer-scaffold`, but we
+ * do the equivalent operation by instantiating a Handler object in order
+ * to continue running in the same process, so that coverage may be
+ * calculated for the code executed by these tests.
+ */
+ public function runScaffold($cwd) {
+ chdir($cwd);
+ $handler = new Handler($this->getComposer(), $this->io());
+ $handler->scaffold();
+ return $this->getOutput();
+ }
+
+ /**
+ * Runs a `composer` command.
+ *
+ * @param string $cmd
+ * The Composer command to execute (escaped as required)
+ * @param string $cwd
+ * The current working directory to run the command from.
+ * @param int $expectedExitCode
+ * The expected exit code; will throw if a different exit code is returned.
+ *
+ * @return array
+ * Standard output and standard error from the command
+ */
+ public function runComposer($cmd, $cwd, $expectedExitCode = 0) {
+ chdir($cwd);
+ $input = new StringInput($cmd);
+ $output = new BufferedOutput();
+ $application = new Application();
+ $application->setAutoExit(FALSE);
+ try {
+ $exitCode = $application->run($input, $output);
+ }
+ catch (\Exception $e) {
+ print "Exception: " . $e->getMessage() . "\n";
+ }
+ if ($exitCode != $expectedExitCode) {
+ print "Command '{$cmd}' - Expected exit code: {$expectedExitCode}, actual exit code: {$exitCode}\n";
+ }
+ $output = $output->fetch();
+ return $output;
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Functional/ComposerHookTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ComposerHookTest.php
new file mode 100644
index 0000000000..12fd9f1166
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ComposerHookTest.php
@@ -0,0 +1,138 @@
+fileSystem = new Filesystem();
+ $this->fixtures = new Fixtures();
+ $this->projectRoot = $this->fixtures->projectRoot();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown() {
+ // Remove any temporary directories et. al. that were created.
+ $this->fixtures->tearDown();
+ }
+
+ /**
+ * Test to see if scaffold operation runs at the correct times.
+ */
+ public function testComposerHooks() {
+ $this->fixturesDir = $this->fixtures->tmpDir($this->getName());
+ $is_link = FALSE;
+ $replacements = ['SYMLINK' => $is_link ? 'true' : 'false', 'PROJECT_ROOT' => $this->projectRoot];
+ $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
+ $topLevelProjectDir = 'composer-hooks-fixture';
+ $sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+ // First test: run composer install. This is the same as composer update
+ // since there is no lock file. Ensure that scaffold operation ran.
+ $this->execComposer("install --no-ansi", $sut);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#Test version of default.settings.php from drupal/core#');
+ // Run composer required to add in the scaffold-override-fixture. This
+ // project is "allowed" in our main fixture project, but not required.
+ // We expect that requiring this library should re-scaffold, resulting
+ // in a changed default.settings.php file.
+ list($stdout, $stderr) = $this->execComposer("require --no-ansi --no-interaction fixtures/scaffold-override-fixture:dev-master", $sut);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#');
+ // Make sure that the appropriate notice informing us that scaffolding
+ // is allowed was printed.
+ $this->assertContains('Package fixtures/scaffold-override-fixture has scaffold operations, and is already allowed in the root-level composer.json file.', $stdout);
+ // Delete one scaffold file, just for test purposes, then run
+ // 'composer update' and see if the scaffold file is replaced.
+ @unlink($sut . '/sites/default/default.settings.php');
+ $this->execComposer("update --no-ansi", $sut);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#');
+ // Delete the same test scaffold file again, then run
+ // 'composer composer:scaffold' and see if the scaffold file is replaced.
+ @unlink($sut . '/sites/default/default.settings.php');
+ $this->execComposer("composer:scaffold --no-ansi", $sut);
+ $this->assertScaffoldedFile($sut . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#');
+ // Run 'composer create-project' to create a new test project called
+ // 'create-project-test', which is a copy of 'fixtures/drupal-drupal'.
+ $packages = $this->fixturesDir . '/packages.json';
+ $sut = $this->fixturesDir . '/create-project-test';
+ $filesystem = new Filesystem();
+ $filesystem->remove($sut);
+ list($stdout, $stderr) = $this->execComposer("create-project --repository=packages.json fixtures/drupal-drupal {$sut}", $this->fixturesDir, ['COMPOSER_MIRROR_PATH_REPOS' => 1]);
+ $this->assertDirectoryExists($sut);
+ $this->assertContains('Scaffolding files for fixtures/drupal-drupal', $stdout);
+ $this->assertScaffoldedFile($sut . '/index.php', FALSE, '#Test version of index.php from drupal/core#');
+ $topLevelProjectDir = 'composer-hooks-nothing-allowed-fixture';
+ $sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+ // Run composer install on an empty project.
+ $this->execComposer("install --no-ansi", $sut);
+ // Require a project that is not allowed to scaffold and confirm that we
+ // get a warning, and it does not scaffold.
+ list($stdout, $stderr) = $this->execComposer("require --no-ansi --no-interaction fixtures/scaffold-override-fixture:dev-master", $sut);
+ $this->assertFileNotExists($sut . '/sites/default/default.settings.php');
+ $this->assertContains('Package fixtures/scaffold-override-fixture has scaffold operations, but it is not allowed in the root-level composer.json file.', $stdout);
+ }
+
+ /**
+ * Runs a `composer` command.
+ *
+ * @param string $cmd
+ * The Composer command to execute (escaped as required)
+ * @param string $cwd
+ * The current working directory to run the command from.
+ * @param array $env
+ * Environment variables to define for the subprocess.
+ *
+ * @return array
+ * Standard output and standard error from the command
+ */
+ protected function execComposer($cmd, $cwd, array $env = []) {
+ return $this->mustExec("composer {$cmd}", $cwd, $env);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Functional/ManageGitIgnoreTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ManageGitIgnoreTest.php
new file mode 100644
index 0000000000..149063c58a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ManageGitIgnoreTest.php
@@ -0,0 +1,172 @@
+fileSystem = new Filesystem();
+ $this->fixtures = new Fixtures();
+ $this->projectRoot = $this->fixtures->projectRoot();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown() {
+ // Remove any temporary directories et. al. that were created.
+ $this->fixtures->tearDown();
+ }
+
+ /**
+ * Create a system-under-test and initialize a git repository for it.
+ */
+ protected function createSutWithGit($topLevelProjectDir) {
+ $is_link = FALSE;
+ $this->fixturesDir = $this->fixtures->tmpDir($this->getName());
+ $sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+ $replacements = ['SYMLINK' => $is_link ? 'true' : 'false', 'PROJECT_ROOT' => $this->projectRoot];
+ $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
+ // .gitignore files will not be managed unless there is a git repository.
+ $this->mustExec('git init', $sut);
+ // Add some user info so git does not complain.
+ $this->mustExec('git config user.email "test@example.com"', $sut);
+ $this->mustExec('git config user.name "Test User"', $sut);
+ $this->mustExec('git add .', $sut);
+ $this->mustExec('git commit -m "Initial commit."', $sut);
+ // Run composer install, but supress scaffolding.
+ $this->fixtures->runComposer("install --no-ansi --no-scripts", $sut);
+ return $sut;
+ }
+
+ /**
+ * Test to see if the scaffold operation correctly manages the .gitignore file.
+ */
+ public function testManageGitIgnore() {
+ // Note that the drupal-composer-drupal-project fixture does not
+ // have any configuration settings related to .gitignore management.
+ $sut = $this->createSutWithGit('drupal-composer-drupal-project');
+ $this->assertFileNotExists($sut . '/docroot/index.php');
+ $this->assertFileNotExists($sut . '/docroot/sites/.gitignore');
+ // Run the scaffold command.
+ $this->fixtures->runScaffold($sut);
+ $this->assertFileExists($sut . '/docroot/index.php');
+ $expected = <<assertScaffoldedFile($sut . '/docroot/.gitignore', FALSE, '#' . $expected . '#msi');
+ $this->assertScaffoldedFile($sut . '/docroot/sites/.gitignore', FALSE, '#example.settings.local.php#');
+ $this->assertScaffoldedFile($sut . '/docroot/sites/default/.gitignore', FALSE, '#default.services.yml#');
+ $expected = <<mustExec('git status --porcelain', $sut);
+ $this->assertEquals(trim($expected), trim($stdout));
+ }
+
+ /**
+ * Test to see if scaffold operation does not manage the .gitignore file when disabled.
+ */
+ public function testUnmanagedGitIgnoreWhenDisabled() {
+ // Note that the drupal-drupal fixture has a configuration setting
+ // `"gitignore": false,` which disables .gitignore file handling.
+ $sut = $this->createSutWithGit('drupal-drupal');
+ $this->assertFileNotExists($sut . '/docroot/index.php');
+ // Run the scaffold command.
+ $this->fixtures->runScaffold($sut);
+ $this->assertFileExists($sut . '/index.php');
+ $this->assertFileNotExists($sut . '/.gitignore');
+ $this->assertFileNotExists($sut . '/docroot/sites/default/.gitignore');
+ }
+
+ /**
+ * Test to see if the scaffold operation disables .gitignore management when git not present.
+ *
+ * The scaffold operation should still succeed if there is no 'git' executable.
+ */
+ public function testUnmanagedGitIgnoreWhenGitNotAvailable() {
+ // Note that the drupal-composer-drupal-project fixture does not
+ // have any configuration settings related to .gitignore management.
+ $sut = $this->createSutWithGit('drupal-composer-drupal-project');
+ $this->assertFileNotExists($sut . '/docroot/sites/default/.gitignore');
+ $this->assertFileNotExists($sut . '/docroot/index.php');
+ $this->assertFileNotExists($sut . '/docroot/sites/.gitignore');
+ // Confirm that 'git' is available (n.b. if it were not, createSutWithGit() would fail).
+ exec('git --help', $output, $status);
+ $this->assertEquals(0, $status);
+ // Modify our $PATH so that it begins with a path that contains an executable
+ // script named 'git' that always exits with 127, as if git were not found.
+ // Note that we run our tests using process isolation, so we do not need to
+ // restore the PATH when we are done.
+ $unavailableGitPath = $this->fixtures->binFixtureDir('disable-git-bin');
+ chmod($unavailableGitPath . '/git', 0755);
+ putenv('PATH=' . $unavailableGitPath . ':' . getenv('PATH'));
+ // Confirm that 'git' is no longer available.
+ exec('git --help', $output, $status);
+ $this->assertEquals(127, $status);
+ // Run the scaffold command.
+ exec('composer composer:scaffold', $output, $status);
+ $this->assertFileExists($sut . '/docroot/index.php');
+ $this->assertFileNotExists($sut . '/docroot/sites/default/.gitignore');
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Functional/ScaffoldTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ScaffoldTest.php
new file mode 100644
index 0000000000..ec6eae0c28
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Functional/ScaffoldTest.php
@@ -0,0 +1,301 @@
+fileSystem = new Filesystem();
+ $this->fixtures = new Fixtures();
+ $this->projectRoot = $this->fixtures->projectRoot();
+ $this->fixturesDir = getenv(self::FIXTURE_DIR);
+ if (!$this->fixturesDir) {
+ $this->fixturesDir = $this->fixtures->tmpDir($this->getName());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown() {
+ // Remove any temporary directories et. al. that were created.
+ $this->fixtures->tearDown();
+ }
+
+ /**
+ * Create the System-Under-Test.
+ */
+ protected function createSut($topLevelProjectDir, $replacements = []) {
+ $this->sut = $this->fixturesDir . '/' . $topLevelProjectDir;
+ // Erase just our sut, to ensure it is clean. Recopy all of the fixtures.
+ $this->fileSystem->remove($this->sut);
+ $replacements += ['PROJECT_ROOT' => $this->projectRoot];
+ $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
+ return $this->sut;
+ }
+
+ /**
+ * Data provider for testComposerInstallScaffold and testScaffoldCommand.
+ */
+ public function scaffoldFixturesWithErrorConditionsTestValues() {
+ return [
+ [
+ 'drupal-drupal-missing-scaffold-file',
+ 'Scaffold file assets/missing-robots-default.txt not found in package fixtures/drupal-drupal-missing-scaffold-file.',
+ TRUE,
+ ],
+ ];
+ }
+
+ /**
+ * Tests that scaffold files throw when they have bad values.
+ *
+ * @dataProvider scaffoldFixturesWithErrorConditionsTestValues
+ */
+ public function testScaffoldFixturesWithErrorConditions($topLevelProjectDir, $expectedExceptionMessage, $is_link) {
+ $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => $is_link ? 'true' : 'false']);
+ // Run composer install to get the dependencies we need to test.
+ $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut);
+ // Test scaffold. Expect an error.
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+ $this->fixtures->runScaffold($sut);
+ }
+
+ /**
+ * Data provider for testComposerInstallScaffold and testScaffoldCommand.
+ */
+ public function scaffoldTestValues() {
+ return [
+ [
+ 'drupal-composer-drupal-project',
+ 'assertDrupalProjectSutWasScaffolded', TRUE,
+ ],
+ [
+ 'drupal-drupal',
+ 'assertDrupalDrupalSutWasScaffolded',
+ FALSE,
+ ],
+ [
+ 'drupal-drupal-test-overwrite',
+ 'assertDrupalDrupalFileWasReplaced',
+ FALSE,
+ ],
+ [
+ 'drupal-drupal-test-append',
+ 'assertDrupalDrupalFileWasAppended',
+ FALSE,
+ ],
+ [
+ 'drupal-drupal-test-append',
+ 'assertDrupalDrupalFileWasAppended',
+ TRUE,
+ ],
+ ];
+ }
+
+ /**
+ * Tests that scaffold files are correctly moved.
+ *
+ * @dataProvider scaffoldTestValues
+ */
+ public function testScaffold($topLevelProjectDir, $scaffoldAssertions, $is_link) {
+ $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => $is_link ? 'true' : 'false']);
+ // Run composer install to get the dependencies we need to test.
+ $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut);
+ // Test composer:scaffold.
+ $scaffoldOutput = $this->fixtures->runScaffold($sut);
+ // @todo We could assert that $scaffoldOutput must contain some expected text
+ call_user_func([$this, $scaffoldAssertions], $sut, $is_link, $topLevelProjectDir);
+ }
+
+ /**
+ * Try to scaffold a project that does not scaffold anything.
+ */
+ public function testEmptyProject() {
+ $topLevelProjectDir = 'empty-fixture';
+ $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']);
+ // Run composer install to get the dependencies we need to test.
+ $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut);
+ // Test composer:scaffold.
+ $scaffoldOutput = $this->fixtures->runScaffold($sut);
+ $this->assertContains('Nothing scaffolded because no packages are allowed in the top-level composer.json file', $scaffoldOutput);
+ }
+
+ /**
+ * Try to scaffold a project that allows a project with no scaffold files.
+ */
+ public function testProjectThatScaffoldsEmptyProject() {
+ $topLevelProjectDir = 'project-allowing-empty-fixture';
+ $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']);
+ // Run composer install to get the dependencies we need to test.
+ $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut);
+ // Test composer:scaffold.
+ $scaffoldOutput = $this->fixtures->runScaffold($sut);
+ $this->assertContains('The allowed package fixtures/empty-fixture does not provide a file mapping for Composer Scaffold', $scaffoldOutput);
+ $docroot = $sut;
+ $this->assertCommonDrupalAssetsWereScaffolded($docroot, FALSE, $topLevelProjectDir);
+ }
+
+ /**
+ * Try to scaffold a project that attempts to scaffold a file with no path.
+ */
+ public function testProjectWithEmptyScaffoldPath() {
+ $topLevelProjectDir = 'project-with-empty-scaffold-path';
+ $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']);
+ // Run composer install to get the dependencies we need to test.
+ $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut);
+ // Test composer:scaffold.
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('No scaffold file path given for [web-root]/my-error in package fixtures/project-with-empty-scaffold-path');
+ $this->fixtures->runScaffold($sut);
+ }
+
+ /**
+ * Try to scaffold a project that attempts to scaffold a directory.
+ */
+ public function testProjectWithIllegalDirScaffold() {
+ $topLevelProjectDir = 'project-with-illegal-dir-scaffold';
+ $sut = $this->createSut($topLevelProjectDir, ['SYMLINK' => 'false']);
+ // Run composer install to get the dependencies we need to test.
+ $this->fixtures->runComposer("install --no-ansi --no-scripts", $this->sut);
+ // Test composer:scaffold.
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Scaffold file assets in package fixtures/project-with-illegal-dir-scaffold is a directory; only files may be scaffolded');
+ $this->fixtures->runScaffold($sut);
+ }
+
+ /**
+ * Asserts that the drupal/assets scaffold files correct for drupal/project layout.
+ */
+ protected function assertDrupalProjectSutWasScaffolded($sut, $is_link, $project_name) {
+ $docroot = $sut . '/docroot';
+ $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name);
+ $this->assertDefaultSettingsFromScaffoldOverride($docroot, $is_link);
+ $this->assertHtaccessExcluded($docroot);
+ }
+
+ /**
+ * Asserts that the drupal/assets scaffold files correct for drupal/drupal layout.
+ */
+ protected function assertDrupalDrupalSutWasScaffolded($sut, $is_link, $project_name) {
+ $docroot = $sut;
+ $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name);
+ $this->assertDefaultSettingsFromScaffoldOverride($docroot, $is_link);
+ $this->assertHtaccessExcluded($docroot);
+ }
+
+ /**
+ * Ensure that the default settings file was overridden by the test.
+ */
+ protected function assertDefaultSettingsFromScaffoldOverride($docroot, $is_link) {
+ $this->assertScaffoldedFile($docroot . '/sites/default/default.settings.php', $is_link, '#scaffolded from the scaffold-override-fixture#');
+ }
+
+ /**
+ * Ensure that the .htaccess file was excluded by the test.
+ */
+ protected function assertHtaccessExcluded($docroot) {
+ // Ensure that the .htaccess.txt file was not written, as our
+ // top-level composer.json excludes it from the files to scaffold.
+ $this->assertFileNotExists($docroot . '/.htaccess');
+ }
+
+ /**
+ * Assert that the appropriate file was replaced.
+ *
+ * Check the drupal/drupal-based project to confirm that the expected file was
+ * replaced, and that files that were not supposed to be replaced remain
+ * unchanged.
+ */
+ protected function assertDrupalDrupalFileWasReplaced($sut, $is_link, $project_name) {
+ $docroot = $sut;
+ $this->assertScaffoldedFile($docroot . '/replace-me.txt', $is_link, '#from assets that replaces file#');
+ $this->assertScaffoldedFile($docroot . '/keep-me.txt', $is_link, '#File in drupal-drupal-test-overwrite that is not replaced#');
+ $this->assertScaffoldedFile($docroot . '/make-me.txt', $is_link, '#from assets that replaces file#');
+ $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name);
+ $this->assertScaffoldedFile($docroot . '/robots.txt', $is_link, "#{$project_name}#");
+ }
+
+ /**
+ * Confirm that the robots.txt file was prepended / appended as stipulated in the test.
+ */
+ protected function assertDrupalDrupalFileWasAppended($sut, $is_link, $project_name) {
+ $docroot = $sut;
+ $this->assertScaffoldedFile($docroot . '/robots.txt', FALSE, '#in drupal-drupal-test-append composer.json fixture.*This content is prepended to the top of the existing robots.txt fixture.*Test version of robots.txt from drupal/core.*This content is appended to the bottom of the existing robots.txt fixture.*in drupal-drupal-test-append composer.json fixture#ms');
+ $this->assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name);
+ }
+
+ /**
+ * Assert that the scaffold files from drupal/assets are placed as we expect them to be.
+ *
+ * This tests that all assets from drupal/assets were scaffolded, save
+ * for .htaccess, robots.txt and default.settings.php, which are scaffolded
+ * in different ways in different tests.
+ */
+ protected function assertCommonDrupalAssetsWereScaffolded($docroot, $is_link, $project_name) {
+ $from_project = "#scaffolded from \"file-mappings\" in {$project_name} composer.json fixture#";
+ $from_core = '#from drupal/core#';
+ // Ensure that the autoload.php file was written.
+ $this->assertFileExists($docroot . '/autoload.php');
+ // Assert other scaffold files are written in the correct locations.
+ $this->assertScaffoldedFile($docroot . '/.csslintrc', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.editorconfig', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.eslintignore', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.eslintrc.json', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.gitattributes', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/.ht.router.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/sites/default/default.services.yml', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/sites/example.settings.local.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/sites/example.sites.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/index.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/update.php', $is_link, $from_core);
+ $this->assertScaffoldedFile($docroot . '/web.config', $is_link, $from_core);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/AppendOpTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/AppendOpTest.php
new file mode 100644
index 0000000000..a1119a262e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/AppendOpTest.php
@@ -0,0 +1,57 @@
+destinationPath('[web-root]/robots.txt');
+ $source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt');
+ $options = ScaffoldOptions::defaultOptions();
+ $originalOp = new ReplaceOp($source, TRUE);
+ $prepend = $fixtures->sourcePath('drupal-drupal-test-append', 'prepend-to-robots.txt');
+ $append = $fixtures->sourcePath('drupal-drupal-test-append', 'append-to-robots.txt');
+ $sut = new AppendOp($prepend, $append);
+ // Assert that there is no target file before we run our test.
+ $this->assertFileNotExists($destination->fullPath());
+ // Test the system under test.
+ $sut->preprocess($originalOp, $destination, $fixtures->io(), $options);
+ // Assert that the target file was created.
+ $this->assertFileExists($destination->fullPath());
+ // Assert the target contained the contents from the correct scaffold files.
+ $contents = trim(file_get_contents($destination->fullPath()));
+ $expected = <<assertEquals(trim($expected), $contents);
+ // Confirm that expected output was written to our io fixture.
+ $output = $fixtures->getOutput();
+ $this->assertContains('Copy [web-root]/robots.txt from assets/robots.txt', $output);
+ $this->assertContains('Prepend to [web-root]/robots.txt from assets/prepend-to-robots.txt', $output);
+ $this->assertContains('Append to [web-root]/robots.txt from assets/append-to-robots.txt', $output);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/OperationCollectionTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/OperationCollectionTest.php
new file mode 100644
index 0000000000..e638ac7b2d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/OperationCollectionTest.php
@@ -0,0 +1,108 @@
+getLocationReplacements();
+ $file_mappings = [
+ 'fixtures/drupal-assets-fixture' => [
+ '[web-root]/index.php' => $fixtures->replaceOp('drupal-assets-fixture', 'index.php'),
+ '[web-root]/.htaccess' => $fixtures->replaceOp('drupal-assets-fixture', '.htaccess'),
+ '[web-root]/robots.txt' => $fixtures->replaceOp('drupal-assets-fixture', 'robots.txt'),
+ '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-assets-fixture', 'default.services.yml'),
+ ],
+ 'fixtures/drupal-profile' => [
+ '[web-root]/sites/default/default.services.yml' => $fixtures->replaceOp('drupal-profile', 'profile.default.services.yml'),
+ ],
+ 'fixtures/drupal-drupal' => [
+ '[web-root]/.htaccess' => new SkipOp(),
+ '[web-root]/robots.txt' => $fixtures->appendOp('drupal-drupal-test-append', 'append-to-robots.txt'),
+ ],
+ ];
+ $sut = new OperationCollection($fixtures->io());
+ // Test the system under test.
+ list($scaffold_list, $resolved_file_mappings) = $this->callProtected($sut, 'collateScaffoldFiles', [$file_mappings, $locationReplacements]);
+ // Confirm that the keys of the output are the same as the keys of the input.
+ $this->assertEquals(array_keys($file_mappings), array_keys($resolved_file_mappings));
+ // Also assert that we have the right ScaffoldFileInfo objects in the destination.
+ $this->assertResolvedToSameOp('fixtures/drupal-assets-fixture', '[web-root]/index.php', $file_mappings, $scaffold_list, $resolved_file_mappings);
+ $this->assertResolvedToSameOp('fixtures/drupal-profile', '[web-root]/sites/default/default.services.yml', $file_mappings, $scaffold_list, $resolved_file_mappings);
+ $this->assertResolvedToSameOp('fixtures/drupal-drupal', '[web-root]/robots.txt', $file_mappings, $scaffold_list, $resolved_file_mappings);
+ // Assert that the files below have been overridden.
+ $this->assertOverridden('fixtures/drupal-assets-fixture', '[web-root]/.htaccess', $scaffold_list, $resolved_file_mappings);
+ $this->assertOverridden('fixtures/drupal-assets-fixture', '[web-root]/robots.txt', $scaffold_list, $resolved_file_mappings);
+ }
+
+ /**
+ * Check to see if a given file was not overridden.
+ *
+ * The package name in the scaffold list for the provided destination should
+ * match the package name from the specified project.
+ */
+ protected function assertResolvedToSameOp($project, $dest, $file_mappings, $scaffold_list, $resolved_file_mappings) {
+ $resolved_file_info = $resolved_file_mappings[$project][$dest];
+ $this->assertEquals(get_class($resolved_file_info), ScaffoldFileInfo::class);
+ $resolved_scaffold_op = $resolved_file_info->op();
+ // If this is an append op then it will be overridden by a preprocess op
+ $expected = get_class($file_mappings[$project][$dest]);
+ if ($expected == AppendOp::class) {
+ $this->assertEquals(PreprocessOp::class, get_class($resolved_scaffold_op));
+ }
+ else {
+ $this->assertEquals($expected, get_class($resolved_scaffold_op));
+ $this->assertEquals($file_mappings[$project][$dest], $resolved_scaffold_op);
+ }
+ $this->assertEquals($project, $scaffold_list[$dest]->packageName());
+ }
+
+ /**
+ * Check if a given file was overridden.
+ *
+ * Assert that the file in the scaffold list at the specified destination
+ * comes from a different package than the one in the file info.
+ */
+ protected function assertOverridden($project, $dest, $scaffold_list, $resolved_file_mappings) {
+ $resolved_file_info = $resolved_file_mappings[$project][$dest];
+ $this->assertEquals(get_class($resolved_file_info), ScaffoldFileInfo::class);
+ $this->assertNotEquals($project, $scaffold_list[$dest]->packageName());
+ }
+
+ /**
+ * Uses reflection to call a protected method of an object.
+ *
+ * @param mixed $obj
+ * The object to inspect.
+ * @param string $methodName
+ * The name of the method to call
+ * @param array $args
+ * The arguments to pass to the protected method
+ *
+ * @return mixed
+ * The return value from the protected methos.
+ */
+ protected function callProtected($obj, $methodName, $args = []) {
+ $r = new \ReflectionMethod($obj, $methodName);
+ $r->setAccessible(TRUE);
+ return $r->invokeArgs($obj, $args);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/ReplaceOpTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/ReplaceOpTest.php
new file mode 100644
index 0000000000..ec568fc8da
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/ReplaceOpTest.php
@@ -0,0 +1,40 @@
+destinationPath('[web-root]/robots.txt');
+ $source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt');
+ $options = ScaffoldOptions::defaultOptions();
+ $sut = new ReplaceOp($source, TRUE);
+ // Assert that there is no target file before we run our test.
+ $this->assertFileNotExists($destination->fullPath());
+ // Test the system under test.
+ $sut->process($destination, $fixtures->io(), $options);
+ // Assert that the target file was created.
+ $this->assertFileExists($destination->fullPath());
+ // Assert the target contained the contents from the correct scaffold file.
+ $contents = trim(file_get_contents($destination->fullPath()));
+ $this->assertEquals('# Test version of robots.txt from drupal/core.', $contents);
+ // Confirm that expected output was written to our io fixture.
+ $output = $fixtures->getOutput();
+ $this->assertContains('Copy [web-root]/robots.txt from assets/robots.txt', $output);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/Integration/SkipOpTest.php b/core/tests/Drupal/Tests/Component/Scaffold/Integration/SkipOpTest.php
new file mode 100644
index 0000000000..fb600bd935
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/Integration/SkipOpTest.php
@@ -0,0 +1,37 @@
+destinationPath('[web-root]/robots.txt');
+ $source = $fixtures->sourcePath('drupal-assets-fixture', 'robots.txt');
+ $options = ScaffoldOptions::defaultOptions();
+ $sut = new SkipOp();
+ // Assert that there is no target file before we run our test.
+ $this->assertFileNotExists($destination->fullPath());
+ // Test the system under test.
+ $sut->process($destination, $fixtures->io(), $options);
+ // Assert that the target file was not created.
+ $this->assertFileNotExists($destination->fullPath());
+ // Confirm that expected output was written to our io fixture.
+ $output = $fixtures->getOutput();
+ $this->assertContains('Skip [web-root]/robots.txt: disabled', $output);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md
new file mode 100644
index 0000000000..e103ffba5c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/README.md
@@ -0,0 +1,34 @@
+# Fixtures README
+
+These fixtures are automatically copied to a temporary directory during test runs. After the test run, the fixtures are automatically deleted.
+
+Set the SCAFFOLD_FIXTURE_DIR environment variable to place the fixtures in a specific location rather than a temporary directory. If this is done, then the fixtures will not be deleted after the test run. This is useful for ad-hoc testing.
+
+Example:
+
+$ SCAFFOLD_FIXTURE_DIR=$HOME/tmp/scaffold-fixtures composer unit
+$ cd $HOME/tmp/scaffold-fixtures
+$ cd drupal-drupal
+$ composer composer:scaffold
+
+Scaffolding files for fixtures/drupal-assets-fixture:
+ - Link [web-root]/.csslintrc from assets/.csslintrc
+ - Link [web-root]/.editorconfig from assets/.editorconfig
+ - Link [web-root]/.eslintignore from assets/.eslintignore
+ - Link [web-root]/.eslintrc.json from assets/.eslintrc.json
+ - Link [web-root]/.gitattributes from assets/.gitattributes
+ - Link [web-root]/.ht.router.php from assets/.ht.router.php
+ - Skip [web-root]/.htaccess: overridden in my/project
+ - Link [web-root]/sites/default/default.services.yml from assets/default.services.yml
+ - Skip [web-root]/sites/default/default.settings.php: overridden in fixtures/scaffold-override-fixture
+ - Link [web-root]/sites/example.settings.local.php from assets/example.settings.local.php
+ - Link [web-root]/sites/example.sites.php from assets/example.sites.php
+ - Link [web-root]/index.php from assets/index.php
+ - Skip [web-root]/robots.txt: overridden in my/project
+ - Link [web-root]/update.php from assets/update.php
+ - Link [web-root]/web.config from assets/web.config
+Scaffolding files for fixtures/scaffold-override-fixture:
+ - Link [web-root]/sites/default/default.settings.php from assets/override-settings.php
+Scaffolding files for my/project:
+ - Skip [web-root]/.htaccess: disabled
+ - Link [web-root]/robots.txt from assets/robots-default.txt
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/assets/robots-default.txt
new file mode 100644
index 0000000000..a26bf8912f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/assets/robots-default.txt
@@ -0,0 +1 @@
+# robots.txt fixture scaffolded from "file-mappings" in composer-hooks-fixture composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl
new file mode 100644
index 0000000000..f08fa60383
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-fixture/composer.json.tmpl
@@ -0,0 +1,67 @@
+{
+ "name": "fixtures/drupal-drupal",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/robots-default.txt"
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/assets/robots-default.txt
new file mode 100644
index 0000000000..a26bf8912f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/assets/robots-default.txt
@@ -0,0 +1 @@
+# robots.txt fixture scaffolded from "file-mappings" in composer-hooks-fixture composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/composer.json.tmpl
new file mode 100644
index 0000000000..2acbea22f4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/composer-hooks-nothing-allowed-fixture/composer.json.tmpl
@@ -0,0 +1,63 @@
+{
+ "name": "fixtures/drupal-drupal",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "locations": {
+ "web-root": "./"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/robots-default.txt"
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.csslintrc b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.csslintrc
new file mode 100644
index 0000000000..f5bca65208
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.csslintrc
@@ -0,0 +1 @@
+# Test version of .csslintrc from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.editorconfig b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.editorconfig
new file mode 100644
index 0000000000..dcf0c98bbf
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.editorconfig
@@ -0,0 +1 @@
+# Test version of .editorconfig from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintignore b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintignore
new file mode 100644
index 0000000000..f0405c03ad
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintignore
@@ -0,0 +1 @@
+# Test version of .eslintignore from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintrc.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintrc.json
new file mode 100644
index 0000000000..6391fb0c6e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.eslintrc.json
@@ -0,0 +1 @@
+// Test version of .eslintrc.json from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.gitattributes b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.gitattributes
new file mode 100644
index 0000000000..03436baebc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.gitattributes
@@ -0,0 +1 @@
+# Test version of .gitattributes from drupal/core.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.ht.router.php b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.ht.router.php
new file mode 100644
index 0000000000..0cd34ad7ad
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/assets/.ht.router.php
@@ -0,0 +1,2 @@
+
+
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/composer.json
new file mode 100644
index 0000000000..cb45ec5dc4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-assets-fixture/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "fixtures/drupal-assets-fixture",
+ "extra": {
+ "composer-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"
+ }
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/.gitignore b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/.gitignore
new file mode 100644
index 0000000000..19982ea3fd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/.gitignore
@@ -0,0 +1,2 @@
+composer.lock
+vendor
\ No newline at end of file
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/assets/robots-default.txt
new file mode 100644
index 0000000000..c29217ef39
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/assets/robots-default.txt
@@ -0,0 +1 @@
+# robots.txt fixture scaffolded from "file-mappings" in drupal-composer-drupal-project composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/composer.json.tmpl
new file mode 100644
index 0000000000..207e3baa6c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/composer.json.tmpl
@@ -0,0 +1,68 @@
+{
+ "name": "fixtures/drupal-composer-drupal-project",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./docroot"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/robots-default.txt"
+ }
+ },
+ "installer-paths": {
+ "docroot/core": ["type:drupal-core"],
+ "docroot/modules/contrib/{$name}": ["type:drupal-module"],
+ "docroot/modules/custom/{$name}": ["type:drupal-custom-module"],
+ "docroot/profiles/contrib/{$name}": ["type:drupal-profile"],
+ "docroot/profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "docroot/themes/contrib/{$name}": ["type:drupal-theme"],
+ "docroot/themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "docroot/libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore
new file mode 100644
index 0000000000..c795b054e5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/.gitignore
@@ -0,0 +1 @@
+build
\ No newline at end of file
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/README.md
new file mode 100644
index 0000000000..7e59600739
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/README.md
@@ -0,0 +1 @@
+# README
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/sites/default/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/sites/default/README.md
new file mode 100644
index 0000000000..7e59600739
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-composer-drupal-project/docroot/sites/default/README.md
@@ -0,0 +1 @@
+# README
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/composer.json
new file mode 100644
index 0000000000..9aa29f55bf
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-core-fixture/composer.json
@@ -0,0 +1,13 @@
+{
+ "name": "fixtures/drupal-core-fixture",
+ "require": {
+ "fixtures/drupal-assets-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-assets-fixture"
+ ]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-missing-scaffold-file/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-missing-scaffold-file/composer.json.tmpl
new file mode 100644
index 0000000000..8d9cf57293
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-missing-scaffold-file/composer.json.tmpl
@@ -0,0 +1,68 @@
+{
+ "name": "fixtures/drupal-drupal-missing-scaffold-file",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/missing-robots-default.txt"
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/append-to-robots.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/append-to-robots.txt
new file mode 100644
index 0000000000..8f05fc3b3a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/append-to-robots.txt
@@ -0,0 +1,3 @@
+# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+# This content is appended to the bottom of the existing robots.txt fixture.
+# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/prepend-to-robots.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/prepend-to-robots.txt
new file mode 100644
index 0000000000..995c204a6e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/assets/prepend-to-robots.txt
@@ -0,0 +1,3 @@
+# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-append composer.json fixture.
+# This content is prepended to the top of the existing robots.txt fixture.
+# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/composer.json.tmpl
new file mode 100644
index 0000000000..9e6c579692
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-append/composer.json.tmpl
@@ -0,0 +1,62 @@
+{
+ "name": "fixtures/drupal-drupal-test-append",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": {
+ "prepend": "assets/prepend-to-robots.txt",
+ "append": "assets/append-to-robots.txt"
+ }
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/replacement.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/replacement.txt
new file mode 100644
index 0000000000..4e23d0c860
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/replacement.txt
@@ -0,0 +1 @@
+# File from assets that replaces file in web root.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/robots-default.txt
new file mode 100644
index 0000000000..28c7646d81
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/assets/robots-default.txt
@@ -0,0 +1 @@
+# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal-test-overwrite composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/composer.json.tmpl
new file mode 100644
index 0000000000..d16205c612
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/composer.json.tmpl
@@ -0,0 +1,80 @@
+{
+ "name": "fixtures/drupal-drupal-test-overwrite",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": "assets/robots-default.txt",
+ "make-me.txt": {
+ "path": "assets/replacement.txt",
+ "overwrite": false
+ },
+ "keep-me.txt": {
+ "path": "assets/replacement.txt",
+ "overwrite": false
+ },
+ "replace-me.txt": {
+ "path": "assets/replacement.txt",
+ "overwrite": true
+ }
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/keep-me.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/keep-me.txt
new file mode 100644
index 0000000000..772a59531a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/keep-me.txt
@@ -0,0 +1 @@
+# File in drupal-drupal-test-overwrite that is not replaced by a scaffold file.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/replace-me.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/replace-me.txt
new file mode 100644
index 0000000000..4147b02214
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal-test-overwrite/replace-me.txt
@@ -0,0 +1 @@
+# File in drupal-drupal-test-overwrite that is replaced by a scaffold file.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/assets/robots-default.txt b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/assets/robots-default.txt
new file mode 100644
index 0000000000..6eb30e86aa
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/assets/robots-default.txt
@@ -0,0 +1 @@
+# robots.txt fixture scaffolded from "file-mappings" in drupal-drupal composer.json fixture.
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/composer.json.tmpl
new file mode 100644
index 0000000000..a8641ef7a2
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-drupal/composer.json.tmpl
@@ -0,0 +1,74 @@
+{
+ "name": "fixtures/drupal-drupal",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "gitignore": false,
+ "overwrite": true,
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false,
+ "[web-root]/robots.txt": {
+ "mode": "replace",
+ "path": "assets/robots-default.txt",
+ "overwrite": true
+ }
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/assets/profile.default.services.yml b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/assets/profile.default.services.yml
new file mode 100644
index 0000000000..2a35c02dba
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/assets/profile.default.services.yml
@@ -0,0 +1,4 @@
+# default.services.yml fixture scaffolded from "file-mappings" in drupal-project composer.json fixture.
+# Add a dummy key until YamlPecl can validate an empty YAML file:
+# https://www.drupal.org/project/drupal/issues/3003300
+foo: bar
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/composer.json.tmpl
new file mode 100644
index 0000000000..b3e4b76640
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/drupal-profile/composer.json.tmpl
@@ -0,0 +1,10 @@
+{
+ "name": "fixtures/drupal-profile",
+ "extra": {
+ "composer-scaffold": {
+ "file-mapping": {
+ "[web-root]/.htaccess": false
+ }
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture-allowing-core/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture-allowing-core/composer.json
new file mode 100644
index 0000000000..66e96bf38b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture-allowing-core/composer.json
@@ -0,0 +1,13 @@
+{
+ "name": "fixtures/empty-fixture-allowing-core",
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ }
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture/composer.json
new file mode 100644
index 0000000000..7eb36a74cb
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/empty-fixture/composer.json
@@ -0,0 +1,3 @@
+{
+ "name": "fixtures/empty-fixture"
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/packages.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/packages.json
new file mode 100644
index 0000000000..de4226972a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/packages.json
@@ -0,0 +1,14 @@
+{
+ "packages": {
+ "fixtures/drupal-drupal": {
+ "dev-master": {
+ "name": "fixtures/drupal-drupal",
+ "version": "1.0.0",
+ "dist": {
+ "url": "./drupal-drupal",
+ "type": "path"
+ }
+ }
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-allowing-empty-fixture/composer.json.tmpl b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-allowing-empty-fixture/composer.json.tmpl
new file mode 100644
index 0000000000..254492a6e9
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-allowing-empty-fixture/composer.json.tmpl
@@ -0,0 +1,77 @@
+{
+ "name": "fixtures/project-allowing-empty-fixture",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "composer-scaffold": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-core-fixture": {
+ "type": "path",
+ "url": "../drupal-core-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "drupal-assets-fixture": {
+ "type": "path",
+ "url": "../drupal-assets-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "empty-fixture": {
+ "type": "path",
+ "url": "../empty-fixture",
+ "options": {
+ "symlink": true
+ }
+ },
+ "scaffold-override-fixture": {
+ "type": "path",
+ "url": "../scaffold-override-fixture",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "drupal/core-composer-scaffold": "*",
+ "fixtures/drupal-core-fixture": "*",
+ "fixtures/empty-fixture": "*",
+ "fixtures/scaffold-override-fixture": "*"
+ },
+ "extra": {
+ "composer-scaffold": {
+ "allowed-packages": [
+ "fixtures/drupal-core-fixture",
+ "fixtures/empty-fixture",
+ "fixtures/scaffold-override-fixture"
+ ],
+ "locations": {
+ "web-root": "./"
+ },
+ "gitignore": false,
+ "symlink": __SYMLINK__,
+ "file-mapping": {
+ "[web-root]/.htaccess": false
+ }
+ },
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-empty-scaffold-path/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-empty-scaffold-path/composer.json
new file mode 100644
index 0000000000..e74df96ff0
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-empty-scaffold-path/composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "fixtures/project-with-empty-scaffold-path",
+ "extra": {
+ "composer-scaffold": {
+ "locations": {
+ "web-root": "./"
+ },
+ "file-mapping": {
+ "[web-root]/my-error": {
+ "path": ""
+ }
+ }
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/assets/README.md b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/assets/README.md
new file mode 100644
index 0000000000..7e59600739
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/assets/README.md
@@ -0,0 +1 @@
+# README
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/composer.json b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/composer.json
new file mode 100644
index 0000000000..5cebfd4b57
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/project-with-illegal-dir-scaffold/composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "fixtures/project-with-illegal-dir-scaffold",
+ "extra": {
+ "composer-scaffold": {
+ "locations": {
+ "web-root": "./"
+ },
+ "file-mapping": {
+ "[web-root]/assets": {
+ "path": "assets"
+ }
+ }
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php
new file mode 100644
index 0000000000..f37a684279
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Scaffold/fixtures/scaffold-override-fixture/assets/override-settings.php
@@ -0,0 +1,6 @@
+