Schon seit Drupal 8 empfand ich die Lösung, mittels phpdotenv Settings für verschiedene Umgebungen bereitzustellen, elegant, praktisch und pflegeleicht.

Anstatt nur 2 Umgebungen mittels settings.php und settings.local.php vorgeben zu können, kann man mit phpdotenv praktisch unbegrenzte Umgebungen definieren und die Werte für betreffende Settings alle zusammen in einer Datei pflegen, die zudem auch noch außerhalb des Web liegt.

Mit dieser Technik, die in Rails erfunden und seit einiger Zeit auch in PHP verfügbar ist, kann man z.B. die klassischen Entwicklungsumgebungen "Dev, Stage, Production" einfach und pflegeleicht abbilden.

Auch bei Settings für spezielles Hosting wie z.B. bei Pantheon, Acquia kann man dies sehr gut einsetzen.

Was ist phpdotenv?

phpdotenv ist ein Projekt von Vance Lucas und Graham Campbell zur Realisierung einer einfachen Lösung zur Schaffung von Konfigurationen für PHP-Anwendungen. Zudem bietet es den Vorteil, dass "Credentials" wie z.B. Datenbank-Zugänge bei Drupal nicht in der settings.php stehen müssen, wo sie tendenziell ausgelesen werden könnten.

Stattdessen wird eine .env Datei im Wurzelverzeichnis der Anwendung erstellt, in der einfache Umgebungsvariablen festgehalten werden und dann via getenv() anderswo, z.B. in der settings.php, abgerufen werden können.

Das phpdotenv Projekt lebt hier: https://github.com/vlucas/phpdotenv

Vorbereitung zur Anwendung in Drupal 9

In den aktuellen Drupal-9 Distributionen ist vlucas/phpdotenv nicht enthalten, daher muß man dieses zunächst hinzufügen.

Mittels composer einfach mit diesem Befehl:

#!/bin/sh
composer require vlucas/phpdotenv

Weiterhin benötigt man die folgende Datei, die die Brücke zwischen Drupal und phpdotenv schlägt und die .env lädt:

load.environment.php, zu finden bei drupal-composer/drupal-project unter der Adresse: https://github.com/drupal-composer/drupal-project/blob/9.x/load.environ…

<?php

/**
 * This file is included very early. See autoload.files in composer.json and
 * https://getcomposer.org/doc/04-schema.md#files
 */

use Dotenv\Dotenv;

/**
 * Load any .env file. See /.env.example.
 *
 * Drupal has no official method for loading environment variables and uses
 * getenv() in some places.
 */
$dotenv = Dotenv::createUnsafeImmutable(__DIR__);
$dotenv->safeLoad();

Natürlich brauchen wir auch eine .env Datei im Wurzelverzeichnis:

# .env file
# comments with leading #

# Environment Switch
# - DEV
# - STAGE
# - PROD
#
ENV='DEV'

...

Damit nun alles Zusammen funktioniert, müssen wir composer noch mitteilen, dass es die Datei load.environment.php mit in seine Liste zu ladender Dateien aufnimmt. Daher muß folgender Eintrag in composer.json vorgenommen werden:

    "autoload": {
        "files": [
            "load.environment.php"
        ]
    },

Dies einfach irgendwo zwischenschieben und darauf achten, dass Kommata und Klammern richtig gesetzt sind.

Führen wir nun ein composer Kommando aus, bei dem das Verzeichnis der autoload-Dateien neu erzeugt wird, wird die .env Datei fortan immer mit geladen und ausgewertet.

#!/bin/sh
composer update

Die .env Datei

Jetzt geht es noch um die Frage, welche Konfigurationswerte man in die .env Datei auslagern könnte. Generell gesagt: Alles, was Umgebungsspezifisch ist. Weiter unten zeige ich auch meine settings.php und zeige auf, wie ich es gelöst habe, als Anregung für eigene Experimente.

Hier ist meine vorläufige Version der .env Datei mit Konfigurationswerten für 3 Umgebungen, DEV, STAGE und PROD. Anhand dieses Beispiels lässt sich die Datei schnell an eigene Bedürfnisse anpassen.

# .env file

HASH_SALT='oF2Y1aZrhjAnCynJpYYTdhL3TTO9s1im87PTbZJ0cv12UgX0Hoy3hm8O1zpd7DMCvo611lwSFA'

# ENVironment Switch
# - DEV
# - STAGE
# - PPROD
ENV='DEV'

# Switches ############################################################
# skip_permissions_hardening
DEV_SKIP_PERMISSIONS_HARDENING=true
STAGE_SKIP_PERMISSIONS_HARDENING=true
PROD_SKIP_PERMISSIONS_HARDENING=false

