-
-
Notifications
You must be signed in to change notification settings - Fork 0
Examples
Complete implementation examples showing Laravel Draftable in production scenarios. Each example includes full code, best practices, and real-world considerations.
A complete blog platform with draft workflows, author collaboration, and editorial review.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Grazulex\LaravelDraftable\Traits\HasDrafts;
use Grazulex\LaravelDraftable\Events\{DraftCreated, DraftPublished};
class BlogPost extends Model
{
use HasDrafts;
protected $fillable = [
'title',
'slug',
'content',
'excerpt',
'status',
'featured_image',
'meta_title',
'meta_description',
'published_at',
'author_id'
];
protected $casts = [
'published_at' => 'datetime',
];
// Only these fields are tracked in drafts
protected $draftable = [
'title',
'content',
'excerpt',
'featured_image',
'meta_title',
'meta_description'
];
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
// Custom logic for auto-saving drafts
public function shouldAutoSaveDraft(): bool
{
// Auto-save for unpublished posts only
return $this->status === 'draft';
}
// Custom validation before publishing
public function canPublish(): bool
{
return strlen($this->title) > 10 &&
strlen($this->content) > 100 &&
!empty($this->excerpt);
}
}
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\BlogPost;
use Grazulex\LaravelDraftable\Services\{DraftManager, DraftDiff};
use Illuminate\Http\Request;
class BlogPostController extends Controller
{
private DraftManager $draftManager;
private DraftDiff $draftDiff;
public function __construct(DraftManager $draftManager, DraftDiff $draftDiff)
{
$this->draftManager = $draftManager;
$this->draftDiff = $draftDiff;
}
public function index()
{
$posts = BlogPost::with(['author', 'latestDraft'])
->orderBy('updated_at', 'desc')
->paginate(20);
return view('admin.posts.index', compact('posts'));
}
public function edit(BlogPost $post)
{
$drafts = $post->drafts()->with('creator')->orderBy('version', 'desc')->get();
$hasUnpublished = $post->hasUnpublishedDrafts();
return view('admin.posts.edit', compact('post', 'drafts', 'hasUnpublished'));
}
public function saveDraft(Request $request, BlogPost $post)
{
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
'excerpt' => 'nullable|max:500',
'featured_image' => 'nullable|url',
'meta_title' => 'nullable|max:60',
'meta_description' => 'nullable|max:160',
]);
// Update model (doesn't save to DB yet)
$post->fill($validated);
// Save as draft
$draft = $post->saveDraft();
return response()->json([
'success' => true,
'message' => "Draft saved as version {$draft->version}",
'version' => $draft->version,
'created_at' => $draft->created_at->format('Y-m-d H:i:s')
]);
}
public function publish(BlogPost $post, ?int $version = null)
{
// Get specific version or latest
$draft = $version
? $post->drafts()->where('version', $version)->first()
: null;
if (!$post->canPublish()) {
return back()->withErrors('Post is not ready for publication');
}
$published = $post->publishDraft($draft);
if ($published) {
$post->update([
'status' => 'published',
'published_at' => now()
]);
return redirect()
->route('admin.posts.index')
->with('success', 'Post published successfully!');
}
return back()->withErrors('Failed to publish post');
}
public function compareVersions(BlogPost $post, int $version1, int $version2)
{
$draft1 = $post->drafts()->where('version', $version1)->firstOrFail();
$draft2 = $post->drafts()->where('version', $version2)->firstOrFail();
$diff = $this->draftDiff->compare($draft1, $draft2);
return view('admin.posts.compare', compact('post', 'draft1', 'draft2', 'diff'));
}
public function restore(BlogPost $post, int $version)
{
$restored = $post->restoreVersion($version);
if ($restored) {
return back()->with('success', "Restored to version {$version}");
}
return back()->withErrors('Failed to restore version');
}
public function preview(BlogPost $post, int $version)
{
$draft = $post->drafts()->where('version', $version)->firstOrFail();
$preview = $this->draftManager->previewDraft($post, $draft);
return view('blog.show', ['post' => $preview, 'preview' => true]);
}
}
<?php
namespace App\Listeners;
use Grazulex\LaravelDraftable\Events\{DraftCreated, DraftPublished, VersionRestored};
use App\Mail\{PostDraftCreated, PostPublished};
use App\Models\User;
use Illuminate\Support\Facades\Mail;
class BlogPostDraftListener
{
public function handleDraftCreated(DraftCreated $event)
{
$post = $event->model;
$draft = $event->draft;
// Notify editors about new draft
$editors = User::role('editor')->get();
foreach ($editors as $editor) {
Mail::to($editor)->send(new PostDraftCreated($post, $draft));
}
// Log activity
activity()
->causedBy(auth()->user())
->performedOn($post)
->withProperties(['version' => $draft->version])
->log('created_draft');
}
public function handleDraftPublished(DraftPublished $event)
{
$post = $event->model;
// Clear cache
cache()->forget("post.{$post->id}");
cache()->forget("post.slug.{$post->slug}");
// Update search index
$post->searchable();
// Send notification
Mail::to($post->author)->send(new PostPublished($post));
// Log activity
activity()
->causedBy(auth()->user())
->performedOn($post)
->log('published_post');
}
public function handleVersionRestored(VersionRestored $event)
{
activity()
->causedBy(auth()->user())
->performedOn($event->model)
->withProperties(['restored_version' => $event->version])
->log('restored_version');
}
}
{{-- resources/views/admin/posts/edit.blade.php --}}
@extends('admin.layout')
@section('content')
<div class="container mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Edit Post: {{ $post->title }}</h1>
@if($hasUnpublished)
<div class="bg-yellow-100 border border-yellow-400 text-yellow-700 px-3 py-2 rounded">
β οΈ Unpublished changes exist
</div>
@endif
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Main Editor --}}
<div class="lg:col-span-2">
<form id="draft-form" class="space-y-4">
@csrf
<div>
<label class="block text-sm font-medium text-gray-700">Title</label>
<input type="text" name="title" value="{{ old('title', $post->title) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Content</label>
<textarea name="content" rows="15"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">{{ old('content', $post->content) }}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Excerpt</label>
<textarea name="excerpt" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">{{ old('excerpt', $post->excerpt) }}</textarea>
</div>
<div class="flex space-x-4">
<button type="button" id="save-draft-btn"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Save Draft
</button>
<button type="button" id="publish-btn"
class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
Publish
</button>
</div>
</form>
</div>
{{-- Version History Sidebar --}}
<div class="lg:col-span-1">
<div class="bg-white shadow rounded-lg p-4">
<h3 class="text-lg font-medium mb-4">Version History</h3>
@forelse($drafts as $draft)
<div class="border-b pb-3 mb-3 last:border-b-0">
<div class="flex justify-between items-center">
<span class="font-medium">Version {{ $draft->version }}</span>
@if($draft->isPublished())
<span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">
Published
</span>
@else
<span class="bg-yellow-100 text-yellow-800 text-xs px-2 py-1 rounded">
Draft
</span>
@endif
</div>
<div class="text-sm text-gray-600 mt-1">
{{ $draft->created_at->format('M j, Y g:i A') }}
@if($draft->creator)
by {{ $draft->creator->name }}
@endif
</div>
<div class="mt-2 space-x-2">
<a href="{{ route('admin.posts.preview', [$post, $draft->version]) }}"
class="text-blue-600 hover:text-blue-800 text-sm">Preview</a>
@if(!$draft->isPublished())
<button onclick="restoreVersion({{ $draft->version }})"
class="text-green-600 hover:text-green-800 text-sm">Restore</button>
@endif
@if($loop->index > 0)
<a href="{{ route('admin.posts.compare', [$post, $drafts[$loop->index - 1]->version, $draft->version]) }}"
class="text-purple-600 hover:text-purple-800 text-sm">Compare</a>
@endif
</div>
</div>
@empty
<p class="text-gray-500">No versions yet</p>
@endforelse
</div>
</div>
</div>
</div>
<script>
document.getElementById('save-draft-btn').addEventListener('click', function() {
const formData = new FormData(document.getElementById('draft-form'));
fetch('{{ route("admin.posts.save-draft", $post) }}', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': document.querySelector('[name="_token"]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Draft saved successfully!');
location.reload();
}
});
});
document.getElementById('publish-btn').addEventListener('click', function() {
if (confirm('Are you sure you want to publish this post?')) {
window.location.href = '{{ route("admin.posts.publish", $post) }}';
}
});
function restoreVersion(version) {
if (confirm(`Restore to version ${version}? This will create a new version.`)) {
window.location.href = `{{ route("admin.posts.restore", $post) }}/${version}`;
}
}
</script>
@endsection
Product management with variants, pricing history, and inventory tracking.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Grazulex\LaravelDraftable\Traits\HasDrafts;
class Product extends Model
{
use HasDrafts;
protected $fillable = [
'name',
'description',
'short_description',
'sku',
'price',
'sale_price',
'stock_quantity',
'weight',
'dimensions',
'images',
'category_id',
'brand_id',
'status',
'featured'
];
protected $casts = [
'price' => 'decimal:2',
'sale_price' => 'decimal:2',
'weight' => 'decimal:2',
'dimensions' => 'array',
'images' => 'array',
'featured' => 'boolean'
];
// Track all product data except internal fields
protected $draftable = [
'name',
'description',
'short_description',
'price',
'sale_price',
'weight',
'dimensions',
'images'
];
public function variants(): HasMany
{
return $this->hasMany(ProductVariant::class);
}
// Custom publishing logic
public function publishDraft($draft = null): bool
{
$result = parent::publishDraft($draft);
if ($result) {
// Update search index
$this->searchable();
// Clear cache
cache()->forget("product.{$this->id}");
// Sync with external systems
$this->syncWithERP();
}
return $result;
}
private function syncWithERP(): void
{
// Integration with external ERP system
// dispatch(new SyncProductWithERPJob($this));
}
}
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Product;
use Grazulex\LaravelDraftable\Services\DraftManager;
use Illuminate\Http\Request;
class ProductController extends Controller
{
private DraftManager $draftManager;
public function __construct(DraftManager $draftManager)
{
$this->draftManager = $draftManager;
}
public function bulkUpdate(Request $request)
{
$validated = $request->validate([
'products' => 'required|array',
'products.*.id' => 'required|exists:products,id',
'products.*.price' => 'required|numeric|min:0',
'products.*.sale_price' => 'nullable|numeric|min:0',
]);
$updatedCount = 0;
foreach ($validated['products'] as $productData) {
$product = Product::find($productData['id']);
$product->fill([
'price' => $productData['price'],
'sale_price' => $productData['sale_price'] ?? null
]);
// Save as draft for review
$product->saveDraft();
$updatedCount++;
}
return response()->json([
'success' => true,
'message' => "Created drafts for {$updatedCount} products"
]);
}
public function bulkPublish(Request $request)
{
$productIds = $request->validate([
'product_ids' => 'required|array',
'product_ids.*' => 'exists:products,id'
])['product_ids'];
$publishedCount = 0;
foreach ($productIds as $productId) {
$product = Product::find($productId);
if ($product->hasUnpublishedDrafts()) {
$product->publishDraft();
$publishedCount++;
}
}
return back()->with('success', "Published {$publishedCount} products");
}
public function priceHistory(Product $product)
{
$priceChanges = $product->drafts()
->whereNotNull('published_at')
->orderBy('created_at', 'desc')
->get()
->map(function ($draft) {
return [
'version' => $draft->version,
'price' => $draft->payload['price'] ?? null,
'sale_price' => $draft->payload['sale_price'] ?? null,
'published_at' => $draft->published_at,
];
})
->filter(fn($change) => !is_null($change['price']));
return view('admin.products.price-history', compact('product', 'priceChanges'));
}
}
Technical documentation with collaborative editing and review workflows.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Grazulex\LaravelDraftable\Traits\HasDrafts;
class Documentation extends Model
{
use HasDrafts;
protected $fillable = [
'title',
'slug',
'content',
'order',
'category_id',
'author_id',
'reviewer_id',
'status',
'tags',
'meta'
];
protected $casts = [
'tags' => 'array',
'meta' => 'array'
];
protected $draftable = [
'title',
'content',
'order',
'tags',
'meta'
];
public function category(): BelongsTo
{
return $this->belongsTo(DocumentationCategory::class);
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function reviewer(): BelongsTo
{
return $this->belongsTo(User::class, 'reviewer_id');
}
// Require review before publishing
public function canPublish(): bool
{
return !is_null($this->reviewer_id) && $this->status === 'approved';
}
public function publishDraft($draft = null): bool
{
if (!$this->canPublish()) {
return false;
}
$result = parent::publishDraft($draft);
if ($result) {
$this->update(['status' => 'published']);
// Regenerate static documentation site
dispatch(new RegenerateDocsJob());
}
return $result;
}
}
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Documentation;
use Grazulex\LaravelDraftable\Services\DraftDiff;
use Illuminate\Http\Request;
class DocumentationReviewController extends Controller
{
public function pendingReview()
{
$pendingDocs = Documentation::whereHas('unpublishedDrafts')
->where('status', 'pending_review')
->with(['author', 'latestDraft'])
->get();
return view('admin.docs.pending-review', compact('pendingDocs'));
}
public function review(Documentation $doc)
{
$latestDraft = $doc->latestDraft();
$publishedDraft = $doc->publishedDrafts()->latest()->first();
$diff = null;
if ($publishedDraft) {
$diff = app(DraftDiff::class)->compare($publishedDraft, $latestDraft);
}
return view('admin.docs.review', compact('doc', 'latestDraft', 'diff'));
}
public function approve(Request $request, Documentation $doc)
{
$validated = $request->validate([
'comments' => 'nullable|string|max:1000'
]);
$doc->update([
'status' => 'approved',
'reviewer_id' => auth()->id()
]);
// Add review comment to latest draft
if ($validated['comments']) {
$latestDraft = $doc->latestDraft();
$meta = $latestDraft->payload['meta'] ?? [];
$meta['review_comments'] = $validated['comments'];
$latestDraft->setPayloadValue('meta', $meta);
$latestDraft->save();
}
return back()->with('success', 'Documentation approved for publishing');
}
public function reject(Request $request, Documentation $doc)
{
$validated = $request->validate([
'reason' => 'required|string|max:1000'
]);
$doc->update(['status' => 'rejected']);
// Add rejection reason
$latestDraft = $doc->latestDraft();
$meta = $latestDraft->payload['meta'] ?? [];
$meta['rejection_reason'] = $validated['reason'];
$latestDraft->setPayloadValue('meta', $meta);
$latestDraft->save();
// Notify author
Mail::to($doc->author)->send(new DocumentationRejectedMail($doc, $validated['reason']));
return back()->with('warning', 'Documentation rejected');
}
}
<?php
namespace App\Models;
use Grazulex\LaravelDraftable\Traits\HasDrafts;
class OptimizedPost extends Model
{
use HasDrafts;
// Exclude large fields from drafts to save space
protected $draftable = [
'title',
'content',
'excerpt'
// Excluded: 'large_binary_data', 'computed_fields'
];
// Custom cleanup logic
public function cleanupOldDrafts(): int
{
// Keep more versions for recent posts
$maxVersions = $this->created_at->gt(now()->subMonths(3)) ? 10 : 5;
$draftsToDelete = $this->drafts()
->orderBy('version', 'desc')
->skip($maxVersions)
->pluck('id');
return Draft::whereIn('id', $draftsToDelete)->delete();
}
// Efficient querying
public function scopeWithDraftCounts($query)
{
return $query->withCount([
'drafts',
'drafts as unpublished_drafts_count' => fn($q) => $q->whereNull('published_at')
]);
}
}
<?php
namespace App\Services;
use Grazulex\LaravelDraftable\Services\DraftManager as BaseDraftManager;
use Illuminate\Support\Facades\Log;
use Exception;
class CustomDraftManager
{
private BaseDraftManager $draftManager;
public function __construct(BaseDraftManager $draftManager)
{
$this->draftManager = $draftManager;
}
public function saveDraftSafely($model, array $attributes = [], ?int $userId = null)
{
try {
$draft = $this->draftManager->saveDraft($model, $attributes, $userId);
Log::info('Draft saved successfully', [
'model' => get_class($model),
'model_id' => $model->getKey(),
'version' => $draft->version,
'user_id' => $userId
]);
return $draft;
} catch (Exception $e) {
Log::error('Failed to save draft', [
'model' => get_class($model),
'model_id' => $model->getKey(),
'error' => $e->getMessage(),
'user_id' => $userId
]);
throw $e;
}
}
public function publishDraftSafely($model, $draft = null)
{
try {
$result = $this->draftManager->publishDraft($model, $draft);
if ($result) {
Log::info('Draft published successfully', [
'model' => get_class($model),
'model_id' => $model->getKey()
]);
}
return $result;
} catch (Exception $e) {
Log::error('Failed to publish draft', [
'model' => get_class($model),
'model_id' => $model->getKey(),
'error' => $e->getMessage()
]);
return false;
}
}
}
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Post;
use Grazulex\LaravelDraftable\Models\Draft;
class DraftWorkflowTest extends TestCase
{
/** @test */
public function it_creates_and_publishes_drafts()
{
// Arrange
$post = Post::factory()->create([
'title' => 'Original Title',
'content' => 'Original Content'
]);
// Act - Create draft
$post->title = 'Updated Title';
$draft = $post->saveDraft();
// Assert - Draft created
$this->assertInstanceOf(Draft::class, $draft);
$this->assertEquals(1, $draft->version);
$this->assertEquals('Updated Title', $draft->payload['title']);
// Act - Publish draft
$published = $post->publishDraft($draft);
// Assert - Published successfully
$this->assertTrue($published);
$this->assertEquals('Updated Title', $post->fresh()->title);
$this->assertNotNull($draft->fresh()->published_at);
}
/** @test */
public function it_handles_version_conflicts()
{
// Arrange
$post = Post::factory()->create();
// Act - Create multiple unpublished drafts
$post->title = 'Version 1';
$draft1 = $post->saveDraft();
$post->title = 'Version 2';
$draft2 = $post->saveDraft();
// Assert - Conflicts detected
$this->assertTrue($post->hasUnpublishedDrafts());
$this->assertEquals(2, $post->unpublishedDrafts()->count());
// Act - Resolve by publishing latest
$post->publishDraft($draft2);
// Assert - Conflict resolved
$this->assertEquals(1, $post->unpublishedDrafts()->count());
}
}
These examples show Laravel Draftable in real production scenarios. Continue with:
- API Reference - Complete method documentation
- Core Concepts - Deep understanding of the system
- Commands - CLI tools for management
π Quick Navigation: Home β’ Installation β’ Getting Started β’ Concepts β’ Commands β’ Examples β’ API Reference β’ Changelog
<?php
use App\Models\Post;
use Grazulex\LaravelDraftable\Traits\HasDrafts;
class Post extends Model
{
use HasDrafts;
protected $fillable = ['title', 'content', 'status', 'published_at'];
}
// Create a new post
$post = Post::create([
'title' => 'My First Blog Post',
'content' => 'Initial content',
'status' => 'draft'
]);
// Make changes and save as draft
$post->title = 'Updated Blog Post Title';
$post->content = 'This content has been revised and improved.';
$post->saveDraft();
// Check if model has drafts
if ($post->hasDrafts()) {
echo "Post has " . $post->getDraftCount() . " draft(s)";
}
// Publish the latest draft
$post->publishDraft();
// Get all drafts for a model
$allDrafts = $post->drafts; // All drafts (published and unpublished)
$unpublishedDrafts = $post->unpublishedDrafts; // Only unpublished
$publishedDrafts = $post->publishedDrafts; // Only published
// Get current version number
$currentVersion = $post->getCurrentVersion(); // Returns integer
// Access specific draft data
foreach ($post->drafts as $draft) {
echo "Version {$draft->version} created at {$draft->created_at}";
echo "Status: " . ($draft->isPublished() ? 'Published' : 'Draft');
// Access draft payload
$draftData = $draft->payload;
echo "Title in this version: " . $draftData['title'];
}
use Grazulex\LaravelDraftable\Services\DraftDiff;
// Inject the service or resolve it
$draftDiff = app(DraftDiff::class);
// Compare two drafts
$draft1 = $post->drafts()->where('version', 1)->first();
$draft2 = $post->drafts()->where('version', 2)->first();
$differences = $draftDiff->compare($draft1, $draft2);
// Example output:
[
'title' => [
'type' => 'modified',
'old' => 'My First Blog Post',
'new' => 'Updated Blog Post Title'
],
'content' => [
'type' => 'modified',
'old' => 'Initial content',
'new' => 'This content has been revised and improved.'
]
]
// Get human-readable summary
$summary = $draftDiff->getSummary($differences);
echo $summary; // "2 fields modified"
// Format for display
$humanReadable = $draftDiff->formatForHumans($differences);
foreach ($humanReadable as $field => $change) {
echo "{$field}: {$change}";
}
// Compare current model state with a draft
$latestDraft = $post->drafts()->latest()->first();
$differences = $draftDiff->compareWithModel($post, $latestDraft);
// Compare two different models
$post1 = Post::find(1);
$post2 = Post::find(2);
$modelDifferences = $draftDiff->compareModels($post1, $post2);
// Restore to a specific version
$post->restoreVersion(3); // Restores to version 3
// This will:
// 1. Apply the version 3 data to the model
// 2. Fire a VersionRestored event
// 3. Update the model in the database
// Check what version we're on after restore
echo "Now on version: " . $post->getCurrentVersion();
// In your model
class Post extends Model
{
use HasDrafts;
// Enable auto-save (saves draft on every model update)
protected $autoSaveDrafts = true;
// Specify which attributes to include in drafts
protected $draftableAttributes = ['title', 'content', 'excerpt'];
// Include additional data in draft payload
protected $additionalDraftData = ['meta_description', 'tags'];
}
// With auto-save enabled
$post = Post::find(1);
$post->title = 'New Title';
$post->save(); // Automatically creates a draft
// config/laravel-draftable.php
return [
'table_name' => 'drafts',
'auto_publish' => false,
'auto_save' => false,
'max_versions' => 50, // Keep only last 50 versions
'cleanup_days' => 90, // Auto-cleanup after 90 days
];
# List all drafts
php artisan laravel-draftable:list
# Filter by model type
php artisan laravel-draftable:list --model=Post
# Show only unpublished drafts
php artisan laravel-draftable:list --status=unpublished
# Limit results
php artisan laravel-draftable:list --limit=10
# Combine filters
php artisan laravel-draftable:list --model=Post --status=published --limit=5
# Compare versions in table format
php artisan laravel-draftable:diff Post 1 --versions=1,2
# Output as JSON
php artisan laravel-draftable:diff Post 1 --versions=1,2 --format=json
# Output as YAML
php artisan laravel-draftable:diff Post 1 --versions=2,3 --format=yaml
# Using full model class name
php artisan laravel-draftable:diff "App\\Models\\Post" 1 --versions=1,2
# Clean up drafts older than 90 days (default)
php artisan laravel-draftable:clear-old
# Custom time period
php artisan laravel-draftable:clear-old --days=30
# Dry run to see what would be deleted
php artisan laravel-draftable:clear-old --days=60 --dry-run
# Force cleanup without confirmation
php artisan laravel-draftable:clear-old --days=180 --force
// Create a policy
class PostPolicy
{
public function publishDraft(User $user, Post $post): bool
{
return $user->isEditor() || $user->id === $post->user_id;
}
public function viewDrafts(User $user, Post $post): bool
{
return $user->canManageContent() || $user->id === $post->user_id;
}
}
// In your controller
class PostController extends Controller
{
public function publishDraft(Post $post)
{
$this->authorize('publishDraft', $post);
$post->publishDraft();
return response()->json(['message' => 'Draft published successfully']);
}
}
class Article extends Model
{
use HasDrafts;
protected $autoSaveDrafts = true;
public function scopePublished($query)
{
return $query->whereNotNull('published_at');
}
public function getPublishedVersionAttribute()
{
return $this->publishedDrafts()->latest()->first();
}
public function hasUnpublishedChanges(): bool
{
return $this->unpublishedDrafts()->exists();
}
}
// Usage in CMS
$article = Article::find(1);
// Editor makes changes
$article->update(['title' => 'Breaking News: Updated']);
// Check for pending changes
if ($article->hasUnpublishedChanges()) {
// Show "Publish Changes" button
echo "You have unpublished changes. <button>Publish</button>";
}
use Grazulex\LaravelDraftable\Events\DraftCreated;
use Grazulex\LaravelDraftable\Events\DraftPublished;
// Listen for draft events
class DraftWorkflowListener
{
public function handleDraftCreated(DraftCreated $event): void
{
$draft = $event->draft;
// Notify editors about new draft
Notification::send(
User::editors(),
new NewDraftCreated($draft)
);
}
public function handleDraftPublished(DraftPublished $event): void
{
$draft = $event->draft;
// Update search index, clear cache, etc.
SearchIndex::update($draft->draftable);
Cache::forget("post.{$draft->draftable_id}");
}
}
// The migration includes optimized indexes
Schema::create('drafts', function (Blueprint $table) {
$table->id();
$table->morphs('draftable'); // Creates indexes automatically
$table->json('payload');
$table->unsignedBigInteger('version')->default(1);
$table->foreignId('created_by')->nullable()->constrained('users');
$table->timestamp('published_at')->nullable();
$table->timestamps();
// Custom performance indexes
$table->index(['draftable_type', 'draftable_id', 'version']);
$table->index('published_at');
$table->index('created_at');
});
// Eager load drafts to avoid N+1 queries
$posts = Post::with(['drafts' => function ($query) {
$query->latest()->limit(5); // Only load recent drafts
}])->get();
// Get only specific draft data
$draftTitles = $post->drafts()
->pluck('payload->title', 'version')
->toArray();
// Efficient version checking
$hasRecentDrafts = $post->drafts()
->where('created_at', '>', now()->subDays(7))
->exists();
π These examples demonstrate the full power of Laravel Draftable in production scenarios!
## Testing Example
```php
use Grazulex\LaravelDraftable\LaravelDraftable;
it('can use the package', function () {
$package = new LaravelDraftable();
expect($package->version())
->toBeString()
->not->toBeEmpty();
});
More examples will be added as the package grows and features are implemented.
Laravel Draftable v1.0.0 π | Production Ready β | Repository | Issues | MIT License
π Quality Metrics: 128/128 tests β | 93.6% coverage β | PHPStan Level 5 β | PSR-12 β
- Getting Started
- Installation
- Concepts
- Examples
- Commands - Artisan Commands
- API Reference - Complete API
- Changelog
π Project Status
- β 128/128 tests passing (100%)
- β 93.6% code coverage
- β PHPStan level 5 (0 errors)
- π Production Ready
Resources