Skip to content

feat: Add configurable logging with channel and level filtering#14

Merged
nfebe merged 6 commits into
devfrom
feat/logging-and-plugin-cache
Jun 7, 2026
Merged

feat: Add configurable logging with channel and level filtering#14
nfebe merged 6 commits into
devfrom
feat/logging-and-plugin-cache

Conversation

@nfebe

@nfebe nfebe commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

No description provided.

nfebe added 3 commits June 7, 2026 16:29
The package can now be installed and tested without PHP or Composer on
the host, matching the setup used by sibling packages. Common workflows
are exposed as make targets.
Discovery logging now goes through an injected logger with a
configurable channel and minimum level (warning by default), so
consuming applications no longer get debug noise in their logs and can
route or silence plugin engine output independently of application
logging.

A missing plugins directory now means no plugins: it is treated as a
normal state logged at debug instead of producing a warning on every
request, and a discovery scan emits a single summary line instead of
one line per directory entry. The configured plugins path is now
honored instead of being hardcoded.
Discovery results can now be compiled to a cache file that is loaded on
boot, removing the filesystem scan and manifest parsing from every
request. Deploys opt in the same way they cache configuration and
routes, and clearing the cache returns to live discovery.

Enabling, disabling, or rediscovering plugins refreshes an existing
cache so it never goes stale.
@sourceant

sourceant Bot commented Jun 7, 2026

Copy link
Copy Markdown

Code Review Summary

This PR introduces configurable logging with channel/level filtering and a plugin discovery cache system. It also provides a robust Docker-based development environment.

🚀 Key Improvements

  • Introduced LevelFilteringLogger to keep discovery logs out of production unless debug is enabled.
  • Added plugin:cache and plugin:clear commands to speed up application boot.
  • Dockerized development workflow with a helper Makefile.

💡 Minor Suggestions

  • Minor code style cleanup in PluginManager cache export.
  • Defensive file existence check in loadCachedPlugins.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

public function log($level, string|\Stringable $message, array $context = []): void
{
// Unknown levels pass through so the inner logger can decide how to handle them
$severity = static::SEVERITY[strtolower((string) $level)] ?? PHP_INT_MAX;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a log level is unknown, defaulting to PHP_INT_MAX ensures it is always logged. This is safe, but it might be clearer to explicitly define a fallback behavior in the class documentation.

Suggested change
$severity = static::SEVERITY[strtolower((string) $level)] ?? PHP_INT_MAX;
$severity = static::SEVERITY[strtolower((string) $level)] ?? 999; // Ensure unknown levels always pass through

$path = $this->getCachedPluginsPath();

File::ensureDirectoryExists(dirname($path));
File::put($path, '<?php return '.var_export($plugins, true).';'.PHP_EOL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using var_export is standard for Laravel caching, but for improved performance and security in production environments, consider using OPcache friendly exports or JSON if the structure becomes deeply nested, though array export is perfectly acceptable here for configuration-like data.

Suggested change
File::put($path, '<?php return '.var_export($plugins, true).';'.PHP_EOL);
File::put($path, '<?php return '.var_export($plugins, true).';');

These files predate the linter check and were failing it, blocking CI
on any branch. No behavior change.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.


public function handle()
{
$this->pluginManager->clearCachedPlugins();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache generation logic does not handle potential filesystem errors (like permission denied) which could lead to a 'Plugins cached successfully' message even if the File::put fails.

Suggested change
$this->pluginManager->clearCachedPlugins();
try {
$this->pluginManager->clearCachedPlugins();
$path = $this->pluginManager->cachePlugins();
$this->info("Plugins cached successfully at [{$path}].");
} catch (\Exception $e) {
$this->error("Failed to cache plugins: {$e->getMessage()}");
return 1;
}

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.

$this->pluginsPath = $app['config']->get('plugins.path') ?? base_path('plugins');
$this->classLoader = require base_path('vendor/autoload.php');
$this->output = $output ?? new OutputStyle(new ArrayInput([]), new NullOutput);
$this->logger = $logger ?? ($app->bound('log') ? $app->make('log') : new NullLogger);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While Laravel 11 uses PHP 8.2+, explicitly checking bound('log') is safe, but using the Application contract's make method with a fallback is cleaner. Additionally, if the app is bound, it's better to resolve the default logger through the container to ensure the LevelFilteringLogger or application default is used.

Suggested change
$this->logger = $logger ?? ($app->bound('log') ? $app->make('log') : new NullLogger);
$this->logger = $logger ?? ($app->has('log') ? $app->make('log') : new NullLogger());

Comment thread Dockerfile
FROM php:8.3-cli

# Install system dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running apt-get upgrade in a Dockerfile is generally discouraged as it makes the build non-deterministic and increases image size. Stick to apt-get update and specific package installations.

Suggested change
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
RUN apt-get update && apt-get install -y \

Bumps the package to 1.1.0 with changelog entries for configurable
logging, the compiled plugin cache, and the dockerized development
environment. Generalizes the package description and drops the stale
unreleased notes that duplicated the 1.0.0 entry.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. See the overview comment for a summary.


protected int $minSeverity;

public function __construct(LoggerInterface $logger, string $minLevel = LogLevel::DEBUG)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value here is redundant since the minSeverity logic already defaults to DEBUG (0) via the null coalescing operator on line 37. Removing it simplifies the signature.

Suggested change
public function __construct(LoggerInterface $logger, string $minLevel = LogLevel::DEBUG)
public function __construct(LoggerInterface $logger, string $minLevel)

*/
protected function loadCachedPlugins(): array
{
$plugins = require $this->getCachedPluginsPath();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The require statement should ideally be wrapped in a check to ensure the file exists, or the error should be gracefully handled, although pluginsAreCached check elsewhere usually prevents this.

Suggested change
$plugins = require $this->getCachedPluginsPath();
if (! $this->pluginsAreCached()) {
return [];
}
$plugins = require $this->getCachedPluginsPath();

@nfebe nfebe merged commit f0b1755 into dev Jun 7, 2026
1 check passed
@nfebe nfebe mentioned this pull request Jun 7, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant