Skip to main content

Overview

An Object Schema is a collection of Properties attached to a model. It is the single source of truth for what a model’s data looks like — its labels, types, validation rules, relation structure, and how values are formatted. Schema support is built into HasDomainObject. Any model implementing DomainObjectContract can define a schema() method, and the schema drives everything else: validation in actions, columns in tables, label formatting in activity logs, and inline edit in the frontend.

Defining a Schema

Implement the protected schema() method on your domain object — similar to how Laravel’s casts() works:
app/Models/Order.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Inly\Core\Contracts\DomainObjectContract;
use Inly\Core\Models\Traits\HasDomainObject;
use Inly\Core\Properties\SchemaDefinition;
use Inly\Core\Properties\Types\StringProperty;
use Inly\Core\Properties\Types\MoneyProperty;
use Inly\Core\Properties\Types\EnumProperty;
use Inly\Core\Properties\Types\ObjectProperty;
use Inly\Core\Properties\Types\DateProperty;

class Order extends Model implements DomainObjectContract
{
    use HasDomainObject;

    protected static function schema(): SchemaDefinition
    {
        return SchemaDefinition::make([
            StringProperty::make('order_number', __('Order Number'))
                ->required(),

            EnumProperty::make('status', OrderStatus::class)
                ->label(__('Status'))
                ->required(),

            MoneyProperty::make('total_amount', __('Total Amount'))
                ->sumOf('orderRows', 'amount'),

            ObjectProperty::make('customer', __('Customer'))
                ->sortBy('customer.name'),

            StringProperty::make('customer_name', __('Customer Name'))
                ->from('customer.name'),

            DateProperty::make('ordered_at', __('Ordered At'))
                ->required(),

            StringProperty::make('sku', __('SKU'))
                ->required(),
        ]);
    }
}
SchemaDefinition::make() accepts an array of property instances. The schema is cached via once() per model class.
Define properties in the order you want them to appear by default in tables and forms — adapters use this ordering.

Accessing the Schema

Static Access (No Values)

Use schemaDefinition() when you need the structure without a model instance — for validation rules, column definitions, and TypeScript generation:
Order::schemaDefinition()->get('status');         // Property instance (no value)
Order::schemaDefinition()->rules();               // all writable validation rules
Order::schemaDefinition()->has('order_number');   // true

Instance Access (With Values)

Use $model->properties() to get a SchemaPropertySet — all properties filled with the model’s current values:
$order->properties()->get('total_amount')->toFormatted();  // '$1,234.56'
$order->properties()->toFormatted();                       // ['total_amount' => '$1,234.56', ...]
$order->properties()->toData();                            // ['total_amount' => [...], ...]
The shorthand $order->property('name') returns a single bound Property:
$order->property('status')->toData();     // {value: 'pending', label: 'Pending', color: 'yellow'}
$order->property('customer')->toData();   // ObjectData shape

Validation

SchemaDefinition::rules() returns validation rules for all writable properties. Computed properties, from() traversals, and aggregates are automatically excluded.
app/Http/Controllers/Actions/UpdateOrderAction.php
<?php

namespace App\Http\Controllers\Actions;

use App\Models\Order;
use Inly\Core\Actions\CoreAction;

class UpdateOrderAction extends CoreAction
{
    protected function rules(): array
    {
        return Order::schemaDefinition()
            ->only(['order_number', 'status', 'ordered_at'])
            ->rules();
    }
}

Extending Rules

Use mergeRules() to add extra rules on top of the schema defaults:
protected function rules(): array
{
    return Order::schemaDefinition()
        ->only(['total_amount', 'status'])
        ->mergeRules([
            'total_amount' => ['max:1000000'],
            'status' => [Rule::notIn([OrderStatus::Cancelled])],
        ])
        ->rules();
}
mergeRules() safely merges rules — it does not overwrite existing rules on a field, it appends them.

Excluding Properties

Order::schemaDefinition()->only(['status', 'ordered_at'])->rules();
Order::schemaDefinition()->except(['customer_name', 'total_amount'])->rules();
only() and except() return new SchemaDefinition instances — the cached original is never mutated.

Query Scoping

applyToQuery() inspects all properties and adds the necessary with(), withSum(), withCount(), etc. calls in one step:
// Instead of manually writing with() and withSum() calls:
Order::schemaDefinition()->applyToQuery(Order::query())->paginate();
You can scope to a subset of properties:
Order::schemaDefinition()
    ->applyToQuery(Order::query(), only: ['customer', 'total_amount', 'customer_name'])
    ->paginate();
