refactor: one logging helper LIT\MCP\Log (M6)

Log::failure(context, detail) keeps the '[wp-lit-mcp] <context> failed: <detail>'
format of the nine former error_log() sites; Log::line() covers the change log line.
One phpcs ignore instead of eleven.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 17:36:01 +02:00
co-authored by Claude Opus 5.5
parent e005d83a3d
commit b6984d3f9a
11 changed files with 102 additions and 26 deletions
+4 -6
View File
@@ -6,6 +6,7 @@ namespace LIT\MCP\Abilities;
use LIT\MCP\Access; use LIT\MCP\Access;
use LIT\MCP\Integrations\IntegrationRegistry; use LIT\MCP\Integrations\IntegrationRegistry;
use LIT\MCP\Log;
use LIT\MCP\Server\RequestContext; use LIT\MCP\Server\RequestContext;
use Throwable; use Throwable;
use WP_Error; use WP_Error;
@@ -64,8 +65,7 @@ final class AbilityRegistrar
try { try {
return $ability->permission(is_array($input) ? $input : []); return $ability->permission(is_array($input) ? $input : []);
} catch (Throwable $e) { } catch (Throwable $e) {
$message = sprintf('[wp-lit-mcp] %s failed: %s', $ability->name(), $e); Log::failure($ability->name(), $e);
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return Errors::internal(); return Errors::internal();
} }
}, },
@@ -73,12 +73,10 @@ final class AbilityRegistrar
try { try {
return $ability->execute(is_array($input) ? $input : []); return $ability->execute(is_array($input) ? $input : []);
} catch (WriteFailed $e) { } catch (WriteFailed $e) {
$message = sprintf('[wp-lit-mcp] %s failed: %s', $ability->name(), $e); Log::failure($ability->name(), $e);
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return $e->toError(); return $e->toError();
} catch (Throwable $e) { } catch (Throwable $e) {
$message = sprintf('[wp-lit-mcp] %s failed: %s', $ability->name(), $e); Log::failure($ability->name(), $e);
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return Errors::internal(); return Errors::internal();
} }
}, },
+2 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace LIT\MCP\Abilities; namespace LIT\MCP\Abilities;
use LIT\MCP\Log;
use Throwable; use Throwable;
/** /**
@@ -20,8 +21,7 @@ final class FollowUp
try { try {
return $step(); return $step();
} catch (Throwable $e) { } catch (Throwable $e) {
$message = sprintf('[wp-lit-mcp] %s failed: %s', $context, $e); Log::failure($context, $e);
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
$warnings[] = $warning; $warnings[] = $warning;
return null; return null;
+2 -2
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace LIT\MCP\ChangeLog; namespace LIT\MCP\ChangeLog;
use LIT\MCP\Abilities\FollowUp; use LIT\MCP\Abilities\FollowUp;
use LIT\MCP\Log;
use LIT\MCP\Server\RequestContext; use LIT\MCP\Server\RequestContext;
/** /**
@@ -57,8 +58,7 @@ class ChangeLog
); );
if ($inserted === false) { if ($inserted === false) {
$message = sprintf('[wp-lit-mcp] change log insert failed for %s %s:%s', $ability, $objectType, $objectId); Log::line(sprintf('change log insert failed for %s %s:%s', $ability, $objectType, $objectId));
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return null; return null;
} }
@@ -10,6 +10,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
use LIT\MCP\Integrations\Divi\BlockTree\Parser; use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\WordPress\PostGuard; use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Log;
use WP_Error; use WP_Error;
use WP_Post; use WP_Post;
@@ -51,8 +52,7 @@ class DiviPageLoader
try { try {
$document = $this->parser->parse($content); $document = $this->parser->parse($content);
} catch (MalformedMarkup $e) { } catch (MalformedMarkup $e) {
$message = sprintf('[wp-lit-mcp] divi parse of post %d failed: %s', $post->ID, $e->getMessage()); Log::failure(sprintf('divi parse of post %d', $post->ID), $e->getMessage());
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return Errors::unsupported( return Errors::unsupported(
sprintf( sprintf(
@@ -15,6 +15,7 @@ use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Integrations\Divi\Compose\TextReplacer; use LIT\MCP\Integrations\Divi\Compose\TextReplacer;
use LIT\MCP\Integrations\WordPress\PostGuard; use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema; use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Log;
use Throwable; use Throwable;
use WP_Error; use WP_Error;
@@ -484,8 +485,7 @@ final class DiviReplaceTextAbility implements Ability
private static function log(int $postId, Throwable $e): void private static function log(int $postId, Throwable $e): void
{ {
$message = sprintf('[wp-lit-mcp] lit/divi-replace-text on post %d failed: %s', $postId, $e); Log::failure(sprintf('lit/divi-replace-text on post %d', $postId), $e);
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
} }
private function db(): \wpdb private function db(): \wpdb
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Pages;
use Closure; use Closure;
use LIT\MCP\Integrations\Divi\Compose\Placeholders; use LIT\MCP\Integrations\Divi\Compose\Placeholders;
use LIT\MCP\Integrations\Divi\Compose\VariableRef; use LIT\MCP\Integrations\Divi\Compose\VariableRef;
use LIT\MCP\Log;
use Throwable; use Throwable;
/** /**
@@ -43,12 +44,7 @@ class DynamicContentMap
try { try {
$options = $this->registry !== null ? ($this->registry)($postId) : self::divi($postId); $options = $this->registry !== null ? ($this->registry)($postId) : self::divi($postId);
} catch (Throwable $e) { } catch (Throwable $e) {
$message = sprintf( Log::failure(sprintf('divi dynamic content options of post %d', $postId), $e->getMessage());
'[wp-lit-mcp] divi dynamic content options of post %d failed: %s',
$postId,
$e->getMessage()
);
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return null; return null;
} }
+2 -2
View File
@@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Media;
use LIT\MCP\Abilities\Errors; use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\WriteFailed; use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\Limits; use LIT\MCP\Limits;
use LIT\MCP\Log;
use Throwable; use Throwable;
use WP_Error; use WP_Error;
@@ -197,8 +198,7 @@ class MediaImporter
); );
if ($attachmentId instanceof WP_Error) { if ($attachmentId instanceof WP_Error) {
// WordPress' message can contain server paths: only for the log, not for the client. // WordPress' message can contain server paths: only for the log, not for the client.
$message = sprintf('[wp-lit-mcp] media sideload failed: %s', $attachmentId->get_error_message()); Log::failure('media sideload', $attachmentId->get_error_message());
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return Errors::writeFailed( return Errors::writeFailed(
'WordPress hat die Datei nicht übernommen (z. B. Upload-Verzeichnis nicht beschreibbar).' 'WordPress hat die Datei nicht übernommen (z. B. Upload-Verzeichnis nicht beschreibbar).'
+2 -2
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\Media; namespace LIT\MCP\Integrations\Media;
use Closure; use Closure;
use LIT\MCP\Log;
/** /**
* Finds where an attachment is used (spec §6.3). Deliberately errs on the side of "used": * Finds where an attachment is used (spec §6.3). Deliberately errs on the side of "used":
@@ -165,8 +166,7 @@ class MediaUsageFinder
} }
$this->queryFailed = true; $this->queryFailed = true;
$message = sprintf('[wp-lit-mcp] media usage query failed: %s', (string) $wpdb->last_error); Log::failure('media usage query', (string) $wpdb->last_error);
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return []; return [];
} }
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace LIT\MCP;
use Throwable;
/**
* The plugin's one way into the PHP error log. Messages carry the "[wp-lit-mcp]" prefix; the
* log is for the site operator only and may contain details (paths, traces) that must never
* reach an MCP client.
*/
final class Log
{
private const PREFIX = '[wp-lit-mcp] ';
/** Logs "[wp-lit-mcp] <context> failed: <detail>"; a Throwable is logged with its trace. */
public static function failure(string $context, Throwable|string $detail): void
{
self::line(sprintf('%s failed: %s', $context, (string) $detail));
}
/** Logs one free-form line with the plugin prefix. */
public static function line(string $message): void
{
error_log(self::PREFIX . $message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
}
+2 -2
View File
@@ -7,6 +7,7 @@ namespace LIT\MCP\Server;
use LIT\MCP\Abilities\ExposurePolicy; use LIT\MCP\Abilities\ExposurePolicy;
use LIT\MCP\Guides\InstructionsBuilder; use LIT\MCP\Guides\InstructionsBuilder;
use LIT\MCP\Integrations\IntegrationRegistry; use LIT\MCP\Integrations\IntegrationRegistry;
use LIT\MCP\Log;
use LIT\MCP\Plugin; use LIT\MCP\Plugin;
use WP\MCP\Core\McpAdapter; use WP\MCP\Core\McpAdapter;
use WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler; use WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler;
@@ -131,8 +132,7 @@ final class ServerRegistrar
$result = ($this->createAdapterServer)($args); $result = ($this->createAdapterServer)($args);
if (is_wp_error($result)) { if (is_wp_error($result)) {
$message = '[wp-lit-mcp] MCP server registration failed: ' . $result->get_error_message(); Log::failure('MCP server registration', $result->get_error_message());
error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
} }
} }
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit;
use LIT\MCP\Log;
use RuntimeException;
final class LogTest extends TestCase
{
/** @return string What the callback wrote to the error log. */
private static function captured(callable $write): string
{
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
$previous = ini_set('error_log', $log);
try {
$write();
} finally {
ini_set('error_log', (string) $previous);
}
$content = (string) file_get_contents($log);
unlink($log);
return $content;
}
public function testFailureKeepsThePrefixedFormat(): void
{
$content = self::captured(static fn () => Log::failure('media usage query', 'Table gone'));
self::assertStringContainsString('[wp-lit-mcp] media usage query failed: Table gone', $content);
}
public function testFailureLogsAThrowableWithItsTrace(): void
{
$e = new RuntimeException('kaputt');
$content = self::captured(static fn () => Log::failure('lit/x', $e));
self::assertStringContainsString('[wp-lit-mcp] lit/x failed: RuntimeException: kaputt', $content);
self::assertStringContainsString('Stack trace', $content);
}
public function testLineOnlyAddsThePrefix(): void
{
$content = self::captured(static fn () => Log::line('change log insert failed for a b:1'));
self::assertStringContainsString('[wp-lit-mcp] change log insert failed for a b:1', $content);
}
}