Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,110 @@ func (c *ComposerExtension) Supply(stager libbuildpack.Stager) error {
}
```

### PHP Extension Configuration

The buildpack supports two methods for specifying PHP extensions to load:

#### Method 1: .ini Files (Standard PHP Format)

Users can create `.ini` files in `.bp-config/php/php.ini.d/` with standard PHP extension directives:

```ini
[PHP]
extension=apcu.so
extension=redis.so
zend_extension=opcache.so
```

**Implementation Details:**

During both the **supply phase** (`src/php/supply/supply.go`) and **composer phase** (`src/php/extensions/composer/composer.go`), the buildpack:

1. Walks the `.bp-config/php/php.ini.d/` directory
2. Parses all `.ini` files looking for `extension=` and `zend_extension=` directives
3. Extracts extension names (stripping `.so` suffix and quotes)
4. Adds these extensions to the buildpack context's `PHP_EXTENSIONS` and `ZEND_EXTENSIONS` lists
5. The buildpack's normal extension configuration mechanism handles loading them

**Key Functions:**

- `loadUserExtensions()` in `src/php/supply/supply.go` (lines 850-945)
- `loadUserExtensions()` in `src/php/extensions/composer/composer.go` (lines 1254-1341)

**Why This Approach:**

- **No duplicate loading**: Extensions are only configured once through the buildpack mechanism
- **Consistent**: Works the same way as extensions specified in `.bp-config/options.json`
- **Build-time and runtime**: Extensions are available during `composer install` and at application runtime
- **Familiar syntax**: PHP developers already know `.ini` file format

#### Method 2: options.json (Buildpack-Specific Format)

Alternatively, users can specify extensions in `.bp-config/options.json`:

```json
{
"PHP_EXTENSIONS": ["apcu", "redis"],
"ZEND_EXTENSIONS": ["opcache"]
}
```

Both methods produce the same result and can be used interchangeably.

#### Extension Types

PHP extensions fall into two categories (defined in `manifest.yml`):

1. **Built-in Extensions** - Have empty `version:` field, compiled into PHP binary
- Examples: `bz2`, `curl`, `fileinfo`, `gettext`, `openssl`, `sockets`, `zip`
- Always available, no explicit loading required

2. **PECL Extensions** - Have version numbers (e.g., `5.1.23`), distributed separately
- Examples: `apcu`, `redis`, `mongodb`, `imagick`, `memcached`, `opcache`
- Require explicit loading via `.ini` files or `options.json`

#### Composer Build-Time PHP Installation

The composer extension creates a separate PHP installation specifically for running `composer install`:

**Location:** `BUILD_DIR/php/` (temporary, not in final application)

**Why separate?**

- Composer may have different PHP version requirements than the runtime
- Allows composer to run with extensions needed for dependency installation
- Isolated from runtime PHP configuration

**Extension Loading Flow:**

```
1. Supply Phase (supply.go)
├─► loadUserExtensions()
│ └─► Parse .bp-config/php/php.ini.d/*.ini
│ └─► Add to ctx.PHP_EXTENSIONS
└─► Install runtime PHP with extensions

2. Composer Phase (composer.go)
├─► loadUserExtensions()
│ └─► Parse .bp-config/php/php.ini.d/*.ini
│ └─► Add to ctx.PHP_EXTENSIONS
├─► Install temporary PHP for composer
│ └─► Configure with user extensions
├─► setupPHPConfig()
│ └─► Generate php.ini with extension_dir
└─► Run: php composer.phar install
└─► Extensions now available during install
```

**Key Files:**

- `src/php/extensions/composer/composer.go` - Main composer extension logic
- `src/php/supply/supply.go` - Runtime PHP installation and extension configuration

## Comparison with Other Buildpacks

### Go Buildpack
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,48 @@ This buildpack uses Cloud Foundry's [libbuildpack](https://github.com/cloudfound
- Monitors all processes
- If any process exits, terminates all others and restarts the application

### PHP Extension Configuration

The buildpack supports loading PHP extensions using standard `.ini` file syntax. This allows extensions to be available during both build-time (composer install) and runtime.

#### Loading Extensions via .ini Files

Create `.ini` files in `.bp-config/php/php.ini.d/` directory with standard PHP extension directives:

**Example: `.bp-config/php/php.ini.d/custom.ini`**
```ini
[PHP]
extension=apcu.so
extension=redis.so
zend_extension=opcache.so
```

These extensions will be loaded during:
- **Build-time**: Available during `composer install` execution (if your dependencies require specific extensions)
- **Runtime**: Available when your application runs

**Alternative Method:** You can also specify extensions in `.bp-config/options.json`:
```json
{
"PHP_EXTENSIONS": ["apcu", "redis"],
"ZEND_EXTENSIONS": ["opcache"]
}
```

Both methods are equivalent and produce the same result. Use whichever format you prefer.

#### Available Extensions

Check the `manifest.yml` file for all available extensions. Extensions fall into two categories:

1. **Built-in Extensions** (no version number) - Compiled into PHP, always available:
- `bz2`, `curl`, `fileinfo`, `gettext`, `openssl`, `sockets`, `zip`, etc.

2. **PECL Extensions** (with version numbers) - Require explicit loading:
- `apcu`, `redis`, `mongodb`, `imagick`, `memcached`, `opcache`, etc.

Only PECL extensions need to be specified in `.ini` files or `options.json`. Built-in extensions are always available.

### Extensions

The buildpack includes several built-in extensions written in Go:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[PHP]
extension=apcu.so
14 changes: 14 additions & 0 deletions fixtures/composer_with_extensions/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "cloudfoundry/composer_with_extensions",
"description": "Test app requiring PHP extensions during composer install",
"require": {
"php": ">=8.1",
"ext-apcu": "*"
},
"scripts": {
"post-install-cmd": [
"php -r \"if (!extension_loaded('apcu')) { echo 'FATAL: apcu extension not loaded during composer!'; exit(1); }\"",
"php -r \"echo 'SUCCESS: All required extensions are loaded during composer install\\n';\""
]
}
}
24 changes: 24 additions & 0 deletions fixtures/composer_with_extensions/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions fixtures/composer_with_extensions/htdocs/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
echo "<h1>PHP Extensions Test</h1>";
echo "<h2>PHP Version: " . phpversion() . "</h2>";

$required_extensions = ['apcu'];

echo "<h3>Required Extensions Status:</h3>";
echo "<ul>";
foreach ($required_extensions as $ext) {
$loaded = extension_loaded($ext);
$status = $loaded ? '✓ LOADED' : '✗ NOT LOADED';
echo "<li><strong>$ext:</strong> $status</li>";
}
echo "</ul>";

echo "<h3>All Loaded Extensions:</h3>";
echo "<ul>";
foreach (get_loaded_extensions() as $ext) {
echo "<li>$ext</li>";
}
echo "</ul>";
?>
17 changes: 9 additions & 8 deletions src/php/extensions/composer/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,12 @@ func (e *ComposerExtension) Compile(ctx *extensions.Context, installer *extensio
return fmt.Errorf("failed to install PHP: %w", err)
}

// Load user-requested extensions from .bp-config/php/php.ini.d/*.ini files
// and add them to the context so they're available during composer
if err := e.loadUserExtensions(ctx); err != nil {
return fmt.Errorf("failed to load user extensions: %w", err)
}

// Setup PHP configuration (config files + process extensions in php.ini)
if err := e.setupPHPConfig(ctx); err != nil {
return fmt.Errorf("failed to setup PHP config: %w", err)
Expand Down Expand Up @@ -878,39 +884,32 @@ func (e *ComposerExtension) setupPHPConfig(ctx *extensions.Context) error {
phpInstallDir := filepath.Join(e.buildDir, "php")
phpEtcDir := filepath.Join(phpInstallDir, "etc")

// Get PHP version from context to determine config path
phpVersion := ctx.GetString("PHP_VERSION")
if phpVersion == "" {
return fmt.Errorf("PHP_VERSION not set in context")
}

// Extract major.minor version (e.g., "8.1.32" -> "8.1")
versionParts := strings.Split(phpVersion, ".")
if len(versionParts) < 2 {
return fmt.Errorf("invalid PHP version format: %s", phpVersion)
}
majorMinor := fmt.Sprintf("%s.%s", versionParts[0], versionParts[1])
phpConfigPath := fmt.Sprintf("php/%s.x", majorMinor)

// Extract PHP config files from embedded defaults
if err := config.ExtractConfig(phpConfigPath, phpEtcDir); err != nil {
return fmt.Errorf("failed to extract PHP config: %w", err)
}

// Create php.ini.d directory for extension configs
phpIniDir := filepath.Join(phpEtcDir, "php.ini.d")
if err := os.MkdirAll(phpIniDir, 0755); err != nil {
return fmt.Errorf("failed to create php.ini.d directory: %w", err)
}

// Process php.ini to replace extension placeholders
phpIniPath := filepath.Join(phpEtcDir, "php.ini")
if err := e.processPhpIni(ctx, phpIniPath); err != nil {
return fmt.Errorf("failed to process php.ini: %w", err)
}

// Copy processed php.ini to TMPDIR for Composer to use
// This matches the Python buildpack behavior where PHPRC points to TMPDIR
tmpPhpIniPath := filepath.Join(e.tmpDir, "php.ini")
if err := util.CopyFile(phpIniPath, tmpPhpIniPath); err != nil {
return fmt.Errorf("failed to copy php.ini to TMPDIR: %w", err)
Expand Down Expand Up @@ -1093,10 +1092,11 @@ func (e *ComposerExtension) runComposerCommand(ctx *extensions.Context, phpPath,
func (e *ComposerExtension) buildComposerEnv() []string {
env := os.Environ()

// Add Composer-specific variables
vendorDir := filepath.Join(e.buildDir, e.composerVendorDir)
binDir := filepath.Join(e.buildDir, "php", "bin")
cacheDir := filepath.Join(e.composerHome, "cache")
phpEtcDir := filepath.Join(e.buildDir, "php", "etc")
phpIniScanDir := filepath.Join(phpEtcDir, "php.ini.d")

env = append(env,
fmt.Sprintf("COMPOSER_HOME=%s", e.composerHome),
Expand All @@ -1105,6 +1105,7 @@ func (e *ComposerExtension) buildComposerEnv() []string {
fmt.Sprintf("COMPOSER_CACHE_DIR=%s", cacheDir),
fmt.Sprintf("LD_LIBRARY_PATH=%s", filepath.Join(e.buildDir, "php", "lib")),
fmt.Sprintf("PHPRC=%s", e.tmpDir),
fmt.Sprintf("PHP_INI_SCAN_DIR=%s", phpIniScanDir),
)

return env
Expand Down
24 changes: 24 additions & 0 deletions src/php/integration/composer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,29 @@ func testComposer(platform switchblade.Platform, fixtures string) func(*testing.
})
})
}

context("composer app with extensions in .bp-config/php/php.ini.d", func() {
it("loads extensions during composer install and at runtime", func() {
deployment, logs, err := platform.Deploy.
WithEnv(map[string]string{
"COMPOSER_GITHUB_OAUTH_TOKEN": os.Getenv("COMPOSER_GITHUB_OAUTH_TOKEN"),
}).
Execute(name, filepath.Join(fixtures, "composer_with_extensions"))
Expect(err).NotTo(HaveOccurred())

// Check build logs first (composer phase)
Expect(logs.String()).To(SatisfyAll(
ContainSubstring("Loading user-requested extensions from .bp-config/php/php.ini.d"),
ContainSubstring("Found 1 extension(s)"),
ContainSubstring("Installing Composer dependencies"),
ContainSubstring("SUCCESS: All required extensions are loaded during composer install"),
Not(ContainSubstring("composer install failed")),
Not(ContainSubstring("ERROR")),
))

// Then wait for deployment and check runtime
Eventually(deployment).Should(Serve(ContainSubstring("apcu:</strong> ✓ LOADED")))
})
})
}
}
6 changes: 6 additions & 0 deletions src/php/supply/supply.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ func (s *Supplier) createExtensionContext() (*extensions.Context, error) {
ctx.Set("WEB_SERVER", s.Options.WebServer)
ctx.Set("COMPOSER_VERSION", ctx.GetString("COMPOSER_DEFAULT")) // Use default from manifest

// Load user-requested extensions from .bp-config/php/php.ini.d/*.ini files
// and add them to the context so they're available at runtime
if err := s.loadUserExtensions(ctx); err != nil {
return nil, fmt.Errorf("failed to load user extensions: %w", err)
}

// Set additional options
ctx.Set("ADMIN_EMAIL", s.Options.AdminEmail)
ctx.Set("COMPOSER_VENDOR_DIR", s.Options.ComposerVendorDir)
Expand Down
Loading