›
byrcsc/laravel-custom-fields · 1.x
Install Laravel Custom Fields, create its two tables, and choose how the current tenant is resolved.
composer require byrcsc/laravel-custom-fieldsThe service provider is discovered automatically. It registers the
CustomFields facade, the custom-fields:prune command, and the tenant
resolver binding.
php artisan vendor:publish --tag=custom-fields-configPublish before you migrate. The migrations read
custom-fields.tables.definitions, custom-fields.tables.values, and
custom-fields.model_key_type, and changing any of the three after the tables
exist takes a migration of your own. Every key and its default is listed under
configuration.
The one you have to decide now is model_key_type, which shapes the model_id
column on the values table:
// config/custom-fields.php
'model_key_type' => env('CUSTOM_FIELDS_MODEL_KEY_TYPE', 'int'),| Value | model_id column | Use it when |
|---|---|---|
int | unsignedBigInteger | Your models use auto-incrementing ids |
uuid | uuid | Your models use UUID primary keys |
ulid | ulid | Your models use ULID primary keys |
string | string | Keys are strings of some other shape |
Anything else throws an InvalidArgumentException while the migration runs,
naming the four it accepts.
One key type for the whole installation. The values table holds rows for every model you add the trait to, so mixing an integer-keyed model and a UUID-keyed one is not supported. Pick
stringif you genuinely have both.
php artisan vendor:publish --tag=custom-fields-migrations
php artisan migrateTwo tables are created:
| Table | What it holds |
|---|---|
custom_field_definitions | One row per field per model class per tenant |
custom_field_values | One row per field per record per tenant, in typed columns |
There is no foreign key between them. That is deliberate: deleting a definition
leaves its values in place until custom-fields:prune
takes them.
use ByRcsc\LaravelCustomFields\Concerns\HasCustomFields;
use Illuminate\Database\Eloquent\Model;
final class Customer extends Model
{
use HasCustomFields;
}The trait adds getCustomField(), setCustomField(), setCustomFields(), a
customFields accessor, a customFieldValues relation, and the
whereCustomField() and whereCustomFieldIn() scopes. Its internals are all
prefixed with customField, because a method of the same name on your model
would win over a trait method and break writes quietly.
Values are stored against the record's morph class, so a morph map alias is respected the same way it is on any other polymorphic relation. Set your morph map before defining fields: definitions written under the class name are not found once the alias is in place.
custom-fields.tenant_resolver defaults to null, which means detect:
spatie/laravel-multitenancy installed, you get the bundled
SpatieTenantResolver and configure nothing.NullTenantResolver, under which every definition is
global and every read sees the same set.Detection is enough for both of those. Name a class to override it. A spatie
application that wants one shared set of fields rather than a set per tenant
points the key at NullTenantResolver explicitly, and any other source of
truth is a class of your own with one method:
use ByRcsc\LaravelCustomFields\Contracts\TenantResolver;
final class SubdomainTenantResolver implements TenantResolver
{
public function currentTenant(): ?string
{
return request()->route('tenant');
}
}// config/custom-fields.php
'tenant_resolver' => App\Tenancy\SubdomainTenantResolver::class,The class is resolved from the container, so it may take constructor
dependencies. A value that is not a TenantResolver throws
InvalidTenantResolverException the first time the resolver is needed, naming
the config key, rather than failing at boot as a container error.
See tenant scoping for what the resolver's answer changes.
use ByRcsc\LaravelCustomFields\Enums\FieldType;
use ByRcsc\LaravelCustomFields\Facades\CustomFields;
CustomFields::define(Customer::class, [
'key' => 'internal_reference',
'type' => FieldType::Text,
]);
CustomFields::definitions(Customer::class); // Collection: ['internal_reference' => ...]If that returns the definition, the tables exist, the facade is bound, and the
resolver is answering. Delete the definition afterwards with
CustomFieldDefinition::query()->where('key', 'internal_reference')->delete().