Skip to content

Examples

Jean-Marc Strauven edited this page Aug 8, 2025 · 1 revision

Examples

πŸ’‘ Real-World Examples

Complete implementation examples showing Laravel Draftable in production scenarios. Each example includes full code, best practices, and real-world considerations.

πŸ“ Blog Management System

A complete blog platform with draft workflows, author collaboration, and editorial review.

Model Setup

<?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);
    }
}

Controller Implementation

<?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]);
    }
}

Event Handlers

<?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');
    }
}

Blade Templates

{{-- 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

πŸ›’ E-commerce Product Catalog

Product management with variants, pricing history, and inventory tracking.

Product Model

<?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));
    }
}

Product Controller with Bulk Operations

<?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'));
    }
}

πŸ“– Documentation System

Technical documentation with collaborative editing and review workflows.

Documentation Model

<?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;
    }
}

Review Workflow

<?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');
    }
}

🎯 Best Practices

Performance Optimization

<?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')
        ]);
    }
}

Error Handling and Logging

<?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;
        }
    }
}

Testing Patterns

<?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());
    }
}

πŸš€ Next Steps

These examples show Laravel Draftable in real production scenarios. Continue with:

πŸ“š Quick Navigation: Home β€’ Installation β€’ Getting Started β€’ Concepts β€’ Commands β€’ Examples β€’ API Reference β€’ Changelog

πŸ“ Basic Draft Operations

Creating and Managing Drafts

<?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();

Working with Draft History

// 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'];
}

πŸ” Version Comparison

Comparing Draft Versions

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 Draft with Live Model

// 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);

βͺ Version Restoration

Restoring Previous Versions

// 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();

πŸŽ›οΈ Configuration Examples

Auto-Save Configuration

// 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

Custom Draft Configuration

// 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
];

πŸš€ Artisan Commands Examples

List Drafts Command

# 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

Diff Command Examples

# 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

Cleanup Command Examples

# 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

πŸ”’ Access Control Examples

Laravel Policies Integration

// 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']);
    }
}

🎯 Advanced Use Cases

Content Management System

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>";
}

Workflow with Approvals

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}");
    }
}

πŸ“Š Performance Optimization

Database Indexes

// 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');
});

Efficient Queries

// 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();
});

Advanced Usage

More examples will be added as the package grows and features are implemented.

Laravel Draftable


πŸ“Š Project Status

  • βœ… 128/128 tests passing (100%)
  • βœ… 93.6% code coverage
  • βœ… PHPStan level 5 (0 errors)
  • πŸš€ Production Ready

Resources

Clone this wiki locally