Download the PHP package byrcsc/laravel-custom-fields without Composer

On this page you can find all versions of the php package byrcsc/laravel-custom-fields. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package laravel-custom-fields

Laravel Custom Fields

Latest Version on Packagist GitHub Tests Action Status GitHub PHPStan Action Status Total Downloads

Add typed, filterable custom fields to any Eloquent model without changing its table. Use one global field set in a single-tenant application, or scope fields and values per tenant when each tenant needs different customer details.

Definitions live in one table and values in a typed polymorphic second table. Models get a trait exposing read and write methods, query scopes, generated validation rules, and virtual defaults.

The package is developer-facing infrastructure. Which fields exist, what they validate, and who may write them stays yours, and so does any admin UI a tenant would use to manage them.

Laravel Tested PHP versions
12.x 8.3, 8.4
13.x 8.3, 8.4

Documentation

Read the official documentation for installation, field definitions, reading and writing values, validation, filtering, tenant scoping, performance, and troubleshooting.

Installation

Install the package via composer:

Publish and run the migrations:

Publish the config file if you need to change anything in it:

What a custom field is

A custom field is a definition a developer creates at runtime: a key, a type, optional validation rules, and an optional tenant. Records store values against those definitions in typed columns, so a number filters as a number and a date is stored as a date. A definition with no tenant is global and visible to every tenant; on a key collision the tenant's own field wins.

Two tables hold all of it. custom_field_definitions has one row per field per model class per tenant. custom_field_values has one row per field per record per tenant, with the value in whichever typed column its type names. Your own tables never change.

Quick start

Add the trait to any model:

Define a field:

Write and read it:

Filter by it:

A customer that never set the field reads the definition's default, so getCustomField('account_tier') returns standard without a value row existing.

Defining fields

define() takes key and type, and optionally label, description, default, options, rules, section, sort_order, and tenant.

A key is lowercase, starts with a letter, and continues with letters, digits, or underscores, up to 64 characters. Anything the facade cannot honour throws on the line that wrote it: an unknown type, an unknown attribute name, a select without options, a rule it will never run.

The eleven types are text, textarea, email, url, number, boolean, date, datetime, select, multi_select, and json. Each is stored in the column its type names, and read back as its native PHP type: a number as int or float, a date as a Carbon instance, a multi-select as a list of strings.

section and sort_order are metadata. The package stores them, returns them, and attaches no behaviour to them; grouping and ordering fields on a screen is yours.

Definitions are queryable through CustomFieldDefinition and writable only through the facade, which is where the checks above live. The model guards every attribute against mass assignment to keep it that way.

Tenant scoping

Which tenant the package is acting for comes from a resolver:

Point custom-fields.tenant_resolver at it. The key defaults to null, which means detect: applications with spatie/laravel-multitenancy installed get the bundled SpatieTenantResolver and configure nothing, and applications without it get NullTenantResolver, under which every field is global. A spatie application that wants one shared set of fields rather than a set per tenant names NullTenantResolver here explicitly.

Null is a real answer, not a failure. It is what a single-tenant application returns always, and what a multi-tenant one returns from a console command. Definitions written under it are global.

Passing 'tenant' => 'acme' to define() writes a field for a tenant you are not currently acting for, which is what a seeder or a console command needs. Passing 'tenant' => null writes a global field while a tenant is current.

Reading and writing values

customFields is an Illuminate\Support\Collection, so ->all() gives you the plain array and everything else a collection does is available on it. Global definitions are shared, but their values are not: the same record can hold a different value for each tenant.

Writes persist immediately. setCustomFields() resolves every key before writing anything, so a call naming one unknown field writes none of them.

Writing null clears a field, which returns it to the definition's default. There is no third state where a record holds an explicit null that outranks the default.

Reading or writing a key the current tenant has no definition for throws UnknownCustomFieldException, on reads as well as writes. A typo that quietly returned null would read exactly like a field nobody has filled in.

Defaults are virtual: they are resolved when you read, never written as rows. Changing a definition's default changes what every unset record returns, and that is the documented behaviour rather than a side effect.

Validation

The generator hands a form request a ready rules array:

