Skip to content

The architecture of a model

A model declared in laraimport.json produces around 58 files. That sounds like a lot, but they follow a single pattern: each piece does one thing, and the logic you write has a fixed place. This page is the map; per-file detail lives in What is generated and where your code goes.

Examples use a Product model inside an application. In a package, app/ is src/ and App\ is the package namespace.

A request from start to finish

Editing a product from the admin panel:

PUT /api/app/product/update            name: api.app.product.update

  ├─ ProductController::update          auth:sanctum; no logic

  ├─ UpdateRequest
  │    authorize()  findOrFail(product_id) and can('update', $product)  → ProductPolicy
  │    rules()      product_id plus the JSON rules
  │    handle()     $product->updateModel($this)                        → ProductStorage
  │                   update() with $updatable only, plus metas if any
  │                 new ProductResource($product)                       → includes actions
  │                 event(new UpdateEvent(...))                          → listeners

  └─ JSON: the record and its actions, with no data wrapper
  • The controller only delegates. Each method calls its request's handle().
  • Every action has its own request. It authorizes against the policy, validates and executes.
  • The model is a facade. The methods that do the work live in its traits.
  • The Resource decides the response shape and the actions the interface offers for that row.
  • Side effects go in listeners, hooked to the action's event.

The twelve actions

Without a routes key, a model has all twelve. Each has its route, controller method, request, event, policy ability, test and, where it applies, a screen. Removing one with routes removes it from all of those at once.

ActionHTTP methodURL (in an application)Route namePolicy ability
policiesGETapi/app/product/policiesapi.app.product.policiesChecks all of them
policyGETapi/app/product/policyapi.app.product.policy
indexGETapi/app/product/indexapi.app.product.indexindex
showGETapi/app/product/showapi.app.product.showview
createPOSTapi/app/product/createapi.app.product.createcreate
updatePUTapi/app/product/updateapi.app.product.updateupdate
deleteDELETEapi/app/product/deleteapi.app.product.deletedelete
restorePOSTapi/app/product/restoreapi.app.product.restorerestore
forceDeleteDELETEapi/app/product/force-deleteapi.app.product.force.deleteforceDelete
exportPOSTapi/app/product/exportapi.app.product.exportexport
bulkUpdatePUTapi/app/product/bulk-updateapi.app.product.bulk.updateupdate, per record
bulkDeleteDELETEapi/app/product/bulk-deleteapi.app.product.bulk.deletedelete, per record

policies answers, for the current user and an optional record, which actions they may perform. It's what the table asks before showing a button.

Three switches change the model's shape without touching code:

  • routes with only or except chooses which actions exist. Without delete, restore and forceDelete, the model also drops soft deletes.
  • immutable: true removes update, delete and both bulk actions, and the model rejects Eloquent modifications.
  • secret: true on a column keeps it out of responses, exports and the table.

See Routes, immutables, secrets and users.

The layers of a model

Domain

FileWhat it is
app/Models/Product.phpThe facade. Its lists ($fillable, $creatable, $updatable, $export_cols, $loadable_relations, $loadable_counts) and casts() come from the JSON.
Traits/Relations/ProductRelations.phpRelations. Slot.
Traits/Operations/ProductOperations.phpBusiness logic. Slot, and the default place.
Traits/Storage/ProductStorage.phpCreate, update and delete, and meta saving. Slot for files.
Traits/Mutators/ProductMutators.phpAccessors and mutators. Slot.
Traits/Assignments/ProductAssignment.phpGenerated empty.
Filters/Product/ManagedFilter.phpWhich records of the index each user sees. Slot.
Filters/Product/{Id,Creation,Updated,EagerLoading}Filter.phpIndex filters, built on search-surge.
app/Policies/ProductPolicy.phpAuthorization per action. Slot. Starts closed.
app/Observers/ProductObserver.phpModel lifecycle. Slot.
app/Models/ProductMeta.phpOnly with metas: true.

HTTP

FileWhat it is
app/Http/Controllers/ProductController.phpDelegates to the requests.
app/Http/Requests/Product/*Request.phpOne per action. rules() is a slot, inside the array.
app/Http/Resources/Models/ProductResource.phpResponse shape and the actions array. Slot.
app/Http/Events/Product/Events/*One event per action.
app/Http/Events/Product/Listeners/*/*Side effects. Slot.
routes/api/models/product.phpThe routes. The file name is snake_case: order_line.php.

Export, data and tests

FileWhat it is
app/Exports/ProductsExports.phpThe Excel export.
app/Notifications/Product/ExportNotification.phpThe notification with the download link.
database/migrations/*_create_products_table.phpThe table, plus *_alter_products_table.php when columns change.
database/factories/ProductFactory.phpTest data that already inserts. Slot.
tests/Feature/Models/ProductEndpointsTest.phpExercises every endpoint. Slot.

Interface

Everything lives in resources/<ui>/src/models/product/, with the same structure in Vue and React:

FileWhat it is
index.jsThe model contract. Pure functions and HTTP calls: API_ROUTE_PREFIX, crudActions(), bulkActions(), dataTableHead(), dataTableSort(), the CRUD functions. It's the same file in Vue and React.
store/index.jsPinia in Vue, Zustand in React, with the same surface.
routes/index.jsThe routes AdminProducts, AdminCreateProduct, AdminShowProduct, AdminEditProduct.
forms/CreateForm, EditForm, FilterForm.
views/AdminView (the table), CreateView, ShowView, EditView.
widgets/DataTable, ModelCard, ModelProfile.

At module level: resources/<ui>/index.js (the entry point), src/routes.js, src/i18n.js, src/locales/{en,es}.json, src/theme.js and src/components/{ActionMenu,Breadcrumbs}. In a package, also package.json and vite.config.js.

Where your code goes

SlotWhat goes there
Traits/OperationsBusiness logic. With metas, also the shape of payload in buildPayload().
Traits/RelationsRelations the JSON doesn't declare.
Traits/StorageFile uploads and deletion.
Traits/MutatorsAccessors and mutators.
ManagedFilter::canViewWho sees what. Without it, the index returns everything.
PolicyAuthorization per action. Starts closed: only the administrator passes, and not even they can force delete until you take forceDelete out of $exceptAbilities.
Requests/*/rules()Rules not coming from the JSON, inside the array.
ResourceThe exact response shape and its actions array.
Listeners and observerSide effects and lifecycle.
FactoryRealistic test data.
tests/FeatureBehavior.

The model is a facade, not a place to pile logic. A public method in Operations orchestrates, and the real work lives in whichever class it belongs to.

If some logic fits in none of these slots, something is missing from laraimport.json.

What you don't touch

  • The controller and the routes file. If an endpoint is missing, it's missing from the JSON or the generator.
  • The model's lists ($fillable, $creatable, $updatable, $export_cols, $loadable_relations, $loadable_counts, $editable_metas, $protected_metas) and casts(): they come from the JSON.
  • Anything outside the //RULES//, //IMPORTS// and //EDIT// markers in files that have them.
  • models/<entity>/index.js in only one of the two frameworks: it would split them.

Writing in these places is exactly the drift larapack:verify reports.

Regenerating without destroying

.larapack/manifest.json records every generated file, the template it came from and its content hash. With that:

  • larapack:import --force regenerates files whose hash hasn't changed and keeps the ones you edited, with a warning.
  • A file the manifest doesn't know is left alone: the generator doesn't touch what it didn't write.
  • The Relations, Storage and Operations traits are only created when missing, and generated tests are never overwritten, not even with --force.
  • A column change on an existing table produces a new alter migration, not a rewrite of the create migration.

larapack:verify reads the same manifest and reports missing files, edited files, undeclared routes, writes to an immutable model and exposed secrets. See Verify and audit.

Next: The front ↔ back contract.