Веб-разработка
April 22, 2022

Знакомство с PHP — Тренажёр HTML Academy

Возвращаемся к нашему препроцессору. Этот блок, возможно, будет просто колоссальным, даже в сравнении с предыдущей статьёй по JavaScript, так как это, судя по всему, второй и последний блок в тренажёре про PHP.

Что-то мне подсказывает, что это не потому что PHP такой лёгкий, что этих двух блоков хватит, а как раз наоборот — тут только вводная часть, более-менее, а дальше курсы, интенсивы и т. д.

Я, кстати, сейчас прохожу у всё той же HTML Academy бесплатный интенсив по HTML и CSS. Может быть позже про него напишу, если будет о чём.

Небольшое отличие от остальных блоков — тут не будет ни практики, ни даже испытание, только уроки. В общем, с PHP в тренажёрах HTML Academy всё сложно, поэтому будем с ним разбираться уже позже на курсах или, может, в других интерактивных тренажёрах.

Часть 1: Условия в PHP

Если в блоке про JavaScript условной конструкции было изначально уделено пару уроков и потом её развивали и дополняли в течение всего блока, то тут сразу всю первую часть посвятили условиям.

Сама условная конструкция в коде PHP выглядит и работает также как в JS, даже с таким же синтаксисом, по крайней мере для двух условий:

if (условие) {
  действия;
} else {
  другие действия;
}

Меняется наполнение, понятное дело, но сама конструкция очень похожа.

Также в этом блоке разобраны логические и арифметические операторы — булевы True и False, Логическое И (&&), Логическое ИЛИ (||), сложение, деление, умножение и остальная арифметика.

На чем стоит заострить внимание, так это на конкатенации — в синтаксисе PHP оператором «склеивания» строк является не плюс «+», а точка «.»:

$product_class = 'item' . ' item-hot';

Это что касается кода самого сценария, в вёрстке шаблона страницы, то есть в коде HTML, тоже можно использовать условия, но здесь синтаксис меняется.

Мы уже знаем про вставки в шаблон всяких переменных, например:

<section class="<?= $product_class ?>">

Но если нужно вставить именно условие, то на этом же примере, код будет следующий:

<section class="item
  <?php if ($is_new): ?> 
    item-new
  <?php endif; ?>">

То есть это такой монструозный элемент с контентом, который появляется (Или исполняется? Может что-то исполнятся в разметке?) при указанном условии в открывающем PHP-теге.

В условии здесь нет фигурных скобок {}, заместо них после условия стоит двоеточие и тег заканчивается. В закрывающем теге обязательно нужно указать конец условия endif;. При этом всём не потеряться с закрытием основного тега.

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

Нужно на страницу карточки товара вывести цену, учесть наличие скидки, посчитать стоимость со скидкой и вывести её рядом, и еще вывести две разных метки на страницу — новый товар и горячий товар.

Задачи с ценой достаточно очевидные. Метка новизны товара определяется просто булевым значением переменной $is_new = true. А вот «горячесть» товара определяется двумя условиями — либо он последний, либо скидка на него больше 1400 руб.

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

$price = 5000; // цена
$discount = 1500; // скидка
$is_new = true; // новый или нет
$is_last = false; // последний или нет
$product_class = 'item'; // шаблон класса секции

if ($discount > 0) { // если скидка вообще есть
  $price_with_discount = $price - $discount ; // цена со скидкой
}

if ($is_new) { // если новый
  $product_class = $product_class . ' item-new'; // добавляем класс
}

if ($discount > 1400 || $is_last) { // последний или большая скидка
  $product_class = $product_class . ' item-hot'; // тоже + (.) класс
};

В вёрстке тоже изменения:

<section class="<?= $product_class ?>"> <!-- подставляем класс -->
  ...
      <div class="price">
        <p class="price-old"><i>Цена </i><?= $price ?></p>
        <!-- условие появления эл-та скидочной цены -->
        <?php if ($discount > 0): ?>
          <p class="price-new">
          <i>Новая цена </i><?= $price_with_discount ?>
          </p>
        <?php endif; ?>
      </div>
  ...
</section>

Таким образом цена со скидкой появляется только если она есть, а метки работают через конкатенацию классов.

В конце части мы также меняем статичные значения в переменных на значения из базы данных, например, $price = get_product_price($id). База данных, кстати, в нашем случае это соседний .php файл с набором переменных и массивов. Кстати, о массивах.

Часть 2: Массивы и циклы в PHP

Массивы

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

Массивы записываются в переменные, а к конкретному элементу массива обращаются через индекс в квадратных скобках [], запись получается такая:

$colors = array('red', 'green', 'blue'); // запись для PHP ver. < 5.4
$colors = ['red', 'green', 'blue']; // сокращенная запись
<ul>
  <li><?= $colors[0] ?></li> <!-- выведет red -->
  <li><?= $colors[1] ?></li> <!-- выведет green -->
</ul>

Но на практике чаще пользуются не обычными массивами с числовыми индексами, а ассоциативными, в которых индекс можно назвать. Такой названный индекс называется ключ, ему «присваивается» значение с помощью команды => и массив в итоге выглядит так:

$item = [
  'color' => 'red', // => это = и > 
  'form' => 'circle'
];
<ul>
  <li><?= $item['color'] ?></li> <!-- выведет red -->
  <li><?= $item['form'] ?></li> <!-- выведет circle -->
</ul>

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

$colors[1] = 'pink' // заменить green на pink
$colors[3] = 'yellow' // добавить 4й элемент yellow

$item['form'] = 'square' // заменить circle на square
$item['size'] = 'big' // добавить новый элемент с индексом size => 'big'

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

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

$gadjets = [
  'phones' => [
    'apple' => 'iphone',
    'samsung' => 'galaxy'
  ]
  'tablets' => [
    'apple' => 'ipad',
    'microsoft' => 'surface'
  ]
]
<ul>
  <li><?= $gadjets['phones']['samsung'] ?></li> <!-- выведет galaxy -->
  <li><?= $gadjets['tablets']['apple'] ?></li> <!-- выведет ipad -->
</ul>

Да, с индексами и ключами массивов вложенность работает немного по другому, нежели, например, теги в HTML. Я сначала тоже вложил вида [1 [1.1]], но оказалось, нужно указывать как, своего рода, адрес.

Также в рамках задачи блока нам рассказали о команде count(массив), которая считает и выдает количество элементов массива. Выглядит оно, например, так:

$colors_num = count($colors); 
// выдаст 4 - red, заменённый pink, blue и добавленный black

$items_num = count($item);
// выдаст 3 - ключи color, form и добавленный size

$gadjets_num = count($gadfets);
// выдаст 2 - почему?

Об этом тоже не упомянуто в тренажёре, но последний count($gadjets) выдаёт 2, так как по умолчанию он не считает вложенность. То есть он считает именно кол-во элементов в указанном массиве, а их два — phones и tablets. Команде можно задать параметр COUNT_RECURSIVE, который будет считать все элементы рекурсивно, но это я, наверное, сильно забегаю вперёд.

Циклы

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

Первый на очереди цикл while(условие) {} — который выполняется «до тех пор пока» условие не станет ложным, то есть не вернёт false. Этот цикл очень универсальный, так как в условие можно много чего написать.

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

$features = ['Радует котов', 'Вызывает зависть', 'Прочно висит'];
// сам список преимуществ

$index = 0 // счетчик итераций для цикла

while ($index < count($features)) { // пока индекс < числа преимуществ
  print($features[$index]); // выводит текущее преимущество
  $index++; // увеличивает индекс на 1
}

Сразу скажу, про команду print() я уже сам подсмотрел, так как в тренажере для упрощения используется несуществующая команда keks_log(). Также в PHP работают такие же правила, т. н. «приращения» как и в JavaScript — и длинная запись i = i + 1, и сокращённые — i += 1 и i++.

Замечательно, теперь нам надо внедрить цикл в вёрстку. Список преимуществ это <ul>, а каждое преимущество, соответственно, <li>. Получается:

<ul>
  <?php $index = 0; ?> <!-- счётчик -->
  <?php while ($index < count($features)): ?> <!-- начало цикла -->
    <li><?= $features[$index] ?></li> <!-- выводим пункт -->
    <?php $index++ ?> <!-- увеличиваем счётчик -->
  <?php endwhile; ?> <!-- конец цикла -->
</ul>

Как видно синтаксис «внедрения» такой же как был в условиях и, забегая вперёд, он примерно такой же для большинства базовых функций. Таким образом, у нас выведутся все преимущества в виде пунктов списка <li> — то что нам и надо.

Все работает, но с циклом while есть нюансы. Главная его особенность в том, что в условии нужно обязательно указывать какое-то выражение, что не всегда удобно и может сильно увеличить объем кода. Отсюда, например, необходимость счётчика в нашей задаче.

Специально для работы с массивами существует цикл foreach(), который очень похож на цикл for() в JavaScript — он тоже перебирает все элементы указанного массива по очереди и выполняется столько раз, сколько элементов в массиве есть.

То есть в данном случае нам больше не нужен счётчик итераций и count () в коде, нужно только задать массив и переменную как итерацию, с которой мы будем работать внутри цикла. И на примере преимуществ товара, которые разбирали выше, запись цикла имеет вид:

$features = ['Радует котов', 'Вызывает зависть', 'Прочно висит'];

foreach ($features as $feauter) { // немного "обратная" запись от JS
  print($feature); // выводит текущее преимущество
  }

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

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

Чтобы не растягивать еще больше статью я не буду выписывать все массивы, лишь упомяну их как вводные данные сейчас:

$items = get_products(); // многомерный массив с ифно о товаре из БД
$filters = get_filters(); // массив фильтров тоже из БД
$type = $_GET['product_type']; // переменная параметра из URL

Как вывести просто все имеющиеся товары с помощью foreach() мы уже, хотя бы примерно, представляем. Давайте сразу и выведем, пока без фильтра:

<ul class="products-list">
  <?php foreach($items as $item): ?> <!-- начало цикла -->
  <li> <!-- в цикле создаем пункты списка для каждого item -->
    <a class="product-card" href="#"> 
      <h3><?= $item['title'] ?></h3> <!-- заголовок по ключу из БД -->
      <!-- аналогично ссылка на изображение и alt берутся из БД -->
      <img src="<?= $item['img_url'] ?>" 
           width="156" height="120" 
           alt="<?= $item['title'] ?>">
      <div class="product-options"> 
        <span class="price">
          <?= $item['price'] ?> <!-- цена из БД -->
        </span>
        <!-- опции цвета это стилизованный список -->
        <ul class="colors-list">
          <!-- цвета - это вложенный массив, для него вложенный цикл -->
          <?php foreach ($item['colors'] as $color): ?>
          <!-- создаем пункт списка, редактируем ему стиль -->
          <li class="color-<?= $color ?>"></li> <!-- цвет и стиль из БД -->
          <?php endforeach;?>
        </ul>
      </div>
    </a>
  </li>
  <?php endforeach; ?> <!-- конец цикла -->
</ul>

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

Но нам нужны фильтры! Фильтровать мы будем по параметру из массива. В нашем основном массиве из базы данных $items у каждого товара хранится еще один ключ 'type' который и хранит значение категории.

Что дальше? Дальше у нас есть двумерный массив $filters, в каждом элементе которого записаны имена фильтров с ключом 'name', а также записаны ключи 'url', которые хранят значение параметры запроса из адресной строки и совпадают по значению с категориями товаров.

Таким образом, мы можем сравнить полученный $type параметр URL с категорией товаров, а значит можем вывести только нужные используя условную конструкцию. Что ж, приступим.

Итак, нам нужно вывести список фильтров на страницу — в нашем случае, это стилизованные под кнопки пункты списка <ul>. С выводом пунктов списка мы уже мастера, с этим разберёмся легко.

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

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

<ul class="filters"> <!-- список фильтров -->
  <?php foreach($filters as $filter): ?> <!-- начало цикла -->
    
    <?php $filter_class = 'filter'; ?> <!-- базовый стиль пункта-кнопки -->
    <!-- сравниваем параметр URL с ключем 'url' -->
    <?php if($type === $filter['url']): { 
      $filter_class = $filter_class . ' filter-current';
    } ?> <!-- если да - добавляем класс "активной" кнопки -->
    <?php endif; ?>
  
  <li> <!-- создаем пункт-кнопку -->
    <!-- подставляем получившийся выше класс -->
    <a class="<?= $filter_class ?>" 
       href="catalog.php?product_type=<?= $filter['url'] ?>">
       <!-- ссылка меняет параметр в URL на ключ из БД -->
       <?= $filter['name'] ?> <!-- имя из массива -->
    </a>
  </li>
  <?php endforeach; ?> <!-- конец цикла -->
</ul>

Таким образом у нас есть кнопки с фильтрами, нажатие на которые меняет запрос в URL и добавляет класс нажатой кнопке, который в свою очередь закрашивает её. Меню фильтров готово!

Осталось как-то связать фильтры со списком товаров. Как у нас выводится этот список? В цикле создаются пункты, как обычно. Фильтр же должен выводить только определённые. Значит нужно что? Добавить условие в цикл!

<ul class="products-list">
  <?php foreach($items as $item): ?>
  <!-- сравниваем параметр URL с ключем 'type' ИЛИ выводим всё -->
  <?php if ($item['type'] === $type || $type === 'all'): ?>
  <li>
  ...
  <!-- дальше изменений нет, формируем список также -->
  ...
  </li>
  <?php endif; ?> <!-- все действия в цикле в условии -->
  <?php endforeach; ?>
</ul>

Готово! Кнопка «Все товары» у нас имеет ключ 'url' со значением 'all', поэтому при нажатии на нее нам нужно только сравнить параметр в URL через $type.

Часть 3: Массивы и функции в PHP

Пользовательские функции

Мы доползли до основного инструмента, наверное, любого языка программирования — до функций. Функция — это кусок кода, который можно вызвать и использовать повторно. Просто и очень удобно.

В PHP функция объявляется после команды function и вся структура функции выглядит подобным образом:

function func_name($arg_1, $arg_2, $arg_n) { // имя ф-ции и её аргументы
  $result = $arg_1 - $arg_2 * $arg_n  // тело ф-ции - какой-то код
  return $result; // результат выполнения ф=ции
}

Имя функции в данном случае func_name(), в скобках у нее аргументы функции — переменные или данные которые функция принимает, что бы уже внутри как-то с ними работать. Аргументов может быть сколь угодно или даже вообще не быть — всё зависит от задачи.

Внутри функции происходит какой-либо сценарий, который должен иметь результат. Именно этот результат, что бы это ни было — число, строка, булевы значения или массив — записывается после команды return. Таким образом функция сообщает «внешнему» коду, что у нее получилось и дальше в коде можно работать с её результатом.

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

Далее в коде функция вызывается просто по имени, как в песне:

func_name($arg_1, $arg_2, $arg_n)

В вёрстке тоже просто, можно вызвать по короткому тегу:

<?= func_name($arg_1, $arg_2, $arg_n) ?>

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

Кстати, упомянутые раньше в тренажёрах «подстановки» из базы данных вида get_product_что-то — это тоже функции, которые работают с одним большим массивом в соседнем файле и, в общем-то, разбирают его на кусочки. Магия перестаёт быть магией.

Это что касаемо так называемых пользовательских функций, то есть заданных самим разработчиком, но в PHP есть тонна встроенных функций. Парочку из них мы уже даже использовали — require() и count().

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

Нам нужно вывести блок с карточками трёх случайных промо-товаров. Промо считаются все новые товары is_new = true и пять самых дешёвых. Новые товары мы отфильтруем позже, а как нам найти самые дешёвые?

Сортировка массива

Для начала нам нужно отсортировать все товары по цене. Сортировкой мы еще не занимались, в этом нам поможет функция uasort(), которая принимает за аргумент массив и функцию (!) сортировки. Выглядит примерно так:

uasort($products, 'sort_by_price')

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

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

Итак, что происходит внутри колбэка sort_by_price():

function sort_by_price($product_1, $product_2) {
  // Если цена первого товара больше, результат - положительное число
  if ($product_1['price'] > $product_2['price']) {
    $result = 1;
  }

  // Если цена первого товара меньше, результат - отрицательное число
  if ($product_1['price'] < $product_2['price']) {
    $result = -1;
  }

  // Если цены равны, результат равен нулю
  if ($product_1['price'] === $product_2['price']) {
    $result = 0;
  }

  return $result;
}

Это один из вариантов колбэка для uasort(), я пока гуглил нашел еще варианты, но суть у них примерно одинаковая — сравнить значения и присвоить их условному «индексу» сортировки либо +1, либо -1, либо не менять.

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

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

Теперь нам нужны только первые пять элементов из массива, в этом нам поможет встроенная функция array_slice() которая копирует указанный участок массива в новый. Выглядит примерно так:

$cheap_products = array_slice($products, 0, 5, true);

Здесь в переменную $cheap_products с помощью нашей функции мы записали отрезок массива $products, указанного как первый аргумент.

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

Третий аргумент — это длинна отрезка, то есть сколько элементов включая указанный начальный нужно скопировать. Это не конец отрезка, как может сначала показаться. В нашем случае 5 — число нужных нам дешёвых товаров.

Последний аргумент — это булево значение — указывает нужно ли сохранять ключи исходного массива. Если стоит true — сохраняет ключи как были до копирования, если же false — сбрасывает их до вида [0, 1, 2, n]. В нашем случае критично сохранит ключи, так как это по сути идентификаторы товара.

Полдела сделано! Да, с отрезком попроще, чем с сортировкой.

Фильтрация массива

Теперь перейдем к новым товарам. Мы уже фильтровали товары в предыдущей части, но это было через циклы и по сути уже в вёрстке. Теперь нам надо отфильтровать массив заранее и в этот раз мы воспользуемся функцией.

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

Итак, нам нужно достать из массива товаров только новые, то бишь те, что имеют параметр $is_new = true. В этом нам поможет функция array_filter(), которая выглядит следующим образом:

$new_products = array_filter($product, 'filter_new');

Похожа на функцию uasort() и тоже принимает как аргументы массив с которым будет работать, в нашем случае это $product, и колбэк функцию в которой написаны параметры или алгоритм фильтрации.

В нашем случае это функция filter_new() которая только и делает, что проверять параметр $is_new и выглядит вот так:

function filter_new($product) {
  return $product['is_new']; // вернет true или false
}

Функция фильтрации array_filter() в свою очередь берет результаты колбэк функции и записывает в массив все элементы вернувшие true. Таким образом у нас получается массив только новых товаров.

Вот, по сути, и весь фильтр. Правда ведь, короче чем через циклы? Хорошо, у нас есть оба массива из условия задачи. Что делаем дальше? Нам надо их сложить в один массив и выводить случайные три товара из него.

Смешивание массивов

Сложение массивов происходит простым сложением переменных:

$promo_products = $cheap_products + $new_products;

Проще некуда, но есть нюансы — порядок элементов при сложении сохранится, если на пальцах:

['red', 'green'] + ['cyan', 'blue'] = ['red', 'green', 'cyan', 'blue']

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

Отлично, у нас есть объединённый массив $promo_products с новыми и дешёвыми товарами, но нам нужно вывести только три случайных. В этом нам поможет еще одна встроенная функция array_rand(), которая выглядит вот так:

 $random_ids = array_rand($promo_products, 3);

Эта функция выдает указанное количество случайных элементов, в нашем случае 3, из указанного массива, в нашем случае наш массив промо-товаров.

Но у этой функции тоже есть нюанс — она выдаст не до конца случайные числа. Она выбирает из массива случайные элементы и выдает их в том же порядке, что они были в массиве:

 $array = ['red', 'green', 'cyan', 'blue', 'yellow'];
 
 print(array_rand($array, 2)) // выдаст, например ['green', 'yellow']
 print(array_rand($array, 3)) // ['red', 'cyan', 'blue']
 
 // никогда не выдаст ['blue', 'green', 'red']

Это может подойди в определенных случаях, но нам нужны совсем случайные товары! Нужна какая-то надстройка которая будет дополнительно перемешивать элементы.

В этом нам поможет еще одна встроенная функция shuffle($random_ids), которая, собственно, просто перемешивает в случайном порядке элементы указанного массива. Перемешивать мы будем уже получившийся случайный массив $random_ids, так как функция перемешивания не сохраняет ключи, поэтому использовать её на исходном массиве не будем.

У нас наконец есть все кирпичики, давайте собирать! Сначала пропишем сценарий:

$products = get_products(); // получаем массив товаров

function sort_by_price($product_1, $product_2) { // колбэк сортировки
  if ($product_1['price'] > $product_2['price']) {
    $result = 1;  }
  if ($product_1['price'] < $product_2['price']) {
    $result = -1;  }
  if ($product_1['price'] === $product_2['price']) {
    $result = 0;  }
  return $result;
}

uasort($products, 'sort_by_price'); // сортируем по стоимости
$cheap_products = array_slice($products, 0, 5, true); // 5 самых дешёвых

function filter_new($product) { // колбэк фильтра "новизны"
  return $product['is_new']; }

$new_products = array_filter($products, 'filter_new'); // фильтруем новые

$promo_products = $cheap_products + $new_products; // дешёвые + новые
$random_ids = array_rand($promo_products, 3); // случайные 3 элемента
shuffle($random_ids); // перемешиваем их

Мы получили наш массив случайных дешёвых и новых товаров, теперь организуем на его основе выдачу в разметке, используем всё тот же foreach () для формирования карточек:

<ul class="products-list">
  <?php foreach($random_ids as $id): ?>
    <li>
      <a class="product-card" href="product.php?product_id=<?= $id ?>">
        <h3><?= get_product_title($id)?></h3>
        <div class="product-options">
          <span class="price"><?= get_product_price($id) ?></span>
        </div>
        <img src="<?= get_img_url($id) ?>"
             width="156" height="120"
             alt="<?= get_product_title($id) ?>">
      </a>
    </li>
  <?php endforeach; ?>
</ul>

Хотел сначала комментариев написать, но тут по сути копия структуры из циклов выше, только без лишних блоков фильтрации. Просто генерируем 3 карточки.

Часть 4: Числа и строки в PHP

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

Какой-то сквозной темы у части нет, поэтому пойдем по заданию — нужно сделать многостраничный каталог, а на каждой странице должно быть по шесть товаров. Такая структура выдачи называется пагинацией, кто работал с WordPress прекрасно с ней знакомы, к счастью или сожалению.

Массив товаров у нас всё так же хранится в $products, как сделать отрезок массива через array_slice() мы тоже знаем. Для этой функции нам еще нужны начало отрезка и количество элементов.

Элементов у нас 6, запишем их сразу в переменную $limit для удобства и если в дальнейшем захотим поменять число. А вот с началом отрезка не всё так просто — на каждой странице свое «начало» отрезка.

Что мы можем сделать? Можем обратиться к адресной строке браузера используя $_GET и забрать от туда параметр номера страницы. Честно говоря, мне пока не понятно как и где задаются сами эти параметры, но пока работаем на примере того что есть. В тренажёре адрес каталога выглядит вот так:

Зная номер страницы мы можем высчитать и задать переменную начала отрезка:

$page = $_GET['page']; // получаем номер страницы
$offset = ($page - 1) * $limit; // считаем начало отрезка

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

Мы отнимаем от номера страницы единицу $page - 1, так как первый товар в массиве под номером 0, а не 1. Затем умножаем на количество элементов на странице. Таким образом для первой страницы $offset = 0, для второй 6 и так далее — как нам и надо.

И вновь не всё так просто! Что если в адрес написать не число, а текст или вообще оставить параметр пустым? В этом случае заместо $page при вычислении PHP сам подставит 0, что нарушает нашу формулу. У нас получится отрицательное значение $offset, что значит нам начнут выдавать элементы с конца массива.

Нам нужно предусмотреть некорректные значения. Для начала нам в этом поможет функция intval(), которая извлекает из указанной в ней строки число. Правда она работает только если число идёт в начале строки. Если же найти число в строке ей не удаётся она возвращает 0.

Тернарный оператор

Что куда более интересно — это тернарный оператор! Работает он как упрощенная версия условной конструкции if () {} и применяется в случаях когда нужно выбрать между двумя значениями.

У оператора есть короткая запись:

$page = intval($_GET['page']) ?: 1;

В данном случае оператор ?: и работает он так — если значение до него истинно, то используется оно, иначе используется значение после оператора.

И у тернарного оператора есть его полная запись, которая позволяет явно задать условие:

условие ? значение1 : значение2;

Если условие истинно, то используется значение1, а если ложно — значение2. Мы чуть позже ей воспользуемся на примере.

Пока же в нашем случае в $page будет записываться число, если intval() так или иначе нашла его в строке из $_GET, или запишется 1 если функция ничего не нашла и вернула 0.

Отлично! Мы наконец можем сформировать наш массив для каждой страницы:

$limit = 6;
$page = intval($_GET['page']) ?: 1;
$offset = ($page - 1) * $limit;
$products_on_page = array_slice($products, $offset, $limit, true);

На этом этапе мы можем вывести нужные карточки товаров на определенных страницах, но нам нужна еще явная пагинация — кнопки страниц. Мы уже делали кнопки фильтра в части про Циклы, в этот раз мы тоже воспользуемся циклом.

Но не while () и не foreach (), хотя второй нам подойдет.

Цикл for ()

Старший брат всех циклов в PHP и, на самом деле, не только PHP. Как написано в спецификации это самый сложный цикл из имеющихся, корни которого идут из языка программирования «C». Работает он примерно также как там:

for ($i = 1; $i <= $pages; $i++) {
  print($i);
}

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

