1212use Symfony \Component \HttpFoundation \Request ;
1313
1414/**
15- * Replaces the cache:rebuild command to correctly work with symlinked Drupal .
15+ * Provides a replacement for the Drush ' cache:rebuild' (cr) command .
1616 *
17- * This prevents contrib modules from vanishing when the cache:rebuild command
18- * is run.
17+ * This command handler is specifically designed to address issues encountered in
18+ * development environments (e.g., DrupalPod, DDEV) where Drupal core is
19+ * symlinked. In such setups, standard Drush commands may fail to correctly
20+ * locate the application root, leading to errors like "Unable to find a
21+ * matching Composer project root".
1922 *
20- * For maintainability, changes from the original command method in
21- * Drush\Commands\core\CacheCommands are marked 'CHANGE'.
23+ * The core mechanism of this replacement involves:
24+ * 1. A custom `getAppRoot()` method: This method reliably determines the
25+ * application root (docroot) based on a known directory structure, bypassing
26+ * potential issues with DrupalFinder in symlinked scenarios.
27+ * 2. Kernel Initialization: The `DrupalKernel` is explicitly instantiated
28+ * using the application root path obtained from `getAppRoot()`. This ensures
29+ * that Drupal bootstraps with the correct context.
30+ *
31+ * This approach ensures that cache rebuilding and other Drupal operations
32+ * function correctly even when the Drupal core directory is a symlink.
33+ *
34+ * Rebuild Command Flow:
35+ * ```mermaid
36+ * sequenceDiagram
37+ * participant User as User/Drush CLI
38+ * participant RebuildCmd as DevelopmentProjectCommands::rebuild()
39+ * participant AppRootHelper as DevelopmentProjectCommands::getAppRoot()
40+ * participant SiteStateHelper as DevelopmentProjectCommands::initializeDrupalSiteState()
41+ * participant DrupalRebuildHelper as DevelopmentProjectCommands::drupal_rebuild()
42+ * participant Logger
43+ *
44+ * User->>RebuildCmd: Executes command (e.g., drush cr)
45+ * RebuildCmd->>RebuildCmd: Check options['cache-clear']
46+ * alt options['cache-clear'] is false
47+ * RebuildCmd->>Logger: Log "Skipping cache-clear"
48+ * RebuildCmd-->>User: Return true
49+ * end
50+ * RebuildCmd->>AppRootHelper: getAppRoot()
51+ * AppRootHelper-->>RebuildCmd: Returns app_root
52+ * RebuildCmd->>SiteStateHelper: initializeDrupalSiteState(app_root)
53+ * SiteStateHelper->>SiteStateHelper: chdir(app_root)
54+ * SiteStateHelper->>SiteStateHelper: require_once utility.inc
55+ * SiteStateHelper->>SiteStateHelper: Get request object
56+ * SiteStateHelper->>SiteStateHelper: Boot Drupal environment
57+ * SiteStateHelper->>SiteStateHelper: Find site_path
58+ * SiteStateHelper->>SiteStateHelper: Initialize Settings
59+ * SiteStateHelper-->>RebuildCmd: Returns request object
60+ * RebuildCmd->>DrupalRebuildHelper: drupal_rebuild(autoloader, request)
61+ * DrupalRebuildHelper-->>RebuildCmd: (Rebuild process completes)
62+ * RebuildCmd->>Logger: Log "Cache rebuild complete."
63+ * RebuildCmd-->>User: Return (implicitly true)
64+ * ```
2265 */
2366class DevelopmentProjectCommands extends DrushCommands {
2467
68+ /**
69+ * Constructs a DevelopmentProjectCommands object.
70+ *
71+ * @param \Composer\Autoload\ClassLoader $autoloader
72+ * The Composer class loader, injected for use in rebuilding the kernel.
73+ */
2574 public function __construct (
2675 private ClassLoader $ autoloader
2776 ) {
2877 parent ::__construct ();
2978 }
3079
80+ /**
81+ * Creates an instance of the command handler.
82+ *
83+ * This is a factory method called by Drush during the command discovery
84+ * and instantiation process, allowing for early instantiation with necessary
85+ * dependencies like the autoloader.
86+ *
87+ * @param \Psr\Container\ContainerInterface $drush_container
88+ * The Drush dependency injection container (DrushContainer alias).
89+ *
90+ * @return self
91+ * A new instance of this command handler.
92+ */
3193 public static function createEarly (DrushContainer $ drush_container ): self {
3294 $ commandHandler = new static (
3395 $ drush_container ->get ('loader ' )
@@ -37,53 +99,158 @@ public static function createEarly(DrushContainer $drush_container): self {
3799 }
38100
39101 /**
102+ * Rebuilds Drupal's cache.
103+ *
104+ * This command is a replacement for the standard 'cache:rebuild' command,
105+ * tailored for environments with symlinked Drupal cores.
106+ *
40107 * @hook replace-command cache:rebuild
108+ * @param array $options
109+ * Command options. Expects 'cache-clear' => TRUE by default.
110+ * @return bool
111+ * TRUE on success, or if cache clear is skipped.
41112 */
42113 public function rebuild ($ options = ['cache-clear ' => true ]) {
43114 if (!$ options ['cache-clear ' ]) {
44115 $ this ->logger ()->info (dt ("Skipping cache-clear operation due to --no-cache-clear option. " ));
45116 return true ;
46117 }
47118
48- // CHANGE: Get the app root ourselves instead of using DRUPAL_ROOT.
119+ // Determine the application root. This is crucial for environments with
120+ // symlinked Drupal cores, ensuring Drush operates on the correct directory.
49121 $ app_root = $ this ->getAppRoot ();
50- chdir ($ this ->getAppRoot ());
122+
123+ // Initialize the Drupal site state using the determined application root.
124+ // This sets up the necessary environment, request, and settings for Drupal.
125+ $ request = $ this ->initializeDrupalSiteState ($ app_root );
51126
52127 // We no longer clear APC and similar caches as they are useless on CLI.
53128 // See https://github.com/drush-ops/drush/pull/2450
54129
130+ // Perform the cache rebuild using a custom drupal_rebuild method.
131+ // This custom version is necessary to correctly pass the application root
132+ // to the DrupalKernel, which is essential for symlinked core scenarios.
133+ $ this ->drupal_rebuild ($ this ->autoloader , $ request );
134+ $ this ->logger ()->success (dt ('Cache rebuild complete. ' ));
135+ }
136+
137+ /**
138+ * Sets up the necessary Drupal environment and site state.
139+ *
140+ * This method is critical for scenarios involving a symlinked Drupal core,
141+ * as it ensures that all paths and settings are initialized correctly
142+ * relative to the true application root.
143+ *
144+ * @param string $appRoot
145+ * The absolute path to the application root (Drupal's docroot).
146+ *
147+ * @return \Symfony\Component\HttpFoundation\Request
148+ * The initialized Symfony request object, prepared for Drupal.
149+ */
150+ private function initializeDrupalSiteState (string $ appRoot ): Request {
151+ // Change the current working directory to the application root.
152+ // This ensures that relative path operations within Drupal work as expected.
153+ chdir ($ appRoot );
154+
155+ // Include Drupal's utility functions, which are needed for bootstrapping.
156+ // DRUSH_DRUPAL_CORE is a Drush constant pointing to the Drupal core directory.
55157 require_once DRUSH_DRUPAL_CORE . '/includes/utility.inc ' ;
56158
159+ // Bootstrap Drush to get the current request object.
160+ // This request object may then be used by DrupalKernel.
57161 $ request = Drush::bootstrap ()->getRequest ();
162+
163+ // Boot the Drupal environment. This prepares Drupal's basic services and
164+ // environment variables but doesn't fully bootstrap Drupal yet.
58165 DrupalKernel::bootEnvironment ();
59166
60- // Avoid 'Only variables should be passed by reference'
61- // CHANGE: Don't use DRUPAL_ROOT.
62- $ root = $ app_root ;
167+ // Find the site path (e.g., 'sites/default') based on the request.
168+ // This is necessary for loading the correct site-specific configuration.
63169 $ site_path = DrupalKernel::findSitePath ($ request );
64- Settings::initialize ($ root , $ site_path , $ this ->autoloader );
65170
66- // drupal_rebuild() calls drupal_flush_all_caches() itself, so we don't do it manually.
67- // CHANGE: call our own version of drupal_rebuild().
68- $ this ->drupal_rebuild ($ this ->autoloader , $ request );
69- $ this ->logger ()->success (dt ('Cache rebuild complete. ' ));
171+ // Initialize Drupal settings. This loads settings.php and services.yml
172+ // for the determined site path and application root. The autoloader is
173+ // passed to allow Drupal to register its own class loader.
174+ Settings::initialize ($ appRoot , $ site_path , $ this ->autoloader );
175+
176+ return $ request ;
70177 }
71178
72179 /**
73- * Replacement for drupal_rebuild().
180+ * Performs a Drupal cache rebuild using a kernel configured with a custom app root.
181+ *
182+ * This method is a customized version of the standard Drupal rebuild process.
183+ * Its primary purpose is to instantiate and use a DrupalKernel that is
184+ * explicitly initialized with the application root path provided by
185+ * `$this->getAppRoot()`. This is crucial for development environments where
186+ * Drupal core might be symlinked, and standard path detection could fail.
187+ *
188+ * The process involves:
189+ * - Temporarily disabling custom Drupal error/exception handlers.
190+ * - Instantiating DrupalKernel with the correct app root and class loader.
191+ * - Setting the site path on the kernel.
192+ * - Invalidating the container, booting the kernel, and pre-handling the request.
193+ * - Flushing all caches using the configured kernel.
194+ * - Triggering the page cache kill switch.
195+ * - Restoring Drupal's error/exception handlers.
196+ *
197+ * @param \Composer\Autoload\ClassLoader $class_loader
198+ * The class loader to be used by the new kernel.
199+ * @param \Symfony\Component\HttpFoundation\Request $request
200+ * The current request object.
201+ *
202+ * Flow Diagram:
203+ * ```mermaid
204+ * sequenceDiagram
205+ * participant RebuildCmd as DevelopmentProjectCommands::rebuild()
206+ * participant DrupalRebuildHelper as DevelopmentProjectCommands::drupal_rebuild()
207+ * participant GlobalHandlers as PHP Global Error/Exception Handlers
208+ * participant AppRootHelper as DevelopmentProjectCommands::getAppRoot()
209+ * participant DrupalKernelInst as DrupalKernel (Instance)
210+ * participant DrupalStatic as \Drupal
211+ * participant PageCacheKillSwitch as Page Cache Kill Switch Service
212+ *
213+ * RebuildCmd->>DrupalRebuildHelper: drupal_rebuild(class_loader, request)
214+ * DrupalRebuildHelper->>GlobalHandlers: restore_error_handler()
215+ * DrupalRebuildHelper->>GlobalHandlers: restore_exception_handler()
216+ *
217+ * DrupalRebuildHelper->>AppRootHelper: getAppRoot() (called internally by new DrupalKernel)
218+ * AppRootHelper-->>DrupalRebuildHelper: app_root (returned to DrupalKernel constructor)
219+ * DrupalRebuildHelper->>DrupalKernelInst: new DrupalKernel('prod', class_loader, TRUE, app_root)
220+ * DrupalRebuildHelper->>DrupalKernelInst: setSitePath(findSitePath(request))
221+ * DrupalRebuildHelper->>DrupalKernelInst: invalidateContainer()
222+ * DrupalRebuildHelper->>DrupalKernelInst: boot()
223+ * DrupalRebuildHelper->>DrupalKernelInst: preHandle(request)
224+ *
225+ * alt PHP_SAPI is not 'cli'
226+ * DrupalRebuildHelper->>DrupalKernelInst: getContainer()->get('session')
227+ * DrupalKernelInst-->>DrupalRebuildHelper: session service
228+ * DrupalRebuildHelper->>DrupalRebuildHelper: request->setSession(session service)
229+ * end
230+ *
231+ * DrupalRebuildHelper->>DrupalKernelInst: drupal_flush_all_caches(kernel)
232+ *
233+ * DrupalRebuildHelper->>DrupalStatic: service('page_cache_kill_switch')
234+ * DrupalStatic-->>PageCacheKillSwitch: (returns service)
235+ * DrupalRebuildHelper->>PageCacheKillSwitch: trigger()
74236 *
75- * This passes the app root to DrupalKernel.
237+ * DrupalRebuildHelper->>GlobalHandlers: set_error_handler('_drupal_error_handler')
238+ * DrupalRebuildHelper->>GlobalHandlers: set_exception_handler('_drupal_exception_handler')
239+ * DrupalRebuildHelper-->>RebuildCmd: (Process completes)
240+ * ```
76241 */
77- function drupal_rebuild ($ class_loader , Request $ request ) {
78- // Remove Drupal's error and exception handlers; they rely on a working
79- // service container and other subsystems and will only cause a fatal error
80- // that hides the actual error.
242+ private function drupal_rebuild (ClassLoader $ class_loader , Request $ request ) {
243+ // Remove Drupal's error and exception handlers temporarily. These handlers
244+ // often rely on a fully working service container and other Drupal subsystems.
245+ // During a rebuild, these subsystems are being reset, so the default handlers
246+ // could trigger fatal errors that obscure the actual problem if one occurs.
81247 restore_error_handler ();
82248 restore_exception_handler ();
83249
84- // Invalidate the container.
85- // Bootstrap up to where caches exist and clear them.
86- // CHANGE: Pass the correct app root to DrupalKernel.
250+ // Instantiate the DrupalKernel. Critically, pass $this->getAppRoot() as the
251+ // appRoot argument. This ensures the kernel operates with the correct paths,
252+ // especially in symlinked Drupal core setups. 'prod' environment is typically
253+ // used for rebuilds, and TRUE indicates to allowlist all modules.
87254 $ kernel = new DrupalKernel ('prod ' , $ class_loader , TRUE , $ this ->getAppRoot ());
88255 $ kernel ->setSitePath (DrupalKernel::findSitePath ($ request ));
89256 $ kernel ->invalidateContainer ();
@@ -99,8 +266,8 @@ function drupal_rebuild($class_loader, Request $request) {
99266 // Disable recording of cached pages.
100267 \Drupal::service ('page_cache_kill_switch ' )->trigger ();
101268
102- // Restore Drupal's error and exception handlers.
103- // @see \Drupal\Core\DrupalKernel::boot()
269+ // Restore Drupal's standard error and exception handlers.
270+ // See \Drupal\Core\DrupalKernel::boot() for where they are typically set.
104271 set_error_handler ('_drupal_error_handler ' );
105272 set_exception_handler ('_drupal_exception_handler ' );
106273 }
@@ -109,12 +276,31 @@ function drupal_rebuild($class_loader, Request $request) {
109276 * Gets the app root.
110277 *
111278 * @return string
112- * The app root.
279+ * The absolute path to the application root (docroot) .
113280 */
114281 protected function getAppRoot (): string {
115- // This core belongs to the project template, so we can hardcode the
116- // location of this file relative to the project root, and the scaffold
117- // location defined in the project's composer.json.
282+ // Assumed directory structure:
283+ // - The project root contains a `web` directory.
284+ // - `web` is the Drupal application root (docroot).
285+ //
286+ // Explanation of the path construction:
287+ // - `__DIR__` is the directory of the current file: `src/drush-commands-core-development/13`.
288+ // - `dirname(__DIR__, 3)` navigates three levels up from the current directory,
289+ // resolving to the project root.
290+ // - `/web` is then appended to get the path to the Drupal application root.
291+ //
292+ // This hardcoded path is a deliberate choice for development environments
293+ // like DrupalPod or similar templates. In these setups, Drupal core might be
294+ // symlinked, and standard Drush methods for discovering the application root
295+ // (like DrupalFinder) can fail. This command is specifically designed for
296+ // such scenarios where the location of this file relative to the project root
297+ // is known and stable. The scaffold location defined in the project's
298+ // composer.json also plays a role in this assumption.
299+ //
300+ // IMPORTANT:
301+ // If the project template's directory structure changes (e.g., the location
302+ // of the 'web' directory relative to the project root, or the location of
303+ // this command file itself), this method will need to be updated accordingly.
118304 return dirname (__DIR__ , 3 ) . '/web ' ;
119305 }
120306
0 commit comments