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 Publishing Laravel Package

lemaur/eloquent-publishing

A Laravel package that provides a Publishes trait to make Eloquent models publishable, along with schema blueprint helpers for adding publishing date fields to migrations. Install via Composer and apply the trait to your models.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

  • The package is a thin, single-concern Eloquent add-on: one trait (Lemaur\Publishing\Database\Eloquent\Publishes), a set of Blueprint macros, and a handful of query scopes. It introduces no new tables, no polymorphic relations, no config file, and no runtime service dependencies beyond Carbon/Laravel's date handling.
  • State is encoded entirely in a nullable timestamp column (published_at by default, overridable via the PUBLISHED_AT class constant). This is the canonical Laravel idiom for "is this row visible yet," which means the package is a convention wrapper rather than an architectural commitment. Anything built on it remains queryable and understandable by developers who have never seen the package.
  • Query scopes (onlyPublished, onlyPlanned, onlyPlannedAndPublished, withoutPlannedAndPublished, latestPublished, oldestPublished, latestPlanned, oldestPlanned) all collapse to simple WHERE/ORDER BY clauses on one column, so they compose cleanly with our own scopes and eager loads and are index-friendly.
  • Event hooks (publishing, published, unpublishing, unpublished) map onto Laravel's model event system, giving us a natural extension point for cache busting, search indexing, sitemap regeneration, and audit logging without touching the package.
  • Notable architectural gap: the package models only two states (published, and "planned" = future-dated), with unpublish() resetting the column to null. There is no draft/review/archived state machine, no per-channel or per-locale publishing, no scheduled job, and no visibility/authorization concept. If our roadmap needs any of that, this trait becomes the storage primitive underneath a richer domain model, not the model itself.

Integration feasibility

  • composer require lemaur/eloquent-publishing, add use Publishes;, add $table->publishes(); (or publishesTz() for timestamptz on Postgres) to the migration. No service provider configuration, no publishable assets, no environment variables. Realistically a same-day integration for one model.
  • The Blueprint macros (publishes, publishesTz, dropPublishes, dropPublishesTz) are convenience only; they save one line per migration. They register globally via the package's service provider, which is a minor global-namespace surface we should be aware of but not fear.
  • The PUBLISHED_AT constant-based override works but is weaker than a protected $publishedAt property: constants can't be overridden per-instance, can't be configured at runtime, and can't be changed by a subclass consumer without redeclaring. If we need per-model variation beyond one column name, we're already at the edge of the package's design.
  • No published support policy or versioning contract beyond semver tags; v4.0.0 was a breaking drop of Laravel 10 and PHP 8.1, which signals the maintainer versions aggressively with upstream EOL rather than maintaining LTS branches.

Technical risk

  • Low code risk, moderate longevity risk. The implementation surface is small enough to audit in an hour and small enough to reimplement if the package is abandoned (~100–200 LOC equivalent). Zero dependents and 20 stars mean no ecosystem pressure testing: we will be one of the first production users to hit edge cases.
  • Single-maintainer bus factor. Releases are largely Dependabot churn plus periodic Laravel-version bumps. Positive counter-signals: SHA-pinned GitHub Actions, least-privilege workflow permissions, a Composer supply-chain audit workflow, and Dependabot auto-merge gated on tests — this is better supply-chain hygiene than most packages at this popularity tier.
  • Semantic edge cases to validate: future-dated publish() relies on wall-clock comparison at query time, so cache layers, CDNs, static generation, and search indexes will not flip content visible at the scheduled instant without our own trigger; unpublish() is destructive (nulls the column) and loses the prior publication timestamp; no transaction wrapping is documented, so event listeners that fail may leave state ambiguous; the trait must coexist with SoftDeletes and any trait that adds global scopes (ordering and boot-method interaction needs a test).
  • Name-collision risk: the dispatched event names are generic (published, publishing). If we already listen for similarly named domain events, or adopt another package using the same vocabulary, listeners may cross-fire.

Key questions

  1. Does publish() persist immediately (save()) and does it respect $fillable/$guarded, model events, and open transactions? Can we get a publishQuietly() or wrap it in our own transaction?
  2. How is timezone handled end-to-end — is published_at stored in UTC and compared against now() in app timezone consistently, and does publishesTz() behave correctly on MySQL (where timestampTz is not native)?
  3. What is required to make scheduled publishing actually take effect for cached/CDN'd/SSG'd surfaces, and is that our job or the package's?
  4. Does the column override via const PUBLISHED_AT survive inheritance, and is there any roadmap to a static property?
  5. Do the scopes add a default index recommendation, or do we own index creation separately?
  6. What is the maintainer's stated support window for Laravel majors, and how quickly do breaking majors land after upstream EOL?

Integration Approach

Stack fit

  • Requires Laravel 11+ and PHP 8.2+ as of v4.x; explicitly supports Laravel 12/13 and PHP 8.5 in recent releases. This aligns with a modern Laravel codebase and rules the package out only for legacy L10/PHP 8.1 applications — a clean, unambiguous compatibility boundary rather than a murky one.
  • Framework-native: no custom query builder, no custom cast, no facade, no container bindings beyond macro registration. It will not fight our existing repository/service layer, API resources, or policy layer.

