Zamanlanmış Görevler (Scheduler)
Sunucuya tek bir cron satırı yazın; görevleri panelden yönetin. Kendi cron ayrıştırıcısı sonraki çalışmayı hesaplar ve ifadeyi insan diline çevirir; atomik kilit üst üste binmeyi engeller, her çalışma süresi ve çıktısıyla kaydedilir.
Ekran Görüntüleri 6 görsel
Bu Örnek Ne Yapıyor?
- Sunucuda tek cron satırı; görevler veritabanından, panelden yönetilir
- Kendi cron ayrıştırıcısı: isValid, isDue, nextRun ve describe
- Operatörler: yıldız, */n, a-b, a,b,c ve a-b/n
- "Sıradaki çalışma" gerçek bir tarih olarak hesaplanır — yanlış kurulan görev anında belli olur
- İfade insan diline çevrilir ("Her 15 dakikada bir")
- Üst üste binmeyi atomik kilit engeller: kontrol ve yazma tek UPDATE ifadesinde
- Görev başına kilit zaman aşımı — çöken süreç görevi sonsuza kadar kilitlemez
- Her çalışma durum, milisaniye cinsinden süre ve çıktısıyla kaydedilir
- Başarısız görev durdurulmaz; bir kez patlayan görev, hiç çalışmayandan iyidir
Nerede İşe Yarar?
- Birden fazla zamanlanmış işi tek yerden görmek isteyenler
- "Gece raporu çalıştı mı?" sorusunu cevaplamak zorunda kalanlar
- Üst üste binen görevlerden dolayı çift kayıt üretenler
- Cron ifadelerinin PHP'de nasıl ayrıştırıldığını öğrenmek isteyenler
Gereksinimler
- PHP 8.0+ (CLI erişimi) · MySQL 5.7+ / MariaDB 10.3+ · pdo_mysql · mbstring · cron veya Görev Zamanlayıcı · Apache mod_rewrite
Nasıl Kurulur?
- Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
- database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
- .env.example dosyasını .env adıyla kopyalayıp veritabanı bilgilerinizi yazın
- Sunucuya tek cron satırını ekleyin: * * * * * php bin/scheduler.php
- Tarayıcıdan açıp [email protected] / Admin1234 ile giriş yapın
Veritabanı şeması projedeki database.sql dosyasında.
Nasıl Çalışıyor?
Zamanlanmış Görevler (Scheduler)
PHP 8 · PDO · MySQL · Oturum Girişli Panel · Cron Ayrıştırıcı · Kilit · Çalışma Geçmişi · Çılgın Yazılım Tasarım Kalıbı
Sunucuya tek bir cron satırı yazın; görevleri panelden yönetin.
🇹🇷 Türkçe · 🇬🇧 English
Canlı Demo
Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.
▲ Görsele tıklayarak demoyu açabilirsinizDemo hesapları
| Rol | E-posta | Parola |
|---|---|---|
| Yönetici | [email protected] | Admin1234 |
| Kullanıcı | [email protected] | Demo1234 |
Demoda 60 saniyede neleri deneyebilirsiniz?
| # | Şunu deneyin | Perde arkasında ne oluyor? |
|---|---|---|
| 1 | Zamanlanmış Görevler sayfasını açın, "Sıradaki" sütununa bakın | Her satırda gerçek bir tarih yazar. Bu, cron ifadesinin PHP'de ayrıştırılıp ileriye doğru hesaplanmasıyla bulunur — sabit bir metin değildir |
| 2 | "Günlük temizliği" satırının ifadesini 0 3 yerine /5 yapıp Kaydete basın | Altındaki açıklama anında "Her 5 dakikada bir"e döner ve "Sıradaki" tarihi yeniden hesaplanır. İnsan diline çeviri de aynı ayrıştırıcıdan gelir |
| 3 | İfadeye abc yazıp kaydetmeyi deneyin | Reddedilir. CronExpression::isValid() kullanıcıdan gelen her ifadeyi süzer; geçersiz bir ifade veritabanına yazılmaz |
| 4 | "Haftalık özet e-postası" satırına bakın: Sıradaki sütunu boş, "durduruldu" diyor | Görev pasiftir. Zamanı gelse bile çalıştırılmaz ve üstteki "etkin görev" sayacına dahil edilmez |
| 5 | Bir görevin Durdur / Başlat anahtarına basın | is_active değişir; sayaç anında güncellenir |
| 6 | "Şimdi çalıştır" düğmesine basın | Görev zamanına bakılmadan tetiklenir. Sonuç saniyeler içinde Çalışma Geçmişi'ne düşer: süre, çıktı ve durum |
| 7 | Çalışma Geçmişi sayfasını açın | Son 24 saatte 16 çalışma, 2'si hatalı. Başarısız satırlar kırmızı rozetli ve okunabilir bir hata mesajı taşır |
| 8 | "Kullanıcı yedeği" satırının son durumuna bakın: Hata | Bir görev başarısız olduğunda fail_count artar ama görev durdurulmaz. Bir kez patlayan görev, bir daha hiç çalışmayan görevden iyidir |
| 9 | Sayfanın altındaki Cron Sözdizimi kartına bakın | Beş alanın ne olduğu ve /15, 30 9-17/2 * 1-5 gibi ifadelerin ne anlama geldiği örneklerle yazılıdır |
| 10 | Telefonunuzdan açın | Görev tablosu daralır, sayaçlar alt alta dizilir; sayfa gövdesinde yatay kaydırma yoktur |
Not: Demo sunucusunda dakikada bir çalışan gerçek bir cron yoktur; geçmişteki kayıtlar örnek veridir. "Şimdi çalıştır" düğmesi ise gerçekten çalışır ve geçmişe yeni bir satır ekler.
Demo alanı hakkında bilinmesi gerekenler
| Konu | Durum |
|---|---|
| Veriler | database.sql içindeki 51 kullanıcı + 5 görev + 21 çalışma kaydı. Gerçek kişi verisi yoktur. |
| Sıfırlama | Demo veritabanı düzenli aralıklarla başlangıç hâline döner. |
| Cron | Demoda kurulu değil. Yerelde tek satırla kurulur. |
| Kimlik doğrulama | Vardır. Oturum, "beni hatırla", hız sınırı ve CSRF. |
APP_DEBUG | Canlıda kendiliğinden false — sunucu adından türetilir. |
| Bağımlılık | Sıfır. Composer yok, npm yok, CDN yok. |
Bu Proje Nedir?
Bir projede zamanlanmış işler çoğalmaya başlar: gece günlük temizliği, 15 dakikada bir sağlık kontrolü, pazar sabahı yedek. Her biri için sunucuya ayrı bir cron satırı yazılır. Sonra şu sorunlar sırayla gelir:
- Hangi görevler var? Cevap
crontab -eçıktısındadır; ekipteki çoğu kişi ona erişemez. - Dün gece çalıştı mı? Cevap yoktur. Cron çıktısı
/dev/null'a gidiyorsa hiçbir iz kalmaz. - Neden iki kez çalıştı? Önceki çalışma bitmeden yenisi başladı; ikisi aynı tabloyu yazdı.
- Bir görevi geçici olarak durdurmak için sunucuya SSH ile bağlanmak gerekir.
Bu proje sorunu tersine çevirir: sunucuya tek bir cron satırı yazılır ve o satır her dakika bin/scheduler.php dosyasını çağırır. Hangi görevin ne zaman çalışacağı artık veritabanındadır, panelden yönetilir; her çalışma süresi, çıktısı ve durumuyla birlikte kaydedilir.
Cron ifadelerini ayrıştıran, ne zaman çalışacağını hesaplayan ve insan diline çeviren CronExpression sınıfı da projenin içindedir — harici bir kütüphane kullanılmaz.
Kimler için uygun?
- Birden fazla zamanlanmış işi olan ve hepsini tek yerden görmek isteyenler
- "Gece raporu çalıştı mı?" sorusunu cevaplamak zorunda kalanlar
- Üst üste binen (overlap) görevlerden dolayı çift kayıt üretenler
- Cron ifadelerinin PHP'de nasıl ayrıştırıldığını öğrenmek isteyenler
- Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir panel kalıbı arayanlar
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
İçindekiler
- Canlı Demo
- Bu Proje Nedir?
- Ekran Görüntüleri
- Tek cron satırı
- Kritik Kararlar
- Neler Var?
- Cron sözdizimi
- Kendi görevinizi yazmak
- Güvenlik: Neyi, Nasıl Kapattık?
- Kurulum
- Yapılandırma
- Dosya Yapısı
- Nasıl Çalışıyor?
- Veritabanı Şeması
- SSS
- Canlı Ortama Alırken
- Sorun Giderme
- Yol Haritası
- Katkı
- Lisans
Ekran Görüntüleri
Zamanlanmış görevler
Cron ifadesi satırın içinde düzenlenir; altında ifadenin insan dilindeki karşılığı ("Her 15 dakikada bir") anında yazar. Sıradaki sütunu gerçek bir tarihtir — yanlış kurulmuş bir görev, aylar sonra değil o an belli olur. Son durum hücresi görevin toplam çalışma ve hata sayısını taşır; altta cron sözdizimi kartı alanları ve sık kullanılan ifadeleri gösterir.

