1

I have a settings.php file in git which has the logic to include settings.local.php already uncommented, with settings.local.php in .gitignore for security purposes.

I would like to be able to install Drupal (8.3.3) so that any changes which are made by the installer (by default: hash salt, databases array, install profile, sync directory location) are made to settings.local.php rather than settings.php as the installation currently does. Is this possible?

0

I think it's possible, but you'll need a custom installation profile to do it.

drupal_rewrite_settings() takes a 2nd parameter for a custom path to settings.php, but none of the default core methods make use of it, they let it fallback to sites/default/settings.php. It's used in three key places:

The form and install_write_profile() are invoked from an install task, so those can be altered with hook_install_tasks_alter(). drupal_install_config_directories() is invoked from the form submit method, which you'll be overriding anyway, so you can replace it with your own method.

A completely untested example:

profiles/foo/foo.profile

function foo_install_tasks_alter(&$tasks, $install_state) {
    $tasks['install_write_profile']['function'] = 'foo_install_write_profile';
    $tasks['install_settings_form']['function'] = 'Drupal\foo\Form\SiteSettingsForm';
}

function foo_install_write_profile($install_state) {
    // Only need to write to settings.php if it is possible. The primary storage
    // for the install profile is the core.extension configuration.
    $settings_path = \Drupal::service('site.path') . '/local.settings.php';

    // Make sure the file exists.
    touch($settings_path);

    if (is_writable($settings_path)) {
        // Remember the profile which was used.
        $settings['settings']['install_profile'] = (object) [
            'value' => $install_state['parameters']['profile'],
            'required' => TRUE,
        ];
        drupal_rewrite_settings($settings, $settings_path);
    }
    elseif (($settings_profile = Settings::get('install_profile')) && $settings_profile !== $install_state['parameters']['profile']) {
        throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation());
    }
}

profiles/foo/src/Form/SiteSettingsForm.php

namespace Drupal\foo\Form;

use Drupal\Core\Installer\Form\SiteSettingsForm as BaseForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Crypt;

class SiteSettingsForm extends BaseForm {

    public function submitForm(array &$form, FormStateInterface $form_state) {
        global $install_state;

        // Update global settings array and save.
        $settings = [];
        $database = $form_state->get('database');
        $settings['databases']['default']['default'] = (object) [
            'value'    => $database,
            'required' => TRUE,
        ];
        $settings['settings']['hash_salt'] = (object) [
            'value'    => Crypt::randomBytesBase64(55),
            'required' => TRUE,
        ];
        // Remember the profile which was used.
        $settings['settings']['install_profile'] = (object) [
            'value' => $install_state['parameters']['profile'],
            'required' => TRUE,
        ];

        $settings_path = \Drupal::service('site.path') . '/local.settings.php';
        drupal_rewrite_settings($settings, $settings_path);

        // Add the config directories to settings.php.
        $this->installConfigDirectories();

        // Indicate that the settings file has been verified, and check the database
        // for the last completed task, now that we have a valid connection. This
        // last step is important since we want to trigger an error if the new
        // database already has Drupal installed.
        $install_state['settings_verified'] = TRUE;
        $install_state['config_verified'] = TRUE;
        $install_state['database_verified'] = TRUE;
        $install_state['completed_task'] = install_verify_completed_task();
    }

    protected function installConfigDirectories() {
        global $config_directories;

        // Add a randomized config directory name to settings.php, unless it was
        // manually defined in the existing already.
        if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) {
            $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync';
            $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
                'value' => $config_directories[CONFIG_SYNC_DIRECTORY],
                'required' => TRUE,
            ];
            // Rewrite settings.php, which also sets the value as global variable.
            $settings_path = \Drupal::service('site.path') . '/local.settings.php';
            drupal_rewrite_settings($settings, $settings_path);
        }

        // This should never fail, since if the config directory was specified in
        // settings.php it will have already been created and verified earlier, and
        // if it wasn't specified in settings.php, it is created here inside the
        // public files directory, which has already been verified to be writable
        // itself. But if it somehow fails anyway, the installation cannot proceed.
        // Bail out using a similar error message as in system_requirements().
        if (!file_prepare_directory($config_directories[CONFIG_SYNC_DIRECTORY], FILE_CREATE_DIRECTORY)
            && !file_exists($config_directories[CONFIG_SYNC_DIRECTORY])) {
            throw new Exception(t('The directory %directory could not be created. To proceed with the installation, either create the directory or ensure that the installer has the permissions to create it automatically. For more information, see the <a href=":handbook_url">online handbook</a>.', [
                '%directory' => config_get_config_directory(CONFIG_SYNC_DIRECTORY),
                ':handbook_url' => 'https://www.drupal.org/server-permissions',
            ]));
        }
        elseif (is_writable($config_directories[CONFIG_SYNC_DIRECTORY])) {
            // Put a README.txt into the sync config directory. This is required so that
            // they can later be added to git. Since this directory is auto-created, we
            // have to write out the README rather than just adding it to the drupal core
            // repo.
            $text = 'This directory contains configuration to be imported into your Drupal site. To make this configuration active, visit admin/config/development/configuration/sync.' . ' For information about deploying configuration between servers, see https://www.drupal.org/documentation/administer/config';
            file_put_contents(config_get_config_directory(CONFIG_SYNC_DIRECTORY) . '/README.txt', $text);
        }
    }

}

The last step, if you want this to be fully automated and possibly avoid errors during installation, is an awkward one. You need to programatically uncomment the include for local.settings.php at the bottom of settings.php.

I'm not aware of any Drupal helpers for that (might be wrong though), so you can probably approach that as a general PHP/other task.

I think the top of the foo_install_write_profile() function would be as good a place as any to do it, unless of course your dev workflow means you can do it before you even carry out the installation (probably best if possible).

  • I don't have the rep to upvote this but this looks promising. Give me a day or two to check this out and I'll accept if it works. (As to your last few paragraphs, none of the other guys on my team got to this code before I checked in an uncommented include statement, so I think we're good there.) – Alan Liddell Jun 16 '17 at 22:20
  • As written, this didn't do it -- settings.php was still being modified and the only thing being written to settings.local.php was the name of the install profile. Based on my use case, I ended up just patching these two files with a Python script as a post-install step. – Alan Liddell Jun 28 '17 at 16:43
0

Clive gave a nice answer, but it didn't work, as the form is not picked up by Drupal and instead throws an error. I'm suspecting that the PSR4 autoloading does not pick up the src directory in a profile. As such, I instead implemented hook_form_alter to override the submit handler, rather than overriding the form class overall. Here is the working code:

use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Crypt;


/**
 * Implements hook__install_tasks_alter().
 */
function MODULE_install_tasks_alter(&$tasks, $install_state) {
  $tasks['install_write_profile']['function'] = 'MODULE_install_write_profile';
}

function MODULE_form_install_settings_form_alter(array &$form, FormStateInterface $form_state) {
  $form['actions']['save']['#submit'] = ['MODULE_install_form_submit'];
}

function MODULE_install_form_submit(array &$form, FormStateInterface $form_state) {
  global $install_state;

  // Update global settings array and save.
  $settings = [];
  $database = $form_state->get('database');
  $settings['databases']['default']['default'] = (object) [
    'value'    => $database,
    'required' => TRUE,
  ];
  // Set the hash salt
  $settings['settings']['hash_salt'] = (object) [
    'value'    => Crypt::randomBytesBase64(55),
    'required' => TRUE,
  ];
  // Remember the profile which was used.
  $settings['settings']['install_profile'] = (object) [
    'value' => $install_state['parameters']['profile'],
    'required' => TRUE,
  ];

  $settings_path = \Drupal::service('site.path') . '/settings.local.php';
  drupal_rewrite_settings($settings, $settings_path);

  // Add the config directories to settings.php.
  MODULE_install_config_directories();

  // Indicate that the settings file has been verified, and check the database
  // for the last completed task, now that we have a valid connection. This
  // last step is important since we want to trigger an error if the new
  // database already has Drupal installed.
  $install_state['settings_verified'] = TRUE;
  $install_state['config_verified'] = TRUE;
  $install_state['database_verified'] = TRUE;
  $install_state['completed_task'] = install_verify_completed_task();
}

function MODULE_install_config_directories() {
  global $config_directories;

  // Add a randomized config directory name to settings.php, unless it was
  // manually defined in the existing already.
  if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) {
    $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync';
    $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
      'value' => $config_directories[CONFIG_SYNC_DIRECTORY],
      'required' => TRUE,
    ];
    // Rewrite settings.php, which also sets the value as global variable.
    $settings_path = \Drupal::service('site.path') . '/settings.local.php';
    drupal_rewrite_settings($settings, $settings_path);
  }

  // This should never fail, since if the config directory was specified in
  // settings.php it will have already been created and verified earlier, and
  // if it wasn't specified in settings.php, it is created here inside the
  // public files directory, which has already been verified to be writable
  // itself. But if it somehow fails anyway, the installation cannot proceed.
  // Bail out using a similar error message as in system_requirements().
  if (
    !file_prepare_directory($config_directories[CONFIG_SYNC_DIRECTORY], FILE_CREATE_DIRECTORY)
    && !file_exists($config_directories[CONFIG_SYNC_DIRECTORY])
  ) {
    throw new Exception(t('The directory %directory could not be created. To proceed with the installation, either create the directory or ensure that the installer has the permissions to create it automatically. For more information, see the <a href=":handbook_url">online handbook</a>.', [
      '%directory' => config_get_config_directory(CONFIG_SYNC_DIRECTORY),
      ':handbook_url' => 'https://www.drupal.org/server-permissions',
    ]));
  }
  elseif (is_writable($config_directories[CONFIG_SYNC_DIRECTORY])) {
    // Put a README.txt into the sync config directory. This is required so
    // that they can later be added to git. Since this directory is
    // auto-created, we have to write out the README rather than just adding
    // it to the drupal core repo.
    $text = 'This directory contains configuration to be imported into your Drupal site. To make this configuration active, visit admin/config/development/configuration/sync.' . ' For information about deploying configuration between servers, see https://www.drupal.org/documentation/administer/config';
    file_put_contents(config_get_config_directory(CONFIG_SYNC_DIRECTORY) . '/README.txt', $text);
  }
}

/**
 * Callback for the install write profile.
 *
 * @see MODULE_install_tasks_alter()
 */
function MODULE_install_write_profile($install_state) {
  // Only need to write to settings.php if it is possible. The primary storage
  // for the install profile is the core.extension configuration.
  $settings_path = \Drupal::service('site.path') . '/settings.local.php';

  // Make sure the file exists.
  touch($settings_path);

  if (is_writable($settings_path)) {
    // Remember the profile which was used.
    $settings['settings']['install_profile'] = (object) [
      'value' => $install_state['parameters']['profile'],
      'required' => TRUE,
    ];
    drupal_rewrite_settings($settings, $settings_path);
  }
  elseif (($settings_profile = Settings::get('install_profile')) && $settings_profile !== $install_state['parameters']['profile']) {
    throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation());
  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.