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: 2
Due to an extensive refactoring aiming to improve DX, there is many breaking changes in the API... But stay with me, it's mainly function renaming and new arguments.
This is true for every update: be sure to grab the latest assets and to clear the view cache:
php artisan vendor:publish --provider="Code16\Sharp\SharpServiceProvider" --tag=assets
php artisan view:clear
It's not a BC, but still, minimal requirements are now these.
In order to reinforce the API, we decided to use PHP type hinting everywhere. Sharp 6 handled the big part, but with php 8.0 we could add a few that were missing.
This means that the getListData() function no longer has a $params argument, which must be replaced by $this->queryParams. This change makes it much easier to build the EntityList depending on the request (hide some columns for instance).
Similarly, the buildWidgetsData() function no longer has a $params argument, which must be replaced by $this->queryParams.
configure...The motivation for this was to provide a clear list of available methods for configuration to the developer. This could lead to many changes, but it's quite easy to fix them. Here's the full list of renamed methods:
Renamed methods of SharpEntityList:
setInstanceIdAttribute() -> configureInstanceIdAttribute()setSearchable() -> configureSearchable()setDefaultSort() -> configureDefaultSort()setPaginated() -> configurePaginated()setReorderable() -> configureReorderable()setPrimaryEntityCommand() -> configurePrimaryEntityCommand()setMultiformAttribute() -> configureMultiformAttribute()Renamed methods of SharpForm:
setDisplayShowPageAfterCreation() -> configureDisplayShowPageAfterCreation()setBreadcrumbCustomLabelAttribute() -> configureBreadcrumbCustomLabelAttribute()Renamed methods of SharpShow:
setMultiformAttribute() -> configureMultiformAttribute()setEntityState() -> configureEntityState()setBreadcrumbCustomLabelAttribute() -> configureBreadcrumbCustomLabelAttribute()Renamed methods of Command:
setConfirmationText() -> configureConfirmationText()setDescription() -> configureDescription()setFormModalTitle() -> configureFormModalTitle()The idea is to provide dedicated objects when needed to clearly indicate the API to the developer.
buildListDataContainers() is renamed to buildListFields(EntityListFieldsContainer $fieldsContainer)The $fieldsContainer parameter is a proxy object with the needed ->addField(...) function.
As a bonus, EntityListDataContainer was renamed EntityListField for clarity.
Example: you need to refactor you code from this:
function buildListDataContainers(): void
{
$this
->addDataContainer(
EntityListDataContainer::make("name")
->setLabel("Name")
)
->addDataContainer(
EntityListDataContainer::make("age")
->setLabel("Age")
);
}
To this:
function buildListFields(EntityListFieldsContainer $fieldsContainer): void
{
$fieldsContainer
->addField(
EntityListField::make("name")
->setLabel("Name")
)
->addField(
EntityListField::make("age")
->setLabel("Age")
);
}
buildListLayout() signature has changed to buildListLayout(EntityListFieldsLayout $fieldsLayout)The $fieldsLayout parameter is a proxy object with the needed ->addColumn(...) function.
Example: you need to refactor you code from this:
function buildListLayout(): void
{
$this->addColumn("picture", 1)
->addColumn("name", 2)
->addColumn("capacity", 2)
->addColumn("type:label", 2)
->addColumn("pilots")
->addColumn("messages_sent_count");
}
To this:
function buildListLayout(EntityListFieldsLayout $fieldsLayout): void
{
$fieldsLayout->addColumn("picture", 1)
->addColumn("name", 2)
->addColumn("capacity", 2)
->addColumn("type:label", 2)
->addColumn("pilots")
->addColumn("messages_sent_count");
}
->addColumn() no longer has optional parameter for small screensInstead, a new optional buildListLayoutForSmallScreens() is available. Refer to doc for details.
buildFormFields() is renamed to buildFormFields(FieldsContainer $formFields)The $formFields parameter is a proxy object with the needed ->addField(...) function.
Example: you need to refactor you code from this:
function buildFormFields(): void
{
$this
->addField(
SharpFormTextField::make("name")
->setLabel("Name")
);
}
To this:
function buildFormFields(FieldsContainer $formFields): void
{
$formFields
->addField(
SharpFormTextField::make("name")
->setLabel("Name")
);
}
buildFormLayout() signature has changed to buildFormLayout(FormLayout $formLayout)The $formLayout parameter is a proxy object with the needed ->addTab(...) and ->addColumn(...) functions.
Example: you need to refactor you code from this:
function buildFormLayout(): void
{
$this
->addColumn(6, function(FormLayoutColumn $column) {
return $column->withSingleField("name")
->withSingleField("email");
});
}
To this:
function buildFormLayout(FormLayout $formLayout): void
{
$formLayout
->addColumn(6, function(FormLayoutColumn $column) {
return $column->withSingleField("name")
->withSingleField("email");
});
}
buildShowFields() is renamed to buildShowFields(FieldsContainer $showFields)The $showFields parameter is a proxy object with the needed ->addField(...) function.
Example: you need to refactor you code from this:
function buildShowFields(): void
{
$this
->addField(
SharpShowTextField::make("name")
->setLabel("Name")
);
}
To this:
function buildShowFields(FieldsContainer $showFields): void
{
$showFields
->addField(
SharpShowTextField::make("name")
->setLabel("Name")
);
}
buildShowLayout() signature has changed to buildShowLayout(ShowLayout $showLayout)The $showLayout parameter is a proxy object with the needed ->addSection(...) and ->addEntityListSection(...) functions.
Example: you need to refactor you code from this:
function buildShowLayout(): void
{
$this
->addSection("Identity", function(ShowLayoutSection $section) {
$section
->addColumn(6, function(ShowLayoutColumn $column) {
$column->withSingleField("name");
});
});
}
To this:
function buildShowLayout(ShowLayout $showLayout): void
{
$showLayout
->addSection("Identity", function(ShowLayoutSection $section) {
$section
->addColumn(6, function(ShowLayoutColumn $column) {
$column->withSingleField("name");
});
});
}
buildWidgets() is renamed to buildWidgets(WidgetsContainer $widgetsContainer)The $widgetsContainer parameter is a proxy object with the needed ->addWidget(...) function.
Example: you need to refactor you code from this:
function buildWidgets(): void
{
$this
->addWidget(
SharpBarGraphWidget::make("travels")
);
}
To this:
function buildWidgets(WidgetsContainer $widgetsContainer): void
{
$widgetsContainer
->addWidget(
SharpBarGraphWidget::make("travels")
);
}
buildWidgetsLayout() signature has changed to buildDashboardLayout(DashboardLayout $dashboardLayout)The $dashboardLayout parameter is a proxy object with the needed ->addRow(...) and ->addFullWidthWidget(...)
functions.
Example: you need to refactor you code from this:
function buildWidgetsLayout(): void
{
$this
->addRow(function(DashboardLayoutRow $row) {
$row->addWidget(6, "types_pie")
->addWidget(6, "features_bars");
});
}
To this:
function buildDashboardLayout(DashboardLayout $dashboardLayout): void
{
$dashboardLayout
->addRow(function(DashboardLayoutRow $row) {
$row->addWidget(6, "types_pie")
->addWidget(6, "features_bars");
});
}
There are various changes on Commands, but it's mainly code reorganization and function renaming, for better clarity and easier configuration.
Commands must now be declared in dedicated function: getInstanceCommands() (EntityList and Show Page)
, getEntityCommands() (EntityList), and getDashboardCommands() (Dashboard) — previously they were declared in
the build...Config() method.
For example, here's a 6.0 EntityList (everything in the buildListConfig() function):
function buildListConfig(): void
{
$this->setInstanceIdAttribute("id")
->setSearchable()
->setDefaultSort("name", "asc")
->addFilter("type", SpaceshipTypeFilter::class)
->addFilter("pilots", SpaceshipPilotsFilter::class)
->addEntityCommand("synchronize", SpaceshipSynchronize::class)
->addEntityCommand("reload", SpaceshipReload::class)
->addInstanceCommand("message", SpaceshipSendMessage::class)
->addInstanceCommand("preview", SpaceshipPreview::class)
->addInstanceCommandSeparator()
->addInstanceCommand("external", SpaceshipExternalLink::class);
}
And the same EntityList in 7.0: Commands are no longer in the buildListConfig function:
function getEntityCommands(): ?array
{
return [
new SpaceshipSynchronize(),
SpaceshipReload::class
];
}
function getInstanceCommands(): ?array
{
return [
SpaceshipSendMessage::class,
new SpaceshipPreview(),
"---",
SpaceshipExternalLink::class
];
}
function buildListConfig(): void
{
$this->configureInstanceIdAttribute("id")
->configureSearchable()
->configureDefaultSort("name", "asc");
}
Also notice that:
This applies in every class that leverage Commands: EntityList, Show Page and Dashboard.
buildCommandConfig() method + removal of old functions: ->description(), ->confirmationText()...In 6.0, some Command configuration was done overriding some methods, without really knowing which ones are concerned. In 7.0, a new buildCommandConfig() is here to handle all configuration.
Here's a 6.0 example:
class TravelSendEmail extends InstanceCommand
{
public function label(): string
{
return "Send email";
}
public function formModalTitle(): string
{
return "Send email";
}
public function description(): string
{
return "Will pretend to send an email to all the passengers of this flight.";
}
[...]
}
And its 7.0 version:
class class TravelSendEmail extends InstanceCommand
{
public function label(): string
{
return "Send email";
}
public function buildCommandConfig(): void
{
$this->configureFormModalTitle("Send email")
->configureDescription("Will pretend to send an email to all the passengers of this flight.");
}
[...]
}
Exactly like for SharpForm (see above), you need to go from this in 6.0:
public function buildFormFields(): void
{
$this
->addField(
SharpFormTextField::make("subject")
->setLabel("Subject")
);
}
To this in 7.0:
public function buildFormFields(FieldsContainer $formFields): void
{
$formFields
->addField(
SharpFormTextField::make("subject")
->setLabel("Subject")
);
}
... so you need to change all implements SelectFilter (for instance) to extends SelectFilter.
Similar to Commands, Filters must now be declared in dedicated function: getFilters().
For example, here's a 6.0 EntityList (everything in the buildListConfig() function):
function buildListConfig(): void
{
$this->setInstanceIdAttribute("id")
->setSearchable()
->setDefaultSort("name", "asc")
->addFilter("type", SpaceshipTypeFilter::class)
->addFilter("pilots", SpaceshipPilotsFilter::class);
}
To this:
function getFilters(): ?array
{
return [
SpaceshipTypeFilter::class,
new SpaceshipPilotsFilter() // Filters can be declared as class name or instances
];
}
function buildListConfig(): void
{
$this->configureInstanceIdAttribute("id")
->configureSearchable()
->configureDefaultSort("name", "asc");
}
Also notice that the Filter keys (type and pilots, in this example) disappeared
This applies in all classes that leverage Filters: EntityList and Dashboard.
Like seen in the previous point, the string key has been removed for simplicity and to avoid mistakes. So in Sharp 7.0, to extract a filter value from the query params, you'll need to provide the filter's class name.
For example:
function getListData(): array|Arrayable
{
$pilot = $this->queryParams->filterFor(SpaceshipPilotFilter::class);
[...]
}
buildFilterConfig() method + removal of all old functions: ->isSearchable(), ->isMaster(), ->template()...Like other classes in Sharp, Filters now have a dedicated buildFilterConfig() method to group all configuration calls. And since we moved from interfaces to abstract classes, it's easier to find all available options with IDE autocompletion: all option methods start with configure[...]().
For example:
public function buildFilterConfig(): void
{
$this->configureLabel("Ship type")
->configureSearchable()
->configureRetainInSession();
}
Global filters no longer need keys (in the configuration, where they are declared):
// in config/sharp.php
return [
[...],
"global_filters" => [
CorporationGlobalFilter::class,
]
];
Also update your queries accordingly, using full class name instead of key, like for other fields:
function getListData(): array|Arrayable
{
$spaceships = Spaceship::select("spaceships.*")
->where("corporation_id", currentSharpRequest()->globalFilterFor(CorporationGlobalFilter::class))
->get();
[...]
}
callback() filter feature was removedThis feature wasn't designed well. It may be replaced in the future is needed.
SharpFormMarkdownField and SharpFormWysiwygField where removed, and replace by SharpFormEditorFieldThe two fields were united in one, with the same UX. To migrate a field, you must change its class and add ->setRenderContentAsMarkdown() is necessary:
Example: you need to refactor your SharpFormMarkdownField from this:
function buildFormFields(): void
{
$this
->addField(
SharpFormMarkdownField::make("description")
->setLabel("Description")
->setToolbar([
SharpFormMarkdownField::B,
SharpFormMarkdownField::I,
SharpFormMarkdownField::A,
])
->setHeight(700)
);
}
To this:
function buildFormFields(FieldsContainer $formFields): void
{
$formFields
->addField(
SharpFormEditorField::make("description")
->setRenderContentAsMarkdown()
->setLabel("Description")
->setToolbar([
SharpFormEditorField::B,
SharpFormEditorField::I,
SharpFormEditorField::A,
])
->setHeight(700)
);
}
Same for SharpFormWysiwygField, except for the ->setRenderContentAsMarkdown().
MarkdownAttributeTransformer no more has a dedicated method to handle embedded imagesIf you allow embed images in a markdown field, previously you would have to call ->handleImages() on the
transformer to configure image embedding: this is now implied, due to the <x-sharp-media> refactoring.
Simply remove this call.
Before:
function find($id): array
{
return $this
->setCustomTransformer(
"description",
(new MarkdownAttributeTransformer())->handleImages(200)
)
->transform([...]);
}
After:
function find($id): array
{
return $this
->setCustomTransformer(
"description",
new MarkdownAttributeTransformer()
)
->transform([...]);
}
If you allow embed images or files in a markdown field, previously you would need to use respectively sharp_markdown_thumbnails()
and sharp_markdown_embedded_files() to display embedded images and files in the public site. Those two helpers were
removed.
You now need to replace them by the new <x-sharp-content> component: see dedicated documentation.
Before with embed images:
<div>
{!! sharp_markdown_thumbnails($object->my_markdown_field, "mw-100", 775) !!}
</div>
or with embed files:
<div>
{!! sharp_markdown_embedded_files($object->my_markdown_field) !!}
</div>
After:
<div>
<x-sharp-content>
{!! $object->my_markdown_field !!}
</x-sharp-content>
</div>
If you allow embed images or files in a markdown field, you probably stored them in database. With previous format, markdown with embed objects looked something like this:
# My title
Some text

