Laravel 开源内容管理框架 Sharp 简介
发布日期:作者: Philippe Lonchampt
让我们从每个 Web 开发人员都知道的两个事实开始
(1) 内容管理很难,无论是在开发人员还是内容管理人员方面:作为开发人员,构建(和维护!)一个合适的工具非常困难,而作为内容管理人员,使用它通常也很痛苦。
(2) 这个主题已经被多次讨论,正如规则所述:“每当一个开发人员遇到一个特定的内容管理案例时,他就会构建一个定制的 CMS”。幸运的是,在 Laravel 世界中有一些优秀的商业项目(例如 Laravel Nova、Statamic),一些非常优秀的开源软件包(例如 Filament),以及其他各种选择(其中之一就是无头 CMS)。
在大多数这些工具出现之前,我在 Laravel 的早期阶段就编写了第一版 Sharp for Laravel,主要是因为我对 WordPress 非常失望——原因很多。Sharp 当时很乱!但在经过几年的努力和大量工作,以及 Antoine Guingand(他加入了 Code 16)在前端的帮助下,我们最终在 2017 年发布了 Sharp 4,我认为这是一款不错的产品。从那时起,我们构建了许多功能并进行了大量的重构,最终发布了最近的 版本 8。
本文应该被视为一篇介绍,内容相当具体:我的目标是通过示例展示如何在一些现有的 Laravel 项目中利用 Sharp 快速有效地构建管理工具,而不是专注于实际的内容管理——这应该是 Sharp 之类的工具的核心,对吗?当然,没错,但是能够将有用的面向业务的工具和信息带给管理员也是非常重要的(也就是说,Sharp 在内容管理方面非常出色)。
开始!第一步:构建一个列表
当您在 Laravel 项目中安装 code16/sharp
(作为 Composer 依赖项)时,您唯一获得的是一个新的 /sharp
路由,其中包含一个登录表单。登录之后的所有操作都必须通过利用 Sharp 的 API 进行配置和开发。
以下是我们的场景:您为一家当地商店构建了一个电子商务网站,该网站与他们用于产品和库存管理的某些软件相关联。最初不需要任何管理部分,因为产品列表来自此软件提供的外部 API,但客户现在希望能够列出在线产品;因此,我们决定将 Sharp 添加到项目中,以便向管理员显示详细的产品列表(参考、价格等)。
为此,我们首先声明一个 ProductEntity
(在 Sharp 中,Entity
是一个可管理的东西;它通常是一个模型,但它可以是任何东西),并开发一个 ProductList
。
class ProductEntity extends \Code16\Sharp\Utils\Entities\SharpEntity{ protected ?string $list = ProductList::class;}
ProductList
代码可以这样编写
// use statements striped for readability class ProductList extends SharpEntityList{ public function __construct(protected ProductApiClient $productApiClient) { } protected function buildListFields(EntityListFieldsContainer $fieldsContainer): void { $fieldsContainer ->addField( EntityListField::make('reference') ->setLabel('Ref.') ->setSortable() ->setWidth(3) ->setWidthOnSmallScreensFill() ) ->addField( EntityListField::make('name') ->setLabel('Name') ->setWidth(6) ->hideOnSmallScreens() ) ->addField( EntityListField::make('price') ->setLabel('Price') ->setSortable() ->setWidth(3) ->setWidthOnSmallScreensFill() ); } public function buildListConfig(): void { $this->configureDefaultSort('reference') ->configureSearchable(); } public function getFilters(): array { return [ ProductPriceFilter::class ]; } public function getListData(): array|Arrayable { return $this ->setCustomTransformer('price', fn ($value) => number_format($value, 2) . ' €'); ->transform( $this->productApiClient ->fetchProducts([ 'sort' => [ 'column' => $this->queryParams->sortedBy(), 'dir' => $this->queryParams->sortedDir(), ], 'price_filter' => match(this->queryParams->filterFor(ProductPriceFilter::class)) { 'sale' => 'sale_only', default => '', }, 'search_query' => $this->queryParams->searchWords(), ]) ); } }
让我们通过快速分解来回顾这段代码
-
buildListField()
包含结构;在本例中,这是一个列表,因此字段是列:我们声明了三个字段,其中两个是sortable
,并使用一些不言自明的代码来定义布局。 -
getListData()
承担了繁重的工作:此方法必须返回每个产品的数组版本,在一个全局数组(或Paginator
,这当然在这里会很合适,但让我们保持简单)。在这里,我们使用在构造函数中注入的ProductApiClient
(此类应该调用外部库存管理软件),传递根据$this->queryParams
中的内容给出的参数,这是一个 Sharp 与用户请求保持同步的对象。还要注意,我们利用了 Sharp 中内置的转换 API,它在很多方面简化了工作。 - 在
buildListConfig()
中,我们可以通过调用以configure...
开头的 API 方法来以多种方式配置列表。 - 在
getFilters()
中声明的过滤器很容易构建(为了保持简单,让我们跳过实现),并允许在列表中显示……嗯,过滤器。
有了这些,我们的列表就起作用了
利用 Eloquent 并添加功能性命令
项目不断发展,现在我们必须提供一种方法来部分更新网站中的产品(也许是为了显示详细的描述和视觉列表)。为此,我们决定用更复杂的计划作业替换简单的 ProductApiClient
,该作业会填充项目数据库中的本地 products
表。
这意味着我们现在有了 Product
Eloquent 模型,因此在我们的 ProductList
中,我们只需要更改 getListData()
代码
class ProductList extends SharpEntityList{ // [...] public function getEntityCommands(): array { return [ SynchronizeProductsFromApi::class ]; } public function getListData(): array|Arrayable { return $this ->setCustomTransformer('price', fn ($value) => number_format($value, 2) . ' €') ->transform( Product::query() ->when($this->queryParams->filterFor(ProductPriceFilter::class), function ($query, $filterValue) { return $query->where('price_status', $filterValue); }) ->when($this->queryParams->hasSearch(), function ($query) { foreach ($this->queryParams->searchWords() as $word) { $query->where('name', 'like', $word); } return $query; }) ->orderBy($this->queryParams->sortedBy(), $this->queryParams->sortedDir()) ->get() ); }}
您可能已经注意到,在这个过程中我们还添加了一个 EntityCommand
,应该使用它来调用产品同步作业。以下是实现方法
class SynchronizeProductsFromApi extends EntityCommand{ public function label(): ?string { return 'Synchronize products...'; } public function buildCommandConfig(): void { $this->configureConfirmationText('Launch a products synchronization, as a background task?'); } public function buildFormFields(FieldsContainer $formFields): void { $formFields ->addField( SharpFormCheckField::make('all', 'Sync all products') ) ->addField( SharpFormDateField::make('start') ->setLabel('Sync products updated after') ->addConditionalDisplay('!all') ); } public function execute(array $data = []): array { $this->validate($data, [ 'start' => [ 'required_if:all,false', 'after:now', ] ]); ProductSynchronizer::dispatch($data['all'] ? null : $data['start']); return $this->info('Synchronization queued. Should be finished in a few minutes.'); }}
此命令使用 Entity 范围定义,这意味着它适用于整个列表,并出现在上面的“操作”下拉菜单中;由于它在 buildFormFields()
方法中定义了表单字段,因此它会在模态框中显示一个表单
以类似的方式,我们可以创建与每一行关联的“实例”范围命令;要了解更多关于此命令的信息,以及关于授权、向导命令、批量命令的信息……请参考 相关文档。
在显示页面中嵌入一个列表
从这里开始,我可以自然地谈论表单,来演示如何使用关系、上传、带有自定义嵌入的富文本、验证等来更新复杂对象——但正如我在本文的介绍中所说,我会跳过这部分,并让您 参考文档 来了解如何处理表单。最后,让我们添加另一个用于客户 Order
的 Entity,但这次我们将重点放在列表和表单之间的可选中间页面上:显示页面。
class OrderShow extends SharpShow{ protected function buildShowFields(FieldsContainer $showFields): void { $showFields ->addField( SharpShowTextField::make('ref') ->setLabel('Reference') ) ->addField( SharpShowTextField::make('created_at') ->setLabel('Date') ) ->addField( SharpShowTextField::make('customer:name') ->setLabel('Name') ) ->addField( SharpShowTextField::make('customer:email') ->setLabel('Email') ) ->addField( // [...] more fields, cut for brievty ) ->addField( SharpShowEntityListField::make('rows', 'product') ->setLabel('Rows') ->hideFilterWithValue('price', 'all') ->showSearchField(false) ->hideEntityCommand([SynchronizeProducts::class]) ->hideFilterWithValue('order', fn ($instanceId) => $instanceId) ); } protected function buildShowLayout(ShowLayout $showLayout): void { $showLayout ->addSection('Order', function (ShowLayoutSection $section) { $section ->addColumn(6, function (ShowLayoutColumn $column) { $column ->withSingleField('ref') ->withSingleField('created_at'); }) ->addColumn(6, function (ShowLayoutColumn $column) { $column ->withFields('total|6', 'shipping_cost|6'); }); }) ->addSection('Customer', function (ShowLayoutSection $section) { // [...] cut for brievty }) ->addEntityListSection('rows'); } protected function find(mixed $id): array { return $this ->setCustomTransformer( 'total', fn ($value, Order $order) => sprintf('%s €', number_format($order->rows->sum('price'), 2)) ) ->setCustomTransformer( 'shipping_cost', fn ($value, Order $order) => sprintf('%s €', number_format(count($order->rows) * 1.8, 2)) ) ->setCustomTransformer( 'created_at', fn ($value, Order $order) => $order->created_at->isoFormat('LLLL') ) ->setCustomTransformer( 'address', fn ($value, Order $order) => $order->delivery_mode === 'ship' ? $value : null ) ->transform(Order::findOrFail($id)); }}
我们在 buildShowFields()
中定义了简单的字段,并在 buildShowLayout()
中决定了它们的显示方式;请注意与列表的一个重大区别:我们有一个 find()
方法,负责返回单个实例。
但我真正想指出的是显示页面的一个特别有用的功能,即我们可以在其中嵌入列表。在上面的代码中,我们声明了一个名为“rows”的 SharpShowEntityListField
,它利用了我们已经开发的 ProductList
,调用 ->hideFilterWithValue('order', fn ($instanceId) => $instanceId)
来过滤仅与特定订单关联的产品。这意味着需要在 ProductList
中处理这个新过滤器
class ProductList extends SharpEntityList{ // [...] public function getListData(): array|Arrayable { return $this // [...] ->transform( Product::query() ->when($this->queryParams->filterFor('order'), function ($query, $orderId) { return $query->whereHas('order', fn ($query) => $query->where('id', $orderId)); }) // [...] ->get() ); }}
以下是结果
这个嵌入式列表具有常规列表的所有功能(过滤器、搜索、排序、重新排序、命令……),而且它可以浏览:单击其中一个产品会将其带到其表单或另一个显示页面,有机会叠加更多内容。面包屑会跟踪您的路径,允许向管理员清晰地显示有用的上下文信息(“我正在编辑‘如何使用 Tailwind’课程 4 月份会话的登记人 John”)。
是的,但是实际应用呢?
首先,请注意,我努力在这篇文章中使用产品和订单,而不是博客文章和作者 😅; 我认为这个虚拟示例足以展示 Sharp 如何尊重我眼中内容管理框架的黄金法则:网站和 CMS 之间没有代码依赖性、术语清晰、无需前端开发,以及可以自由选择持久层。
要查看更高级的示例(它实际上利用了版本 8 的一些新功能,例如 2FA 身份验证、全局搜索、批量命令……),您可以测试 Sharp 的在线演示,它是关于帖子的,并查看 它在 Github 上的代码.
我认为 Sharp 的真正优势在于它需要一些经验,而那就是生产力:在某种程度上,在简单的 CRUD 之上构建复杂的功能会很快,主要关注功能本身,而不会失去对代码的控制或损害项目架构。 而且,因为它具有简单一致的 UI(是的,您可以更改蓝色并添加您的徽标),希望管理员和内容管理员也会喜欢 Sharp。
至于实际应用,在我们网络公司,我们几乎在所有项目中都使用它:网站、移动应用程序、SPA……这里是一些示例,说明了使用方式的多样性
- 在电子商务领域,我们在 Sharp 中开发了一个完整的产品和订单管理系统,允许部分更改和状态更新,保留完整的历史记录,与外部 API 同步以进行运输、库存和退款,提供销售和待办事项仪表板,管理网站内容等等。
- 我们开发的网站非常专注于前端,并包含各种类型的内容(图像、视频、嵌入、自定义嵌入……):对于这类项目,Sharp 的编辑器字段是一个福音。
- 对于具有 API 的移动应用程序,Sharp 用于处理内容,跟踪连接并处理 API 密钥。
- 我们还将 Sharp 用作辅助工具,以便快速添加一种方法来处理现有应用程序上的用户(和 2FA 登录)、权限或配置,这些应用程序已经具有灵活性较差的管理部分。
这就是本文的内容。 联系我以获取任何评论或问题的最简单方法(仍然是)X / twitter,或通过 专用 Discord 服务器.