PhpStorm не видит переменные переменных

PhpStorm не видит динамическое определение переменных. Вот код, в котором IDE видит ошибки там, где их нет:

foreach (array('foo', 'bar', 'baz') as $postfix) {
    $postfixedVariableName = sprintf('variable_%s', $postfix);
    $$postfixedVariableName = $postfix;
}
echo $variable_foo, PHP_EOL; // PhpStorm: Undefined variable 'variable_foo'
echo $variable_bar, PHP_EOL; // PhpStorm: Undefined variable 'variable_bar'
echo $variable_baz, PHP_EOL; // PhpStorm: Undefined variable 'variable_baz'

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

/** @var $variable_foo */
/** @var $variable_bar */
/** @var $variable_baz */
foreach (array('foo', 'bar', 'baz') as $postfix) {
    $postfixedVariableName = sprintf('variable_%s', $postfix);
    $$postfixedVariableName = $postfix;
}
// ...

Эволюция кода

Один из самых приятных в программировании (лично для меня) моментов — это когда тяжелый и запутанный код становится прозрачным. В одном из текущих проектов я отказался от паттерна Active Record в пользу паттерна Data Mapper.

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

public function actionIncludedUnits($id)
{
    $unitsTable        = \app\models\Unit::tableName();
    $unitsBundlesTable = \app\models\UnitsBundle::tableName();
    $unitsGroupsTable  = \app\models\unitsGroups\UnitsGroup::tableName();
    $tangiblesTable    = \app\models\tangibles\Tangible::tableName();

    $query = "SELECT u.id unitId,
                     ug.id unitsGroupId,
                     ug.division_slug slug,
                     ug.group_num l, 
                     u.group_num r, 
                     t.name tangibleName
                FROM {$unitsBundlesTable} ub
           LEFT JOIN {$unitsTable} u ON u.bundle_of_units_id = ub.id
           LEFT JOIN {$unitsGroupsTable} ug ON u.units_group_id = ug.id
           LEFT JOIN {$tangiblesTable} t ON u.tangible_id = t.id
               WHERE ub.complex_unit_id = :id";

    $raw = Yii::$app->db->createCommand($query)
        ->bindValue(':id', $id)
        ->queryAll();

    $res = [];

    foreach ($raw as $item) {
        $res[$item['unitId']] = [
            'unitsGroupNumber' => sprintf('%02d/%02d', $item['l'], $item['r']),
            'tangibleName' => $item['tangibleName'],
            'unitUrl' => \yii\helpers\Url::to([
                'units/view', 
                'slug' => $item['slug'], 
                'group' => $item['unitsGroupId'], 
                'id' => $item['unitId']
            ]),
        ];
    }

    return (\yii\helpers\Json::encode($res));
}

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

public function actionIncludedUnits($id)
{
    $units = (new mappers\UnitMapper)->search([
        'Unit' => [
            'complexUnitId' => $id,
        ],
    ]);

    $res = [];

    foreach ($units->getModels() as $unit)
        $res[$unit->id] = [
            'unitsGroupNumber' => $unit->spoNumberOutput(),
            'tangibleName'     => $unit->tangible->name,
            'categoryName'     => $unit->tangible->category->name,
            'unitUrl'          => \yii\helpers\Url::to([
                'units/view', 
                'slug'  => $unit->unitsGroup->divisionSlug, 
                'group' => $unit->unitsGroup->id, 
                'id'    => $unit->id,
            ]),
        ];

    return \yii\helpers\Json::encode($res);
}

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

PHP: добавить элемент в середину массива

Функция array_splice() удаляет length элементов, расположенных на расстоянии offset из массива input, и заменяет их элементами массива replacement, если он передан в качестве параметра. Эту функцию можно использовать для вставки, замены и удаления элементов массива.

Если передать третьим параметром ноль, то никакие элементы массива не будут удалены.

