Install and Publish
composer require baks-dev/posters
php artisan vendor:publish --provider="BaksDev\Posters\PostersServiceProvider" --tag="posters-config"
Verify config/posters.php exists with default settings.
Configure Ad Slots
Edit config/posters.php to define your first slot:
'slots' => [
'header' => [
'width' => 728,
'height' => 90,
'pages' => ['home', 'blog/*'],
],
],
Create a Migration for Ad Campaigns
Since the package is minimal, manually create a migration for ad_campaigns:
php artisan make:migration create_ad_campaigns_table
Define schema with campaign_id, name, image_url, click_url, and impressions.
Display a Banner in Blade
Add this to your template (e.g., resources/views/layouts/app.blade.php):
@if(config('posters.enabled') && in_array(Request::path(), config('posters.slots.header.pages')))
<div class="posters-slot" data-slot="header"></div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const slot = document.querySelector('.posters-slot');
if (slot) {
fetch('/api/posters/header')
.then(response => response.text())
.then(html => slot.innerHTML = html);
}
});
</script>
@endpush
@endif
Create a Controller Endpoint
Add to routes/api.php:
Route::get('/api/posters/{slot}', [PostersController::class, 'render']);
Implement PostersController to fetch and render the ad:
public function render($slot)
{
$ad = AdCampaign::where('slot', $slot)
->where('active', true)
->inRandomOrder()
->first();
return response()->view('posters::ad', ['ad' => $ad]);
}
Test the Flow
/home).storage/logs/laravel.log).// app/Providers/PostersServiceProvider.php
public function register()
{
$this->app->singleton('posters.slots', function () {
return [
'sidebar' => [
'width' => 300,
'height' => 600,
'pages' => ['blog/*'],
'priority' => 10,
],
];
});
}
// app/Http/Middleware/EnablePosters.php
public function handle(Request $request, Closure $next)
{
if (auth()->check() && auth()->user()->is_premium) {
config(['posters.enabled' => false]);
}
return $next($request);
}
// app/Providers/BladeServiceProvider.php
Blade::directive('adSlot', function ($expression) {
return "<?php echo \\BaksDev\\Posters\\AdRenderer::slot({$expression}); ?>";
});
Usage:
@adSlot('header')
Route::get('/posters/render/{slot}', function ($slot) {
return response()->json([
'html' => AdRenderer::slot($slot),
'impression_id' => Str::uuid(),
]);
});
// After rendering an ad
event(new AdImpression($ad->id, $slot));
// Listen for clicks
AdClick::created(function ($click) {
Log::info("Ad clicked: {$click->ad_id}", ['user_id' => auth()->id()]);
});
AdImpression::created(function ($impression) {
$client = new \Google\Analytics\Data\V1beta\BetaAnalyticsDataClient();
$client->runReport([
'property' => 'properties/YOUR_PROPERTY_ID',
'dateRanges' => [new \Google\Analytics\Data\V1beta\DateRange(['start_date' => now()->toDateString(), 'end_date' => now()->toDateString()])],
'dimensions' => [new \Google\Analytics\Data\V1beta\Dimension(['name' => 'pagePath'])],
'metrics' => [new \Google\Analytics\Data\V1beta\Metric(['name' => 'sessions'])],
]);
});
AdCampaign model to support variants:
// database/migrations/...
Schema::table('ad_campaigns', function (Blueprint $table) {
$table->string('variant')->nullable();
$table->integer('variant_weight')->default(1);
});
Update the renderer to select variants by weight:
$ad = AdCampaign::where('slot', $slot)
->where('active', true)
->whereIn('variant', ['control', 'variant_a'])
->inRandomOrder()
->first();
// After rendering an ad
TrackImpression::dispatch($ad->id, $slot);
// app/Jobs/TrackImpression.php
public function handle()
{
$impression = new AdImpression([
'ad_id' => $this->adId,
'slot' => $this->slot,
'ip' => request()->ip(),
]);
$impression->save();
}
$html = Cache::remember("ad_slot_{$slot}", 300, function () use ($slot) {
return AdRenderer::slot($slot);
});
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
fetch(`/api/posters/${entry.target.dataset.slot}`)
.then(response => response.text())
.then(html => entry.target.innerHTML = html);
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.posters-slot').forEach(slot => observer.observe(slot));
.posters-ad {
max-width: 100%;
height: auto;
}
.posters-ad img {
display: block;
width: 100%;
}
ad_campaigns:
Schema::table('ad_campaigns', function (Blueprint $table) {
$table->index(['slot', 'active']);
$table->index('variant');
});
// app/Console/Commands/ArchiveAdImpressions.php
public function handle()
{
AdImpression::where('created_at', '<=', now()->subMonths(6))
->update(['archived' => true]);
}
ad_campaigns, ad_slots, and tracking. Without migrations, you’ll get ClassNotFound errors.PostersServiceProvider to register migrations:
public function boot()
{
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
How can I help you explore Laravel packages today?