Weave 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

Eloquent Draftable Laravel Package

optix/eloquent-draftable

A Laravel package that adds draft and published states to Eloquent models, letting you save work-in-progress records and publish them later. Drafts stay separate from live content, so unfinished entries remain hidden from your application's normal queries until you're ready.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install via Composer: composer require optix/eloquent-draftable. The package ships a service provider (auto-discovered on Laravel 5.5+); no manual registration needed unless you've opted out.
  2. Check the version matrix before anything else. v2.1.0 adds Laravel 8 support and drops PHP 7.2. That means: your app must be on PHP 7.3+, and the newest framework this release targets is Laravel 8 — Laravel 9/10 are not covered. If you're on 9+, stop here; this release does not unblock you, and you should evaluate a replacement rather than fight the constraint.
  3. Publish and run migrations — this package stores drafts in a separate table (typically a polymorphic drafts table keyed by draftable_id / draftable_type with a JSON payload column). Run php artisan vendor:publish --provider="Optix\Draftable\DraftableServiceProvider" and inspect exactly what schema you get before writing app code. Confirm the column type (longText vs native json) against your DB driver.
  4. Add the trait to any model that needs drafts: use Optix\Draftable\Draftable;. This gives you the draft relationship and save/publish helpers. Check the README for the exact method names on your tagged version — the API has been stable, so the docs should match, but confirm against the tagged source rather than master.
  5. First use case: an admin "Save draft / Publish" workflow on a Post model. Create a draft ($post->saveAsDraft($request->validated()) or equivalent — verify the exact signature), preview it by reading the draft payload without touching the published row, and publish it back onto the source model. That is the whole value proposition: the public site keeps serving the published row while editors iterate on a draft.

Where to look first: the README's API section, the Draftable trait (to see which Eloquent events it hooks), and the migration stub (to verify the JSON column type). If you're upgrading from an older tag, read the composer.json diff for v2.1.0 first — the Laravel constraint and the PHP floor are the two things that can break your build before any app code runs.

Implementation Patterns

  • Single-draft-per-model: The package's mental model is one in-flight draft per model, not version history. If you need N concurrent edits or an audit trail, you need a different tool (or to layer versioning yourself). Design your admin UX around "one editor at a time per record."
  • Preview before publish: Read the draft payload and hydrate a throwaway model instance for rendering — this avoids polluting the published row and keeps your show route unchanged. Keep a single view component that renders either the draft or published instance so previews always match production output.
  • Publish flow: Wire "Publish" to a single action that copies the draft attributes onto the model and clears the draft, wrapped in a transaction. Emit your own domain event (e.g., PostPublished) after committing — don't rely on the package to fire model events for draft saves, because they generally don't.
  • Form validation split: Use one FormRequest per action (draft vs publish) when publishing has stricter requirements than saving (e.g., draft allows missing SEO fields, publish requires them). Validate the draft payload as data, then validate again on publish before mutating the source model.
  • Admin index: Eager-load the draft relation so listing pages can show "has draft" badges without N+1 queries: Post::with('draft')->paginate() (confirm the relationship name in the trait).
  • Authorization: The package has no policy layer. Gate draft read/publish behind your own policies — preview and publish abilities on the model are a clean place to encode that.
  • Jobs/queues: If publishing triggers downstream work (sitemap rebuild, search reindex), dispatch after the transaction commits.
  • Pin and CI-matrix the upgrade: Pin to ^2.1 and add PHP 7.3 / Laravel 8 to your test matrix explicitly. If your CI still runs PHP 7.2 anywhere (older Docker images, a shared base image, a matrix leg you forgot about), it will fail at composer install — not at runtime — which is easy to misread as a package bug.

Gotchas and Tips

  • Version ceiling is Laravel 8: v2.1.0 is a compatibility release, not a feature release — it widens the supported range to Laravel 8 but does nothing for 9/10. If your roadmap includes a framework upgrade, treat this package as a blocker to plan for now, and isolate it behind your own publish/preview actions so swapping it out later is a contained change. Pin the version: "optix/eloquent-draftable": "^2.1".
  • PHP floor moved to 7.3: Dropping 7.2 is low-risk for most apps, but check transitive resolution too — composer update optix/eloquent-draftable --with-dependencies -W in a scratch branch and diff composer.lock. Anything else in your tree still requiring php: ^7.2 constraints can produce a confusing conflict.
  • Maintenance signal is mixed, not resolved: The package was dormant for a long stretch and v2.1.0 is a narrow compatibility bump (one framework version, one PHP drop) with no changelog of fixes or new features. Read the trait source end-to-end before shipping — it's small — and assume no upstream support. Small and stable is fine; small and abandoned is a liability. Budget for owning it.
  • Drafts are data, not models: You're storing a serialized payload, so Eloquent casting, mutators, and relationship logic on the parent do not automatically apply to the draft. If your model casts published_at to a datetime or tags to an array, you must reapply that casting when reading the draft back. Keep a toDraftArray() / fromDraftArray() pair on the model to centralize this.
  • Relationships are not drafted: A draft of a Post won't carry its comments, categories, or media attachments. If the editor needs to touch relations, either defer relation editing until after publish or store them in the draft JSON yourself.
  • Uploads and media: Same story — do not point the draft at temporary media that gets garbage-collected. Use a separate staging collection/disk and only "promote" files on publish.
  • Unique constraints: Because the draft lives in a separate table, saving a draft with a duplicate slug won't be caught by the DB. Validate uniqueness at save-time if slugs matter, or accept that publish is where conflicts surface — and handle that failure gracefully in the UI.
  • JSON column behavior across Laravel versions: The Laravel 8 support in v2.1.0 is exactly the sort of change that touches json casting behavior. Even on a supported version, verify round-tripping of arrays, empty strings, and null through save → read → publish with a test, rather than assuming the 2020-era behavior still holds on your Laravel 8 point release.
  • Concurrency: No optimistic locking out of the box. Two editors on the same record will last-write-win. Add a draft_updated_at column (or a cache lock keyed on the model) if concurrent editing is realistic.
  • Debugging: When a draft "doesn't save," check (a) the payload column length on MySQL (longText vs text), (b) mass-assignment — the package uses fill()/forceFill() on the draft model, so $fillable on the draft model matters, not your source model, and (c) JSON encoding failures on floats/NAN from form casts. After the v2.1.0 bump, also confirm the installed version matches what you expect: composer show optix/eloquent-draftable.
  • Extension points to lean on: The draft model and table name are usually configurable via the published config. If you outgrow the single-draft model, keep the trait for the storage shape but write your own publish/preview actions so you can swap the strategy later without rewriting controllers.
  • Testing: Assert both states — assertDatabaseHas('posts', [...]) for the published row and assertDatabaseHas('drafts', [...]) for the payload. This is the cheapest way to catch a regression where "Save draft" silently overwrites the live row.
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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment