--- url: /docs/9.x/guide.md --- # Getting started ## Terminology, general concept In Sharp, we handle `entities`; an `entity` is simply a data structure which has a meaning in the application context. For instance, a `Person`, a `Post` or an `Order`. In the Eloquent world, for which Sharp is optimized, it's typically a Model — but it's not necessarily a 1-1 relationship, a Sharp `entity` can represent a portion of a Model, or several Models. An instance of an `entity` is simply called an `instance`. Each `entity` in Sharp can be displayed: * in an `Entity List`, which is the list of all the `instances` for this `entity`: with some configuration and code, the user can sort the data, add filters, pagination, and perform searches. From there we also gain access to applicative `commands` applied either to any particular `instance` or to the whole (filtered) list, and to a simple `state` changer (the published state of an Article, for instance). All of that is described below. * In a `Show Page`, optionally, to display an `instance` details. * And in a `Form`, either to update or create a new `instance`. ## Example Let's take a simple example: we want to manage some shop, with 3 obvious entities: `Order`, `Customer` and `Product`. We want to be able to list all the **customers**, to display a detailed view for each of them, and to create or update a customer. That’s an `Entity List` linking to a `Show Page`, linking to a `Form`: For **products**, we decide that we don't need to build a `Show Page`: The product `Entity List` may have filters, sorting columns and search, and an `Entity state` to manage the published state of each product. Finally, **orders** must be listed, detailed and updated, and we also need to manage the **product** list for each order. That's an `Entity List` linking to a `Show Page` which contains another `Entity List`: Maybe we can add an `Entity Command` to export orders in a CSV file in the `Entity List`, and an `Instance command` on the order `Show Page` to declare the order as shipped. This is a simple example to illustrate the main concepts of Sharp: we'll see in this guide how to build such structures but also more complex ones, and how to manage states, commands, dashboards, authorizations, errors, validation... in the process. --- --- url: /docs/9.x/guide/installation.md --- # Installation Sharp 9 needs Laravel 11+ and PHP 8.3+. * Add the package with composer: `composer require code16/sharp` * Then run: `php artisan sharp:install` This last script will publish required assets, create a `SharpServiceProvider` in the `App\Providers` namespace and a `SharpMenu` class in the `App\Sharp` namespace. ## Configuration via a new Service Provider All Sharp behavior is configured in the `App\Providers\SharpServiceProvider` class created by the `sharp:install` command; you can declare your entities in the `configureSharp()` method: ```php use Code16\Sharp\SharpAppServiceProvider; use Code16\Sharp\Config\SharpConfigBuilder; use App\Sharp\SharpMenu; class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setName('My new project') ->setSharpMenu(SharpMenu::class) ->declareEntity(ProductEntity::class); // ... } } ``` ::: tip As shown in the [Entity class](entity-class.md) documentation, you can also let Sharp auto-discover your entities. ::: This `ProductEntity` class could be written like this: ```php class ProductEntity extends SharpEntity { protected string $label = 'Product'; protected ?string $list = ProductList::class; protected ?string $show = ProductShow::class; protected ?string $form = ProductForm::class; protected ?string $policy = ProductPolicy::class; } ``` We chose to define: * a `list` class, responsible for the `Entity List`, * a `show` class, responsible for displaying an `instance` in a `Show Page`, * a `form` class, responsible for the creation and edit `Form`, * and a `policy` class, for authorizations. Almost each one is optional: we could skip the `show` and go straight to the `form` from the `list`, for instance. We'll get into all those classes in this guide. The important thing to notice is that Sharp provides base classes to handle all the wiring (and more), but as we'll see, the applicative code is totally up to you. ::: tip Use the artisan command `php artisan sharp:make:entity` to generate a new entity with all the required classes, or the global one (prompt based) `php artisan sharp:generator`. ::: ## Access to Sharp Once installed, Sharp is accessible via the url `/sharp`, by default. If you wish to change this default value, you'll need to configure a custom segment path: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setCustomUrlSegment('admin') // ... } } ``` --- --- url: /docs/9.x/guide/entity-class.md --- # The entity class An `entity` is simply a data structure which has a meaning in the application context. For instance, a `Person`, a `Post` or an `Order`. It's typically a Model — but it's not necessarily a 1-1 relationship, a Sharp `entity` can represent a portion of a Model, or several Models. The `entity class` is the place where you can declare the entity configuration: its Entity List, Form, Show Page... ## Generator ```bash php artisan sharp:make:entity [--label,--dashboard,--show,--form,--policy,--single] ``` ::: tip The Entity name should be singular, in CamelCase and end with the "Entity" suffix. For instance: `ProductEntity`. ::: ## Write the class The class must extend `Code16\Sharp\Utils\Entities\SharpEntity`. The easiest way to declare your attached classes is to simply override a bunch of protected attributes: ```php class ProductEntity extends SharpEntity { protected string $label = 'Product'; protected ?string $list = ProductList::class; protected ?string $show = ProductShow::class; protected ?string $form = ProductForm::class; } ``` Here is the full list: * `$list`, `$show`, `$form` and `$policy` may be set to a full classname of a corresponding type. The following sections of this documentation describe all this in detail, allowing you to build your Sharp backend. * `string $label` is used in the breadcrumb, as a default ([see the breadcrumb documentation for more on this](sharp-breadcrumb.md)). You can simply put your entity name here. * `bool $isSingle` must be set only if you are dealing [with a single show](single-show.md) * and finally `array $prohibitedActions` can be used to set globally prohibited actions on the entity, [as documented here](entity-authorizations.md). ### Dashboard Dashboard only needs to override one protected attribute: `$view`.\ Note that the class extends `SharpDashboardEntity` instead of `SharpEntity`. ```php class SalesDashboardEntity extends SharpDashboardEntity { protected ?string $view = SalesDashboard::class; } ``` ### Override methods instead If you need more control, you can override these instead of the attributes: ```php protected function getLabel(): string {} protected function getList(): ?SharpEntityList {} protected function getShow(): ?SharpShow {} protected function getForm(): ?SharpForm {} protected function getPolicy(): ?SharpEntityPolicy {} ``` Note that, unlike the `$list`/`$show`/`$form`/`$policy` attributes, these methods return instances, not classnames - resolve the class yourself (e.g. `app($this->list)`) if you still want to store a classname internally. The last one, `getPolicy()`, allows you to return a `SharpEntityPolicy` implementation directly, as it's sometimes easier to declare a quick policy right in here. For example: ```php class MyEntity extends SharpEntity { // ... protected function getPolicy(): ?SharpEntityPolicy { return new class extends SharpEntityPolicy { public function update($user, $instanceId): bool { return $user->isBoss(); } }; } } ``` ### Single shows and forms When you need to configure a "unique" resource that does not fit into a List / Show schema, like an account or a configuration item, you can use a Single Show or Form. This is a dedicated topic, [documented here](single-show.md). ### Handle Multiforms ::: info This feature has been deprecated and was replaced in version 9.6.0 by the [Entity Map](./building-entity-list.md#entity-map) feature. ::: ## Declare the Entity in Sharp configuration The last step is to declare the entity in Sharp, in your `SharpAppServiceProvider` implementation. ### Autodiscovery The easiest way is to let Sharp discover your entities: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setName('My new project') ->discoverEntities(); // ... } } ``` The `discoverEntities()` method will scan the `app_path('Sharp/Entities')` directory for all Entity classes, and declare them in Sharp. You can pass an array of other paths to scan if needed: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setName('My new project') ->discoverEntities([__DIR__ . '../Domain/OtherEntities']); // ... } } ``` Each Entity is keyed by and entity key, used everywhere in Sharp (starting with the URL). When using autodiscovery, the entity key is automatically set to the class name, in kebab-case. For instance, `ProductEntity` will have the entity key `product`. ### Choosing your own entity key If for whatever reason you want to choose your own entity key, you can set it in the entity class: ```php class ProductEntity extends SharpEntity { public static string $entityKey = 'my-product'; // ... } ``` ### Manual declaration If you want to have control over the entity declaration, you can declare them manually instead of using discovery: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setName('My new project') ->declareEntity(ProductEntity::class); // ... } } ``` ### Custom Entity Resolver In some very specific cases, you may want to have full control over the entity declaration, depending on some context. You can use a custom `SharpEntityResolver` to do that. ```php use Code16\Sharp\Utils\Entities\SharpEntityResolver; class MySharpEntityResolver implements SharpEntityResolver { public function entityClassName(string $entityKey): ?string { return match ($entityKey) { 'product' => auth()->user()->isAdmin() ? AdminProductEntity::class : ProductEntity::class, 'order' => OrderEntity::class, // ... }; } } ``` Then, in the ServiceProvider, you can declare the resolver like this: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setName('My new project') ->declareEntityResolver(MySharpEntityResolver::class); // ... } } ``` ::: warning You must remove all `->declareEntity()` calls in order to use `->declareEntityResolver()`. ::: ::: warning If you are using a custom entity resolver, you won’t be able to use the `SharpEntity` classes in the [menu](building-menu.md), or in [`LinkTo` links](link-to.md), or for [Entity List fields](show-fields/entity-list.md): you will have to use the entity key instead. For instance: `LinkToForm::make('products', $id)`. ::: --- --- url: /docs/9.x/guide/building-menu.md --- # Create the main menu The Sharp UI is organized with two menus: the main one on a left sidebar, and the user menu is a dropdown located in the bottom left corner. All links shares common things: an icon, a label and an URL. Links can be grouped in categories. ## Create a SharpMenu class ### Generator ```bash php artisan sharp:make:menu ``` ### Write and declare the class The class must extend `Code16\Sharp\Utils\Menu\SharpMenu`, and define a required `build()` method: ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { // ... } } ``` And should be declared in your SharpServiceProvider: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setSharpMenu(MySharpMenu::class) // ... } } ``` ::: info The `SharpServiceProvider` class is created bye the `sharp:install` artisan command; in case you don't have it, you can create it by yourself in the `App\Providers` namespace, or use the `sharp:make:provider` command. ::: ### Link to an Entity List, a Dashboard or to a single Show Page ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->addEntityLink(PostEntity::class, 'Posts') ->addEntityLink(CategoryEntity::class, 'Categories'); } } ``` In this example, `PostEntity::class` and `CategoryEntity::class` should be `SharpEntity` classes declared in Sharp’s configuration. Sharp will create a link either to the Entity List, to the Dashboard or to a [single Show Page](single-show.md) (depending on the entity configuration). ### Link to an external URL ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this->addExternalLink('https://google.com', 'Some external link'); } } ``` You can open the link in a new tab using the `openInNewTab` parameter: ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this->addExternalLink('https://google.com', 'Some external link', openInNewTab: true); } } ``` ### Define icons Yon can specify a [blade-icons](https://blade-ui-kit.com/blade-icons) name for each link. It can be an icon set coming from a [package](https://github.com/blade-ui-kit/blade-icons?tab=readme-ov-file#icon-packages) or defined in the project config. ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->addEntityLink(PostEntity::class, 'Posts', icon: 'fas-file') ->addEntityLink(DirectoryEntity::class, 'Directories', icon: 'heroicon-o-folder') ->addExternalLink('https://example.org', 'Homepage', icon: 'icon-logo'); // icon defined in the project (e.g. in resources/svg) } } ``` ### Handle notification badges You can display a notification badge on any link, with a count and a tooltip. You can also, optionally, define a tooltip and a link for the badge (usually to a filtered Entity List). Here’s an example of a badge on an Entity List link: ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->addEntityLink( entityKeyOrClassName: PostEntity::class, label: 'Posts', badge: fn () => Post::query()->where('state', 'draft')->count(), badgeTooltip: 'See draft posts', badgeLink: LinkToEntityList::make(PostEntity::class) ->addFilter(StateFilter::class, 'draft'), ); } } ``` ### Group links in sections Sections are groups that can be collapsed ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->addSection('Admin', function (SharpMenuItemSection $section) { $section ->addEntityLink(AccountEntity::class, 'My account') ->addEntityLink(UserEntity::class, 'Sharp users'); }); } } ``` ### Add separators in sections You can add a simple labelled separator in sections: ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->addSection('Admin', function (SharpMenuItemSection $section) { $section ->addEntityLink(AccountEntity::class, 'My account') ->addSeparator('Other users') ->addEntityLink(UserEntity::class, 'Sharp users'); }); } } ``` ### Set a section to be non-collapsible A section is collapsible by default, but you may want to always show it to the user ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->addSection('Admin', function (SharpMenuItemSection $section) { $section ->setCollapsible(false) ->addEntityLink(AccountEntity::class, 'My account'); }); } } ``` ### Hide the menu If for some reason you want to hide the menu, you can do that with the `setVisible()` method: ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->setVisible(auth()->user()->isAdmin()) ->addSection(/*...*/); } } ``` ### Add links in the user (profile) menu Next to user's name or email, Sharp displays a dropdown menu with a logout link. You can add your own links in this menu: ```php class MySharpMenu extends Code16\Sharp\Utils\Menu\SharpMenu { public function build(): self { return $this ->setUserMenu(function (SharpMenuUserMenu $menu) { $menu->addEntityLink(AccountEntity::class, 'My account'); }); } } ``` ### Global menu Filters If you want to display a filter on all pages, above the menu, useful to scope the entire data set (use cases: multi tenant app, customer selector...), you can define a global filter as described in the [Filters documentation](filters.md#global-menu-filters). --- --- url: /docs/9.x/guide/building-entity-list.md --- # Create an Entity List We need an Entity List to display the list of `instances` for an `entity`. This list can be paginated, searchable, filtered, ... as we'll see below. ## Generator ```bash php artisan sharp:make:entity-list [--model=] ``` ::: tip The Entity List name should be singular, in CamelCase and must end with the "List" suffix. For instance: `ProductList`. ::: ## Write the class First let's write the applicative class, and make it extend `Code16\Sharp\EntityList\SharpEntityList`. Therefore, there are two methods to implement: * `buildList(EntityListFieldsContainer $fields)` for the structure, * and `getListData()` for the actual data of the list. There are a two more optional methods, for the list config and instance deletion. Each one is detailed here: ### `buildList(EntityListFieldsContainer $fields)` A field is a column in the `Entity List`. This first function is responsible to describe each column: ```php class ProductList extends SharpEntityList { protected function buildList(EntityListFieldsContainer $fields): void { $fields ->addField( EntityListField::make('name') ->setLabel('Full name') ->setSortable() ->setWidth('50%') ->setHtml() ) ->addField(/* ... */); } // [...] } ``` Setting the label, allowing the column to be sortable and to display html is optional. The optional `->setWidth()` method accepts either an integer (eg: `20` for 20%), a float (eg: `.2` for 20%) or a string (eg: `'20'` or `'20%'`); if missing, it will be deduced (you can use `->setWidthFill()` to force this last behavior). To hide the column on small screens, use `->hideOnSmallScreens()`. ::: warning HTML sanitization is enabled by default for list fields (to prevent XSS attacks when displaying the list). You can disable it by using `->setSanitizeHtml(false)` field method. ::: Sorting columns must be handled in the `getListData()` method, see below. #### Add a badge field The `EntityListBadgeField` allows you to display a badge in the list. It is either a simple dot if the value is `true` or a badge containing the value if it is an integer or a string. ```php class ProductList extends SharpEntityList { protected function buildList(EntityListFieldsContainer $fields): void { $fields ->addField( EntityListBadgeField::make('is_new') ); } } ``` ### `getListData()` Now the real work: grab and return the actual list data. This method must return an array of `instances` of our `entity`. You can do this however you want, so let's see a generic example: The returned array is meant to be built with 2 rules: * each item must define the keys declared in the `buildList()` function, * plus one attribute for the identifier, which is `id` by default (more on that later). So for instance, if we defined 2 columns `name` and `price`: ```php class ProductList extends SharpEntityList { public function getListData(): array|Arrayable { return [ [ 'id' => 1, 'name' => 'Carrot', 'price' => '0.5' ], [ 'id' => 2, 'name' => 'Potato', 'price' => '0.95' ] ]; } // [...] } ``` Of course, real code would imply some data request in a DB, or a file for instance; the important thing is that Sharp don’t care. #### Transformers In a more realistic project, you'll want to transform your data before sending it to the front code. Sharp can help with that, as explained in the detailed [How to transform data](how-to-transform-data.md) documentation. #### Handle query params The EntityList has a valued `$this->queryParams` property. This object will be filled by Sharp with query params: * sorting: `$this->queryParams->sortedBy()` and `$this->queryParams->sortedDir()` * search: `$this->queryParams->hasSearch()` and `$this->queryParams->searchWords()` * filters: `$this->queryParams->filterFor($filter)` If the Entity List was configured to handle sort, filters or search (see below to learn how), and if the user performed such an action, values will be accessible here. You can use the `queryParams` everywhere except in the `buildListConfig()` function. Use cases could be, apart from filtering data: organizing columns, or maybe hiding commands... ##### Sort `$this->queryParams->sortedBy()` contains the name of the attribute, and `$this->queryParams->sortedDir()` the direction: `asc` or `desc`. Note that the ability of sorting a column is defined in `buildList()`. ##### Search `$this->queryParams->hasSearch()` returns true if the user entered a search, and `$this->queryParams->searchWords()` returns an array of search terms. This last method can take parameters, here's its full signature: ```php public function searchWords( $isLike = true, $handleStar = true, $noStarTermPrefix = '%', $noStarTermSuffix = '%' ) ``` * `$isLike`: if true, each term will be surrounded by `%` (by default). * `$handleStar`: if true, and if a char `*` is found in a term, it will be replaced by `%` (default), and this term won't be surrounded by `%` (to allow "starts with" or "ends with" searches). * `$noStarTermPrefix` and `$noStarTermSuffix`: the char to use in a `$isLike` case. Here's a code sample with an Eloquent Model: ```php class ProductList extends SharpEntityList { public function getListData(): array|Arrayable { $products = Product::query(); if ($this->queryParams->hasSearch()) { foreach ($this->queryParams->searchWords() as $word) { $products->where(fn ($query) => $query ->orWhere('name', 'like', $word) ->orWhere('reference', 'like', $word) ); } } return $this->transform($products->paginate(50)); } // ... } ``` ##### Filters A filter is referenced by a `filterKey` and has a `value`. So we can grab this calling `$filterValue = $this->queryParams->filterFor($filterKey)`, and use the value in our query code. #### Pagination It's very common to return in `getListData()` paginated results: return a `Illuminate\Contracts\Pagination\LengthAwarePaginator` or a `Illuminate\Contracts\Pagination\Paginator` in this case. With `Eloquent` or the `QueryBuilder`, this means calling `->paginate($count)` or `simplePaginate($count)` on the query. ### `delete($id): void` Here you might write the code performed on a deletion of the instance. It can be anything, here’s an Eloquent example: ```php class ProductList extends SharpEntityList { function delete($id): void { Product::findOrFail($id)->delete(); } // ... } ``` Deletion is typically an action you perform [in a Show Page](building-show-page.md), but it is also available in the Entity List for convenience. You can configure this action to hide it, and of course leverage specific authorizations (more on this below). ### `buildListConfig()` Finally, this last function must describe the list config. Let's see an example: ```php class ProductList extends SharpEntityList { public function buildListConfig(): void { $this->configureInstanceIdAttribute('id') ->configureSearchable() ->configureDefaultSort('name', 'asc'); } // ... } ``` Here is the full list of available methods: * `configureInstanceIdAttribute(string $instanceIdAttribute)`: define this if the id attribute of an instance is not `id` * `configureReorderable(ReorderHandler|string $reorderHandler)`: allow instances to be rearranged; see [detailed documentation](reordering-instances.md) * `configureSearchable()`: Sharp will display a search text input and process its content to fill `EntityListQueryParams $queryParams` (see above) * `configureDefaultSort(string $sortBy, string $sortDir = "asc")`: `EntityListQueryParams $queryParams` will be filled with this default value (see above) * `configureMultiformAttribute(string $attribute)`: :warning: This feature has been deprecated in version 9.6.0 and was replaced by the [Entity Map](#entity-map) feature. You can still access to the [documentation](multiforms.md) for legacy usage. * `configureEntityMap(string $attribute, EntityListEntities $entities)`: configure an Entity Map to display multiple entities in a single Entity List; [see detailed section](#entity-map) above. * `configureEntityState(string $stateAttribute, $stateHandlerOrClassName)`: add a state toggle, [see detailed doc](entity-states.md) * `configurePrimaryEntityCommand(string $commandKeyOrClassName)`: define an instance command as "primary", by passing its key or full cass name. The command should be declared for this Entity List ([see related doc](commands.md)). * `configureQuickCreationForm(?array $fields = null)`: show the creation form in a modal instead of a full page ([see detailed doc](quick-creation-form.md)) * `configureDelete(bool $hide = false, ?string $confirmationText = null)`: the first argument is to show / hide the delete command on each instance (shown by default); this is only useful to hide the link if you want to only display the delete action in the Show Page (if you have defined one), this is NOT to be used for authorization purpose (see [dedicated documentation on this topic](entity-authorizations.md)). The second argument is the message to display in the confirmation dialog (a sensible default will be used). * `configureCreateButtonLabel(string $label)` to set a custom "New..." button label. ### Display a Page Alert Override `buildPageAlert(PageAlert $pageAlert): void` to display a dynamic message above the list; [see detailed doc](page-alerts.md). ## Declare the Entity List The Entity List must be declared in the correct entity class, as documented here: [Write an entity](entity-class.md)). After this we can access the Entity List at the following URL: **/sharp/s-list/products** (replace "products" by our entity key). To go ahead and learn how to add a link in the Sharp side menu, [look here](building-menu.md). ## Entity Map ::: info This feature replaces the deprecated Multiforms functionality, which remains available for legacy use in version 9.x but will be removed in 10.x. ::: The Entity Map lets you display multiple entities within a single Entity List. This makes it possible to link different Show Pages or Forms based on a discriminating attribute. To set it up, declare the Entity Map in the `buildListConfig()` method by using `configureEntityMap()`: ```php class CarList extends SharpEntityList { // ... public function buildListConfig(): void { $this ->configureEntityMap( attribute: 'engine', entities: EntityListEntities::make() ->addEntity('ice', InternalCombustionEngineCarEntity::class) ->addEntity('ev', ElectricEngineCarEntity::class, 'lucide-plug', 'EV') ); } } ``` The `attribute` parameter defines which attribute will be used to distinguish between entities. If needed, you can compute this value using a [custom transformer](./how-to-transform-data.md). The `entities` parameter expects an instance of `EntityListEntities`, which maps each discriminant value to a specific Entity. You can also specify the icon and label that will be displayed in the "Create" dropdown. Each mapped entity should be declared like any regular Entity, and can include a Show Page, a Form, a Policy, etc. ::: tip It’s not mandatory, but a good practice is to make your “sub-entities” extend the main Entity class to share policies or common logic. For example: ::: ```php class CarEntity extends SharpEntity { protected ?string $list = CarList::class; protected ?string $policy = CarPolicy::class; protected string $label = 'Car'; } ``` ```php class ElectricEngineCarEntity extends CarEntity { protected ?string $form = ElectricEngineCarForm::class; protected string $label = 'EV'; } ``` --- --- url: /docs/9.x/guide/filters.md --- # Filters Filters provide a way for the user to filter list items or dashboard widgets on some attribute; for instance, display only books that cost more than 15 euros. This documentation is written for the Entity List case, but the API is the same for Dashboard (as explained at the end of this page). ## Generator ```bash php artisan sharp:make:entity-list-filter [--required,--multiple,--date-range,--check] ``` ## Write the filter class First, we need to write a class which extends `Code16\Sharp\Filters\SelectFilter`, and therefore declare a `values()` function. This function must return an `[{id} => {label}]` array. For instance, with Eloquent: ```php class ProductCategoryFilter extends SelectFilter { public function values(): array { return ProductCategory::orderBy('label') ->pluck('label', 'id') ->toArray(); } } ``` ## Configure the filter You can implement the optional `buildFilterConfig()` method to configure the filter: ```php class ProductCategoryFilter extends SelectFilter { public function buildFilterConfig(): void { $this->configureLabel('Category') ->configureKey('cat') ->configureRetainInSession(); } // ... } ``` * `configureLabel(string $label)`: use this to define the filter label displayed in the UI. * `configureKey(string $key)`: the default key, meaning the identifier, of a filter is its class name. If you need to change this (which should be a rare case), you can do so with this method. * `configureRetainInSession()`: to keep the filter value in session (see below). ## Declare the filter Next, in the Entity List, we must declare the filter: ```php class ProductEntityList extends SharpEntityList { function getFilters(): ?array { return [ ProductCategoryFilter::class, ]; } // ... } ``` ## Handle filter selection Once the user clicked on a filter, Sharp will call EntityList's `getListData()`; the filter value will be accessible with: * its classname : `$this->queryParams->filterFor(MyFilter::class)` * or its custom key, if defined with `configureKey()`: `$this->queryParams->filterFor('key')` Example: ```php class ProductList extends SharpEntityList { public function getListData(): array|Arrayable { $products = Product::query(); if ($cat = $this->queryParams->filterFor(ProductCategoryFilter::class)) { $products->where('category_id', $cat); } // ... } // ... } ``` ## Multiple filter First, notice that you can have as many filters as you want for an EntityList. The "multiple filter" here designate something else: allowing the user to select more than one value for a filter. To achieve this, make your filter extend `Code16\Sharp\Filters\SelectMultipleFilter`. In this case, with Eloquent for instance, your might have to modify your code to ensure that you have an array (Sharp will return either null, and id or an array of id, depending on the user selection): ```php class ProductList extends SharpEntityList { public function getListData(): array|Arrayable { $products = Product::query(); if ($categories = $this->queryParams->filterFor(ProductCategoriesFilter::class)) { $products->whereIn('category_id', $categories); } // ... } // ... } ``` Note that a filter can't be required AND multiple. ## Date range filter You might find useful to filter list elements on a specific date range. Date range filters enable you to show only data that meets a given time period. To implement such a filter, your filter class must extend `Code16\Sharp\Filters\DateRangeFilter`. Then you need to adjust the query with selected range; in this case, with Eloquent for instance, you might add a condition like: ```php class ProductList extends SharpEntityList { public function getListData(): array|Arrayable; { $products = Product::query(); if ($range = $this->queryParams->filterFor(ProductCreationDateFilter::class)) { $products->whereBetween('created_at', [$range->getStart(), $range->getEnd()]); } // ... } // ... } ``` ### Configuration You can define the date display format (default is `MM-DD-YYYY`, using [Carbon isoFormat() syntax](https://carbon.nesbot.com/docs/#iso-format-available-replacements)) and choose if the week should start on monday (default is sunday). With `configureShowPresets()`, a list of buttons is displayed allowing the user to quickly select a date range. ```php class ProductCreationDateFilter extends DateRangeFilter { public function buildFilterConfig(): void { $this->configureDateFormat("YYYY-MM-DD") ->configureMondayFirst(false) ->configureShowPresets(); } // ... } ``` You can also define specific presets with the `DateRangePreset` class : ```php use Code16\Sharp\Filters\DateRange\DateRangePreset; class ProductCreationDateFilter extends DateRangeFilter { public function buildFilterConfig(): void { $this->configureShowPresets(presets: [ DateRangePreset::make(today()->subDays(3), today(), 'Last 3 days'), DateRangePreset::thisMonth(), ]); } } ``` Following methods are available (default presets) : ```php [ DateRangePreset::today(), DateRangePreset::yesterday(), DateRangePreset::last7days(), DateRangePreset::last30days(), DateRangePreset::last365days(), DateRangePreset::thisMonth(), DateRangePreset::lastMonth(), DateRangePreset::thisYear(), DateRangePreset::lastYear(), ] ``` ## Autocomplete remote filter If you want to use a remote filter, you can use the `Code16\Sharp\Filters\AutocompleteRemoteFilter` class. It is very similar to the `Code16\Sharp\Filters\SelectFilter` class, but it uses a remote endpoint to fetch the values. ```php class ProductCategoryFilter extends AutocompleteRemoteFilter { public function buildFilterConfig(): void { $this ->configureLabel('Category') ->configureDebounceDelay(200) // 300ms per default ->configureSearchMinChars(2); // 1 per default, set 0 to search directly on opening the filter } public function values(string $query): array { return ProductCategory::orderBy('label') ->where('label', 'like', "%$query%") ->pluck('label', 'id') ->toArray(); } public function valueLabelFor(string $id): ?string { return ProductCategory::find($id)?->label; } } ``` The `values()` method must return an `[{id} => {label}]` array. The `valueLabelFor()` method is used to display the label in the dropdown for the selected id. ## Required filters It is sometimes useful to have a filter which can't be null: to achieve this you need to extend the right "Required" subclass (`SelectRequiredFilter` or `DateRangeRequiredFilter`), and define a proper default value. Example for a select filter: ```php class ProductCategoryFilter extends SelectRequiredFilter { public function defaultValue(): mixed { return ProductCategory::orderBy('label')->first()->id; } // ... } ``` Note that a filter can't be required AND multiple. Example for a date range filter: ```php class ProductCreationDateFilter extends DateRangeRequiredFilter { public function defaultValue(): array { return [ 'start' => Carbon::yesterday(), 'end' => Carbon::today(), ]; } // ... } ``` ## Filter search If you want your select filter to be searchable via a search text field, you can use: ```php public function buildFilterConfig(): void { $this->configureSearchable(); } ``` ## Check filter In case of a filter that is just a matter on true / false ("only show admins" for example), just make your filter class extend `Code16\Sharp\Filters\CheckFilter`. ## Master filter In some cases you want to ensure that selecting a filter value will reset all other filters. It's called "master filter". ```php public function buildFilterConfig(): void { $this->configureMaster(); } ``` ## Retained filters value in session If you want to make the filter's value persistent across calls you can leverage the "retain filter" feature. For example, you may have a "country" filter which is common to several Entity Lists: the idea is to keep the user choice even when he changes the current displayed list. ```php public function buildFilterConfig(): void { $this->configureRetainInSession(); } ``` And with that Sharp will keep the filter value in session and ensure it is valued on next requests (if not overridden). This feature works for all types of filters (required, multiple). ::: warning In order to make this feature work, since filters are generalized, you'll need to have unique filters key (the filter class name by default). ::: ## Drop filters depending on functional data Sometimes you may want to hide a filter to the user depending on the actual data, or on other filters values. This can be achieved by using the `hideFilter()` method in your EntityList class, typically in the `getListData()` method. ```php class OrderList extends SharpEntityList { protected function getFilters(): ?array { return [ PaymentMethodFilter::class, OnlinePaymentProviderFilter::class, ]; } public function getListData(): array|Arrayable { if ($this->queryParams->filterFor(PaymentMethodFilter::class) !== 'online') { // No need to show the OnlinePaymentProviderFilter $this->hideFilter(OnlinePaymentProviderFilter::class); } // ... } // ... } ``` ## Filters for Dashboards [Dashboards](building-dashboard.md) also can take advantage of filters; the API the same, but base classes are specific: `Code16\Sharp\Filters\SelectFilter`, `Code16\Sharp\Filters\DateRangeFilter`,`Code16\Sharp\Filters\CheckFilter` and so on. ## Global menu Filters You may want to "scope" the entire data set: an example of this could be a user which can manage several organizations. Instead of adding a filter on almost every Entity List, in this case, you can define a global filter, which will appear on top of the global menu. To achieve this, first write the filter class, like any filter, except it must extend `\Code16\Sharp\Filters\GlobalRequiredFilter` — meaning it must be a required filter. ```php class OrganizationGlobalFilter extends GlobalRequiredFilter { public function values(): array { return Corporation::orderBy('name') ->pluck('name', 'id') ->all(); } public function defaultValue(): mixed { return Corporation::first()->id; } public function authorize(): bool { // Optional: you can define an authorization logic here return true; } } ``` And then, we declare it: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->addGlobalFilter(OrganizationGlobalFilter::class) // ... } } ``` Finally, to get the actual value of the filter on your Entity List, Show Page or Form classes, you must use the context: ```php sharp()->context()->globalFilterValue(OrganizationGlobalFilter::class) ``` The usage of Sharp Context is [detailed here](context.md). --- --- url: /docs/9.x/guide/entity-states.md --- # Entity States Entity states are a bit of sugar to easily propose a state management on entities. It could be a simple "draft / publish" state for a page, or something more advanced with many states for an order for instance. ## Generator ```bash php artisan sharp:make:entity-state [--model=] ``` ## Write the Entity state class First, you'll have to write a class that extends the `Code16\Sharp\EntityList\Commands\EntityState` abstract class. You'll have to implement two functions: `buildStates()` and `updateState($instanceId, $stateId)`. ### Build the states The goal is to declare the available states for the entity, using `$this->addState()`: ```php class ProductState extends EntityState { protected function buildStates(): void { $this->addState('active', 'Active', 'green') ->addState('inactive', 'Retired', 'orange') ->addState('coming', 'Coming soon', '#ddd'); } // ... } ``` `$this->addState()` takes 3 parameters: * a key identifying the state, * the state label as shown to the user, * a color to display. For the color, you may indicate anything that the browser would understand (an HTML color name or a hexadecimal value). ### Update a state When the user clicks on a state to update it, the `updateState()` method is called. ```php class ProductState extends EntityState { public function updateState($instanceId, $stateId): array { Product::findOrFail($instanceId) ->update(['state' => $stateId]); return $this->refresh($instanceId); } // ... } ``` About the `return $this->refresh($instanceId);`: Entity states can return either a `refresh` or a `reload` (as described in the [Commands documentation](commands.md)), but if omitted the refresh of the `$instanceId` is the default (meaning in the code sample above this line can be removed). ## Configure the state Once the Entity state class is defined, we have to add it in the Entity List or in the Show Page config: ```php class ProductList extends SharpEntityList { function buildListConfig(): void { $this->configureEntityState('state', ProductState::class); } // ... } ``` The first parameter is a key which should be the name of the attribute. ## Display the state The state will be displayed in the top section of the Show Page (if you have one). In the Entity List, it will be displayed in a new column at the end of the list, unless you have declared a specific column (in this case, you can choose where to place it): ```php class ProductList extends SharpEntityList { protected function buildList(EntityListFieldsContainer $fields): void { $fields ->addField(EntityListField::make('title')->setLabel('Title')) ->addField(EntityListStateField::make()->setLabel('State')) ->addField(/* ... */); } // ... } ``` ## Authorizations Entity states can declare an authorization check very much like Instance Commands: ```php class ProductState extends EntityState { public function authorizeFor($instanceId): bool { return Product::findOrFail($instanceId)->owner_id == auth()->id(); } // ... } ``` --- --- url: /docs/9.x/guide/reordering-instances.md --- # Reordering instances Allow the user to rearrange instances in the Entity List. ## Generator Command ```bash php artisan sharp:make:reorder-handler [--model=] ``` ## Write the class First, we need to write a class for the reordering itself, which must implement `Code16\Sharp\EntityList\Commands\ReorderHandler`, and therefore the `reorder(array $ids)` function. Here's an example with Eloquent and a numerical `order` column: ```php class PageReorderHandler implements ReorderHandler { function reorder(array $ids) { Page::whereIn('id', $ids) ->get() ->each(function (Page $page) use ($ids) { $page->order = array_search($page->id, $ids) + 1; $page->save(); }); } } ``` ::: tip This simple implementation could be replaced using the `SimpleEloquentReorderHandler` class, see below. ::: ## Configure reorder for the front-end Then, in your Entity List you have to configure your reorder handler: ```php class PageList extends SharpEntityList { public function buildListConfig(): void { $this->configureReorderable(new PageReorderHandler()); } // ... } ``` And that’s it, the list now presents a "Reorder" button, and your code will be called when needed. ::: tip Note that you can also pass a ReorderHandler classname, or an anonymous class that extends ReorderHandler, to the `configureReorderable()` method. ::: ## Authorizations The reorder action depends on the `reorder` permission. You can define it in the [Entity Policy](entity-authorizations.md): Sometimes you may need to restrict the reorder action depending on the actual data, or on some filters values. This can be achieved by using the `disableReorder()` method in your EntityList class, typically in the `getListData()` method. ```php class PostList extends SharpEntityList { public function buildListConfig(): void { $this->configureReorderable(new PostReorderHandler()); } public function getListData(): array|Arrayable { // We can’t reorder if there is a search $this->disableReorder($this->queryParams->hasSearch()); // ... } // ... } ``` ## Handle exceptions If you need to abort the process, for any reason, you can raise a `Code16\Sharp\Exceptions\SharpException\SharpApplicativeException` in the `reorder(array $ids)` function. ## Use the default Eloquent implementation A common pattern with an Eloquent model is to simply define an `order` attribute. In this simple case, you can leverage a default implementation built in Sharp: ```php class PageList extends SharpEntityList { public function buildListConfig(): void { $this->configureReorderable(new SimpleEloquentReorderHandler(MyModel::class)); } // ... } ``` The `Code16\Sharp\EntityList\Eloquent\SimpleEloquentReorderHandler` class expects the full classname of the Eloquent Model to reorder, and will use the `id` and `order` attribute by default. You can change this default behavior with the dedicated methods: ```php class PageList extends SharpEntityList { public function buildListConfig(): void { $this->configureReorderable( (new SimpleEloquentReorderHandler(MyModel::class)) ->setIdAttribute('uuid') ->setOrderAttribute('position') ); } // ... } ``` --- --- url: /docs/9.x/guide/avoid-n1-queries-in-entity-lists.md --- # Avoid n+1 queries in Entity Lists How can we leverage the built-in cache to avoid to query the same instance multiple times per request? ## The problem Every row of an Entity List is an instance (typically a Model) which can define multiple Instance Commands and be constrained by a Policy; and Sharp will run the Policy for every instance (to check if the use is allowed to view, update and delete it), and even worse: execute the authorization logic for every Instance Command times every instance. Let’s consider this simple example: a Post entity with a `PostPolicy` and a `PreviewPostCommand`: ```php class PostList extends SharpEntityList { public function getInstanceCommands(): ?array { return [ PreviewPostCommand::class, ]; } public function getListData(): array|Arrayable { $posts = Post::select('posts.*') ->with('author', 'categories', 'cover') // don't forget to eager load used relations ->paginate(20); return $this->transform($posts); } // ... } ``` ```php class PostPolicy extends SharpEntityPolicy { public function update($user, $instanceId): bool { if ($user->isAdmin()) { return true; } return Post::find($instanceId)->author_id === auth()->id(); } } ``` ```php class PreviewPostCommand extends InstanceCommand { public function authorizeFor(mixed $instanceId): bool { if (auth()->user()->isAdmin()) { return true; } return Post::find($instanceId)->isOnline(); } // ... } ``` Nothing fancy here: if we are an admin, we can update and preview any post; if we are not, we can only preview online posts and update our posts. Notice that this is written in a way which prevents any query to be executed in the "admin" case, which is a good thing for performance; but it can hide the fact that all non-admins will face a different case: they will query the same instance twice per row + one time for the list query itself. ## Leveraging Sharp’s instances list cache to avoid this Here’s how we can rewrite the `PostPolicy` to avoid this: ```php class PostPolicy extends SharpEntityPolicy { public function update($user, $instanceId): bool { if ($user->isAdmin()) { return true; } return sharp()->context() ->findListInstance($instanceId, fn($instanceId) => Post::find($instanceId)) ->author_id === auth()->id(); } } ``` The same should be done for the `PreviewPostCommand`. This `findListInstance()` method in the `sharp()->context()` helper class (see [context documentation](context.md)) will retrieve the instance from a cache that was automatically set by Sharp when calling `$this->transform($posts)`, in `PostList::getListData()`. The `findListInstance()` takes a second argument: this is a callback that will be called only if the instance is not already in the cache, passing the instance id as parameter. This Closure must return the instance. ::: info Note that the cache set is automatic if you use the standard `->transform()` method. In case you don’t, you can still set it manually calling `sharp()->context()->cacheListInstances(?Collection $instances)`. ::: With this quite simple trick you can avoid a lot of useless queries and improve the performance of your Entity Lists. --- --- url: /docs/9.x/guide/building-form.md --- # Create a Form Forms as used to create or update instances. ## Generator ```bash php artisan sharp:make:form [--model=,--single] ``` ::: tip The Form name should be singular, in CamelCase and must end with the "Form" suffix. For instance: `ProductForm`. ::: ## Write the class As usual in Sharp, we begin by creating a class dedicated to our Form and make it extend `Code16\Sharp\Form\SharpForm`; and we'll have to implement at least 4 functions: * `buildFormFields(FieldsContainer $formFields)` to declare fields, * `buildFormLayout(FormLayout $formLayout)` to handle fields layout, * `find($id): array` to get the instance data, * `update($id, array $data)` to update the instance. Let's see the specifics: ### `buildFormFields(FieldsContainer $formFields)` In short, this method is meant to host the code responsible for the declaration and configuration of each form field. This must be done calling `$formFields->addField`: ```php class ProductForm extends SharpForm { // ... public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormTextField::make('name') ->setLabel('Name') ) ->addField( SharpFormTextField::make('capacity') ->setLabel('Full capacity (x1000)') ); } } ``` As we can see in this simple example, we defined two text fields giving them a mandatory `key` and an optional label. #### Form fields shared attributes Every field has the optional following setters: * `setLabel(string $label)` for the field label displayed above it * `setHelpMessage(string $helpMessage)` to add a help text below the field * `setReadOnly(bool $readOnly = true)` * `setExtraStyle(string $style)`: the CSS style will be added in a `style` attribute In addition, all text fields have one more generic setter: * `setPlaceholder(string $placeholder)` #### Conditional display The idea is to hide or show a field (referred as "secondary") depending on some other field (referred as "main") value. To do that, use the `addConditionalDisplay(string $fieldKey, $values = true)` setter giving: * the main `$fieldKey`, which should refer to either a Check, Select, Tags or Autocomplete field, * the `$values` of the main field for which the secondary field must be visible. You can put there a boolean for a Check master field, and for other fields (Select, Tags, Autocomplete), either: * a string value, like for instance `'red'`: the slave field is visible only when the main field value is "red" * a string value with a negation mark as the first char, like `'!red'`: the secondary field is visible only when the main field value is NOT "red" * an array of values: `['red', 'blue']`. The secondary field is visible only when the main field value is either "red" or "blue". You can add multiple conditional display rules, chaining calls to `addConditionalDisplay(string $fieldKey, $values = true)`. In this case, all conditions will be linked with a `AND` operator by default (meaning all conditions must be verified to display the secondary field), but this can be switched to an `OR` easily with `setConditionalDisplayOrOperator()` (and back with `setConditionalDisplayAndOperator()`). #### Formatters Every field is linked to a Formatter, which defines the way data is formatted right before sending it to the front (last step, after transformers) and right after reception from the front (first step, before transformers). Sharp provides a Formatter implementation per field type, but you can override this using the `setFormatter($formatter)` setter, providing a `Code16\Sharp\Form\Fields\Formatters\SharpFieldFormatter` implementation. #### Form fields specific attributes For the specifics of each field, here's the full list and documentation: * [Text](form-fields/text.md) * [Textarea](form-fields/textarea.md) * [Editor (rich text rendered as Markdown or HTML)](form-fields/editor.md) * [Number](form-fields/number.md) * [Html](form-fields/html.md) * [Check](form-fields/check.md) * [Date](form-fields/date.md) * [Upload](form-fields/upload.md) * [Select](form-fields/select.md) * [Autocomplete](form-fields/autocomplete.md) * [Tags](form-fields/tags.md) * [List](form-fields/list.md) * [AutocompleteList](form-fields/autocomplete-list.md) * [Geolocation](form-fields/geolocation.md) ### `buildFormLayout(FormLayout $formLayout)` Now let's build the form layout. A form layout is made of `columns`, which contains `fields`, `lists` of fields and `fieldsets`. If needed, we can even define `tabs` above `columns`. #### Columns and fields Here's how we can define the layout for the simple two-fields form we built above: ```php class ProductForm extends SharpForm { // ... public function buildFormLayout(FormLayout $formLayout): void { $formLayout->addColumn(6, function (FormLayoutColumn $column) { $column->withField('name') ->withField('capacity'); }); } } ``` This will result in a 50% column (columns width are 12-based, like in Entity Lists) with the 2 fields in separate rows. Note that fields are referenced with their key, previously defined in `buildFormFields()`. Here's another possible layout, with two unequally large columns: ```php class ProductForm extends SharpForm { // ... public function buildFormLayout(FormLayout $formLayout): void { $formLayout ->addColumn(7, function (FormLayoutColumn $column) { $column->withField('name'); }) ->addColumn(5, function (FormLayoutColumn $column) { $column->withField('capacity'); }); } } ``` ##### Displaying fields on the same row Here’s how to put fields side by side on the same row, using the `withFields()` (notice the final S) method: ```php class ProductForm extends SharpForm { // ... public function buildFormLayout(FormLayout $formLayout): void { $formLayout->addColumn(6, function (FormLayoutColumn $column) { $column->withFields('name', 'capacity'); }); } } ``` This will align the two fields on the row. They'll have the same width (50%), but we can act on this referencing a 12-based grid system with either variadic arguments: ```php class ProductForm extends SharpForm { // ... public function buildFormLayout(FormLayout $formLayout): void { $formLayout->addColumn(6, function (FormLayoutColumn $column) { $column->withFields(name: 8, capacity: 4); }); } } ``` ... or using the special `|` character instead: ```php class ProductForm extends SharpForm { // ... public function buildFormLayout(FormLayout $formLayout): void { $formLayout->addColumn(6, function (FormLayoutColumn $column) { $column->withFields('name|8', 'capacity|4'); }); } } ``` #### Fieldsets Fieldsets are useful to group some fields in a labelled block. Here's how they work: ```php $formLayout->addColumn(6, function (FormLayoutColumn $column) { $column->withFieldset('Details', function (FormLayoutFieldset $fieldset) { return $fieldset ->withField('name') ->withField('capacity'); }); }); ``` "Details" is here the legend of the fieldset. #### Lists of fields In a `List` case, which is a form fields container [documented here](form-fields/list.md), we have to describe the list item layout, using `->withListField()` and passing a Closure as second argument: ```php $column->withListField('pictures', function (FormLayoutColumn $listItem) { $listItem ->withField('file') ->withField('legend'); }); ``` #### Conditions Since layout classes apply Laravel’s `Conditionable` trait, you can use the `when()` method to conditionally display a column: ```php $column ->withField('title') ->when(sharp()->context()->isUpdate(), function (FormLayoutColumn $column) { $column->withField('author'); }); ``` #### Tabs Finally, columns can be wrapped in tabs if the form needs to be in parts: ```php $formLayout ->addTab('tab 1', function (FormLayoutTab $tab) { $tab->addColumn(6, function (FormLayoutColumn $column) { $column->withField('name'); // ... }); }) ->addTab([...]) ``` The tab will here be labelled "tab 1". ### `find($id): array` Next, we have to write the code responsible for the instance data (in an update case). The method must return a key-value array: ```php class ProductForm extends SharpForm { // ... public function find($id): array { return [ 'name' => 'USS Enterprise', 'capacity' => 3000 ]; } } ``` As for the Entity List, you'll want to transform your data before sending it. Transformers are explained in the detailed [How to transform data](how-to-transform-data.md) documentation. ### `update($id, array $data)` Well, this is the core: how to write the actual update code. #### Form field format Before going into the details, please note that the `$data` array contains the per-field formatted data: depending on the type of SharpFormField you used, the structure may change. For instance, a `SharpFormEditorField` content will be formatted as an array with a `text` attribute for the full text and an optional `fields` attribute with embedded fields (see the Editor field documentation for more details). Sharp will use this format step to perform some tasks: move or copy uploaded files, handle image transformation, ... Note that you can override the formatter of a specific field as explained above in the `buildFormFields()` section. Now let's review two cases: #### General case: you are on your own If you are not using Eloquent (and maybe no database at all), you'll have to do it manually. Remember: Sharp aims to be as permissive as possible. So just write the code to update the instance designated by `$id` with the values in the formatted `$data` array. #### Eloquent case (where the magic happens) Sharp also aims to help the applicative code to be as small as possible, and if you're using Eloquent, you can import a dedicated trait: `Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater`. And then, write this kind of code: ```php class ProductForm extends SharpForm { // ... public function update($id, array $data) { $instance = $id ? Product::findOrFail($id) : new Product; $this ->setCustomTransformer('price', fn ($price) => $price / 100) ->ignore('comment') ->save($instance, $data); } } ``` We first define a custom transformer (see [detailed documentation](how-to-transform-data.md)). Then we decide for some reason to bypass the automatic save process for the `comment` attribute. This `ignore()` function can be called with an array as well. You'll probably do whatever is necessary for this field after the `save()` call. Finally, we call `$this->save()` with the instance and the sent data. This method will do all the persisting code for you, handling if needed related models (for lists, tags, selects, ...), with any relation allowed by Eloquent (hasMany, belongsToMany, morphMany, ...). #### Handle applicative exceptions In the `update($id, array $data)` method you may want to throw an exception on a special case, other than validation (which is explained below). Here's how to do that: ```php class ProductForm extends SharpForm { // ... public function update($id, array $data) { // ... if($sometingIsWrong) { throw new SharpApplicativeException('Something is wrong'); } // ... } } ``` The message will be displayed to the user. #### Return the instance id This method must return the id of the updated or stored instance. #### Display notifications Sometimes you'll want to display a message to the user, after a creation or an update. Sharp way to do this is to call `->notify()` in the Form code: ```php class ProductForm extends SharpForm { // ... public function update($id, array $data) { $instance = $id ? Product::findOrFail($id) : new Product; $this->save($instance, $data); $this->notify('Product was indeed updated.') ->setDetail('As you asked.') ->setLevelSuccess() ->setAutoHide(false); return $instance->id; } } ``` A notification is made of a title, and optionally * a text detail, * a notification level: info (the default), warning, danger, success, * an auto-hide policy (if true, the toasted notification will hide after 4s). The notification will be displayed on the next screen, which is the Entity List. Note that you can add up notifications, calling the `notify()` function multiple times (which is useful to sometimes add a second notification, based on actual form data). ### `create(): array` This method **is not mandatory**, a default implementation is proposed by Sharp, but you can override it if necessary. The aim is to return an array version of a new instance (for the creation form). For instance, with Eloquent and the `Code16\Sharp\Utils\Transformers\SharpAttributeTransformer` trait: ```php class ProductForm extends SharpForm { // ... public function create(): array { return $this->transform(new Product(['name' => 'new'])); } } ``` ### `buildFormConfig(): void` This method, entirely optional, is the place to configure these: * `configureBreadcrumbCustomLabelAttribute(string $attribute)` to declare the attribute used by the breadcrumb (see [breadcrumb documentation](sharp-breadcrumb.md)). * `configureDisplayShowPageAfterCreation(bool $displayShowPage = true)` to tell Sharp to redirect to the entity Show Page (instead of the EntityList) after the store. No existence check is done here, meaning if there is no Show Page configured it will end up in a 404. Example ```php class ProductForm extends SharpForm { // ... public function buildFormConfig(): void { $this->configureBreadcrumbCustomLabelAttribute('name') ->configureDisplayShowPageAfterCreation(); } } ``` ### Display a Page Alert Override `buildPageAlert(PageAlert $pageAlert): void` to display a dynamic message above the Form; [see detailed doc](page-alerts.md). ## Input validation In order to have an input validation on your form, you can either declare a `rules()` methode (and an optional `messages()` one): ```php class ProductForm extends SharpForm { // ... public function rules(array $formattedData): array { return [ 'name' => 'required', 'price' => ['required', 'numeric'], ]; } public function messages(array $formattedData): array { return [ 'price.numeric' => 'The price must be a number', ]; } } ``` ::: tip The `$formattedData` argument is optional, but can be useful if you need to validate a field based on another one. If you don’t need it, you can safely remove it from the method argument list. ::: Or you can manually call `->validate()` in the `update()` method: ```php class ProductForm extends SharpForm { // ... public function update($id, array $data) { $this->validate($data, [ 'name' => 'required', 'price' => ['required', 'numeric'], ]); } } ``` Sharp will handle the error display in the form. ## Declare the form The Form must be declared in the correct entity class, as documented here: [Write an entity](entity-class.md)). --- --- url: /docs/9.x/guide/single-form.md --- # Using Single Form for unique resources Sometimes you will need to configure a "unique" resource that does not fit into a List / Form schema, like for example an account, or a configuration item. Single Forms are the natural companions for Single Shows, [documented here](single-show.md). ## Write the class Instead of extending `SharpForm`, our SingleForm implementation should extend `Code16\Sharp\Form\SharpSingleForm`. We still have to implement `buildFormFields(FieldsContainer $formFields)` and `buildFormLayout(FormLayout $formLayout)` to declare the fields presenting the instance, but other methods are a bit different. First, `find()` and `update()` don't need any `$instanceId` parameter: * `findSingle(): array` * `updateSingle(array $data)` ### Full example Let's write a Single Form for the current User, where he can update its name and email (using `WithSharpFormEloquentUpdater` here as this example uses Eloquent): ```php class AccountSharpForm extends SharpSingleForm { use WithSharpFormEloquentUpdater; function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormTextField::make('name') ->setLabel('Name') ) ->addField( SharpFormTextField::make('email') ->setLabel('Email address') ); } function buildFormLayout(FormLayout $formLayout): void { $formLayout->addColumn(6, function ($column) { return $column ->withField('name') ->withField('email'); }); } protected function findSingle() { return $this->transform( User::findOrFail(auth()->id()) ); } protected function updateSingle(array $data) { return $this->save( User::findOrFail(auth()->id()), $data )->id; } } ``` ## How to declare it? Like said before, Single Forms will only work in pair with a Single Show; please refer [to this documentation](single-show.md#single-show-declaration) to find out how to declare a single show and form. --- --- url: /docs/9.x/guide/quick-creation-form.md --- # Quick creation form Sometimes you may want to allow the creation of a new instance directly from the list page, without having to navigate to a dedicated creation form. It's especially useful when the create form does not require a lot of fields, to keep the user in the list context — and since Sharp will display a “Create and create another” button in the modal, the user can quickly create many instances. ## Prerequisites This feature will only work if a Form is defined for the entity (since Sharp will entirely rely on it). ## Configuration The configuration is done in the Entity List: ```php class MyList extends SharpEntityList { public function buildListConfig(): void { $this->configureQuickCreationForm(); } // ... } ``` With this, when the user clicks on the "New..." button, a modal will open with the form fields defined in the Form. One common practice is to limit the fields to the strict minimum: this can be achieved by passing an array of field keys to the `configureQuickCreationForm` method: ```php class MyList extends SharpEntityList { public function buildListConfig(): void { $this->configureQuickCreationForm(['name', 'price']); } // ... } ``` Of course, ensure that these fields are defined in the Form and that all the required fields are present. ::: warning The quick creation form is designed for simple forms. In particular, the layout is entirely ignored, as fields are simply placed one below another. If your form contains many fields or require a specific layout, it is better to use the regular creation form. ::: ## Redirect to the Show Page When the Form is configured with `configureDisplayShowPageAfterCreation()`, and if the user does not choose to stay in creation (with the "submit and reopen" button), Sharp will redirect to the Show Page after the creation. --- --- url: /docs/9.x/guide/form-editor-embeds.md --- # Write an Embed for the Editor field Form's [Editor field](form-fields/editor.md) is a full-featured wysiwyg / markdown field which can contain images, files, and user defined embeds; tou should first look at the Editor field documentation to understand the concept, since here's the details on how to write an Embed class. ## How does an Embed work? First a quick presentation: an embed is not a structured data, meant to be stored in a database or to be represented by a Model. I will be stored in the content of an Editor text, as a custom HTML tag, with attributes. Here's an example of how a `RelatedPostEmbed` can be presented in the Editor field: Here's how it can be edited, in Sharp: And here's how it could be stored, as a reference: ```html

She was aware that things could go wrong. [...]

``` The purpose of this class is to define all this: how to present, edit and store the embed. ## Write the class ### Required methods The class must extend `Code16\Sharp\Form\Fields\Embeds\SharpFormEditorEmbed`; you'll have to implement at least these two methods: ### `buildFormFields(FieldsContainer $formFields): void` Here you can declare the form fields of the embed; the API is the same as building a standard Form (see [Building an Entity Form](building-form.md)). This form will appear in a modal when the use creates a new embed, or clicks in the edit button of an existing one. ::: tip You can choose to name one (and only one) field `slot`: it will be stored as the component content, rather than in an attribute. This could be easier to handle complex data (an Editor HTML text for instance) this way, in the public site, where you can use the standard `{{ $slot }}` attribute to display it. ::: ### `updateContent(array $data = []): array` This method is called on posting the form. Here you should validate the input if needed, and return the data. ```php public function updateContent(array $data = []): array { $this->validate($data, [ 'post' => [ 'required', Rule::exists('posts', 'id') ], ]); return $data; } ``` ### Configure the embed This is not required, but you should implement `buildEmbedConfig(): void`, where you can call: * `configureLabel(string $label): self`: to define the name of the embed (should be short) * `configureTagName(string $tagName): self`: to define the tag name (typically starting with `x-`) * `configureTemplate(string|View $template): self`: to define the blade as inline string or as a `view('my-template')` for both show & form. If you want to specify different templates between show & form you can use following methods : * `configureShowTemplate(string|View $template): self` * `configureFormTemplate(string|View $template): self` * `configureIcon(string $icon): self`: to define an icon used when the embed is placed in the toolbar. The icon is also displayed in the embed header. * `configureDisplayEmbedHeader(bool $display = true, ?string $title = null): self`: to hide the default embed header. The title is the label defined in `configureLabel()` but it can be overridden here. Here's a complete example: ```php public function buildEmbedConfig(): void { $this ->configureLabel('Related Post') ->configureTagName('x-related-post') ->configureTemplate(<<<'HTML'
@if($online) @else @endif {{ $title }}
HTML); } ``` ### Additional useful methods Two more methods can be implemented, in case you need more control of data transformation: ### `transformDataForTemplate(array $data, bool $isForm): array` This method is called before the template rendering. This is where you have a chance to format data for the template, which could even mean to make a DB query, in some cases. Here's an example, matching the template seen above: ```php public function transformDataForTemplate(array $data, bool $isForm): array { $post = Post::find($data['post']); return $this ->setCustomTransformer('title', function ($value) use ($post) { return $post?->title; }) ->setCustomTransformer('online', function ($value) use ($post) { return $post?->state === 'online'; }) ->transformForTemplate($data); } ``` The embed data is simply `post`, which is an id. So we find the related post, and return attributes needed by the template, leveraging Sharp's transformation API (see [how to transform data](how-to-transform-data.md)) — but we could instead directly build and return an array, as always. ::: warning There is a catch on transformation: instead of simply using `->transform()`, we used `->transformForTemplate()`: although this is not needed in all cases, this will ensure that field formatters are not called, since this could lead to unwanted transformation of the templates. ::: Notice the `$isForm` param, which allows differentiating data depending on the context (Form or Show). ### `transformDataForFormFields(array $data): array` Similarly, this method is called to transform the data before displaying the form. This can be required in case your form includes fields like autocompletes, or uploads. You can refer once again to the documentation on [how to transform data](how-to-transform-data.md), and this time be sure to use the regular `->transform()` method, since data needs to be formatted for fields. ## Configure the fields in Form and Show At this stage, the only remaining step is to declare the embed in the related fields, meaning: * in the Form `SharpFormEditorField`, where it would be inserted / edited * and maybe in a Show `SharpShowTextField`, if the embed should be presented in a show page As this is detailed in documentations of these two fields, the way to achieve this is to call `allowEmbeds`: ```php // In a Form public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormEditorField::make('content') ->allowEmbeds([ RelatedPostEmbed::class, ]) ); // [...] } ``` ```php // In a Show protected function buildShowFields(FieldsContainer $showFields): void { $showFields ->addField( SharpShowTextField::make('content') ->allowEmbeds([ RelatedPostEmbed::class, ]) ); // [...] } ``` ## Display the embed in the public section The embed should be treated like any regular Laravel blade component. Here's an example: ```blade @props([ 'post', ]) @if($post = \App\Models\Post::find($post))

Related post

{{ $post->title }}

