İleri
uygulama-iskeleti/php-baslangic-sablonu
PHP Başlangıç Şablonu (CY PHP Starter)
- PHP 8.1
- PDO
- MySQL
- MariaDB
- +4
309 inceleme 150 canlı açılış 6 beğeni 2 yorum
Uygulama İskeleti Orta
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.
Veritabanı şeması projedeki database.sql dosyasında.
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:
crontab -e çıktısındadır; ekipteki çoğu kişi ona erişemez./dev/null'a gidiyorsa hiçbir iz kalmaz.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?
Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.
Sunucuya yazılacak tek satır budur:
* * * * * cd /var/www/uygulama && php bin/scheduler.php >> /dev/null 2>&1Windows'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
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}");
// ...
}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
}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.
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ı.
scheduled_tasks.task_class → App\Tasks\PruneLogsTask
scheduled_tasks.payload → {"days":90}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.
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.
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.
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.
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ğiCLI
--list görevler ve sıradaki zamanlar--run=<id> tek görevi şimdi çalıştır--quiet sessiz mod (cron için)PHP_SAPI kontrolü — tarayıcıdan çalışmazPanel
Her 15 dakikada bir)Ortak altyapı
script-src 'self'), X-Frame-Options: DENY┌───────────── 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) |
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());
}
}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());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.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.| 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 |
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ı
---
## Nasıl Çalışıyor?
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ı |
Sunucumda cron yok, ne yapabilirim?
Üç seçenek:
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.
.env içinde APP_DEBUG=false (veya satırı tümüyle silin)--quiet ekleyinlock_timeout değerini en uzun çalışmasının üstünde seçinbin/ ve storage/ klasörlerinin web'den erişilemediğini doğrulayın (403)task_runs için bir temizlik görevi ekleyin| 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 |
@daily, @hourly gibi kısayol ifadeleriHata bildirimi ve öneriler için issue açabilirsiniz.
MIT — ticari projelerinizde de özgürce kullanabilirsiniz.
İncelemek istediğiniz dosyayı ağaçtan seçin.
Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).
Üç 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.
Bu örneğin daha gelişmişi projenizde mi lazım?
İhtiyacınızı anlatın; bu yapıyı sizin sisteminize uyarlayalım ya da size özel geliştirelim.
Henüz yorum yok. Takıldığınız bir yer varsa ilk soruyu siz sorun.
Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.
İleri
uygulama-iskeleti/php-baslangic-sablonu
309 inceleme 150 canlı açılış 6 beğeni 2 yorum
Orta
uygulama-iskeleti/php-multi-language-i18n
150 inceleme 48 canlı açılış
İleri
uygulama-iskeleti/php-pwa-push-notification
144 inceleme 51 canlı açılış