Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Publishable Laravel Package

pawelmysior/laravel-publishable

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation: Run composer require pawelmysior/laravel-publishable and ensure compatibility with your Laravel version (check the version table).
  2. Migration: Add a nullable published_at timestamp column to your model’s table:
    $table->timestamp('published_at')->nullable();
    
  3. Apply Trait: Use the Publishable trait in your Eloquent model:
    use PawelMysior\Publishable\Publishable;
    
    (No need to add published_at to $fillable in Laravel 9+; see README.)

First Use Case

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
}

Implementation Patterns

Core Workflows

  1. 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();
    
  2. 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
    
  3. Soft Deletes + Publishable: Combine with SoftDeletes for granular control:

    Post::onlyTrashed()->published()->restore(); // Restore only trashed *and* published posts
    
  4. 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();
        }
    }
    
  5. API Responses: Filter resources by publication status in controllers:

    return PostResource::collection(
        Post::published()->latest()->paginate(10)
    );
    

Integration Tips

  • Observers/Events: Trigger actions on published_at changes:
    class PostObserver {
        public function saved(Post $post) {
            if ($post->wasRecentlyPublished()) {
                event(new PostPublished($post));
            }
        }
    }
    
  • Policy Checks: Restrict access based on publication status:
    public function update(User $user, Post $post) {
        return $user->can('edit-posts') && $post->isPublished();
    }
    
  • Testing: Mock publication status in tests:
    $post = Post::factory()->create(['published_at' => now()]);
    $this->assertTrue($post->isPublished());
    

Gotchas and Tips

Pitfalls

  1. 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';
    }
    
  2. Time Zone Handling: published_at uses the model’s dateFormat or connection timezone. Explicitly set timezone if needed:

    $post->published_at = now()->setTimezone('UTC');
    
  3. 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.

  4. 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();
    
  5. Edge Cases:

    • NULL vs. Past Dates: The trait treats published_at = NULL as unpublished, but you might want to handle past dates (e.g., "expired") separately.
    • Concurrent Updates: Use transactions for publish/unpublish operations in high-traffic apps:
      DB::transaction(function () use ($post) {
          $post->unpublish();
          $post->update(['status' => 'archived']);
      });
      

Debugging Tips

  • Check Column Existence: Verify published_at exists in the DB and is nullable:
    php artisan schema:dump
    
  • Log Queries: Enable Laravel’s query logging to debug scope behavior:
    DB::enableQueryLog();
    Post::published()->get();
    dd(DB::getQueryLog());
    
  • Test Edge Cases: Manually set 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
    

Extension Points

  1. Custom Validation: Override publish()/unpublish() to add logic:

    public function publish() {
        if ($this->isScheduled()) {
            throw new \Exception("Cannot publish scheduled posts manually.");
        }
        parent::publish();
    }
    
  2. 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';
        }
    }
    
  3. Soft Deletes Integration: Add a isPermanentlyPublished() method:

    public function isPermanentlyPublished() {
        return $this->isPublished() && !$this->trashed();
    }
    
  4. 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(),
        ]);
    }
    
  5. 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(),
                                }),
        ],
    ]);
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui
babelqueue/php-sdk
facebook/capi-param-builder-php
babelqueue/symfony
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium