ما هذه البرمجية؟
JoWeather موقع طقس للأردن يقرأ من Open-Meteo عبر المسار العام غير التجاري أو مسار العملاء المدفوع بحسب إعدادات الاستضافة، ثم يخزّن التوقعات محليًا في SQLite ويقدّمها للزوار بصفحات HTML سريعة.
- لغة واحدة: PHP 8.2 — لا JavaScript إلا لأشياء تجميلية، لا CSS framework خارجي.
- قاعدة واحدة: SQLite (ملف واحد) — لا MySQL، لا تهيئة خادم DB.
- صفر تبعيات: لا Composer، لا
vendor/، لاnode_modules. - يعمل على أي استضافة مشتركة تدعم PHP (cPanel، Plesk، DirectAdmin).
المعمارية (Mini-MVC)
السكربت يتبع نمط Model–View–Controller لكن بدون أعباء إطار عمل:
| الطبقة | المسؤولية | المكان |
|---|---|---|
| Model | قراءة/كتابة قاعدة البيانات | app/Models/ + app/Repositories/ |
| View | قوالب HTML/PHP | app/Views/ |
| Controller | استقبال الطلب، تنسيق الرد | app/Controllers/ |
| Service | منطق أعمال معقّد (طقس، SEO، صحة) | app/Services/ |
| Middleware | حماية قبل الكنترولر (Auth, CSRF, RateLimit) | app/Middleware/ |
| Core | الموجّه (Router)، قاعدة البيانات، الجلسات، CSRF، View، Bootstrap | app/Core/ |
دورة الطلب (Request Lifecycle)
1. HTTP request
↓
2. public/index.php ← (نقطة الدخول الوحيدة، Front Controller)
↓
3. app/bootstrap.php → App\Core\Bootstrap
↓
4. Headers الأمان (CSP, X-Frame, HSTS, …)
↓
5. Session + CSRF
↓
6. App\Core\Router ← يطابق URL مع routes/*.php
↓
7. Middleware chain (Auth → Permission → CSRF → RateLimit)
↓
8. Controller@method ← منطق الصفحة
↓
9. Service / Repository ← قراءة DB أو API
↓
10. View (PHP template) ← HTML للزائر
↓
11. HTTP responseapp('users')، app('weather_cache')، app('health') — حاوية بسيطة تربط الخدمات بدون DI framework.
خريطة المجلدات
joweather/
├── public/ ← جذر الويب فقط (ما يراه الزائر)
│ ├── index.php (Front Controller)
│ ├── .htaccess (Rewrite + headers)
│ ├── assets/ (CSS/JS/صور)
│ ├── favicon.svg
│ ├── robots.txt
│ └── sitemap.xml
│
├── app/ ← كود التطبيق (ممنوع وصول HTTP)
│ ├── bootstrap.php (تشغيل الإطار)
│ ├── Core/ (Router, Database, View, Csrf, …)
│ ├── Controllers/ (Admin/, Home, Deploy, About, …)
│ ├── Models/ (Location, User, Setting, WeatherCache)
│ ├── Repositories/ (DB access: Weather, Users, Ads, …)
│ ├── Services/ (طقس، SEO، صحة، كاش)
│ ├── Middleware/ (Auth, Csrf, RateLimit, Permission)
│ ├── Views/ (admin/, pages/, partials/, layouts/)
│ └── Helpers/ (e, url, weather, format, seo)
│
├── config/ (app, database, security, seo, weather)
├── routes/ (web, admin, api)
├── database/ (schema.sql + seeders)
├── storage/ (DB، جلسات، نسخ احتياطية)
├── cron/ (refresh_weather, refresh_seasonal, generate_sitemap)
├── bin/ (sync_locations)
├── tests/ (cases/ + run.php — CLI فقط)
│
├── .env (إعدادات خاصة بالاستضافة — لا ترفعه!)
├── .env.example (نموذج)
├── .htaccess (حماية الجذر — يحجب app/ storage/ .env)
└── router.php (للتطوير المحلي بـ php -S)public/ غير قابل للوصول من HTTP — بفضل .htaccess الجذري (الموجود مع التنزيل).
كيف تعمل الروابط
كل الروابط في الموقع مُعرّفة في routes/web.php و routes/admin.php كقائمة (method, path, [controller, method]):
// routes/web.php
$router->add('GET', '/city/{slug}/', [WeatherController::class, 'city']);
$router->add('GET', '/deploy/', [DeployController::class, 'index']);
// routes/admin.php (لوحة الأدمن)
$router->add('GET', '/w-admin/locations/', [LocationController::class, 'index'], $auth);
$router->add('POST', '/w-admin/locations/save/', [LocationController::class, 'store'], $authPost);سؤال شائع: أين مجلد /w-admin/؟
غير موجود على القرص. w-admin/ مجرد بادئة URL. عندما يطلب الزائر /w-admin/locations/:
- Apache يحوّله إلى
public/index.php(Front Controller). - الـ Router يبحث في
routes/admin.phpعن مطابقة. - يطابق
GET /w-admin/locations/← ينفّذLocationController::index(). - الكنترولر يقرأ من
app/Controllers/Admin/LocationController.php(المسار الفعلي على القرص). - يحمّل العرض من
app/Views/admin/locations.php.
لماذا اخترنا /w-admin/؟
- إخفاء: المهاجمون يبحثون عادةً عن
/admin/،/wp-admin/— لن يجدوا شيئاً. - تمييز: في الـ access logs، كل طلبات اللوحة تبدأ بـ
/w-admin/فتستطيع تمييزها بسرعة. - تغيير بلا تكلفة: لو احتجت لاحقاً، غيّر السلسلة النصية في ملف واحد فقط (
routes/admin.php).
.htaccess الجذري.
قاعدة البيانات SQLite
قاعدة بيانات SQLite هي ملف واحد (storage/database.sqlite) يتضمّن كل الجداول:
| الجدول | الغرض |
|---|---|
users | حسابات الأدمن (id، email، password_hash، role) |
locations | المحافظات والمناطق (slug، name_ar، lat/lng، governorate) |
weather_cache | التوقعات المخزّنة (TTL=60 يوم، يُحذف تلقائياً) |
settings | إعدادات الموقع والثيم (مفتاح/قيمة، group) |
menu_items | روابط الفوتر والقوائم |
ads | كتل الإعلانات (slot، html، active) |
audit_logs | سجل محاولات الدخول + تغييرات الطقس |
custom_code | CSS/JS مخصّص يُحقن في <head> |
rate_limits | عداد محاولات تسجيل الدخول الفاشلة |
weather_logs | سجل طلبات Open-Meteo (نجاح/فشل + رسالة الخطأ) |
لماذا SQLite وليس MySQL؟
- ملف واحد يمكن نسخه بنسخة احتياطية بنقرة واحدة.
- صفر تهيئة: لا مستخدم، لا كلمة مرور، لا منفذ.
- سريع للقراءة: كافية لـ < 100 استعلام/ثانية (حجم موقعنا).
- WAL mode: قراءة وكتابة متزامنتان بدون تعارض.
// نسخة احتياطية يدوية
cp storage/database.sqlite storage/backups/database.$(date +%Y%m%d).sqlite
// أو من cPanel → File Manager → Downloadاستراتيجية الكاش
القاعدة الذهبية: الزوار يقرأون فقط، الكرون وحده يكتب.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ الزائر │ ───→ │ الموقع │ ───→ │ الكاش │
│ (متصفح) │ HTML │ (PHP) │ قراءة │ SQLite │
└──────────────┘ └──────────────┘ └──────────────┘
↑
│ كتابة
│
┌──────────────┐ ┌─────────┴────┐
│ Open-Meteo │ ───→ │ الكرون │
│ API │ │ (كل ساعتين) │
└──────────────┘ └──────────────┘الفوائد
- سرعة فائقة: لا API call لكل زائر — قراءة من قاعدة محلية.
- يعمل عند تعذّر المزود: الزائر يرى آخر قراءة سليمة بدل صفحة فارغة، مع تنبيه عام عند الحاجة.
- حماية حد الطلبات: التحديث الجماعي يستخدم دفعات وميزانية مشتركة محسوبة بالطلبات الموزونة، ويعمل كل ساعتين.
- تنظيف تلقائي: صفوف أقدم من 60 يوم تُحذف كل دورة cron.
طبقات الأمان التسع
الحماية متعدّدة الطبقات — اختراق طبقة واحدة لا يكفي:
| # | الطبقة | المكان |
|---|---|---|
| 1 | عزل الجذر عبر .htaccess | .htaccess الجذري |
| 2 | Front Controller — كل طلب يمر عبر public/index.php | public/index.php + public/.htaccess |
| 3 | Password hashing بـ password_hash() (bcrypt) | UserRepository |
| 4 | CSRF token على كل فورم حساس | CsrfMiddleware |
| 5 | Rate limiting على تسجيل الدخول | RateLimitMiddleware |
| 6 | Permission — سوبر أدمن فقط يرى /w-admin/users/ | PermissionMiddleware |
| 7 | CSP (Content-Security-Policy) يحجب السكربتات الخارجية | config/security.php |
| 8 | HSTS (Strict-Transport-Security) — HTTPS فقط في الإنتاج | config/security.php |
| 9 | Audit logs — كل محاولة دخول/تغيير تُسجَّل | audit_logs table |
/w-admin/health/ — تعرض حالة كل طبقة من هذه الطبقات.
الكرون والتحديث التلقائي
ثلاث مهام كرون تشغّل الموقع بصمت:
# كل ساعتين عند الدقيقة 17: يجدّد كاش الطقس لكل المواقع
17 */2 * * * php /home/USER/public_html/joweather/cron/refresh_weather.php
# يوميًا 03:00: النظرة الموسمية (4 أسابيع)
0 3 * * * php /home/USER/public_html/joweather/cron/refresh_seasonal.php
# أسبوعيًا: مزامنة قائمة المواقع
15 4 * * 0 php /home/USER/public_html/joweather/bin/sync_locations.phpكل كرون يكتب نتيجة إلى الـ audit_logs + weather_logs. يمكنك مراقبتها من /w-admin/logs/.
كيف توسّع النظام
إضافة صفحة عامة جديدة
- أنشئ كنترولر:
app/Controllers/MyController.php - أنشئ عرض:
app/Views/pages/my-page.php - سجّل المسار:
$router->add('GET', '/my-page/', [MyController::class, 'index']); - أضف رابط في القائمة من
/w-admin/menu/
إضافة صفحة لوحة تحكم
- أنشئ كنترولر:
app/Controllers/Admin/MyAdminController.php - أنشئ عرض:
app/Views/admin/my-section.php - سجّل المسار:
$router->add('GET', '/w-admin/my-section/', [MyAdminController::class, 'index'], $auth); - أضف رابط في السايدبار:
app/Views/layouts/admin.php
إضافة عمود لجدول
- أضف ALTER TABLE في
database/schema.sql(للنُسخ الجديدة) - أنشئ migration بسيط شغّله مرة واحدة على الإنتاج
- حدّث الـ Model والـ Repository المعنيّين
إضافة حقل في الإعدادات
من /w-admin/seo/ أو /w-admin/theme/ — كل حقل يُحفظ في جدول settings ويُقرأ عبر app('settings')->get('group', 'key').
أسئلة شائعة
هل يدعم أكثر من لغة؟
حالياً عربي فقط. لكن النصوص في app/Views/ — استبدالها بترجمة ممكن بدون لمس الكود. لاستخدام متعدد اللغات فعلياً، يلزم إضافة جدول translations و helper جديد.
كم موقعاً يمكنني إضافة؟
يمكن إضافة مواقع كثيرة برمجياً، لكن السعة الفعلية مرتبطة بعدد المتغيرات ومدة التوقع وعدد المواقع وجدولة الكرون. النظام يجمّع الإحداثيات ويحسب ميزانية موزونة مشتركة، لذلك يجب اختبار الحصة عند التوسع وعدم تشغيل مهام تحديث مكررة.
هل يمكنني استخدام MySQL؟
نظرياً نعم (PDO يدعمه)، لكن الكود مُحسَّن لـ SQLite (WAL، transactions بسيطة). التحويل يحتاج مراجعة app/Repositories/.
كيف أراقب أداء الموقع؟
/w-admin/health/ يعرض: زمن آخر تحديث cron، حجم الكاش، استخدام القرص، حالة API. للإنتاج، أضف LiteSpeed Cache (طبقة إضافية مجانية).
كيف أترجم القالب لمظهر مختلف؟
من /w-admin/theme/: غيّر الألوان والشعار. للقالب الكامل (تخطيط)، يلزم تعديل app/Views/layouts/public.php و app/Views/partials/ — يُنصح بتوكيل مطوّر.