Jump to content

Custom PHP MVC Framework - Creating Multilevel Navigation & Commenting Systems, etc.


Recommended Posts

I'm trying not to break the main MVC architecture of my custom PHP MVC framework when generating multi-level navigation menus and a commenting system.

In which part of the MVC files should I create recursive functions to generate multilevel navigation menus or if I wanted to generate a commenting system that requires recursion?

Here's part of the main MVC folder/path structure that I'm currently using:

MVCStructureFoldersPaths1.thumb.png.5b30d85cfe93267caff1a471a18045cc.png

 

This is the Core Model "Core/Model.php" file:

<?php
class Model {
    private $host = DBHOST;
    private $user = UN;
    private $pw = PW;
    private $dbname = DBNAME;
    
    private $dbh;
    private $stmt;
    private $error;

    public function __construct() {
        $dsn = "mysql:host=$this->host; dbname=$this->dbname";
        $options = [
            PDO::ATTR_PERSISTENT => true,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ];

        try {
            $this->dbh = new PDO($dsn, $this->user, $this->pw, $options);
            #echo 'Successfully connected to the database using PDO!';
        } catch(PDOException $e) {
            $this->error = $e->getMessage();
            echo $this->error;
        }
    }

    public function query($sql){
        $this->stmt = $this->dbh->prepare($sql);
    }


    #Bind the values: *NOTE: Bind only when selecting by passing a property argument through a param.
    public function bind($param, $value, $type = null){
        if(is_null($type)){
            switch(true){
                case is_int($value):
                    $type = PDO::PARAM_INT;
                    break;
                case is_bool($value):
                    $type = PDO::PARAM_BOOL;
                    break;
                case is_null($value):
                    $type = PDO::PARAM_NULL;
                    break;
                default:
                    $type = PDO::PARAM_STR;
            }
            
        }
        $this->stmt->bindValue($param, $value, $type);
    }
    

    public function execute(){
        return $this->stmt->execute();
    }

    public function resultSet(){
        $this->execute();
        return $this->stmt->fetchAll(PDO::FETCH_OBJ);
    }

    public function single(){
        $this->execute();
        return $this->stmt->fetch(PDO::FETCH_OBJ);
    }

}

Here's the Models directory "models/PagesModel.php" file:

<?php

class PagesModel {

    private $db;

    public function __construct(){

        $this->db = new Model();

    }

  
    #Top Navigation:

    public function top_nav(){

        $this->db->query('SELECT * FROM navs WHERE level = 1');

        return $this->db->resultSet();

    }



}

 

This is the controllers "controllers/Pages.php" file:

<?php

class Pages extends Controller {

    #Load the homepage model data and view:

    public function homepage(){

        $topnav = $this->model('PagesModel')->top_nav();


        // Call a view helper function to render multi-level menu

       


        $data = [

            'topnav' => $topnav,

           

        ];


        $this->view('pages/index', $data);

    }



}

 

This is the Core "core/Controller.php" file:

<?php
class Controller {
    public function model($model){
        if(file_exists('../app/models/'. $model . '.php')){
            require_once '../app/models/'. $model . '.php';
            return new $model();
        }
    }

    public function view($view, $data = []){
        if(file_exists('../app/views/' . $view . '.php')){
            require_once '../app/views/' . $view . '.php';
        } else {
            die('View file does NOT exist.');
        }
    }
}

 

I can share more of the framework files if needed, but what I'm asking is how would I implement the multilevel navigation menus that require recursive functions without breaking the main architecture of the overall framework? Is this supposed to be done in the helper files or which would you recommend I try? Any support is appreciated. 

 

If you want to model the concept of navigation menus then you should probably use a Model.

If you want to write code to determine how navigation menus are viewed then you should probably put the code in a view.
Consider that you can create an anonymous, recursive function in a view file, then call it.

If you're not sure then your first step is to make the functionality happen at all. You can figure things out along the way - it's not like you have to get everything right on your first try. And when you have it working, then you can think about how to improve it.

1 hour ago, requinix said:

If you want to model the concept of navigation menus then you should probably use a Model.

If you want to write code to determine how navigation menus are viewed then you should probably put the code in a view.
Consider that you can create an anonymous, recursive function in a view file, then call it.

If you're not sure then your first step is to make the functionality happen at all. You can figure things out along the way - it's not like you have to get everything right on your first try. And when you have it working, then you can think about how to improve it.

Great response! I've definitely thought of just creating my own way & then improving on it later. Also, here's what the multilevel menu recursive function would look similar to:

<?php

$conn = new PDO('mysql:host=localhost; dbname=Mydb', 'MyUsername', 'MyPassword');

function left_nav($parent_id, $conn){
    
    $sql = 'SELECT * FROM navs WHERE parent_id = ?';
    $stmt = $conn->prepare($sql);
    $stmt->bindParam(1, $parent_id, PDO::PARAM_INT);
    $stmt->execute();

    $data = $stmt->fetchAll(PDO::FETCH_ASSOC);

    if($data){ 
        echo "<ul>\n";
        
        foreach ($data as $row) {
          echo "<li><a href='#'>{$row['nav_item_name']}</a>";
          
          # Recursive (function calls itself in loop) call for sub-items
          left_nav($row['id'], $conn); 

          echo "</li>\n";
        }

        echo "</ul>";
      }  
}

# Start 'left_nav()' function to build navigation from root level (parent_id = 0)
left_nav(0, $conn);

 

I didn't create a Model, but a trait though maybe it will give you some ideas?

<?php
// NavigationMenuTrait.php

