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

Campaign Bundle Laravel Package

os2display/campaign-bundle

The os2display/campaign-bundle is a PHP package for the OS2Display digital signage platform that adds campaign management features, helping organize, schedule, and display campaign content across screens in OS2Display-based applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Read composer.json before anything else. The description says "Extensible PHP/Symfony architecture" — this is an OS2Display Symfony bundle, not a Laravel package. There is no Laravel service provider, no php artisan vendor:publish, no facade, and no config tag. "Install it in Laravel" is not a supported move; Composer will install it, but nothing will boot it. The realistic first use case for a Laravel developer is reference implementation, not dependency: read the bundle to learn how campaign override semantics work, then port the domain into Eloquent models and services. If you genuinely need to run it, it must live inside the OS2Display Symfony platform (with screen-bundle present, since 2.1.0 added support for it) as a sidecar service your Laravel app talks to over HTTP.

Where to look first, in this order:

  1. The Campaign entity — this is the domain model. Note which fields define the override window (start/end), which define targeting (channel/screen relations), and which define the content payload.
  2. The 2.1.0 diff for "screen-bundle support" — that's the code path that decides which screens a campaign forces. This is the whole point of the package and it is undocumented.
  3. The admin controller(s) — the create/edit/publish lifecycle. Compare "published" against "active"; they are usually not the same state.
  4. The bundle registration/config YAML — where the campaign bundle is wired into the screen bundle's resolution list.

First use case to replicate: a channel-bound content override with a time window. A campaign row referencing one or more channels, a start_at/end_at, and a set of content items. When a screen requests its content, the resolver checks for an active campaign on that screen's channels and returns the campaign content if present, otherwise the normal content. Ship the naive version first — campaign row, pivot table, resolver, one Blade component — before touching scheduling state machines.

Implementation Patterns

Resolution-time vs state-machine scheduling. The bundle leans on "force screens … for a given period, afterwards returning." Two ways to honour that, and you should pick deliberately:

Resolver pattern (recommended for Laravel). Store the window as data; compute active campaigns at render time:

$campaign = Campaign::query()
    ->whereHas('channels.screens', fn ($q) => $q->whereKey($screenId))
    ->where('published', true)
    ->where('start_at', '<=', now())
    ->where('end_at', '>', now())
    ->orderByDesc('priority')
    ->orderByDesc('start_at')
    ->first();

Wrap this in a CampaignResolver service and inject it wherever screen content is assembled. Cheap, testable with Carbon::setTestNow(), no cron dependency.

State-machine pattern. A scheduled command flips draft → published → active → expired. Use this only if downstream consumers (CDN purge, signage players polling an API) need to observe transitions. The bundle exposes the lifecycle, so mirroring it in Laravel is a reasonable port, but you inherit "what if the cron doesn't run" as a failure mode.

Channel binding is the integration seam. Model campaigns ↔ channels ↔ screens explicitly. Don't let a campaign reference a screen directly; the package targets channels, and screens are how you observe which channels matter. An API resource GET /api/screens/{id}/content that returns the resolved payload (campaign or default) is the cleanest consumption point for signage frontends and hides resolution logic from the player.

Render-layer integration. Inject the resolved campaign into your existing screen layout via a view composer or a small ScreenContentController, and keep campaign templates as a separate Blade namespace (campaigns::...). The package's templates are Twig — treat them as specification, not code you can drop in.

Screen-bundle registration, if you're porting it. The 2.1.0 capability is "this bundle participates in the screen bundle's list of content providers." In Laravel that becomes a tagged service collection: CampaignContentProvider implementing a ScreenContentProvider contract, registered in a ContentProviderRegistry. That keeps campaign support additive — you can add more providers later without touching the resolver.

Separate the icon change from the behaviour change. If you're porting, the icon is a one-line asset path. The screen-bundle support is a code path. Land them in separate commits so a blank screen is attributable.

Gotchas and Tips

Archived, 2019, zero stars. This is the dominant operational fact. No Laravel support, no security backports, and its Symfony/PHP constraints will almost certainly conflict with a modern Laravel app's dependency tree — a composer require may resolve but pull ancient Symfony components into your lockfile. Pin nothing; don't install it at all. Port the concepts.

Symfony ≠ Laravel, in concrete terms. Doctrine entities, Symfony DI extension classes, YAML routing and bundle registration, Twig templates. config/bundles.php has no Laravel analogue; auto-discovery does not apply. Any tutorial that says "register the bundle" is a Symfony instruction.

The icon change is a silent-breakage candidate. Release notes call it cosmetic, with no deprecation or alias path documented. If you port templates that reference the icon by filename, and any CDN or asset manifest caches that path, a same-name swap won't propagate and a renamed asset 404s on screens — visible to end users, not to logs unless you alert on asset 404s. Add a smoke test that asserts the campaign icon resolves.

Boundary semantics are undefined. Whether end_at is inclusive, and what happens when two campaigns overlap on the same channel, is not stated. Define both explicitly in your port: half-open window [start_at, end_at) stored in UTC, and a deterministic precedence rule (priority, then most recent start_at). Do not leave overlaps to query order.

Timezone handling. Campaign windows are human-entered; screens are distributed. Store UTC, render in the screen's configured locale, and never compare against NOW() in the database using local time. This is the most common source of "the campaign is on the wrong day" bug reports.

Fail-safe over fail-blank. "Force screens to only show the selected content" is dangerous if content resolution fails — you can black out a screen. Make the resolver fall back to default content and log loudly, rather than returning an empty payload. A wrong-content fallback is a bug; a blank screen is an incident.

Caching and invalidation. Cache resolution per screen for the polling interval, and invalidate on campaign publish/edit/expire. If a CDN sits in front of the player API, the key must include campaign state or you'll serve pre-campaign content past the start time — and post-campaign content past the end.

Extension points, if you must extend the bundle in situ. Don't edit vendor. Decorate the resolver as a Symfony service (decorates: in services YAML), or listen on the campaign publish event. That survives the inevitable "we need to patch this" moment.

No tests on the screen-bundle path. Treat that code as unproven. Your own coverage should assert, at minimum: campaign applies one second after start_at; reverts one second after end_at; overlapping campaigns resolve deterministically; a campaign bound to a channel the screen isn't on has no effect.

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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr