From 1e3d61ab6f588a09ad05546c5531cc52f6cd8295 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Sun, 15 Mar 2026 18:38:03 -0700 Subject: [PATCH] Add env() helper for cross-scope environment variable access this.env is only accessible within Application.cfc and its includes. This copies this.env into application.env at app start and adds an env(name, default) helper to Global.cfc so controllers, models, and views can read .env values. Falls back to server.system.environment for cross-engine compatibility. Closes #1783 Co-Authored-By: Claude Opus 4.6 --- public/Application.cfc | 1 + tests/specs/functional/EnvHelperSpec.cfc | 52 ++++++++++++++++++++++++ vendor/wheels/Global.cfc | 23 +++++++++++ 3 files changed, 76 insertions(+) create mode 100644 tests/specs/functional/EnvHelperSpec.cfc diff --git a/public/Application.cfc b/public/Application.cfc index 02e3d04f1..da14b705e 100644 --- a/public/Application.cfc +++ b/public/Application.cfc @@ -91,6 +91,7 @@ component output="false" { include "../config/app.cfm"; function onApplicationStart() { + application.env = duplicate(this.env); injector = new wheels.Injector("wheels.Bindings"); /* wheels/global object */ diff --git a/tests/specs/functional/EnvHelperSpec.cfc b/tests/specs/functional/EnvHelperSpec.cfc new file mode 100644 index 000000000..887a65d5b --- /dev/null +++ b/tests/specs/functional/EnvHelperSpec.cfc @@ -0,0 +1,52 @@ +component extends="wheels.WheelsTest" { + + function run() { + + describe("env()", () => { + + afterEach(() => { + // Clean up any test keys we added + StructDelete(application, "env"); + }) + + it("returns value from application.env when present", () => { + application.env = {TEST_KEY: "from_dotenv"}; + expect(env("TEST_KEY")).toBe("from_dotenv"); + }) + + it("returns default when key is not found anywhere", () => { + application.env = {}; + expect(env("NONEXISTENT_KEY_12345")).toBe(""); + }) + + it("returns custom default when key is not found", () => { + application.env = {}; + expect(env("NONEXISTENT_KEY_12345", "custom_default")).toBe("custom_default"); + }) + + it("prefers application.env over system environment", () => { + // If a key exists in both, application.env should win + application.env = {PATH: "app_path_override"}; + expect(env("PATH")).toBe("app_path_override"); + }) + + it("falls back to server.system.environment", () => { + // PATH should exist in system environment on all platforms + application.env = {}; + local.result = env("PATH"); + expect(local.result).notToBe(""); + }) + + }) + + describe("application.env population", () => { + + it("application.env is populated from Application.cfc this.env", () => { + expect(StructKeyExists(application, "env")).toBeTrue(); + expect(IsStruct(application.env)).toBeTrue(); + }) + + }) + + } +} diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 38a668aec..4bb158a13 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -408,6 +408,29 @@ component output="false" { return $get(argumentCollection = arguments); } + /** + * Returns the value of an environment variable. Checks application.env (loaded from .env files) first, then falls back to system environment variables (server.system.environment). Returns the default if the variable is not found in either location. + * + * [section: Configuration] + * [category: Miscellaneous Functions] + * + * @name The environment variable name to look up. + * @default Value to return if the variable is not found. + */ + public any function env(required string name, any default="") { + if (StructKeyExists(application, "env") && StructKeyExists(application.env, arguments.name)) { + return application.env[arguments.name]; + } + if ( + StructKeyExists(server, "system") + && StructKeyExists(server.system, "environment") + && StructKeyExists(server.system.environment, arguments.name) + ) { + return server.system.environment[arguments.name]; + } + return arguments.default; + } + /** * Use to configure a global setting or set a default for a function. *