Skip to content

Commit 0fd70a9

Browse files
Refactor DevelopmentProjectCommands for clarity and documentation
This commit significantly refactors the `DevelopmentProjectCommands` Drush command, which replaces the standard `cache:rebuild` command to correctly support symlinked Drupal core installations in development environments like DrupalPod. Key changes include: - **Improved Code Structure:** - The `rebuild` method was refactored to delegate environment setup to a new private method `initializeDrupalSiteState`, improving readability. - The `drupal_rebuild` method was corrected to be a private class method with proper type hinting and its logic clarified. - Comments for `getAppRoot` were enhanced to explain its hardcoded path's rationale. - **Enhanced Documentation:** - Added comprehensive docblocks for the class itself and all methods (`__construct`, `createEarly`, `rebuild`, `initializeDrupalSiteState`, `drupal_rebuild`, `getAppRoot`). - Docblocks now clearly explain the purpose of each method, its parameters, return values, and its role in handling symlinked Drupal core. - The necessity for this custom command (addressing issues with standard Drush in symlinked setups) is clearly articulated in the class docblock. - **Visual Flow Diagrams:** - Integrated Mermaid sequence diagrams into the class docblock (for the `rebuild` flow) and the `drupal_rebuild` method's docblock (for its internal flow). These diagrams provide a visual aid to understanding the command's execution. These changes aim to improve the maintainability, readability, and overall understanding of this specialized Drush command.
1 parent ada1cad commit 0fd70a9

1 file changed

Lines changed: 216 additions & 30 deletions

File tree

src/drush-commands-core-development/13/DevelopmentProjectCommands.php

Lines changed: 216 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,84 @@
1212
use 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
*/
2366
class 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

Comments
 (0)