This document provides comprehensive coverage of all features supported by the PHP buildpack v5.x, with integration test verification and implementation details. This is for buildpack developers and maintainers.
For end users: See USER_GUIDE.md for how to use these features.
| Category | Features | Test Status | User Docs |
|---|---|---|---|
| Web Servers | HTTPD, Nginx, FPM-only, Custom pools | ✅ Full | ✅ Complete |
| PHP Versions | 8.3.x, 8.2.x, 8.1.x, 8.0.x | ✅ Full | ✅ Complete |
| Extensions | 30+ standard + custom | ✅ Full | ✅ Complete |
| APM | NewRelic, AppDynamics, Dynatrace | ✅ Full | ✅ Complete |
| Sessions | Redis, Memcached | ✅ Complete | |
| Frameworks | CakePHP, Laminas, Symfony, Laravel | ✅ Partial | ✅ Complete |
| Composer | Auto-detect, caching, custom paths | ✅ Full | ✅ Complete |
| Configuration | php.ini, php.ini.d, fpm.d | ✅ Full | ✅ Complete |
| Advanced | Multi-buildpack, extensions | ✅ Full | ✅ Complete |
src/php/integration/
├── web_servers_test.go # Web server configurations
├── modules_test.go # PHP extensions and modules
├── composer_test.go # Composer and dependencies
├── apms_test.go # APM integrations
├── app_frameworks_test.go # Framework support
├── default_test.go # Basic and multi-buildpack
├── python_extension_test.go # Legacy extensions
└── offline_test.go # Offline/cached buildpack
fixtures/
├── with_httpd/ # Apache HTTPD configuration
├── with_nginx/ # Nginx configuration
├── php_with_fpm_d/ # Custom FPM pools
├── php_with_php_ini_d/ # Custom php.ini.d
├── with_amqp/ # AMQP extension
├── with_apcu/ # APCu extension
├── with_phpredis/ # Redis extension
├── with_argon2/ # Argon2 hashing
├── with_compiled_modules/ # User-compiled extensions
├── composer_default/ # Composer workflow
├── cake/ # CakePHP framework
├── laminas/ # Laminas framework
├── json_extension/ # JSON user extension
└── dotnet_core_as_supply_app/ # Multi-buildpack
Test Coverage: ✅ web_servers_test.go
context("PHP app with httpd web server", func() {
it("builds and runs the app", func() {
deployment, _, err := platform.Deploy.Execute(name,
filepath.Join(fixtures, "with_httpd"))
Expect(err).NotTo(HaveOccurred())
Eventually(deployment).Should(Serve(ContainSubstring("PHP Version")))
})
})Implementation:
- Location:
src/php/supply/supply.go-InstallHTTPD() - Config source:
src/php/config/defaults/config/httpd/ - User config:
.bp-config/httpd/ - Placeholders:
@{WEBDIR},@{PHP_FPM_LISTEN},${HOME},${PORT}
Configuration Files:
httpd.conf- Main configurationextra/httpd-modules.conf- Module loading- Custom user configs in
.bp-config/httpd/
Test Coverage: ✅ web_servers_test.go
context("PHP app with nginx web server", func() {
it("builds and runs the app", func() {
deployment, _, err := platform.Deploy.Execute(name,
filepath.Join(fixtures, "with_nginx"))
Expect(err).NotTo(HaveOccurred())
Eventually(deployment).Should(Serve(ContainSubstring("PHP Version")))
})
})Implementation:
- Location:
src/php/supply/supply.go-installNginx() - Config source:
src/php/config/defaults/config/nginx/ - User config:
.bp-config/nginx/ - Runtime variable substitution via sed in start script
- Placeholders:
@{HOME},@{WEBDIR},@{PHP_FPM_LISTEN},${PORT},${TMPDIR}
Test Coverage: ✅ web_servers_test.go
Implementation: WEB_SERVER: "none" option
Use Case: Multi-buildpack scenarios, external web servers
Test Coverage: ✅ web_servers_test.go - "Default PHP web server with fpm.d dir"
Fixture: fixtures/php_with_fpm_d/
Test Verification:
it("builds and runs the app", func() {
Eventually(deployment).Should(Serve(SatisfyAll(
ContainSubstring("TEST_WEBDIR == htdocs"),
ContainSubstring("TEST_HOME_PATH == /home/vcap/app/test/path"),
)))
})Implementation:
- User configs:
.bp-config/php/fpm.d/*.conf - Processed in:
src/php/finalize/finalize.go- with app context - Placeholders:
@{HOME}→/home/vcap/app,@{WEBDIR},@{LIBDIR},@{TMPDIR}
Test File:
; fixtures/php_with_fpm_d/.bp-config/php/fpm.d/test.conf
[www]
env[TEST_HOME_PATH] = @{HOME}/test/path
env[TEST_WEBDIR] = @{WEBDIR}Test Coverage: ✅ modules_test.go
All Extensions Test:
context("app loads all listed extensions", func() {
it("loads the modules", func() {
// Tests loading 30+ extensions simultaneously
ItLoadsAllTheModules(deployment)
})
})Implementation:
- Extension config:
src/php/config/config.go-ProcessPhpIni() - Placeholder replacement:
@{PHP_EXTENSIONS},@{ZEND_EXTENSIONS} - Supply phase processing
Test Coverage: ✅ modules_test.go - "app with amqp module"
Fixture: fixtures/with_amqp/
it("amqp module is loaded", func() {
Eventually(deployment).Should(Serve(ContainSubstring("amqp")))
})composer.json:
{
"require": {
"ext-amqp": "*"
}
}Test Coverage: ✅ modules_test.go - "app with APCu module"
Fixture: fixtures/with_apcu/
it("apcu module is loaded", func() {
Eventually(deployment).Should(Serve(ContainSubstring("apcu")))
})Test Coverage: ✅ modules_test.go - "app with phpredis module"
Fixture: fixtures/with_phpredis/
it("logs that phpredis could not connect to server", func() {
// Extension loads, connection test expected to fail without Redis service
Eventually(logs).Should(ContainSubstring("Connection refused"))
})Test Coverage: ✅ modules_test.go - "app with argon2 module"
Fixture: fixtures/with_argon2/
it("argon2 module is loaded", func() {
Eventually(deployment).Should(Serve(ContainSubstring("argon2")))
})Test Coverage: ✅ modules_test.go - "app with compiled modules in PHP_EXTENSIONS"
Fixture: fixtures/with_compiled_modules/
Implementation: User-provided .so files in .bp-config/php/lib/
Test Coverage: ✅ composer_test.go - "default PHP composer app"
Fixture: fixtures/composer_default/
it("loads and installs dependencies", func() {
Eventually(deployment).Should(Serve(ContainSubstring("Guzzle")))
})Implementation:
- Detection:
src/php/extensions/composer/composer.go-Detect() - Installation:
Install()method - Caching:
.bp/composer/cache directory - Command:
composer install --no-dev --no-progress --no-interaction
Test Coverage: ✅ composer_test.go
Fixture: fixtures/composer_custom_path/
Implementation: COMPOSER_PATH environment variable
Test Coverage: ✅ composer_test.go - "deployed with invalid COMPOSER_GITHUB_OAUTH_TOKEN"
it("validates token and skips if invalid", func() {
Eventually(logs).Should(ContainSubstring("Invalid GitHub token"))
})Implementation:
- Token validation:
setupGitHubToken()method - Rate limit check: GitHub API call
- Graceful fallback if invalid
Test Coverage: ✅ apms_test.go - "app with newrelic configured"
Extension: src/php/extensions/newrelic/
it("loads newrelic", func() {
Eventually(deployment).Should(Serve(ContainSubstring("newrelic")))
})Implementation:
- VCAP_SERVICES detection during supply phase
- Agent download from NewRelic
- License key extraction
- Profile.d script creation:
newrelic-env.sh
Profile.d Script:
if [[ -z "${NEWRELIC_LICENSE:-}" ]]; then
export NEWRELIC_LICENSE=$(echo $VCAP_SERVICES | jq -r '.newrelic[0].credentials.licenseKey')
fiTest Coverage: ✅ apms_test.go - "app with appdynamics configured"
Extension: src/php/extensions/appdynamics/
Implementation:
- Service binding detection
- Agent download
- Controller configuration
- Tier/node name configuration
Test Coverage: ✅ apms_test.go - "multiple dynatrace services"
Implementation: Service binding detection and agent setup
Test Coverage: src/php/extensions/sessions/
Implementation:
func (e *SessionsExtension) loadSession(ctx *extensions.Context) BaseSetup {
for _, services := range ctx.VcapServices {
for _, service := range services {
if strings.Contains(strings.ToLower(service.Name), "redis") {
return &RedisSetup{Service: service}
}
}
}
}Configuration:
- Auto-detects Redis service in VCAP_SERVICES
- Writes
session.save_handler = redis - Configures
session.save_pathfrom credentials
Test Coverage: src/php/extensions/sessions/
Implementation: Similar to Redis, detects "memcache" in service name
Test Coverage: ✅ app_frameworks_test.go - "CakePHP"
Fixture: fixtures/cake/
context("CakePHP", func() {
it("builds and serves the application", func() {
Eventually(deployment).Should(Serve(ContainSubstring("CakePHP")))
})
})Test Coverage: ✅ app_frameworks_test.go - "Laminas"
Fixture: fixtures/laminas/
context("Laminas", func() {
it("builds and serves the application", func() {
Eventually(deployment).Should(Serve(ContainSubstring("Laminas")))
})
})Test Coverage:
Test Coverage: ✅ modules_test.go - "app with custom conf files in php.ini.d dir"
Fixture: fixtures/php_with_php_ini_d/
it("app sets custom conf and replaces placeholders", func() {
Eventually(deployment).Should(Serve(SatisfyAll(
ContainSubstring("teststring"),
ContainSubstring("/home/vcap/app/lib"),
)))
})Implementation:
- User configs:
.bp-config/php/php.ini.d/*.ini - Processed in:
src/php/finalize/finalize.go- with app context (BUG FIX) - Placeholders:
@{HOME}→/home/vcap/app
Test File:
; fixtures/php_with_php_ini_d/.bp-config/php/php.ini.d/php.ini
error_prepend_string = 'teststring'
include_path = ".:/usr/share/php:@{HOME}/lib"Context Bug Fix (This PR):
- Before: php.ini.d processed with deps context (
@{HOME}=/home/vcap/deps/{idx}) - After: php.ini.d processed with app context (
@{HOME}=/home/vcap/app) - Change:
src/php/finalize/finalize.go:272-296
Test Coverage: ✅ Fixture exists: fixtures/with_preprocess_cmds/
Configuration: ADDITIONAL_PREPROCESS_CMDS in options.json
Implementation:
- Commands run before app starts
- Use cases: migrations, cache warming, permissions
- Executed via start script
Test Coverage: ✅ default_test.go - "dotnet core as supply buildpack"
Fixture: fixtures/dotnet_core_as_supply_app/
it("works with dotnet core buildpack", func() {
deployment, _, err := platform.Deploy.
WithBuildpacks("dotnet_core_buildpack", "php_buildpack").
Execute(name, filepath.Join(fixtures, "dotnet_core_as_supply_app"))
Eventually(deployment).Should(Serve(ContainSubstring("PHP Version")))
})Implementation:
- DEPS_IDX isolation
- Supply vs finalize buildpack roles
- Profile.d script aggregation
Test Coverage: ✅ default_test.go - "app with JSON-based user extension"
Fixture: fixtures/json_extension/
it("loads and runs the extension", func() {
Eventually(deployment).Should(Serve(ContainSubstring("Extension loaded")))
})Implementation:
- Location:
.extensions/<name>/extension.json - Loader:
src/php/extensions/user/ - Features: config files, preprocess commands, dependencies
Test Coverage: ✅ python_extension_test.go
Fixture: fixtures/python_extension/
Implementation: Legacy v4.x compatibility
Build-Time Placeholders (@{VAR}):
Replaced during finalize phase in src/php/finalize/finalize.go:
// PHP configs (deps context)
phpReplacements := map[string]string{
"@{HOME}": "/home/vcap/deps/{idx}",
"@{DEPS_DIR}": "/home/vcap/deps",
"@{LIBDIR}": "lib",
"@{PHP_FPM_LISTEN}": "127.0.0.1:9000",
"@{TMPDIR}": "${TMPDIR}",
}
// FPM/php.ini.d configs (app context)
appContextReplacements := map[string]string{
"@{HOME}": "/home/vcap/app",
"@{WEBDIR}": "htdocs",
"@{LIBDIR}": "lib",
"@{TMPDIR}": "${TMPDIR}",
}Runtime Variables (${VAR}):
Replaced at container startup:
- Nginx: sed replacement in start script
- Apache: Native environment variable expansion
- Shell: Standard bash expansion
Location: src/php/extensions/extension.go
Context Structure:
type Context struct {
BuildDir string
CacheDir string
DepsDir string
DepsIdx string
VcapServices map[string][]Service
VcapApplication VcapApplication
Env map[string]string
}Extension Interface:
type Extension interface {
Detect(ctx *Context) (bool, error)
Install(installer Installer) error
}Built-in Extensions:
composer/- Dependency managementnewrelic/- NewRelic APMappdynamics/- AppDynamics APMsessions/- Session handler configurationuser/- User extension loader
Location: src/php/finalize/finalize.go
Generated Scripts:
start-httpd.sh- Apache HTTPD + PHP-FPMstart-nginx.sh- Nginx + PHP-FPMstart-fpm.sh- PHP-FPM only
Features:
- Sed variable replacement (PORT, TMPDIR)
- Process management
- Graceful shutdown handling
- Log output
| Feature | Implementation | Tests | User Docs | Status |
|---|---|---|---|---|
| Web Servers | ||||
| Apache HTTPD | ✅ supply.go | ✅ Tested | ✅ Documented | Complete |
| Nginx | ✅ supply.go | ✅ Tested | ✅ Documented | Complete |
| PHP-FPM Only | ✅ supply.go | ✅ Tested | ✅ Documented | Complete |
| Custom FPM Pools | ✅ finalize.go | ✅ Tested | ✅ Documented | Complete |
| PHP | ||||
| Version Selection | ✅ supply.go | ✅ Tested | ✅ Documented | Complete |
| php.ini Override | ✅ supply.go | ✅ Documented | Needs test | |
| php.ini.d | ✅ finalize.go | ✅ Tested | ✅ Documented | Complete |
| Extensions | ||||
| Composer detection | ✅ composer/ | ✅ Tested | ✅ Documented | Complete |
| AMQP | ✅ manifest | ✅ Tested | ✅ Documented | Complete |
| APCu | ✅ manifest | ✅ Tested | ✅ Documented | Complete |
| Redis | ✅ manifest | ✅ Tested | ✅ Documented | Complete |
| Argon2 | ✅ manifest | ✅ Tested | ✅ Documented | Complete |
| All Standard | ✅ manifest | ✅ Tested | ✅ Documented | Complete |
| Custom Compiled | ✅ supply.go | ✅ Tested | ✅ Documented | Complete |
| APM | ||||
| NewRelic | ✅ newrelic/ | ✅ Tested | ✅ Documented | Complete |
| AppDynamics | ✅ appdynamics/ | ✅ Tested | ✅ Documented | Complete |
| Dynatrace | ✅ dynatrace/ | ✅ Tested | ✅ Documented | Complete |
| Sessions | ||||
| Redis | ✅ sessions/ | ✅ Documented | Needs test | |
| Memcached | ✅ sessions/ | ✅ Documented | Needs test | |
| Frameworks | ||||
| CakePHP | ✅ Composer | ✅ Tested | ✅ Documented | Complete |
| Laminas | ✅ Composer | ✅ Tested | ✅ Documented | Complete |
| Symfony | ✅ Composer | ✅ Documented | Needs test | |
| Laravel | ✅ Composer | ✅ Documented | Needs test | |
| Advanced | ||||
| Multi-buildpack | ✅ supply.go | ✅ Tested | ✅ Documented | Complete |
| User Extensions | ✅ user/ | ✅ Tested | ✅ Documented | Complete |
| Preprocess Cmds | ✅ finalize.go | ✅ Fixture | ✅ Documented | Needs test |
| Standalone Apps | ✅ finalize.go | ✅ Documented | Needs test |
Legend:
- ✅ Complete - Implemented, tested, documented
⚠️ Implicit - Works but lacks explicit integration test- ❌ Missing - Not implemented
- Custom php.ini - Fixture exists but no explicit test
- Redis Sessions - Works but needs service binding test
- Memcached Sessions - Works but needs service binding test
- Symfony Framework - Implicit through Composer
- Laravel Framework - Implicit through Composer
- Preprocess Commands - Fixture exists, needs test assertion
- Standalone Apps - APP_START_CMD needs integration test
// Redis session test
context("app with redis session store", func() {
it("configures sessions to use redis", func() {
// Bind Redis service, verify session handler
})
})
// Custom php.ini test
context("app with custom php.ini", func() {
it("applies custom php settings", func() {
// Verify memory_limit, upload_max_filesize, etc.
})
})- USER_GUIDE.md - End-user documentation
- VCAP_SERVICES_USAGE.md - Service binding patterns
- BUILDPACK_COMPARISON.md - Cross-buildpack comparison
- REWRITE_MIGRATION.md - v4.x to v5.x migration