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.

PHP 8 PDO MySQL Bootstrap 5 Ajax Oturum Girişi
Seviye
Orta
Dosya
87
Kod satırı
~19.975
Proje boyutu
2.2 MB
Veritabanı
cy_scheduler
Lisans
MIT
İnceleme
27
Beğeni
0
Yayın

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?

  1. Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
  2. database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
  3. .env.example dosyasını .env adıyla kopyalayıp veritabanı bilgilerinizi yazın
  4. Sunucuya tek cron satırını ekleyin: * * * * * php bin/scheduler.php
  5. Tarayıcıdan açıp [email protected] / Admin1234 ile giriş yapın

Veritabanı şeması projedeki database.sql dosyasında.

Nasıl Çalışıyor?

Çılgın Yazılım

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.

PHP
MySQL
Bootstrap
Composer
License

🇹🇷 Türkçe · 🇬🇧 English

▶ Canlı Demo · Kaynak Kütüphanesi · cilginyazilim.com


Canlı Demo

Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.

Canlı Demoyu Aç
Kaynak Kodu İncele
ZIP İndir

Zamanlanmış görevler canlı demo önizlemesi

▲ Görsele tıklayarak demoyu açabilirsiniz

Demo hesapları

RolE-postaParola
Yönetici[email protected]Admin1234
Kullanıcı[email protected]Demo1234

Demoda 60 saniyede neleri deneyebilirsiniz?

#Şunu deneyinPerde arkasında ne oluyor?
1Zamanlanmış Görevler sayfasını açın, "Sıradaki" sütununa bakınHer 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ınAltı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 deneyinReddedilir. 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" diyorGörev pasiftir. Zamanı gelse bile çalıştırılmaz ve üstteki "etkin görev" sayacına dahil edilmez
5Bir görevin Durdur / Başlat anahtarına basınis_active değişir; sayaç anında güncellenir
6"Şimdi çalıştır" düğmesine basınGö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çınSon 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: HataBir 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
9Sayfanın altındaki Cron Sözdizimi kartına bakınBeş alanın ne olduğu ve /15, 30 9-17/2 * 1-5 gibi ifadelerin ne anlama geldiği örneklerle yazılıdır
10Telefonunuzdan açınGö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

KonuDurum
Verilerdatabase.sql içindeki 51 kullanıcı + 5 görev + 21 çalışma kaydı. Gerçek kişi verisi yoktur.
SıfırlamaDemo veritabanı düzenli aralıklarla başlangıç hâline döner.
CronDemoda kurulu değil. Yerelde tek satırla kurulur.
Kimlik doğrulamaVardır. Oturum, "beni hatırla", hız sınırı ve CSRF.
APP_DEBUGCanlıda kendiliğinden false — sunucu adından türetilir.
BağımlılıkSı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


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.

Zamanlanmış görevler: yerinde düzenlenen cron ifadesi, sıradaki çalışma ve son durum

Ç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.

Çalışma geçmişi: başarılı ve hatalı çalışmalar, süreleri ve açılabilir çıktıları

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.

Kontrol paneli: sayaç şeridi ve sunucuya yazılacak tek cron satırı

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.

Giriş ekranı: demo hesapları tek tıkla doldurulur

Koyu tema

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

Koyu tema görünümü

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.

390px genişlikte mobil görünüm

Tek cron satırı

Sunucuya yazılacak tek satır budur:

* * * * * cd /var/www/uygulama && php bin/scheduler.php >> /dev/null 2>&1

Her 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:

AlanDeğer
ProgramC:\xampp\php\php.exe
Bağımsız değişkenC:\xampp\htdocs\scheduler-system\bin\scheduler.php
TekrarlamaHer 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:

MetotCevabı
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/n desteğ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

  • --list görevler ve sıradaki zamanlar
  • --run= tek görevi şimdi çalıştır
  • --quiet sessiz mod (cron için)
  • PHP_SAPI kontrolü — 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)
│ │ │ │ │
* * * * *
İfadeAnlamı
Her dakika
/15 Her 15 dakikada bir
0 Her saat başı
0 3 Her gün saat 03:00
30 2 0Her Pazar 02:30
0 9 1Her Pazartesi 09:00
0 0 1 Her ayın 1'i gece yarısı
30 9-17/2 * 1-5Hafta içi 09:30, 11:30, 13:30, 15:30, 17:30

Dört operatör desteklenir:

