Proje dosyaları eklendi

This commit is contained in:
2026-08-29 15:53:53 +03:00
commit d3313400ca
440 changed files with 24827 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Jobs;
use App\Enums\SerpCheckStatus;
use App\Models\SerpCheck;
use App\Serp\Contracts\AsyncSerpProvider;
use App\Serp\Exceptions\SerpProviderException;
use App\Serp\Exceptions\UnexpectedProviderResponseException;
use App\Serp\Operations\OperationAuditLogger;
use App\Serp\SerpProviderRegistry;
use App\Services\SerpResultPersister;
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\Cache;
use Throwable;
class PollSerpCheck implements ShouldQueue
{
use Dispatchable,InteractsWithQueue,Queueable,SerializesModels;
public int $tries = 3;
public int $timeout = 60;
public array $backoff = [10, 60, 300];
public function __construct(public readonly string $checkUlid) {}
public function handle(SerpProviderRegistry $registry, SerpResultPersister $persister, ?OperationAuditLogger $audit = null): void
{
$audit ??= app(OperationAuditLogger::class);
Cache::lock('serp-poll:'.$this->checkUlid, 75)->block(1, function () use ($registry, $persister, $audit) {
$c = SerpCheck::where('ulid', $this->checkUlid)->firstOrFail();
if (in_array($c->status, [SerpCheckStatus::Completed, SerpCheckStatus::Failed], true)) {
return;
}if ($c->status !== SerpCheckStatus::Running || ! $c->provider_reference) {
$c->fail('provider_task_missing', 'SERP kontrolü tamamlanamadı.');
return;
}$attempts = $c->provider_poll_attempts + 1;
$c->forceFill(['provider_poll_attempts' => $attempts, 'provider_last_polled_at' => now(), 'provider_next_poll_at' => null])->save();
if ($attempts > (int) config('serp.dataforseo.poll_max_attempts') || $c->provider_task_posted_at?->lt(now()->subMinutes((int) config('serp.dataforseo.max_task_age_minutes')))) {
$c->fail('provider_task_expired', 'SERP kontrolü zamanında tamamlanamadı.');
$audit->record('poll_exhausted', ['check_ulid' => $c->ulid, 'attempt' => $attempts, 'error_category' => 'poll_exhausted']);
return;
}try {
$p = $registry->resolve($c->provider);
if (! $p instanceof AsyncSerpProvider) {
throw new UnexpectedProviderResponseException('Provider does not support standard mode.');
}$result = $p->collect($c->provider_reference, 10);
$c->forceFill(['provider_status_code' => $result->statusCode])->save();
if (! $result->ready) {
$delay = (int) config('serp.dataforseo.poll_interval') * min($attempts, 4);
$at = now()->addSeconds($delay);
$c->forceFill(['provider_next_poll_at' => $at])->save();
self::dispatch($c->ulid)->delay($at)->onQueue(config('serp.queue'));
$audit->record('poll_not_ready', ['check_ulid' => $c->ulid, 'attempt' => $attempts]);
return;
}$persister->complete($c, $result->response, ['provider_cost' => $c->provider_cost ?? $result->cost, 'provider_cost_currency' => $result->currency, 'provider_status_code' => $result->statusCode, 'provider_next_poll_at' => null]);
$audit->record('poll_completed', ['check_ulid' => $c->ulid, 'attempt' => $attempts, 'result_count' => count($result->response->results)]);
} catch (SerpProviderException $e) {
$audit->record('poll_failed', ['check_ulid' => $c->ulid, 'attempt' => $attempts, 'error_category' => $e->safeCode()]);
if ($e->retryable()) {
throw $e;
}$c->fail($e->safeCode(), $e->safeMessage());
} catch (Throwable) {
$c->fail('unexpected_error', 'SERP kontrolü tamamlanamadı.');
}
});
}
public function failed(?Throwable $e): void
{
$c = SerpCheck::where('ulid', $this->checkUlid)->first();
if ($c && $e instanceof SerpProviderException) {
$c->fail($e->safeCode(), $e->safeMessage());
} elseif ($c) {
$c->fail('job_failed', 'SERP kontrolü tamamlanamadı.');
}
}
public function tags(): array
{
return ['serp-check:'.$this->checkUlid, 'serp-mode:standard-poll'];
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace App\Jobs;
use App\Enums\SerpCheckStatus;
use App\Models\SerpCheck;
use App\Serp\Data\SerpQuery;
use App\Serp\Exceptions\SerpProviderException;
use App\Serp\Operations\ActivationManager;
use App\Serp\Operations\BudgetManager;
use App\Serp\Operations\CircuitBreaker;
use App\Serp\Operations\OperationAuditLogger;
use App\Serp\SerpProviderRegistry;
use App\Services\ProjectDomainMatcher;
use App\Services\SerpResultPersister;
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\Cache;
use Throwable;
class RunSerpCheck implements ShouldQueue
{
use Dispatchable,InteractsWithQueue,Queueable,SerializesModels;
public int $tries = 1;
public int $timeout = 60;
public array $backoff = [10, 60, 300];
public function __construct(public readonly string $checkUlid) {}
public function handle(SerpProviderRegistry $registry, SerpResultPersister|ProjectDomainMatcher $writer, ?ActivationManager $activation = null, ?BudgetManager $budget = null, ?CircuitBreaker $circuit = null, ?OperationAuditLogger $audit = null): void
{
$persister = $writer instanceof SerpResultPersister ? $writer : new SerpResultPersister($writer);
$activation ??= app(ActivationManager::class);
$budget ??= app(BudgetManager::class);
$circuit ??= app(CircuitBreaker::class);
$audit ??= app(OperationAuditLogger::class);
Cache::lock('serp-job:'.$this->checkUlid, 75)->block(1, function () use ($registry, $persister, $activation, $budget, $circuit, $audit) {
$c = SerpCheck::where('ulid', $this->checkUlid)->firstOrFail();
if (in_array($c->status, [SerpCheckStatus::Completed, SerpCheckStatus::Failed], true)) {
return;
}if ($c->execution_mode !== 'live') {
SubmitSerpCheck::dispatch($c->ulid)->onQueue(config('serp.queue'));
return;
}if ($c->status === SerpCheckStatus::Pending && ! $c->begin()) {
return;
}$started = microtime(true);
try {
if ($c->provider === 'dataforseo') {
$activation->assertPaid();
$circuit->assertClosed();
$budget->reserve('check:'.$c->ulid, 'live', $c->id);
}$audit->record('live_request_started', ['check_ulid' => $c->ulid, 'provider' => $c->provider, 'mode' => 'live']);
$response = $registry->resolve($c->provider)->search(new SerpQuery($c->phrase, $c->search_engine->value, $c->country_code, $c->language_code, $c->device->value, 10));
if ($c->provider === 'dataforseo') {
$budget->reconcile('check:'.$c->ulid, $response->cost, $response->referenceId);
$circuit->success();
}$persister->complete($c, $response, ['provider_cost' => $response->cost, 'provider_cost_currency' => $response->currency, 'provider_status_code' => $response->statusCode, 'duration_ms' => (int) ((microtime(true) - $started) * 1000)]);
$audit->record('live_request_completed', ['check_ulid' => $c->ulid, 'provider' => $c->provider, 'mode' => 'live', 'duration_ms' => (int) ((microtime(true) - $started) * 1000), 'result_count' => count($response->results)]);
} catch (SerpProviderException $e) {
if ($c->provider === 'dataforseo') {
$circuit->failure($e);
}$audit->record('live_request_failed', ['check_ulid' => $c->ulid, 'error_category' => $e->safeCode()]);
if ($e->retryable()) {
throw $e;
}$c->fail($e->safeCode(), $e->safeMessage());
} catch (Throwable) {
$c->fail('unexpected_error', 'SERP kontrolü tamamlanamadı.');
}
});
}
public function failed(?Throwable $e): void
{
$c = SerpCheck::where('ulid', $this->checkUlid)->first();
if ($c && $e instanceof SerpProviderException) {
$c->fail($e->safeCode(), $e->safeMessage());
} elseif ($c) {
$c->fail('job_failed', 'SERP kontrolü tamamlanamadı.');
}
}
public function tags(): array
{
return ['serp-check:'.$this->checkUlid, 'serp-mode:live'];
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Jobs;
use App\Enums\SerpCheckStatus;
use App\Models\SerpCheck;
use App\Serp\Contracts\AsyncSerpProvider;
use App\Serp\Data\SerpQuery;
use App\Serp\Exceptions\SerpProviderException;
use App\Serp\Exceptions\UnexpectedProviderResponseException;
use App\Serp\Operations\ActivationManager;
use App\Serp\Operations\BudgetManager;
use App\Serp\Operations\CircuitBreaker;
use App\Serp\Operations\OperationAuditLogger;
use App\Serp\SerpProviderRegistry;
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\Cache;
use Throwable;
class SubmitSerpCheck implements ShouldQueue
{
use Dispatchable,InteractsWithQueue,Queueable,SerializesModels;
public int $tries = 1;
public int $timeout = 60;
public array $backoff = [10, 60, 300];
public function __construct(public readonly string $checkUlid) {}
public function handle(SerpProviderRegistry $registry, ?ActivationManager $activation = null, ?BudgetManager $budget = null, ?CircuitBreaker $circuit = null, ?OperationAuditLogger $audit = null): void
{
$activation ??= app(ActivationManager::class);
$budget ??= app(BudgetManager::class);
$circuit ??= app(CircuitBreaker::class);
$audit ??= app(OperationAuditLogger::class);
Cache::lock('serp-job:'.$this->checkUlid, 75)->block(1, function () use ($registry, $activation, $budget, $circuit, $audit) {
$c = SerpCheck::where('ulid', $this->checkUlid)->firstOrFail();
if (in_array($c->status, [SerpCheckStatus::Completed, SerpCheckStatus::Failed], true)) {
return;
}if ($c->provider_reference) {
$this->poll($c);
return;
}if ($c->status === SerpCheckStatus::Pending && ! $c->begin()) {
return;
}try {
if ($c->provider === 'dataforseo') {
$activation->assertPaid();
$circuit->assertClosed();
$budget->reserve('check:'.$c->ulid, 'standard', $c->id);
}$audit->record('standard_submit_started', ['check_ulid' => $c->ulid, 'provider' => $c->provider, 'mode' => 'standard']);
$p = $registry->resolve($c->provider);
if (! $p instanceof AsyncSerpProvider) {
throw new UnexpectedProviderResponseException('Provider does not support standard mode.');
}$s = $p->submit(new SerpQuery($c->phrase, $c->search_engine->value, $c->country_code, $c->language_code, $c->device->value, 10));
$c->forceFill(['provider_reference' => $s->taskId, 'provider_task_posted_at' => now(), 'provider_cost' => $s->cost, 'provider_cost_currency' => $s->currency, 'provider_status_code' => $s->statusCode])->save();
if ($c->provider === 'dataforseo') {
$budget->reconcile('check:'.$c->ulid, $s->cost, $s->taskId);
$circuit->success();
}$audit->record('standard_submit_completed', ['check_ulid' => $c->ulid, 'provider' => $c->provider, 'mode' => 'standard', 'task_reference' => substr($s->taskId, 0, 8).'…']);
$this->poll($c);
} catch (SerpProviderException $e) {
if ($c->provider === 'dataforseo') {
$circuit->failure($e);
}$audit->record('standard_submit_failed', ['check_ulid' => $c->ulid, 'error_category' => $e->safeCode()]);
if ($e->retryable()) {
throw $e;
}$c->fail($e->safeCode(), $e->safeMessage());
} catch (Throwable) {
$c->fail('unexpected_error', 'SERP kontrolü tamamlanamadı.');
}
});
}
private function poll(SerpCheck $c): void
{
$at = now()->addSeconds((int) config('serp.dataforseo.poll_initial_delay'));
$c->forceFill(['provider_next_poll_at' => $at])->save();
PollSerpCheck::dispatch($c->ulid)->delay($at)->onQueue(config('serp.queue'));
}
public function failed(?Throwable $e): void
{
$c = SerpCheck::where('ulid', $this->checkUlid)->first();
if ($c && $e instanceof SerpProviderException) {
$c->fail($e->safeCode(), $e->safeMessage());
} elseif ($c) {
$c->fail('job_failed', 'SERP kontrolü tamamlanamadı.');
}
}
public function tags(): array
{
return ['serp-check:'.$this->checkUlid, 'serp-mode:standard-submit'];
}
}