Installation:
composer require novius/laravel-publishable
php artisan vendor:publish --provider="Novius\Publishable\LaravelPublishableServiceProvider" --tag=lang
Add Migration Macro:
Update your model migration to include the publishable() macro:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
$table->publishable(); // Adds status, published_at, and published_first_at fields
});
Apply Migration:
php artisan migrate
Use the Trait:
Add the Publishable trait to your Eloquent model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Novius\Publishable\Publishable;
class Post extends Model
{
use Publishable;
}
First Use Case: Publish a draft post:
$post = Post::create(['title' => 'Hello World', 'content' => '...']);
$post->publish(); // Transitions from 'draft' to 'published'
Publishable trait methods and query scopes in the README.publishable() macro’s generated columns (status, published_at, published_first_at).published(), notPublished(), onlyDrafted(), etc.) for filtering.State Transitions: Use the trait’s methods to manage state transitions:
$post = Post::find(1);
$post->draft(); // Set to 'draft'
$post->publish(); // Set to 'published'
$post->unpublish(); // Set to 'unpublished'
$post->schedule($date); // Set to 'scheduled' with a future date
Query Filtering: Leverage query scopes for consistent filtering:
// Get all published posts
$publishedPosts = Post::published()->get();
// Get only drafted posts
$drafts = Post::onlyDrafted()->get();
// Get posts that will be published in the future
$scheduledPosts = Post::onlyWillBePublished()->get();
Scheduled Publishing: Schedule a post to publish at a future date:
$post = Post::create(['title' => 'Event Announcement', 'content' => '...']);
$post->schedule(now()->addDays(7)); // Publish in 7 days
Conditional Logic: Check a post’s state in business logic:
if ($post->isPublished()) {
// Show to public
} elseif ($post->isDraft()) {
// Show in admin panel only
}
Content Moderation Workflow:
$post->draft()).$post->publish()).$post->unpublish()) if needed.$post->schedule($date)).API Endpoints:
GET /posts/published: Return only published posts.POST /posts/{id}/publish: Transition a post to published.GET /admin/posts/drafts: Show drafts in admin panel.Caching: Cache published content with tags for invalidation:
Cache::tags(['posts'])->put('post-' . $post->id, $post, now()->addHours(1));
event(new PostPublished($post)); // Trigger cache invalidation
Nova Integration:
Extend the package with laravel-nova-publishable for Nova toolbars and filters:
composer require novius/laravel-nova-publishable
Configure Nova resources to use the publishable fields.
Validation: Add validation rules for state transitions:
use Illuminate\Validation\Rule;
$validator = Validator::make($request->all(), [
'status' => [
Rule::in(['draft', 'published', 'unpublished', 'scheduled']),
],
]);
Observers: Use model observers to log state changes:
class PostObserver
{
public function saved(Post $post)
{
if ($post->wasRecentlyCreated && $post->isPublished()) {
Log::info("New post published: {$post->title}");
}
}
}
Testing: Test state transitions and query scopes:
public function test_publish_post()
{
$post = Post::factory()->create(['status' => 'draft']);
$post->publish();
$post->refresh();
$this->assertTrue($post->isPublished());
}
Seeding: Seed test data with different states:
Post::factory()->create(['status' => 'published']);
Post::factory()->create(['status' => 'draft']);
Post::factory()->create(['status' => 'scheduled', 'published_at' => now()->addDay()]);
Global Scope Removal:
The package removed the global scope in v2.0, meaning queries default to all states unless filtered. Always explicitly use scopes like published():
// ❌ Avoid: Assumes global scope (deprecated)
$posts = Post::all();
// ✅ Correct: Explicitly filter
$posts = Post::published()->get();
Timezone Handling:
Scheduled posts use Laravel’s timezone (config('app.timezone')). Ensure consistency across environments:
$post->schedule(now()->timezone('UTC')->addDays(7));
Database Indexes:
For large datasets, add indexes to status and published_at columns to optimize queries:
Schema::table('posts', function (Blueprint $table) {
$table->index('status');
$table->index('published_at');
});
Soft Deletes Conflict:
If using SoftDeletes, ensure logic handles both deleted_at and status:
// Example: Check if a post is truly "visible"
public function isVisible()
{
return !$this->isDeleted && $this->isPublished();
}
Backfilling published_first_at:
When migrating existing data, populate published_first_at for published posts:
DB::table('posts')
->where('status', 'published')
->whereNull('published_first_at')
->update(['published_first_at' => DB::raw('created_at')]);
State Transition Issues: If a post isn’t transitioning states, check:
status column is being updated (use dd($post->fresh()->status)).Query Scope Not Working: Verify the scope is called on the query builder:
// ❌ Wrong: Calls scope on model instance
Post::published()->get(); // Correct
$post = Post::first();
$post->published(); // ❌ Returns boolean, not query builder
Scheduled Posts Not Triggering:
Ensure published_at is set and the current time is checked correctly:
$post = Post::where('status', 'scheduled')
->where('published_at', '<=', now())
->first();
Custom States:
The package enforces 4 states (draft, published, unpublished, scheduled). To extend:
// Override the trait's getStatusAttribute or use a custom trait
protected function getStatusAttribute()
{
return $this->attributes['status'] ?? 'draft';
}
Published First At Logic:
published_first_at defaults to created_at for published posts. Override in a model observer:
public function saved(Post $post)
{
if ($post->isPublished() && $post->wasRecentlyCreated) {
$post->update(['published_first_at' => now()]);
}
}
Nova Toolbar Integration:
If using Nova, ensure the laravel-nova-publishable package is configured to display the correct toolbars for each state.
How can I help you explore Laravel packages today?