Add a read-only Google AdSense analytics module for admins.
Connect AdSense over OAuth, sync entities and daily reports locally, and show placement performance in the admin panel without calling the Management API from public pages.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AdsenseConnection;
|
||||
use App\Services\Adsense\AdsenseSyncResult;
|
||||
use App\Services\Adsense\AdsenseSyncService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
final class AdsenseSyncCommand extends Command
|
||||
{
|
||||
protected $signature = 'adsense:sync
|
||||
{--days= : Number of trailing days to synchronize, including today}
|
||||
{--from= : Inclusive start date (YYYY-MM-DD)}
|
||||
{--to= : Inclusive end date (YYYY-MM-DD)}
|
||||
{--entities-only : Synchronize ad units and custom channels without reports}
|
||||
{--force : Run even when ADSENSE_SYNC_ENABLED is false}';
|
||||
|
||||
protected $description = 'Synchronize Google AdSense entities and daily reporting into Skinbase';
|
||||
|
||||
public function handle(AdsenseSyncService $sync): int
|
||||
{
|
||||
if (! (bool) config('adsense.sync_enabled', true) && ! $this->option('force')) {
|
||||
$this->warn('AdSense synchronization is disabled. Use --force to run anyway.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
[$from, $to] = $this->resolveRange();
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
return self::INVALID;
|
||||
}
|
||||
|
||||
$connection = AdsenseConnection::current();
|
||||
if ($connection === null || ! $connection->hasRefreshToken()) {
|
||||
$this->error('Google AdSense is not connected.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($connection->status === AdsenseConnection::STATUS_RECONNECT_REQUIRED) {
|
||||
$this->error('AdSense authorization expired or was revoked. Reconnect Google AdSense.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ((string) $connection->account_resource_name === '') {
|
||||
$this->error('No AdSense account is selected.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$entitiesOnly = (bool) $this->option('entities-only');
|
||||
$result = $sync->sync($connection, $from, $to, $entitiesOnly);
|
||||
|
||||
$this->renderResult($result, $entitiesOnly);
|
||||
|
||||
if ($result->reconnectRequired) {
|
||||
$this->error('AdSense authorization expired or was revoked. Reconnect Google AdSense.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($result->failedReports !== []) {
|
||||
$this->warn('Synchronization completed with partial report failures: '.implode(', ', $result->failedReports));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Synchronization completed successfully.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Carbon, 1: Carbon}
|
||||
*/
|
||||
private function resolveRange(): array
|
||||
{
|
||||
$days = $this->option('days');
|
||||
$from = $this->option('from');
|
||||
$to = $this->option('to');
|
||||
|
||||
if ($days !== null && ($from !== null || $to !== null)) {
|
||||
throw new \InvalidArgumentException('The --days option cannot be combined with --from or --to.');
|
||||
}
|
||||
|
||||
if (($from !== null) !== ($to !== null)) {
|
||||
throw new \InvalidArgumentException('Both --from and --to are required for a custom range.');
|
||||
}
|
||||
|
||||
if (is_string($from) && is_string($to)) {
|
||||
$start = Carbon::parse($from)->startOfDay();
|
||||
$end = Carbon::parse($to)->startOfDay();
|
||||
if ($end->lt($start)) {
|
||||
throw new \InvalidArgumentException('The --to date must be on or after --from.');
|
||||
}
|
||||
|
||||
return [$start, $end];
|
||||
}
|
||||
|
||||
$trailingDays = $days !== null ? max(1, (int) $days) : 3;
|
||||
$end = now()->startOfDay();
|
||||
$start = $end->copy()->subDays($trailingDays - 1);
|
||||
|
||||
return [$start, $end];
|
||||
}
|
||||
|
||||
private function renderResult(AdsenseSyncResult $result, bool $entitiesOnly): void
|
||||
{
|
||||
$this->line('AdSense account: '.($result->accountDisplayName !== '' ? $result->accountDisplayName : 'unknown'));
|
||||
$this->line('Range: '.$result->from.' -> '.$result->to);
|
||||
$this->newLine();
|
||||
$this->line('Entities:');
|
||||
$this->line(' Ad units: '.$result->adUnits);
|
||||
$this->line(' Custom channels: '.$result->customChannels);
|
||||
|
||||
if ($entitiesOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->line('Reports:');
|
||||
$this->line(' Total: '.$result->totalRows);
|
||||
$this->line(' Ad units: '.$result->adUnitRows);
|
||||
$this->line(' Custom channels: '.$result->customChannelRows);
|
||||
$this->line(' Platform: '.$result->platformRows);
|
||||
$this->line(' Countries: '.$result->countryRows);
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user