Lifecycle & status
Every transition is a facade call (or the matching action or model method) that saves the ad and dispatches an event:
use RoundlyConsulting\Advertisements\Facades\Advertisements;
Advertisements::publish($ad); // live now → AdvertisementPublished
Advertisements::publish($ad, now()->addWeek()); // scheduled → AdvertisementPublished
Advertisements::unpublish($ad); // back to draft → AdvertisementUnpublished
Advertisements::expire($ad); // expired now → AdvertisementExpired
Advertisements::expire($ad, now()->addMonth()); // expires later → AdvertisementExpired
Advertisements::archive($ad); // → AdvertisementArchived
Advertisements::delete($ad); // soft delete → AdvertisementDeletedStatus
Every advertisement has a status of type RoundlyConsulting\Advertisements\Enums\AdvertisementStatus:
| Case | Value | Meaning |
|---|---|---|
Draft | draft | Created, not published. |
Scheduled | scheduled | published_at is in the future. |
Published | published | Live now and not expired. |
Expired | expired | expires_at is in the past. |
Archived | archived | Explicitly archived. |
Read the status through the status accessor (or the is*() helpers and the query scopes). It’s recomputed on every read: an ad you publish() or archive() reports its new status on the same instance, and an ad crossing its published_at or expires_at reads as Published / Expired the moment the clock passes it — even in a long-lived worker and without a re-save.
$ad->status; // AdvertisementStatus::Draft
$ad->publish();
$ad->status; // AdvertisementStatus::Published — same instance, no refresh needed
$ad->isPublished(); // trueuse RoundlyConsulting\Advertisements\Enums\AdvertisementStatus;
$ad->status; // AdvertisementStatus::Published
$ad->status === AdvertisementStatus::Expired; // true once expires_at passes — no re-save needed
$ad->status->label(); // "Published"The status column
- The status column holds a snapshot: create(), update() and every lifecycle action write the status the dates give at that moment, and only Archived overrides the dates.
- Archiving sticks through update() and expire() — an archived ad gets the expiry date but stays archived. Only publish() and unpublish() take an ad out of the archive.
- Time moves on without rewriting the column, so a scheduled ad’s column still says scheduled after it goes live. Query by state with the scopes (published(), active(), expired(), …), which compare the dates, rather than with where('status', …); archived() is the one that reads the column.
Timing
- publish($ad, $at) with a future instant stores Scheduled; the ad goes live by itself once published_at passes — no job or command needed.
- expire($ad, $at) with a future instant only sets expires_at; the ad reads as Expired once it passes. AdvertisementExpired fires at call time either way — time-based expiry itself fires no event.
- unpublish() clears published_at and returns the ad to Draft.
- archive() takes the ad out of the published, active, scheduled, expired and draft scopes without deleting it.
- delete() is a soft delete that keeps the creatives for a restore; forceDelete() removes them too, and expired ads are removed for good by pruning (see Artisan commands).
Convenience methods
Ask an ad about its state, or transition it directly — the model methods are sugar over the same manager, so they run the same actions, fire the same events and are recorded by Advertisements::fake():
$ad->isPublished(); $ad->isActive(); $ad->isExpired(); $ad->isScheduled(); $ad->isArchived();
$ad->publish(); // sugar for Advertisements::publish($ad) — same manager, same event, seen by the fake
$ad->publish(now()->addDay()); // schedule
$ad->expire(now()->addMonth()); // expire later
$ad->unpublish(); $ad->archive(); $ad->delete();
$ad->renderCreative('sidebar'); // sugar for Advertisements::render($ad, 'sidebar')isActive() is true for a published ad whose expires_at is empty or in the future. The model’s delete() routes through the manager and DeleteAdvertisement, so the soft delete and AdvertisementDeleted fire whichever way you call it.
Enum helpers
AdvertisementStatus and AdvertisementEventType use the shared Helpers trait from enums-for-laravel — labels, select options and a validation rule without hand-rolled code:
use RoundlyConsulting\Advertisements\Enums\AdvertisementEventType;
use RoundlyConsulting\Advertisements\Enums\AdvertisementStatus;
AdvertisementStatus::toArray(); // ['draft' => 'Draft', 'scheduled' => 'Scheduled', …]
AdvertisementStatus::options(); // {value, label, name} option DTOs for JS selects
AdvertisementStatus::validationRule(); // 'in:draft,scheduled,published,expired,archived'
AdvertisementEventType::Click->counterColumn(); // 'clicks_count'
$request->validate(['status' => ['required', AdvertisementStatus::validationRule()]]);Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.