عن البرمجية

كيف يعمل JoWeather — شرح كامل للأدمن والمطوّر

سكربت PHP 8.2 خالص بدون أي تبعيات خارجية، مبني على معمارية MVC مبسّطة. يقرأ من كاش SQLite، يكتب عبر الـ Cron فقط، ويعرض صفحات HTML صديقة لـ SEO.

0تبعيات Composer/Node
~6,000سطر كود فقط
12حالة اختبار تلقائية
~30msمتوسط زمن الاستجابة
1

ما هذه البرمجية؟

JoWeather موقع طقس للأردن يقرأ من Open-Meteo عبر المسار العام غير التجاري أو مسار العملاء المدفوع بحسب إعدادات الاستضافة، ثم يخزّن التوقعات محليًا في SQLite ويقدّمها للزوار بصفحات HTML سريعة.

  • لغة واحدة: PHP 8.2 — لا JavaScript إلا لأشياء تجميلية، لا CSS framework خارجي.
  • قاعدة واحدة: SQLite (ملف واحد) — لا MySQL، لا تهيئة خادم DB.
  • صفر تبعيات: لا Composer، لا vendor/، لا node_modules.
  • يعمل على أي استضافة مشتركة تدعم PHP (cPanel، Plesk، DirectAdmin).
الفلسفة: كود بسيط يمكن لمطوّر واحد فهمه كاملاً في يوم، ويعمل لعقد من الزمن دون تحديث تبعيات.
2

المعمارية (Mini-MVC)

السكربت يتبع نمط Model–View–Controller لكن بدون أعباء إطار عمل:

الطبقةالمسؤوليةالمكان
Modelقراءة/كتابة قاعدة البياناتapp/Models/ + app/Repositories/
Viewقوالب HTML/PHPapp/Views/
Controllerاستقبال الطلب، تنسيق الردapp/Controllers/
Serviceمنطق أعمال معقّد (طقس، SEO، صحة)app/Services/
Middlewareحماية قبل الكنترولر (Auth, CSRF, RateLimit)app/Middleware/
Coreالموجّه (Router)، قاعدة البيانات، الجلسات، CSRF، View، Bootstrapapp/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 response
Bootstrap container: app('users')، app('weather_cache')، app('health') — حاوية بسيطة تربط الخدمات بدون DI framework.
3

خريطة المجلدات

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 الجذري (الموجود مع التنزيل).
4

كيف تعمل الروابط

كل الروابط في الموقع مُعرّفة في 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/:

  1. Apache يحوّله إلى public/index.php (Front Controller).
  2. الـ Router يبحث في routes/admin.php عن مطابقة.
  3. يطابق GET /w-admin/locations/ ← ينفّذ LocationController::index().
  4. الكنترولر يقرأ من app/Controllers/Admin/LocationController.php (المسار الفعلي على القرص).
  5. يحمّل العرض من app/Views/admin/locations.php.

لماذا اخترنا /w-admin/؟

  • إخفاء: المهاجمون يبحثون عادةً عن /admin/، /wp-admin/ — لن يجدوا شيئاً.
  • تمييز: في الـ access logs، كل طلبات اللوحة تبدأ بـ /w-admin/ فتستطيع تمييزها بسرعة.
  • تغيير بلا تكلفة: لو احتجت لاحقاً، غيّر السلسلة النصية في ملف واحد فقط (routes/admin.php).
ملاحظة: هذا "أمان عبر الغموض" — ليس بديلاً عن المصادقة. الحماية الفعلية في الـ Middleware (Auth, CSRF, RateLimit) + ملف .htaccess الجذري.
5

قاعدة البيانات 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_codeCSS/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
6

استراتيجية الكاش

القاعدة الذهبية: الزوار يقرأون فقط، الكرون وحده يكتب.

┌──────────────┐        ┌──────────────┐        ┌──────────────┐
│   الزائر     │  ───→  │   الموقع     │  ───→  │   الكاش      │
│  (متصفح)    │  HTML  │  (PHP)      │  قراءة  │   SQLite     │
└──────────────┘        └──────────────┘        └──────────────┘
                                                          ↑
                                                          │ كتابة
                                                          │
                       ┌──────────────┐        ┌─────────┴────┐
                       │  Open-Meteo  │  ───→  │   الكرون     │
                       │     API      │        │ (كل ساعتين) │
                       └──────────────┘        └──────────────┘

الفوائد

  • سرعة فائقة: لا API call لكل زائر — قراءة من قاعدة محلية.
  • يعمل عند تعذّر المزود: الزائر يرى آخر قراءة سليمة بدل صفحة فارغة، مع تنبيه عام عند الحاجة.
  • حماية حد الطلبات: التحديث الجماعي يستخدم دفعات وميزانية مشتركة محسوبة بالطلبات الموزونة، ويعمل كل ساعتين.
  • تنظيف تلقائي: صفوف أقدم من 60 يوم تُحذف كل دورة cron.
7

طبقات الأمان التسع

الحماية متعدّدة الطبقات — اختراق طبقة واحدة لا يكفي:

#الطبقةالمكان
1عزل الجذر عبر .htaccess.htaccess الجذري
2Front Controller — كل طلب يمر عبر public/index.phppublic/index.php + public/.htaccess
3Password hashing بـ password_hash() (bcrypt)UserRepository
4CSRF token على كل فورم حساسCsrfMiddleware
5Rate limiting على تسجيل الدخولRateLimitMiddleware
6Permission — سوبر أدمن فقط يرى /w-admin/users/PermissionMiddleware
7CSP (Content-Security-Policy) يحجب السكربتات الخارجيةconfig/security.php
8HSTS (Strict-Transport-Security) — HTTPS فقط في الإنتاجconfig/security.php
9Audit logs — كل محاولة دخول/تغيير تُسجَّلaudit_logs table
راجع صفحة "فحص الصحة" في /w-admin/health/ — تعرض حالة كل طبقة من هذه الطبقات.
8

الكرون والتحديث التلقائي

ثلاث مهام كرون تشغّل الموقع بصمت:

# كل ساعتين عند الدقيقة 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/.

9

كيف توسّع النظام

إضافة صفحة عامة جديدة

  1. أنشئ كنترولر: app/Controllers/MyController.php
  2. أنشئ عرض: app/Views/pages/my-page.php
  3. سجّل المسار: $router->add('GET', '/my-page/', [MyController::class, 'index']);
  4. أضف رابط في القائمة من /w-admin/menu/

إضافة صفحة لوحة تحكم

  1. أنشئ كنترولر: app/Controllers/Admin/MyAdminController.php
  2. أنشئ عرض: app/Views/admin/my-section.php
  3. سجّل المسار: $router->add('GET', '/w-admin/my-section/', [MyAdminController::class, 'index'], $auth);
  4. أضف رابط في السايدبار: app/Views/layouts/admin.php

إضافة عمود لجدول

  1. أضف ALTER TABLE في database/schema.sql (للنُسخ الجديدة)
  2. أنشئ migration بسيط شغّله مرة واحدة على الإنتاج
  3. حدّث الـ Model والـ Repository المعنيّين

إضافة حقل في الإعدادات

من /w-admin/seo/ أو /w-admin/theme/ — كل حقل يُحفظ في جدول settings ويُقرأ عبر app('settings')->get('group', 'key').

10

أسئلة شائعة

هل يدعم أكثر من لغة؟

حالياً عربي فقط. لكن النصوص في 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/ — يُنصح بتوكيل مطوّر.