Skip to content

Feature/scheduled drafts - #11

Merged
oddvalue merged 26 commits into
mainfrom
feature/scheduled-drafts
Aug 6, 2026
Merged

Feature/scheduled drafts#11
oddvalue merged 26 commits into
mainfrom
feature/scheduled-drafts

Conversation

@oddvalue

@oddvalue oddvalue commented Feb 7, 2023

Copy link
Copy Markdown
Owner

Scheduled drafts

Adds the ability to schedule a draft to be published at a future date, revived from the original 2023 branch and reworked on top of current main.

Usage

$draft = $post->createDraft(['title' => 'Hello World']);

// Schedule (saves the record)
$draft->schedulePublishing(now()->addWeek());

// Change your mind (remember to save)
$draft->clearScheduledPublishing()->save();

Due drafts are published by the new artisan command, intended to run on the scheduler:

Schedule::command(PublishScheduledDrafts::class, [Post::class])->everyMinute();

What's included

  • will_publish_at timestamp column, added by the drafts()/dropDrafts() blueprint macros and overridable via config or a WILL_PUBLISH_AT model constant
  • schedulePublishing(CarbonInterface $date) / clearScheduledPublishing() on HasDrafts
  • drafts:publish {model} command; publishing fires the usual publishing/published events, and publishing a draft directly clears any scheduled date
  • Optional Contracts\Draftable interface covering the stable consumer-facing API. The trait satisfies it; implementing it is recommended and will become required in a future major. UPGRADING.md documents a tested Rector rule (AddInterfaceByTraitRector) that adds it to existing models
  • New UPGRADING.md covering enabling scheduled drafts and auto drafts on existing installations
  • Tests: scheduling flows, command validation, disabled-flag behaviour, and a MissingWillPublishAtColumnTest regression suite mirroring the is_auto one

Backwards compatibility

Non-breaking; ships as a minor:

  • The feature is opt-in behind drafts.scheduled_drafts.enabled (default false), mirroring the auto drafts flag. While disabled no query references the will_publish_at column, so existing tables without it keep working; the scheduling API throws
  • The column is only added to tables created with $table->drafts() from here on; upgrading installations add it manually before enabling (documented)
  • No existing method signatures changed; the blueprint macros gain optional trailing parameters only
  • The Draftable contract is optional and purely a consumer-facing typehint; the drafts:publish command requires an Eloquent model using the HasDrafts trait

Changes from the original branch

  • Dropped the 40-method Draftable interface in src/Contacts/ (namespace typo) in favour of a slim contract in src/Contracts/ that excludes internal plumbing, so future trait changes don't force interface breaks
  • Command requires an Eloquent model using the trait (the contract alone cannot guarantee the query APIs the command relies on), uses publish() instead of raw setLive() so model events fire, eachById() so batches aren't skipped, and <= for the due-time comparison
  • Everything gated behind the config flag (the original referenced the column unconditionally, which would have broken existing installs)

@oddvalue
oddvalue marked this pull request as draft February 7, 2023 07:49
@oddvalue
oddvalue force-pushed the main branch 2 times, most recently from cddcd91 to 16fa512 Compare February 11, 2023 10:49
@oddvalue
oddvalue force-pushed the feature/scheduled-drafts branch from 8a79a76 to 84ba00a Compare February 11, 2023 12:26
@oddvalue
oddvalue force-pushed the feature/scheduled-drafts branch from 16217c2 to 8527f0e Compare February 28, 2023 07:18
oddvalue and others added 9 commits February 28, 2023 07:29
# Conflicts:
#	database/factories/PostFactory.php
#	src/Concerns/HasDrafts.php
#	src/Concerns/Publishes.php
#	src/Facades/LaravelDrafts.php
#	src/LaravelDrafts.php
#	src/LaravelDraftsServiceProvider.php
#	tests/ConfigTest.php
#	tests/RevisionsTest.php
The contract covers the stable consumer-facing draftable API and is
satisfied by the HasDrafts trait. Implementing it on a model is optional
but recommended, allowing user code to typehint against the contract.
The drafts:publish command accepts models that either implement the
contract or use the trait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add UPGRADING.md covering enabling scheduled drafts and auto drafts on
existing installations, plus a Rector rule (verified working) for adding
the Draftable contract to existing models ahead of the future major that
will require it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The heading-structure rule is configured Codacy-side for README-shaped
files and does not fit an upgrade guide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oddvalue
oddvalue marked this pull request as ready for review August 5, 2026 23:34

@oddvalue oddvalue left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheduled publishing implementation still has one contract mismatch that should be resolved before merging.

PublishScheduledDrafts::handle() accepts any class implementing Draftable, but the contract does not require scheduledDraftsEnabled(), newQuery(), or the onlyDrafts() scope. A contract-only implementation therefore passes the validation at src/Commands/PublishScheduledDrafts.php:25 and then fails at runtime.

Please either restrict the command to Eloquent models using HasDrafts, or expand the abstraction and remove the command's reliance on APIs that the contract does not guarantee.

I also found two scheduled-publishing defects and fixed them locally for follow-up: the live row retained will_publish_at after the draft/live attribute swap, and offset-based each() could skip due drafts beyond the first batch. Those changes have not been pushed.

oddvalue and others added 4 commits August 6, 2026 00:53
Addresses review feedback on the contract mismatch: the Draftable
contract doesn't guarantee scheduledDraftsEnabled(), newQuery() or the
onlyDrafts() scope, so a contract-only implementation passed the
command's validation and then failed at runtime. The command now
requires an Eloquent model using the HasDrafts trait; the contract
remains a consumer-facing typehint.

Also includes the pre-existing local fixes: clear will_publish_at on
the live row during the publish attribute swap, and use eachById so
publishing is not skipped past the first batch, with regression tests
for both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stub's parameters are required by the Draftable interface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multi-batch regression test created 1001 records to cross the
default eachById page size, taking ~29s. The command now accepts a
--chunk option (default 1000), letting the test prove the same
cross-batch behaviour with 3 records and a chunk of 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
oddvalue and others added 3 commits August 6, 2026 01:18
throw_if/throw_unless conversions, strict_types declares and formatting
from the project Rector config. The Table/Fillable attribute conversion
on the test Post model is excluded because those attributes require
Laravel 12 and the test matrix still covers Laravel 11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New rector.yml workflow running a dry-run on push, mirroring the
  PHPStan workflow
- Skip the Table/Fillable attribute rules in rector.php since those
  attributes require Laravel 12 and the package supports Laravel 11
- Apply Rector to the remaining files so the dry-run passes repo-wide
- Cover the previously untested paths: the getDraftableAttributes
  branch when replicating HasOne/HasMany relations, cancelling publish
  from a publishing listener, the base Publishes trait's
  setPublishedAttributes, and the deprecated withoutSelf scope

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rector-laravel 2.5 added Signature, Description and WithoutTimestamps
attribute conversions. Those attributes require Laravel 12, and the
package still supports Laravel 11, so skip them alongside the existing
Table and Fillable exclusions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oddvalue
oddvalue merged commit 6889f55 into main Aug 6, 2026
16 checks passed
@oddvalue
oddvalue deleted the feature/scheduled-drafts branch August 6, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant