Fields
Fields used for managing the data of your resources. For example, you may have a Text
field for the resource's name
and a Textarea
field for the resource's description
.
Usage
Fields are defined in the fields()
method of a resource.
public static function fields(): array
{
return [
ID::make('id'),
Text::make('title')->rules('max:100'),
Trix::make('description')->display('show', 'write'),
Pikaday::make('created_at')
->placeholder('DD.MM.YYYY')
->jsFormat('DD.MM.YYYY')
->phpFormat('d.m.Y')
->display('show');
];
}
Lifecycle
Fields have three important parts of their lifecycle:
- Boot — the field is instantiated (
Field::make()
), booted with the default configuration (boot()
method on the field), and configured using calls in thefields()
method (Text::make('id')->these()->calls()
) - Resolution — the field is resolved for a resource. Some additional configuration (that depends on the
Resource
) can be done by listening to theresolved
event. - Hydration — the field is hydrated with the action and value. This is when a field knows exactly what action is displaying it, and what value it has.
That likely sounds confusing, but as you read this page further it will make sense.
Available methods
General methods
make()
Instantiate a field. The first argument is the column name.
Text::make('name'),
extend()
Make additional changes to the field object in the resource class. This is just syntactic sugar so that you don't have to instantiate the object, then do some checks, and then call methods on the object. Makes everything fluent.
Typically extend()
should be at the end of the method chain.
Text::make('key')
->rules('max:250')
->display('show', 'edit')
->extend(function (Text $field) {
if ($something) {
$field->optional();
}
}),
default()
The default value that will be used when the field has no value (either on create
or when it's being saved with an empty value).
Boolean::make('enabled')->default(true),
display()
What actions should the field be displayed on. For example, you may want to display a certain field only on the index
action.
You can either specify actions one by one (index
/create
/show
/edit
), or you can use:
-
read
forindex
andshow
-
write
forcreate
andedit
-
default
for all fields
Password::make('password')
->display(['default' => true, 'index' => false]),
Password::make('password')
->display(['read' => false, 'write' => true]),
Password::make('password')
->display('create', 'show', 'edit'),
Password::make('password')
->display('write', 'show'),
help()
The help text displayed below the field.
Image::make('photo')->help("The product's photo."),
label()
The text shown in the field's label.
Text::make('email')->label(__('Customer email')),
attributes()
HTML attributes that should be passed to the field's main element.
Textarea::make('description')->attributes(['rows' => 3]);
stored()
Should the field be stored.
Password::make('password')
->stored(fn (Password $field, $value) => (bool) $value),
Password::make('password_confirmation')
->stored(false),
enabled()
/ disabled()
Should the field be editable.
Pikaday::make('created_at')
->enabled(false),
Pikaday::make('created_at')
->disabled(),
renderAs()
An override for the field's view. Can be action-specific.
For example, here we override the HTML displayed by the Text
field. If it has a category, it's not considered a draft and it's live on the site. So we add a badge to the text:
Text::make('title')->renderAs(function (Text $field, ?string $value) {
if (! $field->form['category']->value) {
$color = 'blue';
$text = 'Draft';
} else {
$color = 'green';
$text = 'Live';
}
return <<<HTML
{$value}
<span class="inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-{$color}-100 text-{$color}-800">
{$text}
</span>
HTML;
}, 'index'),
This is of course not very pretty, so it shouldn't be used in most cases. But it's good to know that the feature exists and can be used when the situation requires it.
The example above is used on this exact site (yes, you're looking at it). The only modification is that the Closure is extracted into a variable, and we show the badge as a separate field on show/edit actions, and add it to the page title
on index to save screen space. It makes the admin panel feel more tailor-made than just values in columns.
Validation methods
These methods let you specify logic related to validation. Some specific fields have additional methods for configuring validation. Those listed here are available on all fields.
rules()
Specify validation rules.
Text::make('title')->rules('max:250'),
Text::make('title')->rules('max:250', 'unique'),
When you use the syntax above (rules('rule1', 'rule2'))
, the rules will be appended to the field's default rules. For most fields the default rules are ['required']
.
If you'd like to override the rules completely, wrap your rules in an array:
-Text::make('title')->rules('max:250', 'unique'),
+Text::make('title')->rules(['max:250', 'unique']),
Though note that if you just want to make a field nullable, you should use ->optional()
.
createRules()
Create rules default to the base rules()
, but they may be specified separately:
Text::make('title')
->rules('max:250')
->createRules('unique')
Create rules are by default merged with base rules (for the create action). If you'd like to define create rules that don't include base rules, again wrap your rules in an array:
-->createRules('unique')
+->createRules(['unique'])
updateRules()
These are the same as createRules()
, but for update actions. The syntax is identical.
optional()
Removes required
and adds nullable
to rules.
required()
Makes a field required. Usually this is the default behavior already, but some fields can be optional by default so you may make them required manually.
// Make a password always required
Password::make('password')
->required(),
Value callbacks
You can customize how values are being resolved (database -> Lean
) and stored (Lean -> database
).
resolveValueUsing()
Specify how the value in the database should be transformed.
use App\Models\Order;
Text::make('total')
->resolveValueUsing(fn (Text $text, Order $order) => '$' . $order->total)
->stored(false),
The return value of this callback will become the field's $value
property.
storeValueUsing()
Specify how should a value from Lean be transformed before it's stored in the database.
Number::make('price')
->resolveValueUsing(fn (Number $field, int $value) => (float) $value / 100)
->storeValueUsing(fn (Number $field, float $value) => round($value * 100))
This example would display human readable prices and would convert prices from the browser to cents before storing them in the database.
Event methods
Fields come with a simple event system that lets you execute any code when a field hits a certain point in its lifecycle. Listeners (hooks) can make changes to the field.
Available events:
-
resolved
-
hydrated
hook()
Register an event listener.
$textField->hook('hydrated', function (Text $field) {
if ($field->action->is('edit')) {
$field->required();
}
});
on()
Alias for hook()
.
resolved()
Alias for hook('resolved', ...)
. Accepts just the callback.
hydrated()
Alias for hook('hydrated', ...)
. Accepts just the callback.
Available fields
ID
The model's integer id. This matches the usual primary key that Laravel creates in migrations.
ID::make('id'),
By default, this field is disabled. Which means that it can't be edited.
If you wish to allow editing, Lean will automatically add a unique
rule to verify that no other record in the table uses the same id.
Input
An input with no type. By default the type is text (so this field has identical behavior to Text
), but it's more semantically correct for situations where your field is an input but not necessarily a text.
All Input
fields have the following methods:
-
type()
to specify thetype
attribute -
placeholder()
to specify theplaceholder
attribute
Text
An input type="text"
. The most basic field type, appropriate for names, short one-line descriptions, etc.
Extends Input
.
Text::make('title')
An input type="email"
, validated both in the browser and on the server to be an email.
Extends Input
.
Email::make('email'),
Number
An input type="number"
, validated both in the browser and on the server to be a number.
Extends Input
.
Available methods:
-
min()
sets themin
attribute and themin
validation rule -
max()
sets themax
attribute and themax
validation rule -
step()
sets thestep
attribute
Number::make('price')
->min(1)
->max(1000)
->step(0.01)
Password
An input type="password"
. Optional by default (it doesn't get saved if no value was entered), can be manually ->required()
.
Extends Input
.
Available methods:
-
confirmed()
. Sets a rule that requires confirmation and creates a secondPassword
field for the confirmation value.
Password::make('password'),
Password::make('password')->confirmed(),
// confirmed() is identical to:
Password::make('password'),
Password::make('password_confirmation')->stored(false),
Boolean
A checkbox. On read actions this is shown as a green check (true
) or a red cross (false
).
Boolean::make('enabled')
->default(true),
Textarea
A <textarea>
element.
By default, the value is collapsed on read actions. To make a textarea field expanded by default, use the ->expanded()
method.
Available methods:
-
expanded()
sets whether the textarea should be expanded or collapsed by default -
rows()
sets how many rows are displayed by default (rows
HTML attribute)
Textarea::make('description')
->expanded()
->rows(8)
Trix
Trix is a simple WYSIWYG editor. In other words, it's Textarea
, but with formatting.
Extends Textarea
.
Available methods:
-
expanded()
from Textarea -
rows()
from Textarea
Pikaday
Pikaday is a simple date selector. It can be a good choice for timestamps.
Note however that Pikaday is not a datetime picker, hence the bold text. If you need to store time too, a different library might be more appropriate.
Right now we don't ship with any datetime pickers by default, but this will likely change very soon.
Extends Input
.
Available methods:
-
phpFormat()
sets the format that should be used on PHP side (validation and storing) -
jsFormat()
sets the format that should be used in the browser -
options()
lets you specify Pikaday configuration keys. They will be directly passed to the Pikaday object in JavaScript.
Pikaday::make('created_at')
->enabled(false)
->display('show', 'edit')
->placeholder('DD.MM.YYYY')
->jsFormat('DD.MM.YYYY')
->phpFormat('d.m.Y')
->default(now()),
Select
A <select>
element. Lets you specify values and validates that the selected value is one of the available values before saving the data.
Available methods:
-
options()
sets the available options -
placeholder()
sets the non-value option — what should be the displayed text, what value should represent it, and if it should be active
Select::make('status')
->options([
'received',
'shipped',
'delivered',
])
->placeholder('-- Select a value --', true, null)
->optional()
If the second argument of placeholder()
($enabled)
is true, you should also make the field optional()
— to allow selecting placeholder values.
Options can be specified using an indexed array, an associative array (key-value pairs), and a callback that returns one of those arrays.
// Same values are displayed and stored
->options(['received', 'shipped', 'delivered'])
// 'Order has been shipped' is displayed, 'shipped' is stored
->options([
'received' => 'Order has been received',
'shipped' => 'Order has been shipped',
'delivered' => 'Order has been delivered',
])
// 'Order has been shipped' is displayed, 'shipped' is stored
->options(function (Select $field) {
return [
'received' => 'Order has been received',
'shipped' => 'Order has been shipped',
'delivered' => 'Order has been delivered',
];
});
Files
To upload files, you may use the File
and Image
fields. They provide an abstraction layer around handling file uploads, but are still completely customizable like all the other fields.
Technical note: A File
field has a value when a string is stored in the database, and it's empty when null
is stored. Therefore, avoid storing ambiguous things like empty strings.
File
The File
field provides three methods. One for storing the file, one for deleting the file, and one for resolving the display name.
File::make('attachment')
// The storeValueUsing() callback accepts the File field instance, and
// a TemporaryUploadedFile instance that you can store in any way.
->storeValueUsing(function (File $field, TemporaryUploadedFile $file) {
// Make sure to return the result of the call, so that the path is stored.
return $file->storePubliclyAs('files', $field->encodeName($file), ['disk' => 'public']));
})
// This callback accepts the File field instance and the stored path.
->deleteFileUsing(fn (File $image, string $name) => Storage::disk('public')->delete($name)),
// This callback also accepts the File field instance and the stored path.
// This value will be displayed as the Current: xxx text on Create and Edit actions.
->resolveDisplayValueUsing(fn (File $field, string $path) => $field->decodeName($path))
You may also use the encodeName()
and decodeName()
helper methods to conveniently encode files in a way such that they're not enumerable (attackers can't guess the names and make requests), but can be decoded into the original name. The decoded name is displayed on the Create and Edit pages as the current file name.
Image
The Image
field has a few extra features on top of all of the File
features (all of Field
methods are inherited, and interacting with the files works the exact same).
Dimensions
You may set image dimensions, they will be used on all pages (except index) for displaying the stored image, or the preview of the uploaded image.
Image::make('image')
->width('w-16') // string = Tailwind class
->width(300) // integer = pixels
->height('h-8') // same as width()
->dimensions('w-16', 'h-10') // width and height in a single call
Default image
You may specify the default image that should be displayed when no value is set. A use case for this would be a "no image" graphic.
Image::make('image')
->default('/no-image.png');
Thumbnail
You may serve a different image as the thumbnail, which will be displayed on the Index action.
For example, let's imagine that we're the thumbnail path in the thumbnail
column of the table.
Image::make('image')
->resolveThumbnailUsing(function (Image $image) {
return $image->model->thumbnail;
});
You may also specify the "default thumbnail", i.e. the small scale version of the default image. It will be displayed on the Index page when no value is available.
Image::make('image')
->defaultThumbnail('/no-image-small.png');
Storing & deleting files:
The Image
field comes with the same encodeName()
and decodeName()
methods as its parent class — Field
. However, you shouldn't need these when uploading images. It's okay to upload images to completely random file names, because you (likely) will not be displaying the original file name.
->storeFileUsing(fn (Image $field, TemporaryUploadedFile $file) => $file->storePublicly('images', ['disk' => 'public']))
->deleteFileUsing(fn (Image $image, string $name) => Storage::disk('public')->delete($name)),
Relations
Lean currently supports HasMany
and BelongsTo
relations. In the near future, this might expand (and likely will a bit), but there are no definitive plans to support all relations.
The issue with relations is that they easily get complex and confusing. HasMany
is probably at the upper limit of how advanced can a relation be without becoming confusing.
So, what should be used instead?
[Custom fields](https://lean-admin.dev/docs/custom-fields.
Especially if you have more complex screens. For example, showing Order belongsToMany Product
is a really bad idea in 99% of cases. The user just wants to add products to an order, not deal with attaching vs creating, pivot tables, etc. That's already complex enough for developers.
Plus, it's often much easier to actually build a custom component for this, than trying to get a complex relation to work the way you want.
So, again, we may (and likely will) support more relations in the future, but it's not the main focus because we recommend avoiding them even if you there are available.
HasMany
Lets you define a HasMany
relation. On show
and edit
, the relation will be displayed as an embedded index of that resource, with results scoped to the correct parent.
In other words, it will be as if you visited the index
action for the child resource, except it will be embedded in a form with the other fields on the parent resource, and results will be scoped only those which belong to the currently displayed parent.
Available methods:
-
of()
sets the child resource (sounds like "has many of") -
resultsPerPage()
sets how many results should be shown on each page -
fields()
sets what fields should be used
The value passed to make()
should be the name of the relation method.
HasMany::make('tags')->of(TagResource::class)
BelongsTo
BelongsTo
// We're inside TagResource
BelongsTo::make('product')->parent(ProductResource::class);
// product = product() relation on Tag model
Accessing the form
Each field has a form
property. This property is a FieldCollection
that contains all fields that are displayed in the same "form" as the current field.
$field->form['category'] // Lean\Fields\Relations\BelongsTo instance
$field->form['category']->value // '4'
This can be useful for customizing behavior based on what other fields (and values) are shown on the same page.
However keep in mind that this data can potentially be manipulated.
$field->form['category']->value
would be stored in the fieldMetadata.category.value
property on the action's Livewire component. Which can be changed by users on the front-end.
For this reason, this data should only be used for harmless features like UX improvements. It should not be the source of truth for important data.
In some parts of the field lifecycle, notably when it's being saved, you can access the $model
property. When in doubt, re-fetch the data from the database.