namespace clearwebconcepts;

use function htmlspecialchars;
use function hash_equals;

trait NavigationMenuTrait
{
    public function regular_navigation(): void
    {
        $navItems = [
            'Home' => 'index.php',
            'About' => 'about.php',
            'Puzzle' => 'puzzle.php',
            'Portfolio' => 'portfolio.php',
            'Contact' => 'contact.php'
        ];

        // Check if the user is logged in
        $isLoggedIn = isset($_COOKIE['login_token']) && isset($_SESSION['login_token']) && hash_equals($_SESSION['login_token'], $_COOKIE['login_token']);

        if ($isLoggedIn) {
            unset($navItems['Home']); // Remove 'Home' from the navigation menu
            // Add 'Dashboard' to the start of the navigation menu
            $navItems = array('Dashboard' => 'dashboard.php') + $navItems;
        } else {
            $navItems['Login'] = 'login.php'; // Add 'Login' to the navigation menu
        }


        $navLinks = [];

        foreach ($navItems as $title => $path) {
            $href = $this->generateHref($path);
            $safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
            $navLinks[] = "<a href=\"{$href}\">{$safeTitle}</a>";
        }

        // Check if the user is logged in
        if ($isLoggedIn) {
            $navLinks[] = '<a href="logout.php">Logout</a>'; // Add 'Logout' to the end of the navigation menu
        }

        echo implode('', $navLinks);
    }

    public function showAdminNavigation(): void
    {
        $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https://' : 'http://';
        $host = $_SERVER['HTTP_HOST'];

        // Define your base path here
        $base_path = ($host === 'localhost:8888') ? '/clearwebconcepts' : '';

        $base_url = $protocol . $host . $base_path;

        $adminItems = [
            'Create Entry' => $base_url . '/create_cms.php',
            'Edit Entry' => $base_url . '/edit_cms.php',
            'Add to Portfolio' => $base_url . '/new_portfolio.php',
            'Edit Portfolio Page' => $base_url . '/edit_portfolio.php',
            'Add Jigsaw' => $base_url . '/add_to_puzzle.php',
            'Edit Jigsaw' => $base_url . '/edit_puzzle.php',
            'Service Form' => $base_url . '/service_form.php'
        ];

        echo '<div class="admin-navigation">';
        foreach ($adminItems as $adminTitle => $adminPath) {
            $adminSafeTitle = htmlspecialchars($adminTitle, ENT_QUOTES, 'UTF-8');
            echo "<a href=\"{$adminPath}\">{$adminSafeTitle}</a>";
        }
        echo '</div>';
    }


    private function generateHref(string $path): string
    {
        $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https://' : 'http://';
        $host = $_SERVER['HTTP_HOST'];

        // Define your base path here
        $base_path = ($host === 'localhost:8888') ? '/clearwebconcepts' : '';

        $base_url = $protocol . $host . $base_path;

        // Build the URL first, then validate it
        $url = $base_url . '/' . $path;
        $sanitized_url = filter_var($url, FILTER_SANITIZE_URL);
        $valid_url = filter_var($sanitized_url, FILTER_VALIDATE_URL);

        if ($valid_url === false) {
            die('Invalid URL');
        }

        return $valid_url;
    }
}

 

Edited by Strider64
On 2/29/2024 at 1:41 PM, aaroncatolico1 said:

Great response! I've definitely thought of just creating my own way & then improving on it later. Also, here's what the multilevel menu recursive function would look similar to:

<?php

$conn = new PDO('mysql:host=localhost; dbname=Mydb', 'MyUsername', 'MyPassword');

function left_nav($parent_id, $conn){
    
    $sql = 'SELECT * FROM navs WHERE parent_id = ?';
    $stmt = $conn->prepare($sql);
    $stmt->bindParam(1, $parent_id, PDO::PARAM_INT);
    $stmt->execute();

    $data = $stmt->fetchAll(PDO::FETCH_ASSOC);

    if($data){ 
        echo "<ul>\n";
        
        foreach ($data as $row) {
          echo "<li><a href='#'>{$row['nav_item_name']}</a>";
          
          # Recursive (function calls itself in loop) call for sub-items
          left_nav($row['id'], $conn); 

          echo "</li>\n";
        }

        echo "</ul>";
      }  
}

# Start 'left_nav()' function to build navigation from root level (parent_id = 0)
left_nav(0, $conn);

 

Yet it seems you missed several important suggestions.  What you presented is neither a model, nor a view.  

The approach of the function above should be avoided for a few reasons.

  • The actual query code ought to be part of your navs model.
    • You presented your custom model code, so why did you not use it and make a navs model?
  • A view should have nothing other than markup and whatever minimal logic you need to process the data and integrate it.
    • Since you are making your own mvc, have you created a view base class?
      • Typically people will put view in a particular subdirectory, and name the view files using some convention
      • Most view subsystems actually involve a parsing/combination step, since the views are often not .php files
        • This facilitates partials and all sorts of valuable structure support, but you could get away with using require_once and having snippets of code
        • With that said, just keeping it simple your views can be plain old .php files, but perhaps named as home.view.php.
        • You will probably also want files like header.view.php or perhaps header.part.view.php and footer.view.php.  You could also do a view base class to help with template code that should be shared.  Cakephp has something like this and their templates are for the most part straight php code.
      • Views should assume that the required data (typically data from model calls in the controller) is passed in via a standard parameter

 

Take a look at symfony & twig, laravel & blade or Cakephp 4's view system to get some ideas of how popular frameworks have handled Views.  

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.