Components Guide

Learn the DAO, Service, Controller, and Model patterns for building features in Scriptlog.

Home / Documentation / Components

Architecture Pattern

Scriptlog uses a layered architecture for clean separation of concerns:

Request

HTTP request from user

Controller

Handles HTTP logic

Service

Business logic & validation

DAO

Database operations

Database

MySQL/MariaDB

DAO

Data Access Layer - handles all database operations

Service

Business logic, validation, and orchestration

Controller

HTTP request handling, calls services

Model

Data entities and transformation

DAO (Data Access Object)

DAO Pattern Guidelines

Guideline Description
Single Responsibility Each DAO handles one database table
Prepared Statements Use for all queries to prevent SQL injection
Return Format Return associative arrays or objects
Error Handling Handle exceptions gracefully

Example: PostDao

PHP lib/dao/PostDao.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");

class PostDao extends Dao
{
    public function __construct()
    {
        parent::__construct();
    }

    public function findPosts($orderBy = 'ID', $author = null, $onlyPublished = true)
    {
        $allowedColumns = ['ID', 'post_date', 'post_title', 'post_modified'];
        $sortColumn = in_array($orderBy, $allowedColumns) ? $orderBy : 'ID';

        $sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date,
                       p.post_modified, p.post_title, p.post_slug,
                       p.post_content, p.post_status, p.post_visibility,
                       p.post_password, p.post_tags, p.post_headlines,
                       p.post_type, p.post_locale, p.passphrase, u.user_login
                FROM tbl_posts AS p
                INNER JOIN tbl_users AS u ON p.post_author = u.ID
                WHERE p.post_type = 'blog'";

        $data = [];

        if (!is_null($author)) {
            $sql .= " AND p.post_author = ?";
            $data[] = (int)$author;
        }

        if ($onlyPublished) {
            $sql .= " AND p.post_status = 'publish' AND p.post_visibility = 'public'";
        }

        $sql .= " ORDER BY p.$sortColumn DESC";

        $this->setSQL($sql);
        $posts = $this->findAll($data);
        return (empty($posts)) ? [] : $posts;
    }

    public function findPost($id, $sanitize, $author = null, $onlyPublished = true)
    {
        $idsanitized = $this->filteringId($sanitize, $id, 'sql');

        $sql = "SELECT ID, media_id, post_author, post_date, post_modified,
                       post_title, post_slug, post_content, post_summary,
                       post_status, post_visibility, post_password, post_tags,
                       post_headlines, post_locale, comment_status, passphrase
                FROM tbl_posts
                WHERE ID = ? AND post_type = 'blog'";

        $data = [$idsanitized];

        if (!is_null($author)) {
            $sql .= " AND post_author = ?";
            $data[] = (int)$author;
        }

        if ($onlyPublished) {
            $sql .= " AND post_status = 'publish' AND post_visibility = 'public'";
        }

        $this->setSQL($sql);
        $postDetail = $this->findRow($data);
        return (empty($postDetail)) ? false : $postDetail;
    }

    public function createPost($bind, $topicId)
    {
        $this->setSQL("SET SQL_MODE='ALLOW_INVALID_DATE'");
        $this->create("tbl_posts", [
            'media_id' => $bind['media_id'] ?? null,
            'post_author' => $bind['post_author'],
            'post_date' => $bind['post_date'],
            'post_title' => $bind['post_title'],
            'post_slug' => $bind['post_slug'],
            'post_content' => $bind['post_content'],
            'post_summary' => $bind['post_summary'],
            'post_status' => $bind['post_status'],
            'post_visibility' => $bind['post_visibility'],
            'post_password' => $bind['post_password'],
            'post_tags' => $bind['post_tags'],
            'post_headlines' => $bind['post_headlines'],
            'post_locale' => $bind['post_locale'] ?? 'en',
            'comment_status' => $bind['comment_status'],
            'passphrase' => $bind['passphrase']
        ]);

        $postId = $this->lastId();

        if ((is_array($topicId)) && (!empty($postId))) {
            foreach ($_POST['catID'] as $topicId) {
                $this->create("tbl_post_topic", [
                    'post_id' => $postId,
                    'topic_id' => $topicId
                ]);
            }
        } else {
            $this->create("tbl_post_topic", [
                'post_id' => $postId,
                'topic_id' => $topicId
            ]);
        }

        return $postId;
    }

    public function updatePost($sanitize, $bind, $ID, $topicId)
    {
        $cleanId = $this->filteringId($sanitize, $ID, 'sql');

        try {
            $this->callTransaction();
            $this->modify("tbl_posts", [
                'post_author' => $bind['post_author'],
                'post_modified' => $bind['post_modified'],
                'post_title' => $bind['post_title'],
                'post_slug' => $bind['post_slug'],
                'post_content' => $bind['post_content'],
                'post_summary' => $bind['post_summary'],
                'post_status' => $bind['post_status'],
                'post_visibility' => $bind['post_visibility'],
                'post_password' => $bind['post_password'],
                'post_tags' => $bind['post_tags'],
                'post_headlines' => $bind['post_headlines'],
                'post_locale' => $bind['post_locale'] ?? 'en',
                'comment_status' => $bind['comment_status'],
                'passphrase' => $bind['passphrase']
            ], ['ID' => (int)$cleanId]);

            $this->deleteRecord("tbl_post_topic", ['post_id' => $cleanId]);

            if ((is_array($topicId)) && (isset($_POST['catID']))) {
                foreach ($_POST['catID'] as $topicId) {
                    $this->create("tbl_post_topic", [
                        'post_id' => $cleanId,
                        'topic_id' => $topicId
                    ]);
                }
            }

            $this->callCommit();
        } catch (\Throwable $th) {
            $this->callRollBack();
            $this->error = LogError::exceptionHandler($th);
        }
    }

    public function deletePost($id, $sanitize)
    {
        $cleanId = $this->filteringId($sanitize, $id, 'sql');
        $this->deleteRecord("tbl_posts", ['ID' => $cleanId]);
    }

    public function anonymizePostAuthor($authorId)
    {
        $sql = "UPDATE tbl_posts SET post_author = ? WHERE post_author = ?";
        $this->setSQL($sql);
        $this->dbc->dbQuery($sql, [1, (int)$authorId]);
        return true;
    }

    public function checkPostId($id, $sanitizing)
    {
        $sql = "SELECT ID FROM tbl_posts WHERE ID = ? AND post_type = 'blog'";
        $idsanitized = $this->filteringId($sanitizing, $id, 'sql');
        $this->setSQL($sql);
        return $this->checkCountValue([$idsanitized]) > 0;
    }

    public function dropDownPostStatus($selected = "")
    {
        $posts_status = ['publish' => 'Publish', 'draft' => 'Draft'];
        $this->selected = $selected;
        return dropdown('post_status', $posts_status, $this->selected);
    }

    public function dropDownCommentStatus($selected = "")
    {
        $comment_status = ['open' => 'Open', 'closed' => 'Closed'];
        $this->selected = $selected;
        return dropdown('comment_status', $comment_status, $this->selected);
    }

    public function dropDownVisibility($selected = null, $postId = null)
    {
        // Returns HTML select with public/private/protected options
        // Includes password field for protected posts
    }

    public function dropDownLocale($selected = "")
    {
        $locales = [
            'en' => 'English', 'es' => 'Spanish', 'fr' => 'French',
            'de' => 'German', 'zh' => 'Chinese', 'ar' => 'Arabic', ...
        ];
        $this->selected = $selected;
        return dropdown('post_locale', $locales, $this->selected);
    }

    public function totalPostRecords(array $data = []): ?int
    {
        if (!empty($data)) {
            $sql = "SELECT ID FROM tbl_posts WHERE post_author = ? AND post_type = 'blog'";
        } else {
            $sql = "SELECT ID FROM tbl_posts WHERE post_type = 'blog'";
        }
        $this->setSQL($sql);
        return $this->checkCountValue($data) ?? 0;
    }
}

