diff --git a/README.md b/README.md index e7b8cb9..3be277f 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ The following environment settings can be used to troubleshoot unexpected behavi * `XDEBUG_HANDLER_DEBUG=2` As above, but additionally saves the temporary ini file and reports its location in a status message. ### Extending the library -The API is defined by classes and their accessible elements that are not annotated as @internal. The main class has two protected methods that can be overridden to provide additional functionality: +The API is defined by classes and their accessible elements that are not annotated as @internal. The main class has four protected methods, two of which can be overridden to provide additional functionality: #### _requiresRestart(bool $default): bool_ By default the process will restart if Xdebug is loaded and not running with `xdebug.mode=off`. Extending this method allows an application to decide, by returning a boolean (or equivalent) value. @@ -272,6 +272,16 @@ Remember to finish with `parent::restart($command)`. --- +#### _outputError(string $message): void_ +Outputs an error status message. This will be logged at WARNING level and output to STDERR. See [setLogger](#setloggerloggerinterface-logger-self) and [Troubleshooting](#troubleshooting) for more information. + +--- + +#### _outputInfo(string $message): void_ +Output an informational status message. This will be logged at DEBUG level and output to STDERR. See [setLogger](#setloggerloggerinterface-logger-self) and [Troubleshooting](#troubleshooting) for more information. + +--- + #### Example This example demonstrates two ways to extend basic functionality: @@ -296,6 +306,11 @@ class MyRestarter extends XdebugHandler $this->required = (bool) ini_get('phar.readonly'); + if ($this->required) { + // Output a status message + $this->outputInfo('phar.readonly=1 detected, restart required'); + } + return $this->required || $default; } @@ -306,6 +321,9 @@ class MyRestarter extends XdebugHandler $content = file_get_contents($this->tmpIni); $content .= 'phar.readonly=0'.PHP_EOL; file_put_contents($this->tmpIni, $content); + + // Output a status message + $this->outputInfo('phar.readonly=0 added to temporary ini file'); } parent::restart($command); diff --git a/src/Status.php b/src/Status.php index ef64a7c..8673813 100644 --- a/src/Status.php +++ b/src/Status.php @@ -83,13 +83,18 @@ public function setLogger(LoggerInterface $logger): void } /** - * Checking status messages + * Using and Checking status messages */ - public function reportCheck(): void + public function reportCheck(string $class): void { - if ($this->active) { - $this->output('Checking '.$this->envAllowXdebug); + if (!$this->active) { + return; + } + + if ($class !== XdebugHandler::class) { + $this->output(sprintf('Using extending class (%s)', $class)); } + $this->output('Checking '.$this->envAllowXdebug); } /** diff --git a/src/XdebugHandler.php b/src/XdebugHandler.php index 6d13dfd..f7d6e0d 100644 --- a/src/XdebugHandler.php +++ b/src/XdebugHandler.php @@ -142,7 +142,7 @@ public function setPersistent(): self */ public function check(): void { - $this->statusWriter->reportCheck(); + $this->statusWriter->reportCheck(get_class($this)); $envArgs = explode('|', (string) getenv($this->envAllowXdebug)); if (!((bool) $envArgs[0]) && $this->requiresRestart(self::$xdebugActive)) { @@ -260,6 +260,28 @@ public static function isXdebugActive(): bool return self::$xdebugActive; } + /** + * Allows an extending class to output an error status message + * + * This will be logged at WARNING level if a logger has been set and output + * to STDERR if XDEBUG_HANDLER_DEBUG=1. + */ + protected function outputError(string $message): void + { + $this->statusWriter->error(trim($message)); + } + + /** + * Allows an extending class to output an informational status message + * + * This will be logged at DEBUG level if a logger has been set and output to + * STDERR if XDEBUG_HANDLER_DEBUG=1. + */ + protected function outputInfo(string $message): void + { + $this->statusWriter->info(trim($message)); + } + /** * Allows an extending class to decide if there should be a restart * diff --git a/tests/App/Framework/MessageProvider.php b/tests/App/Framework/MessageProvider.php index f7b7beb..ab50876 100644 --- a/tests/App/Framework/MessageProvider.php +++ b/tests/App/Framework/MessageProvider.php @@ -15,10 +15,13 @@ class MessageProvider { + /** @var bool */ + private $extended = false; + /** @var array */ private $items = []; - public function __construct(string $envAllow) + public function __construct(string $envAllow, bool $extended) { // type: array of phrases to check for $this->items = [ @@ -36,18 +39,34 @@ public function __construct(string $envAllow) 'allow-mode' => ['No restart', $envAllow.'=', 'Allowed by xdebug.mode'], 'error' => ['No restart'], ]; + + if ($extended) { + $this->extended = true; + $first = ['extend' => ['Using extending class']]; + $this->items = array_merge($first, $this->items); + } } /** * @param string[] $types + * @param array $extras * @return array */ - public function compose(array $types): array + public function compose(array $types, ?array $extras = null): array { $result = []; + if ($this->extended && ($types[0] ?? null) === 'check') { + array_unshift($types, 'extend'); + } + foreach ($types as $key) { if (!isset($this->items[$key])) { + // Look for extras + if (isset($extras[$key]) && is_array($extras[$key])) { + $result[$key] = $extras[$key]; + continue; + } throw new \LogicException('Message type not found: '.$key); } $result[$key] = $this->items[$key]; diff --git a/tests/App/Helpers/FunctionalTestCase.php b/tests/App/Helpers/FunctionalTestCase.php index 3dc5a4a..0b5acad 100644 --- a/tests/App/Helpers/FunctionalTestCase.php +++ b/tests/App/Helpers/FunctionalTestCase.php @@ -102,6 +102,31 @@ protected function checkExpectedMessage(ProcessLog $log, int $index, string $exp protected function createMessageProvider(string $script): MessageProvider { $envAllow = $this->runner->getEnvAllow($script); - return new MessageProvider($envAllow); + $extended = strpos($script, '-extend-') !== false; + return new MessageProvider($envAllow, $extended); + } + + /** + * Modify the types array based on the specified operation + * + * @param string[] $types + */ + protected function modifyTypes(array &$types, string $type, string $operation, string $newType): void + { + $index = array_search($type, $types, true); + + if (!is_int($index)) { + throw new \LogicException('Type not found: '.$type); + } + + if ($operation === 'replace') { + $types[$index] = $newType; + } elseif ($operation === 'insert-after') { + array_splice($types, $index + 1, 0, $newType); + } elseif ($operation === 'insert-before') { + array_splice($types, $index, 0, $newType); + } else { + throw new \LogicException('Unknown operation: '.$operation); + } } } diff --git a/tests/App/Helpers/RestarterIni.php b/tests/App/Helpers/RestarterIni.php index 00ca87d..61bf21e 100644 --- a/tests/App/Helpers/RestarterIni.php +++ b/tests/App/Helpers/RestarterIni.php @@ -29,6 +29,10 @@ protected function requiresRestart(bool $default): bool { $this->required = (bool) ini_get('phar.readonly'); + if ($this->required) { + $this->outputInfo('phar.readonly=1 detected, restart required'); + } + return $default || $this->required; } @@ -39,6 +43,7 @@ protected function restart(array $command): void $content = file_get_contents((string) $this->tmpIni); $content .= 'phar.readonly = 0'.PHP_EOL; file_put_contents((string) $this->tmpIni, $content); + $this->outputInfo('phar.readonly=0 added to temporary ini file'); } parent::restart($command); diff --git a/tests/AppNoXdebugTest.php b/tests/AppNoXdebugTest.php index 843179b..0891c91 100644 --- a/tests/AppNoXdebugTest.php +++ b/tests/AppNoXdebugTest.php @@ -55,15 +55,20 @@ public function testForcedRestartWithNoXdebug(): void $logs = $this->runner->run($script, $options); + // use types to compose the message $provider = $this->createMessageProvider($script); [$mainTypes, $childTypes] = $provider->getRestartTypes(); + $extras = []; - $index = array_search('loaded', $mainTypes, true); - if (is_int($index)) { - $mainTypes[$index] = 'not-loaded'; - } + $this->modifyTypes($mainTypes, 'check', 'insert-after', 'info1'); + $extras['info1'] = ['phar.readonly=1 detected, restart required']; + + $this->modifyTypes($mainTypes, 'loaded', 'insert-after', 'info2'); + $extras['info2'] = ['phar.readonly=0 added to temporary ini file']; + + $this->modifyTypes($mainTypes, 'loaded', 'replace', 'not-loaded'); - $main = $provider->compose($mainTypes); + $main = $provider->compose($mainTypes, $extras); $child = $provider->compose($childTypes); $this->checkRestart($logs, $main, $child); diff --git a/tests/AppRestartTest.php b/tests/AppRestartTest.php index 5456ac6..de53969 100644 --- a/tests/AppRestartTest.php +++ b/tests/AppRestartTest.php @@ -146,8 +146,19 @@ public function testRestartWithIniChange(): void $logs = $this->runner->run($script, $options); + // use types to compose the message $provider = $this->createMessageProvider($script); - [$main, $child] = $provider->getRestartMessages(); + [$mainTypes, $childTypes] = $provider->getRestartTypes(); + $extras = []; + + $this->modifyTypes($mainTypes, 'check', 'insert-after', 'info1'); + $extras['info1'] = ['phar.readonly=1 detected, restart required']; + + $this->modifyTypes($mainTypes, 'loaded', 'insert-after', 'info2'); + $extras['info2'] = ['phar.readonly=0 added to temporary ini file']; + + $main = $provider->compose($mainTypes, $extras); + $child = $provider->compose($childTypes); $this->checkRestart($logs, $main, $child); // check child