More text

Due to the <x-sharp-media> refactoring, markdown with embed objects looks something like:
# My title
Some text
<x-sharp-image name="my_image.jpg" path="data/img/markdown/my_image.jpg" disk="local">
</x-sharp-image>
More text
<x-sharp-file name="my_document.pdf" path="data/files/markdown/my_document.pdf" disk="local">
</x-sharp-file>
If you want to keep displaying old markdown, you will have to convert embed objects from old to new format in all your stored markdown fields. This could be achieved manually or automated with a custom script based on preg_replace_callback.
Sharp 7 brings a new config format, along with the SharpEntity. In short, this means the sharp.php config file is lighter since all the entity config (list, form, policy, show...) is now declared in a dedicated entity class. This is fully documented here, and it's NOT A BREAKING CHANGE, since the legacy config format is still handled with Sharp 7 (but it's clearly deprecated).
In the config of a Sharp 7 project, we just declare a SharpEntity class for entities and dashboard:
// config/sharp.php
return [
// ...
"entities" => [
"spaceship" => \App\Sharp\Entities\SpaceshipEntity::class,
"pilot" => \App\Sharp\Entities\PilotEntity::class,
],
"dashboards" => [
"company_dashboard" => \App\Sharp\Entities\CompanyDashboardEntity::class,
],
//...
];
SharpEntity's subclasses are responsible for the list, form, show... declaration (see documentation). Note that as stated before, you can keep the old declaration way in the config file, or even mix the new and old methods, but the old one is deprecated and may be removed in a future release.
Validators declaration moved from config file to where it belongs, in the Form itself:
class SpaceshipSharpForm extends SharpForm
{
protected ?string $formValidatorClass = SpaceshipSharpValidator::class;
// ...
}
See related documentation, and do note that the Sharp 6 way of declare it (in config/sharp.php) is still handled as legacy (but deprecated).
Entity and Dashboard Policies must now extend Code16\Sharp\Auth\SharpEntityPolicy, where the methods slightly changed (typehinting mostly). This is not a BC, since the legacy Sharp 6 format is still handled, but it's deprecated.
The permission method to see a Dashboard was renamed from view() to entity(), to be consistent:
class CompanyDashboardPolicy extends SharpEntityPolicy
{
public function entity($user): bool
{
return $user->hasGroup("boss");
}
}
How can I help you explore Laravel packages today?