This handles:
  • ObjectProperty → adds relation to with()
  • from('customer.name') → adds customer to with()
  • sumOf('orderRows', 'amount') → adds withSum('orderRows', 'amount')
  • needsRelations(['orderRows']) → adds orderRows to with()

Tables

Schema-Bound Columns

Use PropertyColumn::fromSchema() to create a column from a schema property. The column automatically:
  • Uses the property’s label
  • Renders with <PropertyValue> in the cell
  • Marks direct-column, aggregate, and from() traversal properties as sortable
  • Sorts ObjectProperty columns when ->sortBy() is declared on the property
  • Maps toFormatted() to the export value
app/Tables/OrderTable.php
<?php

namespace App\Tables;

use App\Models\Order;
use Illuminate\Database\Eloquent\Builder;
use Inly\Core\Tables\CoreTable;
use Inly\Core\Properties\Adapters\InertiaTable\PropertyColumn;
use Inly\Core\Properties\Adapters\InertiaTable\PropertyFilter;
use InertiaUI\Table\Columns\ActionColumn;

class OrderTable extends CoreTable
{
    public function resource(): Builder|string
    {
        return Order::schemaDefinition()->applyToQuery(Order::query());
    }

    public function columns(): array
    {
        return [
            PropertyColumn::fromSchema(Order::class, 'order_number'),
            PropertyColumn::fromSchema(Order::class, 'customer_name'),
            PropertyColumn::fromSchema(Order::class, 'status'),
            PropertyColumn::fromSchema(Order::class, 'total_amount'),
            ActionColumn::new(),
        ];
    }

    public function filters(): array
    {
        return [
            PropertyFilter::fromSchema(Order::class, 'status'),
            PropertyFilter::fromSchema(Order::class, 'ordered_at'),
        ];
    }
}

Inline Edit in Tables

Add ->editable() to a column to enable inline editing. The cell renders <PropertyValue> with an action prop, which opens an edit popover using the property’s input component on click:
PropertyColumn::fromSchema(Order::class, 'status')
    ->editable(
        actionClass: UpdateOrderStatusAction::class,
        params: fn ($record) => ['id' => $record->id],
    ),
The paramName defaults to the property’s name (status). Override it if the action expects a different parameter:
PropertyColumn::fromSchema(Order::class, 'status')
    ->editable(
        actionClass: UpdateOrderStatusAction::class,
        params: fn ($record) => ['id' => $record->id],
        paramName: 'order_status',
    ),

Standalone Columns

You can also use PropertyColumn::make() with a property instance when you don’t have a schema — useful for ad-hoc columns:
use Inly\Core\Properties\Types\StringProperty;

PropertyColumn::make(StringProperty::make('notes', __('Notes'))),
Standalone columns do not auto-derive sortability or export formatting. Chain these manually as needed.

Filters

PropertyFilter::fromSchema() picks the right filter type based on the property’s semantic metadata:
  • Properties with options()CoreSetFilter (checkbox list)
  • boolean → toggle filter
  • date → date range filter
  • number / money → numeric range filter
  • Everything else → text filter
PropertyFilter::fromSchema(Order::class, 'status'),    // → CoreSetFilter with OrderStatus options
PropertyFilter::fromSchema(Order::class, 'ordered_at'), // → CoreDateFilter

Activity Log

When HasDomainObject formats activity log entries, it checks whether the model defines a schema(). If it does, it uses the schema for labels and formatted values automatically — no extra configuration needed.
// In HasDomainObject — happens automatically
public function formatActivityLogAttributeLabel(string $key): ?string
{
    if (static::schemaDefinition()->has($key)) {
        return static::schemaDefinition()->get($key)->label();
    }
    return null;
}
For example, a change to status on an Order will show “Status” (from the property label) and the formatted enum label (from toFormatted()), rather than the raw attribute name and value.
See: Activity Log (coming soon) for full documentation on the activity log system.

Frontend Usage

Passing Schema Data to Inertia

For standard show pages, prefer passing the full object via toObjectData(). That gives the frontend both the object’s identity data and its schema-backed properties in one payload:
app/Http/Controllers/OrderController.php
public function show(Order $order)
{
    return inertia('orders/order-show', [
        'order' => $order->toObjectData(),
    ]);
}
toObjectData() includes schema by default. If the page should expose only a subset of fields, use schemaOnly:
return inertia('orders/order-show', [
    'order' => $order->toObjectData(schemaOnly: [
        'order_number',
        'customer',
        'status',
        'total_amount',
    ]),
]);

Rendering on a Detail Page

