Аргос CMS: Пътят към първият екстеншън

Pok4

Member
Joined
Feb 8, 2022
Messages
218
Reaction score
2
Здравейте на всички съфорумци!
В тази тема ще разясня как да си направите екстеншън към версия 9.5 или по-нова.

Първото, което трябва да направим е да се запознаем с функциите, които можем да ползваме, евентите, в които ще инжектираме код и структурата на екстеншъните.
А също така и със зависимостите, които са ни необходими.

Всички функции, които може да ползвате се намират тук: https://argoscms.xyz/readme/#funcs
Описани са, може да ги намерите и в includes/functions.php. Всичко е на английски, но може да ползвате и гугъл транслейт.
Всички тези функции, можем да ползваме във вашият първи екстеншън.

Всички евенти, в които можем да инжектираме код от екстеншъните, можем да намерим тук:
https://argoscms.xyz/readme/#events

Отново всичко е на английски, но е важно да ги знаете, тъй като без тях не може да направите работещ екстеншън.
Те се намират в includes/events.php
Може да създадете къстъм евенти, различни от тези във _functions.php файла на темплейта ви, но за това ще пишем друг път.

Зависимостите, които можем да ползваме са в App/Entity/Dependencies.php.
Тях можем да викаме в екстеншъните с:
Code:
$this->fastcache - за ползване на кеша
$this->db - за свързване с дб
$this->mdetect - за детекция на таблети и смартфони и т.н..
Всички тях може да ги видите в Dependencies.php, там са и phpbb методите, които също може да ползвате във вашия първи екстеншън.

Аз ви препоръчвам да си отворите табове с функции, евенти и Dependencies.php файла и да продължим напред.

Структурата:
Всички екстеншъни се намират в папка ext/, там може да направите папка с вашия никнейм, но не ползвайте специални символи, може да ползвате _ черта, ако искате.
Аз ще направя папка pok4 и в нея трябва да създадем още една папка, а именно с името на екстеншъна.
Примерно: all_users (ще изкарваме потребителите от форума чрез странициране)
След това в тази новосъздадена папка, трябва да създадем, още 2 файла, а именно:
ext.php - в който ще е нашия код за екстеншъна
sql.php - файла, който съдържа sql таблиците, които ще се изпълняват при пускане/спиране на екстеншъна. В моите екстеншъни почти никога не слагам $drop (при изключване), защото ме е страх, да не изключа и да се изтрие важна информация, но ако екстеншъна ви е малък, може да си дропвате таблиците или други sql свързани команди.

След като вече сме готови, нека отворим sql.php и да разположим нашия код:
Code:
<?php
if(count(get_included_files()) == 1) exit("Direct access not permitted."); //Don't edit
 
$ext_version = '1.0';

$custom_page_name = "all_users";
$custom_page_title = "All Users";

//When ext is ON, this runs:
$sql_insert = "INSERT INTO ".$this->argos_db_prefix."pages (page_name,page_title,menu_type,type) VALUES('".$custom_page_name."','".$custom_page_title."','menu','ext');";

//When ext is OFF, this runs:
$sql_drop = "DELETE FROM ".$this->argos_db_prefix."pages WHERE page_name='".$custom_page_name."';";
Както виждате имаме най-горе проверка за превенция на директен достъп до файла, след него имаме версия на екстеншъна, а по надолу дефинираме чрез променливи името на страницата и нейното заглавие. В случая имаме your-site.com/pages/all_users и заглавие на страницата All users което ще се вижда в браузъра.
По надолу имаме $sql_insert който се изпълнява при пускане на екстеншъна и $sql_drop, който се изпълнява при спиране на екстеншъна.
Може да имате много и различни sql команди в тези две променливи, нищо не ви спира.
В случая по горе инсертваме информация в pages sql таблицата нов запис с all_users страницата и тайтъла, а по долу я дропваме при спиране на екстеншъна, за да не седи в таблицата, тъй като екстеншъна не е пуснат.

Да преминем към ext.php - нашето ядро.
Екстеншъните представляват класове, които се викат от беискотролера при поискване с маджик метода в php __get(). Това е след версия 9.4, където има много важни промени свързани с производителността на системата.
И така, ако трябва да съм честен, винаги кръщавайте името на класа на вашия екстеншън, с това, което сте кръстили папката му.
Тоест, ако папката е all_users, кръстете класа all_users, ето как:
Code:
<?php

/**
 * ../../ext/pok4/all_users/ext.php
 *
 * @package default
 */

if (count(get_included_files()) == 1) exit("Direct access not permitted."); //Don't edit
//Your Extension Script
class all_users extends \App\Controllers\BaseController
{
    public function __construct()
    {
        parent::__construct();
    }

    public function custom_page()
    {

        return 1;
    }

    public function load()
    {
        if (str_contains(request_uri(), '/pages/all_users')) {
            add_event('core_event_inside_custom_menu', [$this->custom_page()]);
        }
    }
};

$load_ext = new all_users;
$load_ext->load();

Тук имаме прост код, който е вид завършен екстеншън. Какво прави?
Ако сте го пуснали, трябва като отворите your-site.com/pages/all_users трябва да видите нова страница, при което в лявото меню трябва да видите 1 (цифрата 1, която сме я върнали custom_page() метода)
Как това става ?
Code:
if (str_contains(request_uri(), '/pages/all_users')) {
    add_event('core_event_inside_custom_menu', [$this->custom_page()]);
}
Цялата магия се крие в евентите, в които вкарваме/инжектиране кода.
В случая със str_contains засичаме дали в url имаме /pages/all_users и ако наистина се съдържа, след това в евента core_event_inside_custom_menu инжектираме всичко от custom_page() метода, а в случая там нямаме нищо, освен върнато едно число, а именно 1.

Всичко е мега лесно и ако разбирате повече от php, може набързо да си създадете набор от екстеншъни и да направите вашия сайт уникален!

Малко повече за евентите и ще преминем нататък
Всеки core_* евент има реципрочен template_* евент. Тоест, темплейтните евенти са предварително разположени в темплейтите, а core_ евентите се използват от екстеншъните, за да се инжектира кода в самите тях.
Още малко да допълня за този евент:
core_event_inside_custom_menu
Това е евент за инжекция на код в страница, която има 2 менюта и се определя от sql заявката в sql.php
По нагоре в sql.php имаме:
Code:
//When ext is ON, this runs:
$sql_insert = "INSERT INTO ".$this->argos_db_prefix."pages (page_name,page_title,menu_type,type) VALUES('".$custom_page_name."','".$custom_page_title."','menu','ext');";
Виждате тук, че на menu_type имаме 'menu', съответно за това отговаря този евент core_event_inside_custom_menu
Ако обаче искаме да имаме страница без менюта, то можем да кажем така:
Code:
//When ext is ON, this runs:
$sql_insert = "INSERT INTO ".$this->argos_db_prefix."pages (page_name,page_title,menu_type,type) VALUES('".$custom_page_name."','".$custom_page_title."','wmenu','ext');";
И така инжектираме кода в този евент:
Code:
core_event_inside_custom_w_menu
, който отговаря за страниците без менюта.
Случвало ми се е клиенти да ползват темплейти без странични менюта и всичко се прави според вашия темплейт и нужди.

Да направим и екстеншъна, за който говорихме още в самото начало, а именно да изкараме последните регистрирани от форума чрез странициране:
Code:
<?php

/**
 * ../../ext/pok4/all_users/ext.php
 *
 * @package default
 */


if (count(get_included_files()) == 1) exit("Direct access not permitted."); //Don't edit

//Your Extension Script
class all_users extends \App\Controllers\BaseController
{
    public function __construct()
    {
        parent::__construct();
    }

    public function custom_page()
    {

        $results    = $this->db->query("SELECT COUNT(`user_id`) FROM `" . $this->forum_db . "`." . $this->forum_db_prefix . "_users WHERE user_id>1 AND user_email <> ''")->fetchColumn();

        $pagination = pagination($results, [
            'per_page'  => 15, //how many results per page
            'per_side'  => 3,
            'get_name'  => 'page'
        ]);

        $get_results = $this->db->query("SELECT * FROM `" . $this->forum_db . "`." . $this->forum_db_prefix . "_users WHERE user_id>1 AND user_email <> ''  order by user_id DESC LIMIT {$pagination['limit']['first']}, {$pagination['limit']['second']}");

        $print = ""; //globalize
        if ($get_results->rowCount() > 0) {

            $all_users_array = []; //globalize
            while ($row = $get_results->fetch(PDO::FETCH_ASSOC)) {
                //check status by user id
                $user_id = $row['user_id'];
                $mysql_get = $this->db->query("select session_user_id from `" . $this->forum_db . "`." . $this->forum_db_prefix . "_sessions s INNER JOIN `" . $this->forum_db . "`." . $this->forum_db_prefix . "_users u ON u.user_id = s.session_user_id WHERE session_user_id = '$user_id' AND (session_time + 300) > UNIX_TIMESTAMP(NOW())");
                $fetchrow = $mysql_get->fetch(PDO::FETCH_ASSOC);

                $status = $fetchrow['session_user_id'] ?? '';
                if (!empty($status)) {
                    $status_icon = url() . '/ext/pok4/all_users/img/online-icon.png';
                } else {
                    $status_icon = url() . '/ext/pok4/all_users/img/online-red-icon.png';
                }
                $user_color = get_user_color_by_id($user_id);
                $regdate = date("d.m.y h:i:s", $row['user_regdate']);
                $last_visit = date("d.m.y h:i:s", $row['user_lastvisit']);
                $user_posts = $row['user_posts'];

                $all_users_array[] = ['all_users_user_id' => $user_id, 'all_users_user_color' => $user_color, 'all_users_user_username' => $row['username'], 'all_users_status_icon' => $status_icon, 'all_users_reg_date' => $regdate, 'all_users_last_visit' => $last_visit, 'all_users_user_posts' => $user_posts];
            }

            return $this->m->render(file_get_contents("ext/pok4/all_users/template/all_users.html"), [
                //variables
                'all_users_table' => $all_users_array,
                'ext_all_users_username' => $this->lang['ext_all_users_username'],
                'ext_all_users_regdate' => $this->lang['ext_all_users_regdate'],
                'ext_all_users_lastvisit' => $this->lang['ext_all_users_lastvisit'],
                'ext_all_users_posts' => $this->lang['ext_all_users_posts'],
                'ext_all_users_base_f_url' => base_forum_url(),
                'ext_all_users_pagination' => $pagination['output'],
            ]);
        }
    }

    public function load()
    {

        if (str_contains(request_uri(), '/pages/all_users')) {
            add_event('core_event_inside_custom_menu', [$this->custom_page()]);
        }
    }
};

$load_ext = new all_users;
$load_ext->load();

Виждате тук в custom_page() метода сме развили вече код, който използва sql заявка, чрез методите от Dependencies.php и по надолу имаме вече while цикъл и въобще неща, които трябва да ги знае и начинаещ според мен.
В този екстеншън ползваме и ланг файлове, а именно превеждаме го.
Ланг файловете се намират в папка lang/ в папката на екстеншъна и се зареждат автоматично при пускането му.
За тях по надолу, а сега малко обяснения за това:
Code:
return $this->m->render(file_get_contents("ext/pok4/all_users/template/all_users.html"), [
    //variables
    'all_users_table' => $all_users_array,
    'ext_all_users_username' => $this->lang['ext_all_users_username'],
    'ext_all_users_regdate' => $this->lang['ext_all_users_regdate'],
    'ext_all_users_lastvisit' => $this->lang['ext_all_users_lastvisit'],
    'ext_all_users_posts' => $this->lang['ext_all_users_posts'],
    'ext_all_users_base_f_url' => base_forum_url(),
    'ext_all_users_pagination' => $pagination['output'],
]);
Това както виждате връща всичко, което ще се изпринти или види от потребителя на страницата.
В случая Аргос ползва темплейтната система Mustache и ние се задължаваме и ползваме при създаването на добре работещи екстеншъни.
В случая имаме:
Code:
return $this->m->render(file_get_contents("ext/pok4/all_users/template/all_users.html"), [
Това ни е темплейтния файл, върху който ще се наслои всичко по-надолу в кода, той ще слухти за масива и ланг дефинициите през mustache темплейтния енджин.
Ето и как изглежда нашия темплейтен файл:
Code:
<div class="table-responsive">
    <table class="table">
        <tr>
            <td>{{ext_all_users_username}}</td>
            <td>{{ext_all_users_regdate}}</td>
            <td>{{ext_all_users_lastvisit}}</td>
            <td>{{ext_all_users_posts}}</td>
        </tr>
        {{#all_users_table}}
        <tr>
            <td><a style="color:#{{all_users_user_color}} !important" href="{{ext_all_users_base_f_url}}memberlist.php?mode=viewprofile&amp;u={{all_users_user_id}}" target="_blank">{{all_users_user_username}}</a> <img src="{{all_users_status_icon}}" alt="status icon"/></td>
            <td>{{all_users_reg_date}}</td>
            <td>{{all_users_last_visit}}</td>
            <td>{{all_users_user_posts}}</td>
        </tr>
        {{/all_users_table}}
    </table>
</div>

{{&ext_all_users_pagination}}
Както виждате всичко се вика с къдрави скоби и е мега лесно за разбиране, само да се научите как правилно да колекционирате и пращате информацията от екстеншъна към темплейтния файл.
Долу имаме {{&ext_all_users_pagination}} - това & отпред е с цел да се поддържа html и да не го филтрира през htmlspecialchars, тъй като в него е цялото ни странициране с html атрибутите.

Mustache се разработва все още и има собствена github страница на адрес:https://github.com/bobthecow/mustache.php
На този адрес има примери и документация, а ако нещо не ви е ясно може да пишете там или в темата тук, аз ще ви помогна с вашите проблеми.

Остана само да обясня за ланг файловете, които се намират в lang/ папката и не са задължителни, но все пак препоръчителни.
В lang/ папката трябва да имате ланг файловете, които се поддържат от системата (препоръчително) или само en.php, който е по дефолт и се чете само той, ако няма други.
Ето пример за en.php към този екстеншън, който разглеждаме тук:
Code:
<?php
$ext_language = [
    'ext_all_users_username'=>'Nickname',
    'ext_all_users_posts'=>'Posts',
    'ext_all_users_regdate'=>'Regdate',
    'ext_all_users_lastvisit'=>'Last visit',
];
Виждате съвсем просто е, това е масив с определените ключове от ext.php и срещу тях имаме преводите.
За bg.php би изглеждало така:
Code:
<?php
$ext_language = [
    'ext_all_users_username'=>'Никнейм',
    'ext_all_users_posts'=>'Постове',
    'ext_all_users_regdate'=>'Дата на регистрация',
    'ext_all_users_lastvisit'=>'Последно засечен',
];
Мега лесно!
Може да си добавяте още ключове с преводи и да ги ползвате вътре в екстеншъна.
Аргос е преведен на български (bg.php), английски (en.php), френски (fr.php), испански (es.php) и руски (ru.php)
Естествено в бъдеще може да има и нови преводи, зависи колко време имам, но искам да я преведа на румънски и немски, съответно ro.php и de.php.

Това е общо взето за нашия първи екстеншън, но няма да спрем дотук. В следващия ми пост, ще разгледаме още по сложен екстеншън с ползване на аякс и модели.
Екстеншъните могат да викат класове, да ползват ajax() метод или да ползват модели.
Имам екстеншън, който е с 4500 реда код наблъскан и работи, но естествено може да се раздели на парчета (няколко екстеншъна, които да работят като 1 или да се раздели на класове, които се викат, подобно на моделите)
Но за това по-късно в нова тема или пост.
Интересувам се от хора, които имат въпроси или проблеми и искат да се научат как се пишат екстеншъни.
Пишете в темата, ако срещате проблеми с вашия пръв екстеншън !
 
Last edited: