Upgrading from 6.x to 7.x

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.

Get new assets, clear cache

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

Laravel 8+ and php 8.0+ required

It's not a BC, but still, minimal requirements are now these.

Type hinting everywhere (part II)

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.

EntityListQueryParams is now an instance property of an EntityList

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).

DashboardQueryParams is now an instance property of a Dashboard

Similarly, the buildWidgetsData() function no longer has a $params argument, which must be replaced by $this->queryParams.

All configuration methods have now a name prefixed with 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()
  • setDisplayShowPageAfterCreation() -> configureBreadcrumbCustomLabelAttribute()

Renamed methods of SharpShow:

  • setMultiformAttribute() -> configureMultiformAttribute()
  • setEntityState() -> configureEntityState()
  • setBreadcrumbCustomLabelAttribute() -> configureBreadcrumbCustomLabelAttribute()

Renamed methods of Command:

  • setConfirmationText() -> configureConfirmationText()
  • setDescription() -> configureDescription()
  • setFormModalTitle() -> configureFormModalTitle()

Add proxy objects for EntityList columns and layout

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");
}

EntityList's ->addColumn() no longer has optional parameter for small screens

Instead, a new optional buildListLayoutForSmallScreens() is available. Refer to doc for details.

Add proxy objects for Form fields and layout

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");
        });
}

Add proxy objects for Show fields and layout

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");
                });
        });
}

Add proxy objects for Dashboard widgets and layout

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");
        });
}

API changes on Commands

There are various changes on Commands, but it's mainly code reorganization and function renaming, for better clarity and easier configuration.

New Command declaration

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:

  • the Command key is now optional
  • the Command separator is a string made of dashes (one is enough)
  • Filters were removed as well, see dedicated section below in this guide

This applies in every class that leverage Commands: EntityList, Show Page and Dashboard.

new 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.");
    }
    
    [...]
}

New proxy object for form building

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")
    );
}

Changes on Filters

Base Filters are no longer interfaces, but abstract classes

... so you need to change all implements SelectFilter (for instance) to extends SelectFilter.

New Filter declaration

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.

No more filter key: update your queries

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);
    [...]
}

New 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();
}

Changes on global filters configuration

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();
    [...]
}

The callback() filter feature was removed

This feature wasn't designed well. It may be replaced in the future is needed.

Markdown editor refactoring

SharpFormMarkdownField and SharpFormWysiwygField where removed, and replace by SharpFormEditorField

The 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().

The MarkdownAttributeTransformer no more has a dedicated method to handle embedded images

If 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([...]);
}

How to display markdown with embed files and images with helpers has changed

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>

Handle existing data

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
![](local:/data/img/markdown/my_image.jpg)
More text
![](local:/data/img/markdown/my_document.pdf)

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_callbackopen in new window.

New Entity classes (SharpEntity)

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).

New config format

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.

Impact on Validators

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).

Impact on policies

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");
    }
}

Impact on global authorizations

Like for policies, global authorizations were moved to the SharpEntity implementation, and renamed $prohibitedActions (see documentation). Legacy declaration is still handled, except on one point (which was kinda buggy anyway): it's not possible to declare a global authorization to be true, meaning you can only globally forbid actions. The new $prohibitedActions is way clearer on this.

New way of configure the Menu

Sharp 7 introduce a new way of defining the Menu: in a class instead of in the config file. This is documented here, and do note that the Sharp 6 way is still supported.

Changes on testing

Commands should be called via class name

As a result of the Command declaration refactoring, you may now use the full command class name instead of its key to test it:

$this
    ->callSharpInstanceCommandFromList(
        "spaceship", 22, SpaceshipSendMessage::class
    )
    ->assertOk();
Last Updated:
Contributors: philippe, antoine, Arnaud Becher, Philippe Lonchampt