Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Opcache Bundle Laravel Package

matthimatiker/opcache-bundle

Symfony bundle for managing and monitoring PHP OPcache from your application. Provides convenient integration for checking cache status, clearing OPcache, and exposing cache statistics, helping deployments and configuration changes run smoothly.

View on GitHub
Deep Wiki
Context7

Getting Started

Step 0 — accept the stack mismatch before you type anything. matthimatiker/opcache-bundle is a Symfony bundle. It ships a DataCollector, a DI extension, compiler passes, and Twig toolbar templates that register against WebProfilerBundle. Laravel does not run that pipeline — there is no registration path for Resources/views toolbar templates, and no DataCollectorInterface contract to satisfy. composer require matthimatiker/opcache-bundle in a Laravel 10/11 app will at best resolve to nothing useful and at worst collide on symfony/* constraints pinned to the Symfony 2.x/3.x era, forcing a downgrade of symfony/console, symfony/http-foundation, and friends. Do not add it as a dependency. You are reading this repo for the collector logic, not the package.

Where to look first, in this order. Open the repo and read four things, roughly 20–30 minutes:

  1. The collector class (under src/DataCollector/) — this is the whole reusable asset. It calls opcache_get_status() and opcache_get_configuration() and derives memory usage, hit rate, cached-script counts, and restart counters. Framework-agnostic.
  2. The DI extension / service definition — tells you what it injects and how it guards against disabled OPcache. Your ServiceProvider is a translation of this.
  3. The Twig templates — a spec for the Blade view: which fields the original author thought were worth showing, and in what grouping. Steal the layout, not the code.
  4. The console commands — the status/config output shape. This is your Artisan command's contract.

First use case: "is OPcache even on, and is it hitting?" That is the question the panel answers and it is the one that costs engineer-hours during performance work. Install nothing; write ~150–200 lines:

// app/Support/OpcacheInspector.php
final class OpcacheInspector
{
    public function status(bool $includeScripts = false): ?array
    {
        if (! function_exists('opcache_get_status')) {
            return null; // extension not loaded at all
        }
        $status = @opcache_get_status($includeScripts);
        return $status === false ? null : $status; // false = disabled or restrict_api
    }

    public function summary(): array
    {
        $s = $this->status(false);
        if ($s === null) {
            return ['enabled' => false];
        }
        $stats = $s['opcache_statistics'];
        return [
            'enabled'          => (bool) $s['opcache_enabled'],
            'cache_full'       => (bool) $s['cache_full'],
            'restart_pending'  => (bool) $s['restart_pending'],
            'hit_rate'         => round($stats['opcache_hit_rate'], 2), // already a percentage
            'hits'             => $stats['hits'],
            'misses'           => $stats['misses'],
            'cached_scripts'   => $stats['num_cached_scripts'],
            'used_memory'      => $s['memory_usage']['used_memory'],
            'free_memory'      => $s['memory_usage']['free_memory'],
            'wasted_memory'    => $s['memory_usage']['wasted_memory'],
            'wasted_pct'       => round($s['memory_usage']['current_wasted_percentage'], 2),
            'oom_restarts'     => $stats['oom_restarts'],
            'hash_restarts'    => $stats['hash_restarts'],
            'manual_restarts'  => $stats['manual_restarts'],
        ];
    }
}

Wire it into a ServiceProvider as a singleton, expose it through a route gated to local/staging, render a Blade partial. That is the entire product. Ship it in half a day and move on.

If you want it in the debug toolbar (because the team already lives there), implement a Laravel Debugbar collector instead of a standalone page — Barryvdh\Debugbar\DataCollector\... / implement the collect(), getName(), getWidgets() contract and register it in config/debugbar.php. Same inspector, one more adapter. This is the natural host for this data in a Laravel estate, and it avoids inventing a new dev-only URL.


Implementation Patterns

Pattern A — Artisan status command (dev/staging only)

Mirror the bundle's console commands as php artisan opcache:status and php artisan opcache:config. Table output, --json flag for piping into a deploy log.

public function handle(OpcacheInspector $inspector)
{
    $s = $inspector->summary();
    if (! $s['enabled']) {
        $this->warn('OPcache is not enabled (or opcache.restrict_api blocked the call).');
        return self::SUCCESS; // degrade, never throw
    }
    $this->table(['Metric', 'Value'], collect($s)->map(fn ($v, $k) => [$k, $v])->values()->all());
}

Read this twice: the Artisan process is a different PHP process from your FPM workers, with a separate OPcache shared memory segment. When opcache.enable_cli=0 (the default), the CLI cache is empty and you will report "0 cached scripts, 0% hit rate" on a perfectly healthy production-ish box. Either set opcache.enable_cli=1 deliberately for dev, or — better — make the command hit an internal HTTP route (Pattern C) so you read the FPM segment.

Pattern B — Debugbar collector

The least friction, highest daily-use value. Collect only the cheap keys (status(false)) on every request; make the scripts listing a separate on-demand fetch behind a ?opcache=1 query flag, or it will slow every dev request on a large app.

public function collect(): array
{
    return $this->inspector->summary(); // no `scripts` key, ever
}

public function getWidgets(): array
{
    return [
        'opcache' => [
            'icon'  => 'tasks',
            'map'   => 'opcache.enabled ? opcache.hit_rate + "% hit" : "OPcache off"',
        ],
    ];
}

Pattern C — Guarded JSON endpoint

The only pattern that reads the web OPcache segment and the only one that scales to "check all our boxes" via a loop of curls.

// routes/web.php
Route::get('/_diagnostics/opcache', OpcacheController::class)
    ->middleware(['web', 'auth', 'can:viewDiagnostics'])
    ->where('', '.*');

Harden it: abort_unless(app()->environment(['local', 'staging']), 404), auth + a gate, and return 404 (not 403) outside allowed environments so scanning bots learn nothing. The payload leaks absolute filesystem paths of cached scripts, php.ini directive values, and memory layout — this is a security review item, not a feature.

Pattern D — Deploy-time reset / invalidation

The bundle's service layer exposes reset and invalidation; in Laravel you call the functions directly.

  • opcache_reset() clears the entire cache of the process handling the call. In FPM all workers of the same pool share one segment, so one successful HTTP request resets the pool. Across N nodes you must hit N nodes.
  • opcache_invalidate($absolutePath, true) for surgical deploys (e.g. deployer symlink swap where you know which files changed). Prefer this over a blanket reset — it preserves hit rate for untouched code.
  • Calling opcache_reset() from a post-deploy Artisan command is a no-op for FPM. This is the #1 cause of "why is staging running old code?". Either hit the endpoint from the deploy script, or systemctl reload php8.2-fpm.
# Deployer / CI hook
for host in $WEB_HOSTS; do
  curl -fsS -H "Authorization: Bearer $DIAG_TOKEN" "https://$host/_diagnostics/opcache/reset"
done

Pattern E — Metric derivation you'll actually chart by hand

The bundle gives you raw status; the derived values are what you paste into a ticket:

  • Hit rate — already computed as opcache_statistics.opcache_hit_rate (a percentage). Do not recompute hits/(hits+misses) and forget blacklist_misses; you'll differ from the panel and confuse people.
  • Memory pressure — used_memory / (used_memory + free_memory + wasted_memory). Sustained >90% with oom_restarts > 0 means raise opcache.memory_consumption.
  • Script-capacity pressure — num_cached_scripts / max_cached_keys with hash_restarts > 0 means raise opcache.max_accelerated_files (round to a prime in the 16k–32k range; the directive rounds up to a prime internally).
  • Wasted-memory reset trigger — current_wasted_percentage approaching opcache.max_wasted_percentage (default 5). Cross it and PHP force-restarts itself; you'll see manual_restarts/oom_restarts climb and hit rate sawtooth.
  • Config drift — restart_pending === true means a directive changed and the segment hasn't reloaded. Surface this explicitly; it's the fastest answer to "we changed php.ini, did it take?".

Pattern F — Script listing done safely

Only when explicitly asked for, and only aggregated:

$status = $inspector->status(true); // include scripts — this can be huge
$top = collect($status['scripts'])
    ->reject(fn ($s) => ($s['type'] ?? '') === 'preload')
    ->sortByDesc('memory_consumption')
    ->take(25)
    ->map(fn ($s) => [
        'file'    => $s['filename'],
        'kb'      => round($s['memory_consumption'] / 1024, 1),
        'hits'    => $s['hits'],
        'idle_s'  => time() - ($s['last_used_timestamp'] ?? time()),
        'changed' => (bool) ($s['changed'] ?? false),
    ]);

scripts can hold tens of thousands of entries on a large app. Never dd() it, never json_encode() the whole thing, never render it paginated server-side. Aggregate, top-N, done.

Pattern G — Testing

Unit-test the derivation against fixture payloads captured from real PHP 8.1, 8.2, 8.3, and 8.4 boxes. The collector predates PHP 7.4 preloading and PHP 8's JIT — shapes differ (jit key exists only on 8.0+, preloaded files appear in scripts with type: "preload"). Fixture-based tests are cheap and catch the semantic drift before it produces a wrong number in a stakeholder deck.


Gotchas and Tips

The dependency itself. Never composer require this into Laravel. Vendor the collector logic (MIT — keep the attribution header in the ported file), pin nothing, and own ~200 lines. Adopting it as a requirement transfers an indefinite fork obligation for a read-only dev panel — a bad trade at 22 stars and 0 dependents.

opcache_get_status() returns false, not an exception, when OPcache is off or opcache.restrict_api is set. Every code path must handle false by rendering "OPcache disabled" — not by crashing. restrict_api is a common hardening directive in shared and containerised environments; you can't detect it from PHP, only observe the false.

opcache_get_status() with no argument includes the full scripts array. Always pass false for the summary path. This is the single easiest way to turn a debug panel into a memory incident.

CLI and FPM are different OPcache instances. Default opcache.enable_cli=0. An Artisan-only implementation will report an empty cache forever. Document this in the class docblock and in the README of your internal package, or you will re-explain it every quarter.

opcache_reset() outside the web context does nothing useful. Same root cause. Deploy resets must go through an HTTP request or an FPM reload — and must fan out to every node behind the LB. There is no cross-node magic in the bundle either; it's per-process, per-node, per-host.

Hit rate is cumulative since the last restart. It resets on deploy, on opcache_reset(), and on automatic restarts. A single reading after a deploy looks terrible and means nothing. If you want trend data, snapshot the counters and diff — but recognise that this is the point where you should stop and reach for an APM (Tideways, Blackfire, Datadog) instead of building a time-series store around a dev panel.

Preloading (7.4+) and JIT (8.0+) change the picture. Preloaded scripts appear in scripts with type: "preload", often with enormous hit counts, and skew naive hit-rate or "top scripts" views. Preloaded files generally can't be invalidated at runtime. If opcache.preload is set, filter them out of script listings and note it.

Octane (Swoole/RoadRunner) is a different runtime shape. Long-lived workers mean OPcache is read once at boot; a hit-rate panel is nearly meaningless because nothing recompiles. The CLI-side Octane master and its workers also have distinct relationships to OPcache. If the app runs Octane, say so in the assessment before this tool enters the discussion — it may be answering the wrong question entirely.

cache_full, restart_pending, restart_in_progress are booleans worth surfacing individually. They answer "did my php.ini change take effect?" and "am I about to take a cache miss storm?" without any math.

Disclosure surface is real. Absolute paths, config values, extension versions. Gate with environment check and auth and a gate; return 404 rather than 403 outside allowed environments. Do not put this behind a feature flag that someone can flip in production.

Extension point if you do fork: keep the inspector a plain, framework-agnostic class with constructor-injected nothing, and let the ServiceProvider/Debugbar collector/Artisan command all consume it. That's exactly the shape the Symfony bundle uses (collector + DI extension + command), and it's why the port is a day, not a week. The bundle's real value to a Laravel team is as a reference implementation and a checklist of OPcache metrics worth looking at — not as software to run.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
cgv/localized-resource