From becdb010e677bafb15dcd9c8ddd82f9c1c22d3b6 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 6 Aug 2026 18:30:42 +0200 Subject: [PATCH 1/3] Move env config to root .env and make app_local.php optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infrastructure variables live in project-root .env (loaded by bootstrap); app.php reads them via env(). Installer creates .env and writes the salt there. app_local.php is no longer generated on install—copy from the example only when local overrides are needed. --- .env.example | 36 ++++++++++++++++++++++++++++++++++ .gitignore | 2 +- README.md | 17 +++++++++++++--- config/.env.example | 41 --------------------------------------- config/app.php | 16 ++++++++++----- config/bootstrap.php | 19 +++++++++--------- src/Console/Installer.php | 30 ++++++++++++++-------------- 7 files changed, 87 insertions(+), 74 deletions(-) create mode 100644 .env.example delete mode 100644 config/.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..78be70b209 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Environment variables for the PHP application. +# Copy to `.env` in the project root. Loaded via config/bootstrap.php. +# +# Optional PHP-level overrides (debug mode, datasource tweaks): copy +# config/app_local.example.php to config/app_local.php (not created on install). +# +# Do not commit `.env` to source control. +export APP_NAME="__APP_NAME__" +export APP_ENCODING="UTF-8" +export APP_DEFAULT_LOCALE="en_US" +export APP_DEFAULT_TIMEZONE="UTC" +export APP_FULL_BASE_URL="https://example.com" +export SECURITY_SALT="__SALT__" + +export DB_HOST="localhost" +export DB_USERNAME="my_app" +export DB_PASSWORD="secret" +export DB_DATABASE="my_app" +export DATABASE_URL="mysql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_DATABASE}?encoding=utf8mb4&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false" +export DATABASE_TEST_URL="sqlite://127.0.0.1/tmp/tests.sqlite" + +# Uncomment these to define cache configuration via environment variables. +#export CACHE_DURATION="+2 minutes" +#export CACHE_DEFAULT_URL="file:///path/to/tmp/cache/?prefix=${APP_NAME}_default_&duration=${CACHE_DURATION}" +#export CACHE_CAKECORE_URL="file:///path/to/tmp/cache/persistent?prefix=${APP_NAME}_cake_translations_&serialize=true&duration=${CACHE_DURATION}" +#export CACHE_CAKEMODEL_URL="file:///path/to/tmp/cache/models?prefix=${APP_NAME}_cake_model_&serialize=true&duration=${CACHE_DURATION}" + +# Uncomment these to define email transport configuration via environment variables. +#export EMAIL_HOST="localhost" +#export EMAIL_PORT="25" +#export EMAIL_TRANSPORT_DEFAULT_URL="" + +# Uncomment these to define logging configuration via environment variables. +#export LOG_DEBUG_URL="file:///path/to/logs/?levels[]=notice&levels[]=info&levels[]=debug&file=debug" +#export LOG_ERROR_URL="file:///path/to/logs/?levels[]=warning&levels[]=error&levels[]=critical&levels[]=alert&levels[]=emergency&file=error" diff --git a/.gitignore b/.gitignore index 03ee77c4f4..92ca4c98f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # CakePHP specific files # ########################## /config/app_local.php -/config/.env +/.env /logs/* /tmp/* /vendor/* diff --git a/README.md b/README.md index 5a614fa263..59837b2f2b 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,20 @@ automated upgrades, so you have to do any updates manually. ## Configuration -Read and edit the environment specific `config/app_local.php` and set up the -`'Datasources'` and any other configuration relevant for your application. -Other environment agnostic settings can be changed in `config/app.php`. +```text +.env infrastructure vars + └─ app.php application config (reads `.env` via env()) + └─ app_local.php local application overrides (gitignored, manual) +``` + +| File | Role | +|------|------| +| `.env.example` → `.env` | Environment variables: database, salt, app name, URLs (created on install) | +| `config/app.php` | Base application config; reads `.env` through `env()` | +| `config/app_local.example.php` → `app_local.php` | Optional local overrides on top of `app.php` (stock CakePHP) | + +**`.env`** — infrastructure and deployment variables. +**`app_local.php`** — application tuning (debug defaults, datasource/email overrides). Not created on install; copy from `config/app_local.example.php` if needed. ## Layout diff --git a/config/.env.example b/config/.env.example deleted file mode 100644 index 793f83b109..0000000000 --- a/config/.env.example +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -# Used as a default to seed config/.env which -# enables you to use environment variables to configure -# the aspects of your application that vary by -# environment. -# -# Having this file in production is considered a **SECURITY RISK** and also decreases -# the bootstrap performance of your application. -# -# To use this file, first copy it into `config/.env`. Also ensure the related -# code block for loading this file is uncommented in `config/bootstrap.php` -# -# In development .env files are parsed by PHP -# and set into the environment. This provides a simpler -# development workflow over standard environment variables. -export APP_NAME="__APP_NAME__" -export DEBUG="true" -export APP_ENCODING="UTF-8" -export APP_DEFAULT_LOCALE="en_US" -export APP_DEFAULT_TIMEZONE="UTC" -# SECURITY: Set this to your domain to prevent Host Header Injection attacks -# This is REQUIRED in production for password resets and other security features -export APP_FULL_BASE_URL="https://example.com" -export SECURITY_SALT="__SALT__" - -# Uncomment these to define cache configuration via environment variables. -#export CACHE_DURATION="+2 minutes" -#export CACHE_DEFAULT_URL="file:///path/to/tmp/cache/?prefix=${APP_NAME}_default_&duration=${CACHE_DURATION}" -#export CACHE_CAKECORE_URL="file:///path/to/tmp/cache/persistent?prefix=${APP_NAME}_cake_translations_&serialize=true&duration=${CACHE_DURATION}" -#export CACHE_CAKEMODEL_URL="file:///path/to/tmp/cache/models?prefix=${APP_NAME}_cake_model_&serialize=true&duration=${CACHE_DURATION}" - -# Uncomment these to define email transport configuration via environment variables. -#export EMAIL_TRANSPORT_DEFAULT_URL="" - -# Uncomment these to define database configuration via environment variables. -#export DATABASE_URL="mysql://my_app:secret@localhost/${APP_NAME}?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false" -#export DATABASE_TEST_URL="mysql://my_app:secret@localhost/test_${APP_NAME}?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false" - -# Uncomment these to define logging configuration via environment variables. -#export LOG_DEBUG_URL="file:///path/to/logs/?levels[]=notice&levels[]=info&levels[]=debug&file=debug" -#export LOG_ERROR_URL="file:///path/to/logs/?levels[]=warning&levels[]=error&levels[]=critical&levels[]=alert&levels[]=emergency&file=error" diff --git a/config/app.php b/config/app.php index 9876cf421c..e841847c69 100644 --- a/config/app.php +++ b/config/app.php @@ -226,8 +226,8 @@ * The keys host, port, timeout, username, password, client and tls * are used in SMTP transports */ - 'host' => 'localhost', - 'port' => 25, + 'host' => env('EMAIL_HOST', 'localhost'), + 'port' => env('EMAIL_PORT', 25), 'timeout' => 30, /* * It is recommended to set these options through your environment or app_local.php @@ -265,6 +265,8 @@ * Connection information used by the ORM to connect * to your application's datastores. * + * Values are read from the root `.env` file via env(). Override in app_local.php. + * * ### Notes * - Drivers include Mysql Postgres Sqlite Sqlserver * See vendor\cakephp\cakephp\src\Database\Driver for the complete list @@ -281,15 +283,18 @@ * * The values in app_local.php will override any values set here * and should be used for local and per-environment configurations. - * - * Environment variable-based configurations can be loaded here or - * in app_local.php depending on the application's needs. */ 'default' => [ 'className' => Connection::class, 'driver' => Mysql::class, 'persistent' => false, 'timezone' => 'UTC', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT'), + 'username' => env('DB_USERNAME', 'my_app'), + 'password' => env('DB_PASSWORD', 'secret'), + 'database' => env('DB_DATABASE', env('APP_NAME', 'my_app')), + 'url' => env('DATABASE_URL', null), /* * For MariaDB/MySQL the internal default changed from utf8 to utf8mb4, aka full utf-8 support @@ -339,6 +344,7 @@ 'quoteIdentifiers' => false, 'log' => false, //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], + 'url' => env('DATABASE_TEST_URL', 'sqlite://127.0.0.1/tmp/tests.sqlite'), ], ], diff --git a/config/bootstrap.php b/config/bootstrap.php index 82a92c60fa..9445b44443 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -55,8 +55,7 @@ /* * See https://github.com/josegonzalez/php-dotenv for API details. * - * Uncomment block of code below if you want to use `.env` file during development. - * You should copy `config/.env.example` to `config/.env` and set/modify the + * You should copy `.env.example` to `.env` and set/modify the * variables as required. * * The purpose of the .env file is to emulate the presence of the environment @@ -66,13 +65,15 @@ * security risks. See https://github.com/josegonzalez/php-dotenv#general-security-information * for more information for recommended practices. */ -// if (!env('APP_NAME') && file_exists(CONFIG . '.env')) { -// $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']); -// $dotenv->parse() -// ->putenv() -// ->toEnv() -// ->toServer(); -// } +if (!env('APP_NAME') && is_readable(ROOT . DS . '.env')) { + if (class_exists(\josegonzalez\Dotenv\Loader::class)) { + (new \josegonzalez\Dotenv\Loader([ROOT . DS . '.env'])) + ->parse() + ->putenv() + ->toEnv() + ->toServer(); + } +} /* * Initializes default Config store and loads the main configuration file (app.php) diff --git a/src/Console/Installer.php b/src/Console/Installer.php index 6ef3c2dd84..0052bf74d0 100644 --- a/src/Console/Installer.php +++ b/src/Console/Installer.php @@ -61,7 +61,7 @@ public static function postInstall(Event $event): void $rootDir = dirname(__DIR__, 2); - static::createAppLocalConfig($rootDir, $io); + static::createEnvFile($rootDir, $io); static::createWritableDirectories($rootDir, $io); static::setFolderPermissions($rootDir, $io); @@ -73,19 +73,19 @@ public static function postInstall(Event $event): void } /** - * Create config/app_local.php file if it does not exist. + * Create .env file if it does not exist. * * @param string $dir The application's root directory. * @param \Composer\IO\IOInterface $io IO interface to write to console. * @return void */ - public static function createAppLocalConfig(string $dir, IOInterface $io): void + public static function createEnvFile(string $dir, IOInterface $io): void { - $appLocalConfig = $dir . '/config/app_local.php'; - $appLocalConfigTemplate = $dir . '/config/app_local.example.php'; - if (!file_exists($appLocalConfig)) { - copy($appLocalConfigTemplate, $appLocalConfig); - $io->write('Created `config/app_local.php` file'); + $envFile = $dir . '/.env'; + $envTemplate = $dir . '/.env.example'; + if (!file_exists($envFile)) { + copy($envTemplate, $envFile); + $io->write('Created `.env` file'); } } @@ -183,7 +183,7 @@ public static function setFolderPermissions(string $dir, IOInterface $io): void public static function setSecuritySalt(string $dir, IOInterface $io): void { $newKey = hash('sha256', Security::randomBytes(64)); - static::setSecuritySaltInFile($dir, $io, $newKey, 'app_local.php'); + static::setSecuritySaltInFile($dir, $io, $newKey, '.env'); } /** @@ -197,10 +197,10 @@ public static function setSecuritySalt(string $dir, IOInterface $io): void */ public static function setSecuritySaltInFile(string $dir, IOInterface $io, string $newKey, string $file): void { - $config = $dir . '/config/' . $file; + $config = $dir . '/' . $file; $content = file_get_contents($config); if ($content === false) { - $io->write('Config file not readable or not found: config/' . $file); + $io->write('Config file not readable or not found: ' . $file); return; } @@ -215,7 +215,7 @@ public static function setSecuritySaltInFile(string $dir, IOInterface $io, strin $result = file_put_contents($config, $content); if ($result) { - $io->write('Updated Security.salt value in config/' . $file); + $io->write('Updated Security.salt value in ' . $file); return; } @@ -233,10 +233,10 @@ public static function setSecuritySaltInFile(string $dir, IOInterface $io, strin */ public static function setAppNameInFile(string $dir, IOInterface $io, string $appName, string $file): void { - $config = $dir . '/config/' . $file; + $config = $dir . '/' . $file; $content = file_get_contents($config); if ($content === false) { - $io->write('Config file not readable or not found: config/' . $file); + $io->write('Config file not readable or not found: ' . $file); return; } @@ -251,7 +251,7 @@ public static function setAppNameInFile(string $dir, IOInterface $io, string $ap $result = file_put_contents($config, $content); if ($result) { - $io->write('Updated __APP_NAME__ value in config/' . $file); + $io->write('Updated __APP_NAME__ value in ' . $file); return; } From 66573fb6b7d6f1acdb904b3360b4fcb29a66476d Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 6 Aug 2026 18:44:27 +0200 Subject: [PATCH 2/3] Update .env.example --- .env.example | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 78be70b209..ba23825397 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,29 @@ #!/usr/bin/env bash -# Environment variables for the PHP application. -# Copy to `.env` in the project root. Loaded via config/bootstrap.php. +# Used as a default to seed `.env` which +# enables you to use environment variables to configure +# the aspects of your application that vary by +# environment. # -# Optional PHP-level overrides (debug mode, datasource tweaks): copy -# config/app_local.example.php to config/app_local.php (not created on install). +# Having this file in production is considered a **SECURITY RISK** and also decreases +# the bootstrap performance of your application. # -# Do not commit `.env` to source control. +# To use this file, first copy it into `.env` in the project root. It is loaded via +# `config/bootstrap.php`. Optional PHP overrides: copy `config/app_local.example.php` +# to `config/app_local.php` (not created on install). +# +# In development .env files are parsed by PHP +# and set into the environment. This provides a simpler +# development workflow over standard environment variables. export APP_NAME="__APP_NAME__" +export DEBUG="true" export APP_ENCODING="UTF-8" export APP_DEFAULT_LOCALE="en_US" export APP_DEFAULT_TIMEZONE="UTC" +# SECURITY: Set this to your domain to prevent Host Header Injection attacks +# This is REQUIRED in production for password resets and other security features export APP_FULL_BASE_URL="https://example.com" export SECURITY_SALT="__SALT__" -export DB_HOST="localhost" -export DB_USERNAME="my_app" -export DB_PASSWORD="secret" -export DB_DATABASE="my_app" -export DATABASE_URL="mysql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_DATABASE}?encoding=utf8mb4&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false" -export DATABASE_TEST_URL="sqlite://127.0.0.1/tmp/tests.sqlite" - # Uncomment these to define cache configuration via environment variables. #export CACHE_DURATION="+2 minutes" #export CACHE_DEFAULT_URL="file:///path/to/tmp/cache/?prefix=${APP_NAME}_default_&duration=${CACHE_DURATION}" @@ -31,6 +35,14 @@ export DATABASE_TEST_URL="sqlite://127.0.0.1/tmp/tests.sqlite" #export EMAIL_PORT="25" #export EMAIL_TRANSPORT_DEFAULT_URL="" +# Uncomment these to define database configuration via environment variables. +export DB_HOST="localhost" +export DB_USERNAME="my_app" +export DB_PASSWORD="secret" +export DB_DATABASE="my_app" +export DATABASE_URL="mysql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_DATABASE}?encoding=utf8mb4&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false" +export DATABASE_TEST_URL="sqlite://127.0.0.1/tmp/tests.sqlite" + # Uncomment these to define logging configuration via environment variables. #export LOG_DEBUG_URL="file:///path/to/logs/?levels[]=notice&levels[]=info&levels[]=debug&file=debug" #export LOG_ERROR_URL="file:///path/to/logs/?levels[]=warning&levels[]=error&levels[]=critical&levels[]=alert&levels[]=emergency&file=error" From 7e2272d4b1dbe686188d0f03c5aba0bd14fd106f Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 6 Aug 2026 19:03:06 +0200 Subject: [PATCH 3/3] Add option to prevent overwriting existing environment variables --- config/bootstrap.php | 1 + 1 file changed, 1 insertion(+) diff --git a/config/bootstrap.php b/config/bootstrap.php index 9445b44443..9268b8b7d2 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -69,6 +69,7 @@ if (class_exists(\josegonzalez\Dotenv\Loader::class)) { (new \josegonzalez\Dotenv\Loader([ROOT . DS . '.env'])) ->parse() + ->skipExisting() ->putenv() ->toEnv() ->toServer();