Service Layer

Service Layer Guidelines

Principle Description
Business Logic Services contain business logic
Validation Services validate input
Data Access Services call DAOs
Composition Services can call other services

Example: PostService

PHP lib/service/PostService.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");

class PostService
{
    private $postId;
    private $post_image;
    private $author;
    private $post_date;
    private $post_modified;
    private $title;
    private $slug;
    private $content;
    private $meta_desc;
    private $post_status;
    private $post_visibility;
    private $post_password;
    private $post_headlines;
    private $comment_status;
    private $passphrase;
    private $topics;
    private $tags;
    private $post_locale;
    private $postDao;
    private $validator;
    private $sanitizer;

    public function __construct(PostDao $postDao, FormValidator $validator, Sanitize $sanitizer)
    {
        $this->postDao = $postDao;
        $this->validator = $validator;
        $this->sanitizer = $sanitizer;
    }

    // Setter methods for post properties
    public function setPostId($postId) { $this->postId = $postId; }
    public function setPostImage($post_image) { $this->post_image = $post_image; }
    public function setPostAuthor($author) { $this->author = $author; }
    public function setPostDate($date_created) { $this->post_date = $date_created; }
    public function setPostModified($date_modified) { $this->post_modified = $date_modified; }
    public function setPostTitle($title) { $this->title = prevent_injection($title); }
    public function setPostSlug($slug) { $this->slug = make_slug($slug); }
    public function setPostContent($content) { $this->content = purify_dirty_html($content); }
    public function setMetaDesc($meta_desc) { $this->meta_desc = prevent_injection($meta_desc); }
    public function setPublish($post_status) { $this->post_status = $post_status; }
    public function setVisibility($post_visibility) { $this->post_visibility = $post_visibility; }
    public function setProtected($post_password) { $this->post_password = $post_password; }
    public function setHeadlines($post_headlines) { $this->post_headlines = $post_headlines; }
    public function setComment($comment_status) { $this->comment_status = $comment_status; }
    public function setPassPhrase($passphrase) { $this->passphrase = md5(app_key() . $passphrase); }
    public function setTopics($topics) { $this->topics = $topics; }
    public function setPostTags($tags) { $this->tags = $tags; }
    public function setPostLocale($post_locale) { $this->post_locale = sanitize_locale($post_locale); }

    // Retrieve posts
    public function grabPosts($orderBy = 'ID', $author = null)
    {
        return $this->postDao->findPosts($orderBy, $author, false);
    }

    public function grabPost($postId)
    {
        return $this->postDao->findPost($postId, $this->sanitizer, null, false);
    }

    // Create new post
    public function addPost()
    {
        $category = new TopicDao();
        $this->validator->sanitize($this->author, 'int');
        $this->validator->sanitize($this->post_image, 'int');
        $this->validator->sanitize($this->title, 'string');

        // Create "Uncategorized" topic if no topic selected
        if ($this->topics == 0) {
            $categoryId = $category->createTopic([
                'topic_title' => 'Uncategorized',
                'topic_slug' => 'uncategorized'
            ]);
            $getCategory = $category->findTopicById($categoryId, $this->sanitizer, PDO::FETCH_ASSOC);
            $topic_id = isset($getCategory['ID']) ? abs((int)$getCategory['ID']) : 0;
        } else {
            $topic_id = $this->topics;
        }

        $new_post = [
            'media_id' => $this->post_image,
            'post_author' => $this->author,
            'post_date' => $this->post_date,
            'post_title' => $this->title,
            'post_slug' => $this->slug,
            'post_content' => $this->content,
            'post_summary' => $this->meta_desc,
            'post_status' => $this->post_status,
            'post_visibility' => $this->post_visibility,
            'post_password' => $this->post_password,
            'post_tags' => $this->tags,
            'post_headlines' => $this->post_headlines,
            'post_locale' => $this->post_locale ?? 'en',
            'comment_status' => $this->comment_status,
            'passphrase' => $this->passphrase
        ];

        return $this->postDao->createPost($new_post, $topic_id);
    }

    // Update existing post
    public function modifyPost()
    {
        $this->validator->sanitize($this->postId, 'int');
        $this->validator->sanitize($this->author, 'int');
        $this->validator->sanitize($this->post_image, 'int');
        $this->validator->sanitize($this->title, 'string');

        $post_data = [
            'post_author' => $this->author,
            'post_modified' => $this->post_modified,
            'post_title' => $this->title,
            'post_slug' => $this->slug,
            'post_content' => $this->content,
            'post_summary' => $this->meta_desc,
            'post_status' => $this->post_status,
            'post_visibility' => $this->post_visibility,
            'post_password' => $this->post_password,
            'post_tags' => $this->tags,
            'post_headlines' => $this->post_headlines,
            'post_locale' => $this->post_locale ?? 'en',
            'comment_status' => $this->comment_status,
            'passphrase' => $this->passphrase
        ];

        if (!empty($this->post_image)) {
            $post_data['media_id'] = $this->post_image;
        }

        return $this->postDao->updatePost($this->sanitizer, $post_data, $this->postId, $this->topics);
    }

    // Delete post with media cleanup
    public function removePost()
    {
        $this->validator->sanitize($this->postId, 'int');

        if (!$data_post = $this->postDao->findPost($this->postId, $this->sanitizer)) {
            $_SESSION['error'] = "postNotFound";
            direct_page('index.php?load=posts&error=postNotFound', 404);
            return false;
        }

        // Delete associated media files
        if ($media_id = $data_post['media_id'] ?? 0) {
            $medialib = new MediaDao();
            $media_data = $medialib->findMediaBlog((int)$media_id);
            // ... delete media files (large_, medium_, small_ variants)
            $medialib->deleteMedia((int)$media_id, $this->sanitizer);
        }

        return $this->postDao->deletePost($this->postId, $this->sanitizer);
    }

    // Dropdown helpers
    public function postStatusDropDown($selected = "") { return $this->postDao->dropDownPostStatus($selected); }
    public function commentStatusDropDown($selected = "") { return $this->postDao->dropDownCommentStatus($selected); }
    public function visibilityDropDown($selected = "") { return $this->postDao->dropDownVisibility($selected); }
    public function localeDropDown($selected = "") { return $this->postDao->dropDownLocale($selected); }

    // Author utilities
    public function postAuthorId() { return Session::getInstance()->scriptlog_session_id; }
    public function postAuthorLevel() { return user_privilege(); }
    public function totalPosts(array $data = []): ?int { return $this->postDao->totalPostRecords($data); }
}

Controller

Controller Guidelines

Guideline Description
HTTP Handling Controllers handle HTTP requests
Service Calls Controllers call services
Response Format Controllers return views or JSON
Thin Design Keep controllers thin, move logic to services

Example: PostController

PHP lib/controller/PostController.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");

class PostController extends BaseApp
{
    private $view;
    private $postService;

    public function __construct(PostService $postService)
    {
        $this->postService = $postService;
    }

    // List all posts
    public function listItems()
    {
        $this->setView('all-posts');
        $this->setPageTitle('Posts');
        
        if ($this->postService->postAuthorLevel() == 'administrator') {
            $this->view->set('postsTotal', $this->postService->totalPosts());
            $this->view->set('posts', $this->postService->grabPosts());
        } else {
            $this->view->set('postsTotal', 
                $this->postService->totalPosts([$this->postService->postAuthorId()]));
            $this->view->set('posts', 
                $this->postService->grabPosts('ID', $this->postService->postAuthorId()));
        }
        
        return $this->view->render();
    }

    // Create new post form and handler
    public function insert()
    {
        $topics = new TopicDao();
        $medialib = new MediaDao();
        $user_level = $this->postService->postAuthorLevel();

        if (isset($_POST['postFormSubmit'])) {
            // CSRF validation
            if (!csrf_check_token('csrfToken', $_POST, 60 * 10)) {
                throw new AppException(MESSAGE_UNPLEASANT_ATTEMPT);
            }

            // Form validation and sanitization
            $filters = [
                'post_title' => isset($_POST['post_title']) ? Sanitize::strictSanitizer($_POST['post_title']) : "",
                'post_content' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
                'post_date' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
                'image_id' => FILTER_SANITIZE_NUMBER_INT,
                'catID' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_ARRAY],
                'post_status' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
                'visibility' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
                'post_password' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
                'comment_status' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
                'post_locale' => FILTER_SANITIZE_FULL_SPECIAL_CHARS
            ];

            // Handle media upload
            if (!empty($_FILES['media']['tmp_name'])) {
                upload_media($_FILES['media']['tmp_name'], $_FILES['media']['type'], 
                    $_FILES['media']['size'], $new_filename);
            }

            // Set post properties via service
            $this->postService->setPostAuthor((int)$this->postService->postAuthorId());
            $this->postService->setPostTitle(distill_post_request($filters)['post_title']);
            $this->postService->setPostSlug(distill_post_request($filters)['post_title']);
            $this->postService->setPostContent(distill_post_request($filters)['post_content']);
            $this->postService->setPublish(distill_post_request($filters)['post_status']);
            $this->postService->setVisibility(distill_post_request($filters)['visibility']);
            
            // Handle password-protected posts
            if (isset($_POST['visibility']) && $_POST['visibility'] == 'protected') {
                $protected = protect_post($content, 'protected', $_POST['post_password']);
                $this->postService->setProtected($protected['post_password']);
                $this->postService->setPassPhrase($_POST['post_password']);
            }

            $this->postService->addPost();
            $_SESSION['status'] = "postAdded";
            direct_page('index.php?load=posts&status=postAdded', 200);
        } else {
            // Display empty form
            $this->setView('edit-post');
            $this->setPageTitle('Add new post');
            $this->setFormAction(ActionConst::NEWPOST);
            $this->view->set('topics', $topics->setCheckBoxTopic());
            $this->view->set('medialibs', $medialib->imageUploadHandler());
            $this->view->set('postStatus', $this->postService->postStatusDropDown());
            $this->view->set('postVisibility', $this->postService->visibilityDropDown());
            $this->view->set('postLocale', $this->postService->localeDropDown());
            $this->view->set('csrfToken', csrf_generate_token('csrfToken'));
        }

        return $this->view->render();
    }

    // Update existing post
    public function update($id)
    {
        $topics = new TopicDao();
        $medialib = new MediaDao();
        $user_level = $this->postService->postAuthorLevel();

        if (!$getPost = $this->postService->grabPost($id)) {
            $_SESSION['error'] = "postNotFound";
            direct_page('index.php?load=posts&error=postNotFound', 404);
        }

        if (isset($_POST['postFormSubmit'])) {
            // CSRF and validation...
            $this->postService->setPostId((int)$id);
            $this->postService->setPostAuthor($this->postService->postAuthorId());
            $this->postService->setPostTitle($title);
            $this->postService->setPostSlug($title);
            // ... set other properties
            
            $this->postService->modifyPost();
            $_SESSION['status'] = "postUpdated";
            direct_page('index.php?load=posts&status=postUpdated', 200);
        } else {
            $this->setView('edit-post');
            $this->setPageTitle('Edit Post');
            $this->setFormAction(ActionConst::EDITPOST);
            $this->view->set('postData', $getPost);
            $this->view->set('topics', $topics->setCheckBoxTopic($getPost['ID']));
            
            // Decrypt protected posts for editing
            if ($getPost['post_visibility'] == 'protected') {
                $decrypted = decrypt_post_admin($getPost['ID']);
                $this->view->set('postContent', $decrypted['post_content']);
            }
        }

        return $this->view->render();
    }

    // Delete post
    public function remove($id)
    {
        $id = abs((int)$id);
        
        if (!$this->postService->grabPost($id)) {
            $_SESSION['error'] = "postNotFound";
            direct_page('index.php?load=posts&error=postNotFound', 404);
        }

        $this->postService->setPostId($id);
        $this->postService->removePost();
        $_SESSION['status'] = "postDeleted";
        direct_page('index.php?load=posts&status=postDeleted', 200);
    }

    protected function setView($viewName)
    {
        $this->view = new View('admin', 'ui', 'posts', $viewName);
    }
}

Model

Model Guidelines

Principle Description
Data Entities Models represent data entities
Transformation Models can contain data transformation logic
View Preparation Models are used for view data preparation

Example: PostModel

PHP lib/model/PostModel.php
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");

class PostModel extends BaseModel
{
    private $linkPosts;

    // Get posts for sharing feeds
    public function getPostFeeds($limit)
    {
        $sql = "SELECT p.ID, p.media_id, p.post_author,
                       p.post_date, p.post_modified, p.post_title,
                       p.post_slug, p.post_content, p.post_type,
                       p.post_status, p.post_tags, p.post_sticky,
                       u.user_fullname, u.user_login
                FROM tbl_posts AS p
                INNER JOIN tbl_users AS u ON p.post_author = u.ID
                WHERE p.post_type = 'blog' AND p.post_status = 'publish'
                AND p.post_visibility = 'public'
                ORDER BY p.ID DESC LIMIT :limit";

        $this->setSQL($sql);
        return $this->findAll([':limit' => $limit]) ?: [];
    }

    // Get latest posts for homepage
    public function getLatestPosts($limit)
    {
        $sql = "SELECT p.ID, p.media_id, p.post_author,
                       p.post_date AS created_at, p.post_modified AS modified_at,
                       p.post_title, p.post_slug, p.post_content, p.post_summary,
                       p.post_keyword, p.post_status, p.post_tags,
                       m.media_filename, m.media_caption, m.media_access,
                       u.user_fullname, u.user_login,
                       (SELECT COUNT(c.ID) FROM " . $this->table('tbl_comments') . " c 
                        WHERE c.comment_post_id = p.ID AND c.comment_status = 'approved') AS total_comments,
                       (SELECT GROUP_CONCAT(CONCAT(t.ID, ':', t.topic_title, ':', t.topic_slug) SEPARATOR '|') 
                        FROM " . $this->table('tbl_post_topic') . " pt 
                        JOIN " . $this->table('tbl_topics') . " t ON pt.topic_id = t.ID 
                        WHERE pt.post_id = p.ID AND t.topic_status = 'Y') AS topics_data
                FROM " . $this->table('tbl_posts') . " AS p
                INNER JOIN " . $this->table('tbl_media') . " AS m ON p.media_id = m.ID
                INNER JOIN " . $this->table('tbl_users') . " AS u ON p.post_author = u.ID
                WHERE p.post_status = 'publish' AND p.post_type = 'blog'
                AND m.media_target = 'blog' AND m.media_access = 'public'
                AND m.media_status = '1' AND u.user_banned = '0'
                ORDER BY p.post_date DESC LIMIT :limit";

        $this->setSQL($sql);
        return $this->findAll([':limit' => $limit]) ?: [];
    }

