MoonShine 5.0
Safer defaults. Faster rendering. A cleaner API.
MoonShine 5 brings a focused upgrade to everyday admin development: escaped display text by default, centralized field settings, less rendering work on the server and in the browser, and a consistent home for list-page behavior.
This is a major release. Applications upgrading from 4.x should review the migration notes below, especially custom HTML labels, import/export handlers, custom fields, and extensions that implement MoonShine contracts.
✨ Highlights
- Display text is escaped by default. Labels, hints, prefixes, suffixes, and string render decorations share safe defaults, with explicit opt-ins for trusted HTML. Field value escaping and escaping on save have their own independent settings.
- Configure uploads once. A global
allowed_extensionslist supplies defaults for File and Image fields, while individual fields retain control. - List behavior belongs to the index page. Filters, query tags, and handlers now live on
IndexPage, completing the removal of the deprecated resource APIs. - Less rendering work. Shared request wrappers, reused resource/page registries, flatter component views, and a dedicated vertical-field view reduce repeated server work. Table measurements, lazy dropdown positioning, and async response insertion reduce browser layout and parsing work.
- More reliable async interactions. The latest list request wins; forms recover after network failures; repeated button clicks do not duplicate an in-flight request; toast timers cannot remove another notification.
- Stronger extension tooling. PHPStan runs at maximum level across the monorepo and all packages, with improved resource/page generics, IDE navigation, enum handling, and mutation coverage.
Requirements
| Component | MoonShine 5 |
|---|---|
| PHP | 8.4 or newer |
| Laravel | 11, 12, or 13; Laravel 10 support is removed |
| MoonShine split packages | 5.x |
| Import/export integration | moonshine/import-export:^3.0
|
| Building UI assets from source | Node.js 22.12 or newer |
Composer and JavaScript dependencies have been refreshed while preserving the version ranges needed for Laravel 11–13. The shipped UI includes Alpine 3.17.5, Axios 1.21.1, TomSelect 2.7.0, Tailwind CSS 4.3.5, and Vite 8.3.8. Using the distributed assets does not require building them yourself.
⚠️ Breaking changes and migration
1. Move filters, query tags, and handlers to IndexPage
The resource-level HasFilters, HasHandlers, and HasQueryTags concerns have been removed. Move filters(), queryTags(), and handlers() overrides from your resource to its index page. Access these APIs through the page, for example $resource->getIndexPage()->getFilters().
ModelResource::getIgnoredFields() is also removed: customize ignored filter fields on the index page instead. Query-state parameters and getFilterParams() remain available on the resource.
For import-export 3.0, keep ImportExportConcern and HasImportExportContract on the resource, and add ImportExportHandlersConcern to its index page:
use MoonShine\ImportExport\Contracts\HasImportExportContract;
use MoonShine\ImportExport\Traits\ImportExportConcern;
use MoonShine\ImportExport\Traits\ImportExportHandlersConcern;
use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\Laravel\Resources\ModelResource;
class PostResource extends ModelResource implements HasImportExportContract
{
use ImportExportConcern;
// Keep import/export fields and events on the resource.
}
class PostIndexPage extends IndexPage
{
use ImportExportHandlersConcern;
// Configure import/export handlers on the index page.
}Register PostIndexPage::class in the resource's pages() method alongside its form and detail pages. Move export(), import(), isExportToCsv(), and handlers() overrides to the index page. Use $this->getResource() when those overrides need resource data.
2. Replace removed compatibility classes
| Removed API | Replacement |
|---|---|
MoonShine\Laravel\Handlers\Handler
| MoonShine\Crud\Handlers\Handler
|
MoonShine\Laravel\Handlers\Handlers
| MoonShine\Crud\Handlers\Handlers
|
MoonShine\Laravel\Notifications\NotificationButton
| MoonShine\Crud\Notifications\NotificationButton
|
MoonShine\Laravel\Http\Responses\MoonShineJsonResponse
| MoonShine\Crud\JsonResponse
|
MoonShine\Laravel\Http\Requests\Relations\AsyncSearchRequest
| MoonShine\Laravel\Http\Requests\Relations\RelationModelFieldRequest
|
MoonShine\Laravel\MoonShineUI::toast()
| The toast() helper
|
3. Opt in explicitly to trusted HTML
HTML supplied as a plain string in labels, hints, prefixes/suffixes, and string beforeRender() / afterRender() output is now escaped. Use the corresponding local opt-in when the markup is trusted:
Text::make('<strong>Name</strong>', 'name')
->unescapeLabel()
->hint('<a href="/help">Help</a>')
->unescapeHint();The matching methods include unescapeLabel(), unescapeHint(), unescapePrefix(), unescapeSuffix(), unescapeBeforeRender(), and unescapeAfterRender(). Explicit titles, field link names, and related record labels have their own unescapeTitle(), unescapeLinkName(), and unescapeOptionLabels() controls. Render decorations can also return an Htmlable value for prepared HTML.
Global defaults live under escapes in config/moonshine.php: label, hint, prefix, suffix, before_render, after_render, value, and value_on_apply. Prefer local opt-ins for individual pieces of trusted markup.
Preview escaping and escaping on save are independent. In 5.x, unescape() controls the displayed value; it no longer implicitly disables escaping when saving. To preserve submitted markup for a trusted HTML field, configure both:
Textarea::make('Body', 'body')
->unescape()
->escapeOnApply(static fn (): bool => false);Raw getters and serialized labels retain their original text. Custom Blade overrides should use the prepared display values, such as labelHtml, hintHtml, and titleHtml, at the HTML boundary. See the UI escaping and extension guide.
4. Review custom fields, contracts, and view overrides
Applications using the supplied implementations inherit these changes. Authors of custom implementations and subclasses should check the updated signatures and property types:
FileableContract::allowedExtensions()and field overrides now acceptarray|string. The inherited$allowedExtensionsproperty is?array, withnullmeaning “inherit the global setting.” An explicit empty list or'*'permits all extensions.- The default
allowed_extensions = []keeps uploads unrestricted. Set an explicit global or per-field allowlist if restrictions are required. Effectiveacceptdefaults are prepared at render/serialization time; reading the attribute bag earlier may returnnullunless an explicit value was set. WithEscapedValue::$unescapeis now?bool;FormElement::$datais?DataWrapperContract. Match these types if a subclass redeclares the properties.- Custom
TranslatorContractimplementations needgetString(). CustomHasResourceContractimplementations needgetResourceOrFail().RequestContract::getScalar()now returnsstring|int|float|bool|null. - Component and field-collection contracts explicitly declare
when()/unless(). Review custom overrides against the updated native signatures and generic annotations.ModelResourcegeneric parameters are model, index page, form page, and detail page. - Arrays and objects without string conversion are rejected with
TypeErrorby the string conversion boundary. Use scalars,Stringablevalues, or supported enums for display text. - Default vertical table rows now render
moonshine::components.table.vertical-field. Overrides of the previously nested Grid/Column/Div views no longer customize those rows; override the dedicated view instead. Rows customized with title/value closures retain the component path.
The new configurable escaping and upload defaults use optional configurator contracts; they do not add methods to the base ConfiguratorContract.
5. Review custom sorting, parent links, and async clients
- Sorting: a requested sort column must be the default column or a visible sortable index field. Unsupported or hidden columns fall back to the default column and direction. Relation tables use the related resource's index fields. Review hand-built
?sort=links, saved sort state, JSON clients, and relation-only sort fields; customizeprepareOrder()when a wider set is intentional. - Parent relations: untyped inverse relations used by
relatedLink()must be declared in the child resource fields or given a relation return type. Unknown or rejected parent relations return 404. - Async CSRF protection: custom clients must satisfy the configured Laravel request-forgery protection for GET, HEAD, and OPTIONS too. Supply the CSRF token/header, including for custom GET/download links where required. Configured middleware exclusions and Laravel 13 origin verification remain in effect. Your custom operation still needs its own authorization.
- Mass deletion: the default path respects model global scopes and the resource item query, requires the MASS_DELETE action/ability, and authorizes DELETE for every matched item before deleting any. Deletion hooks receive only the keys actually matched and deleted. Review hooks that previously expected raw request IDs.
- Links: disallowed URL schemes now render with
href="#". HTTP(S), mailto, tel, relative URLs, and fragments remain supported; review any custom-scheme links.
🛠️ Fixes and refinements
- Preserve Select labels, option metadata, and selected state after form resets; escape option labels and values exactly once, including grouped and relationship options.
- Restore native
requiredvalidation after changing a MorphTo type, with regression coverage for optional selectors and creating relations for both supported target types. - Render the filled field clone for
refreshAfterApply()and consistently return selected relation IDs fromBelongsToMany. - Preserve conditional JSON values and cached
showWhennames after row deletion, insertion, and reindexing; restore HasMany edit actions with the child resource's policy checks. - Preserve custom-view label/slot replacements and iterator-backed Card/Footer data through serialization and rendering.
- Keep async filter Reset from restoring saved filter state, store query-state cache data as arrays, and apply the default query tag for an empty selection.
- Keep enum labels, breadcrumb text, colors, and icons consistent across resources and relationship selectors.
- Keep older async list responses from overwriting newer filtering, sorting, or pagination results.
- Run error callbacks after network failures, timeouts, and unusable blob error bodies, allowing async forms to recover and retry.
- Prevent repeat clicks on an in-flight async button, including
withoutLoading()buttons; keep independently created toast IDs and timers from interfering. - Add
Fragment::updateOnLoad()for fragments that should refresh when the page loads. - Preserve provider registration for Laravel applications using either
bootstrap/providers.phpor the olderconfig/app.phpstructure.
Upgrading from 4.x
-
Upgrade PHP to 8.4+ and use Laravel 11, 12, or 13.
-
Update MoonShine and any separately installed split packages together. Upgrade extensions to versions that support MoonShine 5. For import/export:
composer require moonshine/moonshine:^5.0 moonshine/import-export:^3.0 --with-all-dependencies
Omit
moonshine/import-exportif the application does not use that integration. -
Move list-page definitions and import/export handler overrides, replace the removed aliases, and review trusted HTML and custom extension signatures as described above.
-
Merge the new
escapesand optionalallowed_extensionssettings into your published configuration as needed. Review published Blade overrides instead of blindly overwriting customized views. -
Publish the refreshed assets and clear cached views/configuration:
php artisan vendor:publish --tag=moonshine-assets --force php artisan optimize:clear
-
Check custom async clients, filters/query tags, relation links, sorting, uploads, and import/export flows in your application.
The release commit passed the PHP 8.4/8.5 × Laravel 12/13 CI matrix, PHPStan, JavaScript tests, the production asset build, mutation testing, and style checks. Laravel 11 compatibility was also checked locally against the resolved 11.x-dev branch. All nine split-package synchronization jobs passed.
Complete pull request changelog
- #2058 — Upgrade dependencies and require PHP 8.4 for 5.x
- #2059 — Remove deprecated APIs for MoonShine 5
- #2060 — Raise PHPStan to max and improve resource generics
- #2062 — Fix Select labels after form reset
- #2072 — fix: enforce activeActions for bulk deletion on 5.x
- #2083 — fix: avoid double escaping select option labels and values (5.x)
- #2081 — fix: prevent javascript URLs in rendered links
- #2067 — feat: escape display labels and field decorations by default
- #2088 — Fix refreshed field state and selected relation IDs; expand mutation coverage
- #2087 — perf: avoid forced layouts when list pages load
- #2085 — perf: speed up 5.x server-side rendering
- #2086 — perf: render vertical table fields in one view
- #2068 — Add global allowed file extensions for File and Image fields
- #2069 — feat: add global field escaping configuration
- #2101 — fix: run the error callback when a request fails without a response (5.x)
- #2102 — fix: ignore repeat clicks on an async button while its request runs (5.x)
- #2103 — fix: keep other toasts when a closed toast's timer fires (5.x)
- #2104 — fix: sort a list only by the default column or a visible sortable field
- #2096 — Fix parent relation validation on 5.x
- #2097 — Verify CSRF protection for async methods on 5.x
- #2100 — Scope mass deletion and authorize each item on 5.x
- #2106 — fix: preserve MorphTo required validation when changing type
Additional release maintenance
- Refresh Composer and UI dependencies and rebuild the distributed assets. dd1aaf976
- Switch import/export integration to the stable
^3.0release. 0e5532fef - Port
Fragment::updateOnLoad(), preserve default query-tag selection, and keep the latest async list request in control; these changes are included even where they landed as direct commits.
Thanks to everyone who contributed code, reviewed changes, and reported regressions.
Full changelog: 4.21.0 → 5.0.0