@@ -93,6 +93,12 @@ class Autoloader
9393 */
9494 protected $ helpers = ['url ' ];
9595
96+ /**
97+ * Track if Composer namespaces have been loaded in this initialization.
98+ * Resets on each initialize() call to allow tests to load Composer.
99+ */
100+ private bool $ composerLoaded = false ;
101+
96102 public function __construct (private readonly string $ composerPath = COMPOSER_PATH )
97103 {
98104 }
@@ -109,6 +115,8 @@ public function initialize(Autoload $config, Modules $modules)
109115 $ this ->classmap = [];
110116 $ this ->files = [];
111117
118+ $ this ->composerLoaded = false ;
119+
112120 // We have to have one or the other, though we don't enforce the need
113121 // to have both present in order to work.
114122 if ($ config ->psr4 === [] && $ config ->classmap === []) {
@@ -146,16 +154,36 @@ private function loadComposerAutoloader(Modules $modules): void
146154 define ('VENDORPATH ' , dirname ($ this ->composerPath ) . DIRECTORY_SEPARATOR );
147155 }
148156
149- /** @var ClassLoader $composer */
150- $ composer = include $ this ->composerPath ;
157+ // Skip if already loaded in this initialization
158+ if ($ this ->composerLoaded ) {
159+ return ;
160+ }
151161
152- // Should we load through Composer's namespaces, also?
153- if ($ modules ->discoverInComposer ) {
154- $ composerPackages = $ modules ->composerPackages ;
155- $ this ->loadComposerNamespaces ($ composer , $ composerPackages ?? []);
162+ // Use advisory file lock to coordinate between parallel processes
163+ $ lockFile = sys_get_temp_dir () . '/ci_autoload_ ' . sha1 ($ this ->composerPath ) . '.lock ' ;
164+ $ lockFp = @fopen ($ lockFile , 'cb ' );
165+ if ($ lockFp ) {
166+ flock ($ lockFp , LOCK_EX );
156167 }
157168
158- unset($ composer );
169+ try {
170+ /** @var ClassLoader $composer */
171+ $ composer = include $ this ->composerPath ;
172+
173+ // Should we load through Composer's namespaces, also?
174+ if ($ modules ->discoverInComposer ) {
175+ $ composerPackages = $ modules ->composerPackages ;
176+ $ this ->loadComposerNamespaces ($ composer , $ composerPackages ?? []);
177+ $ this ->composerLoaded = true ;
178+ }
179+
180+ unset($ composer );
181+ } finally {
182+ if ($ lockFp ) {
183+ flock ($ lockFp , LOCK_UN );
184+ fclose ($ lockFp );
185+ }
186+ }
159187 }
160188
161189 /**
@@ -441,7 +469,31 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa
441469
442470 if ($ add ) {
443471 // Composer stores namespaces with trailing slash. We don't.
444- $ newPaths [rtrim ($ namespace , '\\ ' )] = $ srcPaths ;
472+ // Validate and absolutize paths to prevent issues in parallel execution
473+ $ validatedPaths = [];
474+ foreach ($ srcPaths as $ srcPath ) {
475+ // Handle relative paths and paths with vendor/composer prefixes
476+ if (! str_starts_with ($ srcPath , '/ ' ) && (strlen ($ srcPath ) < 2 || $ srcPath [1 ] !== ': ' )) {
477+ // Relative path - make it absolute relative to VENDORPATH
478+ $ srcPath = rtrim (VENDORPATH , '/ \\' ) . DIRECTORY_SEPARATOR . ltrim ($ srcPath , '/ \\' );
479+ }
480+
481+ // Remove /../ patterns to prevent malformed paths
482+ $ srcPath = str_replace (['/../ ' , '\\.. \\' ], DIRECTORY_SEPARATOR , $ srcPath );
483+
484+ // Use realpath if the path exists to get the canonical absolute path
485+ if (is_dir ($ srcPath )) {
486+ $ srcPath = realpath ($ srcPath );
487+ }
488+
489+ if ($ srcPath !== false ) {
490+ $ validatedPaths [] = $ srcPath ;
491+ }
492+ }
493+
494+ if ($ validatedPaths !== []) {
495+ $ newPaths [rtrim ($ namespace , '\\ ' )] = $ validatedPaths ;
496+ }
445497 }
446498 }
447499
0 commit comments