# update_free_access
DEV_UPDATE_FREE_ACCESS=true
STAGE_UPDATE_FREE_ACCESS=true
PROD_UPDATE_FREE_ACCESS=false

# rebuild_access
DEV_REBUILD_ACCESS=true
STAGE_REBUILD_ACCESS=true
PROD_REBUILD_ACCESS=false


# tmp files path ########################################################
DEV_FILE_TEMP_PATH='/Volumes/webroot/webtmp'
STAGE_FILE_TEMP_PATH='~/tmp'
PROD_FILE_TEMP_PATH='~/tmp'


# Site Name ##############################################################
DEV_SITE_NAME='DEV ash'
STAGE_SITE_NAME='STAGE ash'
PROD_SITE_NAME='ash'


# DB Settings ########################################################
# DEV DB Secrets
DEV_DB_NAME='ash'
DEV_DB_USER='dbuser'
DEV_DB_PASS='dbpass'

# STAGE DB Secrets
STAGE_DB_NAME='ash'
STAGE_DB_USER='paneldbuser'
STAGE_DB_PASS='paneldbpass'

# PROD DB Secrets
PROD_DB_NAME='ash'
PROD_DB_USER='dbuser'
PROD_DB_PASS='dbpass'

Die Daten hierin sind nur Beispieldaten zur Veranschaulichung.

Die settings.php Datei(en)

Da man die settings.php Datei durch Kopieren der Datei default.settings.php erzeugt, hat man in dieser alle ausführlichen Kommentare zum Nachschlagen, falls es notwendig ist.

Als erstes habe ich aus der settings.php Datei also alle langen Kommentare rausgelöscht und die Konfigurationen aufgeteilt in "gilt bei allen Umgebungen" und "ist Umgebungs-spezifisch".

<?php
$env = getenv('ENV');

// @codingStandardsIgnoreFile

$databases = [];
$settings['config_sync_directory'] = '../config/sync';
$settings['hash_salt'] = getenv('HASH_SALT');
# $settings['deployment_identifier'] = \Drupal::VERSION;
# $settings['update_fetch_with_http_fallback'] = TRUE;
# $settings['omit_vary_cookie'] = TRUE;
# $settings['cache_ttl_4xx'] = 3600;
# $settings['form_cache_expiration'] = 21600;
# $settings['class_loader_auto_detect'] = FALSE;
# $settings['allow_authorize_operations'] = FALSE;
# $settings['file_chmod_directory'] = 0775;
# $settings['file_chmod_file'] = 0664;
# $settings['file_public_base_url'] = 'http://downloads.example.com/files';
# $settings['file_public_path'] = 'sites/default/files';
$settings['file_private_path'] = '../private';
# $settings['session_write_interval'] = 180;
# $settings['maintenance_theme'] = 'bartik';
# $config['user.settings']['anonymous'] = 'Visitor';

# $config['system.performance']['fast_404']['exclude_paths'] = '/\/(?:styles)|(?:system\/files)\//';
# $config['system.performance']['fast_404']['paths'] = '/\.(?:txt|png|gif|jpe?g|css|js|ico|swf|flv|cgi|bat|pl|dll|exe|asp)$/i';
# $config['system.performance']['fast_404']['html'] = '<!DOCTYPE html><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL "@path" was not found on this server.</p></body></html>';

$settings['container_yamls'][] = $app_root . '/' . $site_path . '/services.yml';
# $settings['container_base_class'] = '\Drupal\Core\DependencyInjection\Container';
# $settings['yaml_parser_class'] = NULL;
$settings['file_scan_ignore_directories'] = [
  'node_modules',
  'bower_components',
];
$settings['entity_update_batch_size'] = 50;
$settings['entity_update_backup'] = TRUE;
$settings['migrate_node_migrate_type_classic'] = FALSE;

/*
 * **************************************************************
 * environment specific settings
 */

/*
 * settings for PROD environment
 */
 $settings['skip_permissions_hardening'] = getenv('PROD_SKIP_PERMISSIONS_HARDENING');
 $settings['update_free_access'] = getenv('PROD_UPDATE_FREE_ACCESS');
 $settings['rebuild_access'] = getenv('PROD_REBUILD_ACCESS');

 $settings['file_temp_path'] = getenv('PROD_FILE_TEMP_PATH');

 $config['system.site']['name'] = getenv('PROD_SITE_NAME');
 $config['system.logging']['error_level'] = 'verbose';

 $databases['default']['default'] = array (
   'database' => getenv('PROD_DB_NAME'),
   'username' => getenv('PROD_DB_USER'),
   'password' => getenv('PROD_DB_PASS'),
   'prefix' => '',
   'host' => 'localhost',
   'port' => '3306',
   'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
   'driver' => 'mysql',
 );


