Bir Laravel Kuyruk İşinde CAPTCHA Nasıl Çözülür (PHP SDK)

Bir Laravel kuyruk işinde CAPTCHA çözmek için CapSkip PHP istemcisini işin handle metodunun içinde çağırın ve token’ı aynı iş içinden gönderin. Çağrı kısadır. Dikkat isteyen kısım, Laravel’in saniyeler içinde biten işler için belirlediği üç sayıdır: worker her işe 60 saniye verir, kuyruk bir işi 90 saniye sonra başka bir worker’a devreder ve her iş tek bir deneme hakkı alır. Bir reCAPTCHA çözümü 300 saniyeye kadar yoklama yapabilir; bu yüzden varsayılan ayarlarla bir Laravel kuyruk captcha işi ya yarıda öldürülür ya da hâlâ çözerken başarısız olarak işaretlenir, yeniden denemeleri eklediğinizde ise iki kez çalışabilir. Bu rehber işi, zaman aşımlarını, bir yeniden deneme kuralını ve worker Windows’ta çalıştığında değişen tek şeyi ele alıyor.
Neye ihtiyacınız var
- Laravel 11 veya üzeri. İş, 11 sürümüyle gelen tek Queueable trait’ini kullanır; buradaki diğer her şey 10 sürümünde de çalışır. Laravel 13, iş ayarlarını #[Tries(3)] gibi attribute’lar olarak da ifade edebilir; aşağıda kullanılan düz özellikler orada da çalışmaya devam eder.
- CapSkip PHP paketi; PHP 8.0 veya üzeri ile curl ve json eklentilerini gerektirir. Worker’ınızın gerçekten yüklediği php.ini dosyasında curl’ün etkin olduğunu kontrol edin, çünkü Windows’ta bu satır bazen yorum satırı olarak bırakılır.
- Bir kuyruk bağlantısı. Yeni Laravel uygulamaları database sürücüsünü kullanır, aşağıdaki örnekler de öyle.
- Bir Windows makinesinde çalışan CapSkip. Local modunda yalnızca o makineye 127.0.0.1 üzerinden yanıt verir; bu, aynı bilgisayardaki bir worker için uygundur. Server modunda ağ adresinizi veya genel IP’nizi dinler, böylece bir Linux sunucusundaki, Forge’daki ya da başka herhangi bir host’taki Laravel uygulaması ona API üzerinden ulaşabilir. İkisi de şurada ayarlanır: bağlantı ayarları.
# Run in the Laravel project root composer require capskip/capskip
1. Adım: istemciyi servis konteynerine kaydedin
Çözücünün adresini işin içinde değil yapılandırmada tutun; böylece aynı kod hem yerel bir çözücüye hem de sunucudaki bir çözücüye karşı çalışır. config/services.php dosyasına bir blok, service provider’ınıza da bir singleton binding ekleyin.
// config/services.php
'capskip' => [
'key' => env('CAPSKIP_API_KEY', 'capskip'),
'host' => env('CAPSKIP_HOST', '127.0.0.1'),
'port' => (int) env('CAPSKIP_PORT', 8080),
],
// app/Providers/AppServiceProvider.php, inside register()
$this->app->singleton(\CapSkip\CapSkip::class, fn () => new \CapSkip\CapSkip([
'apiKey' => config('services.capskip.key'),
'host' => config('services.capskip.host'),
'port' => config('services.capskip.port'),
]));İstemci ortam değişkenlerini hiçbir zaman kendiliğinden okumaz; binding’in her değeri tek tek iletmesinin nedeni budur. Uygulamanın başka her yerinde bu değerleri env() yerine config() ile okuyun: config:cache çalıştırdıktan sonra config dosyalarının dışındaki env() çağrıları null döndürür.
2. Adım: tek bir işte çözün ve gönderin
İstemciyi handle() metodunda type-hint ile belirtin; Laravel singleton’ı enjekte eder. İş, sitekey’i sayfadan okur, çözer ve formu gönderir.
<?php
namespace App\Jobs;
use CapSkip\CapSkip;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;
class SubmitSignup implements ShouldQueue
{
use Queueable;
public function __construct(public string $pageUrl, public array $fields) {}
public function handle(CapSkip $solver): void
{
$html = Http::timeout(30)->get($this->pageUrl)->throw()->body();
preg_match('/data-sitekey="([^"]+)"/', $html, $m);
// Solve and submit in one job: the token expires in two minutes.
$token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
Http::asForm()->timeout(30)
->post($this->pageUrl, $this->fields + ['g-recaptcha-response' => $token])
->throw();
}
}İşi sayfa ve formun diğer alanlarıyla, bir sonraki adımda tanımlanan kendi bağlantısına ve kuyruğuna dispatch edin:
SubmitSignup::dispatch('https://example.com/signup', ['email' => 'YOUR_EMAIL'])
->onConnection('captcha')
->onQueue('captcha');Çözümü ve gönderimi bir arada tutun. Bir reCAPTCHA token’ı yaklaşık iki dakika geçerlidir ve zincirlenmiş ikinci bir işe devredilen bir token bu süreyi başka işlerin arkasında bekleyerek geçirebilir. Bir reCAPTCHA token’ı ne kadar süre geçerli kalır başlıklı yazı bu süreyi ayrıntılı olarak ele alıyor. PHP istemcisi senkron çalışır, bu yüzden worker süreci çözüm boyunca meşgul kalır; Laravel’de çözümleri yan yana çalıştırmanın yolu farklı bir istemci değil, daha fazla worker sürecidir.
3. Adım: zaman aşımlarını sıraya koyun
Bir Laravel kuyruk CAPTCHA işinde dört ayrı zamanlayıcı çalışır. İçeriden dışarıya doğru:
- CapSkip’in kendi ayarları. Bir reCAPTCHA görevi boş bir thread için en fazla 250 saniye bekleyebilir (Wait Timeout) ve çözülmek için 250 saniye alır (Row Timeout). Bunlardan biri dolduğunda, istemcinin aşağıda anlatılan kendi sınırı daha önce dolmamışsa, CapSkip görevi başarısız sayar ve istemci bir ApiException fırlatır.
- İstemcinin yoklaması. recaptcha(), varsayılanı 300 saniye olan recaptchaTimeout süresine kadar yoklama yapar, ardından bir TimeoutException fırlatır. Etrafındaki iki HTTP çağrısı yukarıda her biri 30 saniyeyle sınırlandığından, CapSkip normal yanıt verdiğinde handle() metodunun tamamı yaklaşık altı dakika içinde biter. Ancak 300 saniyelik süre sınırı yoklamalar arasında kontrol edilir ve istemcinin gönderdiği her istek 120 saniyeye kadar askıda kalabilir; bu yüzden yanıt vermeyi bırakan bir CapSkip, bir işi bu sürenin ötesine uzatabilir.
- İş zaman aşımı. Worker’ın timeout seçeneğinin varsayılanı 60 saniyedir ve bundan uzun süren bir işin worker süreci öldürülür. İşe, yukarıdaki altı dakikadan daha uzun bir $timeout ayarlayın.
- retry_after. Her kuyruk bağlantısı, rezerve edilmiş bir işi bu süre kadar çalıştıktan sonra kuyruğa geri bırakır; varsayılan değer 90 saniyedir. İşin deneme hakkı kalmışsa, bir worker’da hâlâ süren yavaş bir çözüm başka bir worker’da yeniden başlar; bu da ikinci bir çözüm ve ikinci bir form gönderimi demektir. Hakkı kalmamışsa ikinci worker işi hemen başarısız olarak işaretler, ilk worker ise yine de formu göndermeye devam edebilir.
Laravel dokümantasyonuna göre zaman aşımı değeri her zaman retry_after değerinden en az birkaç saniye daha kısa olmalıdır. Yani sıralama şöyledir: önce istemci, ardından iş zaman aşımı, en son da retry_after. CAPTCHA işlerine kendilerine ait bir bağlantı verin; böylece uzun retry_after değeri uygulamadaki diğer tüm işlerin kurtarılmasını yavaşlatmaz:
// config/queue.php, under 'connections'
'captcha' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => 'captcha',
// Longer than the job's timeout, so no second worker takes it.
'retry_after' => 420,
'after_commit' => false,
],
// On the job class
public $timeout = 390;Worker’ı bu bağlantı ve kuyruk üzerinde çalıştırın:
php artisan queue:work captcha --queue=captcha
İşin $timeout değeri worker’ın kendi timeout seçeneğinden önceliklidir, bu yüzden komutta bu seçeneğe gerek yoktur.
4. Adım: yalnızca yeniden denemenin düzeltebileceği hataları yeniden deneyin
Aksini belirtmediğiniz sürece bir iş tek bir deneme hakkı alır, bu yüzden herhangi bir istisna onu kalıcı olarak başarısız kılar. Bazı CAPTCHA hataları bir deneme daha yapmaya değer: bir ERROR_CAPTCHA_UNSOLVABLE sonucu, bir TimeoutException ya da CapSkip yeniden başlatıldığı için oluşan bir NetworkException. Diğerleri tekrarda asla başarılı olmaz: yanlış bir API anahtarı, hatalı biçimlendirilmiş bir sitekey (ERROR_GOOGLEKEY) ya da istemcinin reddettiği bir parametre. Google’ın reddettiği bir sitekey ERROR_CAPTCHA_UNSOLVABLE olarak döner, bu yüzden diğer çözülemez sonuçlar gibi yeniden deneme haklarını tüketir. Üç denemeye izin verin, aralarına zaman koyun ve umutsuz durumları hemen başarısız kılın.
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\ValidationException;
// On the job class
public $tries = 3;
public $backoff = [30, 120];
// Inside handle(), around the solve
try {
$token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
} catch (ValidationException $e) {
$this->fail($e); // a bad parameter will not fix itself
return;
} catch (ApiException $e) {
if (! str_contains($e->getMessage(), 'UNSOLVABLE')) {
$this->fail($e); // a wrong API key and the like
return;
}
throw $e; // retried after the backoff
}$this->fail() marks the job failed without spending the remaining attempts, and anything rethrown is retried after 30 seconds, then 120. A job killed by its timeout also uses up an attempt, but it is picked up again only when retry_after expires, not after the backoff. A 4xx from the form also throws, and the retry solves and posts again. If the site answers 4xx for bad input, catch Illuminate\Http\Client\RequestException around the post and call $this->fail($e) when $e->response->clientError() is true.
Worker’ı Windows’ta çalıştırmak
Laravel’i CapSkip ile aynı Windows bilgisayarda çalıştırmak en basit kurulumdur ve tek bir tuzağı vardır. Laravel iş zaman aşımlarını pcntl eklentisiyle uygular ve pcntl Windows’ta yoktur. Şu komut orada bool(false) yazdırır:
php -r "var_dump(extension_loaded('pcntl'));"pcntl olmadan işin $timeout değeri ve worker’ın timeout seçeneği sessizce yok sayılır ve bir iş, ne kadar sürerse sürsün, dönene kadar çalışır. retry_after yine de geçerlidir, çünkü bir sinyalle uygulanmaz, bir sonraki worker bir işi çektiğinde kontrol edilir. Yani Windows’ta Laravel’deki hiçbir şey yavaş bir işi durdurmaz. Bir çözümü sonlandıran şey CapSkip’in kendi Wait Timeout ve Row Timeout ayarları ile istemcinin 300 saniyelik yoklama sınırıdır, bu yüzden bunları yerinde bırakın. retry_after değerini bir işin sürebileceği en uzun sürenin üzerinde tutun: 420, normal yanıt veren bir CapSkip’i karşılar; 660 ise örneğin internet üzerinden Server modu kullanılırken takılan bir bağlantıyı da karşılar.
İki şey daha farklıdır. Laravel Horizon pcntl ve posix eklentilerini gerektirir, bu yüzden Windows’a kurulmaz; orada düz queue:work kullanın ya da Horizon’u, CapSkip’i Server modunda çağıran bir Linux host’ta çalıştırın. Ayrıca Supervisor yoktur; bu yüzden her worker’ı, onu yeniden başlatan bir zamanlanmış görev ya da servis sarmalayıcısı altında çalıştırın. Linux’ta Supervisor’ın stopwaitsecs değeri en uzun işinizden uzun olmalıdır, yoksa bir dağıtım bir çözümü yarıda öldürür. Her iki durumda da bir dağıtımdan ya da .env değişikliğinden sonra, yapılandırmayı önbelleğe alıyorsanız php artisan config:cache, ardından php artisan queue:restart çalıştırın, çünkü bir worker açılırken yüklediği yapılandırmayı korur.
Her worker süreci aynı anda bir CAPTCHA çözer ve CapSkip varsayılan olarak aynı anda en fazla 10 reCAPTCHA çözümü çalıştırır (CapSkip’in reCAPTCHA ayarlarındaki Max. Threads). Yaklaşık ondan fazla worker’ı captcha kuyruğuna koymak, işlerin yalnızca CapSkip içinde beklemesine yol açar ve bu bekleme Wait Timeout süresinden düşer.
Tam çalışan örnek
<?php
namespace App\Jobs;
use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\ValidationException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Throwable;
class SubmitSignup implements ShouldQueue
{
use Queueable;
public $tries = 3;
public $backoff = [30, 120];
// GET 30 s + solve up to 300 s + POST 30 s, plus a margin.
public $timeout = 390;
public function __construct(public string $pageUrl, public array $fields) {}
public function handle(CapSkip $solver): void
{
$html = Http::timeout(30)->get($this->pageUrl)->throw()->body();
if (! preg_match('/data-sitekey="([^"]+)"/', $html, $m)) {
$this->fail(new RuntimeException("No sitekey on {$this->pageUrl}"));
return;
}
try {
$token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
} catch (ValidationException $e) {
$this->fail($e);
return;
} catch (ApiException $e) {
if (! str_contains($e->getMessage(), 'UNSOLVABLE')) {
$this->fail($e);
return;
}
throw $e;
}
// Submit in the same job, while the token is still valid.
Http::asForm()->timeout(30)
->post($this->pageUrl, $this->fields + ['g-recaptcha-response' => $token])
->throw();
}
public function failed(?Throwable $exception): void
{
logger()->warning('Signup gave up', [
'url' => $this->pageUrl,
'error' => $exception?->getMessage(),
]);
}
}Bunu 3. adımdaki captcha bağlantısı ve 1. adımdaki binding ile birlikte kullanın, 2. adımda gösterildiği gibi dispatch edin ve 3. adımdaki komutla bir worker başlatın. Sitekey deseni, reCAPTCHA’nın kendi kod parçacığının yazdığı çift tırnaklı data-sitekey özniteliğini bekler. Invisible veya Enterprise bir widget için recaptcha() metoduna uygun seçeneği verin; nasıl yapılacağı şu sayfada anlatılıyor: reCAPTCHA v2 çözücü sayfası; çağrının arkasındaki ham endpoint ise şurada: API referansı.
Sık görülen hatalar ve anlamları
| Gördüğünüz | Neden | Düzeltme |
|---|---|---|
| 60 saniye sonra "has timed out" hatası alınıyor ve iş başarısız olarak işaretleniyor | Worker’ın varsayılan zaman aşımı 60 saniyedir ve işin tek bir deneme hakkı vardır | İş üzerinde $timeout ayarlayın, yeniden deneme istiyorsanız $tries değerini de ayarlayın |
| Form iki kez gönderiliyor ya da tek bir iş için iki çözüm çalışıyor | İş, varsayılanı 90 saniye olan retry_after süresinden uzun sürdü ve ikinci bir worker onu aldı | CAPTCHA işlerini, retry_after değeri $timeout değerinden uzun olan bir bağlantıya koyun |
| Windows’ta bir iş $timeout değerini çok aşacak kadar çalışıyor | İş zaman aşımları pcntl gerektirir ve Windows’ta pcntl yoktur | CapSkip’in kendi zaman aşımlarına ve istemcinin 300 saniyelik sınırına güvenin; retry_after değerini de işin tamamının üzerinde tutun |
| "has been attempted too many times" hatası | İş, worker’ı öldükten ya da retry_after süresini aştıktan sonra $tries değerinin izin verdiğinden daha sık yeniden alındı | Bir dağıtım yüzünden iş ortasında öldürülen worker’ları ve retry_after süresinden uzun yaşayan işleri arayın |
| ERROR_CAPTCHA_UNSOLVABLE içeren ApiException | CapSkip görevi başarısız saydı; örneğin görev Row Timeout süresini aştığında | Laravel’in backoff süresinden sonra yeniden denemesi için istisnayı yeniden fırlatın |
| ERROR_KEY_DOES_NOT_EXIST içeren ApiException | CapSkip’te API anahtarı doğrulaması açık ve CAPSKIP_API_KEY oradaki hiçbir anahtarla eşleşmiyor | Anahtarı düzeltin ve worker’ları yeniden başlatın; işi yeniden denemek yerine başarısız kılın |
| Bir .env değişikliğinden sonraki ilk çözümde NetworkException | Worker hâlâ eski host ile çalışıyor ya da CapSkip çalışmıyor | Yapılandırmayı önbelleğe alıyorsanız php artisan config:cache, ardından php artisan queue:restart çalıştırın ve Local ya da Server modunu kontrol edin |
| Call to undefined function curl_init() hatası | curl eklentisi, worker’ın yüklediği php.ini dosyasında kapalı | O php.ini dosyasında extension=curl satırını etkinleştirin |
| İş "çalışırken" sayfa isteği zaman aşımına uğruyor | İş captcha bağlantısı olmadan dispatch edildi ve QUEUE_CONNECTION değeri sync olduğu için web isteğinin içinde çalıştı | 2. adımdaki gibi captcha bağlantısına dispatch edin ve bu bağlantıda bir worker çalıştırın |
| Site token’ı reddediyor | Token süresi dolduktan sonra ya da yanlış URL’ye gönderildi | Çözümün hemen ardından aynı iş içinden, formun action URL’sine gönderin |
FAQ
CAPTCHA neden controller içinde çözülmesin?
Çünkü bir çözüm onlarca saniye sürer ve birkaç dakikayı bulabilir. Bu, bir ziyaretçinin bekleyeceğinden de, çoğu PHP kurulumunun bir web isteğine tanıdığı 30 saniyelik max_execution_time süresinden de uzundur. İşi kuyruğa almak, controller’ın yanıtı hemen döndürmesini sağlar; yavaş kısım ise böyle bir sınırı olmayan bir worker sürecinde çalışır.
Bir Laravel kuyruk CAPTCHA işi Horizon altında çalışır mı?
Evet, pcntl bulunan bir host’ta, yani Linux veya macOS üzerinde. Horizon zaman aşımını config/horizon.php içinde supervisor başına ayarlar; bu yüzden captcha kuyruğunu çalıştıran supervisor’a işin 390 saniyesinin biraz üzerinde, örneğin 400 saniyelik bir timeout verin, çünkü auto balancing stratejisinde Horizon ölçeği küçültürken kendi timeout süresini aşan worker’ları durdurur. Redis bağlantısının retry_after değerini bu değerin de üzerinde tutun (420 işe yarar) ve CAPSKIP_HOST değerini, CapSkip’i Server modunda çalıştıran Windows makinesine yönlendirin.
Linux sunucusundaki bir Laravel uygulaması, Windows bilgisayarımdaki CapSkip’i kullanabilir mi?
Evet. Bağlantı ayarlarından CapSkip’i Server moduna alın; böylece bir ağ adresini dinler. Ardından uygulamanın .env dosyasında CAPSKIP_HOST değerini ayarlayın ve worker’ları yeniden başlatın. Rota internetten geçiyorsa statik bir genel IP ve yalnızca sunucunuza izin veren bir güvenlik duvarı kuralı kullanın, API anahtarı doğrulamasını da açın. Çözücü sizin kendi Windows makinenizde kalır, dolayısıyla çözümlerin nasıl sayıldığı konusunda hiçbir şey değişmez.
Bu yaklaşım Hangfire veya Celery ile nasıl kıyaslanır?
Yapı her yerde aynıdır: tek bir işte çözün ve gönderin, yalnızca yeniden denemenin düzeltebileceği hataları yeniden deneyin ve worker sayısını CapSkip’in thread sayısına göre ayarlayın. Farklı olan, can yakan varsayılandır. Laravel tek bir deneme hakkı verir ve yavaş bir işi ikinci bir worker’a devreder; bu worker işi ya başarısız kılar ya da yeniden denemelere izin verdiyseniz yeniden çalıştırır. Hangfire ise saatlere yayılan on yeniden deneme yapar. .NET sürümü şurada: Hangfire CAPTCHA işi rehberi.
Kısa özet
Bir Laravel kuyruk CAPTCHA işi için CapSkip istemcisini servis konteynerine bağlayın, çözümü ve gönderimi aynı işte yapın. CAPTCHA işlerine retry_after değeri 420 saniye olan kendi bağlantılarını verin, $timeout değerini 390, $tries değerini de bir backoff ile birlikte 3 olarak ayarlayın ve yeniden denemenin düzeltemeyeceği hataları başarısız kılın. Windows’ta pcntl olmadan hiçbir zaman aşımının uygulanmadığını unutmayın ve her dağıtımdan sonra worker’ları yeniden başlatın.
- PHP paketinin desteklediği tüm CAPTCHA türleri, sade PHP örnekleriyle: PHP CAPTCHA çözücü sayfası.
- Aynı reCAPTCHA çağrısı, Laravel dışında: PHP’de reCAPTCHA v2 çözme.
Bu ayarların karşılığını aldığınız yer yeniden denemelerdir. Çözülemeyen bir CAPTCHA’yı iki kez yeniden deneyen bir iş, eğer captcha çözücü kendi makinenizde çalışıyorsa olağan bir durumdur, çünkü her ek deneme orada ücretlendirilen yeni bir çözüme değil, worker ve thread zamanına mal olur.
