composer require novius/laravel-publishable
php artisan vendor:publish --provider="Novius\Publishable\LaravelPublishableServiceProvider" --tag=lang
publishable() macro:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
$table->publishable(); // Adds status, published_at, published_first_at
});
php artisan migrate
Publishable trait to your Eloquent model:
use Novius\Publishable\PublishableTrait;
class Post extends Model
{
use PublishableTrait;
}
$publishedPosts = Post::published()->get();
Publish a Draft Post:
$post = Post::create([
'title' => 'Hello World',
'body' => 'This is a draft.',
// Status defaults to 'draft'
]);
// Publish immediately
$post->publish();
// Or schedule for later
$post->schedule(Publishable::STATUS_PUBLISHED, now()->addDay());
Use the trait’s methods to transition between states:
$post = Post::find(1);
// Draft → Published
$post->publish();
// Published → Unpublished
$post->unpublish();
// Draft → Scheduled
$post->schedule(Publishable::STATUS_PUBLISHED, now()->addHours(2));
// Scheduled → Published (auto-triggered by package logic)
Leverage scopes for common queries:
// All published posts
$published = Post::published()->get();
// Only drafts
$drafts = Post::onlyDrafted()->get();
// Posts that will publish in the future
$scheduled = Post::onlyWillBePublished()->get();
// Posts that were published but are now expired (if using TTL)
$expired = Post::onlyExpired()->get();
Extend the trait to hook into state changes:
use Novius\Publishable\PublishableTrait;
class Post extends Model
{
use PublishableTrait;
protected static function booted()
{
static::published(function (Post $post) {
// Send notification, update cache, etc.
event(new PostPublished($post));
});
}
}
If using Laravel Nova, extend the resource:
use Novius\Publishable\PublishableTrait;
use Laravel\Nova\Fields\Select;
class Post extends Resource
{
public function fields(Request $request)
{
return [
// ...
Select::make('Status')
->options([
Publishable::STATUS_DRAFT => 'Draft',
Publishable::STATUS_PUBLISHED => 'Published',
Publishable::STATUS_UNPUBLISHED => 'Unpublished',
Publishable::STATUS_SCHEDULED => 'Scheduled',
])
->onlyOnDetail(),
];
}
}
Custom Validation:
Override the trait’s publish() method to add validation:
public function publish()
{
$this->validatePublishRules();
parent::publish();
}
protected function validatePublishRules()
{
if ($this->body === null) {
throw new \Exception('Body cannot be empty.');
}
}
Soft Deletes Compatibility:
Combine with SoftDeletes trait:
use Illuminate\Database\Eloquent\SoftDeletes;
use Novius\Publishable\PublishableTrait;
class Post extends Model
{
use SoftDeletes, PublishableTrait;
}
Note: A soft-deleted record cannot be published/unpublished (logic must be handled manually).
API Responses: Filter responses by state in controllers:
public function index(Request $request)
{
$posts = Post::when($request->status, function ($query, $status) {
return $query->where('status', $status);
})->published()->get();
return PostResource::collection($posts);
}
Scheduled Jobs: Use Laravel’s scheduler to auto-publish posts:
// In app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('publish:scheduled-posts')->everyMinute();
}
Create a command:
php artisan make:command PublishScheduledPosts
// app/Console/Commands/PublishScheduledPosts.php
public function handle()
{
Post::where('status', Publishable::STATUS_SCHEDULED)
->where('published_at', '<=', now())
->each->publish();
}
Testing: Use factories to create test data:
// database/factories/PostFactory.php
return [
'status' => Publishable::STATUS_DRAFT,
'published_at' => null,
'published_first_at' => null,
];
Test state transitions:
/** @test */
public function it_can_publish_a_post()
{
$post = Post::factory()->create(['status' => Publishable::STATUS_DRAFT]);
$post->publish();
$this->assertEquals(Publishable::STATUS_PUBLISHED, $post->status);
}
Missing Database Indexes:
The published_first_at and status fields should be indexed for performance:
Schema::table('posts', function (Blueprint $table) {
$table->index('status');
$table->index('published_first_at');
});
Timezone Handling:
The published_at field uses the application’s timezone (config('app.timezone')). Ensure this is set correctly to avoid scheduled post delays.
State Transition Logic: The package does not enforce business rules (e.g., "only admins can publish"). Implement these in your application logic:
public function publish()
{
if (!auth()->user()->isAdmin()) {
throw new \AuthorizationException('Unauthorized.');
}
parent::publish();
}
Soft Deletes Conflict: A soft-deleted model cannot be published/unpublished by default. Handle this explicitly:
public function publish()
{
if ($this->deleted_at) {
throw new \Exception('Cannot publish a deleted post.');
}
parent::publish();
}
Scheduled Posts in Development: Scheduled posts may not trigger immediately in development due to Laravel’s queue workers. Use:
php artisan queue:work
Or disable scheduling in tests:
$post->schedule(..., now()); // Force immediate publishing
Backfilling published_first_at:
When migrating existing data, ensure published_first_at is set correctly for published posts:
DB::table('posts')
->where('status', Publishable::STATUS_PUBLISHED)
->whereNull('published_first_at')
->update(['published_first_at' => DB::raw('created_at')]);
Check Status Values:
The package uses constants like Publishable::STATUS_PUBLISHED. Verify these in your code:
dd(Publishable::STATUS_DRAFT, Publishable::STATUS_PUBLISHED, Publishable::STATUS_UNPUBLISHED, Publishable::STATUS_SCHEDULED);
Query Logs: Enable query logging to debug scope issues:
DB::enableQueryLog();
Post::published()->get();
dd(DB::getQueryLog());
Event Debugging: Listen for events to trace state changes:
Post::published(function ($post) {
\Log::info('Post published', ['id' => $post->id]);
});
Migration Issues:
If migrations fail, ensure the publishable() macro is called after the timestamps() macro:
$table->timestamps();
$table->publishable(); // Correct order
class Post extends Model
{
use PublishableTrait;
const STATUS_ARCHIVED = 'archived';
protected $casts = [
'status' => 'string',
];
public function
How can I help you explore Laravel packages today?