The current implementation of the package suggests using a custom caster to the rich text field on any model. Although that's easy enough to use, the ActionText gem is a bit more opinionated. Instead of having rich text columns on multiple tables, the idea is to centralize them all in a single table.
This idea is mentioned briefly in the ActionText introduction video (link with timestamps), but basically it consists of the package shipping with a polymorphic model that stores all rich text content from the application. The trade-off here is that we have to interact with a relationship instead of an attribute. This changes the DX quite a bit.
Rails does some meta-programming to improve the DX of interacting with the rich content realtionship. I'm not sure if this will be possible with Eloquent. Here's what it would look like:
class Post < ApplicationRecord
has_rich_text :content
end
class PostsController < ApplicationController
def create
post = Post.create! params.require(:title, :content)
redirect_to post
end
end
Although it looks like we're setting the content
attribute in the post model, it's actually setting the body
field in the content
relationship. That relationship, when called, will return the rich content model for the body field (you could have multiple rich text fields on the same model, so the rich text model holds a backreference to the field name). Saving the post model, also saves the related data. For more, here's the ruby code that adds the methods to the model with meta-programming.
Having them as a relationship keeps the other models in the application lean (they only have their own attributes, which occupy way less memory when interacting with them), leaving the rich content only when you need to use it (by either eager loading or just lazy loading them). I feel like this is a safe bet. However, I don't how we would we could achieve a similar API for this in Eloquent.
I'll play around with some APIs, but if you have any ideas, let me know.
Some open ideas/questions:
- I want to be able to create dynamic accessors/mutators based on property in the model (see Example 1), is that possible?
- I think we can use dynamic relationships here
- I also need to be able to create some scopes dynamically (see Example 1), not sure if this is possible.
Example 1
This is what the RichText model could look like:
class RichText extends Model
{
protected $fillable = ['field', 'body'];
protected $casts = [
'body' => AsRichTextContent::class,
];
public function record()
{
return $this->morphTo();
}
public function __call($method, $arguments)
{
if (method_exists($this->body, $method)) {
return $this->body->{$method}(...$arguments);
}
return parent::__call($method, $arguments);
}
public function __toString()
{
return $this->body->render();
}
}
There would be a trait for applying some things to the entity that has rich text fields:
class Post extends Model
{
use HasRichText;
protected $richTextFields = [
'body',
];
}
When the HasRichText
boots, it would fetch the $richTextFields
property for the fields and create the relationship and dynamic accessors/mutators for that field, something like (π§ pseudo-code below π§):
trait HasRichText
{
public static function bootHasRichText()
{
$richTextFields = (new static)->richTextFields;
foreach ($richTextFields as $field)
{
static::defineRichTextRelationships($field);
static::defineRichTextScopes($field);
static::defineRichTextAccessorsAndMutators($field);
}
}
protected static function defineRichTextRelationships(string $field)
{
// I think this is the only thing that actually works...
static::resolveRelationUsing($field, function ($postModel) use ($field) {
return $postModel->morphOne(RichText::class, 'record')->where('field', $field);
});
}
protected static function defineRichTextScopes(string $field)
{
static::resolveScopeUsing('withRichText' . Str::studly($field), function ($query) use ($field) {
$query->with($field);
});
}
protected static function defineRichTextAccessorAndMutators(string $field)
{
static::resolveAccessorUsing($field, function () use ($field) {
// return the existing rich text model or make one on the fly.
return $this->{$field} ?: $this->{$field}()->make(['field'=> $field]);
});
static::resolveMutatorUsing($field, function ($content) {
// This will use the accessor above, so it will always return an instance of the rich text model.
return $this->{$field}->body = $content;
});
}
public function scopeWithAllRichTextContent($query)
{
// Eager load everything.
foreach ($this->richTextFields as $field)
{
$query->{'withRichText' . Str::studly($field)}();
}
}
}
This would allow an API like this:
// Setting looks like an attribute, but it's actually setting on the dynamic relationship.
$post = Post::create(['title' => $title, 'body' => $body]);
// Fetching posts won't include the rich text content by default, you could lazy-load it:
$posts = Post::withRichTextBody()->get();
// Eager loading all rich text fields on the post model (assuming there
// are multiple fields that are rich text on the post model):
$posts = Post::withAllRichTextContent()->get();
The rich text content would forward calls to the content field, so things like:
// The first "body" is the relationship, the second "body" is the rich text field on the RichText model...
$post->body->body->attachments();
Would be the same as:
$post->body->attachments();
Rendering the model would also render the content:
{{ $post->body }}
This is pseudo-code, not sure how much of this will be possible.