📜 ⬆️ ⬇️

PHPLego: Do-it-yourself plugins



Good morning, dear Hobrochityateli!

Have you ever wanted to make the modules to the site unobtrusive, so that it was enough to put the module in a folder and not do any more work on their connection. That once written block of the site could be used on new projects again, regardless of their structure.
')
In this article I want to share a modest microbike that helps me in the difficult task of site building.

Our first acquaintance with you turned out to be very interesting, and I sincerely appreciate you for your constructive criticism. Hope to continue in the same vein.

So, for myself, I formulated the task according to the following criteria:

1) Each module must contain everything necessary for work in one folder - both templates, and model, and controller. So that it could be easily copied, corrected - and voila - a new module.
2) The module should not know anything about those who create it - it receives all the data it needs for work through the designer. This is so that the module works not only on my site, but also on all the sites of my friends and clients without any file filing.
3) In order to use the module it should not be necessary to register or include additional files anywhere. It is stupid annoying.
4) A module can consist of modules. Those. There must be support for nested modules.
5) Links (a href = ...) inside the module templates should be relative, regardless of how deep the nesting module is. In order to avoid modifying templates, we move a module from one parent module to another.
6) The site itself should also be a module, for that matter. In order to be able to buy from a friend already a working site, put yourself in a folder and embed all of it on any page without unnecessary rework.

Well, for one article, I think enough, let's proceed to implementation.


Project File Structure


To begin with, we outline the structure of the project files:
/nome/user/www/ |---[.myengine] //    | |---[classes] //   | |---autoload.php //    ,     | `---README.TXT //      |---[classes] //   | |---[articles] //   | | |---[m] //   | | | `---article.class.php //   ( : articles_m_article) | | |---[view] //   CSS   | | | |---[css] //    | | | | `---style.css //    | | | |---article_list.tpl //    | | | `---one_article.tpl //     | | `---controller.class.php //    ( : articles_controller) | |---[comments] //   | |---[fotos] //   | |---[site] //    ( ) | `---[users] //   |---.setup.php //   (  ,    ..) `---index.php //     


Automatic loading of modules


For the purity of the system, we arrange it so that in any place of the site where we want to use the modules, it was necessary to include just one file, some kind of autoload.php. And we will make the path to the module folder to be customizable (some kind of global variable) or, even better, let it be several paths. Well, it may be necessary, for example, in order to make two folders of modules - one private, only for oneself, and the other shared - for collective development.

In our case, there is a folder /.myengine/classes - these are modules of our engine, some modules that we use in all our projects. A / classes is the module folder of the project itself.

So, the autoload.php file itself :
 <?php //    PHP,    ,    new SomeClass() function __autoload($class_name){ $class_folder = 'classes'; //      //   //       $class_folder.      $class_paths[] = dirname($_SERVER['SCRIPT_FILENAME'])."/$class_folder/"; //   $class_paths[] = __DIR__."/classes/"; //     $CLASS_PATHS if(!empty($GLOBALS["CLASS_PATHS"])){ if(!is_array($GLOBALS["CLASS_PATHS"])) throw new Exception('$CLASS_PATHS must be array!'); $class_paths = array_merge($class_paths, $GLOBALS["CLASS_PATHS"]); } //   (      A_B_C) $slashed_class_name = str_replace("_", "/", $class_name); // A/B/C $short_path = substr($slashed_class_name, 0, strrpos($slashed_class_name, '/')); // A/B foreach($class_paths as $class_path){ //   A_B_C    /A/B/C.class.php $file_full_name = "{$class_path}/{$slashed_class_name}.class.php"; if(file_exists($file_full_name)){ require_once($file_full_name); return; } //   A_B_C    /A/B/C/A_B_C.class.php //     -      (     ) $file_full_name = "{$class_path}/{$slashed_class_name}/{$class_name}.class.php"; if(file_exists($file_full_name)){ require_once($file_full_name); return; } } } ?> 


Small explanations to the startup file. Here we made it possible to add paths to modules in the global $ CLASS_PATHS array. Autoload will sort out the model paths in this order:
1) first look in the classes folder next to the file being called
2) if not found, look in the engine modules
2) if you did not find it there, it will go through all the folders added to $ CLASS_PATHS.

I would not recommend adding too many paths to $ CLASS_PATHS - after all, every access to the file system for the existence of a file is time. Although small, but still.

Also, for the convenience and portability of all project files, I propose to create a file, a certain .setup.php. Create it in the project root, as well as in all subfolders where PHP files are stored that use modules. The root .setup.php will include the modules autoload file:

 <?php include __DIR__.'.myengine/autoload.php'; //    //     ,      ?> 


And all the .setup.php files in the project subfolders will include the top-level .setup.php :

 <?php //  .setup.php  : include __DIR__.'/../.setup.php'; ?> 


This is convenient because all files that create modules always contain the same line:

 <?php include '.setup.php'; // --         //   ,   $m = new SomeModule(); $m->run(); echo $m->getOutput(); ?> 


And then, moving files from folder to folder (which is the inevitability of any creative project), I need to edit the paths of inclusion. Everything works right away.

In general, including files is like gluing a project with electrical tape, if there are a lot of them and they are complex - the program turns into a plate with pasta, in which one file pulls the other along, and the third one is uncomfortable. Therefore, we got rid of ikluds - classes of modules do not deceive anything at all - they themselves include autoloading. And PHP files executables always contain only one include - include ".setup.php".

Note: some project files start with '.' (points) so that when sorting by name they are on top


Class controller module


Module classes are essentially the Lego controllers described in my previous article . In which we add a couple of functions that allow us to determine the folder in which this controller lies, and take templates, Java scripts and Css relative to this path.

 abstract class Lego{ .................. //     http://habrahabr.ru/company/microset/blog/109481/ abstract public function getDir(); //  ,       //     (.. ,     ) public function getWebDir(){ $viewdir = str_replace('\\', '/', $this->getViewDir()); //  ,     //, !      DOCUMENT_ROOT: return str_ireplace($_SERVER['DOCUMENT_ROOT'], '', $viewdir).'/'.$dirname; } //    -,    public function getJavascripts(){ $js = array(); $h = @opendir($this->getDir()."/js"); while($file = @readdir($h)) if(preg_match("/(\.js|\.js\.php)$/i", $file)) $js[] = $this->getWebDir()."/js/".$file; return $js; } //    ,    public function getStylesheets(){ $css = array(); $h = @opendir($this->getDir()."/view/css"); while($file = @readdir($h)) if(preg_match("/(\.css|\.css\.php)$/i", $file)) $css[] = $this->getWebDir()."/view/css/".$file; return $css; } //  ,    ,        public function getHeaderBlock(){ $csses = $this->getStylesheets(); $jses = $this->getJavascripts(); $ret = ""; foreach($csses as $one) $ret .= "\n<link rel='stylesheet' href='{$one}' type='text/css' media='screen' />\n"; foreach($jses as $one) $ret .= "\n<script type='text/javascript' src='{$one}'></script>\n"; return $ret; } //    (   ,  ) public function fetch($template){ return Smarty::fetch($this->getDefaultDir().'/view/'.$template); } } 


Thus, we untied the location of style files, Java scripts and templates from the project as a whole. The module can be moved from folder to folder, renamed and the system will continue to work. Now we have the most interesting thing: how to specify links inside the templates? After all, they, too, must be untied from the project as a whole and from the location of the modules.

Relative references in templates


If we recall the previous article, each Lego object rents its array variable in the address bar whose name matches the module name. In order to refer to some method of the module controller, it is enough to take the current address bar, and replace it with data related only to the current module. For lazy work with GET-parameters of the address bar, let's create the UriConstrucor class:
 //       class UriConstructor{ public $arr; public function __construct($arr = false){ $this->arr = $arr ? $arr : $_GET; } //      (     ) public function put($key, $val){ $this->arr = array_replace_recursive($this->arr, array($key => $val)); return $this; } //      public function remove($key){ unset($this->arr[$key]); return $this; } //         public function clear(){ $this->arr = array(); return $this; } //         public function set($lego_name, $action /*....*/){ if(isset($this->arr[$lego_name]) && !is_array($this->arr[$lego_name])) unset($this->arr[$lego_name]); $this->arr[$lego_name]['act'] = $action; $params = func_get_args(); array_shift($params); array_shift($params); foreach($params as $key=>$one){ $this->arr[$lego_name][$action][$key] = $one; } return $this; } //    ,        public function setAct($action /*....*/){ $lego = Lego::current(); $params = array($lego->getName()); $params = array_merge($params, func_get_args()); return call_user_func_array( array($this, "set"), $params ); } //    get-,   (    ) public function url($path = false){ if(!$path) $path = $_SERVER['SCRIPT_NAME']; return $path.'?'.$this; } //      -    GET- public function __toString(){ return http_build_query($this->arr); } //      GET-   public function asArray(){ return $this->arr; } } 


And in the base class Lego add a couple more methods:

 abstract class Lego{ .................. //    public function uri(){ return new UriConstructor(); } //  ,   ,         href //    action- ,    ,    public function actUri($action /* params */){ $params = func_get_args(); array_unshift($params, $this->getName()); return call_user_func_array( array($this->uri(), "set"), $params ); } } 


And in the templates we write the links like this:
 <a href="{$lego->actUri('allfotos')->url()}"> </a>  ,  ,  : <a href="{$lego->actUri('showonefoto', $id)->url()}"> </a> //       : <a href="/index.php?.....__...&fotos[act]=showonefoto&fotos[0]=123">....</a> 


Previously, of course, you need to pass the lego object itself to the template engine so that the $ lego variable is available. This can be done in the run () method of the Lego base class:
 abstract class Lego{ .................. //    public function run(){ Smarty::assign("lego", $this); //   $lego -     ..... //    } } 


Here, now we also untied the templates. The modules no longer know what project they are creating, and what module they have become easily relocatable in the project and between projects.

Of course, the module must be somehow connected with the project, otherwise it would be absolutely meaningless. Therefore, the necessary data he should receive through the designer.

We give the code for a typical module:

 /*  ,   ,    $entity_id  $entity_name */ class fotos_controller extends Lego{ private $entity_id; private $entity_name; private $num_for_page = 5; //     ( ),     function __construct($name = false, $entity_id = 0, $entity_name = "User"){ parent::__construct($name); $this->entity_id = $entity_id; $this->entity_name = $entity_name; } //   ,       Lego. public function getDir(){ return __DIR__; } //   -    //   ,    function action_index(){ Database::query("select * from `fotos` where `entity_name`='{$this->entity_name}' and `entity_id`={$this->entity_id} and deleted = 0 order by created desc"); $fotos = Database::fetchObjects(); Output::assign("fotos", $fotos); return $this->fetch("allfotos.tpl"); } //        function action_mainbar(){ $offset = $this->_get($this->getName()."_offset", 0); Database::query("select * from `fotos` where `entity_name`='{$this->entity_name}' and `entity_id`={$this->entity_id} and deleted = 0 order by created desc limit {$offset}, ".($this->num_for_page+1)); $fotos = Database::fetchObjects(); Output::assign("fotos", $fotos); Output::assign("offset", $offset); Output::assign("num_for_page", $this->num_for_page); return $this->fetch("lego_fotos.tpl"); } //             function action_sidebar(){ return $this->action_mainbar(); } //     (   POST) function action_submit(){ $f = new tbl_fotos(); $f['entity_name'] = $this->entity_name; $f['entity_id'] = $this->entity_id; $f['user_id'] = User::getCurrentUser()->getId(); $f['text'] = $this->_post($this->getName()."_text"); $f['file_id'] = FotoStorage::putFromPost($this->getName()."_file"); if($f['file_id']) $f->insert(); $this->_goto($this->actUri("mainbar")->url()); //_goto -   header("Location: ... } //       function action_showone($foto_id){ $f = new tbl_fotos($foto_id); Output::assign("foto", $f); $ret = $this->fetch("showone.tpl"); // .    $c = new comments_controller("foto_comments", "tbl_fotos", $f->getId()); $c->run(); return $ret.$c->getOutput(); //    } //  "  " function action_set_as_main($foto_id){ Auth::authorize(); $f = new tbl_fotos($foto_id); $user = $f->getOwner(); $user['foto'] = $f['file_id']; $user->update(); $this->_goto($this->actUri("showone", $foto_id)->url()); } //  " " function action_delete($foto_id){ Auth::authorize(); $f = new tbl_fotos($foto_id); //FileStorage::delete($f['file_id']); if($f->isMain()){ $user = User::getCurrentUser(); $user['foto'] = ""; $user->update(); } $f['deleted'] = 1; $f->update(); $this->_goto($this->actUri("showone", $foto_id)->url()); } //  "  " function action_restore($foto_id){ Auth::authorize(); $f = new tbl_fotos($foto_id); $f['deleted'] = 0; $f->update(); $this->_goto($this->actUri("showone", $foto_id)->url()); } } 


Each module, including the root module of the site, is executed by the following lines of code.
For example, this is index.php in the project root:
 <?php include ".setup.php"; //    $lego = new site_controller(); //  -  $lego->run(); //   echo $lego->getOutput();// !      ?> 


That's all.
How to add Lego-modules of life-giving AJAX, you can read here .
You can test how it works with AJAX here .

I hope someone will come in handy this article.
All successful Lego programming. :)
Thanks for attention! Always yours, Jozhik.

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


All Articles