tonysm / rich-text-laravel
将 Trix 内容与 Laravel 集成
Requires
- php: ^8.2
- illuminate/contracts: ^10.0|^11.0
- spatie/laravel-package-tools: ^1.9.2
- tonysm/globalid-laravel: ^1.1
Requires (Dev)
- laravel/pint: ^1.10
- livewire/livewire: ^3.4
- nunomaduro/collision: ^6.0|^8.0
- orchestra/testbench: ^8.21|^9.0
- orchestra/workbench: ^8.0|^9.0
- phpunit/phpunit: ^10.5
- symfony/html-sanitizer: ^7.0
This package is auto-updated.
Last update: 2024-09-14 22:15:44 UTC
README
将 Trix 编辑器 与 Laravel 集成。灵感来自 Rails 的 Action Text。
安装
您可以通过 composer 安装此包
composer require tonysm/rich-text-laravel
然后,运行以下命令进行安装
php artisan richtext:install
接下来,运行迁移
php artisan migrate
确保将样式 Blade 组件添加到您的布局中
<x-rich-text::styles />
如果您使用 Breeze(或 TailwindCSS),您可能更喜欢经过调整的主题
<x-rich-text::styles theme="richtextlaravel" />
最后,您现在可以在表单上像这样使用已发布的输入 Blade 组件
<x-trix-input id="bio" name="bio" />
就是这样!
概述
我们在保存数据库中的丰富文本字段(使用 Trix)之前提取附件,并最小化内容以进行存储。附件将被替换为 rich-text-attachment
标签。从可附加模型中的附件具有 sgid
属性,这应在您的应用程序中全局识别它们。
当直接存储图像时(例如,在简单的图像上传中,您没有在应用程序中表示该附件的模型),我们将在 rich-text-attachment
中填充所有必要的附件属性以再次渲染该图像。存储最小化(规范)的丰富文本内容意味着我们不会存储附件标签的内部内容,只存储在需要时再次渲染它所需的元数据。
有两种使用此包的方式
- 使用推荐的数据库结构,其中所有丰富文本内容都将存储在具有丰富文本内容的模型之外(推荐);以及
- 仅使用
AsRichTextContent
特性将任何模型的任何表上的丰富文本内容字段进行类型转换。
以下我们将介绍每种使用方法。建议您至少阅读一些时候的 Trix 文档 以获得其客户端的概述。
RichText 模型
推荐的方式是将丰富文本内容本身之外保持。这将使您在操作模型时保持模型精简,并且您可以在需要丰富文本内容的地方(贪婪地或懒加载地)仅加载丰富文本字段。
以下是如何在 Post 模型上创建两个丰富文本字段,例如,您需要一个用于内容正文,另一个用于内部备注
use Tonysm\RichTextLaravel\Models\Traits\HasRichText; class Post extends Model { use HasRichText; protected $guarded = []; protected $richTextAttributes = [ 'body', 'notes', ]; }
此特性将为 Post 模型创建 动态关系,每个字段一个。这些关系将被称为 richText{FieldName}
,您可以使用下划线定义字段,因此如果您有一个 internal_notes
字段,则将在模型上添加一个 richTextInternalNotes
关系。
为了获得更好的 DX,此特性还将为 Post 模型上的 body
和 notes
字段添加自定义类型转换,以便将设置/获取操作转发到关系,因为这些字段将不会存储在帖子表中。这意味着您可以使用 Post 模型如下
$post = Post::create(['body' => $body, 'notes' => $notes]);
并且您可以像与 Post 模型上的任何常规字段一样与丰富文本字段交互
$post->body->render();
再次强调,Post 模型上没有 body
或 notes
字段,这些 虚拟字段 将将交互转发到该字段的关联关系。这意味着,当你与这些字段交互时,实际上是在与 RichText
模型的一个实例交互。该模型将有一个包含丰富文本内容的 body
字段。然后,这个字段被转换为 Content
类的一个实例。对 RichText 模型的调用将被转发到 RichText
模型上的 body
字段,它是一个 Content
类的实例。这意味着,而不是
$post->body->body->attachments();
第一个 "body" 是虚拟字段,它将是 RichText 模型的一个实例,第二个 "body" 是该模型上的丰富文本内容字段,它是一个 Content
类的实例,你可以这样做
$post->body->attachments();
类似于 Content 类,RichText 模型将实现 __toString
魔术方法,并通过将其转换为字符串来渲染 HTML 内容(供最终用户使用),在 blade 中可以这样操作
{!! $post->body !!}
注意:由于 HTML 输出不会被转义,请在渲染之前确保对其进行清理。有关更多信息,请参阅 清理 部分。
HasRichText
特性还会添加一个作用域,您可以使用它来预加载丰富文本字段(请记住,每个字段都有自己的关联关系),您可以使用如下方式
// Loads all rich text fields (1 query for each field, since each has its own relationship) Post::withRichText()->get(); // Loads only a specific field: Post::withRichText('body')->get(); // Loads some specific fields (but not all): Post::withRichText(['body', 'notes'])->get();
此示例的数据库结构可能如下所示
posts
id (primary key)
created_at (timestamp)
updated_at (timestamp)
rich_texts
id (primary key)
field (string)
body (long text)
record_type (string)
record_id (unsigned big int)
created_at (timestamp)
updated_at (timestamp)
我们在 rich_texts
表中存储字段的反向引用,因为一个模型可能有多个丰富文本字段,所以这是在 HasRichText
为您创建的动态关系中使用。此外,该表还有一个唯一约束,这可以防止对同一模型/字段对有多个条目。
将丰富文本内容渲染回 Trix 编辑器的操作与渲染给最终用户略有不同,因此您可以使用字段的 toTrixHtml
方法来完成,如下所示
<x-trix-input id="post_body" name="body" value="{!! $post->body->toTrixHtml() !!}" />
接下来,转到 附件 部分以了解更多关于可附加内容的信息。
加密丰富文本属性
如果您想在存储时加密 HTML 内容,可以在 richTextAttributes
属性中将 encrypted
选项指定为 true
use Tonysm\RichTextLaravel\Models\Traits\HasRichText; class Post extends Model { use HasRichText; protected $guarded = []; protected $richTextAttributes = [ 'body' => ['encrypted' => true], // This will be encrypted... 'notes', // Not encrypted... ]; }
这使用了 Laravel 的加密 功能。默认情况下,我们将使用 Laravel 的 Crypt::encryptString()
进行加密,并使用 Crypt::decryptString()
进行解密。如果您是从 Rich Text Laravel 包的 2 版本迁移过来,默认会使用 Crypt::encrypt()
和 Crypt::decrypt()
,您必须手动迁移您的数据(请参阅 2.2.0 版本的说明)。这是升级到 3.x 的推荐方法。
话虽如此,您可以通过在 AppServiceProvider::boot
方法中调用 RichTextLaravel::encryptUsing()
方法来配置包如何处理加密。此方法接收加密和解密处理程序。处理程序将接收要加密的值、模型和键(字段),如下所示
namespace App\Providers; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\ServiceProvider; use Tonysm\RichTextLaravel\RichTextLaravel; class AppServiceProvider extends ServiceProvider { // ... public function boot(): void { RichTextLaravel::encryptUsing( encryption: fn ($value, $model, $key) => Crypt::encrypt($value), decryption: fn ($value, $model, $key) => Crypt::decrypt($value), ); } }
再次强调,建议您迁移现有的加密数据并使用默认的加密处理程序(请参阅 此处 的说明)。
密钥轮换
Laravel 的加密组件依赖于 APP_KEY
主密钥。如果您需要轮换此密钥,您需要手动使用新密钥重新加密加密的丰富文本属性。
此外,存储的内容附件依赖于Globalid Laravel包。该包基于您的APP_KEY
生成一个派生密钥。当旋转APP_KEY
时,您还需要更新所有存储内容附件的sgid
属性。
AsRichTextContent特性
如果您不想使用推荐的架构(无论是由于您在这里有强烈的观点,还是您想控制自己的数据库结构),您可以选择跳过整个推荐数据库结构,并使用自定义转换AsRichTextContent
在您的富文本内容字段上。例如,如果您在posts
表上存储body
字段,您可以这样做
use Tonysm\RichTextLaravel\Casts\AsRichTextContent; class Post extends Model { protected $casts = [ 'body' => AsRichTextContent::class, ]; }
然后自定义转换将解析HTML内容并将其压缩以进行存储。本质上,它将转换由Trix提交的内容,该内容仅包含一个图像附件
$post->update([ 'content' => <<<HTML <h1>Hello World</h1> <figure data-trix-attachment='{ "url": "http://example.com/blue.jpg", "width": 300, "height": 150, "contentType": "image/jpeg", "caption": "Something cool", "filename":"blue.png", "filesize":1168 }'> <img src="http://example.com/blue.jpg" width="300" height="150" /> <caption> Something cool </caption> </figure> HTML, ])
成为这个压缩版本
<h1>Hello World</h1> <rich-text-attachment content-type="image/jpeg" filename="blue.png" filesize="1168" height="300" href="http://example.com/blue.jpg" url="http://example.com/blue.jpg" width="300" caption="testing this caption" presentation="gallery"></rich-text-attachment>
再次渲染时,它将在rich-text-attachment
标签内重新渲染远程图像。您可以通过简单地回显输出来渲染内容以进行查看,如下所示
{!! $post->content !!}
注意:由于 HTML 输出不会被转义,请在渲染之前确保对其进行清理。有关更多信息,请参阅 清理 部分。
当再次为Trix编辑器提供内容时,您需要这样做
<x-trix-input id="post_body" name="body" value="{!! $post->body->toTrixHtml() !!}" />
编辑器的渲染方式略有不同,因此必须这样。
图像上传
Trix显示了附件按钮,但默认情况下它不起作用,我们必须在我们的应用程序中实现此行为。
附件上传的基本版本可能看起来像这样
- 监听Trix元素(或任何父元素,因为它会冒泡)上的
trix-attachment-add
事件; - 实现上传请求。在此事件中,您可以访问Trix附件实例,因此如果您想的话,可以更新它上的进度,但这不是必需的;
- 一旦上传完成,您必须从上传端点返回
attachmentURL
,您可以使用它来设置附件上的url
和href
属性。就是这样。
该包包含一个包含在Workbench应用程序中实现的基本图像上传功能的应用程序示例。以下是相关链接
- 管理上传的Stimulus控制器(您应该能够将其映射到您想要的任何JavaScript框架)可以在resources/views/components/app-layout.blade.php中找到,查找“rich-text-uploader”Stimulus控制器;
- 上传路由可以在routes/web.php中找到,查找
POST /attachments
路由; - resources/components/trix-input.blade.php中的Trix输入Blade组件。这是随包提供的组件的副本,进行了一些调整。
然而,您并不限于在Trix中进行这种基本的附件处理。更高级的附件行为可以创建自己的后端模型,然后在附件上设置sgid
属性,这会让您在文档在Trix编辑器外渲染时完全控制渲染的HTML。
内容附件
使用Trix,我们可以拥有内容附件。为了解决这个问题,让我们在Trix之上构建一个用户提及功能。有一个很好的Rails Conf演讲,构建了整个功能,但使用Rails。工作流程在Laravel中基本上是相同的。
要将任何模型转换为可附加的,您必须实现 AttachableContract
。您可以使用 Attachable
特性来提供一些基本的可附加功能(它实现了大多数可附加的基本处理),除了必须实现的 richTextRender(array $options): string
方法。此方法用于确定如何在 Trix 内部和外部渲染内容附加。
传递给 richTextRender
的 $options
数组存在于您在画廊中渲染多个模型的情况下,因此在这种情况下,您将获得一个可选的 in_gallery
布尔字段(对于此用户提及示例不适用,因此我们可以忽略它)。
您可以使用 Blade 来渲染可附加的 HTML 部分内容。作为一个参考,Workbench 应用程序提供了一个用户提及功能,该功能可以作为内容附加的示例。以下是一些相关链接
- 实现了
AttachmentContract
的用户模型可以在 用户模型 中找到; - 该模型使用一个名为
Mentionee
的自定义特性,它底层使用了Attachable
特性,因此请查看 Mentionee 特性; - 在前端,我们使用 Zurb 的 Tribute 库在用户在 Trix 中输入
@
符号时检测提及。设置它的 Simulus 控制器可以在 resources/views/components/app-layout.blade.php 中找到。查找“rich-text-mentions”控制器。这与前面提到的 RailsConf 谈话中提到的实现相同,因此如果您需要了解正在发生的事情,请查看该内容。Workbench 应用程序中有两个 Trix 组件,一个用于帖子评论,可以在 resources/views/components/trix-input.blade.php 中找到,另一个用于聊天编辑器,可以在 resources/views/chat/partials/trix-input.blade.php 中找到。在这两个组件中,您将找到一个监听tribute-replaced
事件的data-action
条目,这是 Tribute 为我们派发的创建 Trix 附加的事件,提供用户从下拉菜单中选择的选项; - 提及类将在
GET /mentions?search=
路由中查找提及,您可以在 routes/web.php 中找到它。注意,我们将sgid
和content
字段转换为 Trix 附加,返回的name
字段也用于 Tribute 本身来构建提及功能。 - 渲染用户附加的 Blade 视图可以在 resources/views/mentions/partials/user.blade.php 中找到
您可以在稍后从该富文本内容检索所有附加内容。有关更多信息,请参阅 内容对象 部分。
内容对象
您可能希望在稍后检索富文本内容中的所有可附加对象,并对其进行一些花哨的处理,例如将与帖子模型关联的用户提及实际存储起来。或者,您可以检索该富文本内容中的所有链接并对其进行处理。
获取附件
您可以使用 attachments()
方法在 RichText 模型实例或内容实例中检索富内容字段的全部附件。
$post->body->attachments()
这将返回所有附件的集合,实际上是指所有可附件的内容,例如图像和用户。如果您只想获取特定附件的内容,您可以使用集合上的过滤方法,如下所示
// Getting only attachments of users inside the rich text content. $post->body->attachments() ->filter(fn (Attachment $attachment) => $attachment->attachable instanceof User) ->map(fn (Attachment $attachment) => $attachment->attachable) ->unique();
获取链接
要从富文本内容中提取链接,您可以调用links()
方法,如下所示
$post->body->links()
获取附件画廊
Trix有一个画廊的概念,您可能想要检索所有画廊
$post->body->attachmentGalleries()
这将返回所有图像画廊的DOMElement
集合。
获取画廊附件
您可能还想获取图像画廊内的所有附件。您可以通过以下方式实现
$post->body->galleryAttachments()
这将返回一个包含画廊中所有图像附件的集合(所有附件)。然后您可以通过以下方式检索RemoteImage
附件实例
$post->body->galleryAttachments() ->map(fn (Attachment $attachment) => $attachment->attachable)
无SGID的自定义内容附件
您可能想要附加不需要存储在数据库中的资源。一个例子可能是将聊天消息中的OpenGraph嵌入存储。您可能不想将每个OpenGraph嵌入作为一个独立的数据库记录存储。对于这类数据完整性并非关键的情况,您可以注册一个自定义附件解析器
use App\Models\Opengraph\OpengraphEmbed; use Illuminate\Support\ServiceProvider; use Tonysm\RichTextLaravel\RichTextLaravel; class AppServiceProvider extends ServiceProvider { public function boot() { RichTextLaravel::withCustomAttachables(function (DOMElement $node) { if ($attachable = OpengraphEmbed::fromNode($node)) { return $attachable; } }); } }
此解析器必须返回一个AttachableContract
实现实例或如果节点不匹配您的附件,则返回null
。在OpengraphEmbed
的这种情况下,它可能看起来像这样
namespace App\Models\Opengraph; use DOMElement; use Tonysm\RichTextLaravel\Attachables\AttachableContract; class OpengraphEmbed implements AttachableContract { const CONTENT_TYPE = 'application/vnd.rich-text-laravel.opengraph-embed'; public static function fromNode(DOMElement $node): ?OpengraphEmbed { if ($node->hasAttribute('content-type') && $node->getAttribute('content-type') === static::CONTENT_TYPE) { return new OpengraphEmbed(...static::attributesFromNode($node)); } return null; } // ... }
您可以在聊天工作台演示(或在这个PR中)看到这个OpenGraph示例的完整工作实现。
纯文本渲染
Trix内容可以被转换为任何东西。这本质上意味着HTML > something
。该软件包自带一个HTML > Plain Text
实现,因此您可以通过在Trix内容上调用toPlainText()
方法将其转换为纯文本
$post->body->toPlainText()
例如,以下富文本内容
<h1>Very Important Message<h1> <p>This is an important message, with the following items:</p> <ol> <li>first item</li> <li>second item</li> </ol> <p>And here's an image:</p> <rich-text-attachment content-type="image/jpeg" filename="blue.png" filesize="1168" height="300" href="http://example.com/blue.jpg" url="http://example.com/blue.jpg" width="300" caption="The caption of the image" presentation="gallery"></rich-text-attachment> <br><br> <p>With a famous quote</p> <blockquote>Lorem Ipsum Dolor - Lorense Ipsus</blockquote> <p>Cheers,</p>
将被转换为
Very Important Message
This is an important message, with the following items:
1. first item
1. second item
And here's an image:
[The caption of the image]
With a famous quote
“Lorem Ipsum Dolor - Lorense Ipsus”
Cheers,
如果您正在附加模型,您可以在其上实现richTextAsPlainText(?string $caption = null): string
方法,其中您应该返回该附件的纯文本表示。如果该附件上没有实现此方法,并且Trix附件中没有存储标题,则该附件将不会出现在内容的纯文本版本中。
清理
由于我们正在渲染用户生成的HTML,您必须对其进行清理以避免任何安全问题。即使我们控制输入元素,恶意用户也可能在浏览器中篡改HTML并将其替换为允许他们注入自己的HTML的其他内容。
我们建议使用Symfony的HTML清理器。该存储库中的Workbench应用程序自带一个示例实现。以下是一些相关信息
- 您必须始终对由软件包生成的HTML和纯文本版本的HTML进行转义。永远不要信任用户生成的内容。
- 一个转义内容的例子可以在resources/views/posts/show.blade.php中找到。请注意,富文本属性正在传递给
clean()
函数; clean()
函数创建了一个Sanitizer(参见工厂),它是基于Symfony的HTML Sanitizer(参见Sanitizer)的一个轻量级抽象;- 在Workbench应用的所有示例中,我们只在对内容进行渲染时进行清理。您也可以考虑在验证之后对其进行清理,甚至在将其传递到模型之前。
注意:我不是HTML内容清理的专家,所以请多加小心,如果可能的话,请咨询在这方面有更多安全经验的某人。
SGID
当存储自定义附件的引用时,该软件包使用另一个名为GlobalID Laravel的软件包。我们存储一个签名全局ID,这意味着用户无法简单地更改静止状态下的sgids。他们需要使用秘密的APP_KEY
生成另一个有效的签名。
如果您想更换密钥,您需要遍历所有富文本内容,找到所有具有sgid
属性的附件,使用新的密钥和新的签名给它分配一个新值,并使用该新值存储内容。
Livewire
如果您想使用Livewire与Trix和Rich Text Laravel集成,最佳方式是使用Livewire的@entangle()
功能。Workbench应用附带一个示例应用。一些有趣的点:
- 有一个自定义的components/trix-input-livewire.blade.php,仅用于展示如何与Livewire一起使用;
- 如你所见,它依赖于entangle。这是推荐的方式;
- 查看
Livewire\Posts
组件。当用户点击“编辑”时,它会将当前正在编辑的帖子设置为状态,并使用帖子模型中的数据填充PostForm
,包括Trix HTML;
测试
composer test
更新日志
有关最近更改的更多信息,请参阅更新日志。
贡献
有关详细信息,请参阅贡献指南。
安全漏洞
请参阅我们的安全政策以了解如何报告安全漏洞。
致谢
许可
MIT许可(MIT)。有关更多信息,请参阅许可文件。