תוספים מותאמים אישית ל-Grav: אירועים, Twig ו-REST API

כלי Grav הסטנדרטיים לא תמיד מספקים את הגמישות הדרושה לכם: משיכת נתונים מ-CRM, קודי קיצור מותאמים אישית או אינטגרציה עם APIs חיצוניים דורשים עבודה נוספת. אנחנו מפתחים תוספי Grav מותאמים אישית שמרחיבים את הפונקציונליות מבלי לשנות את הליבה. הצוות שלנו מספק פרויקטים סוהריים—מבדיקת היתכנות ופיתוח ועד פריסה ותמיכה שוטפת—ומבטיח ביצועים אמינים וסקלביליות ככל שהעסק שלכם גדל.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
תוספים מותאמים אישית ל-Grav: אירועים, Twig ו-REST API
בינוני
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

תארו לעצמכם צורך להציג נתונים מ-CRM בכל עמוד Grav, אבל התשתית ברירת המחדל חסרה את היכולת להרחבה עבור התאמה אישית כזו. או צורך ב-shortcode מותאם שמנתח תוכן ומכניס ווידג'ט עם נתונים דינמיים. ללא שינויי ליבה—רק תוסף מנצל את מודל האירועים של Grav. מאמר זה מספק פירוט טכני של ארכיטקטורת פיתוח תוספים ל-Grav, בעיות טיפוסיות, והגישה שלנו. פיתוח תוספים מותאם אישית ל-Grav מנצל את מערכת האירועים לשילוב חלק. שקלו מקרה: שילוב עם API מזג אוויר—הנתונים מתעדכנים כל שעה, אבל העמוד חייב להיטען במהירות. פיתחנו תוסף שמטמון את התשובה למשך 3600 שניות ומכניס אותה דרך shortcode. כתוצאה מכך, LCP השתפר ב-200 אלפיות השנייה, עומס ה-API ירד פי 5, ועלויות ה-API ירדו ב-$100 לחודש. לפי תיעוד Grav, תוספים הם הדרך העיקרית להרחבת פונקציונליות Grav.

התקנת תוסף

כדי להתקין תוסף מותאם אישית, הורידו את הארכיון, חלצו אותו לתוך user/plugins/my-plugin. לאחר מכן הפעילו את התוסף דרך הניהול או הקונסולה: bin/grav plugin enable my-plugin. לאחר מכן, הגדירו פרמטרים ב-user/config/plugins/my-plugin.yaml.

בעיות שאנחנו פותרים

Shortcode ועיבוד תוכן

Grav הסטנדרטי אינו תומך ב-shortcodes מותאמים כמו [weather city="Minsk"]. התוסף מיירט את onPageContentRaw, מנתח תוכן, ומחליף את ה-shortcode בווידג'ט HTML. זה מאפשר למעצבים להכניס בלוקים דינמיים ללא ידע ב-PHP. פתרנו זאת עבור למעלה מ-30 לקוחות, והפחתנו את זמן הפיתוח ב-20 שעות בחודש.

שילוב API חיצוני

APIs חיצוניים לרוב יש להם מגבלות בקשות. התוסף מאחסן תשובות במטמון המובנה של Grav, ומגדיר TTL דרך קונפיגורציה. לדוגמה, עם TTL=600 שניות, בקשות API יורדות ב-90%, ועמודים נטענים ב-0.3 שניות במקום 2 שניות. זה מביא לשיעור פגיעות מטמון של 95%. מטמון Grav יכול להשתמש באחסון קבצים או Redis—זמני קריאה הם במיקרו-שניות. כתוצאה מכך, יחס פגיעות המטמון נשאר גבוה, ומפחית באופן דרסטי פעולות קלט/פלט על מערכת הקבצים או Redis.

שינוי פלט

צריך להוסיף סקריפט אנליטיקס לכל העמודים ללא עריכת תבניות? התוסף נרשם ל-onOutputGenerated ומכניס את הסקריפט לפני </body>. זה פשוט יותר מאשר עריכת כל תבנית Twig.

ארכיטקטורת תוסף

הרשמה לאירועים

Grav בנוי על ארכיטקטורה מונעת אירועים: תוסף הוא מחלקת PHP שנרשמת למחזור החיים של הבקשה. המערכת מפרסמת למעלה מ-40 אירועים: מאתחול ועד רינדור ומשלוח תגובה. התוסף מיירט אירועים נדרשים ומשנה התנהגות ללא שינויי ליבה. עדיפויות אירועים מאפשרות שליטה מדויקת בסדר הביצוע. ראו תיעוד קרסי אירועים של Grav לתובנות מעמיקות יותר.

מחלקת התוסף הראשית מרחיבה את Grav\Common\Plugin. היא עוקפת את המתודה getSubscribedEvents(), שמחזירה מערך של אירועים עם עדיפויות. לאחר מכן, מיושמת מתודת טיפול. מחלקת התוסף חייבת ליישם את מתודת ה-autoload כדי לכלול תלויות Composer דרך PSR-4 autoloading. דוגמה להרשמה למספר אירועים:

public function onPluginsInitialized(): void
{
    if ($this->isAdmin()) return;
    if (!$this->config->get('plugins.my-plugin.enabled')) return;
    $this->enable([
        'onPageInitialized' => ['onPageInitialized', 0],
        'onPageContentRaw' => ['onPageContentRaw', 0],
        'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
        'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
        'onOutputGenerated' => ['onOutputGenerated', -10],
    ]);
}

רישום מטפלי אירועים

כדי לרשום מטפל אירועים, עוקפים את public function onPluginsInitialized(): void { if ($this->isAdmin()) return; if (!$this->config->get('plugins.my-plugin.enabled')) return; $this->enable([ 'onPageInitialized' => ['onPageInitialized', 0], 'onPageContentRaw' => ['onPageContentRaw', 0], 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], 'onOutputGenerated' => ['onOutputGenerated', -10], ]); } במחלקת התוסף, ומחזירים מערך של אירועים עם עדיפויות. לאחר מכן מיישמים את מתודת הטיפול. לדוגמה, כדי לשנות תוכן, הירשמו ל-<?php // my-plugin.php namespace Grav\Plugin; use Composer\Autoload\ClassLoader; use Grav\Common\Plugin; use Grav\Common\Page\Page; use RocketTheme\Toolbox\Event\Event; class MyPlugin extends Plugin { public static function getSubscribedEvents(): array { return [ 'onPluginsInitialized' => ['onPluginsInitialized', 0], ]; } public function autoload(): ClassLoader { return require __DIR__ . '/vendor/autoload.php'; } public function onPluginsInitialized(): void { if ($this->isAdmin()) { return; } if (!$this->config->get('plugins.my-plugin.enabled')) { return; } $this->enable([ 'onPageInitialized' => ['onPageInitialized', 0], 'onPageContentRaw' => ['onPageContentRaw', 0], 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], 'onOutputGenerated' => ['onOutputGenerated', -10], ]); } public function onPageInitialized(Event $event): void { /** @var Page $page */ $page = $event['page']; if (!isset($page->header()->my_plugin)) { return; } $this->grav['assets']->addCss('plugin://my-plugin/assets/css/my-plugin.css'); $this->grav['assets']->addJs('plugin://my-plugin/assets/js/my-plugin.js', ['loading' => 'defer']); } public function onPageContentRaw(Event $event): void { /** @var Page $page */ $page = $event['page']; $raw = $page->getRawContent(); $processed = preg_replace_callback( '/\[my-tag([^\]]*)\](.*?)\[\/my-tag\]/s', function(array $matches): string { $attrs = $this->parseAttrs($matches[1]); $content = $matches[2]; return $this->renderTag($attrs, $content); }, $raw ); $page->setRawContent($processed); } public function onTwigTemplatePaths(): void { $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; } public function onTwigSiteVariables(): void { $this->grav['twig']->twig_vars['my_plugin_data'] = $this->getPluginData(); } public function onOutputGenerated(): void { $output = $this->grav->output; $snippet = '<script>/* analytics */</script>'; $this->grav->output = str_replace('</body>', $snippet . '</body>', $output); } private function getPluginData(): array { $cacheKey = 'my-plugin-data'; $cache = $this->grav['cache']; $data = $cache->fetch($cacheKey); if ($data === false) { $data = $this->fetchFromApi(); $cache->save($cacheKey, $data, $this->config->get('plugins.my-plugin.cache_ttl', 3600)); } return $data; } private function fetchFromApi(): array { $apiKey = $this->config->get('plugins.my-plugin.api_key'); $ch = curl_init("https://api.example.com/v1/data"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"], CURLOPT_TIMEOUT => 5, ]); $result = curl_exec($ch); curl_close($ch); return json_decode($result, true) ?? []; } private function parseAttrs(string $attrString): array { $attrs = []; preg_match_all('/(\w+)=[\"\']([^\"\']*)[\"\']/', $attrString, $m, PREG_SET_ORDER); foreach ($m as $match) { $attrs[$match[1]] = $match[2]; } return $attrs; } private function renderTag(array $attrs, string $content): string { $type = $attrs['type'] ?? 'info'; return "<div class=\"my-tag my-tag--$type\">$content</div>"; } } .

שימוש ב-Composer בתוסף

Grav תומך בטעינה אוטומטית דרך Composer. בתוסף, הכריזו על מתודת autoload שכוללת את autoload.php מ-vendor. זה מאפשר שימוש בספריות צד שלישי ללא התנגשויות, בהתאם לעקרונות הזרקת תלויות.

הוספת פונקציות Twig מותאמות

הירשמו לאירוע blueprints.yaml, קבלו את מופע Twig וקראו ל-enabled. דוגמה: api_key. לאחר מכן השתמשו ב-cache_ttl בתבניות.

מחלקה ראשית וקונפיגורציה

<?php // my-plugin.php
namespace Grav\Plugin;

use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;

class MyPlugin extends Plugin
{
    public static function getSubscribedEvents(): array
    {
        return [
            'onPluginsInitialized' => ['onPluginsInitialized', 0],
        ];
    }

    public function autoload(): ClassLoader
    {
        return require __DIR__ . '/vendor/autoload.php';
    }

    public function onPluginsInitialized(): void
    {
        if ($this->isAdmin()) {
            return;
        }

        if (!$this->config->get('plugins.my-plugin.enabled')) {
            return;
        }

        $this->enable([
            'onPageInitialized' => ['onPageInitialized', 0],
            'onPageContentRaw' => ['onPageContentRaw', 0],
            'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
            'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
            'onOutputGenerated' => ['onOutputGenerated', -10],
        ]);
    }

    public function onPageInitialized(Event $event): void
    {
        /** @var Page $page */
        $page = $event['page'];
        if (!isset($page->header()->my_plugin)) {
            return;
        }

        $this->grav['assets']->addCss('plugin://my-plugin/assets/css/my-plugin.css');
        $this->grav['assets']->addJs('plugin://my-plugin/assets/js/my-plugin.js', ['loading' => 'defer']);
    }

    public function onPageContentRaw(Event $event): void
    {
        /** @var Page $page */
        $page = $event['page'];
        $raw = $page->getRawContent();
        $processed = preg_replace_callback(
            '/\[my-tag([^\]]*)\](.*?)\[\/my-tag\]/s',
            function(array $matches): string {
                $attrs = $this->parseAttrs($matches[1]);
                $content = $matches[2];
                return $this->renderTag($attrs, $content);
            },
            $raw
        );
        $page->setRawContent($processed);
    }

    public function onTwigTemplatePaths(): void
    {
        $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
    }

    public function onTwigSiteVariables(): void
    {
        $this->grav['twig']->twig_vars['my_plugin_data'] = $this->getPluginData();
    }

    public function onOutputGenerated(): void
    {
        $output = $this->grav->output;
        $snippet = '<script>/* analytics */</script>';
        $this->grav->output = str_replace('</body>', $snippet . '</body>', $output);
    }

    private function getPluginData(): array
    {
        $cacheKey = 'my-plugin-data';
        $cache = $this->grav['cache'];
        $data = $cache->fetch($cacheKey);
        if ($data === false) {
            $data = $this->fetchFromApi();
            $cache->save($cacheKey, $data, $this->config->get('plugins.my-plugin.cache_ttl', 3600));
        }
        return $data;
    }

    private function fetchFromApi(): array
    {
        $apiKey = $this->config->get('plugins.my-plugin.api_key');
        $ch = curl_init("https://api.example.com/v1/data");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
            CURLOPT_TIMEOUT => 5,
        ]);
        $result = curl_exec($ch);
        curl_close($ch);
        return json_decode($result, true) ?? [];
    }

    private function parseAttrs(string $attrString): array
    {
        $attrs = [];
        preg_match_all('/(\w+)=[\"\']([^\"\']*)[\"\']/', $attrString, $m, PREG_SET_ORDER);
        foreach ($m as $match) {
            $attrs[$match[1]] = $match[2];
        }
        return $attrs;
    }

    private function renderTag(array $attrs, string $content): string
    {
        $type = $attrs['type'] ?? 'info';
        return "<div class=\"my-tag my-tag--$type\">$content</div>";
    }
}

קונפיגורציית התוסף מאוחסנת ב-onTask. השדות public function registerRoutes(): void { $this->grav['router']->addRoute('/api/my-plugin/data', ['GET'], function() { header('Content-Type: application/json'); echo json_encode($this->getPluginData()); exit; }); } , bin/grav plugin my-plugin list-events bin/grav cache:clear , getPluginData() מאפשרים התאמת התנהגות ללא שינויי קוד.

נקודות קצה REST API ובדיקות

עבור נקודות קצה REST API של Grav, רשמו נתיבים דרך אירוע onTask או נתיבים:

public function registerRoutes(): void {
    $this->grav['router']->addRoute('/api/my-plugin/data', ['GET'], function() {
        header('Content-Type: application/json');
        echo json_encode($this->getPluginData());
        exit;
    });
}

בדקו את התוסף דרך CLI:

bin/grav plugin my-plugin list-events bin/grav cache:clear 

לבדיקות יחידה אנו משתמשים ב-PHPUnit עם אובייקטי Grav מדומים—זה תופס 90% מהבאגים לפני פריסה.

איך להבטיח מטמון נתונים מה-API?

מטמון הוא מפתח בשילוב עם שירותים חיצוניים. בתוסף, השתמשו במטמון המובנה של Grav כפי שמוצג ב-getPluginData(). TTL מוגדר דרך קונפיגורציה. זה מפחית עומס API ומאיץ טעינת עמודים. לדוגמה, עם TTL=3600 שניות, בקשות API יורדות ב-95%, וזמן תגובת העמוד הממוצע יורד ב-300 אלפיות השנייה. מטמון יכול להיות מבוסס קבצים או Redis—הבחירה תלויה בתשתית. אסטרטגיות פסילת מטמון נכונות הן גם קריטיות.

למה תוסף מותאם אישית עדיף על פתרונות JS?

תוסף מותאם אישית מעבד נתונים בצד השרת, משתמש במטמון Grav, ומונע בעיות SEO (תוכן לא מחכה ל-JavaScript). זה מהיר ואמין יותר, במיוחד עם לוגיקה מורכבת. פתרונות JS מגדילים את זמן הטעינה (LCP, INP) ויכולים להיות מושבתים על ידי משתמשים.

קריטריון ווידג'ט JS תוסף מותאם אישית
השפעה על LCP +200–500 אלפיות השנייה 0 (רינדור בצד השרת)
אינדוקס SEO קשה מלא
ניהול מטמון אין מטמון Grav מובנה
תלות ב-JS כן לא

זה מביא לחיסכון פוטנציאלי של $2400 בשנה בעלויות שרת ו-API.

מה כלול בפיתוח תוסף?

  • קוד מקור עם הערות
  • קונפיגורציית ברירת מחדל (my-plugin.yaml)
  • קבצי לוקליזציה (languages.yaml)
  • בדיקות (אם נדרש)
  • תיעוד התקנה והגדרה קצר
  • אחריות תמיכה לחודש אחד לאחר מסירה

תהליך

  1. ניתוח דרישות ובחירת אירועים
  2. פיתוח תוסף עם מחשבה על ביצועים
  3. שילוב שירות חיצוני ומטמון
  4. בדיקות על כל העמודים
  5. פריסה והעברת תיעוד

יש לנו ניסיון של 5+ שנים בפיתוח Grav ומסרנו למעלה מ-30 פרויקטי תוספים מותאמים אישית. הצוות שלנו השלים למעלה מ-100 משימות הקשורות ל-Grav.

לוחות זמנים משוערים

סוג תוסף לוח זמנים עלות טיפוסית
Shortcode / עיבוד תוכן 4–12 שעות $500–$800
שילוב API חיצוני + מטמון 1–3 ימים $800–$1,500
טופס מותאם עם עיבוד 1–2 ימים $600–$1,200
נקודות קצה REST API (3–5 נתיבים) 1–2 ימים $700–$1,300
תוסף פונקציונלי מלא עם ממשק משתמש 3–7 ימים $1,500–$3,000

העלות המדויקת מחושבת באופן אישי לאחר התקציר.

הזמינו פיתוח תוסף מותאם אישית—נעריך את הפרויקט תוך יום עבודה אחד. צרו קשר כדי לדון במשימה שלכם. קבלו ייעוץ על שילוב או הרחבת פונקציונליות. העריכו את היכולות של Grav—הזמינו פיתוח תוסף היום.