קונטרולרים של אתר

איך כותבים קונטרולר ב-application/controllers, רושמים אותו ב-CRM_modules, טוענים מודלים וספריות, ומחזירים HTML, JSON או עמוד שגיאה.

⏱ 7 דק' קריאה 1275 מילים ערוך דף זה ב-GitHub

קונטרולר של אתר הוא קובץ PHP אחד ב-application/controllers/<name>.php עם מחלקה באותו שם, שיורשת מ-wz_controller. כל מתודה ציבורית שלו היא כתובת: /<name> מריץ את index(), ו-/<name>/<method> מריץ את המתודה. הקונטרולר מחזיר מחרוזת HTML, וה-theme עוטף אותה בעמוד.

system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url. קונטרולרים של הליבה עצמה מתוארים ב-קונטרולרי מערכת.

שלושה תנאים כדי שכתובת תגיע לקונטרולר#

  1. קובץ ומחלקה. application/controllers/news.php שמגדיר class news. שם הקובץ והמחלקה זהים לחלוטין (רגישים לאותיות גדולות וקטנות ב-Linux).
  2. שורה פעילה ב-CRM_modules. MODULE::get_html מחפש את moduleName ב-MISC::get_modules_tbl() ודורש active == "1". בלי שורה, או עם active = 0, התשובה היא 404.
  3. מתודה ציבורית. is_callable([$mdl, $page]) חייב להצליח. מתודה פרטית או שלא קיימת מובילה ל-404.
INSERT INTO CRM_modules (moduleName, active) VALUES ('news', 1);

אחרי הוספה ידנית ב-SQL נקו את המטמון של הטבלה: cache_engine::remove("modules");. הטבלה נשמרת תחת modules_lang<מזהה פלטפורמה> ל-7 ימים, והמפתח modules תופס אותה לפי קידומת. כשמוסיפים מודול מפאנל {admin}/Modules הניקוי נעשה אוטומטית.

הפעלה בלחיצה

אם קובץ הקונטרולר קיים אבל אין לו שורה פעילה, ומי שגולש הוא מנהל מחובר, הליבה מציגה במקום העמוד טופס "Click here to activate this module", שמוסיף את השורה דרך {admin}/Modules/insert. גולש רגיל באותו מצב מקבל 404.

קונטרולר מינימלי#

<?php
// application/controllers/news.php
class news extends wz_controller
{
    // GET /news          => index()
    // GET /news/12       => index() עם $_GET["id"] = "12"
    // GET /news/archive  => archive()
    function index()
    {
        PAGE::$cache_this_page = true;          // מטמון עמוד מלא (ראו page-cache)
        PAGE::$cache_this_page_hours = 6;

        $id = (int)($_GET["id"] ?? 0);
        if ($id > 0)
        {
            $row = DB::query("news", ["id" => $id])->get_row();
            if (!$row) return "ERRORPAGE";      // 404
            SEO::set_var("title", $row["title"]);
            return $this->view("news/item", ["row" => $row]);
        }

        SEO::set_var("title", "חדשות");
        $rows = DB::query("news", ["is_active" => "1"], "id DESC", "", "0,20")->get_all();
        return $this->view("news/list", ["rows" => $rows]);
    }

    function archive()
    {
        return $this->view("news/archive");
    }
}

תבנית ה-Smarty של הדוגמה, application/views/news/list.tpl:

<ul class="news-list">
{foreach $rows as $row}
    <li><a href="/news/{$row.id}">{$row.title|escape}</a></li>
{/foreach}
</ul>

מה הקונטרולר מחזיר#

