From 398c825caa11aff2cb631fd8b033eb9d2edfc1f8 Mon Sep 17 00:00:00 2001 From: John Stevenson Date: Fri, 30 Oct 2026 20:41:44 +0000 Subject: [PATCH] Allow extending classes to output status messages New protected methods outputError() and outputInfo() have been added to the XdebugHandler class. Additionally, the use of an extending class is now reported in the output: 'Using extending class ...'. --- README.md | 20 +++++++++++++++++- src/Status.php | 13 ++++++++---- src/XdebugHandler.php | 24 ++++++++++++++++++++- tests/App/Framework/MessageProvider.php | 23 ++++++++++++++++++-- tests/App/Helpers/FunctionalTestCase.php | 27 +++++++++++++++++++++++- tests/App/Helpers/RestarterIni.php | 5 +++++ tests/AppNoXdebugTest.php | 15 ++++++++----- tests/AppRestartTest.php | 13 +++++++++++- 8 files changed, 125 insertions(+), 15 deletions(-) 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