Migration path

  • Add the column in a dedicated migration, not in the original create_* migration, so it is reversible and independently deployable. Include the index in the same migration: $table->publishes() followed by an index on published_at (composite with deleted_at/tenant key if those filters are common).
  • Backfill deterministically from existing signals rather than guessing: e.g. UPDATE posts SET published_at = created_at WHERE status = 'published', and leave true drafts null. Run backfill as a batched job for large tables, and verify row counts before/after.
  • Replace ad-hoc whereNotNull('published_at') / where('published_at', '<=', now()) checks with the package scopes so there is exactly one definition of "published" in the codebase. This is the main durable value of adopting the package at all.
  • Introduce a thin internal abstraction (e.g. a Publishable interface or a small domain service with publish/unpublish/isVisible) that delegates to the trait. This costs an hour and makes the package swappable or forkable without touching call sites.

Compatibility

  • Multi-tenant / multi-locale: the package has no concept of either. If content visibility is scoped per tenant or locale, we need either separate columns, a pivot, or a wrapper layer — decide this before rollout, not after.
  • Coexistence with SoftDeletes, HasTranslations, and other trait suites must be covered by a test asserting scope composition and event firing order.
  • Test-suite compatibility: use Carbon::setTestNow() to assert isPublished/isPlanned boundaries, and add a migration test that fails loudly if the column is missing.

Sequencing

  1. Spike on a branch against a low-traffic, non-critical model (e.g. Post/Announcement) with a full test suite covering scopes, events, and timezone boundaries.
  2. Ship the column + index + backfill migration behind a no-op deploy (column present, scopes unused).
  3. Switch read paths to onlyPublished() and friends; verify query plans and cache behavior.
  4. Switch write paths to publish()/unpublish() behind the internal abstraction; wire event listeners for cache/search/sitemap invalidation.
  5. Roll out to remaining models only after the scheduled-publish/cache-invalidation pattern is proven once.

Operational Impact

Maintenance

  • Zero-config dependency with no config file to drift and no published assets to keep in sync. Upgrades are Composer bumps; the only real maintenance event is the maintainer's roughly annual breaking major tied to Laravel/PHP EOL (v4.0.0 dropped L10/PHP 8.1). Budget one small upgrade PR per Laravel major cycle, which is manageable.
  • The dependency is small enough to vendor or fork at any time; treat that as the contingency plan rather than a bridge we cross later. If the package goes stale for two Laravel majors, the fallback is a local trait of comparable size.

Support

  • Community-only: no vendor, no SLA, no security policy beyond a SECURITY.md reference and MIT terms. Acceptable for a non-security-critical visibility helper that touches no auth, no crypto, and no external I/O.
  • Diagnostics are straightforward because the failure surface is one column: most issues will be "column missing," "timezone wrong," or "cache stale" — all debuggable from application logs without package internals.

Scaling

  • Negligible runtime cost: scopes are plain predicates and, with an index on published_at, the onlyPublished filter is a cheap range scan. No N+1 risk, no extra queries, no per-request overhead from the trait.
  • Scheduled publishing scales for free at the read layer — a future-dated row simply stops matching onlyPublished and later starts matching, with no job scheduler involvement. That is elegant for database reads and the exact reason it breaks for caches, CDNs, SSGs, and search indexes, which all need a separate invalidation trigger.
  • Large backfills and index creation on high-write tables are the real operational cost; run them in a maintenance window or with online DDL, and prefer a composite index matching actual filter patterns over a single-column index.

Failure modes

  • Scheduled content doesn't appear (or disappear) on time behind caches/CDN/SSG/search — highest-probability production incident. Mitigate with a scheduled warm-up/invalidation job that queries onlyPlanned() for rows crossing the boundary.
  • Timezone drift between app timezone, DB timezone, and now() causing off-by-hours visibility windows; pin everything to UTC and test boundaries.
  • Irreversible unpublish: nulling published_at loses the original publication date and any audit trail. If we need history, capture it in an events listener or add an audit table.
  • Event listener failure mid-publish leaving state or downstream indexes inconsistent; wrap publish calls in transactions and make listeners idempotent/queued.
  • Trait/global-scope interaction with other packages silently restricting results; cover with a composition test.
  • Missing column in a forgotten environment surfacing as a hard SQL error on deploy; gate with a migration assertion.

Ramp-up

  • Effectively zero for any Laravel developer: the README is complete, idiomatic, and example-driven, and the entire API is discoverable from the trait. Onboarding cost is measured in minutes, with the only real learning being our own internal abstraction and the scheduled-publish cache pattern.
  • Overall TPM recommendation: adopt as a small, well-contained utility on content-style models, wrapped in an internal interface, with an index and a cache-invalidation story owned by us. Do not let it define the domain's publishing workflow — its scope (one nullable column, two states) is deliberately narrower than any real editorial process, and its 20-star/zero-dependent profile means we should plan for the fork rather than assume the ecosystem will maintain it for us.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin