Installation
composer require braunstetter/paragraphs
Publish the migration and config:
php artisan vendor:publish --provider="Braunstetter\Paragraphs\ParagraphsServiceProvider"
php artisan migrate
Basic Model Setup
Extend your model with HasParagraphs trait:
use Braunstetter\Paragraphs\HasParagraphs;
class Post extends Model
{
use HasParagraphs;
}
First Use Case Define paragraphs in a migration:
Schema::create('paragraphs', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Create a paragraph via Tinker:
$post = Post::first();
$post->paragraphs()->create(['title' => 'Intro', 'content' => 'Hello world!']);
Create
$post = Post::create(['title' => 'New Post']);
$post->paragraphs()->createMany([
['title' => 'Section 1', 'content' => 'Content...'],
['title' => 'Section 2', 'content' => 'More content...'],
]);
Read
// Eager-load paragraphs with the post
$post = Post::with('paragraphs')->find(1);
// Access paragraphs
foreach ($post->paragraphs as $paragraph) {
echo $paragraph->title . ': ' . $paragraph->content;
}
Update
$paragraph = $post->paragraphs->first();
$paragraph->update(['content' => 'Updated content']);
Delete
$paragraph->delete(); // Soft deletes if using SoftDeletes trait
Use paragraphs relationship in Form Requests:
public function rules()
{
return [
'paragraphs.*' => 'required|array',
'paragraphs.*.title' => 'required|string|max:255',
'paragraphs.*.content' => 'required|string',
];
}
Extend the package for custom paragraph types (e.g., ImageParagraph, VideoParagraph):
class Post extends Model
{
use HasParagraphs;
protected $paragraphTypes = [
'text' => \Braunstetter\Paragraphs\Models\TextParagraph::class,
'image' => \App\Models\ImageParagraph::class,
];
}
Missing Foreign Key
Ensure the paragraphs table has a foreign key column (e.g., post_id) matching your model’s primary key. The package assumes a paragraphable_id column by default—customize via $paragraphableType in config.
Mass Assignment Risks
Paragraphs are created via create() or createMany(), which may expose sensitive fields. Whitelist allowed fields in your model:
protected $guarded = [];
protected $fillable = ['title', 'content'];
Ordering Issues
Paragraphs lack a default order column. Add one if ordering matters:
Schema::table('paragraphs', function (Blueprint $table) {
$table->integer('order')->default(0);
});
Then sort in queries:
$post->paragraphs()->orderBy('order');
Missing Paragraphs? Check if the relationship is properly defined in your model and if the foreign key exists in the database.
Soft Deletes
If using SoftDeletes, ensure the paragraphs table has deleted_at and your queries include withTrashed():
$post->paragraphs()->withTrashed()->get();
Custom Paragraph Models
Override the default Paragraph model by binding your own in the service provider:
$this->app->bind(
\Braunstetter\Paragraphs\Models\Paragraph::class,
\App\Models\CustomParagraph::class
);
Event Hooks
Listen for paragraph events (e.g., paragraph.created) to trigger side effects:
\Braunstetter\Paragraphs\Models\Paragraph::created(function ($paragraph) {
// Log or notify when a paragraph is created
});
API Resources
Create a dedicated ParagraphResource for API responses:
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => Str::limit($this->content, 200),
'type' => 'text',
];
}
Default Paragraph Model
The package uses Braunstetter\Paragraphs\Models\Paragraph by default. Customize in config/paragraphs.php:
'model' => \App\Models\CustomParagraph::class,
Polymorphic Relationships
For polymorphic paragraphs (e.g., shared across Post and Page), set:
'polymorphic' => true,
Then define the relationship in your models:
public function paragraphs()
{
return $this->morphMany(\Braunstetter\Paragraphs\Models\Paragraph::class, 'paragraphable');
}
How can I help you explore Laravel packages today?