diff --git a/fixtures/custom_libdir_hash/.bp-config/options.json b/fixtures/custom_libdir_hash/.bp-config/options.json
new file mode 100644
index 000000000..dfa573b3b
--- /dev/null
+++ b/fixtures/custom_libdir_hash/.bp-config/options.json
@@ -0,0 +1,4 @@
+{
+ "WEB_SERVER": "httpd",
+ "LIBDIR": "lib"
+}
diff --git a/fixtures/custom_libdir_hash/.bp-config/php/php.ini.d/custom.ini b/fixtures/custom_libdir_hash/.bp-config/php/php.ini.d/custom.ini
new file mode 100644
index 000000000..ddbc9721b
--- /dev/null
+++ b/fixtures/custom_libdir_hash/.bp-config/php/php.ini.d/custom.ini
@@ -0,0 +1,2 @@
+; Custom PHP configuration using #{LIBDIR} placeholder
+include_path = ".:/usr/share/php:#{HOME}/#{LIBDIR}"
diff --git a/fixtures/custom_libdir_hash/index.php b/fixtures/custom_libdir_hash/index.php
new file mode 100644
index 000000000..61ace196d
--- /dev/null
+++ b/fixtures/custom_libdir_hash/index.php
@@ -0,0 +1,2 @@
+
+ Options Indexes FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+DirectoryIndex index.php index.html
+TypesConfig conf/mime.types
+
+
+ SetHandler "proxy:fcgi://@{PHP_FPM_LISTEN}"
+
+
+ErrorLog logs/error.log
+LogLevel warn
diff --git a/fixtures/custom_webdir_config/.bp-config/options.json b/fixtures/custom_webdir_config/.bp-config/options.json
new file mode 100644
index 000000000..c81b4a18d
--- /dev/null
+++ b/fixtures/custom_webdir_config/.bp-config/options.json
@@ -0,0 +1,4 @@
+{
+ "WEB_SERVER": "httpd",
+ "WEBDIR": "htdocs"
+}
diff --git a/fixtures/custom_webdir_config/index.php b/fixtures/custom_webdir_config/index.php
new file mode 100644
index 000000000..61ace196d
--- /dev/null
+++ b/fixtures/custom_webdir_config/index.php
@@ -0,0 +1,2 @@
+
+ Options Indexes FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+DirectoryIndex index.php index.html
+TypesConfig conf/mime.types
+
+
+ SetHandler "proxy:fcgi://#{PHP_FPM_LISTEN}"
+
+
+ErrorLog logs/error.log
+LogLevel warn
diff --git a/fixtures/httpd_hash_placeholder/.bp-config/options.json b/fixtures/httpd_hash_placeholder/.bp-config/options.json
new file mode 100644
index 000000000..9209b39ff
--- /dev/null
+++ b/fixtures/httpd_hash_placeholder/.bp-config/options.json
@@ -0,0 +1,5 @@
+{
+ "WEB_SERVER": "httpd",
+ "WEBDIR": "htdocs",
+ "ADMIN_EMAIL": "test@example.com"
+}
diff --git a/fixtures/httpd_hash_placeholder/index.php b/fixtures/httpd_hash_placeholder/index.php
new file mode 100644
index 000000000..61ace196d
--- /dev/null
+++ b/fixtures/httpd_hash_placeholder/index.php
@@ -0,0 +1,2 @@
+
+ Options Indexes FollowSymLinks
+ AllowOverride All
+ Require all granted
+
+
+DirectoryIndex index.php index.html
+TypesConfig conf/mime.types
+
+
+ SetHandler "proxy:fcgi://#{PHP_FPM_LISTEN}"
+
+
+ErrorLog logs/error.log
+LogLevel warn
diff --git a/fixtures/mixed_placeholder_syntax/.bp-config/options.json b/fixtures/mixed_placeholder_syntax/.bp-config/options.json
new file mode 100644
index 000000000..e16ca91b4
--- /dev/null
+++ b/fixtures/mixed_placeholder_syntax/.bp-config/options.json
@@ -0,0 +1,4 @@
+{
+ "WEB_SERVER": "httpd",
+ "ADMIN_EMAIL": "test@example.com"
+}
diff --git a/fixtures/mixed_placeholder_syntax/index.php b/fixtures/mixed_placeholder_syntax/index.php
new file mode 100644
index 000000000..61ace196d
--- /dev/null
+++ b/fixtures/mixed_placeholder_syntax/index.php
@@ -0,0 +1,2 @@
+
/tmp/app/php"))
+ })
+ })
+
+ context("WEBDIR placeholder in user configs", func() {
+ it("resolves WEBDIR in custom httpd.conf", func() {
+ deployment, logs, err := platform.Deploy.
+ WithEnv(map[string]string{
+ "BP_DEBUG": "1",
+ }).
+ Execute(name, filepath.Join(fixtures, "custom_webdir_config"))
+ Expect(err).NotTo(HaveOccurred(), logs.String)
+
+ Expect(logs).NotTo(ContainSubstring("DocumentRoot '/home/vcap/app/#{WEBDIR}' is not a directory"))
+ Expect(logs).NotTo(ContainSubstring("DocumentRoot '/home/vcap/app/@{WEBDIR}' is not a directory"))
+
+ Eventually(deployment).Should(Serve(
+ ContainSubstring("PHP Version"),
+ ))
+ })
+
+ it("resolves custom WEBDIR value from options.json", func() {
+ deployment, logs, err := platform.Deploy.
+ WithEnv(map[string]string{
+ "BP_DEBUG": "1",
+ }).
+ Execute(name, filepath.Join(fixtures, "custom_webdir_value"))
+ Expect(err).NotTo(HaveOccurred(), logs.String)
+
+ // App has WEBDIR set to "public" in options.json
+ Eventually(deployment).Should(Serve(
+ ContainSubstring("Custom WEBDIR: public"),
+ ))
+ })
+ })
+ }
+}
+
+// Helper function to read response body
+func ReadAll(r io.Reader) []byte {
+ data, _ := io.ReadAll(r)
+ return data
+}
diff --git a/src/php/options/options.go b/src/php/options/options.go
index 49200c185..93339ffee 100644
--- a/src/php/options/options.go
+++ b/src/php/options/options.go
@@ -211,8 +211,22 @@ func (o *Options) validate() error {
}
// GetPHPVersion returns the PHP version to use, either from user config or default
+// Resolves placeholders like {PHP_83_LATEST} to actual versions
func (o *Options) GetPHPVersion() string {
if o.PHPVersion != "" {
+ // Check if it's a placeholder like {PHP_83_LATEST}
+ if strings.HasPrefix(o.PHPVersion, "{") && strings.HasSuffix(o.PHPVersion, "}") {
+ // Extract the placeholder name (remove { and })
+ placeholderName := strings.TrimPrefix(strings.TrimSuffix(o.PHPVersion, "}"), "{")
+
+ // Look up the actual version from PHPVersions map
+ if actualVersion, exists := o.PHPVersions[placeholderName]; exists {
+ return actualVersion
+ }
+
+ // If placeholder not found, return as-is (will fail with clear error message)
+ // This allows the buildpack to show which placeholder was invalid
+ }
return o.PHPVersion
}
return o.PHPDefault
diff --git a/src/php/options/options_test.go b/src/php/options/options_test.go
index 76238c7ef..e696793f2 100644
--- a/src/php/options/options_test.go
+++ b/src/php/options/options_test.go
@@ -259,3 +259,84 @@ func TestGetPHPVersion(t *testing.T) {
t.Errorf("Expected 8.2.15, got %s", opts.GetPHPVersion())
}
}
+
+func TestGetPHPVersion_PlaceholderResolution(t *testing.T) {
+ tests := []struct {
+ name string
+ phpVersion string
+ phpVersions map[string]string
+ expected string
+ }{
+ {
+ name: "Resolves {PHP_83_LATEST} placeholder",
+ phpVersion: "{PHP_83_LATEST}",
+ phpVersions: map[string]string{
+ "PHP_81_LATEST": "8.1.34",
+ "PHP_82_LATEST": "8.2.29",
+ "PHP_83_LATEST": "8.3.30",
+ },
+ expected: "8.3.30",
+ },
+ {
+ name: "Resolves {PHP_82_LATEST} placeholder",
+ phpVersion: "{PHP_82_LATEST}",
+ phpVersions: map[string]string{
+ "PHP_81_LATEST": "8.1.34",
+ "PHP_82_LATEST": "8.2.29",
+ "PHP_83_LATEST": "8.3.30",
+ },
+ expected: "8.2.29",
+ },
+ {
+ name: "Resolves {PHP_81_LATEST} placeholder",
+ phpVersion: "{PHP_81_LATEST}",
+ phpVersions: map[string]string{
+ "PHP_81_LATEST": "8.1.34",
+ "PHP_82_LATEST": "8.2.29",
+ "PHP_83_LATEST": "8.3.30",
+ },
+ expected: "8.1.34",
+ },
+ {
+ name: "Returns invalid placeholder as-is (for clear error message)",
+ phpVersion: "{PHP_99_LATEST}",
+ phpVersions: map[string]string{
+ "PHP_81_LATEST": "8.1.34",
+ "PHP_82_LATEST": "8.2.29",
+ "PHP_83_LATEST": "8.3.30",
+ },
+ expected: "{PHP_99_LATEST}",
+ },
+ {
+ name: "Returns exact version without placeholder syntax",
+ phpVersion: "8.3.21",
+ phpVersions: map[string]string{
+ "PHP_83_LATEST": "8.3.30",
+ },
+ expected: "8.3.21",
+ },
+ {
+ name: "Returns version with partial placeholder syntax (not a placeholder)",
+ phpVersion: "PHP_83_LATEST",
+ phpVersions: map[string]string{
+ "PHP_83_LATEST": "8.3.30",
+ },
+ expected: "PHP_83_LATEST",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ opts := &options.Options{
+ PHPDefault: "8.1.32",
+ PHPVersion: tt.phpVersion,
+ PHPVersions: tt.phpVersions,
+ }
+
+ result := opts.GetPHPVersion()
+ if result != tt.expected {
+ t.Errorf("Expected %s, got %s", tt.expected, result)
+ }
+ })
+ }
+}
|