    // Get all blog posts with pagination
    public function getAllBlogPosts($sanitize, Paginator $perPage)
    {
        $this->linkPosts = $perPage;
        $stmt = $this->dbc->dbQuery("SELECT ID FROM tbl_posts WHERE post_type = 'blog'");
        $this->linkPosts->set_total($stmt->rowCount());

        $sql = "SELECT p.ID, p.media_id, p.post_author,
                       p.post_date AS created_at, p.post_modified AS modified_at,
                       p.post_title, p.post_slug, p.post_content, p.post_summary,
                       m.media_filename, m.media_caption,
                       (SELECT COUNT(c.ID) FROM " . $this->table('tbl_comments') . " c 
                        WHERE c.comment_post_id = p.ID AND c.comment_status = 'approved') AS total_comments,
                       ...topics_data...
                FROM " . $this->table('tbl_posts') . " AS p
                INNER JOIN " . $this->table('tbl_users') . " AS u ON p.post_author = u.ID
                INNER JOIN " . $this->table('tbl_media') . " AS m ON p.media_id = m.ID
                WHERE p.post_type = 'blog' AND p.post_status = 'publish'
                AND m.media_target = 'blog' AND m.media_status = '1'
                ORDER BY p.ID DESC " . $this->linkPosts->get_limit($sanitize);

        $this->setSQL($sql);
        $entries = $this->findAll([]);
        $this->pagination = $this->linkPosts->page_links($sanitize);
        return ['blogPosts' => $entries, 'paginationLink' => $this->pagination];
    }

    // Get single post by ID
    public function getPostById($id)
    {
        $sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date, p.post_modified,
                       p.post_title, p.post_slug, p.post_content, p.post_summary,
                       p.post_keyword, p.post_status, p.post_sticky, p.post_type,
                       p.post_visibility, p.post_password, p.comment_status AS comment_permit,
                       m.media_filename, m.media_caption, m.media_target, m.media_access,
                       u.user_login, u.user_fullname
                FROM tbl_posts p
                INNER JOIN tbl_media m ON p.media_id = m.ID
                INNER JOIN tbl_users u ON p.post_author = u.ID
                WHERE p.ID = :ID AND p.post_status = 'publish'
                AND p.post_type = 'blog' AND m.media_target = 'blog'
                AND m.media_access = 'public' AND m.media_status = '1'";

        $this->setSQL($sql);
        return $this->findRow([':ID' => Sanitize::severeSanitizer($id)]) ?: [];
    }

    // Get single post by slug
    public function getPostBySlug($slug, $fetchMode = null)
    {
        $sql = "SELECT p.ID, p.media_id, p.post_author,
                       p.post_date, p.post_modified, p.post_title,
                       p.post_slug, p.post_content, p.post_summary,
                       p.post_keyword, p.post_status, p.post_sticky,
                       m.media_filename, m.media_caption, m.media_access,
                       u.user_login, u.user_fullname
                FROM tbl_posts AS p
                INNER JOIN tbl_media AS m ON p.media_id = m.ID
                INNER JOIN tbl_users AS u ON p.post_author = u.ID
                WHERE p.post_slug = :slug AND p.post_status = 'publish'
                AND p.post_type = 'blog' AND m.media_target = 'blog'
                AND m.media_access = 'public' AND m.media_status = '1'";

        $this->setSQL($sql);
        return $this->findRow([':slug' => Sanitize::severeSanitizer($slug)]) ?: [];
    }

    // Get random headline posts
    public function getRandomHeadlines()
    {
        $sql = "SELECT p.ID, p.media_id, p.post_author,
                       p.post_date, p.post_modified, p.post_title,
                       p.post_slug, p.post_content, p.post_summary,
                       p.post_tags, u.user_login, u.user_fullname,
                       m.media_filename, m.media_caption
                FROM tbl_posts AS p
                INNER JOIN (SELECT ID FROM tbl_posts ORDER BY RAND() LIMIT 5) AS p2 ON p.ID = p2.ID 
                INNER JOIN tbl_users AS u ON p.post_author = u.ID
                INNER JOIN tbl_media AS m ON p.media_id = m.ID
                WHERE p.post_type = 'blog' AND p.post_status = 'publish'
                AND m.media_target = 'blog' AND p.post_headlines = '1'";

        $this->setSQL($sql);
        return $this->findAll([]) ?: [];
    }

    // Get related posts using FULLTEXT search
    public function getRelatedPosts($post_title)
    {
        $sql = "SELECT ID, media_id, post_author, post_date, post_modified,
                       post_title, post_slug, post_content,
                       MATCH(post_title, post_content, post_tags)
                       AGAINST(? IN BOOLEAN MODE) AS score
                FROM tbl_posts 
                WHERE MATCH(post_title, post_content) AGAINST(? IN BOOLEAN MODE)
                ORDER BY score ASC LIMIT 3";

        $this->setSQL($sql);
        return $this->findAll([$post_title, $post_title]) ?: [];
    }

