Рубрики
PHP

БЭМ в PHP для WordPress

К хорошему привыкаешь быстро, особенно, когда верстаешь страницу в стеке Vue + Pug + Bem!

На мой взгляд это наиболее удобный стек для верстки.

Вот как выглядит код на этом стеке:

+b.product
	+e.inner
		+e.image
		+e.info
			+e.H2.title {{ product.title }}
			+e.price._new {{ product.price }}

А так этот же код выглядит на чистом HTML:

<div class="product">
	<div class="product__inner">
		<div class="product__image"></div>
		<div class="product__info">
			<h2 class="product__title">{{ product.title }}</h2>
			<div class="product__price product__price_new">{{ product.price }}</div>
		</div>
	</div>
</div>

Как видно из примера – вместо каждого +e дописывается название, взятое из предыдущего +b.

Синтаксис таков:

  • Часть названия класса, которая идет после точки компилируется в имя дочернего элемента:+e.inner > product__inner;
  • Часть названия класса, которая идет после точки и начинается с символа нижнего подчеркивания компилируется в имя миксина: +e.price._new > product__price product__price_new;

Работая с WordPress реализовать такую роскошь для создания шаблонов сложно, хотя можно… но кому это вообще надо? )

Делая очередной шаблон страницы на WordPress я очень скучал по простому созданию классов в стиле БЭМ и решил хоть как-тот облегчить себе задачу.

Техническое задание

Без использования шаблонизатора не удастся легко и просто реализовать работу через псевдо-переменные +b и +e, поэтому придется придумать что-то попроще.

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

  1. Если указано название класса, не начинающееся с точки, это должен быть родительский класс, например product;
  2. Если за родительским классом идет один или более классов, начинающихся с точки, после которой нет символа нижнего подчеркивания, то должно быть создано столько экземпляров дочерних классов, сколько объявлено, например: product.one.two.three > product__one product__two product__three;
  3. Если после точки идет символ нижнего подчеркивания – это миксин и он должен быть дописан к каждому классу, например: product.one.two.three._first._second > product__one product__two product__three product__one_first product__one_second product__two_first product__two_second product__three_first product__three_second.

Реализация

Я хочу, чтоб это было максимально просто, на сколько это возможно в условиях создания шаблонов в WordPress, то есть в условия написания кода на чистом PHP.

Пусть функция будет называться bem и для начала пусть она принимает один параметр – строку, которая является цепочкой частей классов:

bem('product.one.two.three._first._second');

Объявим функцию:

function bem( $classTrail = '' ) {
	if ( empty( $classTrail ) ) {
		return '';
	}
}

При этом я сразу говорю, что если на вход ничего не пришло, следует вернуть пустую строку.

Далее следует разбить строку на массив:

$classTrail = array_values( array_filter( explode( '.', $classTrail ) ) );

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

$mixins  = [];
$classes = [];
$block   = '';
$count   = sizeof( $classTrail );

Далее делаем перебор нашего массива $classTrail:

foreach ( $classTrail as $i => $item ) {
    // код
}

Первым делом следует к составному элементу $block приписать название родительского класса, в нашем случае это product. Это следует сделать только в том случае, если мы на первой итерации:

if ( 0 === $i ) {
	$block = $classTrail[ $i ];
}

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

if ( 0 === $i ) {
	$block = $classTrail[ $i ];

	if ( 1 == $count ) {
		$classes[] = $block;
	}
}

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

if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
    // код
}

И если это миксин, сразу добавим его в список миксинов:

if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
	$mixins[] = $classTrail[ $i ];
}

Если при этом в нашей цепочке всего 2 элемента, это означает, что указан родительский класс и миксин: product._new, в этом случае следует часть составного класса добавить в список классов:

if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
	$mixins[] = $classTrail[ $i ];
	if ( 2 == $count ) {
		$classes[] = $block;
	}
}

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

if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
	$mixins[] = $classTrail[ $i ];
	if ( 2 == $count ) {
		$classes[] = $block;
	}
}
else {
	$classes[] = $block . '__' . $classTrail[ $i ];
}

Таким образом мы составили список классов и миксинов:

$mixins  = [];
$classes = [];
$block   = '';
$count   = sizeof( $classTrail );
foreach ( $classTrail as $i => $item ) {
	if ( 0 === $i ) {
		$block = $classTrail[ $i ];

		if ( 1 == $count ) {
			$classes[] = $block;
		}
	}
	else {

		if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
			$mixins[] = $classTrail[ $i ];
			if ( 2 == $count ) {
				$classes[] = $block;
			}
		}
		else {
			$classes[] = $block . '__' . $classTrail[ $i ];
		}
	}
}

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

foreach ( $classes as $i => $class ) {
	foreach ( $mixins as $j => $mixin ) {
		$classes[] = $classes[ $i ] . $mixin;
	}
}

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

bem('product._new js.product');
// в результатае будет: product product_new js__product

Для этого нам необходимо строку разбить на массив по пробелам и весь код, написанный выше, поместить в еще один цикл:

$trails = [];

$classTrails = array_values( array_filter( explode( ' ', $classTrail ) ) );

foreach ( $classTrails as $classTrail ) {

	$classTrail = array_values( array_filter( explode( '.', $classTrail ) ) );

	$mixins  = [];
	$classes = [];
	$block   = '';
	$count   = sizeof( $classTrail );
	foreach ( $classTrail as $i => $item ) {
		if ( 0 === $i ) {
			$block = $classTrail[ $i ];

			if ( 1 == $count ) {
				$classes[] = $block;
			}
		}
		else {

			if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
				$mixins[] = $classTrail[ $i ];
				if ( 2 == $count ) {
					$classes[] = $block;
				}
			}
			else {
				$classes[] = $block . '__' . $classTrail[ $i ];
			}
		}
	}

	foreach ( $classes as $i => $class ) {
		foreach ( $mixins as $j => $mixin ) {
			$classes[] = $classes[ $i ] . $mixin;
		}
	}

	$trails = array_merge( $trails, $classes );
}

Теперь осталось вернуть результат. Его можно вернуть в виде:

  • массива;
  • строки;
  • строки вида class="product product_new".

Для этого я ввел еще два входных параметра в функции, мне удобно было именно так:

  • $toAttribute = false – венуть классы внутри атрибута class;
  • $isArray = true – вернуть массив.
bem('product._new js.product', true);
// вернет class="product product_new"

bem('product._new js.product', false, false);
// вернет 'product product_new'

bem('product._new js.product');
// вернет массив

Полный код функции

/**
 * Function that creates the classes chain in BEM style
 *
 * @param string $classTrail
 * @param bool   $toAttribute
 * @param bool   $isArray
 *
 * @return array|string
 */
function bem( $classTrail = '', $toAttribute = false, $isArray = true ) {
	if ( empty( $classTrail ) ) {
		return '';
	}

	$trails = [];

	$classTrails = array_values( array_filter( explode( ' ', $classTrail ) ) );

	foreach ( $classTrails as $classTrail ) {

		$classTrail = array_values( array_filter( explode( '.', $classTrail ) ) );

		$mixins  = [];
		$classes = [];
		$block   = '';
		$count   = sizeof( $classTrail );
		foreach ( $classTrail as $i => $item ) {
			if ( 0 === $i ) {
				$block = $classTrail[ $i ];

				if ( 1 == $count ) {
					$classes[] = $block;
				}
			}
			else {

				if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
					$mixins[] = $classTrail[ $i ];
					if ( 2 == $count ) {
						$classes[] = $block;
					}
				}
				else {
					$classes[] = $block . '__' . $classTrail[ $i ];
				}
			}
		}

		foreach ( $classes as $i => $class ) {
			foreach ( $mixins as $j => $mixin ) {
				$classes[] = $classes[ $i ] . $mixin;
			}
		}

		$trails = array_merge( $trails, $classes );
	}

	if ( ! empty( $toAttribute ) ) {
		return ' class="' . join( ' ', $trails ) . '" ';
	}

	// if $isArray is false
	if ( empty( $isArray ) ) {
		$trails = join( ' ', $trails );
	}

	// return classes as array
	return $trails;
}

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

Благодаря данной функции наш шаблон станет выглядеть так:

<div <?php echo bem( 'product', true ); ?>>
	<div <?php echo bem( 'product.inner', true ); ?>>
		<div <?php echo bem( 'product.image', true ); ?>></div>
		<div <?php echo bem( 'product.info', true ); ?>>
			<h2 <?php echo bem( 'product.title', true ); ?>>{{ product.title }}</h2>
			<div <?php echo bem( 'product.price', true ); ?>>{{ product.price }}</div>
		</div>
	</div>
</div>

Кому-то может показаться, что стало хуже… но этот только в том случае, если вы не работаете с БЭМ, да еще и в WordPress ))

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