@@ -655,6 +655,12 @@ func (e *ComposerExtension) Compile(ctx *extensions.Context, installer *extensio
655655 return fmt .Errorf ("failed to install PHP: %w" , err )
656656 }
657657
658+ // Load user-requested extensions from .bp-config/php/php.ini.d/*.ini files
659+ // and add them to the context so they're available during composer
660+ if err := e .loadUserExtensions (ctx ); err != nil {
661+ return fmt .Errorf ("failed to load user extensions: %w" , err )
662+ }
663+
658664 // Setup PHP configuration (config files + process extensions in php.ini)
659665 if err := e .setupPHPConfig (ctx ); err != nil {
660666 return fmt .Errorf ("failed to setup PHP config: %w" , err )
@@ -905,39 +911,32 @@ func (e *ComposerExtension) setupPHPConfig(ctx *extensions.Context) error {
905911 phpInstallDir := filepath .Join (e .buildDir , "php" )
906912 phpEtcDir := filepath .Join (phpInstallDir , "etc" )
907913
908- // Get PHP version from context to determine config path
909914 phpVersion := ctx .GetString ("PHP_VERSION" )
910915 if phpVersion == "" {
911916 return fmt .Errorf ("PHP_VERSION not set in context" )
912917 }
913918
914- // Extract major.minor version (e.g., "8.1.32" -> "8.1")
915919 versionParts := strings .Split (phpVersion , "." )
916920 if len (versionParts ) < 2 {
917921 return fmt .Errorf ("invalid PHP version format: %s" , phpVersion )
918922 }
919923 majorMinor := fmt .Sprintf ("%s.%s" , versionParts [0 ], versionParts [1 ])
920924 phpConfigPath := fmt .Sprintf ("php/%s.x" , majorMinor )
921925
922- // Extract PHP config files from embedded defaults
923926 if err := config .ExtractConfig (phpConfigPath , phpEtcDir ); err != nil {
924927 return fmt .Errorf ("failed to extract PHP config: %w" , err )
925928 }
926929
927- // Create php.ini.d directory for extension configs
928930 phpIniDir := filepath .Join (phpEtcDir , "php.ini.d" )
929931 if err := os .MkdirAll (phpIniDir , 0755 ); err != nil {
930932 return fmt .Errorf ("failed to create php.ini.d directory: %w" , err )
931933 }
932934
933- // Process php.ini to replace extension placeholders
934935 phpIniPath := filepath .Join (phpEtcDir , "php.ini" )
935936 if err := e .processPhpIni (ctx , phpIniPath ); err != nil {
936937 return fmt .Errorf ("failed to process php.ini: %w" , err )
937938 }
938939
939- // Copy processed php.ini to TMPDIR for Composer to use
940- // This matches the Python buildpack behavior where PHPRC points to TMPDIR
941940 tmpPhpIniPath := filepath .Join (e .tmpDir , "php.ini" )
942941 if err := e .copyFile (phpIniPath , tmpPhpIniPath ); err != nil {
943942 return fmt .Errorf ("failed to copy php.ini to TMPDIR: %w" , err )
@@ -1200,10 +1199,11 @@ func (e *ComposerExtension) runComposerCommand(ctx *extensions.Context, phpPath,
12001199func (e * ComposerExtension ) buildComposerEnv () []string {
12011200 env := os .Environ ()
12021201
1203- // Add Composer-specific variables
12041202 vendorDir := filepath .Join (e .buildDir , e .composerVendorDir )
12051203 binDir := filepath .Join (e .buildDir , "php" , "bin" )
12061204 cacheDir := filepath .Join (e .composerHome , "cache" )
1205+ phpEtcDir := filepath .Join (e .buildDir , "php" , "etc" )
1206+ phpIniScanDir := filepath .Join (phpEtcDir , "php.ini.d" )
12071207
12081208 env = append (env ,
12091209 fmt .Sprintf ("COMPOSER_HOME=%s" , e .composerHome ),
@@ -1212,6 +1212,7 @@ func (e *ComposerExtension) buildComposerEnv() []string {
12121212 fmt .Sprintf ("COMPOSER_CACHE_DIR=%s" , cacheDir ),
12131213 fmt .Sprintf ("LD_LIBRARY_PATH=%s" , filepath .Join (e .buildDir , "php" , "lib" )),
12141214 fmt .Sprintf ("PHPRC=%s" , e .tmpDir ),
1215+ fmt .Sprintf ("PHP_INI_SCAN_DIR=%s" , phpIniScanDir ),
12151216 )
12161217
12171218 return env
@@ -1250,6 +1251,86 @@ func (e *ComposerExtension) copyFile(src, dst string) error {
12501251 return err
12511252}
12521253
1254+ func (e * ComposerExtension ) loadUserExtensions (ctx * extensions.Context ) error {
1255+ userPhpIniDir := filepath .Join (e .buildDir , ".bp-config" , "php" , "php.ini.d" )
1256+ if _ , err := os .Stat (userPhpIniDir ); os .IsNotExist (err ) {
1257+ return nil
1258+ }
1259+
1260+ fmt .Println (" Loading user-requested extensions from .bp-config/php/php.ini.d" )
1261+
1262+ currentExtensions := ctx .GetStringSlice ("PHP_EXTENSIONS" )
1263+ currentZendExtensions := ctx .GetStringSlice ("ZEND_EXTENSIONS" )
1264+
1265+ extensionsToAdd := make (map [string ]bool )
1266+ zendExtensionsToAdd := make (map [string ]bool )
1267+
1268+ err := filepath .Walk (userPhpIniDir , func (path string , info os.FileInfo , err error ) error {
1269+ if err != nil {
1270+ return err
1271+ }
1272+
1273+ if info .IsDir () || ! strings .HasSuffix (strings .ToLower (path ), ".ini" ) {
1274+ return nil
1275+ }
1276+
1277+ content , err := os .ReadFile (path )
1278+ if err != nil {
1279+ fmt .Printf (" WARNING: Failed to read %s: %v\n " , path , err )
1280+ return nil
1281+ }
1282+
1283+ for _ , line := range strings .Split (string (content ), "\n " ) {
1284+ line = strings .TrimSpace (line )
1285+
1286+ if strings .HasPrefix (line , ";" ) || strings .HasPrefix (line , "#" ) || line == "" {
1287+ continue
1288+ }
1289+
1290+ if strings .HasPrefix (line , "extension=" ) || strings .HasPrefix (line , "extension =" ) {
1291+ extLine := strings .TrimSpace (strings .TrimPrefix (line , "extension=" ))
1292+ extLine = strings .TrimSpace (strings .TrimPrefix (extLine , "extension =" ))
1293+ extName := strings .TrimSuffix (extLine , ".so" )
1294+ extName = strings .Trim (extName , "\" ' " )
1295+ if extName != "" {
1296+ extensionsToAdd [extName ] = true
1297+ }
1298+ } else if strings .HasPrefix (line , "zend_extension=" ) || strings .HasPrefix (line , "zend_extension =" ) {
1299+ extLine := strings .TrimSpace (strings .TrimPrefix (line , "zend_extension=" ))
1300+ extLine = strings .TrimSpace (strings .TrimPrefix (extLine , "zend_extension =" ))
1301+ extName := strings .TrimSuffix (extLine , ".so" )
1302+ extName = strings .Trim (extName , "\" ' " )
1303+ if extName != "" {
1304+ zendExtensionsToAdd [extName ] = true
1305+ }
1306+ }
1307+ }
1308+
1309+ return nil
1310+ })
1311+
1312+ if err != nil {
1313+ return fmt .Errorf ("failed to scan user ini files: %w" , err )
1314+ }
1315+
1316+ for ext := range extensionsToAdd {
1317+ currentExtensions = append (currentExtensions , ext )
1318+ }
1319+ for ext := range zendExtensionsToAdd {
1320+ currentZendExtensions = append (currentZendExtensions , ext )
1321+ }
1322+
1323+ ctx .Set ("PHP_EXTENSIONS" , uniqueStrings (currentExtensions ))
1324+ ctx .Set ("ZEND_EXTENSIONS" , uniqueStrings (currentZendExtensions ))
1325+
1326+ if len (extensionsToAdd ) > 0 || len (zendExtensionsToAdd ) > 0 {
1327+ fmt .Printf (" Found %d extension(s) and %d zend extension(s) in user config\n " ,
1328+ len (extensionsToAdd ), len (zendExtensionsToAdd ))
1329+ }
1330+
1331+ return nil
1332+ }
1333+
12531334// uniqueStrings returns a slice with duplicate strings removed
12541335func uniqueStrings (input []string ) []string {
12551336 seen := make (map [string ]bool )
0 commit comments