pawelmysior/laravel-publishable
composer require pawelmysior/laravel-publishable and ensure compatibility with your Laravel version (check the version table).published_at timestamp column to your model’s table:
$table->timestamp('published_at')->nullable();
Publishable trait in your Eloquent model:
use PawelMysior\Publishable\Publishable;
(No need to add published_at to $fillable in Laravel 9+; see README.)Publish/unpublish a model and query by status:
// Publish a post
$post = new Post();
$post->publish(); // Sets `published_at` to current timestamp
// Query published posts
$publishedPosts = Post::published()->get();
// Check publication status
if ($post->isPublished()) {
$post->unpublish(); // Sets `published_at` to NULL
}
Scoping Queries:
Use published()/unpublished() as query scopes to filter models:
// Active posts (published_at is not NULL)
Post::published()->where('status', 'active')->get();
// Drafts (published_at is NULL)
Post::unpublished()->where('author_id', auth()->id())->get();
Mass Updates: Publish/unpublish multiple records at once:
Post::where('draft', true)->updatePublishedAt(now()); // Publish all drafts
Post::where('expired', true)->updatePublishedAt(null); // Unpublish expired posts
Soft Deletes + Publishable:
Combine with SoftDeletes for granular control:
Post::onlyTrashed()->published()->restore(); // Restore only trashed *and* published posts
Custom Logic: Extend the trait for domain-specific rules:
class Post extends Model {
use Publishable;
public function publish() {
if (!$this->isApproved()) {
throw new \Exception("Post must be approved first.");
}
parent::publish();
}
}
API Responses: Filter resources by publication status in controllers:
return PostResource::collection(
Post::published()->latest()->paginate(10)
);
published_at changes:
class PostObserver {
public function saved(Post $post) {
if ($post->wasRecentlyPublished()) {
event(new PostPublished($post));
}
}
}
public function update(User $user, Post $post) {
return $user->can('edit-posts') && $post->isPublished();
}
$post = Post::factory()->create(['published_at' => now()]);
$this->assertTrue($post->isPublished());
Column Name Assumption:
The trait assumes published_at by default. Override the column name if needed:
use Publishable as PublishableTrait;
class Post extends Model {
use PublishableTrait {
PublishableTrait::boot as bootPublishable;
}
protected $publishableColumn = 'active_since';
}
Time Zone Handling:
published_at uses the model’s dateFormat or connection timezone. Explicitly set timezone if needed:
$post->published_at = now()->setTimezone('UTC');
Mass Assignment Risks:
Even though published_at isn’t fillable in Laravel 9+, ensure your API/Panel doesn’t expose it in $guarded or $fillable accidentally.
Query Performance:
Avoid chaining published() with complex orWhere clauses. Use raw SQL or subqueries for large datasets:
// Slow: Post::published()->where('title', 'like', '%'.$search.'%')
// Faster: Post::where(function($q) {
// $q->whereNull('published_at')->orWhere('title', 'like', '%'.$search.'%');
// })->get();
Edge Cases:
published_at = NULL as unpublished, but you might want to handle past dates (e.g., "expired") separately.DB::transaction(function () use ($post) {
$post->unpublish();
$post->update(['status' => 'archived']);
});
published_at exists in the DB and is nullable:
php artisan schema:dump
DB::enableQueryLog();
Post::published()->get();
dd(DB::getQueryLog());
published_at to a past/future date and test isPublished():
$post->published_at = now()->subYear();
$this->assertFalse($post->isPublished()); // If you want to treat past dates as unpublished
Custom Validation:
Override publish()/unpublish() to add logic:
public function publish() {
if ($this->isScheduled()) {
throw new \Exception("Cannot publish scheduled posts manually.");
}
parent::publish();
}
Additional Statuses: Extend the trait to support "draft," "archived," etc.:
class Post extends Model {
use Publishable;
public function isDraft() {
return $this->published_at === null && $this->status === 'draft';
}
}
Soft Deletes Integration:
Add a isPermanentlyPublished() method:
public function isPermanentlyPublished() {
return $this->isPublished() && !$this->trashed();
}
API Meta Data: Add publication status to API responses:
public function toArray() {
return array_merge(parent::toArray(), [
'is_published' => $this->isPublished(),
'published_at' => $this->published_at?->toDateTimeString(),
]);
}
Admin Panel Filters: Use the trait’s scopes in admin packages like Filament or Nova:
// Filament Table
Table::make(Post::class, [
'columns' => [
// ...
Columns\BooleanColumn::make('isPublished')
->label('Published')
->url(fn ($record) => $record->isPublished() ? 'published' : 'drafts'),
],
'filters' => [
Filters\SelectFilter::make('status')
->options([
'published' => 'Published',
'unpublished' => 'Unpublished',
])
->query(fn (Builder $query, $value) => match ($value) {
'published' => $query->published(),
'unpublished' => $query->unpublished(),
}),
],
]);
How can I help you explore Laravel packages today?