Çalışma geçmişi
Her çalışma; durumu, milisaniye cinsinden süresi ve zamanıyla kaydedilir. "çıktıyı göster" satırı görevin bastığı her şeyi açar — ayrı bir günlük API'si yoktur, çıktı ob_start() ile yakalanır.

Kontrol paneli
Sunucuya yazılacak tek cron satırı burada, kopyalanmaya hazır durur. Sayaç şeridi zamanlayıcının nabzını verir: kaç görev etkin, son 24 saatte kaç çalışma oldu, kaçı hata verdi.

Giriş ekranı
Demo hesapları tek tıkla doldurulur. Giriş denemeleri hız sınırına tabidir; art arda başarısız denemeden sonra hesap geçici olarak kilitlenir.

Koyu tema
Tema tarayıcıda değil kullanıcı hesabında saklanır; başka bir cihazdan girdiğinizde de aynı gelir.

Mobil görünüm
390px genişlikte alt navigasyon devreye girer. Sayfa gövdesinde yatay kaydırma yoktur; geniş tablolar yalnızca kendi kapsayıcılarında kayar.
Tek cron satırı
Sunucuya yazılacak tek satır budur:
* * * * * cd /var/www/uygulama && php bin/scheduler.php >> /dev/null 2>&1Her dakika çalışır, veritabanındaki görevlere bakar ve zamanı gelenleri çalıştırır. Yeni bir görev eklediğinizde sunucuya dokunmanız gerekmez; panele bir satır eklemeniz yeter.
Windows'ta Görev Zamanlayıcı'dan her dakika tekrarlanan bir görev tanımlayın:
| Alan | Değer |
|---|---|
| Program | C:\xampp\php\php.exe |
| Bağımsız değişken | C:\xampp\htdocs\scheduler-system\bin\scheduler.php |
| Tekrarlama | Her 1 dakikada bir |
Elle denemek için:
php bin/scheduler.php --list # görevler ve sıradaki çalışma zamanları
php bin/scheduler.php # zamanı gelenleri çalıştır
php bin/scheduler.php --run=3 # 3 numaralı görevi zamanına bakmadan çalıştır
php bin/scheduler.php --quiet # çıktı verme (cron için)
php bin/scheduler.php --help
Kritik Kararlar
1. Üst üste binmeyi kilit engeller — ve kilidin zaman aşımı vardır
Cron her dakika çalışır. Görev 90 saniye sürerse ikinci çalışma, birincisi bitmeden başlar. İkisi aynı tabloyu yazarsa çift kayıt üretirsiniz.
Klasik ve yanlış çözüm:
if ($task['locked_at'] === null) { // ◄── yarış koşulu
$db->exec("UPDATE tasks SET locked_at = NOW() WHERE id = {$id}");
// ...
}İki süreç
if'i aynı anda geçebilir. Doğrusu, kontrolü ve yazmayı tek ifadede yapmaktır:
UPDATE scheduled_tasks
SET locked_at = NOW()
WHERE id = :id
AND (locked_at IS NULL OR locked_at < (NOW() - INTERVAL :timeout SECOND))if ($lock->rowCount() === 0) {
return false; // kilidi başkası aldı, bu çalışmayı atla
}Zaman aşımı olmazsa ne olur? Süreç çökerse
locked_at sonsuza kadar dolu kalır ve görev bir daha hiç çalışmaz — üstelik sessizce. Bu yüzden kilit her zaman "şu kadar saniyeden eskiyse geçersiz say" kuralıyla birlikte kurulur. Süre her görev için ayrı ayarlanabilir (lock_timeout); yedekleme görevine 3600, sağlık kontrolüne 300 saniye verilmiştir.
2. Cron ifadesi PHP'de ayrıştırılır, kütüphane kullanılmaz
CronExpression sınıfı beş alanı ayrıştırır ve üç soruya cevap verir:
| Metot | Cevabı |
|---|---|
isValid() | Bu ifade geçerli mi? (kullanıcı girdisini süzmek için) |
isDue() | Şu an çalışmalı mı? |
nextRun() | Bir sonraki çalışma ne zaman? |
describe() | İnsan diliyle ne demek? |
nextRun() sayesinde panel "Sıradaki: 04.09.2026 03:00" yazabilir. Bu, cron'un normalde vermediği bir bilgidir ve bir görevin yanlış kurulduğunu anında gösterir: beklediğiniz tarih değilse ifade hatalıdır.
describe() ise /15 * ifadesini "Her 15 dakikada bir" diye yazar. Cron sözdizimi, günde bir kez bakan biri için hiçbir zaman sezgisel olmaz.
Ayrıştırma ayrıntısı: alanlar explode(' ') ile değil preg_split('/\s+/') ile ayrılır. Kullanıcı 0 3 * gibi çift boşluk yazarsa explode boş parçalar üretir ve ifade geçersiz sayılırdı.
3. Görev tanımı veritabanında, görev kodu dosyada
scheduled_tasks.task_class → App\Tasks\PruneLogsTask
scheduled_tasks.payload → {"days":90}Ne zaman çalışacağı ve hangi parametreyle çalışacağı veritabanındadır — bunlar sık değişir ve panelden yönetilmelidir. Ne yapacağı ise koddadır; sürüm kontrolünde durur, kod incelemesinden geçer.
Worker sınıfı yüklerken Task sınıfından türediğini doğrular; task_class alanına elle bir değer yazılsa bile rastgele bir sınıf çalıştırılamaz.
4. Başarısız görev durdurulmaz
Bir görev hata verdiğinde fail_count artar, last_status failed olur ve çalışma geçmişine hata mesajıyla bir satır düşer. Ama görev pasife alınmaz.
Sebep: geçici bir hata (disk doldu, veritabanı bir saniye yanıt vermedi) yüzünden görevi kalıcı olarak durdurmak, bir sonraki gerçek çalışmayı da engellerdi. Bir kez patlayan görev, bir daha hiç çalışmayan görevden iyidir. Sayaçlar zaten sorunu görünür kılar.
5. Her çalışma kaydedilir — süresi ve çıktısıyla
task_runs tablosu her çalışma için bir satır tutar: durum, milisaniye cinsinden süre ve çıktı.
Süre en çok işe yarayan alandır: normalde 40 ms süren sağlık kontrolü bir gün 5 saniye sürüyorsa, henüz hata vermeden önce bir şeyin bozulduğunu anlarsınız.
6. "Şimdi çalıştır" düğmesi zamanı yok sayar, kilidi yok saymaz
Panelden elle tetiklenen görev, isDue() kontrolünden geçmez — zaten amaç zamanına bakmadan çalıştırmaktır. Ama kilidi alır: cron o sırada aynı görevi çalıştırıyorsa düğme onu ikinci kez başlatmaz.
Neler Var?
Zamanlayıcı çekirdeği
CronExpression— ayrıştırma, doğrulama, sonraki çalışma, açıklama,/n,a-b,a,b,c,a-b/ndesteği- Atomik kilit + görev başına zaman aşımı
- Çalışma geçmişi: durum, süre (ms), çıktı
- Çalışma ve hata sayaçları
- Görev başına etkin/pasif anahtarı
CLI
--listgörevler ve sıradaki zamanlar--run=tek görevi şimdi çalıştır--quietsessiz mod (cron için)PHP_SAPIkontrolü — tarayıcıdan çalışmaz
Panel
- Görev listesi, satır içinde ifade düzenleme
- "Sıradaki" sütunu — hesaplanmış gerçek tarih
- İnsan dilinde açıklama (
Her 15 dakikada bir) - Şimdi çalıştır / Durdur / Başlat
- Çalışma geçmişi, başarılı ve hatalı ayrımı
- Cron sözdizimi başvuru kartı
Ortak altyapı
- Oturum girişi, "beni hatırla", hız sınırı, CSRF
- CSP (
script-src 'self'),X-Frame-Options: DENY - Açık / koyu tema, hesaba kayıtlı
- Mobilde alt navigasyon, yatay kaydırma yok
Cron sözdizimi
┌───────────── dakika (0-59)
│ ┌─────────── saat (0-23)
│ │ ┌───────── ayın günü (1-31)
│ │ │ ┌─────── ay (1-12)
│ │ │ │ ┌───── haftanın günü (0-6, 0 = Pazar)
│ │ │ │ │
* * * * *
| İfade | Anlamı |
|---|---|
| Her dakika |
/15 | Her 15 dakikada bir |
0 | Her saat başı |
0 3 | Her gün saat 03:00 |
30 2 0 | Her Pazar 02:30 |
0 9 1 | Her Pazartesi 09:00 |
0 0 1 | Her ayın 1'i gece yarısı |
30 9-17/2 * 1-5 | Hafta içi 09:30, 11:30, 13:30, 15:30, 17:30 |
Dört operatör desteklenir:
| Operatör | Örnek | Anlamı |
|---|---|---|
| | Her değer |
, | 1,15 | Listedeki değerler |
- | 9-17 | Aralık |
/ | */15, 9-17/2 | Adım (aralıkla birlikte de kullanılabilir) |
Kendi görevinizi yazmak
Bir görev, Task sınıfından türeyen ve üç metot uygulayan bir sınıftır.
<?php
namespace App\Tasks;
use PDO;
final class ExpireCouponsTask extends Task
{
public static function label(): string
{
return 'Süresi dolan kuponlar';
}
public static function description(): string
{
return 'Geçerlilik süresi dolmuş kuponları pasife alır.';
}
/**
* Görevin kendisi.
*
* EKRANA YAZDIĞINIZ HER ŞEY geçmişe kaydedilir. Scheduler,
* handle() çağrısını ob_start() / ob_get_clean() arasına alır ve
* yakaladığı metni task_runs.output sütununa yazar. Yani ayrı bir
* "günlüğe yaz" API'si öğrenmenize gerek yok: echo yeter.
*
* Ne olduğunu SÖYLEYEN bir cümle yazın. "Tamamlandı" yerine
* "14 kupon pasife alındı" yazmak, altı ay sonra size çok şey anlatır.
*
* İstisna fırlatırsa çalışma BAŞARISIZ sayılır: hata mesajı çıktının
* sonuna "HATA: ..." olarak eklenir ve fail_count artar.
*/
public function handle(array $payload, PDO $db): void
{
$gun = (int) ($payload['days'] ?? 0);
$stmt = $db->prepare(
'UPDATE kuponlar SET aktif = 0
WHERE aktif = 1 AND bitis_tarihi < (NOW() - INTERVAL :gun DAY)'
);
$stmt->execute([':gun' => $gun]);
printf('%d kupon pasife alındı.' . PHP_EOL, $stmt->rowCount());
}
}Sonra panelden ya da doğrudan veritabanından kaydını ekleyin:
INSERT INTO scheduled_tasks
(name, description, task_class, payload, expression, is_active, lock_timeout, created_at)
VALUES
('Süresi dolan kuponlar',
'Geçerlilik süresi dolmuş kuponları pasife alır.',
'App\\Tasks\\ExpireCouponsTask',
'{"days":0}',
'0 4 * * *',
1, 300, NOW());Üç kural:
- Görev kısa sürsün. Uzun süren işi burada değil, iş kuyruğunda çalıştırın; zamanlanmış görev yalnızca kuyruğa iş bıraksın. Cron zamanı belirler, kuyruk dayanıklılığı sağlar.
lock_timeout'u işin süresinden büyük seçin. Aksi hâlde hâlâ çalışan bir görev "takılmış" sayılıp ikinci kez başlatılır.- Çıktıyı anlamlı yazın.
echo/printfile yazdığınız her şey geçmişe düşer. Altı ay sonra geçmişe baktığınızda "Tamamlandı" hiçbir şey anlatmaz; "2.318 kayıt silindi" anlatır.
Güvenlik: Neyi, Nasıl Kapattık?
| Açık | Tipik hatalı kod | Bu projede |
|---|---|---|
| Rastgele sınıf çalıştırma | (new $row['class'])->run() | Sınıf Task'tan türemiyorsa çalıştırılmaz |
| Yarış koşulu (üst üste binme) | "Önce bak, sonra kilitle" | Kontrol ve yazma tek UPDATE ifadesinde; rowCount() ile doğrulanır |
| Sonsuza kadar kilitli görev | Zaman aşımsız kilit | Kilit lock_timeout saniyeden eskiyse geçersiz sayılır |
| Geçersiz cron ifadesi | Doğrulamadan kaydetmek | CronExpression::isValid() — geçersiz ifade veritabanına yazılmaz |
| SQL enjeksiyonu | "... WHERE id = $id" | Tüm sorgular hazır ifade; ATTR_EMULATE_PREPARES = false |
| CLI betiğine web'den erişim | bin/ klasörü açık | bin/.htaccess ile kapalı ve betikte PHP_SAPI === 'cli' kontrolü |
| CSRF | Gizli alan yok | Her POST'ta jeton; hash_equals() |
| XSS | echo $run['output'] | Sunucuda e(); ayrıca CSP script-src 'self' |
| Parola sızıntısı | md5($parola) | password_hash() / password_verify() |
| Kaba kuvvet | Sınırsız deneme | login_attempts ile e-posta + IP başına hız sınırı |
| Hata sızıntısı | Canlıda display_errors=On | APP_DEBUG ortamdan türetilir |
| Sınırsız büyüyen geçmiş | Temizlenmeyen log tablosu | task_runs için indeksli tarih sütunu; örnek temizlik görevi projede hazır |
Kurulum
Gereksinimler
| PHP | 8.0 veya üzeri (CLI erişimi gerekir) |
| MySQL / MariaDB | 5.7+ / 10.3+ |
| Web sunucusu | Apache (mod_rewrite) veya Nginx |
| PHP eklentileri | pdo_mysql, mbstring |
Adımlar
git clone https://github.com/CilginYazilim/scheduler-system.git
cd scheduler-system
mysql -u root -p < database.sql
cp .env.example .env # Windows: copy .env.example .envAçın:
http://localhost/scheduler-system/ · Giriş: [email protected] / Admin1234
Sonra tek cron satırını kurun → Tek cron satırı
Yapılandırma
APP_DEBUG=true # silerseniz: yerelde açık, canlıda kapalı
APP_URL=
APP_PRETTY_URLS=true
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=cy_scheduler
DB_USER=root
DB_PASS=
••••••••
> **Zaman dilimi önemlidir.** `config/config.php` içindeki `app.timezone` (`Europe/Istanbul`) ile MySQL sunucusunun zaman dilimi aynı olmalıdır. Farklıysa "gece 03:00" dediğiniz görev başka bir saatte çalışır. Kontrol için: `SELECT NOW();` çıktısı ile `date('Y-m-d H:i:s')` çıktısını karşılaştırın.
---
## Dosya Yapısı
scheduler-system/
│
├── index.php Ön denetleyici — TEK giriş noktası
├── database.sql Şema + 5 görev + 21 çalışma kaydı
├── .env.example
│
├── bin/
│ ├── scheduler.php ★ Cron'un çağırdığı tek dosya
│ └── .htaccess Web erişimine kapalı
│
├── app/
│ ├── Core/
│ │ ├── CronExpression.php ★ Ayrıştırma · isDue · nextRun · describe
│ │ ├── Scheduler.php ★ Kilit alma, çalıştırma, geçmişe yazma
│ │ ├── Auth.php · Session.php · Csrf.php · RateLimiter.php
│ │ ├── Database.php PDO (EMULATE_PREPARES = false)
│ │ ├── Env.php .env okuyucu + isLocalHost()
│ │ └── ...
│ │
│ ├── Tasks/
│ │ ├── Task.php ★ Soyut taban: handle() · label() · description()
│ │ ├── PruneLogsTask.php Örnek: eski günlük kayıtlarını sil
│ │ ├── HealthCheckTask.php Örnek: veritabanı/disk/izin denetimi
│ │ └── BackupTask.php Örnek: CSV yedeği al, en yeni 5'i sakla
│ │
│ ├── Http/Controllers/ Auth · Dashboard · Schedule · User
│ ├── Repositories/ ScheduleRepository · UserRepository · ActivityRepository
│ └── Support/helpers.php
│
├── storage/ Yedek ve rapor çıktıları (web'e kapalı)
├── views/ Düzenler, görev ve geçmiş sayfaları
├── assets/ css · js · images
├── config/config.php
├── routes/web.php
└── docs/screenshots/
---
## Nasıl Çalışıyor?
crontab: * php bin/scheduler.php
│
▼ (her dakika)
Scheduler::runDue()
│
▼
SELECT * FROM scheduled_tasks WHERE is_active = 1
│
▼ her görev için
┌───────────────────────────────────┐
│ CronExpression($task['expression'])│
│ ->isDue(now) │
└───────────────────────────────────┘
│ │
hayır │ │ evet
▼ ▼
ATLA ┌──────────────────────────────────┐
│ ATOMİK KİLİT │
│ UPDATE scheduled_tasks │
│ SET locked_at = NOW() │
│ WHERE id = ? │
│ AND (locked_at IS NULL │
│ OR locked_at < NOW() │
│ - INTERVAL timeout SEC) │
└──────────────────────────────────┘
│
rowCount() === 0 ?
│ │
evet │ │ hayır (kilit bizde)
▼ ▼
ATLA (başkası Task::handle($payload, $db)
çalıştırıyor) │
├─ başarılı → last_status='success'
│ run_count++
│
└─ istisna → last_status='failed'
fail_count++
│
▼
task_runs tablosuna satır
(durum · süre(ms) · çıktı)
│
▼
locked_at = NULL ◄── kilit bırakılır
```
Veritabanı Şeması
scheduled_tasks — görev tanımları
| Sütun | Tip | İşi |
|---|---|---|
id | INT UNSIGNED | Birincil anahtar |
name · description | VARCHAR | Panelde görünen ad ve açıklama |
task_class | VARCHAR(190) | Çalıştırılacak sınıf (App\Tasks\PruneLogsTask) |
payload | TEXT | Görevin parametreleri (JSON) |
expression | VARCHAR(100) | Cron ifadesi (0 3 *) |
is_active | TINYINT(1) | Pasifse zamanı gelse bile çalışmaz |
locked_at | DATETIME | Çalışıyorsa dolu; NULL = boşta |
lock_timeout | INT UNSIGNED | Kilit kaç saniye sonra geçersiz sayılsın |
last_run_at · last_status | DATETIME · ENUM | Son çalışma anı ve sonucu |
run_count · fail_count | INT UNSIGNED | Toplam çalışma ve hata sayısı |
task_runs — çalışma geçmişi
| Sütun | Tip | İşi |
|---|---|---|
id | BIGINT UNSIGNED | Birincil anahtar |
task_id | INT UNSIGNED | Hangi göreve ait (ON DELETE CASCADE) |
status | ENUM('success','failed') | Sonuç |
duration_ms | INT UNSIGNED | Süre — erken uyarı sinyali |
output | TEXT | Görevin ekrana yazdığı metin (çıktı tamponundan yakalanır) ve varsa hata mesajı |
ran_at | DATETIME | Çalışma anı (indeksli) |
| Karar | Neden |
|---|---|
| Geçmiş ayrı tabloda | scheduled_tasks her dakika okunur; geçmiş satırlarıyla şişmemeli |
| Süre milisaniye olarak | 40 ms → 5.000 ms değişimi, hata oluşmadan önce bozulmayı gösterir |
locked_at görev satırında, ayrı tabloda değil | Kilidi almak ve görevi güncellemek tek UPDATE olsun diye; ayrı tablo ikinci bir yarış koşulu açardı |
last_status özet olarak tutuluyor | Liste sayfası her satır için geçmiş tablosuna sorgu atmasın (N+1) |
Sayaçlar (run_count) satırda | Aynı sebep: COUNT(*) yerine hazır sayı |
SSS
Sunucumda cron yok, ne yapabilirim?
Üç seçenek:
- cPanel / Plesk: panelde "Cron Jobs" bölümü vardır; komutu oraya yazın.
- Windows: Görev Zamanlayıcı'dan her dakika tekrarlanan bir görev tanımlayın.
- Harici tetikleyici:
bin/scheduler.phpyerine korumalı bir web adresi açıp cron-job.org gibi bir servisle her dakika çağırın. Bu durumda adrese mutlaka bir gizli anahtar koyun; herkese açık bir adres, görevlerinizi isteyen herkesin tetikleyebilmesi demektir.
Görev her dakika çalışıyor, oysa 0 3 * yazdım
Neredeyse her zaman zaman dilimi farkıdır. PHP Europe/Istanbul, MySQL UTC ise NOW() üç saat geride olur ve hesaplar tutmaz.
SELECT NOW(); çıktısı ile php -r "echo date('Y-m-d H:i:s');" çıktısını karşılaştırın. Farklıysa MySQL'in time_zone ayarını düzeltin veya config/config.php içindeki app.timezone değerini sunucuya uydurun.
Görev iki kez çalıştı, kilit çalışmıyor mu?
Kilit çalışıyordur; muhtemelen lock_timeout görevin süresinden kısadır.
Görev 400 saniye sürüyor ama lock_timeout 300 ise, 300. saniyede kilit "eski" sayılır ve ikinci çalışma başlar. Süreyi çalışma geçmişindeki duration_ms değerine bakarak ayarlayın; en uzun çalışmanın en az iki katı iyi bir başlangıçtır.
Uzun süren bir işi buradan çalıştırabilir miyim?
Çalıştırabilirsiniz ama önerilmez. Zamanlanmış görev zamanı belirler; dayanıklılık (yeniden deneme, geri çekilme, başarısız iş kutusu) sağlamaz. Cron'un içinde patlayan iş kaybolur.
Doğru kalıp: zamanlanmış görev kuyruğa bir iş bırakır, işi worker çalıştırır. Kuyruk örneği için İş Kuyruğu ve Worker Sistemi'ne bakın.
Çalışma geçmişi çok büyüdü
Beklenen bir durumdur: 15 dakikada bir çalışan bir görev yılda ~35.000 satır üretir.
Projedeki PruneLogsTask tam da bunun içindir; task_runs için de benzer bir temizlik görevi ekleyin ve gece çalıştırın. ran_at sütunu indekslidir, silme sorgusu hızlı çalışır.
Saniyede bir çalışan görev tanımlayabilir miyim?
Hayır — cron'un en küçük birimi dakikadır ve bu zamanlayıcı da dakika çözünürlüğünde çalışır.
Daha sık çalışması gereken bir iş varsa o bir "zamanlanmış görev" değil, sürekli çalışan bir süreçtir. Kuyruk worker'ı bu iş için doğru araçtır.
Canlı Ortama Alırken
- [ ]
.enviçindeAPP_DEBUG=false(veya satırı tümüyle silin) - [ ] Tek cron satırını kurun ve
--quietekleyin - [ ] PHP ile MySQL'in zaman dilimi aynı mı, doğrulayın
- [ ] Her görevin
lock_timeoutdeğerini en uzun çalışmasının üstünde seçin - [ ]
bin/vestorage/klasörlerinin web'den erişilemediğini doğrulayın (403) - [ ]
task_runsiçin bir temizlik görevi ekleyin - [ ] Veritabanı için root olmayan bir kullanıcı açın
- [ ] HTTPS zorunlu olsun
- [ ] Demo hesaplarının parolalarını değiştirin veya hesapları silin
Sorun Giderme
| Belirti | Sebep | Çözüm |
|---|---|---|
| Hiçbir görev çalışmıyor | Cron kurulu değil | crontab -l ile kontrol edin |
| Görev yanlış saatte çalışıyor | PHP ile MySQL zaman dilimi farklı | İkisini eşitleyin |
| Görev hiç çalışmıyor, "çalışıyor" görünüyor | Süreç çökmüş, kilit üzerinde kalmış | lock_timeout süresi dolunca kendiliğinden çözülür; acilse locked_at = NULL yapın |
| Görev iki kez çalışıyor | lock_timeout çalışma süresinden kısa | Değeri yükseltin |
| "Sıradaki" boş görünüyor | Görev pasif | Başlat düğmesine basın |
| İfade kaydedilmiyor | Geçersiz cron sözdizimi | Beş alan var mı, operatörler doğru mu bakın |
bin/scheduler.php tarayıcıda 403 | Bilerek | CLI betiği web'e kapalıdır |
| Tüm adresler 404 | mod_rewrite kapalı | Açın veya APP_PRETTY_URLS=false yapın |
Yol Haritası
- [ ] Görev başarısız olunca e-posta / webhook bildirimi
- [ ] Panelden yeni görev ekleme formu (şu an SQL ile ekleniyor)
- [ ] Çalışma süresi grafiği (son 24 saat)
- [ ]
@daily,@hourlygibi kısayol ifadeleri - [ ] Görev bağımlılığı (A bitmeden B çalışmasın)
Katkı
Hata bildirimi ve öneriler için issue açabilirsiniz.
Lisans
MIT — ticari projelerinizde de özgürce kullanabilirsiniz.
Kaynak Kod soldaki ağaçtan bir dosya seçin
-
app
-
Core
- Auth.php 13.9 KB
- Autoloader.php 2.2 KB
- Config.php 1.9 KB
- CronExpression.php 12.7 KB
- Csrf.php 2.8 KB
- Database.php 3.5 KB
- Env.php 4.3 KB
- Flash.php 2.6 KB
- Middleware.php 2.5 KB
- Paginator.php 8.6 KB
- RateLimiter.php 3.9 KB
- Request.php 4.5 KB
- Response.php 7 KB
- Router.php 6.3 KB
- Scheduler.php 8.9 KB
- Session.php 6.3 KB
- Validator.php 8.8 KB
- View.php 3.5 KB
-
Http
-
Controllers
-
Api
- PreferenceApiController.php 2.5 KB
- AuthController.php 3.2 KB
- DashboardController.php 1.2 KB
- ScheduleController.php 6.8 KB
- UserController.php 3.1 KB
-
- Controller.php 1.3 KB
-
-
Models
- User.php 5.9 KB
-
Repositories
- ActivityRepository.php 3.8 KB
- RememberTokenRepository.php 5.8 KB
- ScheduleRepository.php 5.5 KB
- UserRepository.php 6.9 KB
-
Support
- helpers.php 12.3 KB
-
Tasks
- BackupTask.php 3.5 KB
- HealthCheckTask.php 2.5 KB
- PruneLogsTask.php 1.7 KB
- Task.php 1.9 KB
- .htaccess 851 B
-
-
assets
-
css
- admin.css 58.6 KB
- bootstrap.min.css 227.5 KB
- cilginyazilim.css 29.8 KB
- feature.css 6.3 KB
-
images
- logo.png 70.4 KB
-
js
- app.js 20.2 KB
- bootstrap.bundle.js 203.2 KB
- jquery-3.7.0.js 278.3 KB
- login.js 1.8 KB
- schedule.js 2.9 KB
- users.js 4.7 KB
-
-
bin
- .htaccess 666 B
- scheduler.php 6 KB
-
config
- .htaccess 196 B
- config.php 5 KB
- menu.php 1.2 KB
-
docs
-
screenshots
- 01-giris.png 373.9 KB
- 02-kontrol-paneli.png 76.4 KB
- 03-gorevler.png 163.6 KB
- 04-calisma-gecmisi.png 126.5 KB
- 05-koyu-tema.png 161.8 KB
- 06-mobil.png 83.2 KB
-
-
routes
- .htaccess 197 B
- web.php 3.3 KB
-
storage
- .gitkeep 1 B
- .htaccess 652 B
-
views
-
auth
- login.php 7.4 KB
-
dashboard
- _feature.php 2.4 KB
- index.php 4 KB
-
errors
- 404.php 844 B
- 500.php 565 B
-
layouts
- admin.php 6.8 KB
- auth.php 2.9 KB
- plain.php 1.3 KB
-
partials
- bottomnav.php 2.1 KB
- head_extra.php 414 B
- pagination.php 4.9 KB
- sidebar.php 2.7 KB
- topbar.php 3.5 KB
-
schedule
- history.php 5.6 KB
- index.php 12 KB
-
users
- index.php 6.8 KB
- .htaccess 262 B
-
- .gitattributes 2.3 KB
- .gitignore 1.4 KB
- .htaccess 6 KB
- CHANGELOG.md 5.6 KB
- database.sql 24.1 KB
- index.php 5.4 KB
- LICENSE 1.1 KB
- README.en.md 30.8 KB
- README.md 32.9 KB
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli
değerler gösterilmeden önce maskelenir (••••••••).
Sık Sorulan Sorular
Üç seçenek var. cPanel veya Plesk kullanıyorsanız panelde "Cron Jobs" bölümü vardır. Windows'ta Görev Zamanlayıcı'dan her dakika tekrarlanan bir görev tanımlayın. Üçüncüsü harici bir tetikleyicidir: korumalı bir web adresi açıp cron-job.org gibi bir servisle çağırın, ama adrese mutlaka bir gizli anahtar koyun.
Neredeyse her zaman zaman dilimi farkıdır. PHP Europe/Istanbul, MySQL UTC ise NOW() üç saat geride olur ve hesaplar tutmaz. SELECT NOW() çıktısı ile PHP'nin date() çıktısını karşılaştırın; farklıysa MySQL'in time_zone ayarını düzeltin veya config içindeki app.timezone değerini sunucuya uydurun.
Kilit çalışıyordur; muhtemelen lock_timeout görevin süresinden kısadır. Görev 400 saniye sürüyor ama zaman aşımı 300 ise, 300. saniyede kilit eski sayılır ve ikinci çalışma başlar. Süreyi çalışma geçmişindeki duration_ms değerine bakarak ayarlayın; en uzun çalışmanın en az iki katı iyi bir başlangıçtır.
Çalıştırabilirsiniz ama önerilmez. Zamanlanmış görev zamanı belirler; dayanıklılık sağlamaz. Cron'un içinde patlayan iş kaybolur, tekrar denenmez. Doğru kalıp: zamanlanmış görev kuyruğa bir iş bırakır, işi worker çalıştırır. Cron zamanı belirler, kuyruk dayanıklılığı sağlar.
Beklenen bir durumdur: 15 dakikada bir çalışan bir görev yılda yaklaşık 35 bin satır üretir. Projedeki PruneLogsTask tam da bunun içindir; task_runs için de benzer bir temizlik görevi ekleyip gece çalıştırın. ran_at sütunu indekslidir, silme sorgusu hızlı çalışır.
Hayır. Cron'un en küçük birimi dakikadır ve bu zamanlayıcı da dakika çözünürlüğünde çalışır. Daha sık çalışması gereken bir iş varsa o bir zamanlanmış görev değil, sürekli çalışan bir süreçtir; kuyruk worker'ı bu iş için doğru araçtır.
Yorumlar 0 konuşma
Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.
Soru Sor veya Yorum Yaz
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.