Pull to refresh

BemPHP: реализация методологии БЭМ средствами PHP

Reading time 8 min
Views 8.5K
Пришла мне тут как-то мысль освоить PHP, а, как известно, лучший способ изучить язык – это создать на нем велосипед фреймворк. При ковырянии в различных форумах и топиках заинтересовала меня одна методология, которую пропагандируют в уважаемой компании «Яндекс» — БЭМ. Кто ещё не в курсе этой методологии, почитайте на официальной страничке. Так же на Хабре есть публикация «Верстка для самых маленьких. Верстаем страницу по БЭМу» от хабраюзера xnim, в котором все объяснятся на конкретном примере. «Яндекс» написали свои модули и скрипты сборки проектов, однако выполнены они все на Node.js, а вот на PHP обнаружить что-то подобное мне не удалось (хотя, признаюсь честно, я особо и не искал). К тому же, PHP, как объектно-ориентированный язык, дает интересные возможности.



Старт


Скачиваем с GitHub’а папку BemPHP и кладем её в папку со свои проектом. Дальше подключаем BemPHP.php:
include_once "BemPHP/BemPHP.php";

Если у вас еще нет функции автозагрузки, регистрируем её:
BemPHP::register_autoload();

Теперь давайте создадим свой первый блок. Для этого необходимо в папке BemPHP/blocks/ создать папку с именем блока, например 'b-block-name'. Собственно всё. Этого уже достаточно что бы можно было загрузить в хранилище блоков объект класса Block с именем b-block-name. Давайте загрузим его:
BemPHP::includeBlocksList('b-block-name');

Конечно загружать каждый блок по отдельности таким образом было бы слишком, поэтому можно написать их через запятую:
BemPHP::includeBlocksList('b-block-1, b-block-2, block-3’);

Обратите внимание, что 'block-3’ написан без префикса ‘b-’, это не ошибка, в данном случае все равно будет загружен блок с именем 'b-block-3’, если он, конечно, есть. Префикс 'b-‘ указывает, что в этой папке находится описание блока, если в папке blocks будет лежать папка которая не начинается с этого префикса, она будет проигнорирована. Указывать префикс в методе includeBlocksList не обязательно.
Перечисление всех необходимых блоков тоже дело довольно трудоемкое, поэтому можно писать маски, например:
BemPHP::includeBlocksList('b-page1*, *input*, *down’);

В данном случае загрузятся все блоки начинающиеся с b-page1, все в названии которых присутствует слово input и все заканчивающиеся на down. Если вам, например, понадобятся все начинающиеся с b-page1 и имеющие input, то запись будет выглядеть так: b-page1*input*. Ну а если вам нужно загрузить все блоки без исключения, то так:
BemPHP::includeBlocksList('*');

Теперь в папку b-block-name можно добавить стили и описание поведения. Для этих целей создаем в папке файлы b-block-name.css и b-block-name.js, они автоматически загрузятся в объект. Стоит отметить, что имена файлов должны совпадать с именем блока, в противном случае они будут игнорированы. Так же во время загрузки произойдет проверка файла b-block-name.css на наличие в нем описания стиля класса '.b-block-name’ и компрессия, то есть удаление лишних пробелов, переносов строк и комментариев.

Определение блока


Поскольку мы имеем дело с объектно-ориентированным языком, то было бы здорово создавать блоки как объекты средствами PHP. Для этого в папке b-block-name создадим файл b-block-name.php. В этом случае файлы .css и .js будут проигнорированы и всё описание блока будет браться из php-файла.

use BemPHP\Block;

$_blockName_ = 'b-block-name'; /* обязательно указать имя блока (оно должно быть уникальным) */
$_blockObj_ = new Block($_blockName_);
$_blockObj_->setBlockDir(__DIR__) /*обязательно указать директорию блока, иначе подключение файлов может работать не корректно*/
           ->loadCSSFile($_blockName_.'.css') /* загружаем данные из css файла (если отправить null, то будет происходить поиск файла $_blockName_.'.css') */
           ->loadJSFile()/*загрузка файла типа .js*/
           ->addCssRule('color:red','hover') /*добавляет в конце описания стиля еще одно правило*/
           ->setTag('a') /*добавляем тэг. если тэг не определен то по умолчанию поставиться Block::TAG_DEFAULT*/
           ->setAttribute('href','http://tricksterz.ru') /*задаем атрибуты тэга*/
           ->setContent('Tricksterz') /*добавляем контент*/
           ->addContent('<b>',true) /*добавляем контент в начало*/
           ->addContent('</b>') /*добавляем в конец*/
;

Здесь мы создаем объект класса Block, который при создании будет сохранен в хранилище блоков, указываем директорию где хранятся файлы блока, загружаем файлы (путь указывается относительно директории указанной выше), добавляем правило стиля (второй параметр – это псевдокласс), задаем тэг блока (по умолчанию задается div), добавляем атрибут, и вставляем контент. Стоит отметить два метода – это setContent и addContent. Первый – задает контент удаляя предыдущий, второй добавляет контент, при этом у метода addContent есть второй параметр, который в случае true добавляет контент в начало, а в случае false – в конец существующего контента. По умолчанию false.

Растим дерево


Блоки созданы и загружены в хранилище блоков, теперь давайте выведем их на свет. Для этого необходимо подключить модуль Tree и построить БЭМ-дерево:
use BemPHP\Tree;	
[…]
echo Tree::html('b-block-name');

В создаваемом html-документе появится запись:
<a class='b-block-name' href='http://tricksterz.ru' ><b>Tricksterz</b></a>

Все хорошо, вот только у нас нет описания стиля класса ‘.b-block-name’. Для этого в заголовке страницы внутри тэгов style выводим css всех наших блоков:
<?php
        echo "<style> ";
        print_r(BemPHP::getCss());
        echo "</style>";
    ?>

Аналогично поступаем с js-скриптами:
<script>
       <?php
              print_r(BemPHP::getJs());
       ?>
</script>

В дальнейшем эти скрипты можно минимизировать и вывести в отдельные файлы (планируется реализация в релизе 1.2).
Стоит отметить, что метод Tree::html($blockName), возвращает объект, но объект не блока, как такового, а блока в обертке. Нужно это для того, что бы была возможность переопределять блок «на лету». Например код:
echo Tree::html('b-block-name')->setTag('p');
echo Tree::html('b-block-name');

Выведет следующее:
<p class='b-block-name' href='http://tricksterz.ru' ><b>Tricksterz</b></p>
<a class='b-block-name' href='http://tricksterz.ru' ><b>Tricksterz</b></a>

То есть сам блок остался не тронутым, просто во время вывода мы переопределили его тэг.
Поскольку Tree::html() возвращает объект, то мы можем присвоить ссылку на него какой-нибудь переменной:
$b_block_name = Tree::html(‘b-block-name’);

В данном случае, если мы переопределим тэг, то он так и останется «жить» в этой переменной, так как мы меняем свойства конкретного объекта обертки:
echo $b_block_name->setTag('p');
echo $b_block_name;

Выведет:
<p class='b-block-name' href='http://tricksterz.ru' ><b>Tricksterz</b></p>
<p class='b-block-name' href='http://tricksterz.ru' ><b>Tricksterz</b></p>

Для того, что бы вернуть обертку в изначальное состояние, т.е. в состояние изначального блока, нужно использовать метод getDefault():
echo $b_block_name->setTag('p');  // будет тэг  p
echo $b_block_name->getDefault();  // будет тэг a

Таким образом, можно, например, создать файл где будут описаны все блоки в виде переменных:
$b_menu = Tree::html(‘b-menu’);
$b_menu__item = Tree::html(‘b-menu__item’);
[…]

Подключить его и описывать БЭМ-деревья с подсветкой синтаксиса:
echo $b_menu->setContent(
        $b_menu__item->setContent('Home')->setAttribute('href','#home').
        $b_menu__item->setContent('Music')->setAttribute('href','#music').
        $b_menu__item->setContent('Video')->setAttribute('href','#video')
         );

Составные блоки


«С блоками вроде все понятно, а как же элементы и модификаторы?» — воскликнет внимательный читатель. Дело в том, что элементы и модификаторы по сути те же самые блоки, только они применяются в связке со своими родительскими блоками. Для понимания разберем конкретный пример, который описан в ранее упомянутом мной посте. Там есть реализация вот такого меню:
image
Давайте его сделаем. Для начала создаем папку ‘b-menu’ внутри неё b-menu.css с содержанием:
.b-menu
{
    margin:0;
    padding:0;
    list-style:none; 
    width:100%;
    display:table;
    table-layout: fixed;   
}

Дальше папка b-link в ней b-link.css:
.b-link
{
    color:#525252;
    font-size:12px;      
}

В папке b-link создаем папку модификатора b-link_menu:
.b-link_menu
{
    text-align:center;
    color:#bfbfbf;
    cursor:pointer;
    font-size:14px;
    height:38px;
    background-color:#f3f3f3;
    line-height:38px;
    border: 1px solid #e7e7e7;
    display:table-cell;
    text-decoration: none;
}

В папке b-link_menu создаем еще один модификатор b-link_menu_active:
.b-link_menu_active
{
    color:#fefefe;
    background-color:#29c5e6;
    border:1px solid #29c5e6;
   cursor: default;
}

Теперь определим параметры блоков, создав в соответствующих папках файлы b-menu.php, b-link.php и b-link_menu_active.php:
//b-menu.php

$_blockObj_ = new Block('b-menu');
$_blockObj_->setBlockDir(__DIR__)
           ->loadCSSFile()
           ->setTag('nav');

//b-link.php

$_blockObj_ = new Block('b-link');
$_blockObj_->setBlockDir(__DIR__)
           ->loadCSSFile()
           ->setTag(‘a’);

//b-link_menu_active.php

$_blockObj_ = new Block('b-link_menu_active');
$_blockObj_->setBlockDir(__DIR__)
           ->loadCSSFile()
           ->setTag(‘div’);

А теперь все это добавим к нам на страничку:
//массив названий разделов и ссылок
$menu_items_arr = array('HOME'=>'home',
                    'ABOUT US'=>'aboutus',
                    'SERVICES'=>'services',
                    'PARTNERS'=>'partners',
                    'CUSTOMERS'=>'customers',
                    'PROJECTS'=>'projects',
                    'CAREERS'=>'careers',
                    'CONTACT'=>'contact');

// в цикле создаем дерево разделов
foreach ($menu_items_arr as $itemName=>$param) {
          if ($_GET['param']==$param)  $active = ' b-link_menu_active'; else $active=null;
            $menu_items_tree = $menu_items_tree.Tree::html('b-link b-link_menu'.$active)->setAttribute('href','?param='.$param)->setContent($itemName);
        }
// оборачиваем дерево разделов в меню
$main_menu = Tree::html('b-menu')->setContent($menu_items_tree);
echo $main_menu;

Как видите в метод Tree::html() можно добавлять сразу несколько имен блоков через пробел. В этом случае произойдет каскадное переназначение параметров блоков. Например в блоке b-link определен тэг 'a', а в блоке b-link_menu_active тэг ‘div’. Если мы пишем Tree::html(‘b-link b-link_menu’) при выводе будет указан тэг ‘a’, потому что в блоке b-link_menu тэг не определен и он будет наследован у предыдущего блока. В случае записи Tree::html(‘b-link b-link_menu b-link_menu_active’) будет указан тэг ‘div’, так как он определен у блока находящегося в конце цепочки. То же самое будет происходить с контентом. С атрибутами будет происходить следующее: если данный атрибут уже есть, то он будет переопределен, если такого атрибута у предыдущего блока не было, то он будет добавлен к тем атрибутам, которые уже были.

Плюшки


Если вы взгляните на html-код, который у вас получится в результате создания своей страницы, то прочитать его будет трудновато, потому что в нем отсутствуют отступы и переносы строк, но если вам нужно будет посмотреть сгенерированный html какого-нибудь блока, то его можно «подсветить» методом lighting():
echo Tree::html('b-block-name')->lighting();

Тогда в сгенерированном html будет следующее:
<!-- == BLOCK b-block-name HERE!!! == --!>
 
<a class='b-block-name' href='http://tricksterz.ru' ><b>Tricksterz</b></a>
 
<!-- ========= END ========= --!>

И вы без труда сможете найти нужный вам код.
Так же в BemPHP есть класс для записи логов – LogWriter. Добавить туда запись можно так:
LogWriter::putLog('Комментарий',3);

В этом случае будет создан объект Log, с msg = ‘Комментарий’ и msgType =’3’, помещенный в хранилище логов класса LogStorage. Вы можете реализовать свой собственный вывод логов, для этого нужно создать класс, с подключенным интерфейсом Notifycator и методом notify($logStorage), который в качестве параметра принимает объект класса LogStorage. А можно воспользоваться уже существующим логгером:
echo BemPHP::showLogger();

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

Стоит отметить, что для работы Logger’а нужно подключение библиотеки JQuery и что вызов метода ShowLogger должен происходить самым последним на вашей странице.

Зачем все это нужно?


Идея методологии БЭМ заключается в том, что бы создавать верстку независимыми блоками. При таком подходе можно быть спокойным, что во время внесений исправлений ваша верстка не развалится. Так же можно более эффективно распределять задачи внутри команды разработчиков и создать единый язык модели для всех лиц относящихся к проекту. BemPHP позволяет перенести методологию в объектно-ориентированную среду. В результате чего вы получаете не просто верстку, а объектно-ориентированную верстку!

Буду рад вашим комментариям, замечаниям и вопросам.
Tags:
Hubs:
+3
Comments 20
Comments Comments 20

Articles