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

Laravel Extendable Laravel Package

ironshark/laravel-extendable

Add extensibility to Laravel Eloquent models: dynamically attach extra attributes, methods and relationships at runtime without new migrations, letting packages and apps customise core models cleanly and safely.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and inspect before wiring anything in.

composer require ironshark/laravel-extendable
php artisan vendor:publish --provider="Ironshark\Extendable\ExtendableServiceProvider"

At 24 stars and zero dependents, treat the publish step as the first thing you do — you want the config file and migrations in your own repo so you can read and own them. If the package ships migrations, check whether they create a JSON/TEXT column on your table or a side table (e.g. extendable_fields); that single detail determines your querying strategy later, so look there before anything else.

2. Run migrations and add the trait to one model.

php artisan migrate
use Ironshark\Extendable\Traits\Extendable;

class Product extends Model
{
    use Extendable;
}

3. First use case — the one that justifies the package.

The realistic day-one scenario is a model that needs per-tenant or per-customer extra fields you don't want to migrate for: a Product with warranty_months for one client and hs_code for another. Instead of ALTER TABLE per customer, the trait lets those live as data:

$product->warranty_months = 24;   // stored in the extendable payload
$product->save();

$product->warranty_months;        // 24

Verify the round-trip with $product->fresh() and $product->getAttributes() — if the value comes back as a string "24", the package is relying on raw assignment and you'll need to add casts or a field-type registry yourself. Do this check in artisan tinker before you commit to the pattern.

4. Read the trait source. It's a small package; vendor/ironshark/laravel-extendable/src/Traits/ is a five-minute read and will tell you more than the README about how attributes are resolved, whether getAttribute is overridden, and what the storage column is actually called.

Implementation Patterns

JSON blob vs. side table — decide deliberately.

Most packages in this space use one of two shapes, and they behave very differently:

  • Single JSON column (meta, custom_fields, extendable): one UPDATE, cheap to load, but every custom field is opaque to SQL unless you use whereJsonContains / -> operators. Good for display-only attributes.
  • Side table (one row per model+key): queryable and indexable per field, but adds a join or a lazy-load per model. Watch for N+1 on index pages.

Check which one you got from the published migration and design accordingly. If it's a side table, eager load it in list queries the same way you would any relation.

Define the field schema in config, not in your head.

Whatever mechanism the package offers for declaring allowed keys, centralise it. The common failure mode with extendable fields is that a typo in a Blade template silently creates a new field instead of throwing. A registry gives you validation and a place to document intent:

// config/extendable.php or a dedicated ProductFields class
'product' => [
    'warranty_months' => ['type' => 'integer', 'rules' => ['nullable', 'integer', 'min:0']],
    'hs_code'         => ['type' => 'string',  'rules' => ['nullable', 'string', 'max:32']],
],

Then validate on write with Validator::make($request->only(array_keys($schema)), $rules) — do not assume the package validates for you; most don't.

Wrap the trait behind your own base model.

Because you're depending on a 24-star package, isolate it:

abstract class ExtendableModel extends Model
{
    use Extendable;
}

Now every model that needs custom fields extends ExtendableModel, and if the package is abandoned you swap the trait in one file. This is cheap insurance and costs you nothing on day one.

Admin/API surfaces.

In toArray() / API resources, custom fields normally come through automatically if getAttribute is overridden — confirm with $model->toArray(). For admin panels (Filament/Nova) you'll usually be building fields dynamically from the schema registry anyway, so keep the registry the single source of truth and generate both the validation rules and the form controls from it.

Bulk import / seeding. When importing, assign extended fields after fill() and before save(); if the package hooks saved to persist the payload separately, bulk insert() calls will bypass it entirely. Use Model::create() in imports unless you've verified the persistence path.

Gotchas and Tips

Mass assignment will bite you. Extended fields are typically not in $fillable, so Product::create(['warranty_months' => 24]) may silently drop the value. Either add the keys to $fillable, or (better) set them explicitly after creation. Test this — silent drops are the #1 support ticket with extendable-field packages.

Type coercion is your problem. JSON columns round-trip integers as integers in Laravel when cast to array/json, but a side-table design usually returns strings. Budget for a small cast layer or $casts-style mapping in your schema registry. Booleans ("true" vs true) and nulls-vs-missing are the two that cause UI bugs.

Don't let the payload grow unbounded. A JSON column that accumulates abandoned keys from old UI experiments is a real maintenance cost. Since keys are data, not schema, nothing stops them multiplying. Add a periodic cleanup command or an admin screen listing distinct keys per model.

Querying is the main trade-off. whereJsonContains('meta->warranty_months', 24) works on MySQL 5.7+/Postgres but not SQLite in older versions — relevant because you'll want feature tests. If you need to filter or sort on a custom field regularly, that's a signal it should become a real column; migrate it out. The package is for the long tail, not for your primary filters.

Dirty tracking and model events. If the trait stores the payload in a JSON column, isDirty() and wasChanged() behave as expected. If it uses a side table, $model->isDirty('warranty_months') may return false even though data changed. Audit-logging and observer code that relies on wasChanged needs a test here.

Multi-tenancy and migrations. Per-tenant databases mean the published migration has to run in every tenant context. If you're using stancl/tenancy or similar, add the migration to your tenant migration path explicitly rather than assuming the package's service provider handles it.

Extension points worth knowing. Look for a custom cast class or a resolvable field-type registry in the package — these are the seams for adding validation, serialization, and search behaviour without forking. If none exist, your own ExtendableModel base class plus a schema registry is the extension point, which is another argument for the wrapper.

Debugging checklist when a field "doesn't save":

$model->fill([...]);
dump($model->getAttributes());      // did the value ever land on the model?
$model->save();
dump($model->fresh()->toArray());   // did it persist?
dump($model->getDirty());           // was it considered changed?

That three-line sequence distinguishes "package ignored the key" from "database/column problem" from "mass-assignment problem" faster than reading the source again.

Abandonment risk. With 24 stars and no dependents, pin the version in composer.json ("ironshark/laravel-extendable": "^1.0") and don't rely on a Laravel 12 compatibility release arriving. The wrapper-model pattern above, plus keeping the published migration in your repo, means a fork or an in-house rewrite is a contained afternoon rather than a project.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource