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

Xtend Laravel Package

konekt/xtend

Konekt Extend lets Laravel apps extend existing PHP classes at runtime—attach traits, methods, interfaces, and relationships (even to Eloquent models) without editing vendor or source files. Configuration-driven and ideal for modular packages and plugins.

View on GitHub
Deep Wiki
Context7

Getting Started

konekt/xtend is a micro-library that makes any PHP class extensible at runtime — you add a trait to a class, then register new methods (or whole plugin classes) onto it from the outside, typically from a Laravel service provider. It's the mechanism Konekt uses internally for their modular framework (Concord), and it's useful on its own when you need to bolt behavior onto a class you can't or don't want to edit directly.

Install it like any other package:

composer require konekt/xtend

Minimal setup: add the Extendable trait to the class you want to be extensible.

use Konekt\Extend\Concerns\Extendable;

class Invoice
{
    use Extendable;
}

Then register an extension and call it:

Invoice::extend('totalWithVat', function (float $vat = 0.21) {
    return $this->subtotal * (1 + $vat);
});

$invoice = new Invoice();
$invoice->subtotal = 100;

echo $invoice->totalWithVat(); // 121

Where to look first: the README.md, then src/Concerns/ (the Extendable trait is the entire public surface) and finally tests/. The source is small enough that reading it end-to-end takes ten minutes, and the tests are the real documentation — they show every supported calling convention, including how $this is bound and whether extensions take priority over real methods.

First real use case: you have a vendor model, an SDK client, or a value object from a package you shouldn't fork. Instead of subclassing, wrapping, or decorating everywhere, you add Extendable to a small local class and let your service provider plug in the extra behavior. In modular apps it's the cleanest way for one module to "contribute" methods to a host class without a compile-time dependency.

Implementation Patterns

Register from a service provider, not from register(). Extensions are conceptually boot-time configuration. Put extend() calls in boot() so the container and config are ready, and so the registration happens once per request in tests too.

class CrmServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Customer::extend('avatarUrl', function () {
            return $this->avatar
                ? Storage::url($this->avatar)
                : asset('img/default-avatar.png');
        });

        Customer::extend('displayName', fn () => $this->company ?: $this->name);
    }
}

Group extensions by concern, not by class. Instead of one giant AppServiceProvider with twenty extend() calls, create small dedicated providers per feature area (BillingExtensionsProvider, SearchExtensionsProvider). It mirrors how Laravel splits event listeners and keeps the boot() methods readable.

Use a plugin class when the extension is big. For anything more than a single closure — say a group of related methods, or a method that needs its own dependencies — pull it into a class and register that. This is the pattern Konekt uses internally: a class with register()-style static methods that adds several extensions at once.

class CustomerSearchPlugin
{
    public static function register(): void
    {
        Customer::extend('searchable', fn () => $this->scopeActive()->get());
        Customer::extend('matches', fn (string $q) => str_contains($this->name, $q));
    }
}

// boot()
CustomerSearchPlugin::register();

Testing pattern for extended methods. If your extensions depend on auth(), config, or the DB, write the test against the extended class, not the raw one. Use Laravel's standard feature tests — the extension is registered via the provider automatically. If you want a focused unit test, register the extension inline in setUp() and clear the state in tearDown() (see gotchas).

public function test_total_with_vat(): void
{
    Invoice::extend('totalWithVat', fn () => $this->subtotal * 1.21);

    $invoice = new Invoice();
    $invoice->subtotal = 100;

    $this->assertEquals(121, $invoice->totalWithVat());
}

Integration tip — keep it at the edges. The temptation is to model core domain behavior with extensions. Resist it: extensions are invisible to static analysis and to junior readers grepping the class. Use them for presentation helpers, cross-cutting conveniences, and vendor-object augmentation. Anything the domain depends on should be a real method.

Gotchas and Tips

Extensions are usually static state — this is the #1 source of bugs. Registering in boot() means every test that boots the app gets the registration, and if you register the same name twice (e.g., in a test and again in a provider), the second call overrides the first silently. Get in the habit of adding a tearDown() or TestCase::beforeApplicationDestroyed() reset that clears extensions between tests if you do a lot of inline registration. Check the trait source for a flushExtensions() or equivalent — if it doesn't exist, wrap it yourself.

$this binding is not guaranteed across versions. Most macro-style libraries (including Laravel's Macroable) use Closure::bindTo() so $this inside the extension points to the instance. Verify this in src/Concerns/Extendable.php before writing a closure that relies on it. A common failure mode is a package that calls the closure statically for "simplicity" — the closure then sees $this as null and you get a confusing error about accessing a property on null.

No IDE or static analysis support. Your IDE sees Invoice and has no idea totalWithVat() exists, so autocomplete fails and Psalm/PHPStan will flag every call. Mitigate with @method docblocks on the class, or (better) put the extensions behind a real façade method that calls the extension internally. Do not rely on extension methods being refactor-safe — renaming an extension string won't be flagged by any tooling.

Priority is usually last-wins, not first-wins. If both a package and your app register avatarUrl, whichever registers later wins. Because provider order in config/app.php controls the ordering, this can produce environment-specific bugs. If you need to guarantee your extension wins, register it in a provider that runs after the package's — or check if the trait exposes a hasExtension() guard you can use to fail loudly.

You cannot extend a class you cannot modify. The trait has to be on the class — there is no runtime "inject trait into an existing vendor class" magic here (and the libraries that claim to do it via reflection/eval are fragile and incompatible with opcache). If you need to extend Illuminate\Support\Collection and it doesn't already use a compatible trait, you're subclassing instead.

Debugging: dump the registry. When "my extension isn't being called", the cause is usually one of: (1) it was registered on a different class, (2) a later registration overwrote it, (3) the real method exists on the class and takes precedence over the magic __call. Check the trait source for a public registry getter, or use ReflectionClass to inspect the static $extensions property. Also confirm the class actually uses the trait — a silently use-less class will throw BadMethodCallException (or worse, __call silently returns null in some implementations).

Tiny library, low activity. With one star and no dependents, treat this as code you could reasonably vendor into app/Support/ if you were forced to. The upside: the surface area is small and stable (last release 2025-03), so the risk of breaking changes is low. The downside: no community to lean on if it breaks against a future PHP version. Pin the version in composer.json and be comfortable reading the trait if the docs (which are minimal) leave something ambiguous.

Extension points to know. Most of this library's usefulness comes from three things: the Extendable trait itself, whatever plugin/registration interface the package exposes for grouping extensions, and the binding strategy for closures. If you're building a package on top of xtend, expose your own registerPlugin(PluginInterface $plugin) and plugins() accessor so downstream apps can compose your extensions — that's the extension point that turns a toy into infrastructure.

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.
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus