Как составить фреймворк

Время на прочтение
6 мин

Количество просмотров 27K

Вы когда-нибудь задавались вопросом о том, как работают фреймворки? Автор материала, перевод которого мы сегодня публикуем, говорит, что когда он, много лет назад, после изучения jQuery, наткнулся на Angular.js, то, что он увидел, показалось ему очень сложным и непонятным. Потом появился Vue.js, и разбираясь с этим фреймворком, он вдохновился на написание собственной системы двусторонней привязки данных. Подобные эксперименты способствуют профессиональному росту программиста. Эта статья предназначена для тех, кто хочет расширить собственные знания в сфере технологий, на которых основаны современные JS-фреймворки. В частности, речь здесь пойдёт о том, как написать ядро собственного фреймворка, поддерживающего пользовательские атрибуты HTML-элементов, реактивность и двустороннюю привязку данных.

image

О системе реактивности

Хорошо будет, если мы, в самом начале, разберёмся с тем, как работают системы реактивности современных фреймворков. На самом деле, всё тут довольно просто. Например, Vue.js, при объявлении нового компонента, проксирует каждое свойство (геттеры и сеттеры), используя паттерн проектирования «прокси».

В результате фреймворк сможет обнаружить каждый факт изменения значения, выполняемый и из кода, и из пользовательского интерфейса.

Паттерн «прокси»

В основе паттерна «прокси» лежит перегрузка механизмов доступа к объекту. Это похоже на то, как люди работают со своими банковскими счетами.

Например, никто не может напрямую работать со своим счётом, меняя его баланс в соответствии со своими нуждами. Для того, чтобы произвести со счётом какие-то действия, нужно обратиться к кому-то, у кого есть разрешение на работу с ним, то есть — к банку. Банк играет роль прокси между владельцем счёта и самим счётом.

var account = {
    balance: 5000
}

var bank = new Proxy(account, {
    get: function (target, prop) {
        return 9000000;
    }
});
console.log(account.balance); // 5,000 (реальный баланс)
console.log(bank.balance);    // 9,000,000 (банк сообщает ложные сведения)
console.log(bank.currency);   // 9,000,000 (некие действия банка)

В этом примере, при использовании объекта bank для доступа к балансу счёта, представленного объектом account, осуществляется перегрузка функции-геттера, что приводит к тому, что в результате подобного запроса всегда возвращается значение 9,000,000, вместо реального значения свойства, даже в том случае, если это свойство не существует.

А вот пример перегрузки функции-сеттера.

var bank = new Proxy(account, {
    set: function (target, prop, value) {
        // Всегда устанавливать значение свойства в 0
        return Reflect.set(target, prop, 0); 
    }
});

account.balance = 5800;
console.log(account.balance); // 5,800

bank.balance = 5400;
console.log(account.balance); // 0 (некие действия банка)

Здесь, перегрузив функцию set, можно управлять её поведением. Например, можно изменить значение, которое требуется записать в свойство, записать данные в какое-нибудь другое свойство, или даже попросту ничего не делать.

Пример системы реактивности

Теперь, когда мы разобрались с паттерном «прокси», приступим к разработке нашего собственного JS-фреймворка.

Для того чтобы не усложнять его, мы воспользуемся синтаксисом, очень напоминающим тот, который применяется в Angular.js. В результате объявление контроллера и привязка элементов шаблона к свойствам контроллера будет выглядеть просто и понятно.

Вот код шаблона.

<div ng-controller="InputController">
    <!-- "Hello World!" -->
    <input ng-bind="message"/>   
    <input ng-bind="message"/>
</div>

<script type="javascript">
  function InputController () {
      this.message = 'Hello World!';
  }
  angular.controller('InputController', InputController);
</script>

Для начала надо объявить контроллер со свойствами. Далее — использовать этот контроллер в шаблоне, и, наконец — применить атрибут ng-bind для того, чтобы наладить двустороннюю привязку данных для значения элемента.

Разбор шаблона и создание экземпляра контроллера

Для того, чтобы у нас были некие свойства, к которым можно привязывать данные, нам нужно место (контроллер), в котором можно объявить эти свойства. Таким образом, необходимо описать контроллер и включить его во фреймворк.

В процессе работы с контроллерами фреймворк будет искать элементы, имеющие атрибуты ng-controller. Если то, что удалось найти, будет соответствовать одному из объявленных контроллеров, фреймворк создаст новый экземпляр этого контроллера. Данный экземпляр контроллера ответственен лишь за этот конкретный фрагмент шаблона.

var controllers = {};
var addController = function (name, constructor) {
    // Конструктор контроллера
    controllers[name] = {
        factory: constructor,
        instances: []
    };
    
    //Ищем элементы, использующие контроллер
    var element = document.querySelector('[ng-controller=' + name + ']');
    if (!element){
       return; // Нет элементов, использующих этот контроллер
    }
    
    // Создаём новый экземпляр и сохраняем его
    var ctrl = new controllers[name].factory;
    controllers[name].instances.push(ctrl);
    
    // Ищем привязки данных.....
};

addController('InputController', InputController);

Ниже показано объявление «самодельной» переменной controllers. Обратите внимание на то, что объект controllers содержит все контроллеры, объявленные во фреймворке путём вызова addController.

var controllers = {
    InputController: {
        factory: function InputController(){
            this.message = "Hello World!";
        },
        instances: [
            {message: "Hello World"}
        ]
    }
};

У каждого контроллера предусмотрена функция factory, делается это для того, чтобы, при необходимости, можно было создать экземпляр нового контроллера. Кроме того, фреймворк хранит, в свойстве instances, все экземпляры контроллера одного типа, использованные в шаблоне.

Поиск элементов, участвующих в привязке данных

В данный момент у нас есть экземпляр контроллера и фрагмент шаблона, использующий этот контроллер. Следующим нашим шагом будет поиск элементов с данными, которые нужно привязать к свойствам контроллера.

var bindings = {};

// Обратите внимание: element это элемент DOM, использующий контроллер
Array.prototype.slice.call(element.querySelectorAll('[ng-bind]'))
    .map(function (element) {
        var boundValue = element.getAttribute('ng-bind');

        if(!bindings[boundValue]) {
            bindings[boundValue] = {
                boundValue: boundValue,
                elements: []
            }
        }

        bindings[boundValue].elements.push(element);
    });

Здесь показана организация хранения всех привязок объекта с использованием хэш-таблицы. Рассматриваемая переменная содержит все свойства для привязки, с их текущими значениями, и все DOM-элементы, привязанные к конкретному свойству.

Вот как выглядит наш вариант переменной bindings:

    var bindings = {
        message: {
            // Переменная ссылается на:
            // controllers.InputController.instances[0].message
            boundValue: 'Hello World',

            // HTML-элементы (элементы управления с ng-bind="message")
            elements: [
                Object { ... },
                Object { ... }
            ]
        }
    };

Двусторонняя привязка свойств контроллера

После того, как фреймворк выполнил предварительную подготовку, приходит время одного интересного дела: двусторонней привязки данных. Смысл этого заключается в следующем. Во-первых, свойства контроллера нужно привязать к элементам DOM, что позволит обновлять их тогда, когда значения свойств меняются из кода, и, во-вторых, элементы DOM также должны быть привязаны к свойствам контроллера. Благодаря этому, когда пользователь воздействует на такие элементы, это приводит к изменению свойств контроллера. А если к свойству привязано несколько HTML-элементов, то это приводит и к тому, что их состояние также обновляется.

Обнаружение изменений, выполняемых из кода, с помощью прокси

Как уже было сказано выше, Vue.js оборачивает компоненты в прокси для того, чтобы выявлять изменения свойств. Сделаем то же самое, проксировав лишь сеттер для свойств контроллера, участвующих в привязке данных:

// Обратите внимание: ctrl - это экземпляр контроллера
var proxy = new Proxy(ctrl, {
    set: function (target, prop, value) {
        var bind = bindings[prop];
        if(bind) {
            // Обновим каждый элемент DOM, привязанный к свойству  
            bind.elements.forEach(function (element) {
                element.value = value;
                element.setAttribute('value', value);
            });
        }
        return Reflect.set(target, prop, value);
    }
});

В результате получается, что при записи значения в привязанное свойство прокси обнаружит все элементы, привязанные к этому свойству, и передаст им новое значение.

В этом примере мы поддерживаем лишь привязку элементов input, так как здесь установлен лишь атрибут value.

Реакция на события элементов

Теперь нам осталось лишь обеспечить реакцию системы на действия пользователя. Для этого нужно учитывать тот факт, что элементы DOM вызывают события при обнаружении изменений своих значений.

Организуем прослушивание этих событий и запись в привязанные к элементам свойства новых данных, получаемых из событий. Все остальные элементы, привязанные к тому же свойству, будут, благодаря прокси, обновляться автоматически.

Object.keys(bindings).forEach(function (boundValue) {
    var bind = bindings[boundValue];
    
    // Прослушивание событий элементов и обновление свойства прокси
    bind.elements.forEach(function (element) {
      element.addEventListener('input', function (event) {
        proxy[bind.boundValue] = event.target.value; // Кроме того, это вызывает срабатывание сеттера прокси
      });
    })  
  });

Итоги

В результате, собрав воедино всё то, о чём мы тут рассуждали, вы получите собственную систему, реализующую базовые механизмы современных JS-фреймворков. Вот работающий пример реализации такой системы. Надеемся, этот материал поможет всем желающим лучше разобраться в том, как работают современные средства веб-разработки.

Уважаемые читатели! Если вы профессионально используете современные JS-фреймворки, просим рассказать о том, как вы начинали их изучение, и о том, что помогло вам в них разобраться.

Привет тебе, коллега-разработчик или просто случайно зашедший на мой блог посетитель. В сегодняшней заметке я хочу рассказать вам о том, как прошел у меня вчерашний вечер (нет, нет, тут не будет ничего личного, аля покатался на роликах, попил пивка в парке). Как и у каждого веб-программиста рано или поздно возникает идея создать свой велосипед, пусть и с квадратными колесами и вместо руля торчащий штырь. Вчерашним велосипедом для меня стал легкий простой фреймворк, хотя с натяжкой его можно так назвать, но есть несколько моментов в нем, которые могут послужить отправной точкой для создание нечто большего. Ну обо всем по порядку.

Структура папок

Так как на меня большое влияние в последнее время оказал Zend Framework, структура папок, а также несколько еще штук будут очень похожи.

В корне мы имеем две папки app и library. Также в корне есть два файла (опять же все как у ZF) .htaccess (будет перенаправлять все запросы в index.php) и сам index.php, который эти самые запросы принимать будет.

.htaccess и index.php

.htaccess полностью взял с ZF

1
2
3
4
5
6
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]

Мне этого было более, чем достаточно. По сути этот .htaccess перенаправляет все запросы, если они не ведут на существующую директорию или файл нашему index.php.

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// объявляем нужные константы
define('APPLICATION_PATH', realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'app'));
define('LIBRARY_PATH', realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library'));
// добавляем путь к library к indlude path
set_include_path(implode(DIRECTORY_SEPARATOR, array(
            realpath(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'library'),
            get_include_path()
        )));

// подключаем Автолоадер
require_once 'MVC/Autoload/Autoloader.php';

// регистрируем наш автолоадер
spl_autoload_register(array('MVC_Autoload_Autoloader', 'autoload'));

// создаем инстант нашего "Фронт Контроллера"
// в кавычках он потому, что это вовсе не паттерн Фронт Контроллер )
$frontController = new MVC_FrontController($_SERVER);
// запускаем наше приложение
$frontController->run();

папка library

В данной папке будет пока две директории MVC и Smarty, первая – это наш фреймворк, вторая как вы уже догадались – шаблонизатор :)

Рассмотрим поподробнее папку MVC
MVC_Autoload_Autoloader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

class MVC_Autoload_Autoloader
{

    public static function autoload($className)
    {
        // все как в ZF, чтобы отыскать класс заменяем в нем
      // '_' на '/'
        $className = str_replace('_', '/', $className);
        $classPath = LIBRARY_PATH . DIRECTORY_SEPARATOR . $className . '.php';
        if (file_exists($classPath) && is_readable($classPath)) {
         // подключаем его, если файл имеется и мы имеем к нему доступ
            require_once $classPath;
        } else {
            //throw new MVC_Exception("Class name '{$className}' not found");
        }
        //echo $classPath;
    }

   // тут я планировал указывать какие еще префиксы загружать автоматически
    public static function registerAutoload()
    {

    }
}

MVC_Autoload_Resource – будет отвечать за подгрузку контроллеров, правда получился он не особо гибким и пути были жестко прописаны в код (очень не хорошо получилось).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

class MVC_Autoload_Resource
{
   // думал будет у меня несколько ресурсов
   // аля контроллеры, модели и прочие штуки
    protected static $_resources = array(
        'controller' => '@App_Controller_(w+)Controller@',
    );

   // метод без проверок, т.к. фреймворк создавался для ознакомительных целей
    public static function autoload($className)
    {
      // пробегаемся по всем ресурсам
      // и смотрим на что заканчивает класс
        foreach(self::$_resources as $resource => $path) {
         // если на то, что нужно подгружаем его
            if (ucfirst($resource) == substr($className, -strlen($resource))) {
                if (preg_match(self::$_resources[$resource], $className, $matches)) {
                    self::load($matches[1], $resource);
                }
            }
        }
    }

    protected static function load($name, $resourceType)
    {
        switch ($resourceType) {
            case 'controller':
                require_once APPLICATION_PATH . DIRECTORY_SEPARATOR
                        . 'controllers' . DIRECTORY_SEPARATOR
                        . "{$name}Controller.php";


                break;

            default:
                break;
        }
    }

}

Стукрутра папок в приложении (App)

3 папки: config, controllers, views
config – содержит на данный момент всего один файл с настройками приложения application.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

$settings = array(
    'application' => array(
        'applicationName' => 'first application'
    ),
   // самое интересное, наверное, в этом фреймворке
    'routes' => array(
      // все рауты имеют имя, шаблон или регулярное выражение
      // контроллер, экшн и параметры
        'static' => array(
            'template' => '^(w+).html$',
            'controller' => 'static',
            'action' => 'show',
            'params' => array(
                'name' => 1
            )
        ),
        'main' => array(
            'template' => null,
            'controller' => 'index',
            'action' => 'index'
        ),
        'dynamic' => array(
            'template' => '^(w+)/(w+)$',
            'controller' => 'dynamic',
            'action' => 'show',
            'params' => array(
                'category' => 1,
                'article' => 2
            )
        )
    ),
    'view' => array(
      // где будут лежать наши шаблоны (views)
        'viewPath' => APPLICATION_PATH . DIRECTORY_SEPARATOR . 'views',
      // имя нашего шаблона (layout)
        'template' => 'template.tpl'
    )
);

Вскользь хочу упоминуть лишь некоторые классы
MVC_Registry – стандартный паттерн Registry без излишеств, нужен нам для того, чтобы хранить данные и предоставлять доступ к ним из любого места в приложении.

MVC_Request – класс запроса, который мы строим в нашем “Фронт контроллере” и передаем в контроллеры приложения. Содержит параметры в запросе, активный контроллер и экшн.

MVC_View – класс, который просто инициализует Smarty и устанавливает все нужные нам значения путей для шаблонов.

“Front Controller”

Этот класс – точка входа для нашего приложение, хоть и должен был он реализован быть как Одиночка (Singleton)

MVC_FrontController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php

class MVC_FrontController
{

    protected $_routes;
    protected $_settings;
    protected $_controller;
    protected $_action;
    protected $_params;

    public function __construct($request)
    {
        $this->_initConfigs();
        $this->_initResources();
        $this->_initRoutes();
    }

    protected function _parseRequest()
    {
        return $_SERVER['REQUEST_URI'];
    }

    public function run()
    {
        $uri = $this->_parseRequest();

        $activeRoute = $this->_checkActiveRoute($uri);
        if (null === $activeRoute)
            throw new MVC_Exception('Cannot find active route');
        $this->_dispatch($activeRoute);

        $controllerName = sprintf('App_Controller_%sController', ucfirst($this->_controller));
        $controllerObj = new $controllerName(
                        new MVC_Request(
                                array('params' => $this->_params,
                                    'controller' => $this->_controller,
                                    'action' => $this->_action)
                        )
        );
        $methodName = $this->_action . 'Action';
        if (method_exists($controllerObj, $methodName)) {
            $controllerObj->$methodName();
            $controllerObj->render();
        } else {
            throw new MVC_Exception('action not found');
        }
    }

    protected function _initConfigs()
    {

        require_once APPLICATION_PATH . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'application.php';

        MVC_Registry::getInstance()->set('settings', $settings);
        $this->_settings = $settings;
    }

    /**
     * init resource autoloading
     */
    protected function _initResources()
    {
        spl_autoload_register(array('MVC_Autoload_Resource', 'autoload'));
    }

    /**
     * init routes
     */
    protected function _initRoutes()
    {
        if (isset($this->_settings['routes'])) {
            $this->_routes = $this->_settings['routes'];
        } else {
            throw new MVC_Exception('Routes must be specified');
        }
    }

    /**
     * get active route name
     *
     * @param string $uri
     * @return string active route name
     */
    protected function _checkActiveRoute($uri)
    {
        $uri = substr($uri, 1);
        if (trim($uri)) {
            $activeRoute = null;

            foreach($this->_routes as $name => $routeSettings) {
                if (!$routeSettings['template'])
                    continue;
                if (preg_match('@' . $routeSettings['template'] . '@', $uri, $matches)) {
                    if (isset($routeSettings['params'])) {
                        foreach($routeSettings['params'] as $paramName => $param) {
                            $this->_params[$paramName] = $matches[$param];
                        }
                    }
                    $activeRoute = $name;
                }
            }
        } else {
            $activeRoute = 'main';
        }

        return $activeRoute;
    }

    /**
     * dispatch
     *
     * @param string $activeRoute active route name
     */
    protected function _dispatch($activeRoute)
    {
        if (isset($this->_routes[$activeRoute])) {
            $this->_controller = $this->_routes[$activeRoute]['controller'];
            $this->_action = $this->_routes[$activeRoute]['action'];
        }
    }

}

Я старался комментировать не особо очевидные места, но если возникнут вопросы, обязательно задавайте их в комментериях внизу заметки. Метод _checkActiveRoute() определяет в зависимости от REQUEST_URI активный раут. И заполняет параметры для контроллера нашего приложения.

Метод _dispatch() по активному рауту определяет нужный в данный момент контроллер и экшн.

Метод run() запускает наш контроллер, передавая ему объект запроса с нужными нам параметрами. После запуска нужного экшна мы запускаем метод контроллера render() который и выводит контент.

MVC_Controller_Abstract

Этот класс раширяют все контроллеры нашего приложения, единственный метод, на котором стоит заостроить внимание это render()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function render()
    {
        $templatePath = $this->_view->template_dir . DIRECTORY_SEPARATOR;
        $templatePath .= strtolower(
                $this->getRequest()->getController()
                . DIRECTORY_SEPARATOR
                . $this->getRequest()->getAction() . '.tpl'
        );
        if (file_exists($templatePath) && is_readable($templatePath)) {
            $this->getView()->assign('tplName', $templatePath);

            if (null === $this->_view->template)
                $this->getView()->display('template.tpl');
            else {
                $this->getView()->display($this->getView()->template);
            }
        } else {
            throw new MVC_Exception("Template '{$templatePath}' not found");
        }
    }

Допустим мы прошли по адресу example.com/ нашего приложения. Наш “Фронт Контроллер” запустит app/controllers/IndexController.php и метод indexAction(). В нем, к примеру, мы присвоим переменной name значение vredniy

1
2
3
4
public function indexAction()
    {
        $this->getView()->assign('name', 'Vredniy');
    }

Дальше метод MVC_Controller_Abstract::render() отыщет наш шаблон (layout). В данном случает это, находящийся в папке app/views, файл template.tpl. Приведу его содержимое

1
2
3
4
5
6
7
8
9
10
<html>
    <head>
        <title>title</title>
    </head>
    <body>
        <h1>template</h1>
        {include file="$tplName"}
        <h2>footer</h2>
    </body>
</html>

Это и есть основной шаблон для нашего приложения. Вы спросите, а как же тогда выводится переменная name, значение которой мы присваивали в IndexController::indexAction(). Все просто: встроенная конструкция Smarty {include file=“$tplName”} заменяется содержимым соответствующего экшна видом.
Метода render() в MVC_Controller_Abstract

1
$this->getView()->assign('tplName', $templatePath);

Сам же вид экшна имеет вид

1
2
<b>{$name|upper}</b>
<h2>MAin page</h2>

Вот вроде и все. Данный фреймворк не предназначен для построения сложных сайтов, да и вообще для сайтов. Да и вообще ни для чего он не предназначен, кроме понимания некоторых важных для любого фреймворка вещей. В данном ознакомительном фреймворке куча ошибок и недоработок, реализация или исправления которых могли существенно захломить тестовый фреймворк и намного усложнить его понимание.

Спасибо всем за внимание, надеюсь, хоть часть мною написанного вам пригодится. Ах да, чуть не забыл прилагаю к заметке ссылку на репозиторий, чтобы вы смогли скачать данный фреймворк и запустить у себя.

UPDATE: Сегодня решил, что в связке MVC не может не быть модели, поэтому решил я ее сегодня дописать.

Модель в MVC

Добавил я два класса: первый это MVC_Db_Exception, чтобы выбрасывать эксепшны не просто MVC_Exception. Второй – освновной MVC_Db_Abstract, который мы будем расширять в модели. Приведу его код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php

class MVC_Db_Abstract
{

    /**
     * PDO object
     *
     * @var Pdo
     */
    private $_connection;
    /**
     * PDO options
     *
     * @var array
     */
    protected $_options = array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'");

    /**
     * PDO attributes
     *
     * @param array|string $attribs
     */
    public function __construct()
    {
        $this->_initConnection();
    }

   // инициализуем соединение, если его еще нет
    private function _initConnection()
    {
        if (!$this->_connection instanceof PDO) {
            $settings = MVC_Registry::get('settings');
            $dbSettings = $settings['database'];
            $dsn = sprintf('%s:host=%s;dbname=%s', $dbSettings['adapter'], $dbSettings['params']['host'], $dbSettings['params']['dbname']);
            try {
                $this->_connection = new PDO($dsn, $dbSettings['params']['username'], $dbSettings['params']['password'],
                                $this->_options);
            } catch (PDOException $e) {
                echo 'Connection failed: ' . $e->getMessage();
            }
        }
    }

   // возвращаем соединение, которое и будем использовать в моделях
    public function getConnection()
    {
        return $this->_connection;
    }

}

И внимание, кто пробует это проделать по заметке, будьте внимательны, потому что я чуть дописал MVC_Autoload_Resource, чтобы он помимо контроллеров мог подгружать еще и наши модельки. Самая актуальная версия в репозитории.

Использование модели

Создадим папку app/models, где и будут лежать наши модели. В ней создадим файл PageModel.php с таким содержимым

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

class App_Model_PageModel extends MVC_Db_Abstract
{

    /**
     * get page id
     *
     * @param int
     */
    public function getPage($id)
    {
        $statementString = 'SELECT id, alias, content FROM pages WHERE %s = ?';
        if (is_int($id)) {
            $statementString = sprintf($statementString, 'id');
        } else {
            throw new MVC_Db_Exception('id must be int');
        }
        $statement = $this->getConnection()->prepare($statementString);
        $statement->execute(array($id));
        return $statement->fetch();
    }

}

Данная модель умеет извлекать из таблицы pages нужные нам данные с помощью PHP Data Object (PHP) по id. Дамп базы данных я выложил в репозитории в папке data/dumps

Теперь в контроллере попробуем использовать нашу модель. Я взял для примера IndexController, в index экшн которого я дописал следующее.

1
2
3
4
5
6
7
8
9
public function indexAction()
{
  // создаем нашу модель
    $model = new App_Model_PageModel();
  // извлекаем из нее нужные нам данные и
  // отдаем их Smarty, т.е. во View
    $this->getView()->assign('data', $model->getPage(1));
    $this->getView()->assign('name', 'Vredniy');
}

В шаблоне views/index/index.tpl я добавил три строчки, которые и будут выводить содержимое:

1
2
3
<b>{$name|upper}</b>
<h2>{$data.alias}</h2>
{$data.content}

Вот вроде и все, еще раз удачи вам :)

Создание фреймворка

От автора: приветствую вас, друзья. С этого урока мы с вами начнем создание собственного PHP фреймворка. По итогу мы должны получить вполне функциональное решение, а заодно познакомиться с принципами работы современных PHP фреймворков. В этом уроке мы создадим начальную структуру фреймворка, а также начнем писать класс Router, который будет отвечать за маршрутизацию во фреймворке.

скачать исходникискачать урок

В видео версии урока вы найдете ответы на такие вопросы:

Что такое PHP фреймворк?

Профессия PHP-разработчик с нуля до PRO

Готовим PHP-разработчиков с нуля

Вы с нуля научитесь программировать сайты и веб-приложения на PHP, освоите фреймворк
Laravel
, напишете облачное хранилище и поработаете над интернет-магазином в команде.
Сможете устроиться на позицию Junior-разработчика.

Узнать подробнее

Командная стажировка под руководством тимлида

90 000 рублей средняя зарплата PHP-разработчика

3 проекта в портфолио для старта карьеры

Чем полезен процесс написания собственного фреймворка?

Что такое маршрутизация?

Какова структура адресов фреймворка?

Что такое контроллер и экшен (действие)?

Антон Шевчук // Web-разработчик

PHP Frameworks
Для начинающих “велосипедистов” иль просто любопытствующих…

Данная статья не призыв к действию, а лишь небольшая зарисовка на тему “Как бы я это сделал”. На данный момент у меня в отделе активно используется Zend Framework, и именно с ним я лучше всего знаком, поэтому не пугайтесь параллелей, это не реклама, ведь большинство фреймворков в равной степени сочетают в себе плюсы и минусы, а нам нужны лишь преимущества…

Правила

Начал бы с регламентирования правил:

  • Стандарты кодирования – лучше воспользоваться существующими, советую стандарты Zend Framework’а
  • Процесс добавления кода в репозиторий (даже если вы сами в проекте – это будет хорошо дисциплинировать), только не перегибайте палку, иначе это замедлит развитие проекта

Не выработав данных правил, вы рискуете превратить фреймворк в помойку. Так же, настоятельно рекомендую писать юнит тесты – они помогут сэкономить уйму времени.

Архитектура

Надеюсь большинство читателей уже знакома с патерном MVC (Model-View-Controller) – так давайте на нем и базировать наш фреймворк, использования чего-то иного, боюсь, будет отпугивать пользователей (тут я подразумеваю программистов :) ).

Model

В типовом проекте модель завязывается на одну таблицу в базе данных, но исключений хватает, поэтому не следует принимать данное утверждение за аксиому. Наша модель должна с легкостью работать с различными хранилищами данных, будь то БД, файлы, или память.

Давайте представим как мы будем пользоваться такой моделью:

// модель User использует в качестве хранилища БД
class Model_User extends Framework_Model_Database
{
    $_table = &amp;amp;amp;quot;users&amp;amp;amp;quot;;
    $_pkey = &amp;amp;amp;quot;id&amp;amp;amp;quot;;

    function getByLogin($login) { /*...*/ }
    function getByEmail($email) { /*...*/ }
}

// модель MainConfig использует в качестве хранилища ini файл
class Model_MainConfig extends Framework_Model_Ini
{
    protected $_file = &amp;amp;amp;quot;application.ini&amp;amp;amp;quot;;

    function setOption($key) { /*...*/ }
    function getOption($key) { /*...*/ }
}

// модель Registry использует в качестве хранилища память - некая альтернатива глобальным переменным
class Model_Registry extends Framework_Model_Memory
{
    function setOption($key) { /*...*/ }
    function getOption($key) { /*...*/ }
}

// модель Session использует в качестве хранилища файлы сессии
class Model_Session extends Framework_Model_Session
{
    protected $_namespace = &amp;amp;amp;quot;global&amp;amp;amp;quot;;

    function setOption($key) { /*...*/ }
    function getOption($key) { /*...*/ }
}

В действительности такими примерами я сильно коверкаю представление о MVC – ведь зачастую под моделью подразумевают некую бизнес модель, но никак не сессию или конфигурационный файл.

View

Каковы нынче требования к шаблонизатору? Лично для меня нативный PHP синтаксис, поддержка различного рода хелперов и фильтров. Так же должен быть реализован паттерн “двухэтапного представления” (Two Step View pattern), в ZF для этого служат два компонента – Zend_View и Zend_Layout.

Приведу пример такого представления:

&amp;amp;lt;?php if ($this-&amp;amp;gt;books): ?&amp;amp;gt;
    &amp;amp;lt;!-- Таблица из нескольких книг. --&amp;amp;gt;
    &amp;amp;lt;table&amp;amp;gt;
        &amp;amp;lt;tr&amp;amp;gt;
            &amp;amp;lt;th&amp;amp;gt;Author&amp;amp;lt;/th&amp;amp;gt;
            &amp;amp;lt;th&amp;amp;gt;Title&amp;amp;lt;/th&amp;amp;gt;
        &amp;amp;lt;/tr&amp;amp;gt;
        &amp;amp;lt;?php foreach ($this-&amp;amp;gt;books as $key =&amp;amp;gt; $val): ?&amp;amp;gt;
        &amp;amp;lt;tr&amp;amp;gt;
            &amp;amp;lt;td&amp;amp;gt;&amp;amp;lt;?php echo $this-&amp;amp;gt;escape($val['author']) ?&amp;amp;gt;&amp;amp;lt;/td&amp;amp;gt;
            &amp;amp;lt;td&amp;amp;gt;&amp;amp;lt;?php echo $this-&amp;amp;gt;escape($val['title']) ?&amp;amp;gt;&amp;amp;lt;/td&amp;amp;gt;
        &amp;amp;lt;/tr&amp;amp;gt;
        &amp;amp;lt;?php endforeach; ?&amp;amp;gt;
    &amp;amp;lt;/table&amp;amp;gt;
&amp;amp;lt;?php else: ?&amp;amp;gt;
    &amp;amp;lt;p&amp;amp;gt;Нет книг для отображения.&amp;amp;lt;/p&amp;amp;gt;
&amp;amp;lt;?php endif; ?&amp;amp;gt;

Пример использование layout’ов (взят из документации по Zend_Layout):

Layout Example

О да, в Zend Framework’е удачная реализация представления, она мне нравится, конечно, не без мелких нареканий, но в целом – это пять.

Controller

Контроллер должен выполнять свои обязанности – обрабатывать запрос, пинать модель и представление – дабы пользователь получил желаемый результат.

Давайте попробуем среагировать на запрос пользователя следующего вида:

http://example.com/?controller=users&amp;amp;amp;action=profile&amp;amp;amp;id=16

Так, проведем разбор – у нас просят показать профайл пользователя с id=16. Соответственно напрашивается существование контроллера users с методом profile, который бы смог получить в качестве параметра некий id:

// название контроллера должно содержать префикс - дабы чего не напутать
class Controller_Users extends Framework_Controller_Action
{
    public function actionProfile()
    {
        // получаем данные из запроса
        $id = $this-&amp;amp;gt;request-&amp;amp;gt;get('id');

        // пинаем модель
        $user = new Model_User();
        $user -&amp;amp;gt; getById($id);

        // закидываем данные в представление
        $this-&amp;amp;gt;view-&amp;amp;gt;user = $user;
    }
}

Естественно, на плечи контроллера так же ложится обязанность изменять формат представление, т.е. если нам надо вернуть данные в JSON формате, то никакого иного вывода быть не должно (это и так подразумевается самим MVC, но стоит лишний раз напомнить).

Кто повнимательней увидит в данном примере появление некого Request’a – это объект который занимается разбором входящего запроса. Зачем он нужен – об этом чуть далее.

Routers

Теперь немного о требованиях со стороны конечных пользователей – в частности о ЧПУ. Сравните следующие варианты ссылок:

http://example.com/?controller=users&amp;amp;amp;action=profile&amp;amp;amp;id=16 
http://example.com/users/profile/id/16/ // стандартная схема построения URL'a в ZF
http://example.com/users/profile/16/ // CodeIgniter
http://example.com/profile/16/ // возможное пожелание заказчика, и его нужно выполнять

Для генерации/разбора подобного входящего запроса в ZF используются роутеры – по факту – это правила построения URL’ов, нам так же придется их реализовать – и с этим сложно поспорить.

Мне больше по душе передача именнованых параметров – такой URL легче читаем, сравните:

http://example.com/users/list/page/2/limit/20/filter/active

и

http://example.com/users/list/2/20/active

Вы наверное захотите сразу засунуть данный функционал непосредственно в класс Request, но не спешите, ведь нам еще потребуется генерировать правильные URL во View – а вызывать там объект Request – немного не логично, давайте таки оставим это на совести отдельного класса, к которому может обращаться как Request так и View

Request & Response

С назначением класса Request думаю проблем не возникает – в его функции входит не так много:

  • обработка входящих параметров всеми правилами из Router’а
  • отдавать параметры в контроллер по требованию

Но есть еще Response – о нем я как то не упоминал ранее, что же он должен делать:

  • формировать заголовок ответа
  • формировать ответ – т.е. брать view, оборачивать в некий layout и на выход

Мне не очень нравится реализация Response в ZF – слишком много лишнего в нем

Modules

Фреймворк должен быть модульным, т.е. написав какой-то модуль (блог, форум, и т.д.) вы сможете с легкостью использовать данный код в других приложениях. Для этого нам понадобится лишь отделить MVC каждого модуля в свою директорию, при этом какой-то модуль останется за главного.

Core

Теперь стоит перейти к самому вкусному – непосредственно к ядру системы, его функционал мы практически уже описали, стоит лишь подвести черту:

  1. При инициализации входящий запрос должен быть обработан всеми правилами Router’ов, дабы объект Request мог вернуть нам запрашиваемое значение по ключу
  2. Объект Request так же должен знать, какой модуль/контроллер/экшен запрашивается
  3. Ядро должно подгрузить необходимый контроллер и вызвать запрашиваемый экшен (метод контроллера)
  4. После отработки контроллера вызывается Response и ставит точку

Для гибкости в систему стоит добавить либо хуки, либо плагины на каждый из перечисленных этапов.

Вспомогательный классы

Если вы захотите потренироваться в написании “велосипедов”, то можете начать отсюда:

  • Работа с БД – необходима поддержка MySQL, SQLite, PostgreSQL (это минимум), а в целом стоит уделить этому пункту много внимания, т.к. он один может привлечь множество пользователей
  • Валидаторы – необходимая вещь, для экономии времени при написании форм (см. Zend_Validate)
  • Транслятор – для реализации мультиязычности в системе, возможно хватит и gettext’a, но не стоит на это надеяться
  • Почта – можно обойтись лишь функцией mail, но это как-то не по-взрослому
  • Пагинатор – для решения тривиальной задачи – разбиение по страницам (см. Zend_Paginator)
  • Навигатор – построение меню, карты сайта и “хлебных крошек” (см. Zend_Navigation)
  • Кэширование – без него никуда (см. Zend_Cache)
  • Конфигурационные файлы – Zend_Config слишком большой для того, чтобы обрабатывать лишь один ini файл, тут можете попрактиковаться, но все же посматривайте на Zend_Config_Ini
  • Автозагрузчик – очень полезная вещь, и главное удобное – Zend_Loader
  • ACL – возможно потребуется – по крайней мере, распределение прав по запросу модуль/контроллер/экшен лучше пусть будет зашит в системе

Я не случайно привожу ссылки на пакеты Zend Framewrok’а – они вполне адекватны и самостоятельны, могут быть использованы сами по себе, т.е. никто ведь не мtшает вам построить свой фреймворк из кубиков Zend’a (и вот тому пример: ZYM engine)

Тривиальные задачи

В фреймворке должен быть заложен функционал для решения следующих тривиальных задач (мелких и не очень):

  • Redirect – самый обычный, вызывается из контроллера
  • Forward – это пересылка с одного модуль/контроллер/экшен на другой без перезагрузки страницы
  • Messages – различные сообщения, с возможностью получения их после перезагрузки страницы
  • Scaffold – быстрый способ построения приложения для редактирования записей в базе данных (утрированно)

Еще лучше, если с фреймворком будет поставляться готовая к использованию CMS система – она позволит популяризировать ваше детище, и возможно привлечет сторонних разработчиков.

Возможно чего забыл из “тривиального” – пишите…

Структура каталога

И так, что у нас получается, если взглянуть на файловую систему (в document_root должна лежать лишь папка public):

project
|-- application
|    |-- configs
|    |-- layouts
|    |-- controllers
|    |-- models
|    |-- views
|    `-- modules
|         `-- &amp;amp;lt;module_name&amp;amp;gt;
|              |-- layouts
|              |-- controllers
|              |-- models
|              `-- views
|-- data
|    |-- cache
|    |-- logs
|    `-- sessions
|-- library
|    `-- Framework
|-- public
|    |-- styles
|    |-- scripts
|    |-- images
|    |-- uploads
|    |-- .htaccess
|    `-- index.php
`-- tests

Вывод

To Be Or Not To Be – решать вам, как по мне – можно смириться с недостатками какого-то одного фреймворка, и наслаждаться его преимуществами. Возможно, вы попытаетесь написать свое решение или скрестить существующие, но не забываете – написание такого рода приложения влечет за собой ответственность по его поддержке.

P.S. Для всех моих читателей – RSS канала доступен по адресу http://anton.shevchuk.name/feed/ (если Вы используете какой иной – исправьте).
P.P.S. Еще я достаточно активно зависаю на твитере, так что следуйте за мной…

Если Вы давно работаете в сфере web-разработок, то рано или поздно встанет вопрос о создании своего фреймворка для быстрой вёрстки. Например, Вы получили в работу новый проект. Вы понимаете, что использовать новые крупные библиотеки (bootstrap и тому подобные) на данном проекте нельзя, т.к. сразу поедет разметка всего сайта. А работу нужно выполнить быстро, причём задачи полностью переделать сайт у Вас нет. Вам просто нужно быстро сверстать пару страниц или пару блоков. И писать кучу одинаковых стилей, которые Вы привыкли использовать стандартно в каждом своём проекте, у Вас просто нет времени. И тут приходит идея: а почему бы не сделать свой собственный фреймворк, который поможет быстро вставлять нужные классы в html и в процессе вёрстки не нужно будет каждые 5 секунд заходить в css файл и писать там новые правила. Это удобно, быстро и очень эффективно. Например, я использую для данных целей свойства, название классов которых берётся из Emmet. Кто ещё не в курсе (такие вообще есть..?), Emmet — это самый быстрый способ писать html и css, используя конструкции типа: .row>.col-md-6*2>(h3+p). В html после нажатия клавиши Tab получится вот такая разметка:

<div class="row">
    <div class="col-md-6">
        <h3></h3>
        <p></p>
    </div>
    <div class="col-md-6">
        <h3></h3>
        <p></p>
    </div>
</div>

Т.е. с помощью одной строчки Emmet мы можем за одну минуту создать разметку на всю нашу страницу, которую просто нужно будет заполнить контентом. В примере Emmet-кода выше, мы указываем, что нам нужен родительский элемент <div> с классом .row, внутри которого будет 2 элемента <div> с классом .col-md-6 и внутри него будут 2 элемента (<h3> и <p>), находящиеся на одном уровне.

Также с Emmet мы пишем правила css. Например, вот так: пишем mt10 и нажимаем Tab. Получаем: margin-top: 10px; Стили таким образом можно писать очень быстро, причём без каких-либо опечаток.

И у меня возник вопрос: почему бы не сделать классы css, в которых можно будет указывать основные правила Emmet. Например, написать:

<div class="p20"></div>

и получить элемент <div>, с отступами по 20 пикселей.

Или использовать другие правила Emmet для аналогичной быстрой и понятной разметки. Чтобы автоматизировать работу, я написал фреймворк, и ниже рассказываю, как сделать аналогичный под Ваши нужды.

Итак, как написать свой фреймворк для быстрой и эффективной работы

Нам понадобится:

  1. Sass (Scss)
  2. Программа Koala

Чтобы написание стилей не было утомительным занятием, будем использовать препроцессор Sass. А точнее, Scss — «диалект» языка Sass. Он позволяет писать стили, используя переменные, функции и математические правила. Например, мы можем написать:

@for $i from 0 through 10 {
    .mt#{$i*5} {margin-top: 5 * $i +px !important;}
}

и после работы интерпретатора получим следующий набор правил:

.mt0{margin-top:0px !important}
.mt5{margin-top:5px !important}
.mt10{margin-top:10px !important}
.mt15{margin-top:15px !important}
.mt20{margin-top:20px !important}
.mt25{margin-top:25px !important}
.mt30{margin-top:30px !important}
.mt35{margin-top:35px !important}
.mt40{margin-top:40px !important}
.mt45{margin-top:45px !important}
.mt50{margin-top:50px !important}

Т.е. мы создаём в цикле переменную $i и, начиная с нулевого значения, проходим цикл 10 раз. При каждой итерации цикла мы создаём класс, начинающийся на .mt и со значением переменной $i, умноженной на 5. Данному классу мы присваиваем правило margin-top, в значении которого мы указываем количество пикселов, равное значению переменной $i, умноженной на 5. И добавляем !important, чтобы наш стиль точно перебивал все остальные стили сайта. Вообще, все мы знаем, что !important лучше не использовать, но в нашем случае это позволительно, т.к. мы делаем фреймворк и нам нужно, чтобы данное правило сработало наверняка, иначе попросту наша разметка не будет выглядеть как нужно.

Мощь Scss именно в этом. В правиле выше мы могли бы написать скажем сделать цикл не 10, а 100 раз. И мы получили бы сто разных стилей за долю секунды, не используя копипаст и кучу правок. Изучите Scss и Вы увидите, насколько вырастет Ваша производительность и Ваша ценность как специалиста.

Следовательно, наша задача сводится к тому, чтобы с помощью шаблона выше создать разные правила для margin, padding и других правил css. В качестве примера, я сделал основные правила, плюс добавил несколько своих собственных стилей.

Итог css-файла получился следующий: https://bitbucket.org/lisogorsky/dev/raw/1f43e26a2ff513970e97b143cda576e6cb2afbb4/cssFramework/my.css

Если Вам понравилась идея моего фреймворка, Вы можете использовать его на своём проекте. Для этого, вставьте в область <head> Вашего сайта следующую строку:

<link rel="stylesheet" href="https://bitbucket.org/lisogorsky/dev/raw/1f43e26a2ff513970e97b143cda576e6cb2afbb4/cssFramework/my.css">

Или перейдите по ссылке, скопируйте стили и вставьте в файл стилей своего сайта.

Оригинал файла Scss: https://bitbucket.org/lisogorsky/dev/raw/1f43e26a2ff513970e97b143cda576e6cb2afbb4/cssFramework/my.scss

Скачайте и редактируйте файл Scss на своё усмотрение, добавляйте свои правила по аналогии.

Далее нам понадобится программа-компилятор. Мы будем использовать Koala. Это очень мощная, гибкая и абсолютно бесплатная программа. Она будет переводить Ваши файлы Scss в вид обычного css. Разумеется, мы можем настроить всё это на сервере, но об этом расскажу как-нибудь позже, в других постах. Сейчас наша задача — научиться быстро создавать собственные фреймворки и использовать их в своих проектах. Скачиваем программу на официальном сайте: koala-app.com

После установки, создаём папку, в которую помещаем наш отредактированный Scss файл. Далее эту папку перетаскиваем в окно программы. Программа сразу увидит Ваш файл. Кликните на него. Справа появится меню. Ставим галочки: «Автокомпиляция«, «Autoprefix» и стиль вывода «compact» или «compressed«. При варианте «compact» все стили будут отдельно на каждой строчке, а при «compressed» все стили будут минимизированы в одну строку. Нажмите единственную кнопку «compile«. Ваш файл стилей появится в той же папке, что и файл Scss.

Используйте и наслаждайтесь результатом!

Подписывайтесь на группу в ВКонтакте, вступайте в сообщество на Facebook, чтобы всегда быть в курсе актуальных выпусков
Web development blog!

Понравилась статья? Поделить с друзьями:
  • Как помочь ребенку исправить почерк в 4 классе
  • Как найти длительность дня
  • Как найти subnet mask
  • На геймпаде перепутаны кнопки как исправить windows 10
  • Как найти созданное резюме на авито