code16/sharp
Code-driven CMS framework for Laravel (PHP 8.3+/Laravel 11+). Build admin/CMS sections with a clean UI and strong DX: CRUD with validation, search/sort/filter, bulk or custom commands, and authorization—no front-end code required, data-agnostic.
outline: [1, 2]
This is a very big release, with a lot of changes. We try to limit breaking changes, but there are some... This guide will help you to upgrade your Sharp 8.x app to Sharp 9.x.
This is true for every update: be sure to grab the latest assets and to clear the view cache:
php artisan vendor:publish --tag=sharp-assets --force
php artisan view:clear
The command used to publish sharp's assets changed, you should update your composer.json:
{
"scripts": {
"post-autoload-dump": [
[...],
- "[@php](https://github.com/php) artisan vendor:publish --provider='Code16\\Sharp\\SharpServiceProvider' --tag=assets --force",
+ "[@php](https://github.com/php) artisan vendor:publish --tag=sharp-assets --force"
]
}
}
buildListFields() and buildListLayout() were removed, use buildList() insteaddelete() method was removed (since it was moved to show / entity list in 8.x)The config/sharp.php file was entirely removed in favor of a dedicated builder class. This is not a breaking change since the config file is still supported, but deprecated, so you are encouraged to migrate to the new builder class.
To migrate, you should first create a new Service Provider which extends Code16\Sharp\SharpAppServiceProvider and implements the configureSharp() method:
use Code16\Sharp\SharpAppServiceProvider;
class MySharpServiceProvider extends SharpAppServiceProvider
{
protected function configureSharp(SharpConfigBuilder $config): void
{
$config
->setName('My project')
->declareEntity(PostEntity::class)
// ...
}
}
Report all you configuration using the API of this new SharpConfigBuilder class. It should be pretty straightforward, as all the methods are named after the config keys they replace. For example:
In 8.x:
// Old config/sharp.php
return [
'name' => 'Demo project',
'custom_url_segment' => 'sharp',
'display_breadcrumb' => true,
'entities' => [
'posts' => \App\Sharp\Entities\PostEntity::class,
],
'global_filters' => fn () => auth()->id() === 1 ? [] : [\App\Sharp\DummyGlobalFilter::class],
'search' => [
'enabled' => true,
'placeholder' => 'Search for posts or authors...',
'engine' => \App\Sharp\AppSearchEngine::class,
],
'menu' => \App\Sharp\SharpMenu::class,
// ...
];
In 9.x:
class MySharpServiceProvider extends SharpAppServiceProvider
{
protected function configureSharp(SharpConfigBuilder $config): void
{
$config
->setName('Demo project')
->setCustomUrlSegment('sharp')
->setDisplayBreadcrumb()
->declareEntity(PostEntity::class)
->addGlobalFilter(DummyGlobalFilter::class) // The auth()->id() === 1 no longer can be handled here, as the auth context is yet not available. Use the new authorize() method of the global filter instead.
->enableGlobalSearch(AppSearchEngine::class, 'Search for posts or authors...')
->setMenu(SharpMenu::class)
// ...
}
}
::: warning Be sure to register this new Service Provider in your app. :::
Due to migration to inertia, three middleware must be added to the config. Also, SetSharpLocale must be removed from api group.
::: info If you migrated to the new config builder class, you should be ok unless you have explicitly overridden the whole middleware list. :::
Here is the impact on the deprecated config file:
// config/sharp.php
return [
'middleware' => [
'common' => [
// ...
\Code16\Sharp\Http\Middleware\HandleGlobalFilters::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, // <- be sure to place this one after HandleGlobalFilters
],
'web' => [
// ...
\Code16\Sharp\Http\Middleware\HandleSharpErrors::class,
\Code16\Sharp\Http\Middleware\HandleInertiaRequests::class,
],
'api' => [
// To remove :
// \Code16\Sharp\Http\Middleware\Api\SetSharpLocale::class,
]
],
]
blade-iconsIn 8.x, menu icons were FontAwesome classes like fa fa-user or fas fa-user. Now icons must be blade-icons icon names with its associated package installed.
Icons are not required but if you want to keep FontAwesome, you can install the following package:
composer require owenvoke/blade-fontawesome
And rename old icon names to blade-fontawesome names in your SharpMenu :
# Solid icons
- ->addEntityLink('entity', 'Entity', 'fas fa-user')
- ->addEntityLink('entity', 'Entity', 'fa fa-user')
+ ->addEntityLink('entity', 'Entity', logo: 'fas-user')
# Regular (outline) icons
- ->addEntityLink('entity', 'Entity', 'far fa-envelope')
- ->addEntityLink('entity', 'Entity', 'fa fa-envelope-o')
+ ->addEntityLink('entity', 'Entity', logo: 'far-envelope')
# Brand icons
- ->addEntityLink('entity', 'Entity', 'fa fa-github')
- ->addEntityLink('entity', 'Entity', 'fab fa-github')
+ ->addEntityLink('entity', 'Entity', logo: 'fab-github')
If you were using old fontawesome 4 icons you may need to rename them.
As the default behavior of blade-icons (which uses an svg tag) differs from the previous behavior of FontAwesome’s <i/> tag (which was inline and matched the font size), you may need to adjust the blade-icons configuration to replicate the original behavior.
First, publish the FontAwesome blade-icons configuration file:
php artisan vendor:publish --tag=blade-fontawesome-config
Then add default attributes for each FontAwesome classes (solid, regular, brands, and optionally pro):
// file: config/blade-fontawesome.php
return [
// ...
'regular' => [
// ...
'attributes' => [
'width' => '1rem',
'height' => '1rem',
'style' => 'display: inline;'
],
],
];
You may be using Custom HTML (entity list row, editor embeds, form HTML field, form Autocomplete item template, page alerts) with CSS classes that was present in Sharp 8.x but that don't exist anymore:
row / col / badge in HTML content. These are no longer available, you can either :
<i class="fas fa-user">
<x-fas-user style="width: 1rem; height: 1rem" />. In Sharp 9.x all templates are now Blade.<i class="fas fa-user"> inside a custom transformer of an EntityList field, you must now do Blade::render('<x-fas-user style="width: 1rem; height: 1rem; display:inline;" />')This part has been entirely rewritten, and will need substantial changes in your code.
In 8.x and below, you were asked to configure page alerts in the buildConfig() method; and if your page alert was displaying dynamic data, you had to use a custom transformer to inject the data in the page alert. All of this was removed, in favor of a much simpler "back only" system. Here’s an example of a page alert in a Show Page (this is the same in Form, Dashboard, Entity List, Embed and Command cases):
class MyShow extends SharpShow
{
// ...
protected function buildPageAlert(PageAlert $pageAlert): void
{
$pageAlert
->setLevelInfo()
->setMessage(function (array $data) {
return $data['is_planned']
? 'This post is planned for publication, on ' . $data['published_at']
: null;
});
}
}
As you can see, this new buildPageAlert() method takes a PageAlert object as parameter to work with. You'll have access to the $data array returned by your find() or getListData() method, to inject dynamic data in your page alert if needed. Vue templates are no longer handled, as the page alert is now rendered on the back only.
See global page alert documentation for more detail.
This bug fix potentially brings a breaking change: if you were using a custom transformer to handle related models, you may have to update it.
Here’s code which will work in Sharp 8.x and below:
$this
->setCustomTransformer('customer:name', function ($value, $instance, $attribute) {
return $instance->customer->name; // $instance is the Order
})
->transform(Order::find(1))
Is has to be rewritten like this in Sharp 9.x:
$this
->setCustomTransformer('customer:name', function ($value, $instance, $attribute) {
return $instance->name; // $instance is the Customer, as it should be.
})
->transform(Order::find(1))
The main difference is that the $instance parameter refers to the related model, not the main model anymore. To summarize:
In 8.x:
$value: 'Joe Doe'
$instance: // the Order instance
$attribute: 'customer:name'
In 9.x:
$value: 'Joe Doe'
$instance: // the **Customer** instance
$attribute: 'name'
First, if you defined custom filters classes for your thumbnails, you must refactor it to the new ThumbnailModifier API, which is very close:
In 8.x
class MyFilter extends ThumbnailFilter
{
public function applyFilter(Image $image): Image
{
// ...
}
}
In 9.x
use Code16\Sharp\Form\Eloquent\Uploads\Thumbnails\ThumbnailModifier;
use Intervention\Image\Interfaces\ImageInterface;
class MyModifier extends ThumbnailModifier
{
public function apply(ImageInterface $image): ImageInterface
{
// ...
}
}
And secondly, in 9.x you can’t pass modifier's parameters as an array anymore:
In 8.x
$book->cover->thumbnail(100, 100, ['fit'=>['w'=>100, 'h'=>100]]);
In 9.x
$book->cover->thumbnail(100, 100, [new FitModifier(100, 100)]);
// or with the new fluent API
$book->cover->thumbnail()
->addModifier(new FitModifier(100, 100))
->make();
currentSharpRequest() is now deprecated in favor of sharp()->context() helperThe currentSharpRequest() helper is now deprecated, and will be entirely removed in a future version. You should migrate your code to use the sharp()->context() helper instead (see the dedicated documentation).
SharpAuthenticationCheckHandler is now deprecated in favor of viewSharp GateThe use of a SharpAuthenticationCheckHandler is now deprecated, and will be entirely removed in a future version. You should migrate your handler to a Gate:
In 8.x:
class MySharpAuthenticationCheckHandler implements SharpAuthenticationCheckHandler
{
public function check(Authenticatable $user): bool;
{
return $user->is_sharp_admin;
}
}
In 9.x:
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot(): void
{
Gate::define('viewSharp', fn ($user) => $user->is_sharp_admin);
}
}
::: tip
You should place this code in the new Sharp dedicated Service Provider you will create to configure your Sharp app, overriding the declareAccessGate() method. See the dedicated documentation.
:::
Next, the sharp.auth.check_handler config key can be safely removed from your config/sharp.php file (in case you have not yet migrated to the dedicated builder class, see above), along with the SharpAuthenticationCheckHandler implementation class.
SharpConfigBuilderIn 8.x,
// config/sharp.php
return [
'extensions' => [
'assets' => [
'strategy' => 'vite',
'head' => [
'resources/css/sharp.css',
],
],
],
];
In 9.x :
class MySharpServiceProvider extends SharpAppServiceProvider
{
protected function configureSharp(SharpConfigBuilder $config): void
{
$config
->loadViteAssets(['resources/css/sharp.css']) // to load a CSS file built with Vite
->loadStaticCss(asset('/css/sharp.css')) // Or to load a static CSS file
}
}
All assertions, like for instance assertSharpHasAuthorization, were removed because they were clumsy and not really useful. You must remove them from your tests, and use standard comparisons instead — although in real world, it’s easier and cleaner to just check the return status (ie: assertOk()) and check, if needed, the consequences in the database directly.
This means you also need to remove all $this->initSharpAssertions() calls from your tests.
Of course, the test helpers remain available, see the dedicated testing documentation.
Also take note that the withSharpCurrentBreadcrumb() method is now deprecated, in favor of the new withSharpBreadcrumb() method also documented in the section linked above.
SharpWidgetPanels are now based on blade templateIn a similar way to Page Alerts, we abandoned Vue templates for custom SharpWidgetPanels in favor of blade templates. This is a breaking change, as the setTemplatePath(...)and setInlineTemplate(...) methods were removed, placed by a unique setTemplate(View $template).
There's almost nothing to change on the PHP side:
In 8.x:
class MyDashboard extends SharpDashboard
{
// ...
protected function buildWidgets(WidgetsContainer $widgetsContainer): void
{
$widgetsContainer
->addWidget(
SharpPanelWidget::make('my_custom_panel')
->setTitle('My custom panel')
->setTemplatePath('sharp.templates.my_template') // Must be an existing **vue file**
);
}
}
In 9.x:
class MyDashboard extends SharpDashboard
{
// ...
protected function buildWidgets(WidgetsContainer $widgetsContainer): void
{
$widgetsContainer
->addWidget(
SharpPanelWidget::make('my_custom_panel')
->setTitle('My custom panel')
->setTemplate(view('sharp.templates.my_template')) // Must be an existing **blade view**
);
}
}
The main change is the template itself, which must be a blade view now.
See panel widget documentation for more detail.
reorderPreviously the reorder authorization was handled by the update authorization, which can lead to unwanted effects. You should now declare a specific reorder authorization in your Policies:
class PostPolicy extends SharpEntityPolicy
{
public function reorder($user): bool
{
return $user->isEditor();
}
// ...
}
setWidth() method as a new signature (non-breaking change: old signature is still supported)The ->setWith($width) method now expects a percentage value, expressed as a string (eg: '20' or '20%'), a float (eg: .2 for 20%) or an integer (eg: 20 for 20%). The old signature use to accept a 1 to 12 integer (12-grid): it is still supported (Sharp will transform a 6 in 50%), but deprecated, and you are strongly encourage to migrate to the new signature.
The ->setWithOnSmallScreen($width) and ->setWithOnSmallScreenFill() methods were deprecated, because they are no more used in the new front table UI system. You can safely remove them, Sharp will rely on the setWidth($width) method, or, even easier, will deduce width based on content like a regular table.
The ->hideOnSmallScreens() method remains.
In Sharp 8.x it was possible, in an Embedded Entity List (EEL) case, to use a filter without declaring it. This was a kind of bug, and has been fixed in 9.x: all filters must be declared in the getFilters() method.
Consider this code in 8.x, where we have a PostShow that embeds a PostBlockList as an EEL:
class PostShow extends SharpShow
{
protected function buildShowFields(FieldsContainer $showFields): void
{
$showFields
->addField(SharpShowTextField::make('title')->setLabel('Title'))
->addField(
SharpShowEntityListField::make('blocks')
->setLabel('Blocks')
->hideFilterWithValue('post', fn($instanceId) => $instanceId)
);
}
// ...
}
class PostBlockList extends SharpEntityList
{
// ...
protected function getFilters(): ?array
{
return [
// Nothing there
];
}
public function getListData(): array|Arrayable
{
return $this->transform(
PostBlock::query()
->where('post_id', $this->queryParams->filterFor('post'))
->get()
);
}
}
We can see that PostBlockList does not define any Filter, but uses one in the getListData() method, valued by the PostShow via hideFilterWithValue(). In 9.x, this won't work as the Filter must be declared in the getFilters() method. There is a new way to quickly declare such Filters that are not meant to be shown to the user, HiddenFiler:
In 9.x
use \Code16\Sharp\EntityList\Filters\HiddenFilter;
class PostBlockList extends SharpEntityList
{
// ...
protected function getFilters(): ?array
{
return [
HiddenFilter::make('post')
];
}
// ... The rest is the same
}
You can of course instead declare a real Filter.
configureTemplate() has been droppedIf you were using this method, you must do the string transformation in the label of each value.
This is not a breaking change, in fact you can ignore this step entirely, but since it's can lead to a significative performance boost, this is worth mentioning: you can now quite easily implement a caching mechanism of instances for your Commands and Policies in Entity List.
The ->withSingleField() method was deprecated, in favor of:
->withField(string $fieldKey) for simple fields->withListField(string $fieldKey, Closure $subLayoutCallback) for List fields, which requires a sub-layout handled by a callback.The method ->withFields(string ...$fieldKeys), used for multiple fields layout, remains unchanged.
This shouldn’t cause any trouble, as this is a fix, but it could break unorthodox code: field formatters (which are used to format the field value for the frontend) are now properly and always called before displaying data to the front, even if you don’t transform your data with $this->transform() method.
In 8.x, you could define custom field by creating a Vue component (in form or in show) but this is not supported anymore.
In the initial 9.0 release, there isn't a straightforward alternative, but we are looking for ways to easily integrate Alpine / Livewire component. If you only need to render static blade file you can use SharpFormHtmlField or SharpShowTextField.
The validation system has been revamped in version 9.x to align better with Laravel and improve consistency.
You may now define your validation rules in two ways:
rules() and messages() methods in your SharpForm or Command; those methods accepts an optional $formattedData parameter which represents the... formatted posted data.->validate(array $data, array $rules).In consequence, Form Validators are now deprecated: the $formValidatorClass property in SharpForm is deprecated. You are strongly encouraged to switch to the rules() method.
This code in 8.x:
class PostForm extends SharpForm
{
protected ?string $formValidatorClass = PostFormValidator::class;
// ...
}
class PostFormValidator extends SharpFormRequest
{
public function rules(): array
{
return [
'title' => 'required',
'content' => 'required',
];
}
}
Should be rewritten in this in 9.x:
class PostForm extends SharpForm
{
public function rules(): array
{
return [
'title' => 'required',
'content' => 'required',
];
}
// ...
}
Or:
class PostForm extends SharpForm
{
// ...
public function update($id...
How can I help you explore Laravel packages today?