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 Custom Fields Laravel Package

asseco-voice/laravel-custom-fields

Extend Laravel Eloquent models with user-defined custom fields, no schema changes needed. Define fields per model, store values flexibly, and use them in forms, validation, and queries—a simple way to let apps add their own attributes dynamically.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect before writing any code — the migration is the API contract for this package:

composer require asseco-voice/laravel-custom-fields
php artisan vendor:publish --provider="AssecoVoice\CustomFields\CustomFieldsServiceProvider"
php artisan migrate

Read the published migration first. Everything you need to know about how the package stores data (model_type/model_id morphs, the field key column, the value column, and — as of v3.3.1 — a renderer column) is there, and the trait's method names follow from it. Then attach the trait to any model that needs user-defined attributes:

use AssecoVoice\CustomFields\Traits\HasCustomFields;

class Product extends Model
{
    use HasCustomFields;
}

First use case: let an admin add "Internal SKU", "Warranty period" or "Country of origin" to Product without a schema migration or a deploy. That is the whole value proposition — treat the package as a key/value sidecar, not as a replacement for real columns. Anything you filter, sort, or join on heavily should still be a real column; anything editorial and sparse belongs in custom fields.

Implementation Patterns

Trait + morph map. Register a morph map before you ship, or your rows will be tied to FQCNs and break the day someone moves a class into App\Domain\...:

Relation::enforceMorphMap(['product' => Product::class, 'customer' => Customer::class]);

Read as a keyed array, not a collection. The relationship returns rows; views and API resources almost always want key => value:

$product->load('customFields');
$fields = $product->customFields->pluck('value', 'field_name');

$fields->get('warranty_period');

Eager load with with('customFields') on any index/list endpoint — this is a polymorphic relation, so it will N+1 silently otherwise.

Drive forms from the renderer column (the v3.3.1 addition). The new column lets each field declare how it should be presented, so the frontend stops hardcoding "everything is a text input":

// config/custom-fields.php — your own whitelist, not the DB's
'renderers' => [
    'text'     => \App\View\Components\Fields\Text::class,
    'number'   => \App\View\Components\Fields\Number::class,
    'select'   => \App\View\Components\Fields\Select::class,
    'date'     => \App\View\Components\Fields\Date::class,
],
$product->customFields->map(fn ($f) => [
    'key'      => $f->field_name,
    'value'    => $f->value,
    'renderer' => $f->renderer ?? 'text',   // backfill nulls in code first
]);

Resolve the renderer through your own map and fall back to text on anything unknown — never to a value straight from the database.

Validate and cast on the way out. The package stores what you give it, as strings. Put the typing in a FormRequest and a cast layer:

'warranty_period' => ['nullable', 'integer', 'min:0', 'max:120'],
'country'         => ['nullable', Rule::in(array_keys(config('countries')))],
$fields->put('is_fragile', filter_var($fields->get('is_fragile'), FILTER_VALIDATE_BOOLEAN));

Define field schemas in config, sync into the DB. For anything beyond a handful of fields, keep the canonical list in config/custom-fields.php per model type and write an artisan command that upserts rows and deletes removed keys. Hand-editing rows in production is how you end up with Warranty_Period and warranty_period both live.

Filtering. whereHas('customFields', fn ($q) => $q->where('field_name', 'country')->where('value', 'DE')) works, but only add an index on (field_name, value) if you actually need it — value columns are not indexed by the package and this is the query that will fall over first at scale.

Gotchas and Tips

  • The v3.3.1 renderer column only lands if you run the package migrations. If you published and hand-edited the migration earlier, vendor:publish will not overwrite it — add the column yourself as nullable()->default('text'), then backfill existing rows. Treat null as text in every read path so pre-3.3.1 data keeps working.
  • Mass assignment. If you create rows via ->create(), check the custom field model's $fillable; a silently dropped renderer attribute is the most likely first bug you hit with this release.
  • "0" is a valid value and falsy. empty(), filter(), and ?? will all bite you. Use !== null and !== '' checks when reading field values.
  • Nothing cascades for free. The morph columns are usually not foreign keys, so deleting a Product orphans its custom fields. Add a deleting observer to clean up.
  • Renderers are a presentation hint, not a security boundary. Whitelist them against a known map and render via Blade components — never interpolate a DB value into a view path or a {!! !!} block.
  • Multi-tenancy is yours to implement. The package has no notion of tenant; add a global scope to the relationship or a tenant_id on the table, or admins will see each other's field definitions.
  • Cache field definitions, not values. Definitions change on the order of weeks, values on the order of requests.
  • Debugging: $product->customFields()->toSql() and DB::listen() will tell you more than the exception handler here, because most failures are "the row was never written" rather than "the package threw."
  • Extension points: the published config and the trait are the two surfaces worth overriding — subclass the trait if you need per-model defaults, and wrap the model in your own repository/service if you need audit logging on value changes.
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