    // Get random posts for homepage
    public function getRandomPosts($start, $end)
    {
        $sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date,
                       p.post_title, p.post_slug, p.post_content,
                       m.media_filename, m.media_caption,
                       u.user_login, u.user_fullname,
                       (SELECT COUNT(c.ID) FROM " . $this->table('tbl_comments') . " c 
                        WHERE c.comment_post_id = p.ID) AS total_comments,
                       ...topics_data...
                FROM " . $this->table('tbl_posts') . " AS p
                INNER JOIN (SELECT ID FROM " . $this->table('tbl_posts') . " 
                            ORDER BY RAND() LIMIT 3) AS p2 ON p.ID = p2.ID
                INNER JOIN " . $this->table('tbl_users') . " AS u ON p.post_author = u.ID
                INNER JOIN " . $this->table('tbl_media') . " AS m ON p.media_id = m.ID
                WHERE p.post_type = 'blog' AND p.post_status = 'publish'
                AND m.media_target = 'blog'
                LIMIT :position, :end";

        $this->setSQL($sql);
        return $this->findAll([':position' => $start, ':end' => $end]) ?: [];
    }

    // Get posts for sidebar
    public function getPostsOnSidebar($limit)
    {
        $sql = "SELECT p.ID, p.media_id, p.post_author, p.post_date,
                       p.post_title, p.post_slug, p.post_summary,
                       u.user_login, u.user_fullname
                FROM tbl_posts AS p
                INNER JOIN tbl_users AS u ON p.post_author = u.ID
                WHERE p.post_type = 'blog' AND p.post_status = 'publish'
                ORDER BY p.post_date DESC LIMIT :limit";

        $this->setSQL($sql);
        return $this->findAll([':limit' => $limit]) ?: [];
    }
}

Utility Functions

Utility functions are loaded via lib/utility-loader.php:

Category Functions
Security csrf-defender.php, remove-xss.php, form-security.php
Validation email-validation.php, url-validation.php
Plugins plugin-helper.php, plugin-validator.php, invoke-plugin.php
Formatting escape-html.php, limit-word.php
Media invoke-frontimg.php, upload-video.php
Session turn-on-session.php, regenerate-session.php

Image Handling Functions

Function Description Location
invoke_webp_image() Returns WebP URL if available, else original lib/utility/invoke-webp-image.php
invoke_frontimg() Primary function for displaying featured images lib/utility/invoke-frontimg.php
invoke_responsive_image() Generates <picture> element with WebP lib/utility/invoke-responsive-image.php
invoke_hero_image() Hero images with fetchpriority="high" lib/utility/invoke-responsive-image.php
invoke_gallery_image() Gallery images with lazy loading lib/utility/invoke-responsive-image.php

Access Control

All admin pages must implement proper authorization checks:

PHP Authorization Check
// In admin pages, check authorization before processing
if (false === $authenticator->userAccessControl(ActionConst::PRIVACY)) {
    direct_page('index.php?load=403&forbidden=' . forbidden_id(), 403);
}

Action Constants & Required Levels

Action Required Level
ActionConst::PRIVACY administrator
ActionConst::USERS administrator
ActionConst::IMPORT administrator
ActionConst::PLUGINS, THEMES, CONFIGURATION administrator, manager
ActionConst::PAGES, NAVIGATION administrator, manager
ActionConst::TOPICS administrator, manager, editor
ActionConst::COMMENTS, MEDIALIB, REPLY administrator, manager, author
ActionConst::POSTS administrator, manager, editor, author, contributor

Security Considerations

Always use prepared statements
Sanitize all user input
Validate data before processing
Check authorization before actions
Use CSRF tokens on all forms
Log errors securely (no secrets)