{{ Str::limit(strip_tags($post->content), 200) }}
@endif ``` ## Security If you add a `slot` field to the embed, the text of the slot is **not sanitized**. If you use a textarea or text field, you will have to call `->setSanitizeHtml()` on those. --- --- url: /docs/9.x/guide/multiforms.md --- # Multi-Forms (deprecated) ::: warning Multi-Forms is a feature that has been deprecated in version 9.6.0 and was replaced by the [Entity Map](./building-entity-list.md#entity-map) feature. ::: Let's say you want to handle different variants for an entity in one Entity List. For instance, maybe you want to display sold cars on an Entity List: easy enough, you create a `Car` entity, list and form. But you want to handle different form fields for cars with an internal combustion engine and those with an electric engine; you can of course use a form and [conditional display](building-form.md#conditional-display) to achieve this, but in a case where there are many differences, the best option may be to split the Entity in two (or more) Forms. That's Multi-Form. ## Write the Form classes Following up the car example, we would write two Form classes: `CombustionCarForm` and `ElectricCarForm`, maybe. They are regular `SharpForm` classes, as [described here](building-form.md). ::: tip Note that You'll probably be able to regroup some common code in a trait or by inheritance: it's up to you. ::: ## Configuration Once the classes are written, you must declare the forms in the entity class: ```php class CarEntity extends SharpEntity { protected ?string $list = CarList::class; protected string $label = 'Car'; public function getMultiforms(): array { return [ 'combustion' => [\App\Sharp\CombustionCarForm::class, 'Combustion car'], 'electric' => [\App\Sharp\ElectricCarForm::class, 'Electric car'], ]; } } ``` The expected return of the `getMultiforms()` method is an array with: * the subentity key as key: this is the value of the split attribute, to disambiguate each type (see below), * and, as value, an array with the Form class and the subentity label. At this stage, you need only one more thing: configure the Entity List to handle Multi-Form. ## The Entity List Now we want to "merge" our Car entity in the Entity List, and allow the user to create or edit either a combustion or an electric car. With the configuration added to the entity class, at the previous step, we already have a dropdown button replacing the "New" button, each value leading to the right Form. You must configure an instance attribute to disambiguate each type: each instance must have his attribute valuated either with "electric" or "combustion", in our example. You declare this attribute in the Entity List `buildListConfig()` method: ```php class CarList extends SharpEntityList { // [...] function buildListConfig(): void { $this->configureMultiformAttribute('engine'); } } ``` Here, the `engine` attribute must be filled for each Car instance. So how you do that? Obviously, the first way is to keep the same attribute you use in your database: in many cases, you already have this `engine` value in a column. If not, or if the value is something less readable (an ID for instance), use a [custom transformer](how-to-transform-data.md): ```php class CarList extends SharpEntityList { // [...] function getListData(): array { return $this ->setCustomTransformer('engine', function($value, Car $car) { return $car->motor === 'EV' ? 'electric' : 'combustion'; }) ->transform(Car::get()); } } ``` --- --- url: /docs/9.x/guide/form-fields/text.md --- # Text Class: `Code16\Sharp\Form\Fields\SharpFormTextField` ## Configuration ### `setInputTypeText()` Used to set the type to regular `text` (the default). ### `setInputTypePassword()` Used to set the type to `password`. An "eye button" is displayed to show / hide the input value. ### `setInputTypeEmail()` Used to set the type to `email`. ### `setInputTypeTel()` Used to set the type to `tel`. ### `setInputTypeUrl()` Used to set the type to `url`. ### `setMaxLength(int $maxLength)` Set a max character count. ### `setMaxLengthUnlimited()` Unset the max character count. ### `setSanitizeHtml()` Enable HTML sanitization (to prevent XSS attacks if this field data is used as raw HTML). ## Formatter * `toFront`: expect a string. * `fromFront`: returns a string. --- --- url: /docs/9.x/guide/form-fields/textarea.md --- # Textarea Class: `Code16\Sharp\Form\Fields\SharpFormTextareaField` ## Configuration ### `setRowCount(int $rows)` Used to set the textarea row count. ### `setMaxLength(int $maxLength)` Set a max character count. ### `setMaxLengthUnlimited()` Unset the max character count. ### `setSanitizeHtml()` Enable HTML sanitization (to prevent XSS attacks if this field data is used as raw HTML). ## Formatter * `toFront`: expect a string. * `fromFront`: returns a string. --- --- url: /docs/9.x/guide/form-fields/editor.md --- # Editor This form field is a rich text editor, with formatting and an optional toolbar. Class: `Code16\Sharp\Form\Fields\SharpFormEditorField` ## Configuration ### `setHeight(int $height, int|null $maxHeight = null)` Set the textarea height, in pixels.\ If `$maxHeight` is set, the field will auto-grow until: * the indicated height in pixels * on infinitely if set to `0` ### `showToolbar()` ### `hideToolbar()` Show or hide the toolbar (shown by default). ### `setToolbar(array $toolbar)` Override the default toolbar, providing an array built with `SharpFormEditorField`'s constants (each one backed by the `FormEditorToolbarButton` enum): ```php const B = FormEditorToolbarButton::Bold; // 'bold' const I = FormEditorToolbarButton::Italic; // 'italic' const HIGHLIGHT = FormEditorToolbarButton::Highlight; // 'highlight' const SMALL = FormEditorToolbarButton::Small; // 'small' const UL = FormEditorToolbarButton::BulletList; // 'bullet-list' const OL = FormEditorToolbarButton::OrderedList; // 'ordered-list' const SEPARATOR = FormEditorToolbarButton::Separator; // '|' const A = FormEditorToolbarButton::Link; // 'link' const H1 = FormEditorToolbarButton::Heading1; // 'heading-1' const H2 = FormEditorToolbarButton::Heading2; // 'heading-2' const H3 = FormEditorToolbarButton::Heading3; // 'heading-3' const CODE = FormEditorToolbarButton::Code; // 'code' const QUOTE = FormEditorToolbarButton::Blockquote; // 'blockquote' const UPLOAD_IMAGE = FormEditorToolbarButton::UploadImage; // 'upload-image' const UPLOAD = FormEditorToolbarButton::Upload; // 'upload' const HR = FormEditorToolbarButton::HorizontalRule; // 'horizontal-rule' const TABLE = FormEditorToolbarButton::Table; // 'table' const IFRAME = FormEditorToolbarButton::Iframe; // 'iframe' const RAW_HTML = FormEditorToolbarButton::Html; // 'html' const CODE_BLOCK = FormEditorToolbarButton::CodeBlock; // 'code-block' const SUP = FormEditorToolbarButton::Superscript; // 'superscript' const FOOTNOTE = FormEditorToolbarButton::Footnote; // 'footnote' const UNDO = FormEditorToolbarButton::Undo; // 'undo' const REDO = FormEditorToolbarButton::Redo; // 'redo' ``` Example: ```php SharpFormEditorField::make("description") ->setToolbar([ SharpFormEditorField::B, SharpFormEditorField::I, SharpFormEditorField::SEPARATOR, SharpFormEditorField::A, ]); ``` ::: warning HTML included using RAW\_HTML button is not sanitized. ::: If you have editor embeds you can add them to the toolbar alongside other buttons (instead of the embeds dropdown) : ```php SharpFormEditorField::make("description") ->setToolbar([ SharpFormEditorField::B, SharpFormEditorField::I, AuthorEmbed::class, ]) ->allowEmbeds([ AuthorEmbed::class, ]); ``` See full [embed docs](../form-editor-embeds.md). ### `setRenderContentAsMarkdown(bool $renderAsMarkdown = true)` If true the front will send the content as markdown to the back, for storage. Default is false. ### `setWithoutParagraphs(bool $withoutParagraphs = true)` If true the editor won’t create `

`, but `
`. This is useful on some specific cases (everytime inline HTML is needed, maybe for a title or a legend). Default is false. ### `setMaxLength(int $maxLength)` Set an informative max character count. Will enforce `showCharacterCount(true)`. ### `setMaxLengthUnlimited()` Unset the max character count. ### `showCharacterCount(bool $showCharacterCount = true)` Display a character count in the status bar. Default is false. ### `allowFullscreen(bool $allowFullscreen = true)` Allow fullscreen mode. Default is false. ### `setSanitizeHtml(bool $sanitizeHtml = true)` Toggle HTML sanitization (enabled by default). See [security](#security). ## Embed images and files in content The Editor field can embed images or regular files. To use this feature, you must first allow the field to handle uploads: ### `allowUploads(SharpFormEditorUpload $formEditorUpload)` This method allows the user to upload files and images in the editor: ```php $formFields->addField( SharpFormEditorField::make('bio') ->allowUploads( SharpFormEditorUpload::make() ->setStorageBasePath('posts/embeds') ->setStorageDisk('local') ) ); ``` The `SharpFormEditorUpload` can be configured with the same API as the `SharpFormUploadField`: `setMaxFileSize()`, `setImageOnly()`, `setAllowedExtensions()`, ... ([see full documentation](../form-fields/upload.md)) ### A note on `setImageTransformable(bool $transformable = true, bool $transformKeepOriginal = true)` As for a regular upload field, you can allow the user to crop or rotate the visual, after the upload.\ With `$transformKeepOriginal` set to true, the original file will remain unchanged, meaning the transformations will be stored directly in the `` tag. For instance: ```blade {{-- (attribute JSON formatted for readability) --}} ``` Then at render Sharp will take care of that for the thumbnail (see *Display embedded files in the public site* below). ### Store images and files Sharp takes care of copying the file at the right place (after image transformation, if wanted), based on the configuration. When inserting a file, the following tag is added in field text value: ```blade {{-- (attribute JSON formatted for readability) --}} ``` In case of an image the inserted tag is: ```blade {{-- (attribute JSON formatted for readability) --}} ``` ### Display embedded files / images in the public site You may need to display those embedded files in the public website. The idea here is to display embedded images as thumbnails, and other files as you need. Sharp provides a component for that: ```blade {!! $html !!} ``` To handle image thumbnails, you can pass the following props: ```blade {!! $html !!} ``` ::: warning `` must have the editor content as direct child. ```diff -

{!! $html !!}
{{-- this will not work --}} + {!! $html !!} ``` ::: #### Advanced usages To add custom attributes to `` component you can use the following syntax: ```blade {!! $html !!} ``` #### Customize views You can extend `` and `` components by publishing them: ``` php artisan vendor:publish --tag=sharp-views ``` Here are the parameters passed to the components: * `$fileModel` which is a `SharpUploadModel` instance (see the [documentation](../sharp-uploads.md)); if you want to inject here your own `SharpUploadModel` implementation, you can do it by typing the full class path in the `sharp.uploads.model_class` config key. * `$width`, `$height`, `$filters`: whatever you passed as attribute #### Handle markdown The `` component does not render markdown, you will have to use your own `` component or helper function. To make `` elements working you must enable HTML in your parser (e.g. pass `['html_input' => 'allow']` to [league/commonmark](https://commonmark.thephpleague.com/2.0/configuration/)) Example: * [Blade view file](https://github.com/code16/sharp/blob/e387562698a2908f0f575cc5fd96705b9b78e078/saturn/resources/views/pages/spaceships/spaceship.blade.php) with `` usage * [`Markdown` component](https://github.com/code16/sharp/blob/e387562698a2908f0f575cc5fd96705b9b78e078/saturn/app/View/Components/Markdown.php) using league/commonmark ::: warning [cebe/markdown](https://github.com/cebe/markdown) is not compatible with sharp components ::: ## Custom embeds This feature allows to embed any structured data in the content. A common use case is to embed a reference to another instance, like for example: in a blog post, you want to insert a reference to another post, that would be rendered as a “read also” block / link in the public section. In practice, the Editor field can allow custom embeds, which defines how the data is stored in the field (as HTML attributes), and how it is edited in the UI, via a full-featured form. ### `allowEmbeds(array $embeds)` This method expects an array of embeds that could be inserted in the content, declared as full class names. An embed class must extend `Code16\Sharp\Form\Fields\Embeds\SharpFormEditorEmbed`. The [documentation on how to write an Embed class is available here](../form-editor-embeds.md). ## Security Editor content is sanitized by default before storing the data (to prevent XSS attack when displaying HTML content). To disable sanitizing you can call `->setSanitizeHtml(false)`. ## Formatter * `toFront`: expects a string; will extract embedded files for the front. * `fromFront`: returns a string, handle files (format, transformation, copy). --- --- url: /docs/9.x/guide/form-fields/number.md --- # Number Designate a numeric textfield. Class: `Code16\Sharp\Form\Fields\SharpFormNumberField` ## Configuration ### `setMin(float $min)` The minimum value that the UI allows. ### `setMax(float $max)` The maximum value that the UI allows. ### `setStep(float $step)` The step between values (with controls or arrow keys). Default is 1. ### `setShowControls(bool $showControls = true)` Display mouse control (spinner). Default is false. ## Formatter * `toFront`: will cast the provided value as a float. * `fromFront`: returns a float. --- --- url: /docs/9.x/guide/form-fields/html.md --- # Html Class: `Code16\Sharp\Form\Fields\SharpFormHtmlField` This field is read-only, and is meant to display some dynamic information in the form. ## Configuration ### `setTemplate(string|View|Closure $template)` Write the blade template as a string. Example: ```php SharpFormHtmlField::make('panel') ->setTemplate('This product is offline since {{ $date }}') ``` This example would mean that your transformed data has an object named `panel` containing a `date` attribute. Here a custom transformer example for this particular case: ```php function find($id): array { return $this ->setCustomTransformer('panel', fn ($value, $instance) => [ 'date' => $instance->deprecated_at->isoFormat() ]) ->transform(Product::find($id)); } ``` You can also pass a view (blade) : ```php SharpFormHtmlField::make('panel') ->setTemplate(view('sharp.form-htm-field')) ``` Using a closure: ```php SharpFormHtmlField::make('panel') ->setTemplate(function (array $data) { return 'You have chosen:'.$data['another_form_field'].'. Date: '.$data['date']; }) ``` #### Accessing to other field values in the form In the template, all other field values of the form are available (alongside the Html field value). This is particularly useful when using `setLiveRefresh()` (described below). ### `setLiveRefresh(bool $liveRefresh = true, ?array $linkedFields = null)` Use this method to dynamically update Html field when the user changes another field. The `$linkedFields` parameter allows filtering which field to watch (without it the internal refresh endpoint is called on any field update). ```php SharpFormHtmlField::make('total') ->setLiveRefresh(linkedFields: ['products']) ->setTemplate(function (array $data) { return 'Total:'.collect($data['products']) ->sum(fn ($product) => $product['price']); }) ``` ## Formatter * `toFront`: sent as provided. * `fromFront`: returns null (read-only). --- --- url: /docs/9.x/guide/form-fields/check.md --- # Check Designate a checkbox field. Class: `Code16\Sharp\Form\Fields\SharpFormCheckField` ## Configuration ### `setText(string $text)` Set the text of the checkbox (its label). Note that the `SharpFormCheckField` constructor requires this attribute. ## Formatter * `toFront`: casts whatever is provided as boolean. * `fromFront`: return a boolean. --- --- url: /docs/9.x/guide/form-fields/date.md --- # Date Class: `Code16\Sharp\Form\Fields\SharpFormDateField` ## Configuration ### `setHasDate($hasDate = true)` Let the user enter a date (default is true). ### `setHasTime($hasTime = true)` Let the user enter a time (default is false). ### `setMondayFirst(bool $mondayFirst = true)` Put monday as the first day in the calendar (default: true). ### `setSundayFirst(bool $sundayFirst = true)` Put sunday as the first day in the calendar (default: false). ### `setMinTime(int $hours, int $minutes = 0)` ### `setMaxTime(int $hours, int $minutes = 0)` If set, the time-chooser will be constraint as defined. ### `setStepTime(int $step)` Set a time step (in minutes) for the time-chooser. Default is 30. ## Formatter * `toFront`: accept either a Carbon instance, a DateTime instance of a "Y-m-d H:i:s" string. * `fromFront`: return a "Y-m-d H:i:s" string. --- --- url: /docs/9.x/guide/form-fields/upload.md --- # Upload Class: `Code16\Sharp\Form\Fields\SharpFormUploadField` ## General configuration You can define the temp disk and directory where files will be stored until they are moved to the final folder, as well as a global max file size (which can be overriden by each field). Here are the default values: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->configureUploads( uploadDisk: 'local', uploadDirectory: 'tmp', globalMaxFileSize: 5, keepOriginalImageOnTransform: true ) // [...] } } ``` The fourth argument, `keepOriginalImageOnTransform`, is a boolean that defines if the original image should be kept when a transformation is applied on it (meaning that transformations are stored and applied on-the-fly: this is transparent when using Sharp’s [built-in way to handle uploads](../sharp-uploads.md). It can be overridden by each field (see below). Sharp allows admins to download all uploaded files directly from the Upload field UI. However, this capability may introduce security concerns, since Sharp can access any file on the server (although this is largely mitigated by Flysystem, which is used under the hood). You can control this behavior by specifying a list of allowed disks in the configuration: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->configureDownloads( allowedDisks: ['local', 'public'], ) // ... } } ``` ## Field Configuration ### `setStorageDisk(string $storageDisk)` Set the destination storage disk (as configured in Laravel’s `config/filesystem.php` config file). ### `setStorageBasePath(string|Closure $storageBasePath)` Set the destination base storage path. ::: warning If you want to use a `{id}` special placeholder to add the instance id in the path (for instance: `$field->setStorageBasePath('/users/{id}/avatar')`), you must be the Eloquent case, leveraging `Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater` (see [Eloquent form](../building-form#eloquent-case-where-the-magic-happens)) ::: ### `setStorageTemporary()` Keep the file only in the upload directory/disk (configured [here](#general-configuration)). `setStorageDisk()` and `setStorageBasePath()` will be ignored. ### `setAllowedExtensions(string|array $extensions)` Define the allowed file extensions. For instance: `$field->setAllowedExtensions(['pdf', 'zip'])` ## Field Configuration in image case ### `setImageOnly(bool $imageOnly = true)` When an upload field is configured to accept only images: * the field will be forced to accept only images (allowed extensions set to "jpg, png, gif, svg, webp, bmp" by default), * the uploaded file will be validated as an image (see below for more options), * a thumbnail will be generated for the uploaded image. ### `setImageTransformable(bool $transformable = true, ?bool $transformKeepOriginal = null)` Allow the user to crop or rotate the visual, after the upload.\ The argument `$transformKeepOriginal` overrides the global config (which is `true` by default). With `$transformKeepOriginal` set to true, the original file will remain unchanged, meaning the transformations will be stored apart: using the [built-in way to handle uploads](../sharp-uploads.md), it's transparent. Otherwise, see the Formatter part below. ### `setImageCropRatio(?string $ratio = null, ?array $transformableFileTypes = null)` Set a ratio constraint to uploaded images, formatted like this: `width:height`. For instance: `16:9`, or `1:1`. When a crop ratio is set, any uploaded picture will be auto-cropped (centered). The second argument, `$transformableFileTypes`, provide a way to limit the crop configuration to a list of image files extensions. For instance, it can be useful to define a crop for jpg and png, but not for gif because it will destroy animation. ### `setImageCompactThumbnail(bool $compactThumbnail = true)` If true and if the upload has a thumbnail, it is limited to 60px high (to compact in a list item, for instance). ### `setImageOptimize(bool $imageOptimize = true)` If true, some optimization will be applied on the uploaded images (in order to reduce files weight). It relies on spatie's [laravel-image-optimizer](https://github.com/spatie/laravel-image-optimizer). Please note that you will need some of these packages on your system: * [JpegOptim](http://freecode.com/projects/jpegoptim) * [Optipng](http://optipng.sourceforge.net/) * [Pngquant 2](https://pngquant.org/) * [SVGO](https://github.com/svg/svgo) * [Gifsicle](http://www.lcdf.org/gifsicle/) * [cwebp](https://developers.google.com/speed/webp/docs/precompiled) Check their documentation for [more instructions](https://github.com/spatie/image-optimizer#optimization-tools) on how to install. ::: info If a JPG image with EXIF orientation is uploaded, it will use your configured upload thumbnail driver (GD or Imagick) instead of JpegOptim. This will ensure that the orientation is preserved after optimization. ::: ## Validation Notice that `setAllowedExtensions()` and `setImageOnly()` already are basic validation rules that Sharp will use both on the front-end and in the back-end. But there are a few more rules available: ### `setMaxFileSize(float $maxFileSizeInMB)` and `setMinFileSize(float $minFileSizeInMB)` Set the maximum and minimum (even if this is a rare use-case) file size in MB. ### `setImageDimensionConstraints(Illuminate\Validation\Rules\Dimensions $dimensions)` Set image dimension constraints, leveraging the dedicated Laravel validation rule (see [the documentation](https://laravel.com/docs/validation#rule-dimensions)). ## Formatter First, let's mention that Sharp provides an Eloquent built-in solution for uploads with the `SharpUploadModel` class, as [detailed here](../sharp-uploads.md), which greatly simplify the work (to be clear: it will handle everything from storage to image transformations). Here's the documentation for the **not built-in solution**: ### `toFront` The front expects an array with these keys: ```php [ 'name' => '', // The file name 'path' => '', // Relative file path 'disk' => '', // Storage disk name 'thumbnail' => '', // URL of the thumbnail (if image, obviously) 'size' => x, // Size in bytes 'filters' => [ // Transformations applied to the (image) file 'crop' => [ 'x' => x, 'y' => y, 'width' => w, 'height' => h, ], 'rotate' => [ 'angle' => a, ] ] ] ``` The formatter can't handle it automatically, it is too project-specific. You'll have to provide this in a custom transformer ([see full documentation](../how-to-transform-data.md)) like this: ```php function find($id): array { return $this ->setCustomTransformer('picture', function($value, $product, $attribute) { return [ 'name' => basename($product->picture->name), 'path' => $product->picture->name, 'disk' => 's3', 'thumbnail' => /* thumbnail URL */, 'size' => $product->picture->size, 'filters' => $product->picture->filters ]; } ) ->transform(Product::find($id)); } ``` Do note that the thumbnail should comply to following rules: be at least 200x200 pixels, and more importantly it must apply the transformations defined by the filters if there is some. ### `fromFront` There are four cases: #### newly uploaded file The formatter must return an array like this: ```php [ 'file_name' => '', // Target file path (relative) 'size' => x, // File size in bytes 'mime_type' => '', // File mime type 'disk' => '', // Target storage disk name 'filters' => [ // Transformations applied to the (image) file 'crop' => [ 'x' => x, 'y' => y, 'width' => w, 'height' => h, ], 'rotate' => [ 'angle' => a, ] ] ]; ``` It's up to you then to store any of these values in a DB or elsewhere. Using the `Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater`, you will probably reach a solution like this: ```php function update($id, array $data) { $instance = $id ? Product::findOrFail($id) : new Product; $this->ignore('picture')->save($instance, $data); // Then handle $data['picture'] here } ``` #### existing transformed image In this case, the image was already handled in a previous post, and was then transformed (cropped, or rotated). The formatter will simply return and array with one `filters` key: ```php [ 'filters' => [ 'crop' => [ 'x' => x, 'y' => y, 'width' => w, 'height' => h, ], 'rotate' => [ 'angle' => a, ] ] ]; ``` Then you'll have to catch and store that if needed. #### deleted file The formatter will return `null` (note that the file **will not** be deleted from the storage). #### existing and unchanged file The formatter will return **an empty array**. ## Configure files jobs Sharp handle files in jobs (copy / move and transformation). You can configure how these job should be dispatched: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->configureUploads( fileHandingQueue: 'default', fileHandlingQueueConnection: 'sync', ) // [...] } } ``` Queue and connection should be [properly configured](https://laravel.com/docs/queues). --- --- url: /docs/9.x/guide/form-fields/select.md --- # Select Class: `Code16\Sharp\Form\Fields\SharpFormSelectField` ## Configuration ### `self::make(string $key, array $options)` The `$options` array can be either: * a simple key-value array * an array of arrays with `id` and `label` keys. Fore instance: ```php [ ['id'=>1, 'label'=>'Label 1'], ['id'=>2, 'label'=>'Label 2'], ] ``` This allows to write code like this: ```php SharpFormSelectField::make( 'travel_id', Travel::orderBy('departure_date') ->get() ->map(function ($travel) { return [ 'id' => $travel->id, 'label' => $travel->departure_date->format('Y-m-d') . ' — ' . $travel->destination ]; }) ->all() ); ``` ### `setMultiple(bool $multiple = true)` Allow multi-selection (default: false) ### `setClearable(bool $clearable = true)` Allow null value in non-multiple selection (default: false) ### `setDisplayAsList()` Display as a list (the default value): * radio if multiple=false * checkboxes if multiple=true ### `setDisplayAsDropdown()` Display as a classic dropdown. ### `setMaxSelected(int $maxSelected)` Set a maximum item selection (multiple only). Default: unlimited. ### `setMaxSelectedUnlimited()` Unset a maximum item selection (multiple only). ### `setInline(bool $inline = true)` Display an inline checklist (if multiple + display=list). ### `setIdAttribute(string $idAttribute)` Set the id name attribute of options (default: "id"). ### `setOptionsLinkedTo(string ...$fieldKeys)` Thanks to this feature, you can link the dataset (meaning: the `options`) of the select to another field of the form (or even: to some other fields). In this case, the `options` array must be indexed with the value of the linked field. For instance: ```php SharpFormSelectField::make( 'brand', [ 'France' => [ ['id'=>1, 'label'=>'Renault'], ['id'=>2, 'label'=>'Peugeot'], ], 'Germany' => [ ['id'=>3, 'label'=>'Audi'], ['id'=>4, 'label'=>'Mercedes'], ] ] )->setOptionsLinkedTo('country') ``` This would work on relation with a `country` form field, which may be valued with "France" or "Germany". In some cases you may want to depend on more than one field; you must add a nested level in the `options` array: ```php SharpFormSelectField::make( 'model', [ 'France' => [ 1 => [['id'=>67, 'label'=>'Clio'], ...], 2 => ... ], 'Germany' => [ 3 => [['id'=>98, 'label'=>'A4'], ...], 4 => ... ] ] )->setOptionsLinkedTo('country', 'brand') ``` ## Formatter * `toFront`: expects * a single id value if multiple=false * an array of id values OR an array of models if multiple=true * `fromFront`: returns * a single id value if multiple=false * an array of arrays with the "id" key otherwise: ```php [ ['id' => 1], ['id' => 2] ] ``` --- --- url: /docs/9.x/guide/form-fields/autocomplete.md --- # Autocomplete Classes: `Code16\Sharp\Form\Fields\SharpFormAutocompleteLocalField` and `Code16\Sharp\Form\Fields\SharpFormAutocompleteRemoteField` ## Configuration for local autocomplete ### `setLocalValues($localValues)` Set the values of the dictionary on mode=local, as an object array with at least an `id` attribute (or the `setItemIdAttribute` value). ### `setLocalSearchKeys(array $searchKeys)` Set the names of the attributes used in the search (mode=local). Default: `['value']` ### `setLocalValuesLinkedTo(string ...$fieldKeys)` This method is useful to link the dataset of a local autocomplete (aka: the `localValues`) to another form field. Please refer to [the documentation of the select field's `setOptionsLinkedTo()` method](select.md), which is identical. ## Configuration for remote autocomplete ### `setRemoteEndpoint(string $remoteEndpoint)` The remote endpoint which should return JSON-formatted results. Note that you can add the `sharp_auth` middleware to this route to handle authentication and prevent this API endpoint to be called by non-sharp users: ```php // in a route file Route::get('/api/sharp/clients', [MySharpApiClientController::class, 'index']) ->middleware('sharp_auth'); ``` ::: tip This endpoint MUST be part of your application. If you need to hit an external endpoint, you should create a custom endpoint in your application that will call the external endpoint (be sure to check the alternative `setRemoteCallback` method). ::: ### `setRemoteCallback(Closure $closure, ?array $linkedFields = null)` To avoid the pain of writing a new dedicated endpoint, and for simple cases, you can use this method to provide a callback that will be called when the autocomplete field needs to fetch data. The callback will receive the search string as a parameter and should return an array of objects. Example: ```php SharpFormAutocompleteRemoteField::make('customer') ->setRemoteCallback(function ($search) { return Customer::select('id', 'name', 'email') ->where('name', 'like', "%$search%") ->get(); }); ``` The second argument, `$linkedFields`, allows you to provide a list of fields that will be sent with their values to the callback, so you can filter the results based on the values of other fields. Example: ```php SharpFormAutocompleteRemoteField::make('customer') ->setRemoteCallback(function ($search, $linkedFields) { return Customer::select('id', 'name', 'email') ->when( $linkedFields['country'], fn ($query) => $query->where('country_id', $linkedFields['country']) ) ->where('name', 'like', "%$search%") ->get(); }, linkedFields: ['country']); ``` ### `allowEmptySearch()` This method allows to call the endpoint / callback with empty search (on first click on the field for example). It's equivalent to `setSearchMinChars(0)`. ### `setSearchMinChars(int $searchMinChars)` Set a minimum number of character to type before performing the search. Default: `1` ### `setRemoteSearchAttribute(string $remoteSearchAttribute)` The attribute name sent to the remote endpoint as search key. Default: `'query'` ### `setDataWrapper($dataWrapper)` Configure an optional dataWrapper to handle results sent in a wrapper, typically "data". Default: empty string. ### `setDebounceDelayInMilliseconds($debounceDelay)` Configure the debounce delay between each endpoint call Default: 300. ### `setRemoteMethodGET()` ### `setRemoteMethodPOST()` Set the remote method to GET (default) or POST. ### `setDynamicRemoteEndpoint(string $dynamicRemoteEndpoint, array $defaultValues)` In a remote autocomplete case, you can use this method instead of `setRemoteEndpoint` to handle a dynamic URL, based on another form field. Here's how, for example: ```php SharpFormAutocompleteRemoteField::make('brand') ->setDynamicRemoteEndpoint('/brands/{{country}}'); ``` In this example, the `{{country}}` placeholder will be replaced by the value of the `country` form field. You can define multiple replacements if necessary. You may need to provide a default value for the endpoint, used when `country` (in our example) is not valued (without default, the autocomplete field will be displayed as disabled). To do that, fill the second argument: ```php SharpFormAutocompleteRemoteField::make('model') ->setDynamicRemoteEndpoint(''/models/{{country}}/{{brand}}'', [ 'country' => 'france', 'brand' => 'renault' ]); ``` The default endpoint would be `/brands/france/renault`. ## Common configuration for both modes ### `setItemIdAttribute(string $itemIdAttribute)` Set the name of the id attribute for items. This is useful : * if you pass an object as the data for the autocomplete (meaning: in the formatter's `toFront`). * to designate the id attribute in the remote API call return. Default: `"id"` ### `setListItemTemplate(View|string|Closure $template)` ### `setResultItemTemplate(View|string|Closure $template)` The templates for the list and result items can be set in two ways: either by passing a string, or by passing a Laravel view. Examples: ```php SharpFormAutocompleteRemoteField::make('customer') ->setRemoteEndpoint('/api/customers') ->setListItemTemplate('
{{$name}}
{{$email}}
') ->setResultItemTemplate(view('my/customer/blade/view')); ``` Note that the template can access to every attribute of the item (which will be sent as JSON by the API endpoint, and cast into an array) as a variable. In this example, we assume that the API endpoint returns an array of objects with `id`, `name` and `email` attributes. There is a third way to set the templates, by passing a Closure. This is **only suitable in one case: a remote autocomplete with a callback**. The closure will receive the unchanged item as a parameter (it’s useful when this item is an object, like a Model for instance), and must return a string. Here’s a simple example: ```php SharpFormAutocompleteRemoteField::make('customer') ->setRemoteCallback(function ($search) { return Customer::select('id', 'name', 'email') ->where('name', 'like', "%$search%") ->get(); }) ->setListItemTemplate(fn ($customer) => '
{{$customer->getFullName()}}
'); ``` ## Formatter ### `toFront` If **mode=local**, you must pass there either: * a single id, since the label will be grabbed from the `localValues` array, * or an object with an `id` (or whatever was configured through `setItemIdAttribute()`) property. If **mode=remote**, you must pass an object with at least an `id` (or whatever was configured through `setItemIdAttribute()`) attribute and all attributes needed by the item templates. ### `fromFront` Returns the selected item id. --- --- url: /docs/9.x/guide/form-fields/tags.md --- # Tags Class: `Code16\Sharp\Form\Fields\SharpFormTagsField` ## Configuration ### `setCreatable(bool $creatable = true)` If true, the user can create a new value from the form. Default: false. ### `setCreateText(string $createText)` The text displayed to the user when creating a new value. Default: "Create" ### `setCreateAttribute(string $attribute)` The name of the attribute which should be used for the creation. ### `setCreateAdditionalAttributes(array $attributes)` Optional additional attributes to be set at creation. Example: with `->setCreateAdditionalAttributes(["group"=>"public"])`, the `group` attribute of a created tag would be set to "public". Default: \[] ### `setIdAttribute(string $idAttribute)` Set the id name attribute of tags. Default: "id" ### `setMaxTagCount(int $maxTagCount)` Set a maximum tags selection. Default: unlimited. ### `setMaxTagCountUnlimited` Unset a maximum tags selection. ## Formatter * `toFront`: expects an array of id values OR an array of models. * `fromFront`: returns an array of arrays with the "id" key, and the "createAttribute" key in creation case: ```php [ ["id" => 1], ["id" => null, "name" => "Bob Marley] ] ``` In this example, the user selected one tag and created another one with the "Bob Marley" text. --- --- url: /docs/9.x/guide/form-fields/list.md --- # List Class: `Code16\Sharp\Form\Fields\SharpFormListField` A List is made of items, and each item contains form fields. Let's review a simple use case: a museum with all kind of art pieces. In the DB it's a 1-N relationship. If we choose to define a ArtPiece Entity in Sharp, we'll end up with maybe a Select, or an Autocomplete, to designate the Museum. But here, we want to do the opposite: define a Museum Entity, with an ArtPiece list. Here's how we can build this: ```php function buildFormFields() { $this->addField( SharpFormListField::make('pieces') ->setLabel('Art pieces') ->setAddable() ->setRemovable() ->addItemField( SharpFormDateField::make('acquisition_date') ->setLabel('Acquisition') ) ->addItemField( SharpFormTextField::make('title') ->setLabel('Title') ) ->addItemField( SharpFormSelectField::make('artist_id', /*[...]*/) ->setLabel('Artist') ) ); } ``` ## Configuration ### `addItemField(SharpFormField $field)` Add a SharpFormField in the item, building it like for the regular Form, with `SharpFormField::make()`. ### `setAddable(bool $addable = true)` Defines if new items can be added to the List. Default: false. ### `setAddText(string $addText)` Define the text of the Add item button. Default: "Add an item". ### `setMaxItemCount(int $maxItemCount)` ### `setMaxItemCountUnlimited()` If the List is `addable`, you can specify a maximum item count with these. Default: unlimited. ### `setSortable(bool $sortable = true)` Defines if items can be sorted by the user. Default: false. ### `setOrderAttribute(string $orderAttribute)` This is only useful when using the `WithSharpFormEloquentUpdater` trait. You can define here the name of an numerical order attribute (typically: `order`), and it will be automatically updated in the `save()` process. ### `setRemovable(bool $removable = true)` Defines if items can be removed by the user. Default: false. ### `setItemIdAttribute(string $itemIdAttribute)` Defines the id attribute name for items. Default: id. ### `allowBulkUploadForField(string $itemFieldKey)` ### `doNotAllowBulkUpload()` If the list item contains an `UploadFormField` field, this option can be used to present a bulk upload area to the user. The `$itemFieldKey` must refer to the key of the `UploadFormField`. Default is false (do not allow bulk upload) ### `setBulkUploadFileCountLimitAtOnce(int $limit)` Sets a file count limit to bulk upload (useful to prevent unwanted mass upload...). Default: 10 ## Layout The List item layout must be defined like the form itself, in the `buildFormLayout()` function. The item layout is managed as a Form column, with a `FormLayoutColumn` object. To link the column and the item, use `withListField()`, which takes the list field's key and a Closure accepting a `FormLayoutColumn` for the item's own layout. Here's an example for the Museum List defined above: ```php class MyForm extends SharpForm { // [...] function buildFormLayout(FormLayout $formLayout) { $this->addColumn(6, function (FormLayoutColumn $column) { $column->withListField('pieces', function (FormLayoutColumn $listItem) { $listItem ->withField('acquisition_date') ->withField('title') ->withField('artist_id'); }); }); } } ``` ## Formatter ### `toFront` The Formatter expects an array or a `Collection` of models, each one defining attributes for each list item keys at the format expected by the corresponding Field Formatter. So in our Museum example, we must provide an array of ArtPiece models with at least those attributes: `id`, `title`, `acquisition_date`, `artist_id`. ### `fromFront` Returns an array with the same shape. Newly added items will have a `null` id. --- --- url: /docs/9.x/guide/form-fields/autocomplete-list.md --- # AutocompleteList Class: `Code16\Sharp\Form\Fields\SharpFormAutocompleteListField` This one may seem a little strange. The goal is to build a List with only one field in each item: an Autocomplete. First let's see a use case: imagine you want to handle a list of `winners` by selecting them in a big list of Players, for which a remote Autocomplete is the best choice (otherwise you could have opted for a Tags Field). You can in fact define the list as this: ```php SharpFormAutocompleteListField::make('winners') ->setLabel('Winners') ->setItemField( SharpFormAutocompleteRemoteField::make('item') ->setRemoteEndpoint('/players') // [...] ) ); ``` ::: tip The key of the Autocomplete, `item` here, could be anything you want, as soon you stay consistent in the `buildFormLayout()` part. ::: But why can't we use a classic List for this? Well, the `model->winners` relation is N-N, here (`belongsToMany`), but Lists are meant to handle 1-N relationships (`hasMany`). Here we want one field, the Autocomplete, to represent the whole item object. ## Configuration Configuration is the same as the classic [List](list.md), except for: ### `setItemField(IsSharpFormAutocompleteField $field)` You can use this function instead of `addItemField`, since items of AutocompleteList have only one field. It accepts either a `SharpFormAutocompleteRemoteField` or a `SharpFormAutocompleteLocalField`. ### `addItemField(SharpFormField $field)` This method is an alias for `setItemField()`, meaning that you can only pass an Autocomplete, and it can only be called once. ## Formatter ### `toFront` Well, you must provide an array or Collection (same as for a List, see [related documentation](list.md)) of models with at least attributes designated by templates for the Autocomplete (see [related documentation](autocomplete.md)). ### `fromFront` Returns the selected item id. --- --- url: /docs/9.x/guide/form-fields/geolocation.md --- # Geolocation A map-based field to pick a precise location and return its coordinates (latitude and longitude) Class: `Code16\Sharp\Form\Fields\SharpFormGeolocationField` ## Configuration ### `setDisplayUnitDegreesMinutesSeconds()` Sets the coordinate display to be degrees-minutes-second, eg: `17°10'16'', 89°17'45''` ### `setDisplayUnitDecimalDegrees()` Sets the coordinate display to be decimal degrees, eg: `0.36666667, 17.15722222`. This is the default. ### `setInitialPosition(float $lat, float $lng)` ### `clearInitialPosition()` Sets the initial position of the edit map, when there in no marker yet. ### `setBoundaries(float $northEastLat, float $northEastLng, float $southWestLat, float $southWestLng)` ### `clearBoundaries()` If needed, set boundaries to the edit map, providing a north-east and a south-west position. ### `setZoomLevel(int $zoomLevel)` Set the map zoom level, from 1 (the World) ou 25. Default is 10. ### `setMapsProvider(string $provider, array $options = [])` You can choose between 2 providers for the Maps display: * "gmaps" for Google Maps (requires an API key, see below) * "osm" for Open Street Maps ### `setGeocoding(bool $geocoding = true)` Authorize geocoding, meaning enter an address and get back the coordinates. Default is false. May require an API key depending on the provider (see below). ### `setGeocodingProvider(string $provider, array $options = [])` You can choose between 2 providers for geocoding: * "gmaps" for Google Maps (requires an API key, see below) * "osm" for Open Street Maps ([Nominatim](https://nominatim.openstreetmap.org)) ### `setApiKey(string $apiKey)` If you use Google Maps as provider, for maps ou geocoding, you'll need a valid Google Maps Api key. This method will set the API key for both maps and geocoding. ### `setGeocodingApiKey(string $apiKey)` This method will set the API key for geocoding only. ### `setMapsApiKey(string $apiKey)` This method will set the API key for maps only. ### `setGoogleMapsMapId(string $mapId)` Google Maps API now requires a [Map ID](https://developers.google.com/maps/documentation/get-map-id) to use markers. It must be defined if maps provider is set to "gmaps". ## Formatter * `toFront`: expects a string with comma-separated decimal degrees values (`0.36666667,17.15722222` for instance). * `fromFront`: returns a string with the same format than `toFront`. --- --- url: /docs/9.x/guide/building-show-page.md --- # Create a Show Page Between an Entity List and a Form, you might want to add a Show page to display a whole instance, and allow the user to interact with it through Commands. Note that building a Show Page is really optional; but in some situations it could be really helpful to add this layer — and it can be even a must-have when dealing with "single" resources, such as a personal account, or a configuration entity, for which it's weird to build an Entity List. ## Generator ```bash php artisan sharp:make:show-page [--model=,--single] ``` ::: tip The Show Page name should be singular, in CamelCase and must end with the "Show" suffix. For instance: `ProductShow`. ::: ## Write the class First we build a class dedicated to our Show Page extending `Code16\Sharp\Show\SharpShow`; and we'll have to implement: * `buildShowFields(FieldsContainer $showFields)` and `buildShowLayout(ShowLayout $showLayout)` to declare the fields presenting the instance. * `find($id): array` to retrieve the instance. * `delete($id): void` to delete the instance. * `buildShowConfig()` (optional). In detail: ### `buildShowFields(FieldsContainer $showFields): void` Very much like Form's `buildFormFields()`, this method is meant to host the code responsible for the declaration and configuration of each show field. This must be done by calling `$showFields->addField`: ```php class MyShow extends SharpShow { // ... public function buildShowFields(FieldsContainer $showFields): void { $showFields ->addField( SharpShowTextField::make('name') ->setLabel('Name') ) ->addField( SharpShowPictureField::make('picture') ); } } ``` #### Common attributes to all show fields Each available Show field is detailed below; here are the attributes they all share : * `setShowIfEmpty(bool $show = true): self`: by default, an empty field (meaning: with null or empty data) is not displayed at all in the Show UI. You can change this behaviour with this attribute. This method has no impact for the [Entity List field](show-fields/entity-list.md). #### Available simple Show fields * [Text](show-fields/text.md) * [Picture](show-fields/picture.md) * [File](show-fields/file.md) * [List](show-fields/list.md) #### Embedding an Entity List in a Show A crucial feature in the ability given to add a full Entity List in a Show, to display and interact with some "one to many" related data. Let's review a simple example: we want to display the product list of an order. In the order Show, we can add a products Entity List as a field: ```php class MyShow extends SharpShow { // ... public function buildShowFields(FieldsContainer $showFields): void { $showFields ->addField( SharpShowEntityListField::make('products') ); } } ``` Sharp will consider this as a regular Entity List configured with the `products` entity key (this name can be overridden as a second argument), and will display it the Show as a field (see below for layout), with the full feature set of an Entity List: filters, commands, reorder, entity state, search... Clicking a row in the EntityList can lead to a Form, or another Show Page (depending on the Entity configuration). Sharp will maintain a navigation breadcrumb to keep track of the user path. Notice that you have three possibilities for the actual code of this Entity List: * if you want to have a "products" entity in the main menu, you can reuse the same Entity List instance for the Show (and configure it to scope the data, as we'll discuss below), * or you can configure a dedicated Entity with a specific Entity List, without declaring it in the main menu, * or you can have both, making the orders Show version of the products Entity List extend the main one. As always with Sharp, implementation is up to you. The next thing to do is to scope the data of the Entity List field. In our case, we want to display and interact only with the products for this order... For this and more on personalization, refer to the detailed documentation of this field: * [Entity List field](show-fields/entity-list.md) ### `buildShowLayout(ShowLayout $showLayout): void` The show layout is a simplified version of the Form layout, and is made of sections which contains `columns` of `fields`. #### Sections A section is just a block of fields, packed in columns: ```php class MyShow extends SharpShow { // ... public function buildShowLayout(ShowLayout $showLayout): void { $showLayout->addSection( 'Description', function (ShowLayoutSection $section) { ... } ); } } ``` A section can be declared *collapsable*: ```php class MyShow extends SharpShow { // ... public function buildShowLayout(ShowLayout $showLayout): void { $showLayout->addSection( 'Description', function (ShowLayoutSection $section) { $section->setCollapsable(); } ); } } ``` #### Columns and fields Just like for Forms, a `ShowLayoutSection` is made of columns and fields. So completing the example above: ```php class MyShow extends SharpShow { // ... public function buildShowLayout(ShowLayout $showLayout): void { $showLayout->addSection( 'Description', function (ShowLayoutSection $section) { $section->addColumn(9, function (ShowLayoutColumn $column) { $column->withField('description'); }); } ); } } ``` A `ShowLayoutColumn`, very much like a `FormLayoutColumn`, can declare single field rows and multi fields rows. Report to the [Form layout documentation](building-form.md#buildformlayoutformlayout-formlayout) to find out how. #### SharpShowListField's layout Like `SharpFormListField` in Forms, a `SharpShowListField` must declare its item layout, in order to describe how fields are displayed, like in this example: ```php class MyShow extends SharpShow { // ... public function buildShowLayout(ShowLayout $showLayout): void { $showLayout->addSection( 'Pictures', function (ShowLayoutSection $section) { $section->addColumn(9, function (ShowLayoutColumn $column) { $column->withListField('pictures', function (ShowLayoutColumn $listItem) { // Notice that the list item layout is just a ShowLayoutColumn $listItem ->withField('file') ->withField('legend'); }); }); ); } } ``` #### Embedded Entity Lists An embedded Entity List in treated as a special section; its label will be displayed as section title. ```php class MyShow extends SharpShow { // ... public function buildShowLayout(ShowLayout $showLayout): void { $showLayout->addEntityListSection('members'); } } ``` Like regular sections, embedded Entity List can be declared *collapsable*. ```php class MyShow extends SharpShow { // ... public function buildShowLayout(ShowLayout $showLayout): void { $showLayout->addEntityListSection('members', collapsable: true); } } ``` ### `find($id): array` As for Forms, the method must return a key-value array: ```php class MyShow extends SharpShow { // ... public function find($id): array { return [ 'name' => 'USS Enterprise', 'capacity' => 3000 ]; } } ``` And as for Forms, you'll want to transform your data before sending it. ```php class MyShow extends SharpShow { // ... public function find($id): array { return $this ->setCustomTransformer( 'name', fn ($value, $product) => strtoupper($product->name) ) ->setCustomTransformer( 'picture', new SharpUploadModelThumbnailUrlTransformer(600) ); } } ``` Transformers are explained in the detailed [How to transform data](how-to-transform-data.md) documentation. ### `delete($id): void` Here you might write the code performed on a deletion of the instance. It can be anything, here's an Eloquent example: ```php class MyShow extends SharpShow { // ... public function delete($id): void { Product::findOrFail($id)->delete(); } } ``` ### `buildShowConfig(): void` Very much like EntityLists, a Show can declare a config with `EntityState` handler, or Breadcrumb configuration; you can also define here an attribute that will be used as page title. ```php class MyShow extends SharpShow { // ... public function buildShowConfig(): void { $this ->configureBreadcrumbCustomLabelAttribute('name') ->configurePageTitleAttribute('title') ->configureEntityState('state', OrderEntityState::class); } } ``` Here is the full list of available methods: * `configureBreadcrumbCustomLabelAttribute(string $breadcrumbAttribute)`: declare the data attribute to use for the breadcrumb; [see detailed doc](sharp-breadcrumb.md) * `configureEntityState(string $stateAttribute, $stateHandlerOrClassName)`: add a state toggle, [see detailed doc](entity-states.md) * `configurePageTitleAttribute(string $titleAttribute, bool $localized = false)`: define a title to the Show Page, configuring an attribute that should be part of the `find($id)` array * `configureDeleteConfirmationText(string $text)` to add a custom confirm message when the use clicks on the delete button. * `configureEditButtonLabel(string $label)` to set a custom "Edit..." button label. ### Display a Page Alert Override `buildPageAlert(PageAlert $pageAlert): void` to display a dynamic message above the Show Page; [see detailed doc](page-alerts.md). ## Accessing the navigation breadcrumb A common pattern for Shows is to add an embedded EntityList with related entities, and to allow update but also creation from there. Taking back our order / products example, we may need to add a product to the order. Question is: how can we attach a newly created product to an existing order? The answer is by accessing the navigation breadcrumb, with [Sharp Context](context.md), and more precisely with its `breadcrumb()->previousShowSegment()` method. Here's a full example: ```php class ProductSharpForm extends SharpForm { function update($id, array $data) { $product = $id ? Product::findOrFail($id) : new Product; $product = $this->save($product, $data); if (sharp()->context()->isCreation()) { Order::findOrFail(sharp()->context()->breadcrumb()->previousShowSegment()->instanceId()) ->products() ->attach($product->id); } } } ``` ## Declare the Show Page The show Page must be declared in the correct entity class, as documented here: [Write an entity](entity-class.md). --- --- url: /docs/9.x/guide/single-show.md --- # Using Single Show for unique resources Sometimes you will need to configure a "unique" resource that does not fit into a List / Show schema, like for instance an account, or a configuration item. To handle this kind of "unique" resource, Sharp provides a way to build Single Shows. ## Write the class Instead of extending `SharpShow`, our SingleShow implementation should extend `Code16\Sharp\Show\SharpSingleShow`. We still have to implement `buildShowFields(FieldsContainer $showFields)` and `buildShowLayout(ShowLayout $showLayout)` to declare the fields presenting the instance, an optionally `buildShowConfig()`, but the `find()` method is different: * `findSingle(): array`, without any parameter because in a single case the functional code has to determine the instance on its side (based on the current user, for instance). ```php class ProfileSingleShow extends SharpSingleShow { // [...] public function findSingle(): array { return $this->transform(auth()->user()); } } ``` ## Single Show declaration We must declare in the entity class that we want to use a Single Show: ```php class ProfileEntity extends SharpEntity { protected bool $isSingle = true; protected string $label = 'My profile'; protected ?string $show = ProfileSingleShow::class; protected ?string $form = ProfileSingleForm::class; } ``` Notice the `$isSingle` property, which indicates that this entity does not have an Entity List. ## Single Commands Declared Commands must also be implemented as *single*. Like for Shows, this only means extending a more specific abstract class: `Code16\Sharp\EntityList\Commands\SingleInstanceCommand`. The two differences with regular `InstanceCommand` are: * `executeSingle(array $data = []): array`, which does not take any `$instanceId` is parameter * `authorize(): bool`, in case you need to define a specific authorization, instead of `authorizeFor($instanceId)`. ## Single EntityState Same for EntityState: in a `SingleShow` case, you must implement EntityState as a `Code16\Sharp\EntityList\Commands\SingleEntityState`, which differs a bit: * `updateSingleState(string $stateId)` * `authorize(): bool` ## What if you need a Form? Well, that's a [SingleForm](single-form.md) then. --- --- url: /docs/9.x/guide/show-fields/text.md --- # Text Class: `Code16\Sharp\Show\Fields\SharpShowTextField` ## Configuration ### `setLabel()` Set the field label. ### `collapseToWordCount(int $wordCount)` Collapse the text if too long, and add a "show more" link. Use it for long texts (even markdown formatted) in sections with only one field. ### `doNotCollapse()` Reset the collapse configuration. ### `setHtml(bool $html = true)` By default, the text is escaped. If you want to display HTML, set this to true. ### `setSanitizeHtml(bool $sanitizeHtml = true)` HTML sanitization is enabled by default for text fields (to prevent XSS attacks when displaying the show). To disable it, call `->setSanitizeHtml(false)`. ### `allowEmbeds(array $embeds)` This method expects an array of embeds that could be inserted in the content, declared as full class names. An embed class must extend `Code16\Sharp\Form\Fields\Embeds\SharpFormEditorEmbed`. The [documentation on how to write an Embed class is available here](../form-editor-embeds.md). ## Transformer For markdown-formatted texts, be sure to use the built-in `MarkdownAttributeTransformer`: ```php function find($id): array { return $this ->setCustomTransformer( 'description', new MarkdownAttributeTransformer() ) ->transform([...]); } ``` --- --- url: /docs/9.x/guide/show-fields/picture.md --- # Picture Class: `Code16\Sharp\Show\Fields\SharpShowPictureField` ## Configuration The picture field has no configuration. ## Transformer You must value this field with an URL of the image. If you are using [Sharp built-in Upload solution](../sharp-uploads.md), be sure to use the `SharpUploadModelThumbnailUrlTransformer`: ```php function find($id): array { return $this ->setCustomTransformer( 'picture', new SharpUploadModelThumbnailUrlTransformer(600) ) ->transform([...]); } ``` --- --- url: /docs/9.x/guide/show-fields/list.md --- # List Class: `Code16\Sharp\Show\Fields\SharpShowListField` This field is very similar to the [Form's List field](../form-fields/list.md), and its purpose is to display items made of other Show fields. Here's an example, for a list of pictures with a legend: ```php class MyShow extends SharpShow { // [...] function buildShowFields(FieldsContainer $showFields): void { $showFields->addField( SharpShowListField::make('pictures') ->setLabel('additional pictures') ->addItemField( SharpShowFileField::make('file') ) ->addItemField( SharpShowTextField::make('legend') ->setLabel('Legend') ) ); } } ``` ## Configuration ### `setLabel()` Set the field label. ### `addItemField(SharpShowField $field)` Add a SharpShowField in the item. ## Layout The List item layout must be defined like the show itself, in the `buildShowLayout()` function. The item layout is managed as a column, with a `ShowLayoutColumn` object. To link the column and the item, use the classic `withField()` function with a second argument, a Closure accepting a `ShowLayoutColumn`. Example: ```php class MyShow extends SharpShow { // [...] function buildShowLayout(ShowLayout $showLayout): void { $showLayout->addColumn(6, function (ShowLayoutColumn $column) { $column->withListField('pieces', function (ShowLayoutColumn $listItem) { $listItem->withField('acquisition_date') ->withField('title') ->withField('artist'); }); }); } } ``` ## Formatter The Formatter expects an array or a `Collection` of models, each one defining attributes for each list item keys at the format expected by the corresponding Field Formatter. --- --- url: /docs/9.x/guide/show-fields/file.md --- # File Class: `Code16\Sharp\Show\Fields\SharpShowFileField` The purpose of this field is to present a downloadable file to the user. ## Configuration ### `setLabel()` Set the field label. ## Transformer Sharp expects an array formatted like this: ```php [ 'name' => '', // Relative file path 'path' => '', // Full file path 'disk' => '', // Disk name 'mime_type' => '', // Mime type 'thumbnail' => '', // 1000px w * 400px h thumbnail full url 'size' => x, // Size in bytes ] ``` If you are using Sharp’s solution for uploads, meaning the `SharpUploadModel` class [detailed here](../sharp-uploads.md), you can call the built-in transformer: ```php $this->setCustomTransformer('file', new SharpUploadModelFormAttributeTransformer()); ``` This transformer allows acting a bit on the thumbnail creation part, see its constructor for more details. ## A note on security Sharp allows admins to download all uploaded files directly from the File field UI. However, this capability may introduce security concerns, since Sharp can access any file on the server (although this is largely mitigated by Flysystem, which is used under the hood). You can control this behavior by specifying a list of allowed disks in the configuration: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->configureDownloads( allowedDisks: ['local', 'public'], ) // ... } } ``` --- --- url: /docs/9.x/guide/show-fields/entity-list.md --- # Entity List Class: `Code16\Sharp\Show\Fields\SharpShowEntityListField` The field allows you to integrate an [Entity List](../building-entity-list.md) into your Show Page. ## Constructor This field needs, as first parameter, either the entity key or the `SharpEntity` class that declares the Entity List which will be included in the Show Page. For instance: ```php SharpShowEntityListField::make('products') ``` or ```php SharpShowEntityListField::make(ProductEntity::class) ``` ::: warning This last syntax is better in terms of DX (since it allows using the IDE to navigate to the Entity List implementation), but it won’t work in two specific cases: if you use a custom `SharpEntityResolver` or if you your Entity is declared with multiple keys. ::: To handle special cases, you can provide a second string argument: the first argument is the field key (as referred in the layout), and the second argument is the related Entity List key: ```php SharpShowEntityListField::make('order_products', 'products') ``` ::: tip Note that unlike every other Show Field, the `instance` of the Show don't have to expose an attribute named like that, since the Entity List data is gathered with a dedicated request. ::: Entity List fields really are just regular Entity List presented in a Show page: we therefore need a full Entity List implementation, declared in an Entity. To scope the data to the `instance` of the Show, you can use `hideFilterWithValue()` (see below). ## Configuration ### `hideFilterWithValue(string $filterName, $value)` This is the most important method of the field, since it will not only hide a filter but also set its value. The purpose is to allow to **scope the data to the instance** of the Show Page. For example, let’s say we display an Order and that we want to embed a list of its products: ```php class OrderShow extends SharpShow { // ... public function buildShowFields(FieldsContainer $showFields): void { $showFields->addField( SharpShowEntityListField::make(ProductEntity::class) ->hideFilterWithValue(OrderFilter::class, 64) ); } } ``` We defined here that we want to display the Entity List defined in the `ProductEntity`, with its `OrderFilter` filter (which must be declared as usual in the Entity List implementation) hidden AND valued to `64` when gathering the data. In short: we want the products for the order of id `64`. ::: tip Note on the filter name: passing its full classname will always work, but you can also directly pass its `key`, in case you defined one. ::: You can pass a closure as the value, and it will contain the current Show instance id. In most cases, you'll have to write this: ```php SharpShowEntityListField::make('products') ->hideFilterWithValue(OrderFilter::class, fn ($instanceId) => $instanceId); ``` One final note: sometimes the linked filter is really just a scope, never displayed to the user. In this case, it can be tedious to write a full implementation in the Entity List. In this situation, you can use the `HiddenFilter` class for the filter, passing a key: ```php class OrderShow extends SharpShow { // ... public function buildShowFields(FieldsContainer $showFields): void { $showFields->addField( SharpShowEntityListField::make('products') ->hideFilterWithValue('order', fn ($instanceId) => $instanceId); ); } } ``` ```php use \Code16\Sharp\EntityList\Filters\HiddenFilter; class OrderProductList extends SharpEntityList { // ... protected function getFilters(): ?array { return [ HiddenFilter::make('order') ]; } public function getListData(): array|Arrayable { return $this->transform( Products::query() ->forOrderId($this->queryParams->filterFor('order')) ->get() ); } } ``` ### `hideEntityCommand(array|string $commands): self` Use it to hide any entity command in this particular Entity List (useful when reusing an Entity List displayed elsewhere). This will apply before looking at authorizations. ### `hideInstanceCommand(array|string $commands): self` Use it to hide any instance command in this particular Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. ### `showEntityState(bool $showEntityState = true): self` Use it to show or hide the EntityState label and command (useful when reusing an Entity List). This will apply before looking at authorizations. ### `showCreateButton(bool $showCreateButton = true): self` Use it to show or hide the "create" button in this particular Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. ### `showReorderButton(bool $showReorderButton = true): self` Use it to show or hide the reorder button in this particular Entity List (useful when reusing an Entity List). This will apply before looking at authorizations. ### `showSearchField(bool $showSearchField = true): self` Use it to show or hide the search field in this particular Entity List (useful when reusing an Entity List). ### `showCount(bool $showCount = true): self` Use it to show or hide the count of items in the Entity List. ## Display in layout To display your entity list in your show page's layout, you have to use the `addEntityListSection()` method in your Show Page's `buildShowLayout()` method. ```php protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout ->addSection(function (ShowLayoutSection $section) { $section ->addColumn(7, function (ShowLayoutColumn $column) { $column ->withFields(categories: 5, author: 7) // ... }) ->addColumn(5, function (ShowLayoutColumn $column) { // ... }); }) ->addEntityListSection(ProductEntity::class); } ``` ## Transformer There is no transformer, since Sharp will NOT look for an attribute in the instance sent. The data of the Entity List is brought by a distinct XHR call, the same Sharp will use for any Entity List. --- --- url: /docs/9.x/guide/show-fields/dashboard.md --- # Dashboard Class: `Code16\Sharp\Show\Fields\SharpShowDashboardField`. The field allows you to integrate a [Dashboard](../building-dashboard.md) into your Show Page. ## Constructor This field needs, as first parameter, either the entity key or the `SharpDashboardEntity` class that declares the dashboard which will be included in the Show Page. For instance: ```php SharpShowDashboardField::make('posts_dashboard') ``` or ```php SharpShowDashboardField::make(PostDashboardEntity::class) ``` ::: warning This last syntax is better in terms of DX (since it allows using the IDE to navigate to the Entity List implementation), but it won’t work in two specific cases: if you use a custom `SharpEntityResolver` or if you your Entity is declared with multiple keys. ::: ## Configuration ### `hideFilterWithValue(string $filterName, $value)` This is the most important method of the field, since it will not only hide a filter but also set its value. The purpose is to allow to **scope the data to the instance** of the Show Page. For example, let’s say we display a Post and that we want to embed a dashboard with the post's statistics: ```php class PostShow extends SharpShow { // ... public function buildShowFields(FieldsContainer $showFields): void { $showFields->addField( SharpShowDashboardField::make(PostDashboardEntity::class) ->hideFilterWithValue(PostFilter::class, 64) ); } } ``` Here we're scoping the `PostDashboard` declared in the `PostDashboardEntity` to the instance of the `Post` with id 64. You can pass a closure as the value, and it will contain the current Show instance id. In most cases, you'll have to write this: ```php SharpShowDashboardField::make(PostDashboardEntity::class) ->hideFilterWithValue(PostFilter::class, fn ($instanceId) => $instanceId); ``` **One final note**: sometimes the linked filter is really just a scope, never displayed to the user. In this case, it can be tedious to write a full implementation in the Dashboard. In this situation, you can use the `HiddenFilter` class for the filter, passing a key: ```php class PostShow extends SharpShow { // ... public function buildShowFields(FieldsContainer $showFields): void { $showFields->addField( SharpShowDashboardField::make(PostDashboardEntity::class) ->hideFilterWithValue('post', fn ($instanceId) => $instanceId); ); } } ``` ```php use \Code16\Sharp\EntityList\Filters\HiddenFilter; class PostDashboard extends SharpDashboard { // ... protected function getFilters(): ?array { return [ HiddenFilter::make('post') ]; } protected function buildWidgetsData(): void { $this->setFigureData('visit_count', figure: Post::query() ->findOrFail($this->queryParams->filterFor('post')) ->visit_count ); } } ``` ### `hideDashboardCommand(array|string $commands): self` Use it to hide any dashboard command in this particular Dashboard (useful when reusing a Dashboard). This will apply before looking at authorizations. ## Display in layout To display your dashboard in your show page's layout, you have to use the `addDashboardSection()` method in your Show Page's `buildShowLayout()` method. ```php protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout ->addSection(function (ShowLayoutSection $section) { $section ->addColumn(7, function (ShowLayoutColumn $column) { $column ->withFields(categories: 5, author: 7) // ... }) ->addColumn(5, function (ShowLayoutColumn $column) { // ... }); }) ->addDashboardSection(PostDashboardEntity::class); } ``` --- --- url: /docs/9.x/guide/building-dashboard.md --- # Create a Dashboard A Dashboard is a good way to present synthetic data to the user, with graphs, stats, or personalized reminders for instance. ## Generator ```bash php artisan sharp:make:dashboard ``` ::: tip The Dashboard name should be singular, in CamelCase and must end with the "Dashboard" suffix. For instance: `ActivityDashboard`. ::: ## Write the class The first step is to create a new class extending `Code16\Sharp\Dashboard\SharpDashboard`, and to implement three functions: * `buildWidgets(WidgetsContainer $widgetsContainer)`, * `buildDashboardLayout(DashboardLayout $dashboardLayout)`, * and `buildWidgetsData()`, for the actual Dashboard data ### `buildWidgets(WidgetsContainer $widgetsContainer): void` This method is meant to host the code responsible for the declaration and configuration of each widget. This must be done by calling `$widgetsContainer->addWidget()`: ```php class SalesDashboard extends SharpDashboard { // [...] function buildWidgets(WidgetsContainer $widgetsContainer): void { $widgetsContainer ->addWidget( SharpLineGraphWidget::make('sales') ->setTitle('Sales evolution') ) ->addWidget( SharpFigureWidget::make('pendingOrders') ->setTitle('Pending orders') ->setLink(LinkToEntityList::make('orders')->addFilter(StateFilter::class, 'pending')) ); } } ``` As we can see in this example, we defined two widgets giving them a mandatory `key` and some optional properties. Every widget has the optional following setters: * `setTitle(string $title)` for the widget title displayed above it * `setLink(SharpLinkTo $sharpLinkTo)` to make the whole widget linked to a specific page (see [dedicated SharpLinkTo documentation](link-to.md)) And here's the full list and documentation of each widget available, for the specifics: * [Graph](dashboard-widgets/graph.md) * [Panel](dashboard-widgets/panel.md) * [Figure](dashboard-widgets/figure.md) * [OrderedList](dashboard-widgets/ordered-list.md) ### `buildDashboardLayout(DashboardLayout $dashboardLayout): void` The layout API is a bit different from Forms or Show Pages here, because we think in terms of rows and not columns. ```php function buildDashboardLayout(DashboardLayout $dashboardLayout): void { $dashboardLayout ->addSection('Posts', function (DashboardLayoutSection $section) { $section->addRow(function (DashboardLayoutRow $row) { $row->addWidget(6, 'draft_panel') ->addWidget(6, 'online_panel'); }); }) ->addSection('Stats', function (DashboardLayoutSection $section) { $section->addFullWidthWidget('visits_line'); }); } ``` Note that: * Sections are optional but useful to group related widgets; you can add rows directly to the layout if you don’t need them. * Rows group widgets in a 12-based grid. ### `buildWidgetsData(): void` Widget data is set with specific methods depending on their type. The documentation is therefore split: * [Graph](dashboard-widgets/graph.md) * [Panel](dashboard-widgets/panel.md) * [Figure](dashboard-widgets/figure.md) * [OrderedList](dashboard-widgets/ordered-list.md) ## Configure the Dashboard A Dashboard must have his own [Entity class, as documented here](entity-class.md). Once this class (`CompanyDashboardEntity` for instance) written, we have to declare it: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->declareEntity(CompanyDashboardEntity::class); // ... } } ``` In the menu, like an Entity, a Dashboard can be displayed anywhere. ```php class AppSharpMenu extends SharpMenu { public function build(): self { return $this ->addEntityLink(CompanyDashboardEntity::class, 'Dashboard'); // ... } } ``` ## Dashboard commands Like Entity Lists, Commands can be declared in a Dashboard with `getDashboardCommands()` : [see the Command documentation](commands.md). And like Show Pages, Commands can be visually attached to a specific section: ```php protected function buildDashboardLayout(DashboardLayout $dashboardLayout): void { $dashboardLayout ->addSection('Posts', function (DashboardLayoutSection $section) { // ... }) ->addSection('Stats', function (DashboardLayoutSection $section) { $section ->setKey('stats-section') // <- define a key here... ->addRow(function (DashboardLayoutRow $row) { // ... }); }); } public function getFilters(): ?array { return [ 'stats-section' => [ PeriodRequiredFilter::class, ], ]; } public function getDashboardCommands(): ?array { return [ 'stats-section' => [ // <- use the section key here... ExportStatsAsCsvCommand::class, ], ]; } ``` ## Dashboard filters Just like Entity Lists, Dashboard can display filters, as [documented on the Filter page](filters.md). And very much like Commands, Filters can be visually attached to a specific section of the dashboard: ```php public function getFilters(): ?array { return [ 'stats-section' => [ // <- must be a section key PeriodRequiredFilter::class, ], ]; } ``` ## Dashboard policy You can define a Policy for a Dashboard; [see the authorization documentation](entity-authorizations.md). --- --- url: /docs/9.x/guide/dashboard-widgets/graph.md --- # Graph widget This widget intends to display a graph visualization of any data. There are four graph types, and they mostly share the same API. To choose one or the other, use its dedicated class: ## Line ```php $widgetsContainer->addWidget( SharpLineGraphWidget::make('sales') ); ``` Along with the [common configuration](#common-configuration), the following methods are available: ### `setShowDots(bool $showDots = true)` Display dots on the graph lines. ### `setCurvedLines(bool $curvedLines = true)` Display lines with curved angles. Default is `true`. ## Area ```php $widgetsContainer->addWidget( SharpAreaGraphWidget::make('sales') ); ``` Along with the [common configuration](#common-configuration), the following methods are available: ### `setCurvedLines(bool $curvedLines = true)` Display lines with curved angles. Default is `true`. ### `setOpacity(float $opacity)` Change the opacity of the filled areas. Default is `0.4`. ### `setShowGradient(bool $gradient = true)` Display a gradient on top of the filled areas. ### `setStacked(bool $stacked = true)` Stack areas on top of each other. Useful for comparing two or more series. The order of `->addGraphDataSet()` calls defines the stacking order. ### `setShowStackTotal(bool $showStackTotal = true, ?string $label = null)` Show the total of all stacked areas in the tooltip. The label can be customized. ## Bar ```php $widgetsContainer->addWidget( SharpBarGraphWidget::make('sales') ); ``` Along with the [common configuration](#common-configuration), the following methods are available: ### `setHorizontal(bool $horizontal = true)` Display horizontal bars instead of vertical ones. Default is `false`. ## Pie ```php $widgetsContainer->addWidget( SharpPieGraphWidget::make('sales') ); ``` ## Common configuration ### `setRatio(string $ratio)` This attribute is used to define the graph ratio, which will be consistent in responsive mode. The expected format is `width:height`, so for instance `16:9` (the default) or `4:3`. ### `setHeight(int $height)` Used to set an arbitrary height, in px; if set the ratio is ignored. ### `setShowLegend(bool $showLegend = true)` Display or not the graph legend. Default is `true`. ### `setMinimal(bool $minimal = true)` If true, legend and axis are hidden. Default is `false`. ### `setDisplayHorizontalAxisAsTimeline(bool $displayAsTimeline = true)` **(Line, Area, Bar)** If true, and if X axis values are valid dates, the graph will create a timeline repartition of dates, creating visual gaps between dates. Default is `false`. ### `setEnableHorizontalAxisLabelSampling(bool $enableLabelSampling = true)` **(Line, Area, Bar)** If true, only some labels will be displayed depending on available space. It prevents label rotation. This method has no impact when `setDisplayHorizontalAxisAsTimeline()` is called. Default is `false`. ## Data valuation Valuation is handled by a dedicated method: `$this->addGraphDataSet(string $graphWidgetKey, SharpGraphWidgetDataSet $dataSet)`, in the Dashboard class: ```php $this->addGraphDataSet( 'sales', SharpGraphWidgetDataSet::make($values) ->setLabel('Sales') ->setColor('blue') ); ``` We use an instance of `Code16\Sharp\Dashboard\Widgets\SharpGraphWidgetDataSet` to handle graph data. This object is built with a `$values` array which must contain numeric values, keyed by a label. Example: ```php [ 'Pizza' => 4, 'Hamburgers' => 12 ] ``` `SharpGraphWidgetDataSet` defines two useful setters: * `setLabel(string $label)` to set the legend label * `setColor(string $color)` where $color can be an HTML constant or an hexadecimal value, to set the data color. You can chain calls to `addGraphDataSet()` to add multiple data sets, with different colors and labels. ### Date labels When `setDisplayHorizontalAxisAsTimeline()` is used, dataset values should have a key parsable by Carbon (e.g `Y-m-d` or `Y-m-d H:i:s`), for example: ```php $this->addGraphDataSet( 'daily_records', SharpGraphWidgetDataSet::make(DailyRecord::get()->mapWithKeys(fn ($record) => [ $record->date->format('Y-m-d') => $record->fail_count ])) ->setLabel('Daily records') ->setColor('blue') ); ``` --- --- url: /docs/9.x/guide/dashboard-widgets/panel.md --- # Panel widget This widget is intended to display any useful information to the user, based on a custom blade template. ## Attributes (setters) ```php $widgetsContainer->addWidget( SharpPanelWidget::make('messages') ->setTemplate('my/blade/template') ); ``` The Panel needs a blade template to be rendered: ### `setTemplate(View|string $template)` Pass a blade view path, or a blade content. ## Data valuation Valuation is handled by a dedicated `$this->setPanelData(string $panelWidgetKey, array $data)` in the Dashboard class. Example: ```php class MyDashboard extends SharpDashboard { // ... protected function buildWidgets(WidgetsContainer $widgetsContainer): void { $widgetsContainer ->addWidget( SharpPanelWidget::make('my_panel') ->setTitle('My custom panel') ->setTemplate(view('sharp.templates.dashboard_panel')) // Must be an existing blade view ); } public function buildWidgetsData(): void { // ... $this->setPanelData('my_panel', [ // Add here every data required by the blade template 'author' => $author, 'post_count' => $count, ]); } } ``` --- --- url: /docs/9.x/guide/dashboard-widgets/figure.md --- # Figure widget This widget is intended to display a single figure, with an optional evolution indicator. ## Attributes (setters) ```php $widgetsContainer->addWidget( SharpFigureWidget::make('sales') ->setTitle('Total sales in €') ->setLink(LinkToEntityList::make('orders')->addFilter(StateFilter::class, 'confirmed')) ); ``` Note that the `setLink()` method is expecting a [LinkTo... instance](../link-to.md). ## Data valuation Valuation is handled by a dedicated `$this->setFigureData(string $figureWidgetKey, string $figure, ?string $unit = null, ?string $evolution = null)` in the Dashboard class: ```php class MyDashboard extends \Code16\Sharp\Dashboard\SharpDashboard { // [...] public function buildWidgetsData(): void { $this->setFigureData('sales', 135, 'k€', '+3%'); } } ``` Of course in a real word project you would probably fetch the data from your database, and compute the evolution from a comparison period. The fourth parameter, `$evolution`, is optional and will display a green figure with a ↑ when starting with a `+`, and a red figure with a ↓ when starting with a `-` sign. --- --- url: /docs/9.x/guide/dashboard-widgets/ordered-list.md --- # Ordered list widget This widget intends to display data as an ordered list of items ```php $widgetsContainer->addWidget( SharpOrderedListWidget::make('bestSellers') ); ``` ## Data valuation Valuation is handled by a dedicated `$this->setOrderedListData(string $panelWidgetKey, array $data)` in the Dashboard class: ```php function buildWidgetsData(): void { $this->setOrderedListData( 'bestSellers', [ [ 'label' => 'model EF5978', 'count' => 89 ], [ 'label' => 'model TT4448', ], [ 'label' => 'model EF5978', 'count' => 17 ], [ 'label' => 'model YY5557' ] ] ); } ``` Pass there the widget `key` and an array with the data as an array. Each item of the array should be an associative array. The key `label` is mandatory as it defines the ordered list item main content. You can also optionally define a count with key `count` associated with a number, it will show a badge with given value. Here's a more realistic example with data fetched from a Model: ```php $this->setOrderedListData( 'bestSellers', Product::orderBy('sales_count', 'desc') ->take(5) ->get() ->map(function (Product $product) { return [ 'id' => $product->id, 'label' => $product->name, 'count' => $product->sales_count, ]; }) ->values() ->all() ); ``` ## Item URL You may want to add a link on each row. To do that, use the `buildItemLink()` method on the widget creation: ```php $widgetsContainer->addWidget( SharpOrderedListWidget::make('bestSellers') ->buildItemLink(fn ($item) => url('some-link')) ); ``` In order to make a link to a Sharp EntityList, Show or Form, this method can also return a [LinkTo instance](../link-to.md): ```php $widgetsContainer->addWidget( SharpOrderedListWidget::make('bestSellers') ->buildItemLink(function ($item) { return LinkToShowPage::make('products', $item['id']); }) ); ``` As you can see, the link is built for each row, and is therefore data-dependant. --- --- url: /docs/9.x/guide/commands.md --- # Write a Command Commands in Sharp are a powerful way to integrate functional processes in the content management. They can be used to re-send an order to the customer, to synchronize pictures of a product, or to preview a page for instance. Commands can be defined in an Entity List, in a Show Page or in a Dashboard. This documentation will focus on the Entity List, but the API is very similar in all three cases as explained at the end of this page. ## Generator for an 'Entity' command ```bash php artisan sharp:make:entity-command [--wizard,--form] ``` ## Generator for an 'Instance' command ```bash php artisan sharp:make:instance-command [--wizard,--form] ``` ## Write the Command class First we need to write a class for our Command. It must extend the `Code16\Sharp\EntityList\Commands\EntityCommand` abstract class (for "entity commands", more on that below), and implement two functions. * `label(): string`: must return the text label of the Command, displayed to the user * `execute(array $data=[]): array` handles the work of the Command itself. ```php class ReloadCommand extends EntityCommand { public function label(): string { return 'Reload full list'; } public function execute(array $data=[]): array { return $this->reload(); } } ``` ### Command scope: instance or entity The example above is an "entity" case, which is reserved to Entity Lists: Command applies to a subset of instances, or all of them. To get the Entity List context (search, page, filters...), you can check `$this->queryParams`, just like in the Entity List itself. To create an instance Command (relative to a specific instance, which can be placed on each Entity List row, or in a Show Page), the Command class must extend `Code16\Sharp\EntityList\Commands\InstanceCommand`. The execute method signature is a bit different: ```php class PromoteToAdminCommand extends InstanceCommand { public function execute($instanceId, array $params = []): array { // ... } } ``` Here we get an `$instanceId` parameter to identify the exact instance involved. The rest is the same, except for authorization detailed below. ### Add a Command form The second parameter in the `execute()` function is an array named `$data`, which contains values entered by the user in a Command specific form. A use case might be to allow the user to enter a text to be sent to the customer with his invoice. In order to do that, we have first to write a `buildFormFields()` function in the Command class: ```php class SendInvoiceToCustomerCommand extends InstanceCommand { // ... function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormTextareaField::make('message') ->setLabel('Message') ) ->addField( SharpFormCheckField::make('now', 'Send right now?') ->setHelpMessage('Otherwise it will be sent next night.') ); } } ``` The API is the same as building a standard Form (see [Building an Entity Form](building-form.md)). Once this method has been declared, a form will be prompted to the user in a modal as he clicks on the Command. Then, in the `execute()` method, you can grab the entered value and handle validation: ```php class SendInvoiceToCustomerCommand extends InstanceCommand { // ... public function execute($instanceId, array $data = []): array { $this->validate($data, [ 'message' => 'required' ]); Order::findOrFail($instanceId) ->sendInvoice($data['message'], $data['now'] ?? false); return $this->info('Invoice sent.'); } } ``` ::: tip Validation can be extracted to a dedicated `rules()` method instead. ::: #### Initializing form data You may need to display the form filled with some data; in order to do that, you have to implement the `initialData()` method: ```php protected function initialData(): array { return [ 'message' => 'Some initial value' ]; } ``` For an Instance command, add the `$instanceId` as a parameter: ```php protected function initialData($instanceId): array { // ... } ``` This method must return an array of formatted values, like for a regular [Entity Form](building-form.md). This means you can [transform data](how-to-transform-data.md) here: ```php protected function initialData($instanceId): array { return $this ->setCustomTransformer('message', function($value, Order $instance) { return sprintf('Message #%s:', $instance->messages_sent_count); }) ->transform(Order::find($instanceId)); } ``` Note that in both cases (Entity or Instance Command) you can access to the Entity List querystring via the request. ### Configure the command (confirmation text, description, form modal title...) You can tweak this in an optional `buildCommandConfig()` function: ```php public function buildCommandConfig(): void { $this->configureConfirmationText('Sure, really?') ->configureDescription('This action will send a text message to your boss') ->configureFormModalTitle('Text message') ->configureFormModalButtonLabel('Execute'); } ``` Here is the full list of available methods: * `configureConfirmationText(string $confirmationText, ?string $title = null, ?string $buttonLabel = null)`: if set the Command will ask a confirmation to the user before executing (warning: for now, the confirmation will not properly work in a Wizard Command) * `configureDescription(string $description)`: this text will appear under the Command label * `configureFormModalTitle(string $formModalTitle)`: if the Command has a Form, the title of the modal will be its label, or `$formModalTitle` if defined * `configureFormModalButtonLabel(string $formModalButtonLabel)`: if the Command has a Form, the label of the OK button will be `$formModalButtonLabel` * `configureFormModalSubmitAndReopenButton(?string $label = null)`: only useful to Commands with forms; if set, an additional button will be displayed to allow the user to submit the form and immediately reopen the Command; the label of the button will be `$label` if defined. ### Display a Page Alert Override `buildPageAlert(PageAlert $pageAlert): void` to display a message above the Command's Form; [see detailed doc](page-alerts.md). ### Command return types Finally, let's review the return possibilities: after a Command has been executed, the code must return something to tell to the front what to do next. There are eight of them: * `return $this->info(string $message, bool $reload = false)`: displays the entered text in a modal. The second argument allows reloading the page first. * `return $this->reload()`: reload the current page (with context). * `return $this->refresh(mixed $ids)`\*: refresh only instance(s) with an id in `$ids`, which can be either a single id or an array. * `return $this->view(string $bladeView, array $params = [])`: display a view right in Sharp; useful for page previews. * `return $this->html(string $htmlContent)`: display an HTML content. * `return $this->link(string $link, bool $openInNewTab = false)`: redirect to the given path. The second argument, optional (default is `false`), is a boolean to open the link in a new tab. * `return $this->download(string $filePath, ?string $fileName = null, ?string $diskName = null)`: the browser will download the specified file. * `return $this->streamDownload(string $fileContent, string $fileName)`: the browser will stream the specified file. \* `refresh()` is only useful in an Entity List case (in a Dashboard or a Show Page, it will be treated as a `reload()`). To make it work properly, you have to slightly adapt the `getListData()` of your Entity List implementation, making use of `$this->queryParams->specificIds()`: ```php class OrderList extends SharpEntityList { // ... function getListData(): array|Arrayble { return Order::query() ->when($this->queryParams->specificIds(), fn ($query, $ids) => $query->whereIn('id', $ids)) ->transform($orders->get()); } } ``` ### Display notifications In the same fashion as for a Form, you can display notifications after a Command has been executed. Here is an example: ```php public function execute($instanceId, array $data= []): array { // ... $this->notify('This is done.') ->setDetail('As you asked.') ->setLevelSuccess() ->setAutoHide(false); return $this->reload(); } ``` See [form documentation](building-form.md#display-notifications) to learn more about the `notify()` method. ::: warning Ensure to only use `notify()` in a Command that returns `reload()` or `refresh()`, otherwise the notification be delayed to the next browser reload. ::: ## Declare the Command Once the Command class is written, we must add it to the Entity List or Show Page: ```php class OrderList extends SharpEntityList { // ... function getInstanceCommands(): ?array { return [ OrderSendMessage::class ]; } function getEntityCommands(): ?array // Not available in a Show Page { return [ OrderReload::class ]; } } ``` or to the Dashboard: ```php class SalesDashboard extends SharpDashboard { // ... function getDashboardCommands(): ?array { return [ DashboardDownloadCommand::class ]; } } ``` For the command itself, you can type a class name (as show in these examples), a class instance or a Closure. You can optionally specify a command key. Sharp will use the command class name, if missing, as a default behavior, which should be ok in most cases. ```php function getInstanceCommands(): ?array { return [ 'message' => OrderSendMessage::class ]; } ``` ## Handle authorizations It's often mandatory to add authorizations to a Command. Here's how to do that: ### Authorizations for entity Commands Implement the `authorize(): bool` function, which must return a boolean to allow or disallow the Command execution: ```php public function authorize(): bool { return auth()->user()->hasGroup('boss'); } ``` ### Authorizations for instance Commands For instance Commands we have to know the instance involved, which means the signature is different: ```php public function authorizeFor($instanceId): bool { return Order::find($instanceId)->owner_id == auth()->id(); } ``` ### Define an entity Command as primary An Entity List can declare one (and only one) of its entity Commands as "primary". In this case, the command will appear at the top, next to the creation button ("New..."). The idea is to provide more visibility to an important Command, but could also be to replace the creation button entirely (you need to remove the "create" authorization to achieve this). ```php class UserList extends SharpEntityList { // ... function buildListConfig(): void { $this->configurePrimaryEntityCommand(InviteNewUser::class); } function getEntityCommands(): ?array { return [ InviteNewUser::class ]; } } ``` A use case could be to provide a Command with a form for the "create" task, leaving the real Form only for update. ## Commands for Show Page Show Pages can only define instance commands (obviously); apart from that, the API is the same. It's a common pattern to reuse the same instance commands in an Entity List and in a Show Page. Remember that `reload()` return action is treated as a `refresh()`. ### Attach Commands to sections One small difference between Commands in Entity List and in Show Page is that in the latter case it's possible to move the Command to a specific section (of the Show Page layout). To achieve this, you must choose a unique key and attach it to the layout section, and use this key on instance commands declaration: ```php class PostShow extends SharpShow { // ... protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout ->addSection('General', function (ShowLayoutSection $section) { // ... }) ->addSection('Content', function (ShowLayoutSection $section) { $section ->setKey('content-section') // <- The key could be anything ->addColumn(8, function (ShowLayoutColumn $column) { // ... }); }); } public function getInstanceCommands(): ?array { return [ 'content-section' => [ // <- Use the same key here PreviewPostCommand::class, ], EvaluateDraftPostWizardCommand::class, ]; } } ``` With that, the `PreviewPostCommand` will appear alongside the "Content" section. ## Commands for Dashboard Dashboard can use Commands too, with a very similar API, apart for: * There is no Instance or Entity distinction; a command handler must extend `Code16\Sharp\Dashboard\Commands\DashboardCommand`. * A Dashboard Command can not return a `refresh()` action, since there is no Instance. ## Bulk Commands (Entity List only) As seen before, Entity Commands are executed on multiple instances: either all of them, or a sublist based on active filters. But sometimes you may need to execute a Command on a custom list of instances, crafted by the user. In order to allow that, you can: ### Configure the Entity Command to allow an instance selection ```php class MyBulkCommand extends EntityCommand { // ... public function buildCommandConfig(): void { $this->configureInstanceSelectionAllowed(); } } ``` ::: tip You may use `configureInstanceSelectionRequired()` instead to declare that the command can not be executed without a selection. ::: ### Apply the Command to the selected instances Use the `$this->selectedIds()` method to retrieve the list of selected instances ids and apply the Command to them; for instance: ```php class MyBulkCommand extends EntityCommand { // ... public function execute(array $data = []): array { Post::whereIn('id', $this->selectedIds()) ->get() ->each(fn (Post $post) => $post->update(/* ... */)); return $this->refresh($this->selectedIds()); } } ``` ## Wizard Commands A Wizard Command is a special kind of Command that will be executed in a modal, and will be able to display several steps to the user. See [dedicated documentation here](commands-wizard.md). --- --- url: /docs/9.x/guide/commands-wizard.md --- # Write a Wizard Command A Wizard is a multistep Command. A common example would be a first step with a resource selection, and a second step with a message box, pre-filled with the previous selection. In Sharp, Wizard are similar to Commands in many ways: they can be scoped to an instance or to an entity, and can be attached to an Entity List, a Show Page or a Dashboard. A Wizard Command can not be configured as bulk (meaning: with instance selection). ![](./img/v9/wizard-command.gif) ## Generator ```bash php artisan sharp:make:entity-command --wizard php artisan sharp:make:instance-command --wizard ``` ## Write the Wizard Command class The class must extend either: * `Code16\Sharp\EntityList\Commands\Wizards\EntityWizardCommand`: for an Entity command, on an Entity List * `Code16\Sharp\EntityList\Commands\Wizards\InstanceWizardCommand`: for an Instance command, on an Entity List or a Show Page * `Code16\Sharp\Dashboard\Commands\DashboardWizardCommand`: for a Dashboard Command Like any Command, you must extend `label(): string` function, and can extend `buildCommandConfig(): void` (see [Commands documentation](commands.md)). ## Implement the first step of the Wizard Instead of `execute()`, you must implement `executeFirstStep(array $data): array`, or `executeFirstStep(mixed $instanceId, array $data): array` in an instance case. This method, as expected, must contain the execution code of your first step: ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] public function executeFirstStep(array $data): array { // Do something } } ``` You must also implement `protected function buildFormFieldsForFirstStep(FieldsContainer $formFields): void`, to build the first step's form: ### Add a form for the first step Wizard Commands needs forms, one for each step. To build the forms, we use the same API as usual (see [Commands documentation](commands.md)), the only difference is where to put the code. For the first step, you already have the answer, it's in `buildFormFieldsForFirstStep`: ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void { $formFields->addField( SharpFormSelectField::make('posts', Post::pluck('name', 'id')->toArray()) ->setMultiple() ->setLabel('Posts to add to the message') ); } protected function buildFormLayoutForFirstStep(FormLayoutColumn &$column): void { $column->withField('posts'); } } ``` ::: tip The layout is optional: if you don't define one, fields will appear in the order of declaration. In the above case, the method can be entirely removed without any impact. ::: And finally, if you need to set initial data for the form, you should implement: ```php protected function initialDataForFirstStep(): array { return ['name' => 'Bob']; } ``` ### Link a step to another one To tell Sharp to go to the next step, Wizard commands expose a new `toStep(string $step)` action, which expects a string key representing you step: ```php public function executeFirstStep(array $data): array { // Do something return $this->toStep('compose-message'); } ``` This string key must be "sluggable": only chars, carets and underscores. ## Implement further steps ### Add a form to each step There are two options: #### First option: one method for all If your Wizard is small, this could be the right way to proceed. Simply extend the `buildFormFieldsForStep(string $step, FieldsContainer $formFields): void` method, with a test on `$step`: ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] protected function buildFormFieldsForStep(string $step, FieldsContainer $formFields): void { if ($step === 'compose-message') { $formFields->addField( SharpFormTextareaField::make('message') ->setLabel('Message text') ->setRowCount(8), ); } elseif ($step === 'my-other-step') { // ... } } } ``` #### Second option: one method per step This should be a better option in many cases, to clarify things in the Wizard class; you can define a `buildFormFieldsForStepXXX(FieldsContainer $formFields): void`, where `XXX` is the camel cased name of you step. So in our example: ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] public function buildFormFieldsForStepComposeMessage(FieldsContainer $formFields): void { $formFields ->addField( SharpFormTextareaField::make('message') ->setLabel('Message text') ->setRowCount(8), ); } } ``` ### Define form layouts (if needed) By default, fields will appear in the order of declaration, like for a regular Command. In case you need more control, you might want to define a layout; once again, you can use one global method: ```php protected function buildFormLayoutForStep(string $step, FormLayoutColumn &$column): void { // ... } ``` ... or define one per step: ```php protected function buildFormLayoutForStepComposeMessage(FormLayoutColumn &$column): void { // ... } ``` ### Initialize form data You will start to notice a pattern; one method for all: ```php protected function initialDataForStep(string $step): array { // ... } ``` or one method per step: ```php protected function initialDataForStepComposeMessage(): array { // ... } ``` In the Instance case, methods have an `$instanceId` param: `initialDataForFirstStep(mixed $instanceId): array` and `initialDataForStep(string $step, mixed $instanceId): array`. ### Write the execution code of each step Very much like first step, you must define the execution code of each step. And like form declaration, this could be done either in one method, or in one by step: #### One method for all Entity and Dashboard case: ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] public function executeStep(string $step, array $data = []): array { if ($step === 'compose-message') { return $this->toStep('checkout'); } else { // ... } } } ``` Instance case: ```php public function executeStep(string $step, mixed $instanceId, array $data = []): array { // ... } ``` #### One method per step Similarly to forms and layouts; for Entity and Dashboard cases: ```php public function executeStepComposeMessage(array $data = []): array { // ... } ``` Instance case: ```php public function executeStepComposeMessage(mixed $instanceId, array $data = []): array { // ... } ``` ### Validate posted data Validation works the same as for regular Commands, with `$this->validate()`: ```php public function executeStepComposeMessage(array $data = []): array { $this->validate($data, ['message' => 'required']); // ... } ``` ## Link steps, terminate the Wizard As seen before, Wizard commands provide a new `toStep(string $step)` action that can be returned in execution methods. An any point, if a step returns another action (`view`, `download`, `info`...), this will lead to terminate the Wizard. This means that steps are dynamically linked: you can finish after the first step if some data was entered, or link to another one in other cases. If an exception is thrown (an in particular, a `SharpApplicativeException`), the Wizard is also stopped. ## Keep context between steps This is a key part of Wizard commands: each step may need data from the previous one. To achieve this, you may use you regular storage (database) to store some state, but often it's better not to persist anything before the end of the Wizard. For this purpose, you have access to a shared context, maintained between each step, via `$this->getWizardContext()`. You can: * store a value: `$this->getWizardContext()->put('name', 'value')` (typically, in the `execute()` method) * retrieve a value: `$this->getWizardContext()->get('name')` (in the `initialData()` method) * validate the stored values: `$this->getWizardContext()->validate($rules)` (in the `initialData()` method) Consider the following example; first we build and execute the first step; in the process, we save the select post ids in the context: ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void { $formFields->addField( SharpFormSelectField::make('posts', Post::pluck('name', 'id')->toArray()) ->setMultiple() ->setLabel('Posts to add to the message') ); } public function executeFirstStep(array $data = []): array { $this->validate($data, ['posts' => 'required']); $this->getWizardContext()->put('posts', $data['posts']); return $this->toStep('compose_message'); } } ``` For the `compose_message` step, we initialize data based on what is in the context, after validating that te context has post ids (to ensure we are coming from step 1): ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] protected function initialDataForStepComposeMessage(): array { $this->getWizardContext()->validate(['posts' => 'required']); return [ 'message' => collect( ['Here’s a list of posts I think you may like:']) ->merge( Post::whereIn('id', $this->getWizardContext()->get('posts')) ->get() ->pluck('title') ) ->implode("\n"), ]; } } ``` We build the form, and store a result useful for the next step in the context (and so on, until the end): ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { // [...] public function buildFormFieldsForStepComposeMessage(FieldsContainer $formFields): void { $formFields->addField( SharpFormTextareaField::make('message')->setLabel('Message text') ); } public function executeStepComposeMessage(array $data = []): array { $this->validate($data, ['message' => 'required']); $this->getWizardContext()->put('message', $data['message']); return $this->toStep('choose_recipients'); } } ``` ## Tip: compose the code Sharp forces you to define the whole Wizard, with all its steps, in a single Command class. This is wanted, since the Command still has only one label, config, authorization... and is seen as a single process, as it should be. But it can lead to a big file with many rows; for this, there is a simple solution provided by PHP: using traits, one per step. Maybe something like this, where each trait contains step related methods (`initialData`, `buildFormField`, `buildFormLayout`, `execute`): ```php class SendEmailWithPostsWizardCommand extends EntityWizardCommand { use SendEmailStepChoosePosts, SendEmailStepComposeMessage, SendEmailStepSelectRecipients; public function label(): ?string { return 'Compose an email with chosen posts...'; } public function authorize(): bool { return auth()->user()->hasRole('admin'); } } ``` --- --- url: /docs/9.x/guide/authentication.md --- # Authentication Sharp won’t be used as a guest (at least in most cases). It leverages a default authentication system base on Laravel standards that you can configure to fit your needs. You can also entirely override the authentication workflow, as explained at the end of this page. ## Configure user attributes The Sharp login form asks for a login and a password field; to handle the authentication, Sharp has to know what attributes it must test in your User model. Defaults are `email` and `password`, and can be overridden in the Sharp config: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setLoginAttributes('login', 'pwd') ->setUserDisplayAttribute('last_name') ->setUserAvatarAttribute('avatar_url') // [...] } } ``` * The `setUserDisplayAttribute()` is useful to display the user's name in the Sharp UI. Default is `name`. * The `setUserAvatarAttribute()` is useful to display the user's avatar in the Sharp UI. By default, a user icon is displayed instead. ## Login form Sharp provides a login controller and view, which requires a session based guard. If you are in this case, you can use this default implementation and benefit from some classic features. You can display a “Remember me” checkbox to the user, and leverage [rate limiting](https://laravel.com/docs/rate-limiting) to prevent brute force attacks: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->suggestRememberMeOnLoginForm() ->enableLoginRateLimiting(maxAttempts: 3) // [...] } } ``` ## Restrict access to Sharp to some users It's very likely that you don't want to authorize all users to access Sharp. You can fix this in two ways: ### Global access gate A simple way to restrict access to Sharp is to define the `viewSharp` global Gate, in the Service Provider: ```php class SharpServiceProvider extends SharpAppServiceProvider { // [...] public function declareAccessGate(): void { Gate::define('viewSharp', function ($user) { return $user->is_sharp_admin; // Or any check you need }); } } ``` ### Custom guard You can also hook into the [Laravel custom guards](https://laravel.com/docs/authentication#adding-custom-guards) functionality, with this config: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setAuthCustomGuard('sharp') // [...] } } ``` This implies that you defined a “sharp” guard in `config/auth.php`, as detailed [in the Laravel documentation](https://laravel.com/docs/authentication#adding-custom-guards). ## Forgotten password You can activate the classic Laravel workflow of forgotten password with a simple config: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableForgottenPassword() // [...] } } ``` This feature will imply by default that your User model implements a few interfaces, as detailed here: https://laravel.com/docs/passwords#model-preparation (and also refer to the [notification customization](https://laravel.com/docs/passwords#reset-email-customization) part of Laravel’s documentation). And since Sharp was developed to allow various situations, you can tweak this feature depending on your actual implementation. You can provide a custom reset password callback to decide how your user should be updated: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableForgottenPassword(resetCallback: function ($user, $password) { $user->updatePasswordAfterReset($password); }) // [...] } } ``` Or alternatively, you can provide a full `Illuminate\Contracts\Auth\PasswordBroker` implementation, allowing you full control on how the reset should work: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableForgottenPassword(broker: MyPasswordBroker::class) // [...] } } ``` Finally, you can decide to hide the "reset password" link displayed in Sharp’s login form (in case you want to provide this functionality in another way, like a custom command in Sharp for instance): ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableForgottenPassword(showResetLinkInLoginForm: false) // [...] } } ``` These customizations will not interfere with any default behavior that you may have implemented for your app, outside Sharp. ## Allow the current user to change his password Sharp provides a helper trait to quickly build a command that lets the currently authenticated user change his password: `Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait`. Using this trait, you can quickly build a Sharp command, with a few configuration options. The trait will take care of the form, validation and rate-limiting. Note that: * This helper is designed for the “current user changes his own password” scenario. If you need admin-managed password resets for other users, implement a different command with the proper authorization checks. * Persisting the new password is up to you (see example below). ### Configuration and behavior You can configure the behavior of the command with the following methods (should be called in your `buildCommandConfig()` method): * `configureConfirmPassword(?bool $confirm = true)`: (false by default) enable password confirmation. * `configurePasswordRule(Password $rule)`: (default: `Password::min(8)`) change the default password validation rule. * `configureValidateCurrentPassword(?bool $validate = true)`: (true by default) if true, a `password` field that uses Laravel’s `current_password` rule (which compares against the currently authenticated user’s stored password) is added. Make sure you use Eloquent, and that your `User` model stores a hashed password as usual. ### Full example ```php use Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait; // ... class ChangePasswordCommand extends SingleInstanceCommand { use IsChangePasswordCommandTrait; public function buildCommandConfig(): void { $this->configureConfirmPassword() ->configurePasswordRule( Password::min(8) ->numbers() ->symbols() ->uncompromised() ); } protected function executeSingle(array $data): array { // The trait handles validation and rate limiting. auth()->user()->update([ 'password' => $data['new_password'], // Considering hashing is done by the model (cast) ]); $this->notify('Password updated!'); return $this->reload(); } } ``` ::: info In this example we chose to create a `SingleInstanceCommand`, since it’s a common use-case to attach such a command to a "Profile" single Show Page that could be [placed in the user menu](building-menu.md#add-links-in-the-user-profile-menu), but you can decide to create an `EntityCommand` or even an `InstanceCommand` instead. ::: ## User impersonation (dev only) At the development stage, it can be useful to replace the login form by a user impersonation. Sharp allows you to do that out of the box: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableImpersonation() // [...] } } ``` ::: warning By default Sharp will also check the `APP_ENV` value to be `local` (or `testing`) to enable this feature, since this should never hit the production by mistake. You can override this behavior by providing a custom handler class, see below. ::: Configured like this, Sharp will display a dropdown list of all users in the login form, allowing you to select one and be logged in as this user. If you want more control on this users list, or if you need to opt out from this default Eloquent implementation, you can provide either a Closure with must return a key-value array: ```php use \Code16\Sharp\Auth\Impersonate\SharpImpersonationHandler; class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableImpersonation(fn () => User::query() ->where('is_admin', true) ->pluck('email', 'id') ->all() ) // ... } } ``` Or your own handler class, which must extend `Code16\Sharp\Auth\Impersonate\SharpImpersonationHandler`: ```php use \Code16\Sharp\Auth\Impersonate\SharpImpersonationHandler; class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableImpersonation(new class extends SharpImpersonationHandler { public function getUsers(): array { return User::where('is_admin', true) ->get() ->filter(fn ($user) => $user->canImpersonate()) ->pluck('email', 'id') ->all(); } }) // ... } } ``` ## Use a custom authentication workflow You can entirely override the authentication workflow (view and controller) providing your custom endpoint: ```php use \Code16\Sharp\Auth\Impersonate\SharpImpersonationHandler; class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->redirectLoginToUrl('/my_login') ->redirectLogoutToUrl('/my_logout') // [...] } } ``` ## Two-factor authentication (2fa) See [Two-factor authentication](authentication-2fa) ## Passkeys authentication See [Passkeys authentication](authentication-passkeys) --- --- url: /docs/9.x/guide/authentication-2fa.md --- # Two-factor authentication (2fa) Sharp provides a two-factor authentication (2fa) system, out of the box. You can configure it like this: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enable2faByNotification() // or ->enable2faByTotp() // or ->enable2faCustom() // [...] } } ``` With this configuration, Sharp will display a second screen to the user, after a successful password based login, asking for a 6-digit code. This code will be provided to the user depending on the configuration: * `enable2faByNotification()`: a notification is sent to the user (email by default, but you can tweak this, see below) * `enable2faByTotp()`: the user must use a 2fa authenticator app (like Google or Microsoft Authenticator) to generate a code * `enable2faCustom()`: in this case you must provide your own 2fa handler, see below. ### Handling the 2fa code via a notification ::: warning To be able to receive notifications, your User model must use the `Illuminate\Notifications\Notifiable` trait. ::: With this option, Sharp will send a notification to the user, containing the 6-digit code. By default, this notification is sent by email. You can override this behavior by providing your own handler class which must extend `Code16\Sharp\Auth\TwoFactor\Sharp2faNotificationHandler`: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enable2faCustom(\App\Sharp\My2faNotificationHandler::class) // [...] } } ``` ```php class My2faNotificationHandler extends Sharp2faNotificationHandler { protected function getNotification(int $code): Notification { return new My2faDefaultNotification($code); } } ``` ### Handling the 2fa code via a TOTP authenticator app ::: warning This implies a bit more work to implement, but this method is more secure than the notification handler. The out-of-the-box implementation implies that you leverage Eloquent. ::: With this option, Sharp will ask the user to register the app in a 2fa authenticator (like Google or Microsoft Authenticator). The user will have to provide a 6-digit code generated by the app to Sharp, in order to be authenticated. First, require two packages needed for this feature: ```bash composer require pragmarx/google2fa-laravel composer require bacon/bacon-qr-code ``` Then, you'll need to configure the totp handler: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enable2faByTotp() // [...] } } ``` Add three columns in the users table to store the 2fa secret, 2fa recovery codes and 2fa confirmation timestamp. Here’s a migration example: ```php return new class extends Migration { public function up(): void { Schema::table('users', function (Blueprint $table) { $table->text('two_factor_secret') ->after('password') ->nullable(); $table->text('two_factor_recovery_codes') ->after('two_factor_secret') ->nullable(); $table->timestamp('two_factor_confirmed_at') ->after('two_factor_recovery_codes') ->nullable(); }); } }; ``` After that, you must provide a way for your users to register the app in their 2fa authenticator. Sharp can help a lot with that, by extending two built-in Commands; one for activating and one for deactivating 2fa. The idea is to add these commands in a "profile" SingleShow, or in some related Entity List. ```php class Activate2faCommand extends SingleInstanceWizardCommand { use Code16\Sharp\Auth\TwoFactor\Commands\Activate2faViaTotpWizardCommandTrait; } ``` ```php class Deactivate2faCommand extends SingleInstanceCommand { use Code16\Sharp\Auth\TwoFactor\Commands\Deactivate2faViaTotpSingleInstanceCommandTrait; // or Code16\Sharp\Auth\TwoFactor\Commands\Deactivate2faViaTotpEntityCommandTrait } ``` The first command is a wizard which will guide the user through the registration process; the second one is to deactivate the 2fa. Both require to enter a password. You can tweak these commands and provide your own implementation if needed. Finally, if you need more control, you can provide your own handler class via `->enable2faCustom()`, which must extend `Code16\Sharp\Auth\TwoFactor\Sharp2faTotpHandler`. ### Enabling 2fa for some users only Providing your own handler implementation, you can override the `isEnabledFor` method to enable 2fa for some users only: ```php class My2faNotificationHandler extends Sharp2faNotificationHandler // or Sharp2faTotpHandler { public function isEnabledFor($user): bool { return $user->hasGroup('sharp'); } } ``` ### Customize the 2fa form You can also change the default help text display above the 2fa form in the handler: ```php class My2faNotificationHandler extends Sharp2faNotificationHandler // or Sharp2faTotpHandler { public function formHelpText(): string { return sprintf( 'You code was sent via SMS to your phone number ending in %s', substr(User::find($this->userId())->phone, -4) ); } } ``` --- --- url: /docs/9.x/guide/authentication-passkeys.md --- # Passkeys Sharp provides a built-in solution to manage and authenticate with passkeys. Passkeys are a replacement for passwords that provide faster, easier, and more secure sign-ins to websites and apps across a user’s devices. ## Installation Passkeys in Sharp requires the `spatie/laravel-passkeys` package. Follow the [installation instructions](https://spatie.be/docs/laravel-passkeys/installation-setup) of the package **(the JavaScript installation part is not needed for Sharp)**. ## Configuration To enable passkeys in Sharp, use the `enablePasskeys()` method in your `SharpServiceProvider`: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enablePasskeys() // [...] } } ``` By default, Sharp will prompt the user to create a passkey after a successful password-based login if they don't have one yet. You can disable this feature like this : ```php $config->enablePasskeys(promptAfterLogin: false); ``` ## Management in the User Profile Once enabled, Sharp automatically registers a `PasskeyEntity` that you can use in your application. A common use case is to allow users to manage their passkeys from their profile page using a `SharpShowEntityListField`. Here is an example of how to add the passkey management list to a `ProfileSingleShow`: ```php use Code16\Sharp\Auth\Passkeys\Entity\PasskeyEntity; use Code16\Sharp\Show\Fields\SharpShowEntityListField; // ... class ProfileSingleShow extends SharpSingleShow { protected function buildShowFields(FieldsContainer $showFields): void { $showFields // [...] ->addField( SharpShowEntityListField::make(PasskeyEntity::class) ->setLabel('Passkeys') ); } protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout ->addSection('', function (ShowLayoutSection $section) { // [...] }) ->addEntityListSection(PasskeyEntity::class); } // ... } ``` --- --- url: /docs/9.x/guide/entity-authorizations.md --- # Entity authorizations You can check documentation of authorizations for [Commands](commands.md) or [Entity States](entity-states.md). Here we are going to see how we can define authorizations for an entity. ## Available permissions Entities have six permission keys: * `entity`: to see the entity in the side-menu, and to display its Entity List or single Show Page. Without this, the entity is hidden to the user. * `view`: without this, the user can access the Entity list, but not the Show Page nor the Form. * `update`: without this, the user can't access the Form. * `create`: without this, the user can't display the create Form. * `reorder`: without this, the user can't reorder instances in the Entity List (if a [reorder handler](reordering-instances.md) is configured). * `delete`: without this, the user can't delete an instance. ## Globally prohibited actions As a first step, in some cases you may want to forbid some actions to anyone: just an application rule, like "no one can delete an Order", or "no one can edit a User". For this add the permission keys in the `$prohibitedActions` attribute og the Entity class: ```php class UserEntity extends SharpEntity { // ... protected ?string $list = UserSharpList::class; protected array $prohibitedActions = [ 'delete', 'create' ]; } ``` Note that you can't define here the `entity` permission. ## Policies For user-based rules, create a `Policy` class which is just a plain class defining methods for some (or all) permissions. ### Write the class It must extend `Code16\Sharp\Auth\SharpEntityPolicy`: ```php class PostPolicy extends SharpEntityPolicy { public function entity($user): bool { return $user->hasGroup('admin'); } public function view($user, $instanceId): bool { return Post::find($instanceId)?->owner_id == $user->id; } public function update($user, $instanceId): bool { // ... } public function delete($user, $instanceId): bool { // ... } public function create($user): bool { // ... } public function reorder($user): bool { // ... } } ``` Only write methods which don't return true, as this is the default behaviour. ### Configure the policy The policy must be declared in the [Entity class](entity-class.md): ```php class PostEntity extends SharpEntity { // ... protected ?string $policy = PostSharpPolicy::class; } ``` ### Policies for Dashboards The only useful method in case of a Dashboard is `function entity($user)`; apart from this, they work the same. ```php class SalesDashboardPolicy extends SharpEntityPolicy { public function entity($user): bool { return $user->hasGroup('admin'); } } ``` --- --- url: /docs/9.x/guide/context.md --- # Sharp Context Sharp provide a way to grab some request context values in the application code. ## Generalities The class handling the context is `Code16\Sharp\Http\Context\SharpContext`; at any point in the request, you can get it via the global helper: ```php sharp()->context(); ``` ## Current context Let's start with a simple example of how to use the context in a Form to set a field as read-only when the form is in update mode: ```php class MyForm extends SharpForm { // ... function buildFormFields() { $this ->addField( SharpFormTextField::make('key') ->setReadOnly(sharp()->context()->isUpdate()) ) ->addField(/*...*/); } } ``` The SharpContext class allows you to get the following information: ### `entityKey(): ?string` Grab the current entity key. ### `isEntityList(): bool` ### `isShow(): bool` ### `isForm(): bool` Find out the current page type. ### `isUpdate(): bool` ### `isCreation(): bool` In Form case, check the current status. ### `instanceId(): ?string` In Form and Show Page cases, grab the instance id. ## Interact with Sharp's Breadcrumb To interact with Sharp's breadcrumb, you can call: ```php sharp()->context()->breadcrumb(); ``` ... and then use the following methods: ### `currentSegment(): BreadcrumbItem` ### `previousSegment(): BreadcrumbItem` Get the current or previous breadcrumb item. ### `previousShowSegment(?string $entityKeyOrClassName = null, ?string $multiformKey = null): ?BreadcrumbItem` ### `previousListSegment(?string $entityKeyOrClassName = null): ?BreadcrumbItem` Get (if existing) the closest Show or List in the breadcrumb. ::: tip As always, prefer the entity class name to the entity key. For instance: `sharp()->context()->breadcrumb()->previousShowSegment(MyEntity::class)`. ::: ### The `BreadcrumbItem` class A `BreadcrumbItem` instance has most of the methods seen above (note: no `isUpdate()`/`isCreation()` here, since a breadcrumb segment doesn't carry that information): #### `entityKey(): string` #### `isEntityList(): bool` #### `isShow(): bool` #### `isSingleShow(): bool` #### `isForm(): bool` #### `isSingleForm(): bool` #### `instanceId(): ?string` #### `entityIs(string $entityKeyOrClassName, ?string $multiformKey = null): bool` Here's an example of how this information could be useful: imagine you have a Show for a `Post` instance, with an Embedded Entity List of `Comment`. When creating a new `Comment`, you'll need to set its `post_id` attribute on the Form `update()` method. You can for this make use of the breadcrumb context like this: ```php class CommentForm extends SharpForm { // ... function update($id, array $data) { $comment = $id ? Comment::find($id) : new Comment([ 'post_id' => sharp()->context() ->breadcrumb() ->previousShowSegment(PostEntity::class) ->instanceId() ]); $this->save($comment, $data); return $comment->id; } } ``` ## Global and retained filters ### `globalFilterValue(string $handlerClassOrKey): array|string|null` Get the value of a Global Filter: see the [Global Filter documentation](filters.md) to know more about this feature. ### `retainedFilterValue(string $handlerClassOrKey): array|string|null` Get the value of a retained Filter: see the [Retained Filter documentation](filters.md) to know more about this feature. ## Access instances cached by an Entity List This feature is really useful to avoid multiple queries on the same instance in a single request. The [specific documentation is available here](avoid-n1-queries-in-entity-lists.md). --- --- url: /docs/9.x/guide/how-to-transform-data.md --- # How to transform data Data transformation is useful when sending data to the front, which happens in the Entity List `getListData()`, and in the Form or Show Page `find()` methods. ::: warning PRE-REQUISITES Note that transformers need your data models to allow direct access to their attributes, like for instance `product->price`, and to implement `Illuminate\Contracts\Support\Arrayable` interface. Eloquent models fulfill those needs. ::: ## The `transform()` function In an Entity List, a Show Page or a Form, you can use the `transform()` function which will: * apply all custom transformers on your list (see below), * transform the given model(s) into an array, handling pagination if a `Paginator` is provided. Eloquent example in an Entity List: ```php class ProductEntityList extends SharpEntityList { // [...] public function getListData(): array|Arrayable { return $this->transform( Product::with('pictures') ->paginate(50) ); } } ``` Eloquent example in a Form (or a Show Page, they share the API): ```php class ProductForm extends SharpForm { // [...] public function find($id): array { return $this->transform( Product::findOrFail($id) ); } } ``` ## Custom transformers Wa can handle transformations with `setCustomTransformer()`: ```php class ProductEntityList extends SharpEntityList { // [...] function getListData(): array|Arrayable { return $this ->setCustomTransformer( 'price', function ($price, $product, $attribute) { return number_format($price, 2).' €'; } ) ->transform( Product::with('pictures') ->paginate(50) ); } } ``` The `setCustomTransformer()` function takes the key of the attribute to transform, and either a `Closure`, an instance of a class which implements `Code16\Sharp\Utils\Transformers\SharpAttributeTransformer`, or even just the full class name of the latest. ::: tip Note that a custom transformer defined on a missing attribute will add the attribute to the result array. It's a convenient way to add a computed attribute, like for instance a `full_name` built with a bunch of real attributes.\ But if this isn't the wanted behaviour, the solution is to define in the `SharpAttributeTransformer` implementation a public `applyIfAttributeIsMissing()` function, which when returning `false` ensure that Sharp will ignore the attribute if it is missing. ::: ## Transform attribute of a related model (hasMany relationship) Sometimes you would like to transform an attribute of a related model in a hasMany relationship. For instance let's say you want to display the names of the sons of a father in caps: ```php return $this ->setCustomTransformer( "sons[name]", fn ($son) => strtoupper($son->name) ) ->transform($father); ``` The convention in this case is to use an array notation, given that `$father->sons` is a collection of objects with a `name` attribute ## The ":" separator and transformers Sometimes you'll need to reference a related attribute, like for instance the name of the author of a Post, either in an Entity List: ```php class ProductEntityList extends SharpEntityList { // [...] function buildList(EntityListFieldsContainer $fields): void { $fields->addField( EntityListField::make('author:name') ->setLabel('Author') ); } } ``` or in a Form / Show Page: ```php class ProductForm extends SharpForm { // [...] function buildFormFields(FieldsContainer $formFields): void { $formFields->addField( SharpFormTextField::make('picture:legend') ->setLabel('Legend') ); } } ``` The `:` separator used here will be interpreted in `transform()`, and the `$post->author->name` attribute will be used. --- --- url: /docs/9.x/guide/sharp-uploads.md --- # Sharp built-in solution for uploads Uploads are painful. Sharp provide a very opinionated and totally optional solution to handle if you are using Eloquent and the `WithSharpFormEloquentUpdater` trait (see [related documentation](building-form.md)). The proposal is to use a special Sharp Model for all your uploads, and to link them to your Models with Eloquent’s Morph relationships. ## Use `SharpUploadModel` The base Model class is `Code16\Sharp\Form\Eloquent\Uploads\SharpUploadModel`. Just create your own Model class and make it extends this base class. You’ll have to define the Eloquent `$table` attribute to indicate the table name. So for instance, let’s say your Model name choice is `Media`, here’s the class code: ```php use Code16\Sharp\Form\Eloquent\Uploads\SharpUploadModel; class Media extends SharpUploadModel { protected $table = 'medias'; } ``` ### Generator ```bash php artisan sharp:make:media --table= ``` ## Create the migration Sharp provides an artisan command for that: `sharp:create_uploads_migration ` Pass your specific table name in the `table_name` argument ("medias" in our example). This command will create a migration file like this one: ```php class CreateMediasTable extends Migration { public function up() { Schema::create('medias', function (Blueprint $table) { $table->increments('id'); $table->morphs('model'); $table->string('model_key')->nullable(); $table->string('file_name')->nullable(); $table->string('mime_type')->nullable(); $table->string('disk')->default('local')->nullable(); $table->unsignedInteger('size')->nullable(); $table->text('custom_properties')->nullable(); $table->unsignedInteger('order')->nullable(); $table->timestamps(); }); } public function down() { Schema::dropIfExists('medias'); } } ``` ## Link to your Models Now, you need to define the relationships. Let's say you have a Book model, and you want the user to be able to upload its cover and PDF version. **With Laravel 12:** ```php class Book extends Model { public function cover() { return $this->morphOne(Media::class, 'model') ->withAttributes(['model_key' => 'cover']); } public function pdf() { return $this->morphOne(Media::class, 'model') ->withAttributes(['model_key' => 'pdf']); } } ``` **Before Laravel 12:** (The `withAttributes` method is not available before Laravel 12) ```php class Book extends Model { public function cover() { return $this->morphOne(Media::class, 'model') ->where('model_key', 'cover'); } public function pdf() { return $this->morphOne(Media::class, 'model') ->where('model_key', 'pdf'); } } ``` ::: tip Prefer the Laravel 12+ syntax: it offers clarity and, more importantly, simplifies updates by eliminating the need for an additional method (see below). ::: ## Use it! ### Properties By default, you can get the `file_name`, but also `mime_type` and file's `size`. ### Custom properties You can add whatever property you need through custom properties, by setting it: ```php $book->cover->author = 'Tomi Ungerer'; ``` Custom properties are stored in the `custom_properties` column, as JSON. You can retrieve the value the same way: ```php $author = $book->cover->author; ``` ### Thumbnails Thumbnail creation is built-in, you can configure thumbnail disk and base directory: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->configureUploadsThumbnailCreation( // NB: all these values are the default ones thumbnailsDisk: 'public', thumbnailsDir: 'thumbnails', ) // ... } } ``` Then you can create a thumbnail using the `thumbnail` method directly on the upload model: ```php thumbnail(int $width = null, int $height = null, array $modifiers = []); ``` For instance, you can display a 150px width thumbnail in a view like this: ```php My picture ``` Another option is to use the fluent API, calling `thumbnail()` without parameters: ```php $thumb = $book->cover->thumbnail()->setQuality(60)->toJpeg()->make(150); ``` Available methods are: * `setQuality(int $quality)`: set the quality of the thumbnail used by some encoders (default to 90). * `toWebp()`, `toPng()`, `toJpeg()`, `toGif()`, `toAvif()`: force the use of a specific encoder for the thumbnail. * `setAppendTimestamp(bool $appendTimestamp = true)`: append a timestamp to the thumbnail URL (useful for browser cache). * `setAfterClosure(Closure $closure)`: set a closure to be executed after the thumbnail creation. Intended to be used like this: ```php $book->cover ->thumbnail() ->setAfterClosure(function ($wasCreated, $thumbnailPath, $thumbnailDisk) { // Do something... }) ->make(150); ``` * `addModifier(ThumbnailModifier $modifier)`: apply an image modifier (see below). * `make(int $width = null, int $height = null)`: create the thumbnail, with the given size. Must be called last. #### Modifiers You can specify Modifiers to perform image processing on the fly. A Modifier must extend the `Code16\Sharp\Form\Eloquent\Uploads\Thumbnails\ThumbnailModifier` class: ```php class MyModifier extends ThumbnailModifier { public function apply(ImageInterface $image): ImageInterface { // Do something... } } ``` The following modifiers are available out of the box: * `GreyscaleModifier` * `FitModifier`: will center-fit the image within the constraints passed to its constructor, e.g. `new FitModifier($width, $height)`. You can provide a custom Modifier; you’ll need to create a class that extends `Code16\Sharp\Form\Eloquent\Uploads\Thumbnails\ThumbnailModifier`, implementing: * `function apply(ImageInterface $image): ImageInterface`: apply your filter, using the great [Intervention API](https://image.intervention.io/v3). * `function resized(): bool`: must return true if the resize is part of the `apply()` code (optional, default to false). ## Update with Sharp The best part is this: Sharp will take care of everything related to update and store. Declare your upload, as usual, and add a transformer: ```php use Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelFormAttributeTransformer; // ... class MyForm extends SharpForm { function buildFormFields() { $this->addField( SharpFormUploadField::make('cover') ->setLabel('Cover') ->setImageOnly() ->setImageCropRatio('1:1') ->setStorageDisk('local') ->setStorageBasePath('data/Books') ); // ... } function find($id): array { return $this ->setCustomTransformer('cover', new SharpUploadModelFormAttributeTransformer()) ->transform(Book::with('cover')->findOrFail($id)); } // ... } ``` ### Updating custom attributes (Laravel 11 and below) If you use the Laravel 12+ syntax for the relationships, you are done. **Otherwise, you need to add a `getDefaultAttributesFor()` method in your Model**: ```php class Book extends Model { public function cover() { return $this->morphOne(Media::class, 'model') ->where('model_key', 'cover'); } public function getDefaultAttributesFor($attribute) { return $attribute === 'cover' ? ['model_key' => $attribute] : []; } // ... } ``` This will tell SharpEloquentUpdater to add the necessary `model_key` attribute when creating a new upload. Again, this is not needed if you declare the relationship with Laravel 12+ syntax (`->withAttributes(['model_key' => 'cover'])`). And... voilà! From there, Sharp will handle the rest. ### Updating custom attributes So we want to add an `author` custom attribute to our cover field: for this we add the field in the Sharp Entity Form, using the `:` separator to designate a related attribute: ```php $this->addField( SharpFormTextField::make('cover:author') ->setLabel('Author') ); ``` Here we intend to update the `author` attribute of the `cover` relation. ## What about upload lists? So let's say we want to add pictures of inner pages, for our Book. It can be easily done by creating a `morphMany` relation in the Book Model: ```php public function pictures() { return $this->morphMany(Media::class, 'model') ->where('model_key', 'pictures') ->orderBy('order'); } ``` And then add the field in the Sharp Entity Form: ```php $this->addField( SharpFormListField::make('pictures') ->setLabel('Additional pictures') ->setAddable()->setAddText('Add a picture') ->setRemovable() ->setSortable() ->setOrderAttribute('order') ->addItemField( SharpFormUploadField::make('file') ->setImageOnly() ->setStorageDisk('local') ->setStorageBasePath('data/Books/Pictures') ) ); ``` Note that we use the special `file` key for the SharpFormUploadField in the item. #### Updating custom attributes in upload lists ```php $this->addField( SharpFormListField::make('pictures') // ... ->addItemField(SharpFormUploadField::make('file')) ->addItemField(SharpFormTextField::make('legend')) ); ``` In this code, the `legend` designates a custom attribute. ## Preview audio or video upload If the field allows to upload an audio or video file, you can display a preview of it by specifying the `withPlayablePreview` option: ```php class MyForm extends SharpForm { // ... function find($id): array { return $this ->setCustomTransformer( 'video', new SharpUploadModelFormAttributeTransformer(withPlayablePreview: true) ) ->transform(Book::with('video')->findOrFail($id)); } ``` ::: warning This feature is using Laravel's file [Temporary URL](https://laravel.com/docs/12.x/filesystem#temporary-urls) feature which only supports S3 & local driver. ::: --- --- url: /docs/9.x/guide/data-localization.md --- # Data localization in Form and Show Page Sharp can help in data localization handling, both in the Form and in the Show Page. But first, let's mention that it could be perfectly fine to handle data localization with a `locale` field in a Model, and a [List Filter](filters.md): we can call this a full separated localization strategy, where each instance is in one locale only. This chapter is about another strategy, where a `Book` can have English and French title and summary, but a common author name and cover picture. ## Configure the Form First, define which locales the Form should handle: ```php class BookForm extends SharpForm { // [...] function getDataLocalizations() { return ['en', 'fr']; } } ``` ## Configure the form fields Next, each localized field must be marked, using `setLocalized()`: ```php class BookForm extends SharpForm { // [...] function buildFormFields() { $this->addField( SharpFormTextField::make('title') ->setLabel('Title') ->setLocalized() ); } } ``` Once one field at least is localized, the form will present a global locale selector, and additionaly each localized field will have his own locale selector. ## Transform the data accordingly ### General approach Sharp is expecting, for localized fields, a key / value array where the locales are keys. Here's an example of how it could be achieved: ```php class BookForm extends SharpForm { use Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater; // [...] function find($id): array { return $this ->setCustomTransformer('title', function($title, $book) { return [ 'fr' => $book->title_french, 'en' => $book->title_english ]; }) ->transform( Book::findOrFail($id) ); } function update($id, array $data) { $instance = $id ? Book::findOrFail($id) : new Book; $data['title_french'] = $data['title']['fr']; $data['title_english'] = $data['title']['en']; $this ->ignore('title') ->save($instance, $data); return $instance->id; } } ``` ::: info `ignore()` and `save()` come from the `WithSharpFormEloquentUpdater` trait ([see the Eloquent updater documentation](building-form.md)) - a plain `SharpForm` subclass doesn't have them. ::: As you see here, Sharp data structure for localized values is the name of the field suffixed with a dot and the locale. So if `title` is a localized field, and "en" and "fr" locales are configured for the Form, Sharp will expect `title` to be a key / value array with the locales as keys, and will send it back in the `update()` method with this same format. ### Using this format as data structure This data structure is in fact pretty common for localization in the database structure, using JSON-based fields. Spatie's popular [laravel-translatable](https://github.com/spatie/laravel-translatable) package is using it, for instance. With this package, here's how our `Book` Model can be written: ```php class Book extends Model { use Spatie\Translatable\HasTranslations; public $translatable = ['title']; [...] } ``` And since the package, like other, is using this array with locales convention, it should work right away, without any tricks in the Sharp Form: ```php class BookForm extends SharpForm { // [...] function find($id): array { return $this->transform(Book::findOrFail($id)); } function update($id, array $data) { $instance = $id ? Book::findOrFail($id) : new Book; $this->save($instance, $data); return $instance->id; } } ``` ## Validation Validation allows differentiating rules between locales: ```php class BookForm extends SharpForm { // [...] public function rules() { return [ 'title.fr' => 'required', ]; } } ``` ## Display locales on a Show Page First, like expressed before, a solution could be to display both versions for each localized field, mentioning the locale in the field label. But you can also let Sharp display a locale selector, by configuring locales, defining which fields are localized, and transforming data accordingly, very much like for the Form: ```php class BookShow extends SharpShow { // [...] protected function buildShowFields(FieldsContainer $showFields): void { $showFields->addField( SharpShowTextField::make('title') ->setLabel('Title') ->setLocalized() ); } public function getDataLocalizations(): array { return ['en', 'fr']; } } ``` --- --- url: /docs/9.x/guide/sharp-breadcrumb.md --- # Sharp's breadcrumb Under the hood Sharp manages a breadcrumb to keep track of stacked pages. ## Configure entity label In Entity classes, you can define how an entity should be labeled in the breadcrumb with the `label` attribute: ```php class PostEntity extends \Code16\Sharp\Utils\Entities\SharpEntity { // [...] protected string $label = 'Post'; } ``` ## Customize the label on an instance In the Form and in the Show Page, you can define which attribute should be used as the breadcrumb label, if you need to be specific. ```php class PostShow extends \Code16\Sharp\Show\SharpShow { // [...] function buildShowConfig(): void { $this->configureBreadcrumbCustomLabelAttribute('title'); } } ``` As any attribute, you can use a dedicated custom transformer to valuate it as you want: ```php class PostShow extends \Code16\Sharp\Show\SharpShow { // [...] function buildShowConfig(): void { $this->configureBreadcrumbCustomLabelAttribute('breadcrumb_label'); } function find($id): array { return $this ->setCustomTransformer('breadcrumb_label', function($role, $post) { return str($post->title)->limit(20); }) ->transform(Post::findOrFail($id)); } } ``` ::: tip In the Form, the breadcrumb label is only used in one particular case: when coming from an embedded Entity List inside a Show Page. In this case, the Show Page and the Form entity are different, and the breadcrumb helps to keep track of the current edited entity. ::: ## Configure custom labels cache Breadcrumb labels are cached for 30 minutes to reduce DB queries between each navigation. If you don't want to cache them, which means all `SharpShow` in breadcrumb are loaded on every navigation, you can update the config in the SharpServiceProvider: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->configureBreadcrumbLabelsCache(false) // ... } } ``` Alternatively, you can change the cache duration (default is 30 minutes): ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->configureBreadcrumbLabelsCache(duration: 10) // ... } } ``` ### Lazy loading In some cases, having the labels replaced by the default Entity label is acceptable and you want to have less DB queries, you can activate the lazy loading: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableBreadcrumbLabelsLazyLoading(); } } ``` ::: warning Be aware that the user may see the breadcrumb with default entity labels (e.g. "Posts > Post > Category > Edit") when : * a nested page is accessed directly (e.g. direct link) * cached labels are expired ::: ## Hide the breadcrumb If you don't want any breadcrumb, you can hide it in sharp's configuration: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->displayBreadcrumb(false) // [...] } } ``` ## Interact with Sharp's Breadcrumb Refer to [the Context documentation](context.md) to find out how to interact with Sharp's breadcrumb. --- --- url: /docs/9.x/guide/link-to.md --- # Create links to an entity You may need to create a link to an EntityList, a Show Page or a Form. ## Classes Depending on your target, you'll want to use either: * `Code16\Sharp\Utils\Links\LinkToEntityList` * `Code16\Sharp\Utils\Links\LinkToForm` * `Code16\Sharp\Utils\Links\LinkToShowPage` * `Code16\Sharp\Utils\Links\LinkToSingleShowPage` * `Code16\Sharp\Utils\Links\LinkToSingleForm` * `Code16\Sharp\Utils\Links\LinkToDashboard` To create an instance, use the static `make` method, which may take one or two arguments: * For `LinkToEntityList`, `LinkToSingleShowPage`, `LinkToSingleForm` and `LinkToDashboard`: `::make($entityClassOrKey)` * For `LinkToForm` and `LinkToShowPage`: `::make($entityClassOrKey, $instanceId)` ::: tip Prefer using the full class name of the entity instead of an entity key, as it will be more robust to potential renaming. This means you should use `LinkToForm::make(PlayerEntity::class, $id)` instead of `LinkToForm::make('players', $id)`. ::: ## Link use case Each link class has a `renderAsText` method, which will render the link as a `` tag. Let’s see an example, in which we want to list the players of a team in an Entity List column and directly link each one to its form. We leverage a custom transformer to do so: ```php class TeamsList extends \Code16\Sharp\EntityList\SharpEntityList { // ... function getListData(): array|Arrayable { return $this ->setCustomTransformer('players', function($value, $yeam) { return $yeam->players ->map(fn ($player) => LinkToForm::make(PlayerEntity::class, $player->id) ->renderAsText($player->name); // This will render a full tag ) ->implode('
'); }) ->transform(Team::orderBy('name')->get()); } } ``` ## URL use case If you only need the URL and not the `
` tag, use `$link->renderAsUrl()`. ## Handle the breadcrumb In Form or Show Page cases, you may want to handle the breadcrumb. The most common case is to insert a Show Page between an Entity List and a Form. To do so, you can use the `throughShowPage` method: ```php LinkToForm::make(PlayerEntity::class, 3)->throughShowPage()->renderAsUrl(); ``` This will generate the URL corresponding to the breadcrumb * Entity List (player) * Show Page (player #3) * Form (player #3) ### Full control of the breadcrumb In more complex cases you can also handle the full breadcrumb, by using the `withBreadcrumb()` method: ```php LinkToShowPage::make(PlayerEntity::class, 1) ->withBreadcrumb(fn (BreadcrumbBuilder $builder) => $builder ->appendEntityList(TeamEntity::class) ->appendShowPage(TeamEntity::class, 6) ) ->renderAsUrl(), ``` This will generate the URL corresponding to the breadcrumb * Entity List (team) * Show Page (team #6) * Show Page (player #1) ::: warning There is no technical limit to the number of breadcrumb items, but you should keep in mind that Sharp will NOT check the functional validity of the breadcrumb you build (apart for basic checks, like piling up Entity Lists). ::: ## All available methods ### `renderAsText(string $text)` Render the link as a `` tag. ### `renderAsUrl()` Render the link as an URL (string). ### `setTooltip(string $toltip)` Set a link tooltip (only when rendered as link). ### `setSearch(string $searchText)` `LinkToEntityList` only Set a search text. ### `addFilter(string $filterFullClassNameOrKey, string $value)` `LinkToEntityList` only Set a filter and its value; for the filter, you can either pass its custom key or (more conveniently) its full class name. ### `setSort(string $attribute, string $dir = 'asc')` `LinkToEntityList` only Set a default sort. ### `withGlobalFilterValues(array|string $globalFilterValues)` Set a global filter value(s). Ex: `LinkToShowPage::make(...)->withGlobalFilterValues($tenant->id)->renderAsUrl()`. ### `setFullQuerystring(array $querystring)` `LinkToEntityList` only To manually build the querystring (which you should avoid). ### `throughShowPage(boolean $throughShowPage = true)` `LinkToForm` only To generate a list > show > form breadcrumb, instead of (by default) just a list > form. ### `withListEntityKey(string $entityClassOrKey)` `LinkToShowPage` and `LinkToForm` only Allows specifying the entity key of the list, very useful in multi-entities lists (see the [Entity map feature](building-entity-list.md#entity-map)). ### `withBreadcrumb(Closure $closure)` `LinkToForm` and `LinkToShowPage` only To take full control of the breadcrumb, by passing a closure that will receive a `BreadcrumbBuilder` instance. --- --- url: /docs/9.x/guide/global-search.md --- # Implement global search This feature allows the user to globally search across a selected set of entities of your application. ![The global search in action](./img/v9/global-search.png) ## Configuration ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->enableGlobalSearch(\App\Sharp\MySearchEngine::class, 'Search for anything...') // ... } } ``` ## Write the class The search engine class must extend `Code16\Sharp\Search\SharpSearchEngine`, which would imply to implement the `searchFor(array $terms): void` method; here’s an example: ```php class MySearchEngine extends SharpSearchEngine { public function searchFor(array $terms): void { $resultSet = $this ->addResultSet('Posts'); $builder = Post::query(); foreach ($terms as $term) { $builder->where('title', 'like', $term); } $builder ->limit(10) ->get() ->each(function (Post $post) use ($resultSet) { $resultSet->addResultLink( link: LinkToShowPage::make('posts', $post->id), label: $post->title, detail: $post->author->name, ); }); } } ``` This code manipulates `Code16\Sharp\Search\SearchResultSet` objects, which are used to group results by entity type (meaning: you may add several result sets). Here’s a list of methods exposed by this object: * `addResultLink(SharpLinkTo $link, string $label, ?string $detail = null): ResultLink`: add a result in the set, providing a `SharpLinkTo` object (see [documentation](link-to.md)). * `hideWhenEmpty(bool $hideWhenEmpty = true): self`: hide the result set if it’s empty (default is false). * `setEmptyStateLabel(string $emptyStateLabel): self`: override the default empty state label (not used if `hideWhenEmpty()` is true). * `validateSearch(array $rules, array $messages = []): bool`: handle validation, see below. ::: tip In the very likely case you need to query multiple models, write a separate method for each of them (`searchForPosts($terms)`, `searchForOrders($terms)`...), and call them from the `searchFor()` method. ::: ### Validate search terms You may need to validate whatever was typed by the user in the search field. Sharp allow to do so in each result set independently, for convenience: ```php class MySearchEngine extends SharpSearchEngine { public function searchFor(array $terms): void { $resultSet = $this ->addResultSet('Posts'); if (! $resultSet->validateSearch( ['string', 'min:3'], ['min' => 'Please type at least 3 characters'] )) { // No need to query the DB return; } // Search terms are valid, proceed with query // ... } } ``` As you can see in this example, the `validateSearch()` method accepts an array of regular Laravel validation rules, and an optional array of custom validation messages. Sharp will not display results if the validation fails, but a good practice is to return early in this case, to avoid unnecessary queries. ### Authorization You may need to enable global search only for a subset of your users. You can do so by overriding the `authorize()` method in your search engine class: ```php class MySearchEngine extends SharpSearchEngine { // ... public function authorize(): bool { return auth()->user()->isAdministrator(); } } ``` ## Use the search field The search field is available in the top bar of Sharp, and can be called with these pretty standard keyboard shortcuts: `Ctrl+K`, `Cmd+K` (Mac) or simply `/`. --- --- url: /docs/9.x/guide/page-alerts.md --- # Add global page alert This feature makes it possible to add a message (with an alert or not) at the top of an Entity List, a Form (including a Command Form), a Show Page, a Dashboard or an Embed. ![](./img/v9/page-alert.png) A global page alert can be great to provide feedback to the user, to remind him of a particular state, to warn him of potential consequences of a Command... ## Declaration Create a `buildPageAlert()` method: ```php class MyShow extends SharpShow { // ... protected function buildPageAlert(PageAlert $pageAlert): void { $pageAlert ->setLevelInfo() ->setMessage('This post is planned for publication'); } } ``` You can use several styles: `setLevelInfo()`, `setLevelWarning()`, `setLevelDanger()`, `setLevelPrimary()` or `setLevelSecondary()`. ## Dynamic messages To provide a dynamic message, depending on the actual data of the Show, Entity List and so on, you can pass a closure to the `setMessage()` method: ```php 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; }); } } ``` The `$data` array passed to the closure is the result of your `find()` (Show, Form), `getListData()` (Entity List), `buildWidgetsData()` (Dashboard) or `initialData()` (Command) method. ::: tip If your message is complex to build, you can defer to a blade template to encapsulate the logic, eg: `return view('sharp._post-planned-info', ['data' => $data])->render();` ::: ## Add a button link The `setButton()` method allows you to add a link to your alert: ```php class MyShow extends SharpShow { // ... protected function buildPageAlert(PageAlert $pageAlert): void { $pageAlert ->setMessage('This page has been edited recently.') ->setButton('Go to page', route('pages.show', sharp()->context()->instanceId())); } } ``` You can also pass a `SharpLinkTo` object. It's useful for filtering an Entity List, for example: ```php class MyEntityList extends SharpEntityList { // ... protected function buildPageAlert(PageAlert $pageAlert): void { $pageAlert ->setMessage('There are new orders to handle.') ->setButton('See orders', LinkToEntityList::make(MyEntity::class) ->addFilter('is_new', 1) ); } } ``` ## Attach the page alert to a specific section (Show Page and Dashboard only) The `onSection()` method allows you to specify the section where the alert should be displayed (instead of the default, the top of the page): ```php class MyShow extends SharpShow { // ... protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout ->addSection(function (ShowLayoutSection $section) { $section ->setKey('content') ->addColumn(/* ... */); }) ->addSection(/* ... */); } protected function buildPageAlert(PageAlert $pageAlert): void { $pageAlert ->setMessage('This page has been edited recently.') ->onSection('content'); } } ``` --- --- url: /docs/9.x/guide/style-visual-theme.md --- # Style & Visual Theme ### Custom colors The primary color is customisable, and is applied to the header and buttons. Although every hue works well, too light colors aren't supported (e.g. works well with [tailwind colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) >= 600). ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setThemeColor('#004D40') // [...] } } ``` ### Header logo By default, the configured `name` is displayed on the header. If you want to show custom logo, you can do it with this config: ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->setName('My Sharp App') ->setThemeLogo( logoUrl: '/my-sharp-assets/my-custom-logo.svg', logoHeight: '1.5rem', faviconUrl: '/my-sharp-assets/favicon.png' ) // [...] } } ``` The file should be an SVG, you can customize the logo height by setting the `logo_height` config. You can also define a URL for a favicon as the `faviconUrl` argument of the same `setThemeLogo()` method. :::tip With the newly added dark theme, it is recommended to use an SVG logo with `fill="currentColor"` to allow the logo to adapt to the theme, Sharp will handle it for you. ::: ### Login form You can customize the login form with a custom message. ```php class SharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->appendMessageOnLoginForm('sharp.login-page-message') // or a direct message // ->appendMessageOnLoginForm('Display a custom message to your users') // [...] } } ``` The custom message is displayed under the form; you can either provide HTML or the name of a custom blade template file. ```blade Display a custom message to your users ``` ### Injecting CSS If you want to inject custom CSS in Sharp, you can do so by using `loadViteAssets()` or `loadStaticCss()`. Be aware that tailwind classes may clash with Sharp default CSS so you may define a [Tailwind prefix](https://tailwindcss.com/docs/configuration#prefix). ```php class SharpServiceProvider 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 } } ``` --- --- url: /docs/9.x/guide/testing.md --- # Testing ::: tip INFO This page documents the new Testing API. If you use the legacy one, please refer to [Testing (legacy)](testing-legacy.md). ::: Sharp provides a fluent testing API to help you test your Sharp code. These assertions and helpers are designed to be used in Feature tests. ## The `SharpAssertions` trait To use Sharp's testing helpers, include the `Code16\Sharp\Utils\Testing\SharpAssertions` trait in your TestCase class: ```php use Code16\Sharp\Utils\Testing\SharpAssertions; abstract class TestCase extends BaseTestCase { use SharpAssertions; // ... } ``` or in `Pest.php`: ```php use Code16\Sharp\Utils\Testing\SharpAssertions; pest() ->extend(\Tests\TestCase::class) ->use(SharpAssertions::class); ``` ## Authentication ### `loginAsSharpUser($user)` Sharp provides a helper to log in a user. By default, it will use the `SharpAssertions` internal logic to ensure the user is authorized to access Sharp. ```php it('allows the user to access the list', function () { $user = User::factory()->create(); $this ->loginAsSharpUser($user) ->sharpList(Post::class) ->get() ->assertOk(); }); ``` ## Testing Entity Lists Use `sharpList()` to test your Entity Lists. ### `sharpList(string $entityKey)` Starts a fluent interaction with an Entity List. ```php $this->sharpList(Post::class) ->get() ->assertOk() ->assertListData(fn (AssertableJson $data) => $data ->count(3) ->has('0.title', 'My first post') ->etc() ); ``` ### Filtering the list You can use `withFilter()` to apply filters to the list before calling `get()` or a command. ```php $this->sharpList(Post::class) ->withFilter(CategoryFilter::class, 1) ->get() ->assertOk(); ``` ### Entity Commands You can call an Entity Command directly from the list: ```php $this->sharpList(Post::class) ->entityCommand(ExportPosts::class) ->post() ->assertOk() ->assertReturnsDownload('posts.csv'); ``` If the command has a form, you can test it: ```php $this->sharpList(Post::class) ->entityCommand(ExportPosts::class) ->getForm() ->assertFormData(fn (AssertableJson $data) => $data ->where('format', 'xls') ->etc() ) ->post(['format' => 'csv']) ->assertOk(); ``` ### Instance Commands Similarly, you can call an Instance Command: ```php $this->sharpList(Post::class) ->instanceCommand(PublishPost::class, 1) ->post() ->assertOk() ->assertReturnsReload(); ``` ### Multi-step Commands (Wizards) For commands that have multiple steps, you can use `getNextStepForm()`: ```php $this->sharpList(Post::class) ->entityCommand(MyWizardCommand::class) ->getForm() ->post(['step1_data' => 'value']) ->assertReturnsStep('step2') ->getNextStepForm() ->assertFormData(fn (AssertableJson $data) => $data ->where('step2_field', 'default') ->etc() ) ->post(['step2_data' => 'value']) ->assertOk(); ``` ### Deleting an instance ```php $this->sharpList(Post::class) ->delete(1) ->assertOk(); ``` ## Testing Show Pages Use `sharpShow()` to test your Show Pages. ### `sharpShow(string $entityKey, $instanceId)` Starts a fluent interaction with a Show Page. ```php $this->sharpShow(Post::class, 1) ->get() ->assertOk() ->assertShowData(fn (AssertableJson $data) => $data ->where('title', 'My first post') ->where('author', 'John Doe') ->etc() ); ``` ### Instance Commands from Show ```php $this->sharpShow(Post::class, 1) ->instanceCommand(PublishPost::class) ->post() ->assertOk(); ``` ### Deleting an instance ```php $this->sharpShow(Post::class, 1) ->delete() ->assertRedirect(); ``` ### List & dashboard fields Show Pages can contain embedded Entity Lists or Dashboards. You can test them using `sharpListField()` and `sharpDashboardField()`. #### `sharpListField(string $entityKey)` ```php $this->sharpShow(Post::class, 1) ->sharpListField(Comment::class) ->get() ->assertOk() ->assertListData(fn (AssertableJson $data) => $data ->count(5) ); ``` #### `sharpDashboardField(string $entityKey)` ```php $this->sharpShow(User::class, 1) ->sharpDashboardField(UserStatsDashboard::class) ->get() ->assertOk(); ``` ### Nested shows There are some cases where you have nested shows by navigating through Show List fields. You can chain `sharpShow()` calls to simulate the correct breadcrumb : ```php $this->sharpList(Post::class) ->sharpShow(Post::class, 1) ->sharpListField(Comment::class) ->sharpShow(Comment::class, 1) ->get() ->assertOk(); ``` ## Testing Forms Use `sharpForm()` to test your Forms. ### `sharpForm(string $entityKey, $instanceId = null)` Starts a fluent interaction with a Form. If `$instanceId` is provided, it targets an edit form; otherwise, it targets a creation form. ### Creating and Updating ```php // Create $this->sharpForm(Post::class) ->store(['title' => 'New Post']) ->assertValid() ->assertRedirect(); // Update $this->sharpForm(Post::class, 1) ->update(['title' => 'Updated Post']) ->assertValid() ->assertRedirect(); ``` ### Testing the "Creation" or "Edit" request itself If you want to test that the form displays correctly: ```php $this->sharpForm(Post::class, 1) ->edit() ->assertOk() ->assertFormData(fn (AssertableJson $data) => $data ->where('title', 'Existing Post') ->etc() ); ``` From an `AssertableForm` (the result of `edit()` or `create()`), you can also call `update()` or `store()`: ```php $this->sharpForm(Post::class, 1) ->edit() ->update(['title' => 'New title']) ->assertValid(); ``` ## Testing Dashboards Use `sharpDashboard()` to test your Dashboards. ### `sharpDashboard(string $entityKey)` Starts a fluent interaction with a Dashboard. ```php $this->sharpDashboard(MyDashboard::class) ->get() ->assertOk(); ``` ### Filtering the dashboard ```php $this->sharpDashboard(MyDashboard::class) ->withFilter(PeriodFilter::class, ['start' => '2023-01-01', 'end' => '2023-01-31']) ->get() ->assertOk(); ``` ### Dashboard Commands ```php $this->sharpDashboard(MyDashboard::class) ->dashboardCommand(RefreshStats::class) ->post() ->assertOk(); ``` ## Global filters If your app contains global filters, you should be able to test normally, but it will be set to its default value. If you need, you can set a specific value using `withSharpGlobalFilter()`: ```php $this->withSharpGlobalFilter(CompanyFilter::class, 'apple') ->sharpList(Post::class) //... ``` --- --- url: /docs/9.x/guide/testing-legacy.md --- # Testing with Sharp (legacy API) ::: warning This page documents the old Testing API, we recommend using the new [Testing API](/guide/testing). ::: Sharp provides a few assertions and helpers to help you test your Sharp code. ## The `SharpAssertions` trait The `Code16\Sharp\Utils\Testing\SharpAssertions` trait is intended to be used in a Feature test. ```php class PostFormTest extends TestCase { use SharpAssertions; // ... } ``` ### Helpers The trait adds a few helpers: #### `loginAsSharpUser($user)` Logs in the given user as a Sharp user. #### `getSharpShow(string $entityClassNameOrKey, $instanceId)` Call the Sharp API to display the Show Page for the Entity `$entityClassNameOrKey` and instance `$instanceId`. #### `getSharpForm(string $entityClassNameOrKey, $instanceId = null)` Call the Sharp API to display the Form for the Entity `$entityClassNameOrKey`. If `$instanceId` is provided, it will be an edit form, and otherwise a creation one. #### `getSharpSingleForm(string $entityClassNameOrKey)` Call the Sharp API to display the edit Form for the single Entity `$entityClassNameOrKey`. #### `updateSharpForm(string $entityClassNameOrKey, $instanceId, array $data)` Call the Sharp API to update the Entity `$entityClassNameOrKey` of id `$instanceId`, with `$data`. #### `updateSharpSingleForm(string $entityClassNameOrKey, array $data)` Call the Sharp API to update the single Entity `$entityClassNameOrKey` with `$data`. #### `storeSharpForm(string $entityClassNameOrKey, array $data)` Call the Sharp API to store a new Entity `$entityClassNameOrKey` with `$data`. #### `deleteFromSharpList(string $entityClassNameOrKey, $instanceId)` Call the Sharp API to delete an `$entityClassNameOrKey` instance on the Entity List. #### `deleteFromSharpShow(string $entityClassNameOrKey, $instanceId)` Call the Sharp API to delete an `$entityClassNameOrKey` instance on the Show Page. #### `callSharpEntityCommandFromList(string $entityClassNameOrKey, string $commandKeyOrClassName, array $data, ?string $commandStep = null)` Call the `$commandKeyOrClassName` Entity Command with the optional `$data`. In case of a wizard command, here’s how you can specify the step in the `$commandStep` parameter: ```php it('allows the user to use the wizard', function () { // First step, no need to declare any previous step $step = $this ->callSharpEntityCommandFromList( entityClassNameOrKey: MyEntity::class, commandKeyOrClassName: MyWizardCommand::class, data: ['some_key' => 'some value'], ) ->assertOk() ->json('step'); // Get back the step key from the response // Second step $this ->callSharpEntityCommandFromList( entityClassNameOrKey: MyEntity::class, commandKeyOrClassName: MyWizardCommand::class, data: ['another_key' => 'another value'], commandStep: $step // We must specify the step we got from the first call ) ->assertOk(); // ... }); ``` #### `callSharpInstanceCommandFromList(string $entityClassNameOrKey, $instanceId, string $commandKeyOrClassName, array $data, ?string $commandStep = null)` Call the `$commandKeyOrClassName` Instance Command with the optional `$data`. For a wizard command, you can refer to the [previous example](#callsharpentitycommandfromlist-string-entitykey-string-commandkeyorclassname-array-data-string-commandstep-null). #### `callSharpInstanceCommandFromShow(string $entityClassNameOrKey, $instanceId, string $commandKeyOrClassName, array $data, ?string $commandStep = null)` Call the `$commandKeyOrClassName` Instance Command with the optional `$data`. For a wizard command, you can refer to the [previous example](#callsharpentitycommandfromlist-string-entitykey-string-commandkeyorclassname-array-data-string-commandstep-null). #### `withSharpBreadcrumb(Closure $callback): self` Most of the time, the breadcrumb automatically set by Sharp is enough. But sometimes it can be useful to define a whole Sharp context before calling an endpoint, and that's the purpose of this method. The `$callback` contains a built instance of Code16\Sharp\Utils\Links\BreadcrumbBuilder, which can be used like this: ```php it('allows the user to display a leaf form', function () { $this ->loginAsSharpUser() ->withSharpBreadcrumb(function (BreadcrumbBuilder $builder) { return $builder ->appendEntityList(TreeEntity::class) ->appendShowPage(TreeEntity::class, 6) ->appendShowPage(LeafEntity::class, 16); }) ->getSharpForm(LeafEntity::class, 16) ->assertOk(); }); ``` #### `withSharpGlobalFilterValues(array|string $globalFilterValues): self` You can specify the global filter values to use in the Sharp context. ```php it('allows the user to display a leaf form', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $this ->loginAsSharpUser($user) ->withSharpGlobalFilterValues($tenant->id) ->getSharpForm(LeafEntity::class, 16) ->assertOk(); }); ``` --- --- url: /docs/9.x/guide/artisan-generators.md --- # Artisan Generators For more information on each command and its options & arguments run `php artisan --help` ```bash # Prompt Generator (interactive) php artisan sharp:generator # Generate an entity php artisan sharp:make:entity [--label,--dashboard,--show,--form,--policy,--single] # Generate the Menu class php artisan sharp:make:menu # Generate the SharpServiceProvider class php artisan sharp:make:provider # Generate a Dashboard class php artisan sharp:make:dashboard # Generate an Entity List php artisan sharp:make:entity-list [--model=] # Generate a Form php artisan sharp:make:form [--model=,--single] # Generate a Show Page php artisan sharp:make:show-page [--model=,--single] # Generate a Policy php artisan sharp:make:policy [--single] # Generate an Entity Command php artisan sharp:make:entity-command [--wizard,--form] # Generate an Instance Command php artisan sharp:make:instance-command [--wizard,--form] # Generate a Entity List Filter php artisan sharp:make:entity-list-filter [--required,--multiple,--date-range,--check] # Generate a ReorderHandler php artisan sharp:make:reorder-handler [--model=] # Generate a Entity State php artisan sharp:make:entity-state [--model=] # Generate sharp media model php artisan sharp:make:media [--table=] ``` --- --- url: /docs/9.x/guide/upgrading/9.0.md --- # Upgrading from 8.x to 9.x 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. # General ## Get new assets, clear cache This is true for every update: be sure to grab the latest assets and to clear the view cache: ```bash php artisan vendor:publish --tag=sharp-assets --force php artisan view:clear ``` ## Update your composer.json The command used to publish sharp's assets **changed**, you should update your `composer.json`: ```diff { "scripts": { "post-autoload-dump": [ [...], - "@php artisan vendor:publish --provider='Code16\\Sharp\\SharpServiceProvider' --tag=assets --force", + "@php artisan vendor:publish --tag=sharp-assets --force" ] } } ``` ## Deprecated methods have been removed * Entity List: deprecated `buildListFields()` and `buildListLayout()` were removed, use `buildList()` instead * Form: deprecated `delete()` method was removed (since it was moved to show / entity list in 8.x) ## New way to configure Sharp, via a dedicated builder class 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: ```php 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: ```php // 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: ```php 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](https://laravel.com/docs/providers#registering-providers) in your app. ::: ## Middleware updates (legacy config only) 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: ```php // 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, ] ], ] ``` ## Migration to `blade-icons` In 8.x, menu icons were FontAwesome classes like `fa fa-user` or `fas fa-user`. Now icons must be [blade-icons](https://blade-ui-kit.com/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: ```bash composer require owenvoke/blade-fontawesome ``` And rename old icon names to blade-fontawesome names in your `SharpMenu` : ```diff # 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](https://docs.fontawesome.com/v5/web/setup/upgrade-from-v4#icon-name-changes-between-version-4-and-5). ### Restore fontawesome icons default behavior *(optional)* As the default behavior of `blade-icons` (which uses an `svg` tag) differs from the previous behavior of FontAwesome’s `` 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: ```bash php artisan vendor:publish --tag=blade-fontawesome-config ``` Then add default attributes for each FontAwesome classes (solid, regular, brands, and optionally pro): ```php // file: config/blade-fontawesome.php return [ // ... 'regular' => [ // ... 'attributes' => [ 'width' => '1rem', 'height' => '1rem', 'style' => 'display: inline;' ], ], ]; ``` ## No more Bootstrap.css / Font awesome classes 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: * If you were using bootstrap classes like `row` / `col` / `badge` in HTML content. These are no longer available, you can either : * Convert to inline CSS (for bootstrap grid classes / utilities) * Inject a custom CSS file as described [here](../style-visual-theme.md) * If you were using inline Font Awesome icons like `` * Using [blade-fontawesome](https://github.com/owenvoke/blade-fontawesome) component like : ``. In Sharp 9.x all templates are now Blade. * For `` inside a custom transformer of an EntityList field, you must now do `Blade::render('')` ## Page Alerts (aka global messages) are not based on Vue templates anymore 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): ```php 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](../page-alerts.md) for more detail. ## Related models handling in custom transformers was fixed (and potentially breaking) 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: ```php $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: ```php $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: ```php $value: 'Joe Doe' $instance: // the Order instance $attribute: 'customer:name' ``` In 9.x: ```php $value: 'Joe Doe' $instance: // the **Customer** instance $attribute: 'name' ``` ## Thumbnails custom filters must be refactored to Modifiers 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 ```php class MyFilter extends ThumbnailFilter { public function applyFilter(Image $image): Image { // ... } } ``` In 9.x ```php 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 ```php $book->cover->thumbnail(100, 100, ['fit'=>['w'=>100, 'h'=>100]]); ``` In 9.x ```php $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()` helper The `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](../context.md)). ## `SharpAuthenticationCheckHandler` is now deprecated in favor of `viewSharp` Gate The 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: ```php class MySharpAuthenticationCheckHandler implements SharpAuthenticationCheckHandler { public function check(Authenticatable $user): bool; { return $user->is_sharp_admin; } } ``` In 9.x: ```php 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](../authentication#global-access-gate). ::: 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. ## Injected CSS must now be loaded with the `SharpConfigBuilder` In 8.x, ```php // config/sharp.php return [ 'extensions' => [ 'assets' => [ 'strategy' => 'vite', 'head' => [ 'resources/css/sharp.css', ], ], ], ]; ``` In 9.x : ```php 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 test assertions were removed 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](../testing.md). Also take note that the `withSharpCurrentBreadcrumb()` method is now deprecated, in favor of the new `withSharpBreadcrumb()` method also documented in the section linked above. # Dashboard ## `SharpWidgetPanel`s are now based on blade template In a similar way to Page Alerts, we abandoned Vue templates for custom `SharpWidgetPanel`s 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: ```php 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: ```php 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](../dashboard-widgets/panel.md) for more detail. # Entity Lists ## Entity Lists have a new authorization: `reorder` Previously 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: ```php class PostPolicy extends SharpEntityPolicy { public function reorder($user): bool { return $user->isEditor(); } // ... } ``` ## Entity List's `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. ## Methods to handle Entity List columns width on small screen were deprecated 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. ## All filters must be declared in order to be used 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: ```php 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 ```php 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. ## Select filter `configureTemplate()` has been dropped If you were using this method, you must do the string transformation in the label of each value. ## New performance optimization for Commands and Policies in Entity List (n+1) 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](../avoid-n1-queries-in-entity-lists.md). # Forms & Shows ## Form and Show layout methods were renamed (old ones are deprecated) 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. ## Form and Show Fields are now formatted even if you don’t transform them 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. ## Custom form / show fields are not supported anymore In 8.x, you could define custom field by creating a Vue component ([in form](https://sharp8.code16.fr/docs/guide/custom-form-fields) or [in show](https://sharp8.code16.fr/docs/guide/custom-show-fields)) 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](../form-fields/html.md) or [SharpShowTextField](../show-fields/text.md). # Forms ## New validation system (and deprecation of the old one) 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: * implement new `rules()` and `messages()` methods in your `SharpForm` or `Command`; those methods accepts an optional `$formattedData` parameter which represents the... formatted posted data. * or make a call to `->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: ```php 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: ```php class PostForm extends SharpForm { public function rules(): array { return [ 'title' => 'required', 'content' => 'required', ]; } // ... } ``` Or: ```php class PostForm extends SharpForm { // ... public function update($id, array $data) { $this->validate($data, [ 'title' => 'required', 'content' => 'required', ]); // ... } } ``` This version brings two huge benefits (besides the fact that it's clearer): * the "delayed creation" thing is gone (hooray!): the `{id}` parameter in a `SharpFormUploadField` storageBasePath isn't anymore an issue in creation case as Sharp will no longer call the `update()` method twice. * **Validation is now called AFTER data formatters**, even in `SharpForm` (it was already the case with `Command`). This cause a breaking change (see below). **If you decide to migrate** to this new validation system (and you should), pay attention to: * remove special workarounds you may have done to handle the "delayed creation" thing, * remove the `.text` suffix you may have added for `SharpFormEditorField` validation rules, * remove the common `.id` suffix you may have added for `SharpFormAutocomplete(Remote|Local)Field` validation rules. Here's a code example (in which editor is a `SharpFormEditorField` and authors is a `SharpFormAutocompleteLocalField`): ```diff class PostForm extends SharpForm { // ... public function rules(): array { return [ - 'editor.text' => 'required', + 'editor' => 'required', - 'authors.id' => 'required', + 'authors' => 'required', ]; } } ``` **If you decide not to migrate just now**, you should ensure that the `\Code16\Sharp\Http\Middleware\Api\BindSharpValidationResolver` middleware in added to the `api` group: either in the (deprecated) config file: ```php // config/sharp.php return [ 'middleware' => [ //... 'api' => [ // ... \Code16\Sharp\Http\Middleware\Api\BindSharpValidationResolver::class, ], ], ] ``` ... or in the shinny new config builder: ```php class MySharpServiceProvider extends SharpAppServiceProvider { protected function configureSharp(SharpConfigBuilder $config): void { $config ->appendToMiddlewareApiGroup( \Code16\Sharp\Http\Middleware\Api\BindSharpValidationResolver::class ) // ... } } ``` ## Localization feature was removed for `SharpFormSelectField`, `SharpFormTagsField` and `SharpFormAutocompleteField` fields Those fields could be localized in 8.x in a weird way: **labels** were localized, but not **values**. This was really misleading, so we decided to remove entirely this behavior in 9.x. The only real impact should be to remove setLocalized() calls in your code for these fields. ## `SharpFormGeolocationField` using Google Maps API must now provide a Map ID Sharp 9.x now uses Advanced Markers which requires a [Map ID](https://developers.google.com/maps/documentation/get-map-id), register it with the following method of the field : ```php class PostForm extends SharpForm { public function buildFormFields(FieldsContainer $formFields): void { $formFields->addField( SharpFormGeolocationField::make('position') ->setMapsProvider('gmaps') ->setApiKey('...') ->setGoogleMapsMapId('...') // new method ); } } ``` ## `SharpFormAutocompleteFormField` was rewritten and need to be migrated First, the `SharpFormAutocompleteFormField` class was split into two classes: `SharpFormAutocompleteLocalField` and `SharpFormAutocompleteRemoteField`, to clearly separate these two different use cases. Second, Vue templates must be migrated to Blade templates (similar to the `SharpWidgetPanel` or Page Alerts migrations). You can either pass a view name or a blade template directly to the newly named `setListItemTemplate()` and `setResultItemTemplate()` methods (the old `setListItemInlineTemplate()`, `setListItemTemplatePath()`, `setResultItemInlineTemplate()` and `setResultItemTemplatePath()` were removed). The `setAdditionalTemplateData()` method was also removed, in favor of a more straightforward way to pass additional data to the template. Example in 8.x: ```php class PostForm extends SharpForm { public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormAutocompleteField::make('author_id', 'remote') ->setRemoteEndpoint('/api/admin/users') ->setListItemInlineTemplate('{{name}}') ->setResultItemInlineTemplate('{{name}}') ) ->addField( SharpFormAutocompleteField::make('category_id', 'local') ->setLocalValues([ 1 => 'Category 1', 2 => 'Category 2', 3 => 'Category 3', ]) ) ->addField( // ... ); } // ... } ``` In 9.x: ```php class PostForm extends SharpForm { public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormAutocompleteRemoteField::make('author_id') ->setRemoteEndpoint('/api/admin/users') ->setListItemTemplate('{{$name}}') ) ->addField( SharpFormAutocompleteLocalField::make('category_id') ->setLocalValues([ 1 => 'Category 1', 2 => 'Category 2', 3 => 'Category 3', ]) ) ->addField( // ... ); } // ... } ``` Finally, there is a big evolution which concerns the remote autocomplete endpoint: your 8.x implementation should still work, but you should note that: * you can now directly write the autocomplete endpoint as a callback closure in the field (no need to use a dedicated route + controller), * external endpoint URLs aren’t supported anymore (you must write a wrapper around this external endpoint, either as a route + controller or via the new callback option). Example in 8.x: ```php class PostForm extends SharpForm { public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormAutocompleteField::make('author_id', 'remote') ->setRemoteEndpoint('/api/admin/users') ->setListItemInlineTemplate('{{name}}') ->setResultItemInlineTemplate('{{name}}') ); } // ... } ``` In 9.x: ```php class PostForm extends SharpForm { public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormAutocompleteRemoteField::make('author_id') ->setRemoteCallback(function ($search) { return User::where('name', 'like', "%$search%")->get(); }) ->setListItemTemplate('{{$name}}') ); } // ... } ``` ## `SharpFormUploadField`’s image related methods were renamed This isn't a breaking change, since the old methods are still available, but deprecated. You should migrate to the new methods: * `setFileFilterImage()` -> `setImageOnly()` * `setCropRatio()` = `setImageCropRatio()` * `shouldOptimizeImage()` = `setImageOptimize()` * `setTransformable()` = `setImageTransformable()` * `setCompactThumbnail()` = `setImageCompactThumbnail()` and in addition: * `setFilterFilter()` -> `setAllowedExtensions()` See [full documentation here](../form-fields/upload.md). ## The API for embedded uploads in Editor field was rewritten The `SharpFormEditorField` no longer has all the upload-related methods directly in the class. Instead, you must use a new `SharpFormEditorUpload` builder class, passed as a parameter to the `allowUploads()` method. In 8.x: ```php // in a SharpForm public function buildFormFields(FieldsContainer $formFields): void { $formFields->addField( SharpFormEditorField::make('content') ->setMaxLength(1000) ->setToolbar([ SharpFormEditorField::B, SharpFormEditorField::A, SharpFormEditorField::UPLOAD, ]) ->setStorageDisk('local') ->setStorageBasePath('data/posts/{id}/embed'), ) ); } ``` In 9.x: ```php // in a SharpForm public function buildFormFields(FieldsContainer $formFields): void { $formFields->addField( SharpFormEditorField::make('content') ->setMaxLength(1000) ->setToolbar([ SharpFormEditorField::B, SharpFormEditorField::A, SharpFormEditorField::UPLOAD, ]) ->allowUploads( SharpFormEditorUpload::make() ->setStorageDisk('local') ->setStorageBasePath('data/posts/{id}/embed') ); ); } ``` See [full documentation here](../form-fields/editor.md). ## New markup for Embedded uploads (in Editor field) If you are using `SharpFormEditorField` uploads, you will need migrate `` and `` elements in the content, meaning in the database. Sharp provides a helper trait intended to be used in your migration like this: ```php use Code16\Sharp\Form\Eloquent\Migrations\MigrateEditorContentsForSharp9; use Illuminate\Database\Migrations\Migration; use Illuminate\Support\Facades\DB; new class extends Migration { use MigrateEditorContentsForSharp9; public function up(): void { $this->updateEditorContentOf(DB::table('posts'), ['content']); } } ``` ## Editor embeds has now blade templates Like Autocomplete / Dashboard Widget panel, Editor embeds must now be blade inline string or a `view('...')` : ### Inline templates Inline templates in 8.x : ```php public function buildEmbedConfig(): void { $this ->configureFormInlineTemplate('
{{ title }}
') ->configureShowInlineTemplate('
{{ title }}
'); } ``` Inline templates in 9.x : ```php public function buildEmbedConfig(): void { $this // if only one template is defined (for both form & show) ->configureTemplate('@if($online)
{{ $title }}
@endif') // if form & show has 2 different templates ->configureFormTemplate('@if($online)
{{ $title }}
@endif') ->configureShowTemplate('@if($online)
{{ $title }}
@endif'); } ``` ### Path templates Path templates in 8.x : ```php public function buildEmbedConfig(): void { $this ->configureFormTemplatePath('sharp/embed.vue') ->configureShowTemplatePath('sharp/embed.vue'); } ``` Path templates in 9.x : ```php public function buildEmbedConfig(): void { $this // if only one template is defined (for both form & show) ->configureTemplate(view('sharp.embed')) // if form & show has 2 different templates ->configureFormTemplate(view('sharp.embed')) ->configureShowTemplate(view('sharp.embed')); } ``` ## `SharpFormListField` collapsed items template feature was removed `setCollapsedItemInlineTemplate()` & `setCollapsedItemTemplatePath()` methods was removed due to limited usage and general migration into blade templates. ## `SharpFormHtmlField` has migrated to blade templates `setInlineTemplate()` & `setTemplatePath()` must be converted to : * `setTemplate('blade template string')` or * `setTemplate(view('sharp.form-field'))`. See [field page](../form-fields/html) for more information. --- --- url: /docs/9.x/guide/upgrading/8.0.md --- # Upgrading from 7.x to 8.x ## Get new assets, clear cache This is true for every update: be sure to grab the latest assets and to clear the view cache: ```bash php artisan vendor:publish --provider="Code16\Sharp\SharpServiceProvider" --tag=assets php artisan view:clear ``` ::: tip Information Due to the migration to Vite, all `.js`, `.css` files moved from\ `/public/vendor/sharp` to `/public/vendor/sharp/assets`. ::: ## implement `Show::delete($id)`, remove `Form::delete($id)` In Sharp 8 we decided to finally move the instance deletion (meaning the Delete command) where it belongs: in the Show Page and the Entity List, and not in the Form as it was before for legacy reasons. This implies that a new Delete command is added in each instance of the Entity List and in the Show Page (of course, depending on authorizations). This means that the Form `delete($id)` method is deprecated in v8 (and will be removed in 9.x), which may impact your code in two ways, depending on your situation: * **All Show Pages must now define a `delete($id)` method** — it should be in most cases a copy / paste of the Form `delete($id)`. In this case, you should remove entirely the Form `delete($id)` method. * For entities without Show Pages, you are not required to update your code because Sharp will detect and call the `Form::delete()` method as a workaround; the right way though (required in 9.x) would be to move the `delete()` method to your Entity List implementation. For custom delete confirmations, you should call the new `SharpShow::configureDeleteConfirmationText(string $confirmationText)` and `SharpEntityList::configureDelete(?bool $hide = false, ?string $confirmationText = null)`. Note: the (undocumented) `deleteSharpShow()` test assertion was also removed, use `deleteSharpEntityList()` or `deleteSharpShow()` instead. ## All deprecated methods were removed Methods that were deprecated in 7.x were removed entirely. This includes: * handling sharp's menu and entity in `sharp.php` config file (use [SharpMenu](../building-menu.md) and [SharpEntity](../entity-class.md) classes instead) * policies which does not extend `Code16\Sharp\Auth\SharpEntityPolicy` (see [Entity policies](../entity-authorizations.md)) * passing a closure for the `$collapsible` param of `ShowLayout::addEntityListSection` (pass a boolean) * old test assertions for commands * `SharpFormUploadField::setCroppable()` replaced with `SharpFormUploadField::setTransformable()` (see [Upload documentation](../form-fields/upload.md)) ## Laravel 10+ and php 8.2+ required It's not a breaking change but minimal requirements are now these. ## New way to build Entity List layout `SharpEntityList`'s `buildListFields()`, `buildListLayout()` and `buildListLayoutForSmallScreens()` are now deprecated, in favor of an easier way to build the layout in a new `buildList()` method. This means we can replace this code from 7.x: ```php class PostList extends SharpEntityList { protected function buildListFields(EntityListFieldsContainer $fieldsContainer): void { $fieldsContainer ->addField( EntityListField::make('cover'), ) ->addField( EntityListField::make('title') ->setLabel('Title'), ) ->addField( EntityListField::make('author:name') ->setLabel('Author') ->setSortable(), ) ->addField( EntityListField::make('published_at') ->setLabel('Published at') ->setSortable(), ); } protected function buildListLayout(EntityListFieldsLayout $fieldsLayout): void { $fieldsLayout ->addColumn('cover', 1) ->addColumn('title', 4) ->addColumn('author:name', 3) ->addColumn('published_at', 4); } protected function buildListLayoutForSmallScreens(EntityListFieldsLayout $fieldsLayout): void { $fieldsLayout ->addColumn('title', 6) ->addColumn('published_at', 6); } // ... } ``` With this in 8.x: ```php class PostList extends SharpEntityList { protected function buildList(EntityListFieldsContainer $fields): void { $fields ->addField( EntityListField::make('cover') ->setWidth(1) ->hideOnSmallScreens(), ) ->addField( EntityListField::make('title') ->setLabel('Title') ->setWidth(4) ->setWidthOnSmallScreens(6), ) ->addField( EntityListField::make('author:name') ->setLabel('Author') ->setWidth(3) ->hideOnSmallScreens() ->setSortable(), ) ->addField( EntityListField::make('published_at') ->setLabel('Published at') ->setWidth(4) ->setWidthOnSmallScreens(6) ->setSortable(), ); } // ... } ``` The old API is still supported to avoid breaking changes, but is deprecated and will be removed in 9.x. This new format is the only one documented in 8.x, here: [Building EntityList](../building-entity-list.md). ## Middleware declaration in config file Sharp now uses the `middleware` key in the `sharp.php` config file to declare the middleware to be applied to all routes. In the unlikely case that you were injecting / replacing a middleware, you should now update this config key. --- --- url: /docs/9.x/guide/upgrading/7.0.md --- # 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: ```bash 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()` * `setBreadcrumbCustomLabelAttribute()` -> `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: ```php function buildListDataContainers(): void { $this ->addDataContainer( EntityListDataContainer::make("name") ->setLabel("Name") ) ->addDataContainer( EntityListDataContainer::make("age") ->setLabel("Age") ); } ``` To this: ```php 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: ```php 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: ```php 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](../building-entity-list.md) 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: ```php function buildFormFields(): void { $this ->addField( SharpFormTextField::make("name") ->setLabel("Name") ); } ``` To this: ```php 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: ```php function buildFormLayout(): void { $this ->addColumn(6, function(FormLayoutColumn $column) { return $column->withSingleField("name") ->withSingleField("email"); }); } ``` To this: ```php 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: ```php function buildShowFields(): void { $this ->addField( SharpShowTextField::make("name") ->setLabel("Name") ); } ``` To this: ```php 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: ```php function buildShowLayout(): void { $this ->addSection("Identity", function(ShowLayoutSection $section) { $section ->addColumn(6, function(ShowLayoutColumn $column) { $column->withSingleField("name"); }); }); } ``` To this: ```php 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: ```php function buildWidgets(): void { $this ->addWidget( SharpBarGraphWidget::make("travels") ); } ``` To this: ```php 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: ```php function buildWidgetsLayout(): void { $this ->addRow(function(DashboardLayoutRow $row) { $row->addWidget(6, "types_pie") ->addWidget(6, "features_bars"); }); } ``` To this: ```php 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): ```php 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: ```php 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: ```php 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: ```php 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: ```php public function buildFormFields(): void { $this ->addField( SharpFormTextField::make("subject") ->setLabel("Subject") ); } ``` To this in 7.0: ```php 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): ```php function buildListConfig(): void { $this->setInstanceIdAttribute("id") ->setSearchable() ->setDefaultSort("name", "asc") ->addFilter("type", SpaceshipTypeFilter::class) ->addFilter("pilots", SpaceshipPilotsFilter::class); } ``` To this: ```php 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: ```php 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: ```php 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): ```php // 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: ```php 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: ```php function buildFormFields(): void { $this ->addField( SharpFormMarkdownField::make("description") ->setLabel("Description") ->setToolbar([ SharpFormMarkdownField::B, SharpFormMarkdownField::I, SharpFormMarkdownField::A, ]) ->setHeight(700) ); } ``` To this: ```php 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 `` 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 `` component: [see dedicated documentation](../form-fields/editor.md#configuration). Before with embed images: ```
{!! sharp_markdown_thumbnails($object->my_markdown_field, "mw-100", 775) !!}
``` or with embed files: ```
{!! sharp_markdown_embedded_files($object->my_markdown_field) !!}
``` After: ```
{!! $object->my_markdown_field !!}
``` ### 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 `` refactoring, markdown with embed objects looks something like: ``` # My title Some text More text ``` 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](https://www.php.net/manual/en/function.preg-replace-callback.php). ## 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](../entity-class.md), 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: ```php // 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](../entity-class.md)). 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: ```php class SpaceshipSharpForm extends SharpForm { protected ?string $formValidatorClass = SpaceshipSharpValidator::class; // ... } ``` See [related documentation](../building-form.md#input-validation), 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: ```php 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](../entity-class.md)). 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](../building-menu.md), 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: ```php $this ->callSharpInstanceCommandFromList( "spaceship", 22, SpaceshipSendMessage::class ) ->assertOk(); ``` --- --- url: /docs/9.x/guide/upgrading/6.0.md --- # Upgrading from 5.x to 6.x This version brings more breaking changes than the previous ones, since we decided to clean up old code parts and to fully embrace PHP 7.x type hinting. ## Laravel 7+ and php 7.4+ required It's not a BC, but still, minimal requirements are now these. ## Type hinting everywhere In order to reinforce the API, we decided to use PHP type hinting everywhere (well, almost everywhere). This means that you'll have to add parameters and return types on EntityLists, Forms, Shows, Filters, Commands, EntityStates... ## Sharp URLs changed Not sure it's really a BC, unless you use direct links to a Sharp EntityList, Show, or Form in a notification for instance. Anyways all URLs changed as a consequence of the new breadcrumb feature. ## `LinkToEntity` must be replaced by `LinkTo[XXX]` classes The 5.0 LinkToEntity class has been removed in favor of: * LinkToEntityList * LinkToForm * LinkToShowPage * LinkToSingleForm * LinkToSingleShowPage See [related documentation](../link-to.md). ## `SharpWidget::setLink()` changed its parameters This method is now expecting a [`LinkTo[XXX]` instance](../link-to.md). ```php SharpPanelWidget::make("activeSpaceships") ->setInlineTemplate("

{{count}}

spaceships in activity") ->setLink(LinkToEntityList::make("spaceship")); ``` ## The Closure of `SharpOrderedListWidget::buildItemLink()` changed its parameters In order to use the new `LinkTo[XXX]` classes, the Closure is now expected to either return a string (URL) or a [`LinkTo[XXX]` instance](../link-to.md). ```php SharpOrderedListWidget::make("widget") ->buildItemLink(function($item) { return LinkToEntityList::make("entity")->addFilter("type", $item['id']); }); ``` ## `SharpContext` was removed in favor of `CurrentSharpRequest` The `Code16\Sharp\Http\Context\SharpContext` class and the `Code16\Sharp\Http\WithSharpContext` trait were removed. If you need to know the current context, use the new `Code16\Sharp\Http\Context\CurrentSharpRequest` class, [documented here](../context.md). ## Deprecated filter classes were removed `EntityListFilter`, `EntityListMultipleFilter`, `EntityListRequiredFilter`, `DashboardFilter`, `DashboardRequiredFilter`, `DashboardMultipleFilter`, which were deprecated in 5.x, were removed (and replaced by `EntityLostSelectFilter`, `EntityListSelectMultipleFilter`, `EntityListSelectRequiredFilter` ...). ## `SharpUploadModelAttributeTransformer` class was removed I was used to EntityList as an attribute transformer in EntityList, for SharpUploadModel. In 6.x, you should used in replacement: `->setCustomTransformer("picture", (new SharpUploadModelThumbnailUrlTransformer(100))->renderAsImageTag())` ## `sharp_markdown_thumbnails()` helper is now deprecated Replaced by `sharp_markdown_embedded_files()`, to take benefit of the feature of file embeds in the Markdown field (see [documentation](../form-fields/editor.md)). ## `EntityState` color const are deprecated `PRIMARY_COLOR`, `SECONDARY_COLOR`, ... are deprecated. For the primary color, use the new config `config('sharp.theme.primary_color')`. For other legacy colors, replace with the following hex code: Const | Color Hex \---|--- PRIMARY\_COLOR | `"#5596E6"` SECONDARY\_COLOR | `"#FD7400"` GRAY\_COLOR | `"#8C9BA5"` LIGHTGRAY\_COLOR | `"#EFF2F5"` DARKGRAY\_COLOR | `"#394B54"` --- --- url: /docs/9.x/guide/upgrading/5.0.md --- # Upgrading from 4.2.x to 5.x First, notice that the 5.X version is the first to follow semver — so do not expect breaking change until 6.x. ## FormUploadModelTransformer was renamed The `Code16\Sharp\Form\Eloquent\Transformers\FormUploadModelTransformer` class was refactored and renamed to `Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelAttributeTransformer` ## The `sharp.extensions.activate_custom_form_fields` was renamed The `sharp.extensions.activate_custom_form_fields` config key was generalized and renamed to `sharp.extensions.activate_custom_fields` --- --- url: /docs/9.x/guide/upgrading/4.2.md --- # Upgrading from 4.1.x to 4.2 Unlike the changed version number would suggest, there is no breaking change in this upgrade, only a major new feature: [Show pages](../building-show-page.md). --- --- url: /docs/9.x/guide/upgrading/4.1.3.md --- # Upgrading from 4.1 to 4.1.3 This should be straightforward, the only breaking change concerns an undocumented (at the time) feature, SharpContext (that's why it's a minor version even if there is technically a BC). ## The `WithSharpFormContext` trait First, the `Code16\Sharp\Http\WithSharpFormContext` trait was renamed to `Code16\Sharp\Http\WithSharpContext` since it can be used in an Entity List context as well. ## The `entityId()` method And second, the `entityId()` method of `SharpContext` was renamed to a much clearer (and less wrongly named...) `instanceId()` method. --- --- url: /docs/9.x/guide/upgrading/4.1.md --- # Upgrading from 4.0 to 4.1 ## Menu syntax was updated In Sharp 4.0, it was allowed to declare the entity `key`, for menus, like this: ```php "menu" => [ [ "label" => "Equipment", "entities" => [ "spaceship" => [ "label" => "Spaceships", "icon" => "fa-space-shuttle" ] ] ] ] ``` The key => value array syntax is now forbidden, for consistency. The right way is: ```php "menu" => [ [ "label" => "Equipment", "entities" => [ [ "entity" => "spaceship", // notice the change here "label" => "Spaceships", "icon" => "fa-space-shuttle" ] ] ] ] ``` Notice there is now a [dedicated doc section for menus](../building-menu.md). ## Dashboards were generalized The "only one Dashboard" limitation is gone, bringing more control and features (policies). As a consequence, if you previously declared a Dashboard, you'll need to adapt your configuration in `sharp.php` , [as documented here](../building-dashboard.md), going from this: ```php return [ "entities" => [ [...] ], "dashboard" => \App\Sharp\Dashboard::class ]; ``` to this: ```php return [ "entities" => [ [...] ], "dashboards" => [ "dashboard" => [ "view" => \App\Sharp\Dashboard::class ] ], [...] "menu" => [ [ "label" => "Company", "entities" => [ [ "label" => "My Dashboard", "icon" => "fa-dashboard", "dashboard" => "dashboard" ], [...] ] ] ] ]; ``` --- --- url: /docs/9.x/guide/custom-form-fields.md --- # Custom form field ## On the front side ### Creating the Vue component Example of custom sharp field: ```vue ``` #### Exposed Props | Prop | Description | |-----------------|---------------------------------------------| | value | value of the field, *required* | | fieldKey | field key in the form | | locale | current locale, `undefined` if the form is not localized | | uniqueIdentifier| Global unique field identifier, corresponding to the laravel error key | | ... | *All other props given in the field definition* | #### Listened events | Event | Description | Parameters | |-----------------|---------------------------------------------|------------| |input | Update the form data with the emitted value, *the force option will change the value even if the field is read-only* | (value, { force: Boolean }) | ### Register the custom field Add `sharp-plugin` npm package to your project: ``` npm install -D sharp-plugin ``` #### Sharp plugin file Add a separated `.js` file in your project to register fields components : *sharp-plugin.js* ```js import Sharp from 'sharp-plugin'; import EmojiPicker from './components/EmojiPicker.vue'; Vue.use(Sharp, { customFields: { 'emojiPicker': EmojiPicker } }) ``` **Important**: The key must be `'emojiPicker'` for `FIELD_TYPE = "custom-emojiPicker"` Vue is exposed to the window scope, it's the current Vue version used by sharp (cf. package.json). ::: warning It's not recommended to use other Vue plugins in this file because it may change the behavior of the Sharp front-end. ::: #### With Laravel Mix *webpack.mix.js* ```js mix.js('/resources/assets/js/sharp-plugin.js', '/public/js') ``` ::: warning The file name must be **sharp-plugin.js** in order to ensure Sharp will find it. ::: You can `.version()` this JS file if you want to. #### With Vite Publish views with: ```bash php artisan vendor:publish --tag=sharp-views ``` Add your `.js` file to `resources/views/vendor/sharp/partials/plugin-scripts.blade.php`: ```blade @vite('resources/js/sharp-plugin.js') ``` ## On the back side ### Activate custom fields in config ```php // config/sharp.php 'extensions' => [ 'activate_custom_fields' => true ], // ... ``` ### Write the form field class and formatter Next step is to build your form field class. It must extend `Code16\Sharp\Form\Fields\SharpFormField`. Here's an example: ```php class SharpCustomFormFieldEmojiPicker extends SharpFormField { const FIELD_TYPE = 'custom-emojiPicker'; protected string $emojiSet = 'native'; public static function make(string $key): self { return new static($key, static::FIELD_TYPE, new TextFormatter); } protected function validationRules(): array { return [ 'emojiSet' => 'in:native,apple,google', ]; } public function toArray(): array { return parent::buildArray([ 'emojiSet' => $this->emojiSet, ]); } } ``` A few things to note: * The `FIELD_TYPE` const must be "custom-" + your custom field name, defined on the front side. * To respect the Sharp API, you must define a static `make` function with at least the field key; this function must call the parent constructor, passing the `$key`, the `FIELD_TYPE` and a Formatter, which can also be a custom one ( see [documentation](building-form.md#formatters) and `Code16\Sharp\Form\Fields\Formatters\SharpFieldFormatter` base class). * `validationRules()` implementation is optional, but advised. * the `toArray()` function is mandatory, and must call `parent::buildArray()` with additional attributes. ### Use it Next step is using the new form field: *in some `Code16\Sharp\Form\SharpForm` subclass:* ```php function buildFormFields(FieldsContainer $formFields): void { $formFields->addField( SharpCustomFormFieldEmojiPicker::make('emoji') ->setLabel('Emoji') ); } ``` --- --- url: /docs/9.x/guide/custom-show-fields.md --- # Custom show field ## On the front side ### Creating the Vue component Example of custom sharp field: ```vue ``` #### Exposed Props | Prop | Description | |-----------------|---------------------------------------------| | value | value of the field, *required* | | fieldKey | field key in the show | | emptyVisible | boolean determined by the [->setShowIfEmpty()](building-show-page.md) method, true by default | | ... | *All other props given in the field definition* | #### Listened events | Event | Description | Parameters | |-----------------|---------------------------------------------|------------| | visible-change | Update the field visibility | Boolean | ### Register the custom field Add `sharp-plugin` npm package to your project: ``` npm install -D sharp-plugin ``` #### Sharp plugin file Add a separated `.js` file in your project to register fields components : *sharp-plugin.js* ```js import Sharp from 'sharp-plugin'; import ShowTitle from './components/ShowTitle.vue'; Vue.use(Sharp, { customFields: { 'title': ShowTitle } }) ``` **Important**: The key must be `'title'` for `FIELD_TYPE = "custom-title"` Vue is exposed to the window scope, it's the current Vue version used by sharp (cf. package.json). ::: warning It's not recommended to use other Vue plugins in this file because it may change the behavior of the Sharp front-end. ::: #### With Laravel Mix *webpack.mix.js* ```js mix.js('/resources/assets/js/sharp-plugin.js', '/public/js') ``` ::: warning The file name must be **sharp-plugin.js** in order to ensure Sharp will find it. ::: You can `.version()` this JS file if you want to. #### With Vite Publish views with: ```bash php artisan vendor:publish --tag=sharp-views ``` Add your `.js` file to `resources/views/vendor/sharp/partials/plugin-scripts.blade.php`: ```blade @vite('resources/js/sharp-plugin.js') ``` ## On the back side ### Activate custom fields in config ```php // config/sharp.php 'extensions' => [ 'activate_custom_fields' => true ], // ... ``` ### Write the show field class and formatter Next step is to build your show field class. It must extend `Code16\Sharp\Show\Fields\SharpShowField`. Here's an example: ```php class SharpCustomShowFieldTitle extends SharpShowField { const FIELD_TYPE = 'custom-title'; protected int $level = 1; public static function make(string $key): self { return new static($key, static::FIELD_TYPE); } public function setTitleLevel(int $level): self { $this->level = $level; return $this; } protected function validationRules(): array { return [ 'level' => 'required|integer|min:1|max:5', ]; } public function toArray(): array { return parent::buildArray([ 'level' => $this->level, ]); } } ``` A few things to note: * The `FIELD_TYPE` const must be "custom-" + your custom field name, defined on the front side. * To respect the Sharp API, you must define a static `make` function with at least the field key; this function must call the parent constructor, passing the `$key` and the `FIELD_TYPE`. * `validationRules()` implementation is optional, but advised. * the `toArray()` function is mandatory, and must call `parent::buildArray()` with additional attributes. ### Use it Next step is using the new show field: *in some `Code16\Sharp\Show\SharpShow` subclass:* ```php function buildShowFields(FieldsContainer $showFields): void { $showFields->addField( SharpCustomShowFieldTitle::make('name') ->setTitleLevel(2) ); } ```