ערך החזרהתוצאה
מחרוזתהפלט של העמוד; עובר ל-wrapper() ואחר כך לתוך ה-theme
"ERRORPAGE"MODULE::errorpage(): 404 (או 503 אם הייתה שגיאת DB), ראו 404, 503 ושגיאות
"REQUIRE_LOGIN"מדפיס סקריפט שמפעיל $('#page').trigger('REQUIRE_LOGIN') בצד הלקוח (דורש jQuery ואלמנט #page)
כל מתודה ציבורית היא נקודת כניסה

גם view(), system_view() ו-wrapper() שירשתם מ-wz_controller הם ציבוריים, ולכן /news/wrapper ו-/news/view מורצים. עזרים פנימיים הגדירו תמיד כ-private או protected, ואל תניחו שמתודה "לא מפורסמת" לא נגישה. בדקו הרשאה בתוך כל מתודה שחושפת מידע.

המחלקה wz_controller#

הקובץ system/collections/MODULE.php מגדיר את הבסיס (bgl_controller הוא כינוי ל-wz_controller, והוא נפוץ בקוד הליבה ובקוד ישן):

חברחתימהמה עושה
$loadbgl_loaderטוען מודלים וספריות, ראו למטה
wrapperfunction wrapper($pageContent)נקרא על התוצאה של כל מתודה; ברירת המחדל מחזירה אותה כמות שהיא. דורסים כדי לעטוף את כל המתודות (למשל JSON)
viewfunction view($tpl, $vars = [])מרנדר את application/views/<tpl>.tpl דרך FILES::html_template ומחזיר מחרוזת
system_viewfunction system_view($tpl, $vars = [])אותו דבר מ-system/views/<tpl>.tpl (תבניות הליבה)
מאפיינים דינמיים__get / __setמאפיין שלא הוגדר נשמר במערך פנימי; כך $this->m_articles עובד אחרי load->model

view() מקבל נתיב בלי סיומת ובלי / בהתחלה. הרינדור עצמו מתואר ב-תבניות Smarty.

$this->load: מודלים וספריות#

function index()
{
    $this->load->model("m_articles");     // application/models/m_articles.php, ויוצר $this->m_articles
    $this->load->library("mail_tpl");      // system/libraries/mail_tpl.php (require_once בלבד)

    $rows = $this->m_articles->latest(10);
    return $this->view("news/list", ["rows" => $rows]);
}
מתודההתנהגות
model($path)מכניס CONFIG::$models_folder . "/" . $path . ".php" (ברירת מחדל application/models), ואם הקובץ חסר: die("ERROR: model $path is not exists"). אחרי הטעינה יוצר new <basename($path)> ושם אותו ב-$this-><basename>. אפשר תת-תיקיות: model("trg/m_banner") יוצר $this->m_banner
library($path)אם system/libraries/<path> הוא תיקייה, כולל את כל קבצי ה-PHP שבה (FILES::include_dir). אחרת require_once של system/libraries/<path>.php. לא יוצר מופע: משתמשים בקלאס ישירות (mail_tpl::...)

מודל הוא קלאס רגיל, בדרך כלל extends bgl_model (כינוי ל-wz_model, שיורש מ-wz_controller ומוסיף את static merge(&$settings, $m_settings) למיזוג הגדרות ברירת מחדל):

<?php
// application/models/m_articles.php
class m_articles extends bgl_model
{
    function latest($count)
    {
        return DB::query("articles", ["is_active" => "1"], "id DESC", "", "0," . (int)$count)->get_all();
    }
}
שם הקובץ הוא שם המחלקה
model("m_articles") מניח מחלקה בשם m_articles. אם שם הקובץ שונה משם המחלקה, new ייכשל. ושימו לב: שום מודל או ספרייה לא נטענים אוטומטית, כל library() ו-model() נקראים במפורש.

נקודות סיום בלי עמוד: JSON, הפניה, קובץ#

הקונבנציה בליבה: להגדיר $_GET["pmode"] = "empg" בתוך המתודה, ואז PAGE::output() מדלג על ה-theme ועל ה-<html> ומדפיס רק את מה שהוחזר.

<?php
// application/controllers/news_api.php
class news_api extends wz_controller
{
    // GET /news_api/latest
    function latest()
    {
        $_GET["pmode"] = "empg";
        header("Content-Type: application/json; charset=utf-8");

        $rows = DB::query("news", ["is_active" => "1"], "id DESC", "", "0,5")->get_all();
        return json_encode($rows ?: [], JSON_UNESCAPED_UNICODE);
    }

    // GET /news_api/go/12
    function go()
    {
        $_GET["pmode"] = "empg";
        $id = (int)($_GET["id"] ?? 0);
        header("Location: /news/" . $id);
        exit;
    }
}

לפלט אחיד לכל המתודות, דורסים את wrapper(): analytics של הליבה מגדיר pmode=empg ועוטף כל תוצאה ב-json_encode(["data" => $content]).

מטמון עמודים ופלט שאינו עמוד

אם PAGE::$cache_this_page דלוק, התשובה נשמרת כקובץ HTML בכתובת הזו. בנתיבי JSON ופעולות (שגם נקראים ב-POST) השאירו אותו כבוי. ראו מטמון עמודים מלא.

קוד שרץ בכל בקשה: application/includes#

קבצי *.php ב-application/includes נכללים (FILES::include_dir, לפי סדר שם הקובץ, פעם אחת לבקשה) בכל בקשת לקוח, אחרי שהניתוב נקבע ולפני שהקונטרולר רץ. זה המקום לרשום אירועים (PAGE::bind), להוסיף נכסים גלובליים ולהגדיר PAGE::$parse_page_func. ראו PAGE: נכסים, מטא ואירועים.

מלכודות#

שימו לב
/p/<slug> ושמות שמורים הכתובת /p/<slug> נתפסת לפני כל מודול (דפי "דף חדש"), ולכן קונטרולר בשם p לא יקבל כתובות כאלה. מודול ששמו system לא נגיש כמודול (הקידומת system/ שייכת לקונטרולרי מערכת), וכתובות כמו minify/<id>.js, assets/<קובץ קיים>, tag_manager.js, pg.js ו-<שם>.scss.css נתפסות לפני כל מודול.

אל תעתיקו קוד ישן שמשתמש ב-
DB::insert DB::insert(...) לא קיים (שגיאה קטלנית). הכנסה היא DB::update("table")->set_var("col", $val)->insert(), ועדכון הוא ->update($id). שמות טבלה ב-builder ללא CRM_; ב-SQL גולמי (DB::get_all, DB::sql) חייבים את הקידומת המלאה, וכל קלט חיצוני עובר ב-DB::escape. ראו שאילתות.

ראו גם#

מצאתם טעות או חוסר? תקנו את הדף או פתחו Issue בריפו. התיעוד נכתב מתוך הקוד של ליבה 5.0.115.