You are here

composer_manager.writer.inc in Composer Manager 6

Functions related to the creation of the consolidated composer.json file.

File

composer_manager.writer.inc
View source
<?php

/**
 * @file
 * Functions related to the creation of the consolidated composer.json file.
 */

/**
 * Fetches the data in each module's composer.json file.
 *
 * @return array
 *
 * @throws \RuntimeException
 */
function composer_manager_fetch_data() {
  $data = array();
  foreach (module_list() as $module) {
    $module_dir = drupal_get_path('module', $module);
    $composer_file = $module_dir . '/composer.json';
    $args = array(
      '@file' => $composer_file,
    );
    if (!file_exists($composer_file)) {
      continue;
    }
    if (!($filedata = @file_get_contents($composer_file))) {
      throw new \RuntimeException(t('Error reading file: @file', $args));
    }
    if (!($json = @json_decode($filedata, TRUE))) {
      throw new \UnexpectedValueException(t('Expecting contents of file to be valid JSON: @file', $args));
    }
    $data[$module] = $json;
  }
  return $data;
}

/**
 * Builds the JSON array ccontaining the combined requirements of each module's
 * composer.json file.
 *
 * @param array $data
 *   An array of JSON arrays parsed from composer.json files keyed by the module
 *   that defines it. This is usually the return value of the
 *   composer_manager_fetch_data() function.
 *
 * @return array
 *   The consolidated JSON array that will be written to a compsoer.json file.
 *
 * @throws \RuntimeException
 */
function composer_manager_build_json(array $data) {
  $combined = array();
  foreach ($data as $module => $json) {
    if (!$combined) {
      $combined = array(
        'require' => array(),
        'config' => array(
          'autoloader-suffix' => 'ComposerManager',
        ),
      );
      $vendor_dir = composer_manager_relative_vendor_dir();
      if (0 !== strlen($vendor_dir) && 'vendor' != $vendor_dir) {
        $combined['config']['vendor-dir'] = $vendor_dir;
      }
    }

    // @todo Detect duplicates, maybe add an "ignore" list. Figure out if this
    // encompases all keys that should be merged.
    $to_merge = array(
      'require',
      'require-dev',
      'conflict',
      'replace',
      'provide',
      'suggest',
      'repositories',
    );
    foreach ($to_merge as $key) {
      if (isset($json[$key])) {
        if (isset($combined[$key]) && is_array($combined[$key])) {
          $combined[$key] = array_merge($combined[$key], $json[$key]);
        }
        else {
          $combined[$key] = $json[$key];
        }
      }
    }
    $autoload_options = array(
      'psr-0',
      'psr-4',
    );
    foreach ($autoload_options as $option) {
      if (isset($json['autoload'][$option])) {
        $namespaces = (array) $json['autoload'][$option];
        foreach ($json['autoload'][$option] as $namesapce => $dirs) {
          $dirs = (array) $dirs;
          array_walk($dirs, 'composer_manager_relative_autoload_path', $module);
          if (!isset($combined['autoload'][$option][$namesapce])) {
            $combined['autoload'][$option][$namesapce] = array();
          }
          $combined['autoload'][$option][$namesapce] = array_merge($combined['autoload'][$option][$namesapce], $dirs);
        }
      }
    }

    // Merge in the "classmap" and "files" autoload options.
    $autoload_options = array(
      'classmap',
      'files',
    );
    foreach ($autoload_options as $option) {
      if (isset($json['autoload'][$option])) {
        $dirs = (array) $json['autoload'][$option];
        array_walk($dirs, 'composer_manager_relative_autoload_path', $module);
        if (!isset($combined['autoload'][$option])) {
          $combined['autoload'][$option] = array();
        }
        $combined['autoload'][$option] = array_merge($combined['autoload'][$option], $dirs);
      }
    }

    // Take the lowest stability.
    if (isset($json['minimum-stability'])) {
      if (!isset($combined['minimum-stability']) || -1 == composer_manager_compare_stability($json['minimum-stability'], $combined['minimum-stability'])) {
        $combined['minimum-stability'] = $json['minimum-stability'];
      }
    }
  }
  drupal_alter('composer_json', $combined);
  return $combined;
}

/**
 * Writes the composer.json file in the specified directory.
 *
 * @param string $dir_uri
 *   The URI of the directory that the composer.json file is being written to.
 *   This is usually the "composer_manager_file_dir" system variable.
 * @param array $json
 *   A model of the JSON data being written. This is usually the return value of
 *   composer_manager_build_json().
 *
 * @throws \RuntimeException
 */
function composer_manager_put_file($dir_uri, array $json) {
  composer_manager_prepare_directory($dir_uri);

  // Make the composer.json file human readable for PHP >= 5.4.
  // @see drupal_json_encode()
  // @see http://drupal.org/node/1943608
  // @see http://drupal.org/node/1948012
  $json_options = JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT;
  if (defined('JSON_PRETTY_PRINT')) {
    $json_options = $json_options | JSON_PRETTY_PRINT;
  }
  if (defined('JSON_UNESCAPED_SLASHES')) {
    $json_options = $json_options | JSON_UNESCAPED_SLASHES;
  }
  $file_uri = $dir_uri . '/composer.json';
  if (!@file_put_contents($file_uri, json_encode($json, $json_options))) {
    throw new \RuntimeException(t('Error writing composer.json file: @file', array(
      '@file' => $file_uri,
    )));
  }

  // Displays a message to admins that a file was written.
  if (user_access('administer site configuration')) {
    $command = file_exists($dir_uri . '/composer.lock') ? 'update' : 'install';
    drupal_set_message(t('A composer.json file was written to @path.', array(
      '@path' => realpath($dir_uri),
    )));
    if ('admin/settings/composer-manager' != $_GET['q']) {
      $args = array(
        '!command' => $command,
        '@url' => url('http://drupal.org/project/composer_manager', array(
          'absolute' => TRUE,
        )),
      );
      if ('install' == $command) {
        $message = t('Composer\'s <code>!command</code> command must be run to generate the autoloader and install the required packages.<br/>Refer to the instructions on the <a href="@url" target="_blank">Composer Manager project page</a> for installing packages.', $args);
      }
      else {
        $message = t('Composer\'s <code>!command</code> command must be run to install the required packages.<br/>Refer to the instructions on the <a href="@url" target="_blank">Composer Manager project page</a> for updating packages.', $args);
      }
      drupal_set_message($message, 'warning');
    }
  }
}

