Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
pre_composer_update_2.3.php
Go to the documentation of this file.
1 #!/usr/bin/php
2 <?php
7 declare(strict_types=1);
8 
9 $_scriptName = basename(__FILE__);
10 
11 define(
12  'SYNOPSIS',
13 <<<SYNOPSIS
14 Updates Magento with 2.3 requirements that can't be done by `composer update` or `bin/magento setup:upgrade`.
15 Run this script after upgrading to PHP 7.1/7.2 and before running `composer update` or `bin/magento setup:upgrade`.
16 
17 Steps included:
18  - Require new version of the metapackage
19  - Update "require-dev" section
20  - Add "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" to composer.json "autoload":"psr-4" section
21  - Update Magento/Updater if it's installed
22  - Update name, version, and description fields in the root composer.json
23 
24 Usage: php -f $_scriptName -- --root='</path/to/magento/root/>' [--composer='</path/to/composer/executable>']
25  [--edition='<community|enterprise>'] [--repo='<composer_repo_url>'] [--version='<version_constraint>']
26  [--help]
27 
28 Required:
29  --root='</path/to/magento/root/>'
30  Path to the Magento installation root directory
31 
32 Optional:
33  --composer='</path/to/composer/executable>'
34  Path to the composer executable
35  - Default: The composer found in the system PATH
36 
37  --edition='<community|enterprise>'
38  Target Magento edition for the update. Open Source = 'community', Commerce = 'enterprise'
39  - Default: The edition currently required in composer.json
40 
41  --repo='<composer_repo_url>'
42  The Magento repository url to use to pull the new packages
43  - Default: The Magento repository configured in composer.json
44 
45  --version='<version_constraint>'
46  A composer version constraint for allowable 2.3 packages. Versions other than 2.3 are not handled by this script
47  See https://getcomposer.org/doc/articles/versions.md#writing-version-constraints for more information.
48  - Default: The latest 2.3 version available in the Magento repository
49 
50  --help
51  Display this message
53 );
54 
55 $opts = getopt('', [
56  'root:',
57  'composer:',
58  'edition:',
59  'repo:',
60  'version:',
61  'help'
62 ]);
63 
64 // Log levels available for use with output() function
65 define('INFO', 0);
66 define('WARN', 1);
67 define('ERROR', 2);
68 
69 if (isset($opts['help'])) {
71  exit(0);
72 }
73 
74 try {
75  if (version_compare(PHP_VERSION, '7.1', '<') || version_compare(PHP_VERSION, '7.3', '>=')) {
76  preg_match('/^\d+\.\d+\.\d+/',PHP_VERSION, $matches);
77  $phpVersion = $matches[0];
78  throw new Exception("Invalid PHP version '$phpVersion'. Magento 2.3 requires PHP 7.1 or 7.2");
79  }
80 
81  /**** Populate and Validate Settings ****/
82 
83  if (empty($opts['root']) || !is_dir($opts['root'])) {
84  throw new BadMethodCallException('Existing Magento root directory must be supplied with --root');
85  }
86  $rootDir = $opts['root'];
87 
88  $composerFile = "$rootDir/composer.json";
89  if (!file_exists($composerFile)) {
90  throw new InvalidArgumentException("Supplied Magento root directory '$rootDir' does not contain composer.json");
91  }
92 
94 
95  $metapackageMatcher = '/^magento\/product\-(?<edition>community|enterprise)\-edition$/';
96  foreach (array_keys($composerData['require']) as $requiredPackage) {
97  if (preg_match($metapackageMatcher, $requiredPackage, $matches)) {
98  $edition = $matches['edition'];
99  break;
100  }
101  }
102  if (empty($edition)) {
103  throw new InvalidArgumentException("No Magento metapackage found in $composerFile");
104  }
105 
106  // Override composer.json edition if one is passed to the script
107  if (!empty($opts['edition'])) {
108  $edition = $opts['edition'];
109  }
110  $edition = strtolower($edition);
111 
112  if ($edition !== 'community' && $edition !== 'enterprise') {
113  throw new InvalidArgumentException("Only 'community' and 'enterprise' editions allowed; '$edition' given");
114  }
115 
116  $composerExec = (!empty($opts['composer']) ? $opts['composer'] : 'composer');
117  if (basename($composerExec, '.phar') != 'composer') {
118  throw new InvalidArgumentException("'$composerExec' is not a composer executable");
119  }
120 
121  // Use 'command -v' to check if composer is executable
122  exec("command -v $composerExec", $out, $composerFailed);
123  if ($composerFailed) {
124  if ($composerExec == 'composer') {
125  $message = 'Composer executable is not available in the system PATH';
126  }
127  else {
128  $message = "Invalid composer executable '$composerExec'";
129  }
130  throw new InvalidArgumentException($message);
131  }
132 
133  // The composer command uses the Magento root as the working directory so this script can be run from anywhere
134  $composerExec = "$composerExec --working-dir='$rootDir'";
136  // Set the version constraint to any 2.3 package if not specified
137  $constraint = !empty($opts['version']) ? $opts['version'] : '2.3.*';
139  // Composer package names
140  $project = "magento/project-$edition-edition";
141  $metapackage = "magento/product-$edition-edition";
143  // Get the list of potential Magento repositories to search for the update package
144  $mageUrls = [];
145  $authFailed = [];
146  if (!empty($opts['repo'])) {
147  $mageUrls[] = $opts['repo'];
148  }
149  else {
150  foreach ($composerData['repositories'] as $label => $repo) {
151  if (strpos(strtolower($label), 'mage') !== false || strpos($repo['url'], '.mage') !== false) {
152  $mageUrls[] = $repo['url'];
153  }
154  }
155 
156  if (count($mageUrls) == 0) {
157  throw new InvalidArgumentException('No Magento repository urls found in composer.json');
158  }
159  }
161  $tempDir = findUnusedFilename($rootDir, 'temp_project');
162  $projectConstraint = "$project='$constraint'";
163  $version = null;
164  $description = null;
165 
166  output("**** Searching for a matching version of $project ****");
167 
168  // Try to retrieve a 2.3 package from each Magento repository until one is found
169  foreach ($mageUrls as $repoUrl) {
170  try {
171  output("\\nChecking $repoUrl");
172  deleteFilepath($tempDir);
173  runComposer("create-project --repository=$repoUrl $projectConstraint $tempDir --no-install");
174 
175  // Make sure the downloaded package is 2.3
176  $newComposer = json_decode(file_get_contents("$tempDir/composer.json"), true);
177  $version = $newComposer['version'];
178  $description = $newComposer['description'];
179 
180  if (strpos($version, '2.3.') !== 0) {
181  throw new InvalidArgumentException("Bad 2.3 version constraint '$constraint'; version $version found");
182  }
183 
184  // If no errors occurred, set this as the correct repo, forget errors from previous repos, and move forward
185  output("\\n**** Found compatible $project version: $version ****");
186  $repo = $repoUrl;
187  unset($exception);
188  break;
189  }
190  catch (Exception $e) {
191  // If this repository doesn't have a valid package, save the error but continue checking any others
192  output("Failed to find a valid 2.3 $project package on $repoUrl", WARN);
193  $exception = $e;
194  }
195  }
196 
197  // If a valid project package hasn't been found, throw the last error
198  if (isset($exception)) {
199  throw $exception;
200  }
201 
202  output("\\n**** Executing Updates ****");
203 
204  $composerBackup = findUnusedFilename($rootDir, 'composer.json.bak');
205  output("\\nBacking up $composerFile to $composerBackup");
208  // Add the repository to composer.json if needed without overwriting any existing ones
209  $repoUrls = array_map(function ($r) { return $r['url']; }, $composerData['repositories']);
210  if (!in_array($repo, $repoUrls)) {
211  $repoLabels = array_map('strtolower',array_keys($composerData['repositories']));
212  $newLabel = 'magento';
213  if (in_array($newLabel, $repoLabels)) {
214  $count = count($repoLabels);
215  for ($i = 1; $i <= $count; $i++) {
216  if (!in_array("$newLabel-$i", $repoLabels)) {
217  $newLabel = "$newLabel-$i";
218  break;
219  }
220  }
221  }
222  output("\\nAdding $repo to composer repositories under label '$newLabel'");
223  runComposer("config repositories.$newLabel composer $repo");
224  }
225 
226  output("\\nUpdating Magento metapackage requirement to $metapackage=$version");
227  if ($edition == 'enterprise') {
228  // Community -> Enterprise upgrades need to remove the community edition metapackage
229  runComposer('remove magento/product-community-edition --no-update');
230  output('');
231  }
232  runComposer("require $metapackage=$version --no-update");
233 
234  output('\nUpdating "require-dev" section of composer.json');
235  runComposer('require --dev ' .
236  'phpunit/phpunit:~6.2.0 ' .
237  'friendsofphp/php-cs-fixer:~2.10.1 ' .
238  'lusitanian/oauth:~0.8.10 ' .
239  'pdepend/pdepend:2.5.2 ' .
240  'sebastian/phpcpd:~3.0.0 ' .
241  'squizlabs/php_codesniffer:3.2.2 --no-update');
242  output('');
243  runComposer('remove --dev sjparkinson/static-review fabpot/php-cs-fixer --no-update');
245  output('\nAdding "Zend\\\\Mvc\\\\Controller\\\\": "setup/src/Zend/Mvc/Controller/" to "autoload": "psr-4"');
246  $composerData['autoload']['psr-4']['Zend\\Mvc\\Controller\\'] = 'setup/src/Zend/Mvc/Controller/';
247 
248  if (preg_match('/^magento\/project\-(community|enterprise)\-edition$/', $composerData['name'])) {
249  output('\nUpdating project name, version, and description');
250  $composerData['name'] = $project;
251  $composerData['version'] = $version;
252  $composerData['description'] = $description;
253  }
254 
255  file_put_contents($composerFile, json_encode($composerData, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
257  // Update Magento/Updater if it's installed
258  $updateDir = "$rootDir/update";
259  if (file_exists($updateDir)) {
260  $updateBackup = findUnusedFilename($rootDir, 'update.bak');
261  output("\\nBacking up Magento/Updater directory $updateDir to $updateBackup");
262  rename($updateDir, $updateBackup);
263  output('\nUpdating Magento/Updater');
264  rename("$tempDir/update", $updateDir);
265  }
266 
267  // Remove temp project directory that was used for repo/version validation and new source for Magento/Updater
268  deleteFilepath($tempDir);
269 
270  output("\\n**** Script Complete! $composerFile updated to Magento version $version ****");
271  if (count($authFailed) > 0) {
272  output('Repository authentication failures occurred!', WARN);
273  output(' * Failed authentication could result in incorrect package versions', WARN);
274  output(' * To resolve, add credentials for the repositories to auth.json', WARN);
275  output(' * URL(s) failing authentication: ' . join(', ', array_keys($authFailed)), WARN);
276  }
277 } catch (Exception $e) {
278  if ($e->getPrevious()) {
279  $e = $e->getPrevious();
280  }
281 
282  try {
283  output($e->getMessage(), ERROR, get_class($e));
284  output('Script failed! See usage information with --help', ERROR);
285 
286  if (isset($composerBackup) && file_exists($composerBackup)) {
287  output("Resetting $composerFile backup");
290  }
291  if (isset($updateBackup) && file_exists($updateBackup)) {
292  output("Resetting $updateDir backup");
294  rename($updateBackup, $updateDir);
295  }
296  if (isset($tempDir) && file_exists($tempDir)) {
297  output('Removing temporary project directory');
298  deleteFilepath($tempDir);
299  }
300  }
301  catch (Exception $e2) {
302  output($e2->getMessage(), ERROR, get_class($e2));
303  output('Backup restoration or directory cleanup failed', ERROR);
304  }
305 
306  exit($e->getCode() == 0 ? 1 : $e->getCode());
307 }
308 
316 function findUnusedFilename($dir, $filename) {
317  $unique = "$dir/$filename";
318  if (file_exists($unique)) {
319  $unique = tempnam($dir, "$filename.");
320  unlink($unique);
321  }
322  return $unique;
323 }
324 
332 function runComposer($command)
333 {
335  $command = "$composerExec $command --no-interaction";
336  output(" Running command:\\n $command");
337  exec("$command 2>&1", $lines, $exitCode);
338  $output = ' ' . join('\n ', $lines);
339 
340  // Reload composer object from the updated composer.json
341  $composerData = json_decode(file_get_contents($composerFile), true);
342 
343  if (0 !== $exitCode) {
344  $output = "Error encountered running command:\\n $command\\n$output";
345  throw new RuntimeException($output, $exitCode);
346  }
347  output($output);
348 
349  if (strpos($output, 'URL required authentication.') !== false) {
350  preg_match("/'(https?:\/\/)?(?<url>[^\/']+)(\/[^']*)?' URL required authentication/", $output, $matches);
351  $authUrl = $matches['url'];
352  $authFailed[$authUrl] = 1;
353  output("Repository authentication failed; make sure '$authUrl' exists in auth.json", WARN);
354  }
355 
356  return $lines;
357 }
358 
365 function deleteFilepath($path) {
366  if (!file_exists($path)) {
367  return;
368  }
369  if (is_dir($path)) {
370  $files = array_diff(scandir($path), array('..', '.'));
371  foreach ($files as $file) {
372  deleteFilepath("$path/$file");
373  }
374  rmdir($path);
375  }
376  else {
377  unlink($path);
378  }
379  if (file_exists($path)) {
380  throw new Exception("Failed to delete $path");
381  }
382 }
383 
391 function output($string, $level = INFO, $label = '') {
392  $string = str_replace('\n', PHP_EOL, $string);
393 
394  if (!empty($label)) {
395  $label = "$label: ";
396  }
397  else if ($level == WARN) {
398  $label = 'WARNING: ';
399  }
400  else if ($level == ERROR) {
401  $label = 'ERROR: ';
402  }
403  $string = "$label$string";
404 
405  if ($level == WARN) {
406  error_log($string);
407  }
408  elseif ($level == ERROR) {
409  error_log(PHP_EOL . $string);
410  }
411  else {
412  echo $string . PHP_EOL;
413  }
414 }
deleteFilepath($path)
output($string, $level=INFO, $label='')
if($edition !=='community' && $edition !=='enterprise') $composerExec
elseif(isset( $params[ 'redirect_parent']))
Definition: iframe.phtml:17
exec($command, array &$output=null, &$return_var=null)
if(!file_exists($composerFile)) $composerData
if(count($authFailed) > 0) catch(Exception $e) findUnusedFilename($dir, $filename)
$count
Definition: recent.phtml:13
defined('MTF_BOOT_FILE')||define('MTF_BOOT_FILE' __FILE__
Definition: bootstrap.php:7
$message
foreach(array_keys($composerData['require']) as $requiredPackage) if(empty($edition)) if(!empty($opts['edition'])) $edition
$label
Definition: details.phtml:21
exit
Definition: redirect.phtml:12
const SYNOPSIS
runComposer($command)
$rootDir
Definition: website.php:12
$i
Definition: gallery.phtml:31
foreach($appDirs as $dir) $files