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

Unfinalize Laravel Package

stevebauman/unfinalize

Unfinalize is a Composer plugin for Laravel and PHP projects that strips the final keyword from vendor classes, making third-party classes easier to extend, decorate, or mock in tests without editing package source.

Deep Wiki
Context7

Getting Started

composer require stevebauman/unfinalize --dev

The package registers a Composer plugin that hooks into post-autoload-dump, so it runs automatically on the next composer install / composer update / composer dump-autoload. No service provider, no artisan command needed.

Create a unfinalize.php config at your project root and list what you want to rewrite:

<?php

return [
    'paths' => [
        __DIR__.'/vendor/laravel/framework/src/Illuminate/Foundation/Application.php',
        __DIR__.'/vendor/some/sdk/src/Client.php',
    ],
];

Then run:

composer dump-autoload

The listed files are parsed (nikic/php-parser under the hood) and the final keyword is stripped from the class and its methods in place. Verify by opening the vendor file, or reflect on it:

$r = new ReflectionClass(\Illuminate\Foundation\Application::class);
dump($r->isFinal()); // false

First use case: you need to subclass or Mockery::mock() a vendor class that's marked final — commonly Laravel framework internals, an HTTP SDK client, or a value object from a third-party package. Extend it, bind your subclass in a service provider, and register the path so the mutation survives every composer install.

Implementation Patterns

Surgical, per-class paths. List the exact file you need to extend. Broad directory paths work, but each file is rewritten on every autoload dump, so keep the blast radius tight — cheaper runs, easier review of what changed.

Pair with your override. The tool only removes final; it doesn't wire anything up. The complete pattern is:

// unfinalize.php
'paths' => [__DIR__.'/vendor/acme/sdk/src/HttpClient.php'],
// app/Services/HttpClient.php
class HttpClient extends \Acme\Sdk\HttpClient { /* override */ }
// app/Providers/AppServiceProvider.php
$this->app->bind(\Acme\Sdk\HttpClient::class, \App\Services\HttpClient::class);

Treat the config as committed source. Commit unfinalize.php; never commit the rewritten vendor file. The hook regenerates it, so the config is the change. Code review the config diff, not vendor.

CI / deploy ordering. Because it's a post-autoload-dump hook, it fires during composer install in CI. Ensure autoload-dump is not skipped (--no-scripts will bypass it — don't use that flag if you depend on unfinalizing). After install, run your test suite; the classes you mocked are now mockable.

Production without dev dependencies. If the override is needed at runtime (not just tests), composer install --no-dev won't install the plugin, and vendor stays finalized. Either promote it to a normal require, or run composer dump-autoload in a build stage before dropping dev deps — but the cleanest fix is to move the config path and the dependency to require when the override ships to production. For test-only mocking, --dev is correct.

Compare with cweagans/composer-patches. Patches are the right tool when you need to change behavior; unfinalize is the right tool when you only need to change extensibility. Prefer unfinalize for subclassing, patches for bug fixes — patches are more fragile across package updates.

Idempotency. Running the hook repeatedly is safe; the parser just rewrites an already-unfinalized file. No need to guard the config.

Gotchas and Tips

Vendor is regenerated. Any composer update/install that touches the package rewrites the vendor file from source, then the hook re-unfinalizes it. The window between is why you must not rely on manual sed edits — always go through the config.

--no-scripts and COMPOSER_NO_SCRIPTS. Both silently disable the plugin. If mocks suddenly fail in CI with "cannot mock final class," check for these flags first, then confirm the plugin is in composer.lock and not blocked by allow-plugins.

Composer allow-plugins prompt. Composer 2.2+ requires interactive approval for plugins. In CI this hangs or errors. Add to composer.json:

"config": {
    "allow-plugins": {
        "stevebauman/unfinalize": true
    }
}

OPcache and long-running workers. Rewriting a file on disk does not invalidate PHP's opcache or your already-booted workers. After install in a running environment: reload PHP-FPM, restart queue workers, and php artisan octane:reload if you use Octane. Locally, opcache_reset() or a FPM restart fixes "it still says final."

Only final is removed. The tool doesn't widen visibility on private constructors or private methods. If a class has a private constructor and you were hoping to instantiate it, you still need a patch or reflection.

Class-level vs. method-level final. Both are stripped from listed files, but if a final method lives on a parent class in a different file, list that parent file too. Unfinalizing the child doesn't unfinalize inherited methods.

Signature and static-analysis drift. Your IDE, Psalm/PHPStan, and composer dump-autoload --optimize all read the file at different times. After changing config, re-run the dump and re-index the IDE. A stale .phpstorm.meta or precomputed classmap will report "cannot extend final class" even though the file is fixed.

Forking is the more honest alternative. If you're unfinalizing three or more classes from the same package, that's the package telling you it wasn't designed to be extended. Consider a maintained fork or a wrapper/decorator (composition over inheritance) before relying on source rewriting. Unfinalize is a pragmatic stopgap, not an architectural solution — treat it as a debt marker with a comment in the config explaining why.

Debugging checklist. (1) Is the path absolute and correct? (2) Did autoload-dump actually run (check for the plugin's output or -vvv)? (3) grep -n 'final class' vendor/.../File.php. (4) OPcache / worker restart. (5) Composer scripts and allow-plugins.

Extension point to know: the plugin is Composer-driven, so the reliable integration surface is composer.json scripts and the unfinalize.php paths array. There's no Laravel event or Artisan command to hook — if you need conditional unfinalizing (e.g., only in testing), do it by swapping the config file or setting the environment before composer install in CI, not at runtime.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core