В цикле for() принято писать их в одну строку. Разберемся что где:

  1. Первым указывается код исполняемый до начала итерации. В нашем случае мы задали переменную $i играющую роль счетчика итераций.
  2. Далее идет условие выполнения итерации. Если условие вернет true — код в теле цикла { } исполняется, иначе false — цикл останавливается. В нашем случае счетчик остановится когда $i станет больше кол-ва страниц $pages.
  3. Последним указывается код исполняемы после каждой итерации. В нашем случае это приращение к счетчику $i++.

Такая конструкция «счётчик-условие-приращение» одна из самых, если не самая, популярная для цикла for().

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

<ol>
  <?php for ($i = 1; $i <= $pages; $i++ : ?> <!-- начало цикла -->
    <li> <!-- каждая кнопка пункт списка -->
      <a href="catalog.php?page=<?= $i ?>" 
         class="<?= $i === $page ? 'current' : '' ?>">
           <?= $i ?>
      </a>
    </li>
  <?php endfor; ?> <!-- конец цикла -->
</ol>

В комментариях плохо помещается, поясню так — каждая полученная в цикле кнопка страницы совпадает по номеру её итерации как с параметром страницы в адресе кнопки в href="", так и с контентом внутри ссылки. Поэтому мы подставили везде переменную счетчика <?= $i ?>.

В класс ссылки <a> мы подставили полный тернарный оператор, проверяющий совпадение счетчика с текущей странице в адресе. Если значения совпадают, то бишь true — кнопке подставляется класс current, который закрашивает кнопку. Иначе false — не добавляет ничего, в операторе это т. н. пустая строка ''.

С циклом разобрались теперь вернёмся немножечко назад. Сейчас наш цикл не будет работать, так как мы забыли объявить переменную $pages — общее количество страниц для всех товаров. Мы заранее не знаем сколько их, да и точное число нам не нужно.

Обратимся к нашему старому знакомому count()! С его помощью посчитаем количество товаров в основном массиве и найдем нужное количество страниц:

$products_quantity = count($products); // кол-во товаров
$pages = $products_quantity / $limit; // делим на кол-во на странице

Должно все заработать, но мы опять сталкиваемся с проблемой — у нас не кратное $limit количество товаров. Из-за этого $pages у нас становиться равно дробному числу, а в этом случае у нас не выполнится последняя итерация в цикле. Например, если товаров у нас 21, то $pages = 3.5, а цикл завершиться на третьей итерации:

// Третья итерация: $i = 3;
3 <= 3.5; // true

// Чертвёртая итерация: $i = 4;
4 <= 3.5; // false

В решении этой проблемы нам помогут встроенные функции округления чисел. Их в PHP три — ceil() округляет до целого вверх, floor() округляет до целого вниз и round() округляет до ближайшего целого в обе стороны.

Если кому-то как и мне стало интересно куда округлит round(3.5), то ответ — округлит до 4, так как 3.0 она округлит до 3 (несмотря на то, что округлять как бы нечего), поэтому формально 3.5 ближе к 4.

Мы воспользуемся ceil(), просто потому, что у нас в задаче всегда надо округлять вверх.

$pages_total = ceil($pages);

Мы близки к завершению, у нас есть все корректные элементы для сбора всего задания. У нас есть массив с товарами на страницу $products_on_page и теперь у нас есть точное число с запасом самих страниц $pages_total.

Соберем все в одном месте, вместе с формированием пагинации и карточек:

$products = get_products(); // товары
$limit = 6; // лимит на страницу
$page = intval($_GET['page']) ?: 1; // номер страницы или 1 по умолчанию
$offset = ($page - 1) * $limit; // постраничное смещение товаров
$products_on_page = array_slice($products, $offset, $limit, true); // >_<
$products_quantity = count($products);  // количество товаров
$pages = $products_quantity / $limit; // дробное кол-во страниц
$pages_total = ceil($pages); // целое кол-во страниц
<!-- блок пагинации - нумерованный список -->
<ol class="pagination">
  <!-- меняем $pages на $pages_total -->
  <?php for ($i = 1; $i <= $pages_total; $i = $i + 1): ?> 
  <li>
    <a href="catalog.php?page=<?= $i ?>" 
       class="<?= $i === $page ? 'current' : '' ?>">
         <?= $i ?>
    </a>
  </li>
  <?php endfor; ?>
</ol>

<!-- блок выдачи карточек товаров -->
<ul class="products-list">
  <!-- работаем с отрезком массива для страницы -->
  <?php foreach($products_on_page as $i => $item): ?>
  <li>
    <a class="product-card"  href="product.php?product_id=<?= $i ?>">
    ...
    <!-- наполнение карточки такое же, см. выше  -->
    ...
    </a>
  </li>
  <?php endforeach; ?>
</ul>

Сразу стоит заметить, что $i в обоих циклах это разные переменные, так как «существуют» только в рамках своего цикла. То есть $i в foreach() это не счетчик из for() и наоборот. Таким образом принято помечать итерации в циклах, поэтому у новичков типа меня могут возникать путаницы.

Также выделим другой синтаксис в foreach(), в тренажёре как-то очень мельком об этом сказано. Мы указали ключ текущего элемента итерации — это и есть переменная $i. Если постараться совсем на пальцах:

  • $i это ключ текущего элемента;
  • $item это значение текущего элемента, в нашем случае это массив.

И то и другое мы использовали в цикле формирования карточки товара. И вот что в итоге визуально у нас получается:

На этом основное задание части заканчивается, но последние несколько уроков посвящены еще одной достаточно интересной функции. К ней и перейдем.

Функция date ()

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

Я, наверное, не буду вываливать все символы и варианты, просто приведу несколько примеров:

$time_now = date('H:i:s');
  print($time_now); // Текущее время сервера Часов:Минут:Секунд - 13:30:12
  
$today = date('d.m.y');
  print($today); // номер-дня.номер-месяца.короткий-год - 20.04.22
  
$today_full = date('j F Y');
  print($today_full); // день.месяца.полный-год - 20 April 2022

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

$months = [1 => 'января',    2 => 'февраля', 
           3 => 'марта',     4 => 'апреля', 
           5 => 'мая',       6 => 'июня', 
           7 => 'июля',      8 => 'августа', 
           9 => 'сентября',  10 => 'октября', 
           11 => 'ноября',   12 => 'декабря'];

$day = date('j'); // день без лишнего 0 - не 02 Апреля, а 2 Апреля
$month = date('n');  // месяц без тоже лишнего 0 
$year = date('Y');  // полная форма года

$today = $day . $months[$month] . $year; 
  print($today); // Выдаст, например 20Апреля2022

Дату сплющит без пробелов, потому что мы их не задали. Справедливо! Можно подставить строки с пробелами и увеличить длину выражения. Но лучше использовать хитрость с кавычками!

В большинстве случаев в PHP разницы между одинарными '…' и двойными «…» кавычками нет, но на самом деле разница большая. Лучше всего показать на примере:

print('Сегодня $today'); // выдаст Сегодня $today
print("Сегодня $today"); // выдаст Сегодня 20Апреля2022

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

$today = "$day $months[$month] $year"; 
  print($today); // Выдаст корректно 20 Апреля 2022


Добрались-таки до конца! Статья заняла больше времени на фоне того же Интенсива, да и сама статья просто монструозная. Хотя я начинаю ловить какое-то мазохистское удовольствие от этого.

По поводу интенсива напишу сразу — он из себя представлял бесплатный доступ к определённым частям тренажеров. К тем, что мы уже прошли, ссылки как обычно ниже, и к трём частям которые на очереди — это «Погружение в HTML и CSS», несколько частей из «Продвинутый HTML и CSS» и весь блок «Построение сеток».

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

Кстати на этом блоке завершается уровень «Новичок/Любитель», мы завершаем свои «знакомства» и мы переходим к «Среднему» уровню.

На этом еще заканчивается вообще обучение по PHP в тренажёрах. Как минимум пока что больше тренажёров по PHP нет, поэтому мы с ним на время прощаемся. Дальше только HTML, CSS и JavaScript.

Тем кто дочитал или хотя бы до листал до конца большое спасибо за внимание!

Ссылки на остальные статьи по HTML Academy:

Знакомство с Веб-разработкой
Знакомство с HTML и CSS
Знакомство с JavaScript
Знакомство с PHP <- Вы здесь
Таблицы и подробно о формах
Наследование, каскады и селекторы
Блочная модель, поток и сетка на float
Гибкие флексбоксы display: flex
Удобные сетки на гридах display: grid
Пропуск блока «Погружение»
Позиционирование и двумерные трансформации
Теневое искусство и линейные градиенты
CSS-фильтры и Кекстаграм
Мастерские
Продвинутые Мастерские
...

Остальные статьи можно посмотреть у меня на главной странице блога.

Также мои соц. сетки, которые я продолжаю вести:

Мой Twitter
Мой Telegram
Мой Паблик ВК

Заходите куда удобно вам и подписывайтесь! Еще раз спасибо за внимание!