📜 ⬆️ ⬇️

Its WEB application, with MVC and registry


I want to bring a simple and working project template, with which any new to PHP programming can create their own web application and at the same time get involved in the MVC theme.

The article is aimed at beginners, i.e. there is nothing new in it, just a few ideas are collected in a working draft that solves most of the problems.

The project begins with a structure. Simple and logical structure - the key to success of the project and the fact that someone other than the creator will be able to understand it.

In detail, what is MVC, can be found here and here .
')
In short, this is the division of code into Model (logic), Representation and Controller.

Slightly more
The controller responds to user actions. This is a certain manager, he does not know how to create or present a product, but he knows who can create (model) and present (presentation). The controller does not process the data, the maximum adds the result of several model calls.

The model contains the logic and data of the application. A certain San Sanych, that the controller will order from him, he will get it from the warehouse, if necessary by finishing it with a file. The model interacts with the database, and processes the result. It is important to process data in the model; there is an article on this topic https://habrahabr.ru/post/175465/ . In short, when the logic is in models, it is easier to reuse it both within the project and transfer it to others.

The presentation is a real seller. Not too smart and knows only what the controller has trusted. The task of the presentation is only to provide information. It includes the layout (templates).

In the MVC architecture, it is important that the components know nothing about each other, and therefore are independent. If we want to change the appearance of the form on the site, it should affect only the presentation. If we make changes to the database, then this should apply only to the model.

The advantages of such an architecture: it is possible to test the components separately - it is easier to look for bugs, the designer can correct the layout on the site, and the logic can be transferred to another project.

I suggest using as a simple application architecture:


Controllers are separate, models are separate, the presentation is folded into folders (in the base folder there are universal reusable templates, such as pagination). In the folder s - resources: images, JavaScript and styles.

First of all, the rules for the server in htacess:

# AddDefaultCharset utf-8 RewriteEngine on RewriteBase / #     RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d #    /employee/ RewriteRule ^([Az]*)\/$ ?type=page&controller=$1&action=main [L,QSA] #   RewriteRule ^([Az]*)\/([Az]*)\/$ ?type=page&controller=$1&action=$2 [L,QSA] #Ajax RewriteRule ^ajax\/([Az]*)\/([Az]*)\/$ ?type=ajax&controller=$1&action=$2 [L,QSA] ErrorDocument 404 /404.html 

The application will have a single entry point (that is, all requests to the site, with the exception of files, will be redirected to 1 script) - index.php. In him:

 <? session_start(); //   $type = $_REQUEST['type']; //   include $_SERVER['DOCUMENT_ROOT']. "/config.php"; //   include $_SERVER['DOCUMENT_ROOT']."/model/functions.php"; //       include $_SERVER['DOCUMENT_ROOT']."/model/db.php"; //  include $_SERVER['DOCUMENT_ROOT']."/model/reestr.php"; //   r::g('Controller_Main')->setRequest($_REQUEST); if ($type == 'ajax') { r::g('Controller_Main')->ajax($_REQUEST['controller'], $_REQUEST['action'], $_REQUEST['send']); } else { r::g('Controller_Main')->page($_REQUEST['controller'], $_REQUEST['action'],$_REQUEST); } 

Our application will have 2 types of requests: pages and ajax requests, both types are processed in the main controller, the corresponding functions.

Config file
<?
// MySQl database connection settings
define ("sql_user", "root"); // Login
define ("sql_host", "localhost"); // Server address
define ("sql_pass", ""); // Password
define ("sql_table", "loumvc"); // Database table
define ("title", "Easy MVC"); // Basic title

Classes will be loaded at initialization using the autoloader. That is, when you initialize the class new Controller_main (), provided that a file with such a class is not connected, 'Controller_main' will be transferred to our function loader, and it will break up the class name by "_" and try to connect the /controller/main.php file . If it fails, it will redirect the user to 404 page. Announcement and autoloader connections, we will write in the /model/function.php file, which we manually connected.

 <?php function autoload($class_name) { $class_name = mb_strtolower($class_name); $arPath = explode("_", $class_name); if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/" .implode("/", $arPath) . ".php")) { include_once($_SERVER['DOCUMENT_ROOT'] . "/" .implode("/", $arPath) . ".php"); }else{ //   r::g('Controller_main')->error404(); die(); } } //     spl_autoload_register('autoload'); 

More in the file are optional, but very useful debug functions
 // ,       function pr() { $args = func_get_args(); foreach ($args as $item) { echo "<pre>"; print_r($item); echo "</pre>"; } } //    function dlog($name,$text){ if(!is_string($text)){ $text = print_r($text,true); } db::insertArr('log',array( 'name'=>$name, 'val'=>$text ),1); } 


To work with a database, a class with static functions. It can be used anywhere in the program. (This is done to make it easier to use the logging function; it is not necessary to access the database outside the model.).

Code
 <? class db{ //   private static $connect = null; //  id private static $lastId = 0; // private static $log = 0; //    static function query($query,$skipLog=0){ if(self::$connect==null){ self::$connect = new mysqli(sql_host, sql_user, sql_pass, sql_table); if(self::$connect->connect_errno){ die('   '); } } //  , id   ,       self::$lastId = 0; $res = self::$connect->query($query); $lastId = self::$connect->insert_id; //  ,   ,   if(!$skipLog && self::$log){ dlog('query',$query); } self::$lastId = $lastId; return $res; } static function insertArr($table,$arr,$skipLog=0){ $sql = "INSERT INTO `$table` "; //   foreach($arr as $key=>$val){ $val = self::$connect->mysqli_escape_string($val); } $sql .= "(`".implode("`,`",array_keys($arr))."`) VALUES ('".implode("','",array_values($arr))."')"; self::query($sql,$skipLog); return self::insertId(); } // id    static function insertId(){ return self::$lastId; } static function insert($query){ self::query($query); // id    return self::insertId(); } static function selectCell($query){ $row = self::selectRow($query); if($row){ return reset($row); } } static function selectRow($query){ $res = self::select($query); if($res){ return reset($res); } } static function select($query){ $res = self::query($query); $mas = array(); if($res) { while ($row = $res->fetch_assoc()){ if($row['ARRAY_KEY']){ $key = $row['ARRAY_KEY']; unset($row['ARRAY_KEY']); $mas[$key]=$row; }else{ $mas[]=$row; } } return $mas; } } static function update($table,$mass,$id){ $sql = "UPDATE `$table` SET "; foreach($mass as $key=>$it){ $it = self::$connect->mysqli_escape_string($it); $sql .= " $key='$it',"; } $sql = substr($sql, 0,-1); $sql .= " where id = $id"; return self::query($sql); } } 


Instead of a great bike class , you can use a wrapper implementation for a plugin, for example db simple.

Code
 class db { protected static $connect = null; static function getConnect(){ if(self::connect == null){ require($_SERVER['DOCUMENT_ROOT'] . '/libs/DbSimple/Generic.php'); self::$connect = DbSimple_Generic::connect("mysql://" . sql_user . ":" . sql_pass . "@" . sql_host . "/" . sql_table); if(!self::$connect){ die("   "); } self::$connect->query("SET NAMES UTF8"); } return self::$connect; } //  query static function query() { $res = call_user_func_array(array(self::getConnect(), 'query'), func_get_args()); return $res; } //  select static function select() { $res = call_user_func_array(array(self::getConnect(), 'select'), func_get_args()); return $res; } //      static function __callStatic($name, $arguments) { if(method_exists(self, $name)){ return call_user_func_array(array(self, $name), $arguments); }else{ return call_user_func_array(array(self::getConnect(), $name), func_get_args()); } } } 


The application uses a registry — a singleton pattern implementation — a loner that stores all instances (initialized class objects) of all models and controllers. Thanks to him, each class in the project will be initialized only once, and we will easily get access to it. To do this, refer to the classes as follows:

 r::g(" ") 

Registry implementation:

 <? //     class r { //       protected static $instance = array();//  //   static function g($name){ if(self::$instance[$name]){ return self::$instance[$name]; }else{ //     return self::add($name, new $name() ); } } //     static function add($name,$val){ return self::$instance[$name] = $val; } } 

Each controller will inherit a base one in which we implement the function that outputs a representation.

 <?php class controller_base{ function view($name,$args=null,$isBase=0){ ob_start(); if($isBase){ $tpl = $_SERVER['DOCUMENT_ROOT']. "/view/base/{$name}.php"; }else{ //   $class = explode("_",get_class($this)); $tpl = $_SERVER['DOCUMENT_ROOT']. "/view/{$class[1]}/{$name}.php"; } if(file_exists($tpl)){ if($args){ //    ,     extract($args); } include $tpl; }else{ echo "no tpl {$class[1]}/{$name}"; } return ob_get_clean(); } 

Even in the base controller, we implement the registry to store the page title, with the ability to change it from any controller.

Code
  private static $title = ''; function getTitle(){ if(!self::$title){ //    return title; } return self::$title; } function setTitle($title){ self::$title = $title; } 


And saving the query
 //      private static $request = null; function setRequest($param){ self::$request = $param; } function getRequest(){ return self::$request; } 


The main controller, all calls will come to him first

 class Controller_main extends Controller_base{ function __construct(){ $this->model = r::g('Model_main'); } 

In it, and in every other controller, we create a link to its model. To handle requests for building a page, we will entrust the page functions:

 function page($controller,$action,$param){ //    ( ) if(!$controller){ $controller = 'main'; } //   main if(!$action){ $action = 'main'; } $method_name = "page_".$action; $controller_name = 'Controller_'.$controller; if(method_exists(r::g($controller_name), $method_name)){ //   ob_start(); r::g($controller_name)->{$method_name}($param); $html = ob_get_clean(); //   echo $this->view('page',array( 'html' => $html, 'title' => $this->getTitle() )); }else{ //    404 $this->error404(); } } 

It checks for the existence of the requested method in the requested controller, well, when you try to connect a non-existent controller, 404 will fall out (we arranged this in the class loader).

The body of the page is generated by the page_ {action} methods. So visually you can immediately see which functions are responsible for the generation of the pilgrim, and the methods with the name dropDataBase are protected from outside calls. An example of such a function:

 function page_main($param){ echo $this->view('main'); } 

Code of the main page template
 <html> <head> <title> <?= $title ?> </title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script> <script src="/s/js/base.js"></script> <script src="/s/js/main.js"></script> <script src="/s/js/employee.js"></script> <link href="/s/css/index.css" rel="stylesheet"> </head> <body> <div class="menu"> <?= $this->widget_menu() ?> </div> <?= $html ?> </body> </html> 


Menu widget code in the main controller used in the main template
 function widget_menu(){ // ,  ,      $request = $this->getRequest(); //    ,   ,   $menu = $this->model->getMenu($request['controller'],$request['action']); //    return $this->view('menu',[ 'list'=>$menu ]); } 


Ajax requests are handled by the function:

 function ajax($controller,$action,$send){ $method_name = "ajax_".$action; $controller_name = 'Controller_'.$controller; if(method_exists(r::g($controller_name), $method_name)){ $res = r::g($controller_name)->$method_name($send); if($res){ echo "<ja>" . json_encode($res) . "</ja>"; } } } 

It works like a page, with a few exceptions:


The latter is needed if we need to return more and structured data in javascript. In the method called ajax_ {action}, you just need to return an array, and then the Controller_main-> ajax function will “figure out” how to transfer the data to the frontend.

Tags ja need to easily disassemble the answer on the frontend. Let's receive separately the text, separately serialized array. This will save traffic and CPU, so that we will not encode / decode html page. And still protected from errors, no extra output will not break our front end.

An example from a class of employees, a method that opens a form for creating employees:
 function ajax_add(){ echo $this->widgetAdd(); return ['status'=>1]; } 

And the page method looks like this:

 function page_add(){ $this->setTitle(' '); echo $this->view('page_add',[ 'html' => $this->widgetAdd() ]); } 

Widget that is used in both cases:

 function widgetAdd(){ return $this->view('add',[ 'divisions'=>$this->model->getDivisions() ]); } 


I strongly recommend that you call templates by the name of the function they perform, or the method in which they are used. And not only templates, but also functions in js.

It turns out: in javascript you create the method “addEmployee”, it sends a request to “ajax_addEmployee”, and the function uses the template “addEmployee”. Then it will be much easier to find them. Then you want to fix the template, look in js for the name of the function and immediately edit the view, no need to search in the controller.

A little bit about the frontend:


The folder with the scripts will look like a folder of controllers:


Also a basic script that all JS controllers will inherit from.

 function base(){ this.post = function(method,send,callback){ var controller = this.url ; var url = "/ajax/"+controller+"/"+method+"/"; return $.post(url,{ send : send },function (re) { var res = re.split('<ja>'); if (res[1] != undefined) { var text = res[0]; res = res[1].split('</ja>'); text += res[1]; re = text; var mas = $.parseJSON(res[0]); } else { var mas = {}; } if (typeof (callback) == 'function') { callback(re, mas) } }) } this.createPop = function(title, html) {} } }base = new base(); 

The code of the window creating function, a cheap analogue of the fancybox, with methods and buns.
 this.createPop = function(title, html) { //  ,    jqary var $win = $("<div/>", { class: 'Pop' }); // box c   var $box = $("<div/>", { class: 'winBox' }); //  , var $top = $("<div/>", { class: "winTop" }); //  var $title = $('<div/>', { class: "winTitle", html: title }); //     $top.append($title); //   , -     var $close = $("<div/>", { class: 'winClose', html: "x" }); $top.append($close); //     -    -   $close.on('click', function () { $win.remove(); }); //  $win.close = function () { $win.remove(); } //   null,  ,   if (html == null) { //   ,    html = "<div class='help'>   </div>"; } //     $box.html(html); //        $win.append($top); $win.append($box); //   ,   $win.update = function (html) { $box.html(html); } //,         if ($('.BoxPop').length == 0) { //  -   $('body').append($('<div/>', { class: 'BoxPop' })); } //     ,     $('.BoxPop').append($win); //      return $win; } 


Actually inheritance and use of ajax:

 function employee(){ //  this.url = "employee"; //   var self = this; this.add = function(){ var $win = self.createPop(' ',null); self.post('add',{},function(re){ $win.update(re); }) } } employee.prototype = base; employee = new employee(); 

And also, in order to protect your project from accessing scripts, in the folder controller, model and. view it is necessary to put .htacess with the content:

 Deny from all 

Using this template, you can implement most of the tasks for web applications. And most importantly, any other programmer who is familiar with MVC can easily understand your project.

→ All source code

Source: https://habr.com/ru/post/321982/


All Articles