Default detail pages should render the schema directly with <PropertyDetailCard>:
resources/js/pages/orders/order-show.tsx
import { PropertyDetailCard } from '@/core/components/property-detail-card';
import { ObjectWithSchema } from '@/core/properties/types';

interface Props {
  order: ObjectWithSchema<App.Models.OrderSchema>;
}

export default function OrderShow({ order }: Props) {
  return (
    <PropertyDetailCard properties={Object.values(order.schema)} />
  );
}
PropertyDetailCard filters hidden properties automatically, and Object.values(order.schema) preserves the backend schema order. For custom layouts, you can still render individual properties manually:
import { PropertyValue } from '@/core/properties/property-value';

<PropertyValue data={order.schema.status} />
<PropertyValue data={order.schema.total_amount} />

Inline Edit on a Detail Page

Pass an action to make a field inline-editable:
<PropertyValue data={order.schema.status} action={updateStatusAction} />
Clicking the rendered value opens an edit popover pre-filled with the current value, using the EnumInput component automatically.

Form with Schema Fields

Use <PropertyCoreFormInput> inside a CoreForm. It reads name, label, and input type from the property data and binds to the form context automatically:
resources/js/pages/orders/order-edit.tsx
import { useForm } from '@inertiajs/react';
import { CoreForm, CoreFormSubmitButton } from '@/core/components/core-form';
import { PropertyCoreFormInput } from '@/core/properties/property-core-form-input';
import { ObjectWithSchema } from '@/core/properties/types';

interface Props {
  order: ObjectWithSchema<App.Models.OrderSchema>;
}

export default function OrderEdit({ order }: Props) {
  const form = useForm({
    status: order.schema.status.value,
    ordered_at: order.schema.ordered_at.value,
  });

  return (
    <CoreForm
      form={form}
      onSubmit={() => form.put(route('orders.update', order.id))}
      footer={<CoreFormSubmitButton variant="brand">{__('Save')}</CoreFormSubmitButton>}
    >
      <PropertyCoreFormInput data={order.schema.status} />
      <PropertyCoreFormInput data={order.schema.ordered_at} />
    </CoreForm>
  );
}

Reference

SchemaDefinition

MethodDescription
make(array $properties): staticStatic factory. Accepts an array of Property instances.
get(string $name): PropertyGet a property by name.
has(string $name): boolCheck if a property exists.
all(): arrayAll properties as an array.
only(array $names): staticReturns a new schema with only the specified properties.
except(array $names): staticReturns a new schema excluding the specified properties.
writable(): arrayWritable properties only (excludes from(), computed, aggregates).
rules(?array $only = null): arrayValidation rules for all writable properties.
rulesFor(array $names): arrayValidation rules for specific properties.
mergeRules(array $extra): staticFluent. Appends extra rules onto the existing rule set.
fill(Model $model): SchemaPropertySetFills all properties from a model instance.
applyToQuery(Builder $query, ?array $only = null): BuilderApplies eager loads and aggregates to a query.
eagerLoads(?array $only = null): arrayRelations to eager load from this schema.
aggregates(?array $only = null): arrayAggregate definitions from this schema.
propertiesWithOptions(): arrayProperties that have declared options().

SchemaPropertySet

MethodDescription
get(string $name): PropertyReturns a cloned property with its value resolved from the model.
all(): arrayAll bound properties.
toFormatted(): array['name' => property->toFormatted(), ...] for all properties.
toData(): array['name' => property->toData(), ...] for all properties.

Schema Methods on HasDomainObject

MethodDescription
schema(): SchemaDefinition (protected static)Define the schema. Implement this in your model.
schemaDefinition(): SchemaDefinition (static)Public cached accessor. Calls schema()->forModel(static::class).
properties(): SchemaPropertySetInstance accessor. Returns the schema filled with this model’s values.
property(string $name): PropertyShorthand for $this->properties()->get($name).

PropertyColumn

MethodDescription
make(Property $property): staticStandalone column from a property instance.
fromSchema(string $schemaClass, string $propertyName): staticSchema-bound column. Auto-derives sortability and export format.
sortBy(string $column): staticEnable sorting by a specific column. Accepts a plain column name or a dot-notation path for a relationship join (e.g. 'customer.name'). Overrides any sortBy declared on the property itself.
editable(string $actionClass, ?Closure $params, ?string $paramName = null): staticEnable inline edit via <PropertyValue> action props.

PropertyFilter

MethodDescription
make(Property $property): mixedStandalone filter from a property instance.
fromSchema(string $schemaClass, string $propertyName): mixedSchema-bound filter. Auto-selects filter type from property.