העמוד הזה עוקב אחרי בקשה אחת מרגע שהיא מגיעה לשרת ועד שהדפדפן מקבל HTML. הוא נכתב מקריאת הקוד עצמו, ולכן אפשר לסמוך עליו כשמשהו "לא מתנהג כמו שחשבתי": ברוב המקרים התשובה נמצאת באחד מהשלבים כאן. מפת העל נמצאת ב-סקירת ארכיטקטורה.
הליבה יושבת ב-api/core/ בריפו ונפרסת ל-system/core/ באתר. כל נתיב בעמוד שאינו מתחיל ב-/ הוא יחסי לתיקיית הליבה (לדוגמה collections/ROUTER.php). כתובות שמתחילות ב-/system/<name> הן כתובות URL של קונטרולרי מערכת, לא תיקייה. {admin} בכתובות הוא CONFIG::$admin_url.
התמונה כולה#
Request
|
v
[0] Apache .htaccess
static file / cached js, css, scss on disk ----> served by Apache, PHP never runs
everything else ------------------------------> index.php
|
v
[1] index.php ob_start(gzip) -> system/config.php -> real core path -> core.php
|
v
[2] core.php
require collections/*.php
CONFIG::preinit() base_path, protocol, models and cache folders
MISC::cli_check() only when PHP runs from the command line
CONFIG::init() DB platforms table -> platform for this host
application/includes/init.php (site hook, optional)
cache_engine::cache_php_check() --- fresh page file? ---> echo + exit
MISC::init_security() SQL-looking $_GET is rejected
CronLogger::start_capture()
require Smarty
|
v
[3] PAGE::load($_SERVER['REQUEST_URI'])
PAGE::reset, PARAMS::init, SEO::init
ROUTER::parse_friendly_url URL -> $_GET (module / pname / id / sys_controller)
client: domain check, ROUTER::redirections (301), application/includes/*.php
admin : forms + AdminModule libraries, application/admin/includes/*.php
LANGS::init_words, PAGE::trigger("SYSTEM_LOADED")
client: MODULE::get_html(...) controller method -> app_html
admin : AdminModule::get_html() ADMINMODULE_<panel> -> app_html (or login screen)
PAGE::output() theme + head/body assets -> full HTML
|
v
[4] core.php again
MASK::out(html)
client + opted in: cache_engine::cache_all_page_save
echo PAGE::parse_page(html) PAGE::$parse_page_func hook
CronLogger::finish_capture, STORAGE::FtpClose, DB::close_connection
| שלב | קובץ | מה הוא עושה | מה אפשר להתחבר אליו |
|---|---|---|---|
| Apache | .htaccess (של האתר) | מגיש קבצים סטטיים ו-JS/CSS ממטמון, שולח כל השאר ל-index.php | כללי Rewrite, Expires ו-Deflate של האתר |
| כניסה | index.php (של האתר) | פותח buffer, טוען את system/config.php, מחשב נתיב ליבה, טוען core.php | קוד לפני ה-require של הליבה |
| אתחול | core.php, collections/CONFIG.php | טוען את כל ה-collections, קובע נתיבים ופלטפורמה | application/includes/init.php |
| מטמון עמוד | collections/cache_engine.php | מגיש עמוד שמור ויוצא | cache_engine::cache_all_page() מתוך קונטרולר |
| אבטחה | collections/MISC.php | פוסל $_GET שנראה כמו SQL | אין |
| ניתוב | collections/ROUTER.php | כתובת ל-$_GET: מודול, מתודה ו-id | שורות CRM_seoUrl ו-CRM_redirections |
| טעינת מערכת | collections/PAGE.php | יוצר סביבה, נכסים, מילים, אירועים | application/includes/*.php, PAGE::bind("SYSTEM_LOADED", ...) |
| קונטרולר | collections/MODULE.php | מוצא קובץ, יוצר מחלקה, קורא למתודה | הקונסטרקטור, wrapper(), "ERRORPAGE", "REQUIRE_LOGIN" |
| תצוגה | collections/FILES.php | html_template מרנדר .tpl עם Smarty | קבצי application/views |
| Theme | collections/theme.php, themes/<name>/ | מעטפת העמוד | TEMPLATE_<name>::index() |
| פלט | PAGE::output | מרכיב <html> מלא, נכסים, תגיות SEO, סקריפטים | PAGE::add_asset, add_meta, טבלת CRM_scripts |
| סיום | core.php | הסוואה, שמירה במטמון, parse_page, סגירת חיבורים | PAGE::$parse_page_func |
שלב 0: Apache ו-.htaccess#
קובץ .htaccess שיושב בשורש האתר הוא של האתר ולא של הליבה, והוא עושה שלושה דברים:
- מגיש ישירות קבצי JS ו-CSS שכבר נוצרו במטמון (
cache/<SERVER_NAME>/...) וקבצי SCSS שכבר קומפלו. כשהקובץ קיים, PHP לא רץ בכלל. כשהוא לא קיים, הכתובת*.scss.cssו-minify/*.js|cssנשלחות ל-index.phpכדי שהקונטרולרminifyייצור אותן. - מבצע הפניות 301 מ-
wwwל-ללאwwwומ-httpל-https, וחוסם גישה לקבצי.chcבתיקיית המטמון. - שולח כל כתובת אחרת ל-
index.php(ה-front controller), כל עוד היא לא קובץ אמיתי ולא אחת מהתיקיות שהוחרגו:
RewriteCond $1 !^(\.well-known)
RewriteCond $1 !^(firebase-messaging-sw\.js)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond $1 !^(index\.php|cache|application/views|application/views_mobile|system/js|system/libraries|system/css|media|themes)
RewriteRule ^(.*)$ /index.php?%{QUERY_STRING} [L]
בניגוד ל-JS ו-CSS, עמודי HTML שמורים אינם מוגשים על ידי Apache. הם מוגשים על ידי PHP, בתחילת core.php (שלב 2). לכן גם עמוד שנמצא במטמון עולה בקריאה אחת ל-PHP ובקריאת הפלטפורמה מהמסד, ולא "אפס עבודה".
ב-.htaccess מוגדר ErrorDocument 404 /404. כיוון שכמעט כל כתובת מנותבת ל-index.php, ה-404 של Apache כמעט לא מופעל. 404 באתר מגיע מהליבה (ראו "404, 503 ושגיאות" למטה).
שלב 1: index.php#
הקובץ index.php בשורש האתר קצר, ובנוי כך (בגרסת פרודקשן מומלץ להשמיט את שורת display_errors):
<?php
error_reporting(E_ALL); // בפרודקשן אל תדליקו display_errors
ob_start("ob_gzhandler");
require_once("system/config.php");
// נתיב הליבה: יחסי ב-CONFIG_USER ("system/core" או "../shared/system/core"), כאן הוא נהיה מוחלט
$ex = explode("../", CONFIG_USER::$core_path);
$realPath = dirname(__FILE__);
if (CONFIG_USER::$core_path != "system/core") for ($i = 0; $i < (count($ex)); $i++) $realPath = dirname($realPath);
CONFIG_USER::$core_path = $realPath . "/" . $ex[(count($ex) - 1)];
require_once(CONFIG_USER::$core_path . "/core.php");
ob_start("ob_gzhandler")דוחס את כל הפלט, כולל עמודי JSON.system/config.phpמגדיר את המחלקהCONFIG_USER(גישה למסד,admin_url,default_module, תיקיות מטמון ועוד; הפירוט ב-קונפיגורציה).- הנתיב לליבה יחסי: אתר שמשתף ליבה עם אתרים אחרים מצביע ל-
../shared/system/core.CONFIG::preinit()יודעת את תיקיית האתר מהקובץ הראשי שהתחיל את הבקשה (index.php), ולא מנתיב הליבה.
שלב 2: core.php שורה אחר שורה#
core.php הוא כל מה שרץ ברמה העליונה, ולכן כדאי להכיר אותו בסדרו:
foreach (glob($file_location . "/collections/*.php") as $filename) require_once($filename);
(1) טעינת ה-collections. כל קובץ בתיקיית collections/ נטען, בסדר אלפביתי. בקבצים עצמם יש הגדרות מחלקות בלבד, אין להם תופעות לוואי בטעינה. אין autoloader ואין namespaces (ראו ה-Collections ו-מוסכמות ושמות).
CONFIG::preinit();
(2) CONFIG::preinit(). קובעת אזור זמן (Asia/Jerusalem), את CONFIG::$protocol (https אם $_SERVER["HTTPS"] דולק, ובשורת פקודה תמיד https), את CONFIG::$base_path מהקובץ שהתחיל את הריצה, ואת הנתיבים המוחלטים של models_folder ושל cache_folder.
MISC::cli_check();
(3) MISC::cli_check(). בריצה משורת פקודה קוראת את הארגומנטים כ-key=value ומכניסה אותם ל-$_GET; הארגומנט url= הופך ל-REQUEST_URI. בבקשת HTTP רגילה היא לא עושה כלום. פרטים ב"נתיב ה-CLI" למטה.
CONFIG::init();
(4) CONFIG::init(). קוראת את טבלת CRM_platforms (ממטמון של 30 יום), ובוחרת את הפלטפורמה לפי SERVER_NAME. אם הדומיין לא נמצא, היא מחפשת אותו בעמודה allowed_domains של כל פלטפורמה, ואם גם שם אין, לוקחת את הפלטפורמה הראשונה בטבלה. משם נקבעים CONFIG::$site_url, CONFIG::$lang (שהוא מזהה הפלטפורמה) ו-MISC::$GLOBALS["lang"]. הפירוט ב-פלטפורמות ורב-לשוניות.
if (file_exists(CONFIG::$base_path . "/application/includes/init.php")) require_once(...);
(5) application/includes/init.php. קובץ הקרס הראשון של האתר. הוא רץ אחרי שהפלטפורמה והמסד זמינים ולפני כל דבר אחר, כולל בדיקת מטמון העמודים, ולכן מתאים למשהו שחייב לרוץ בכל בקשה.
PAGE::load מתחילה בקריאה ל-PAGE::reset(), שמאפסת את האירועים (PAGE::bind), את הנכסים, תגיות ה-meta, ה-theme וכן את PAGE::$include_system_js. כל מה שרשמתם ב-init.php נעלם. אירועים ונכסים רושמים בקבצים אחרים תחת application/includes/ (שנטענים אחרי ה-reset), ו-init.php נשאר ל-CONFIG, ל-cache_engine ול-PAGE::$parse_page_func (שאינו מאופס).cache_engine::cache_php_check();
(6) בדיקת מטמון עמודים. אם CONFIG_USER::$allow_cache דולק, הפונקציה מנרמלת את REQUEST_URI (מסירה פרמטרי מעקב כמו utm_* ו-gclid, כדי שגלישה עם פרמטר כזה תשתמש באותו קובץ מטמון), ומחפשת קובץ cache/<host>/pages/<url>.html.gz (או pages_mobile למכשיר נייד). זמן התפוגה הוא זמן השינוי של הקובץ: הוא נחשב טרי כשה-mtime שלו עדיין בעתיד. אם יש עמוד טרי הוא מוגש (הדחוס נשלח כמו שהוא כשהדפדפן מקבל gzip), ו-exit מסיים את הבקשה. ב-hit כזה הניתוב, הקונטרולרים, Smarty והתבניות לא נטענים בכלל. אם PAGE::$parse_page_func מוגדר, העמוד עובר דרכו גם ב-hit. הנושא כולו ב-מטמון עמודים מלא.
MISC::init_security();
(7) סינון בסיסי של קלט. עוברת על הקלט של הבקשה. מפתח או ערך ב-$_GET שנראים כשאילתת SQL לפי היוריסטיקה (ביטוי כמו select * או union all, או צבר של מילות SQL כמו select, where, and, or באותה מחרוזת) מפיל את הבקשה ב-die("SECURITY ERROR1"). זו רשת ביטחון גסה ולא תחליף ל-DB::escape ו-prepared statements, ולעיתים היא חוסמת קלט לגיטימי, למשל חיפוש חופשי בעל ניסוח SQL-י. אל תסתמכו עליה לשום קלט: כל ערך שמגיע לשאילתה גולמית עובר DB::escape. ראו CSRF, XSS ו-SQLi.
CronLogger::start_capture();
require_once(CONFIG::$core_path . "/addons/Smarty/libs/Smarty.class.php");
class_alias('\Smarty\Smarty', 'Smarty');
(8) לוג cron ו-Smarty. CronLogger פותחת buffer רק כשהבקשה היא ריצת cron מאומתת (טוקן ו-id של שורת לוג). Smarty נטען כאן, ורק אחרי בדיקת המטמון. אם CONFIG::$storage_remote == "s3", נטענת גם ספריית AWS.
$outpout = PAGE::load($_SERVER['REQUEST_URI']);
(9) PAGE::load. הלב: ניתוב, קונטרולר, תצוגה ופלט. מפורט בשלב הבא. התוצאה היא מחרוזת ה-HTML המלאה (המשתנה נקרא בקוד $outpout, כך באמת).
$outpout = MASK::out($outpout);
if (CONFIG::$system_type == "client" && CONFIG::$allow_cache == true && PAGE::$cache_this_page === true)
cache_engine::cache_all_page_save($outpout, cache_engine::get_page_url());
echo PAGE::parse_page($outpout);
(10) סיום. מצב הסוואה מחליף נתיבים ציבוריים בשמות ניטרליים, לפני השמירה במטמון, כך שהמטמון מכיל כבר HTML מוסווה. העמוד נשמר רק אם הוא בצד הלקוח, המטמון מופעל בהגדרות, וקונטרולר ביקש במפורש לשמור אותו (PAGE::$cache_this_page, שהקריאה cache_engine::cache_all_page($hours) מדליקה). ברירת המחדל היא לא לשמור. אחר כך PAGE::parse_page מריץ את ה-hook של האתר (PAGE::$parse_page_func) אם הוגדר; הוא רץ על כל תשובה, גם על עמוד שהוגש מהמטמון, כי הוא מופעל אחרי השמירה. לבסוף נסגרים לוג ה-cron, חיבור ה-FTP וחיבור המסד.
שלב 3: PAGE::load, הרצף המלא#
הפונקציה PAGE::load($url, $isMobile = null) רצה בסדר קבוע:
PAGE::reset(): איפוס מצב העמוד (ראו האזהרה למעלה).PARAMS::init()טוען את טבלתCRM_params, ו-SEO::init()מאפס את שדות ה-SEO.ROUTER::parse_friendly_url($url): הופך כתובת לערכי$_GETומכריע אם הבקשה היאadminאוclient(CONFIG::$system_type). ראו "ROUTER" למטה.- בצד הלקוח: בדיקת דומיין. אם
SERVER_NAMEאינו הדומיין של הפלטפורמה ולא מופיע ב-allowed_domains, התשובה היאdie("URL ERROR"). הבקרה מוחרגת לבקשותminifyו-Tools, ולריצה משורת פקודה (ignore_domain_check). - בצד הלקוח:
ROUTER::redirections()ואחריהFILES::include_dir("application/includes"), שטוען כל קובץ*.phpשבתיקייה, ממוין לפי שם קובץ, עםrequire_once. זה המקום של האתר לרשום אירועים ולהוסיף נכסים. בצד הניהול הנטען הואapplication/admin/includes(ראו "נתיב הניהול"). - כל קובצי
system/js/*.jsנרשמים כנכסים, כש-system/js/system.jsבראש העמוד והשאר בסופו. אפשר לבטל על ידיPAGE::$include_system_js = falseבקובץ include. - בצד הלקוח: טעינת
CRM_minify_filesו-CRM_minify_groups(מטמון של 30 יום) ורישום קבוצות הנכסים שמתאימות ל-theme. הרישום הזה קורה לפני שה-theme ברירת המחדל נקבע בשלב 8. קבוצה שמוגבלת ל-theme מסוים תיכלל רק אם הקוד כבר קרא ל-PAGE::set_theme(למשל מקובץ include), ועל קבוצות בלי הגבלה זה לא משפיע. ראו צינור הנכסים. LANGS::init_words()טוען את מילות הממשק של השפה, ואזPAGE::trigger("SYSTEM_LOADED"). מכאן אפשר לדעת שהמערכת, השפה והנכסים מוכנים.- בצד הלקוח: אם אף אחד לא בחר theme, נבחר
defthemeשל הפלטפורמה. אם$_GET["sys_controller"]קיים, מופעלMODULE::get_html(controller, pname, true)(קונטרולר מערכת), אחרתMODULE::get_html(module, pname). התוצאה נשמרת ב-MISC::$GLOBALS["app_html"]. PAGE::output()מרכיב את העמוד.
ROUTER: מהכתובת ל-$_GET#
ROUTER::parse_friendly_url היא רצף בדיקות לפי סדר, והראשונה שמתאימה מנצחת (רוב הבדיקות מסתיימות ב-return או die). הסדר חשוב כי הוא קובע מי "תופס" כתובת:
| # | תנאי | מה קורה |
|---|---|---|
| 0 | הכתובת מכילה ? | $_GET מוחלף בפרמטרי המחרוזת (כולל פרמטרי המעקב שהוסרו מ-REQUEST_URI) |
| 1 | MASK::in($url) | מחזיר נתיב אמיתי מנתיב מוסווה (או מגיש קובץ סטטי ויוצא) |
| 2 | כתובת ריקה | module = CONFIG::$default_module, pname = "" |
| 3 | <32 hex>.txt | קובץ מפתח IndexNow, מוגש על ידי SeoIndexNow אם המפתח נכון |
| 4 | poosh-sw.js, tjs.js, pjs.js | קבצים ראשונים-צד של שירותי Wizzo, מוגשים ו-die |
| 5 | p/<slug> | דף NEWPAGE: קונטרולר מערכת newpage_public |
| 6 | pg.js, pg.css | נכסי NEWPAGE במטמון |
| 7 | tag_manager.js | קונטרולר minify, pname=tag_manager |
| 8 | minify/<id>.js|css | קונטרולר minify |
| 9 | assets/<x>.scss.css ואז <x>.scss.css | קומפילציית SCSS על ידי minify (pname=scss) |
| 10 | assets/<x>.<סיומת> (קבצים סטטיים של הליבה) | אם הקובץ קיים ב-core/assets הוא מוגש ב-readfile ו-die; אחרת ממשיכים |
| 11 | {admin} ושאר תבניות הניהול | system_type = "admin" (ראו למטה) |
| 12 | צד לקוח: חיפוש ב-CRM_seoUrl | התאמה מדויקת לפי sysname, אחר כך ביטויים רגולריים בסדר ord |
| 13 | אין התאמת SEO | הכתובת נשארת כפי שהיא ועוברת לתבניות הבאות |
| 14 | תבניות system/... | קונטרולר מערכת (sys_controller) |
| 15 | תבניות מודול | module, pname, id |
דוגמאות לכתובות שאחרי ה-SEO:
| הכתובת (אחרי SEO) | תוצאה ב-$_GET |
|---|---|
blog | module=blog |
blog/12 | module=blog, id=12 (קטע שני מספרי בלבד) |
blog/post | module=blog, pname=post |
blog/post/12 | module=blog, pname=post, id=12 |
blog/post/a/b | module=blog, pname=post, id=a/b (הקטע השלישי אוסף את כל השאר) |
system/cron_runner | sys_controller=cron_runner |
system/x/y/z | sys_controller=x, pname=y, id=z |
שלבים 3 עד 10 מתבצעים לפני חיפוש ה-SEO והמודולים, ולכן לא אפשר להשתלט עליהם בשורת CRM_seoUrl. בפועל, מודול בשם p לא יענה בכתובת /p/<משהו>, ומודולים בשמות minify, assets ו-system שמורים. שמות קבצים כמו poosh-sw.js שמורים גם הם. כשבוחרים שם למודול עדיף להימנע מכל אלה.
הניתוב לא יודע אם המודול קיים. הוא רק ממלא $_GET. הבדיקה מול CRM_modules והקובץ נעשית אחר כך ב-MODULE. כתובת שאין לה שום תבנית מתאימה נגמרת ב-die("router error"), מצב נדיר כי כל כתובת לא ריקה מתאימה לאחת מתבניות המודול.
ניהול. תבניות ה-admin הן {admin} לבדו, {admin}/<panel>, {admin}/<panel>/<page>, {admin}/<panel>/<page>/<id> ו-{admin}/<panel>/<מספר>, וכולן מציבות system_type = "admin". אם מוגדרת הגבלת מדינות לניהול, הניתוב בודק את מדינת הגולש לפי IP ומפנה לדף הבית אם היא לא ברשימה, חוץ מקריאה פנימית מהשרת עצמו וכתובות התשתית של Wizzo.
SEO. שורת CRM_seoUrl רגילה (regularexp = 0) מתאימה כש-sysname שווה לכתובת (גם מפוענחת). שורה עם regularexp = 1 היא ביטוי רגולרי מלא כולל מפרידים ב-sysname, וה-pageurl שלה משתמש ב-$1, $2 להחלפות. הנתיב שהתקבל נכנס מחדש לתבניות המערכת והמודולים. אם pageurl מכיל ?, הפרמטרים שאחריו מתמזגים אל $_GET. פרטים ב-כתובות ידידותיות וב-ניתוב. הטבלה נשמרת במטמון בשם seoUrl_tbl.
הפניות. ROUTER::redirections() רצה אחרי הניתוב ומחפשת את הכתובת (בלי לוכסנים בקצוות, ובלי מחרוזת שאילתה) בטבלת CRM_redirections לפי url_from. אם נמצאה, נשלחת תשובת 301 עם Location והמחרוזת המקורית מתווספת. ההפניות מתבצעות רק בצד הלקוח, ולכן אינן משפיעות על הניהול (ראו הפניות 301).
דיבוג. מנהל מחובר, או בקשה מהשרת עצמו, יכול להוסיף ?__router_trace=1 לכל כתובת ולקבל בתחתית הדף את כל ההחלטות. ראו דיבוג ניתוב.
MODULE::get_html: מ-$_GET למתודה#
public static function get_html($module, $page = "index", $is_system = false)
הפונקציה מקבלת את שם המודול ואת שם המתודה ("" הופך ל-index) ופועלת בשני מצבים:
קונטרולר מערכת ($is_system = true): הקובץ הוא core/controllers/<name>.php, בלי בדיקה ב-CRM_modules.
קונטרולר אתר: קודם MISC::get_modules_tbl() (שורות CRM_modules לפי moduleName, ממטמון modules_lang<id> של 7 ימים). המודול חייב להופיע וה-active שלו חייב להיות 1. רק אז:
MODULE::get($module)עושהrequire_onceל-application/controllers/<module>.php(אם הקובץ קיים), ובודק ש-class_exists($module). כל כשל מחזירfalseונגמר ב-404.new $module()יוצר את המחלקה. הקונסטרקטור רץ גם אם המתודה המבוקשת לא קיימת, כך שתופעות לוואי שלו (הפניה,header,$_GET["pmode"] = "empg") קורות בכל מקרה.- אם
is_callable([$mdl, $page]), נקראת המתודה ($pg_content = $mdl->$page()), והתוצאה עוברת דרך$mdl->wrapper($pg_content)(ברירת המחדל מחזירה אותה כמות שהיא; אפשר לדרוס במחלקת בסיס משותפת). - כל מתודה ציבורית נחשבת "callable", גם כזו שהקונטרולר ירש. ראו האזהרה בעמוד העמוד הראשון שלך.
מה שהמתודה מחזירה קובע את ההמשך:
| ערך מוחזר | משמעות |
|---|---|
| מחרוזת HTML | תוכן העמוד; נכנס ל-theme כ-content |
"ERRORPAGE" | 404, דרך MODULE::errorpage() |
"REQUIRE_LOGIN" | מוחזר סקריפט קטן שמפעיל את אירוע REQUIRE_LOGIN של האתר (הקליינט מציג התחברות) |
die או echo בסוף | עוקף את כל השלבים הבאים; מתאים ל-JSON ולקבצים |
אם המודול לא פעיל אבל קובץ הקונטרולר קיים, ומנהל מחובר, הליבה מדפיסה טופס "Click here" להפעלה ומסיימת. בכל מקרה אחר התוצאה היא "ERRORPAGE".
עזרי הקונטרולר: view, system_view, load#
כל קונטרולר יורש מ-wz_controller (בשמו הישן bgl_controller):
function view($tpl, $vars = array()) // application/views/<tpl>.tpl
function system_view($tpl, $vars = array()) // core/views/<tpl>.tpl
שתיהן קוראות ל-FILES::html_template(path, vars). אם CONFIG_USER::$use_smarty הוא true, נוצר מופע Smarty, כל המשתנים ב-$vars מוצמדים אליו, וגם המערך MISC::$GLOBALS כמשתנה $GLOBALS בתבנית. תיקיות הקומפילציה והמטמון של Smarty הן cache/Smarty/templates_c/ ו-cache/Smarty/cache/ יחסית לתיקיית העבודה של התהליך, שהיא שורש האתר תחת Apache. אין כאן קישור לתבניות הניידות (views_mobile): הקריאה לתיקייה מוחלפת בקוד כהערה ואינה פעילה.
הטוען $this->load (המחלקה wz_loader) עושה שני דברים:
$this->load->model("shop"); // require CONFIG::$models_folder . "/shop.php"; $this->shop = new shop
$this->load->library("forms"); // require core/libraries/forms.php (או את כל הקבצים אם זו תיקייה)
המודל נטען מתיקיית models של האתר ומוצמד לקונטרולר כמאפיין בשם המחלקה. ספרייה נטענת מתיקיית libraries של הליבה בלבד. הקונטרולר משתמש ב-__get ו-__set קסומים, ולכן שגיאת כתיב במאפיין ($this->shpo->x()) מחזירה null ולא שגיאה, ופירושה "Call to a member function on null".
Theme ו-PAGE::output#
PAGE::output() מתחילה בבדיקת $_GET["pmode"]:
pmode=empgאוpmode=inner: בלי theme ובלי מעטפת HTML. הפונקציה שולחת כותרותscriptsו-scripts_verעם רשימת הנכסים שנרשמו (כדי שה-JS בצד הלקוח יטען אותם), והפלט הואapp_htmlבלבד. זה הנתיב של AJAX, JSON וחלונות פנימיים.- אחרת: אם נקבע theme, נטען
themes/<name>/index.php(לניהול: מהליבה,themes/admin_loginו-themes/admin_panel), ונוצרת המחלקהTEMPLATE_<name>(יורשת מ-bgl_themeשיורש מ-bgl_controller).get_html()שלה קורא ל-index()שרושם נכסים ומשתנים, ואחר כך מרנדר אתthemes/<name>/index.tplעםcontent(תוכן העמוד) ו-pgClass. בלי theme, התוכן הואapp_htmlעצמו.
אחר כך מורכב ה-HTML לפי סדר קבוע:
<!DOCTYPE html><html lang="he" dir="...">(הכיוון מ-CRM_platforms.direction; ה-langקבוע ל-he).- בתוך
<head>: תגיות SEO (SEO::tags_summary()),<base href>, נכסי CSS וה-JS של האזורhead, תגיות meta של האזור,robots, favicon, ובצד הלקוח גם JSON-LD מסוגWebPageוסקריפטים מטבלתCRM_scripts(אזורhead). <body class="...">, ואחריו נכסים, סקריפטים ותגיות meta של האזורbody_start.- תוכן ה-theme. כל תגית
<script>שבתוכו שאין עליהnominifyמוצאת מהמקום שבו כתבתם אותה ומוצבת בסוף ה-body, אחרי נכסיbody_end, כשהקוד הפנימי עובר כיווץ JShrink. - נכסי
body_end, סקריפטים שלCRM_scriptsמאזורbody_end, ואם קייםsystem/system_tags.json, גם סקריפט ה-tag manager. </body></html>, ובסוףPAGE::minify_html().
סקריפט inline שכתבתם באמצע תבנית יזוז לסוף העמוד, ולכן קוד שמניח שהוא רץ "ברגע שהאלמנט שלפניו נטען" ישתנה. סקריפט שחייב להישאר במקומו מסמנים ב-nominify. סקריפט עם src אינו מכווץ, אבל גם הוא מועבר.
PAGE::minify_html() רצה על כל תוצאה של output(), כולל pmode=empg. היא מקצרת רצפי רווחים ומסירה שורות ריקות ורווחים בין תגיות, ולכן מחרוזת JSON או טקסט שחוזרים כ-return יכולים להשתנות (כמה רווחים רצופים בערך מחרוזת הופכים לאחד). אפשר לכבות לגמרי ב-CONFIG_USER::$minify_html = false. לתשובות JSON ראו "AJAX ו-JSON" למטה.404, 503 ושגיאות#
כש-MODULE::get_html מקבלת "ERRORPAGE" (או שהמודול לא נמצא), נקראת MODULE::errorpage():
- אם היו שגיאות מסד נתונים בבקשה הזו (
DB::had_error()), התשובה היא 503 עםRetry-Afterועמוד קבוע. הסיבה: שגיאת מסד מרוקנת את רשימת המודולים והופכת עמודים תקינים ל"לא נמצאו", ו-404 מוחק אותם מגוגל, בעוד 503 נחשב זמני. - אחרת,
ROUTER::parse_friendly_url("404")נקראת מחדש: היא שולחתHTTP/1.1 404, מציבהPAGE::$is_404 = trueומנתבת לשורתCRM_seoUrlשה-sysnameשלה הוא404. - אם ה-
sysname404מצביע על מודול פעיל, המתודה שלו מורצת והתוצאה היא העמוד.PAGE::$cache_this_pageנכבה כך שה-404 לא נשמר. - אחרת, התשובה היא
<h1>404 - Page not found</h1>.
בלי שורת 404 בטבלת CRM_seoUrl מתקבל עמוד גנרי. יש בקוד הניתוב ענף שאמור להדפיס "page not found. please set 404 page.", אך הוא לא יכול להתממש: המשתנה שהוא בודק כבר מקבל ערך קודם לכן. לכן התנהגות אחרי "אין שורה" היא העמוד הגנרי. עוד ב-404, 503 ושגיאות.
נתיב הניהול#
כש-ROUTER מציבה system_type = "admin", PAGE::load עושה במקום הצעדים של צד הלקוח:
- טוענת את ספריות הניהול (
forms,market_service,AdminModule,panel_table), מאתחלת את ריבוי השפות של הניהול, טוענת אתagent_toolsומריצה את כלapplication/admin/includes/*.php. אלה קבצים שבהם האתר רושם כלי MCP בעזרתAGENT_TOOLS::add_tool(כתיבת כלי MCP). כאן איןROUTER::redirections()ואיןapplication/includes. ADMIN::is_admin()בודק התחברות. לא מחובר: ה-theme הואadmin_login, מסך התחברות מבוסס Vue, שמתקשר עם קונטרולר המערכת/system/admin_login?action=.... מחובר: ה-theme הואadmin_panel, ו-AdminModuleמוצא את הפאנל.AdminModule::GetPageContentמזהה פאנל וקובץ: קודםadmin/<module>.phpבליבה, ורק אם אינו קיים,application/admin/<module>.phpשל האתר. קובץ ליבה בשם זהה מגבה את פאנל האתר. המחלקה היאADMINMODULE_<module>, והמתודה ברירת המחדלindex.- הרשאות: הפאנל חייב להיות רשום ב-
CRM_adminPanel_panels(לפיpanel_name), למשתמש חייבת להיות הרשאה אליו, ופאנל שמסומןperm_developerדורש מפתח. מחלקה שמגדירה את המאפייןskip_permission_checkפטורה מבדיקת ההרשאה לפאנל. פאנלים של שירותי השוק מגיעים מ-system/market_settings.json. ראו התחברות, סשנים ו-WIZZO ID ו-הרשאות וקבוצות. - אם המתודה חסרה, או שהפאנל לא קיים או חסום, נשלחת תשובת 404 והפניה ל-
/404. בקשה של לקוח תכנותי (עם כותרתAI-...) מקבלת במקום זה JSON עם הסיבה. - התוצאה עוברת דרך
Wrapper()של הפאנל ונכנסת ל-themeadmin_panel.
המסגרת עצמה, panel_table, Form ושאר הכלים, ב-סקירת פאנל הניהול וב-בניית פאנל. בצד הניהול, CONFIG::$system_type הוא admin, ולכן תוצאת העמוד אינה נשמרת במטמון ו-MASK אינו מחליף נתיבים.
נתיב ה-CLI וה-cron#
הליבה יודעת לרוץ משורת פקודה: php index.php url=<path> key=value ... (חייבים להריץ מתיקיית האתר, כי חלק מהנתיבים בליבה יחסיים לתיקיית העבודה). MISC::cli_check() (שלב 3 ב-core.php) מעבדת את הארגומנטים:
url=...נכנס ל-$_SERVER["REQUEST_URI"], וכלkey=valueאחר ל-$_GET.- אין
SERVER_NAMEבשורת פקודה, ולכןCONFIG::init()בוחרת את הפלטפורמה הראשונה בטבלה, ואחר כךSERVER_NAMEו-HTTP_HOSTמוגדרים לדומיין שלה.CONFIG::$ignore_domain_checkנקבע ל-true, כך שבדיקת הדומיין מוחרגת. - משם הבקשה ממשיכה כמו בקשה רגילה, כולל ניתוב ומודולים.
כך עובדים ה-cron ושירותי רקע: controllers/cron_runner.php רץ בקצב קבוע, בוחר משימות שהגיע זמנן מטבלת CRM_cron_tasks, ולכל אחת קורא ל-MISC::ping($url, $params) שמפעיל תהליך PHP מנותק ברקע (nohup php index.php url=... &). לכל ריצה נפתחת שורת לוג בסטטוס running, ו-CronLogger (שמופעל ב-core.php כשהבקשה מכילה את הטוקן ואת מזהה הלוג) תופס את הפלט ושומר אותו. הקריאה ל-cron_runner דורשת טוקן תקף (tk, מושווה ל-cron_runner_token). פירוט מלא ב-מנהל משימות (Cron).
AJAX ו-JSON#
קונטרולר שמחזיר נתונים ולא עמוד פועל לפי אותו דפוס בכל הליבה:
class orders_api extends wz_controller
{
function __construct()
{
parent::__construct();
$_GET["pmode"] = "empg"; // בלי theme ובלי מעטפת HTML
}
function list()
{
if (!ADMIN::is_admin()) { http_response_code(403); die; }
header("Content-Type: application/json; charset=utf-8");
echo json_encode(["ok" => true, "items" => []], JSON_UNESCAPED_UNICODE);
die; // עוקף minify_html, parse_page ושמירה במטמון
}
}
הכללים:
pmode=empgנקבע לפני שהקונטרולר מחזיר תוכן, ובדרך כלל בקונסטרקטור.pmode=innerדומה, וקיים לחלונות פנימיים בניהול. שניהם משמיטים את ה-theme.echoואחריוdieהוא הדפוס הבטוח ל-JSON: כך הפלט לא עובר דרךminify_html, לא דרךparse_pageולא נשמר במטמון. קונטרולרים כמוcore_pingו-Toolsשל הליבה עובדים כך. סוג התוכן (Content-Type) אינו נקבע אוטומטית.- 404 מחזירים בעזרת
"ERRORPAGE", ואם רוצים סטטוס אחר שולחים אותו בעצמכם לפניdie. - אימות והרשאה הם באחריות הקונטרולר. אין בדיקת הרשאה מרכזית על קונטרולר רגיל או על קונטרולר מערכת. כל קונטרולר מערכת נגיש לכל מי שמבקש את הכתובת, ולכן הקוד שלו חייב לבדוק לבד (
ADMIN::is_admin(), טוקן). ראו קונטרולרי מערכת, JSON API של טפסים וטבלאות ו-CSRF, XSS ו-SQLi. - בקשות ניהול אסינכרוניות (טפסים, טבלאות) מצרפות
pmode=empgלכתובת, והפאנלים מחזירים JSON או קטעי HTML.
נקודות חיבור מרוכזות#
| מה רוצים | איפה | מתי רץ |
|---|---|---|
| קוד בכל בקשה, לפני מטמון עמודים | application/includes/init.php | שלב 5 ב-core.php, לפני PAGE::reset |
| קוד בכל בקשת לקוח, רישום אירועים ונכסים | application/includes/*.php | PAGE::load, אחרי הניתוב וההפניות |
| קוד בכל בקשת ניהול | application/admin/includes/*.php | PAGE::load, בצד הניהול |
| אחרי שהמערכת והשפה מוכנות | PAGE::bind("SYSTEM_LOADED", function(){...}) | ממש לפני בחירת הקונטרולר |
| החלפת theme לבקשה | PAGE::set_theme("x") | כל זמן שלפני PAGE::output |
| שמירת העמוד במטמון | cache_engine::cache_all_page($hours) | בקונטרולר, בבקשה שבונה את העמוד |
| תוכן ה-HTML הסופי | PAGE::$parse_page_func = function($html){...} | אחרי השמירה, ובכל hit של מטמון |
| כתובת ידידותית | שורה ב-CRM_seoUrl | ב-ROUTER |
| הפניה | שורה ב-CRM_redirections | אחרי הניתוב, בצד הלקוח |
| כותרת ותיאור | SEO::set("title", ...) | בקונטרולר, לפני החזרת התוכן |
| תגיות ונכסים | PAGE::add_asset, add_meta, טבלת CRM_scripts | בקונטרולר, ב-theme או ב-includes |
אירועים שהליבה יורה: SYSTEM_LOADED (בסוף PAGE::load לפני הקונטרולר), delete.cache (בניקוי מטמון), storage.update ו-words_update. כל ה-API ב-PAGE: נכסים, מטא ואירועים וב-PAGE (רפרנס).
מלכודות לסיכום#
- קובץ קונטרולר בלי שורה ב-
CRM_modulesהוא 404, ושורה שנוספה ישירות למסד לא נראית עד שמנקים את המטמון (7 ימים). ראו העמוד הראשון שלך. PAGE::bindב-init.phpנמחק על ידיPAGE::reset.- כל מתודה ציבורית היא כתובת, כולל
view,system_viewו-wrapperשהקונטרולר יורש, ולכן מתודות עזר צריכות להיותprivateאוprotected. - הקונסטרקטור רץ לפני בדיקת המתודה: אותה בקשה ל-404 יכולה להפעיל את התופעות שלו.
$_GETמוחלף בניתוב; עריכה שלו לפניPAGE::load(למשל ב-init.php) אינה שורדת.- שמות שמורים בניתוב (
p,minify,assets,system,poosh-sw.jsועוד) לא יגיעו למודול שלכם. - תשובת JSON שחוזרת ב-
returnעוברתminify_html; השתמשו ב-echoו-die. - סקריפט inline בתבנית מועבר לסוף העמוד, אלא אם סומן
nominify. - נתיבים יחסיים לתיקיית העבודה (Smarty,
system/js,application/admin/includes) מחייבים להריץ CLI משורש האתר. - דומיין שאינו רשום מקבל
URL ERROR, ובשורת פקודה נבחרת הפלטפורמה הראשונה בטבלה. - שגיאת מסד בזמן ניתוב נראית כ-503, לא 404.
להמשך: ניתוב, קונטרולרים של אתר, קונטרולרי מערכת, ערכות נושא, מטמון עמודים מלא ו-ROUTER ו-MODULE (רפרנס).