/*
 * if other Environment, overload values for PROD.
 */
switch( $env )
{
  case 'DEV':
  case 'STAGE':
    $path = $app_root . '/' . $site_path . '/settings.' . strtolower( $env ) . '.php';

    if( file_exists( $path ))
    {
      include $path;
    }

    break;
}

Als "Umgebungs-spezifisch" habe ich also für mich folgende Dinge rausgepickt:

  • skip_permissions_hardening
  • update_free_access
  • rebuild_access
  • file_temp_path
  • system.site name
  • system.logging error_level
  • Datenbankverbindung

Die Werte für die Produktionsumgebung stehen direkt in der settings.php. Durch die Lade-Anweisung unterhalb und die weiteren Dateien settings.dev.php und settings.stage.php werden diese Werte überschrieben, falls der entsprechende Schalter "ENV" gesetzt ist.

Die Lade-Anweisung ist absichtlich super-flexibel gehalten, um Raum für Ideen zu schaffen. In den case-Zweigen könnten spezielle andere Dinge aufgerufen werden, die nur für eine bestimmte Umgebung gelten sollen.

Als Beispiel fällt mir hierzu das Hosting bei Pantheon ein: Hier werden die Site-Dateien nicht unter sites/default/files abgelegt, sondern in einem Ordner außerhalb des Webroot, der via Symlink auf /sites/default/files zeigt. So kann man das Projekt einfacher mit oder ohne diese Dateien kopieren und bearbeiten.

Entsprechend sind die ggf. nachzuladenden Dateien klein, hier settings.dev.php und settings.stage.php:

<?php
/* settings.dev.php */
$settings['skip_permissions_hardening'] = getenv('DEV_SKIP_PERMISSIONS_HARDENING');
$settings['update_free_access'] = getenv('DEV_UPDATE_FREE_ACCESS');
$settings['rebuild_access'] = getenv('DEV_REBUILD_ACCESS');

$settings['file_temp_path'] = getenv('DEV_FILE_TEMP_PATH');

$config['system.site']['name'] = getenv('DEV_SITE_NAME');
$config['system.logging']['error_level'] = 'verbose';

$databases['default']['default'] = array (
  'database' => getenv('DEV_DB_NAME'),
  'username' => getenv('DEV_DB_USER'),
  'password' => getenv('DEV_DB_PASS'),
  'prefix' => '',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);
<?php
/* settings.stage.php */
$settings['skip_permissions_hardening'] = getenv('STAGE_SKIP_PERMISSIONS_HARDENING');
$settings['update_free_access'] = getenv('STAGE_UPDATE_FREE_ACCESS');
$settings['rebuild_access'] = getenv('STAGE_REBUILD_ACCESS');

$settings['file_temp_path'] = getenv('STAGE_FILE_TEMP_PATH');

$config['system.site']['name'] = getenv('STAGE_SITE_NAME');
$config['system.logging']['error_level'] = 'verbose';

$databases['default']['default'] = array (
  'database' => getenv('STAGE_DB_NAME'),
  'username' => getenv('STAGE_DB_USER'),
  'password' => getenv('STAGE_DB_PASS'),
  'prefix' => '',
  'host' => 'localhost',
  'port' => '3306',
  'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
  'driver' => 'mysql',
);

Deployment

So lassen sich nun beliebig viele Umgebungen definieren. Die .env Datei ist quasi die Fernsteuerung für die Umgebung. Durch Setzen des Schalters auf einen anderen Wert erwartet die Anwendung (Drupal) andere Werte für Datenbankverbindung etc.

Ich werde wahrscheinlich auch die Werte für trusted_hosts als Umgebungsspezifisch deklarieren und mit in die entsprechenden Abschnitte einbringen. Das Hosting auf meinem lokalen Entwicklungsserver erwartet z.B. projektname.sites.test als Domainnamen, während der Stage-Server auf projektname.realedomain.de ausgelegt ist. In der Produktionsumgebung auf dem Hosting des Kunden sieht es dann wieder anders aus: domainname.de.

Die bearbeitete Anwendung lässt sich nun jederzeit deployen und zwischen den verschiedenen Umgebungs-Servern hin- und herkopieren. Zum Setzen der richtigen Werte für die jeweilige Umgebung editiere ich einfach die .env Datei und setze den richtigen Schalter.

Ich finde das sehr praktisch und ich hoffe, dass Drupal zukünftig wieder phpdotenv in das empfohlene Composer-Projekt aufnimmt.

 

Sektor