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:

  1. Boot — the field is instantiated (Field::make()), booted with the default configuration (boot() method on the field), and configured using calls in the fields() method (Text::make('id')->these()->calls())
  2. Resolution — the field is resolved for a resource. Some additional configuration (that depends on the Resource) can be done by listening to the resolved event.
  3. 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 for index and show
  • write for create and edit
  • 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 the type attribute
  • placeholder() to specify the placeholder attribute

Text

An input type="text". The most basic field type, appropriate for names, short one-line descriptions, etc.

Extends Input.

Text::make('title')

Email

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 the min attribute and the min validation rule
  • max() sets the max attribute and the max validation rule
  • step() sets the step 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 second Password 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.