$input = ["red", "green", "blue", "yellow"];
array_splice($input, 3, 0, "white");
// => ["red", "green", "blue", "white", "yellow"];
array_splice($input, 3, 0, ["purple", "black"]);
// => ["red", "green", "blue", "purple", 
//     "black", "white", "yellow"]

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

Зоны ответственности в программном коде

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

Пример. В базе данных хранится длительность события. Длительность указана как целое число (int) и указывается в минутах. Информацию о событии мы готовим при помощи дата маппера и велик соблазн сразу же привести число в понятный человеку вид (135 → 2 ч. 15 мин.).

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

Итак, в коде возвращаемого маппером объекта будет как свойство $duration, так и метод, возвращающий длительность в понятном человеке виде:


class Event extends DomainObject
{
    $duration = null;

    public function durationOutput()
    {
        if ($this->duration < 60) 
            return "{$this->duration} мин.";

        $hours = floor($this->duration/60);
        $min   = $this->duration % 60;

        if ($min) 
            return "{$hours} ч. {$min} мин.";

        return "{$hours} ч.";
    }
}

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

Нумерация внутри групп при выборке в MySQL

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

Решение:

SET @curRow := 1, @marker := '';
SELECT *,
    CASE t.field
      WHEN @marker
      THEN @curRow := @curRow + 1
      ELSE @curRow := 1 AND @marker := t.field
    END AS range
FROM sometable t
ORDER BY t.field;

Идея в использовании переменных пользователя. Мы определяем номер внутри группы (@curRow) и некоторый маркер (@marker). В самом запросе сравниваем значение поля, определяющего группу (идентификатор класса, описи, корзины), с текущим значением переменной. Если они равны, то это значит, что группа продолжается и номер @curRow становится больше на единицу, в противном случае номер сбрасывается на значение по умолчанию, а маркер @marker становится равным значению поля, определяющего группу. При этом в поле range попадает искомый номер в последовательности внутри группы.

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

Изменить шаблон (лейаут) для части сайта в Yii2

В Yii2 все страницы отрисовываются внутри базового шаблона (лейаута) — app\views\layouts\main.php. Однако, можно оторвать как контроллер, так и метод от шаблона.

Чтобы все методы контроллера использовали отдельный шаблон, необходимо явно задать в контроллере атрибут $layout:

class SomeController extends Controller
{
    public $layout = 'another-layout';
    /* остальной код контроллера */
}

Если вместо имени шаблона передать false, то шаблон не будет применён ни к одному действию контроллера. Значение по умолчанию — null — используется для наследования шаблона модуля (документация).

Если нужно отвязать от базового шаблона отдельное действие контроллера, то меняем

$this->render(['view' compact('foo', 'bar')])

на

$this->renderPartial(['view' compact('foo', 'bar')])

renderPartial() отрисует переданные данные, не добавляя их в шаблон проекта.

Создание массивов для форм в Yii2

При генерации форм в Yii2 для выпадающих списков и списков чекбоксов нужны массивы данных в виде пар [ключ => значение]. Подготовить такие массивы можно либо исключительно силами ORM:

use app\models\Brand;

$brands = Brand::find()
    ->select('brand')
    ->orderBy('brand')
    ->indexBy('id')
    ->column();

либо при помощи класса-помощника ArrayHelper:

use app\models\Brand;
use yii\helpers\ArrayHelper;

$brands = ArrayHelper::map(Brand::findAll(), 'id', 'brand');

Добавляем в ActiveForm выпадающий список:

$form->field($model, 'id_brand')
    ->label('Бренд')
    ->dropdownList($brands);

Список чекбоксов:

Html::activeCheckboxList($model, 'id_brand', $brands);

Поиск по композитным ключам в MySQL

Предикат IN в MySQL позволяет проверять не только вхождение значения одного поля в список, но и вхождения значений нескольких полей одновременно:

SELECT * 
  FROM foo
 WHERE (bar, baz) IN ((1, 2), (3, 4), (5, 6));

Это может быть особенно полезно, когда первичный ключ — композитный.