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) + } + }) + } +}