OperatörÖrnekAnlamı
Her değer
,1,15Listedeki değerler
-9-17Aralık
/*/15, 9-17/2Adı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:

  1. 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.
  2. 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.
  3. Çıktıyı anlamlı yazın. echo/printf ile 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çıkTipik hatalı kodBu 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örevZaman aşımsız kilitKilit lock_timeout saniyeden eskiyse geçersiz sayılır
Geçersiz cron ifadesiDoğrulamadan kaydetmekCronExpression::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şimbin/ klasörü açıkbin/.htaccess ile kapalı ve betikte PHP_SAPI === 'cli' kontrolü
CSRFGizli alan yokHer POST'ta jeton; hash_equals()
XSSecho $run['output']Sunucuda e(); ayrıca CSP script-src 'self'
Parola sızıntısımd5($parola)password_hash() / password_verify()
Kaba kuvvetSınırsız denemelogin_attempts ile e-posta + IP başına hız sınırı
Hata sızıntısıCanlıda display_errors=OnAPP_DEBUG ortamdan türetilir
Sınırsız büyüyen geçmişTemizlenmeyen log tablosutask_runs için indeksli tarih sütunu; örnek temizlik görevi projede hazır

Kurulum

Gereksinimler

PHP8.0 veya üzeri (CLI erişimi gerekir)
MySQL / MariaDB5.7+ / 10.3+
Web sunucusuApache (mod_rewrite) veya Nginx
PHP eklentileripdo_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 .env

Açı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ütunTipİşi
idINT UNSIGNEDBirincil anahtar
name · descriptionVARCHARPanelde görünen ad ve açıklama
task_classVARCHAR(190)Çalıştırılacak sınıf (App\Tasks\PruneLogsTask)
payloadTEXTGörevin parametreleri (JSON)
expressionVARCHAR(100)Cron ifadesi (0 3 *)
is_activeTINYINT(1)Pasifse zamanı gelse bile çalışmaz
locked_atDATETIMEÇalışıyorsa dolu; NULL = boşta
lock_timeoutINT UNSIGNEDKilit kaç saniye sonra geçersiz sayılsın
last_run_at · last_statusDATETIME · ENUMSon çalışma anı ve sonucu
run_count · fail_countINT UNSIGNEDToplam çalışma ve hata sayısı

task_runs — çalışma geçmişi

SütunTipİşi
idBIGINT UNSIGNEDBirincil anahtar
task_idINT UNSIGNEDHangi göreve ait (ON DELETE CASCADE)
statusENUM('success','failed')Sonuç
duration_msINT UNSIGNEDSüre — erken uyarı sinyali
outputTEXTGörevin ekrana yazdığı metin (çıktı tamponundan yakalanır) ve varsa hata mesajı
ran_atDATETIMEÇalışma anı (indeksli)
KararNeden
Geçmiş ayrı tablodascheduled_tasks her dakika okunur; geçmiş satırlarıyla şişmemeli
Süre milisaniye olarak40 ms → 5.000 ms değişimi, hata oluşmadan önce bozulmayı gösterir
locked_at görev satırında, ayrı tabloda değilKilidi almak ve görevi güncellemek tek UPDATE olsun diye; ayrı tablo ikinci bir yarış koşulu açardı
last_status özet olarak tutuluyorListe sayfası her satır için geçmiş tablosuna sorgu atmasın (N+1)
Sayaçlar (run_count) satırdaAynı sebep: COUNT(*) yerine hazır sayı

SSS

Sunucumda cron yok, ne yapabilirim?

Üç seçenek:

  1. cPanel / Plesk: panelde "Cron Jobs" bölümü vardır; komutu oraya yazın.
  2. Windows: Görev Zamanlayıcı'dan her dakika tekrarlanan bir görev tanımlayın.
  3. Harici tetikleyici: bin/scheduler.php yerine 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

  • [ ] .env içinde APP_DEBUG=false (veya satırı tümüyle silin)
  • [ ] Tek cron satırını kurun ve --quiet ekleyin
  • [ ] PHP ile MySQL'in zaman dilimi aynı mı, doğrulayın
  • [ ] Her görevin lock_timeout değerini en uzun çalışmasının üstünde seçin
  • [ ] bin/ ve storage/ klasörlerinin web'den erişilemediğini doğrulayın (403)
  • [ ] task_runs iç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

BelirtiSebepÇözüm
Hiçbir görev çalışmıyorCron kurulu değilcrontab -l ile kontrol edin
Görev yanlış saatte çalışıyorPHP ile MySQL zaman dilimi farklıİkisini eşitleyin
Görev hiç çalışmıyor, "çalışıyor" görünüyorSü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ışıyorlock_timeout çalışma süresinden kısaDeğeri yükseltin
"Sıradaki" boş görünüyorGörev pasifBaşlat düğmesine basın
İfade kaydedilmiyorGeçersiz cron sözdizimiBeş alan var mı, operatörler doğru mu bakın
bin/scheduler.php tarayıcıda 403BilerekCLI betiği web'e kapalıdır
Tüm adresler 404mod_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, @hourly gibi 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
Dosya seçilmedi
İncelemek istediğiniz dosyayı soldaki ağaçtan seçin.

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.

Yayınlanmaz; yalnızca yanıt bildirimi için kullanılır.
En az 10 karakter.

İlgili Kod Örnekleri