prefix nests the keys for a form that posts its custom fields under one name. Pass the record being updated as ignoring: so a unique rule does not read its own stored value as a duplicate.

The same rules run on write. setCustomField() throws Laravel's own ValidationException on a value that breaks them, which custom-fields.validate_on_write turns off for an application that would rather validate every write itself.

Per-field rules are required, min, max, regex, and unique, given as a map:

unique means unique for that field, under that tenant, on that model type. It is advisory. It reads and then writes with no lock between, so two concurrent requests can both pass it and both insert. Enforcing it properly would need a partial unique index across one of six columns chosen by a value in another table, which no supported engine expresses. It is refused on multi_select and json, whose whole-value comparison is not portable.

Filtering

Both compile to a comparison on one typed column inside an exists clause, and both are scoped to the field the current tenant can see.

Filtering a multi_select means contains: pass one option, and it matches every record whose list holds it.

Two limits worth knowing. whereCustomFieldIn is for single-value types; multi-select is a list already, so use the contains form above. And json cannot be filtered by equality at all, because comparing a whole JSON value differs across MySQL, PostgreSQL, and SQLite down to key order and whitespace. Both throw UnfilterableFieldException rather than returning different rows on different engines.

string_value, number_value, boolean_value, and date_value are indexed alongside the field id, so those filters use an index. text_value and json_value are not: no portable index covers a column wide enough to hold them, so filtering a textarea or a multi_select scans the values for that field.

Eager loading

Reading custom fields on a listing costs one extra query for the whole page, not one per record, as long as you eager load the relation:

The definitions themselves are resolved once per model class and tenant for the life of the request, so they do not become the N+1 that the eager-loaded values no longer are.

Definition lifecycle

Three guarantees, all of them about not losing data quietly:

A type change is refused while values exist. Each type lives in its own column, so changing a type points the field at a column its values are not in. CannotChangeFieldTypeException names the field, both types, and how many values are in the way. Migrate or clear them, then change the type.

A stored option that was later removed keeps being returned. It was valid when it was written. Validation applies on the next write, so writing it again is refused, but reading it back is not.

Deleting a definition leaves its values alone. There is no foreign key and no cascade, so removing a field by mistake does not take the data with it. When the removal was meant, one command takes the orphans:

Prune deletes only orphaned values, one row at a time so the value model's delete events fire, and reports how many it took.

The type guard hangs on the definition model's updating event, so it sees a change made through the model, which is the intended path. It cannot see a saveQuietly(), a write inside withoutEvents(), or a mass query()->update(), because none of those fire a model event. The same is true of the resolved-definitions cache; call CustomFields::flush() after a write of that kind.

Events

The definition and value models fire Laravel's standard Eloquent events: creating, created, updating, updated, saving, saved, deleting, and deleted. There are no dedicated event classes, so hook the models:

Configuration

Key Default What it does
tenant_resolver null The resolver class. Null means detect.
tables.definitions custom_field_definitions Definitions table name.
tables.values custom_field_values Values table name.
model_key_type int Shape of model_id: int, uuid, ulid, or string.
validate_on_write true Whether the trait validates before writing.

model_key_type and validate_on_write read CUSTOM_FIELDS_MODEL_KEY_TYPE and CUSTOM_FIELDS_VALIDATE_ON_WRITE from the environment.

Set the table names and the key type before migrating. Changing them afterwards takes a migration of your own.

Out of scope

Deliberate boundaries rather than gaps:

Versioning

The package follows semantic versioning.

Bug fixes go into the newest version only. To get a fix, upgrade to it.

Questions and issues

This package is maintained by one person, so replies can take a while. Everything gets read.

License

MIT. See CHANGELOG.md.


All versions of laravel-custom-fields with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
illuminate/console Version ^12.0||^13.0
illuminate/contracts Version ^12.0||^13.0
illuminate/database Version ^12.0||^13.0
illuminate/support Version ^12.0||^13.0
illuminate/validation Version ^12.0||^13.0
spatie/laravel-package-tools Version ^1.16
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package byrcsc/laravel-custom-fields contains the following files

Loading the files please wait ...