/**
 * Ensures the directory is created and protected via .htaccess if necessary.
 *
 * @param string $directory
 *   The URI or path to the directory being prepared.
 *
 * @return bool
 */
function composer_manager_prepare_directory($directory) {
  if (!file_check_directory($directory, FILE_CREATE_DIRECTORY)) {
    return FALSE;
  }
  if (0 === strpos($directory, file_directory_path())) {
    $htaccess_lines = "Deny from all\n\n" . file_htaccess_lines();
    if (($fp = fopen("{$directory}/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) {
      fclose($fp);
      chmod($directory . '/.htaccess', 0664);
    }
    else {
      $variables = array(
        '%directory' => $directory,
        '!htaccess' => '<br />' . nl2br(check_plain($htaccess_lines)),
      );
      watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables, WATCHDOG_ERROR);
    }
  }
  return TRUE;
}

/**
 * Compares the passed minimum stability requirements.
 *
 * @return int
 *   Returns -1 if the first version is lower than the second, 0 if they are
 *   equal, and 1 if the second is lower.
 *
 * @throws \UnexpectedValueException
 */
function composer_manager_compare_stability($a, $b) {
  $number = array(
    'dev' => 0,
    'alpha' => 1,
    'beta' => 2,
    'RC' => 3,
    'rc' => 3,
    'stable' => 4,
  );
  if (!isset($number[$a]) || !isset($number[$b])) {
    throw new \UnexpectedValueException(t('Unexpected value for "minimum-stability"'));
  }
  if ($number[$a] == $number[$b]) {
    return 0;
  }
  else {
    return $number[$a] < $number[$b] ? -1 : 1;
  }
}

/**
 * Returns the realpath to the Composer file directory.
 *
 * @return string
 *
 * @throws \RuntimeException
 */
function composer_manager_file_dir() {
  $dir_uri = variable_get('composer_manager_file_dir', file_directory_path() . '/composer');
  if (!composer_manager_prepare_directory($dir_uri)) {
    throw new \RuntimeException(t('Error creating directory: @dir', array(
      '@dir' => $dir_uri,
    )));
  }
  if (!($realpath = realpath($dir_uri))) {
    throw new \RuntimeException(t('Error resolving directory: @dir', array(
      '@dir' => $dir_uri,
    )));
  }
  return $realpath;
}

/**
 * Returns the path for the autoloaded directory or class relative to the
 * directory containing the composer.json file.
 */
function composer_manager_relative_autoload_path(&$path, $key, $module) {
  static $dir_from = NULL;
  if (NULL === $dir_from) {
    $dir_from = composer_manager_file_dir();
  }
  $dir_to = getcwd() . '/' . drupal_get_path('module', $module) . '/' . $path;
  $path = composer_manager_relative_dir($dir_to, $dir_from);
}

/**
 * Returns the vendor directory relative to the composer file directory.
 *
 * @return string
 *
 * @throws \RuntimeException
 */
function composer_manager_relative_vendor_dir() {
  return composer_manager_relative_dir(composer_manager_vendor_dir(), composer_manager_file_dir());
}

/**
 * Gets the path of the "to" directory relative to the "from" directory.
 *
 * @param array $dir_to
 *   The absolute path of the directory that the relative path refers to.
 * @param array $dir_from
 *   The absolute path of the directory from which the relative path is being
 *   calculated.
 *
 * @return string
 */
function composer_manager_relative_dir($dir_to, $dir_from) {
  $dirs_to = explode('/', ltrim($dir_to, '/'));
  $dirs_from = explode('/', ltrim($dir_from, '/'));

  // Strip the matching directories so that both arrays are relative to a common
  // position. The count of the $dirs_from array tells us how many levels up we
  // need to traverse from the directory containing the composer.json file, and
  // $dirs_to is relative to the common position.
  foreach ($dirs_to as $pos => $dir) {
    if (!isset($dirs_from[$pos]) || $dirs_to[$pos] != $dirs_from[$pos]) {
      break;
    }
    unset($dirs_to[$pos], $dirs_from[$pos]);
  }
  $path = str_repeat('../', count($dirs_from)) . join('/', $dirs_to);
  if (PHP_OS == 'WINNT') {
    $path = preg_replace('%..\\/([a-zA-Z])%i', '${1}', $path, 1);
  }
  return $path;
}

Functions

Namesort descending Description
composer_manager_build_json Builds the JSON array ccontaining the combined requirements of each module's composer.json file.
composer_manager_compare_stability Compares the passed minimum stability requirements.
composer_manager_fetch_data Fetches the data in each module's composer.json file.
composer_manager_file_dir Returns the realpath to the Composer file directory.
composer_manager_prepare_directory Ensures the directory is created and protected via .htaccess if necessary.
composer_manager_put_file Writes the composer.json file in the specified directory.
composer_manager_relative_autoload_path Returns the path for the autoloaded directory or class relative to the directory containing the composer.json file.
composer_manager_relative_dir Gets the path of the "to" directory relative to the "from" directory.
composer_manager_relative_vendor_dir Returns the vendor directory relative to the composer file directory.