File: /home/xedaptot/ai.naniguide.com/app/Jobs/Ads/AbstractAdsJob.php
<?php
namespace App\Jobs\Ads;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Throwable;
/**
* Base class for every queued Ads job.
*
* Enforces production-grade defaults:
* - Dedicated `ads` queue (email sends must not share — one Meta outage
* should NOT delay email deliveries)
* - Webhook callbacks go on separate `ads-webhooks` queue (subclasses
* override `$queue` to `'ads-webhooks'`)
* - Progressive backoff over 5 attempts up to 4 hours
* - Every subclass implements `failed()` — mandatory fatal log + AdActivityLog
* - Tags for filtering in Laravel Horizon (or any queue monitor)
*
* Acelle ships with `QUEUE_CONNECTION=database` by default. Commercial
* customers can swap to Redis for scale without touching job code.
*
* Subclasses MUST:
* - Implement `handle()`
* - Implement `failed(Throwable $e)` — call `parent::logFailure($e)` as minimum
* - Set `$customerId` + `$platform` properties in constructor when known,
* so `tags()` labels are accurate for monitoring
*/
abstract class AbstractAdsJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
// $connection and $queue are declared by Queueable trait WITHOUT defaults.
// Redeclaring them here would be "incompatible composition". Set via
// onConnection() / onQueue() in constructor.
// $tries, $backoff, $timeout, $maxExceptions are NOT declared by any
// framework trait — they are convention properties Laravel reads via
// reflection. Declare them here so subclasses (and PHPStan) can see them.
/** 5 retries over 1m → 5m → 15m → 1h → 4h, covering platform outages. */
public int $tries = 5;
/** @var array<int,int> */
public array $backoff = [60, 300, 900, 3600, 14400];
/** 5 minutes — must be longer than any single adapter call's timeout. */
public int $timeout = 300;
/** Stop retrying after 3 distinct exceptions (separate from $tries). */
public int $maxExceptions = 3;
/**
* Defaults bound in constructor. Subclasses MUST call parent::__construct()
* when they define their own __construct so queue routing is applied.
*
* Queue name comes from config('ads.queues.default') which defaults to
* 'default' — matches Acelle's out-of-the-box supervisor setup so existing
* installs keep working after upgrade. Customers opting in to queue
* isolation set ADS_QUEUE_NAME=ads in .env and reconfigure their worker.
*/
public function __construct()
{
$this->onQueue(config('ads.queues.default', 'default'));
}
/**
* Subclasses populate in constructor for Horizon tag filtering.
* Nullable — some jobs (PullAdMetrics across all customers) have no single scope.
*/
protected ?int $customerId = null;
protected ?string $platform = null;
/**
* Tags used by Laravel Horizon (if installed) + custom monitors.
*
* @return array<int,string>
*/
public function tags(): array
{
return array_values(array_filter([
'ads',
$this->platform ? "platform:{$this->platform}" : null,
$this->customerId ? "customer:{$this->customerId}" : null,
class_basename(static::class),
]));
}
/**
* Every subclass MUST implement `failed()`. This base helper gives them a
* standard structured log entry to call from their override:
*
* public function failed(Throwable $e): void
* {
* $this->logFailure($e);
* // job-specific cleanup: mark DB row as 'error', notify customer, etc.
* }
*/
protected function logFailure(Throwable $e): void
{
Log::channel(config('ads.observability.log_channel', 'stack'))->error(
'ads.job.failed',
[
'job' => static::class,
'customer_id' => $this->customerId,
'platform' => $this->platform,
'tries' => $this->attempts(),
'error_class' => $e::class,
'error_message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]
);
}
/**
* Shared implementation marker so subclasses can't accidentally skip failed().
*/
abstract public function failed(Throwable $exception): void;
}