Статьи

Использование RulerZ Rule Engine для улучшения построения списка воспроизведения

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

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

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

Образ героя

Вы можете найти пример кода на https://github.com/assertchris-tutorials/rulerz .

Проблема

Я слушаю музыку все время. Я предпочитаю iTunes, а не другие медиаплееры, по многим причинам. Но одна из причин, которая выделяется, заключается в том, что я могу использовать iTunes, чтобы создавать для меня большие и сложные списки воспроизведения.

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

Умный плейлист

Но как это сделать? Как это превратить мои простые правила в фильтр для треков? Когда я говорю это такими вещами, как; «Дайте мне все из The Glitch Mob, выпущенной до 2014 года, где количество игр меньше 20», — оно понимает, что я имею в виду.

Теперь мы можем создавать эти умные плейлисты с множеством условий. Если ты хоть как-то похож на меня, ты просто съежился от этой мысли.

Введите RulerZ

RulerZ — это механизм правил. Это реализация шаблона спецификации . Вы знаете, где еще вы видели образец спецификации? В слоях абстракции базы данных, таких как Eloquent и Doctrine!

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

$list
    ->whereArtist("The Glitch Mob")
    ->whereYearLessThan(2015)
    ->wherePlayCountLessThan(20)
    ->all();

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

Мы бы не хотели делать эти фильтры условными с помощью PHP. Шаблон спецификации (и, соответственно, SQL) отлично подходит для применения этой логической логики.

Давайте посмотрим, как использовать RulerZ:

 use RulerZ\Compiler;
use RulerZ\Parser;
use RulerZ\RulerZ;

$compiler = new Compiler\EvalCompiler(
    $parser = new Parser\HoaParser()
);

$rulerz = new RulerZ(
    $compiler, [
        $visitor = new Compiler\Target\ArrayVisitor(),
    ]
);

$tracks = [
    [
        "title"  => "Animus Vox",
        "artist" => "The Glitch Mob",
        "plays"  => 36,
        "year"   => 2010
    ],
    [
        "title"  => "Bad Wings",
        "artist" => "The Glitch Mob",
        "plays"  => 12,
        "year"   => 2010
    ],
    [
        "title"  => "We Swarm",
        "artist" => "The Glitch Mob",
        "plays"  => 28,
        "year"   => 2010
    ]
    // ...
];

$filtered = $rulerz->filter(
    $tracks,
    "artist = :artist and year < :year and plays < :plays",
    [
        "artist" => "The Glitch Mob",
        "year"   => 2015,
        "plays"  => 20
    ]
);

В этом примере у нас есть список треков. Это может быть то, что мы экспортируем из iTunes …

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

Как SQL, но в PHP, против записей, хранящихся в памяти. Это просто и элегантно!

Создание умных плейлистов

Давайте использовать эти знания для использования! Начнем с извлечения библиотеки iTunes:

Откройте iTunes, нажмите «Файл» → «Библиотека» → «Экспортировать библиотеку…»

Экспорт библиотеки из iTunes

Сохраните файл XML как library.xml

Сохранение библиотеки в XML

В зависимости от размера вашей библиотеки этот файл может быть большим. Мой файл library.xml

Этот XML-файл может быть сложным для работы. Это в нечетном формате ключ / значение. Итак, мы собираемся преобразовать его в файл JSON, содержащий только данные трека:

 $document = new DomDocument();
$document->loadHTMLFile("library.xml");

$tracks = $document
    ->getElementsByTagName("dict")[0] // root node
    ->getElementsByTagName("dict")[0] // track container
    ->getElementsByTagName("dict");   // track nodes

$clean = [];

foreach ($tracks as $track) {
    $key = null;
    $all = [];

    foreach ($track->childNodes as $node) {
        if ($node->tagName == "key") {
            $key = str_replace(" ", "", $node->nodeValue);
        } else {
            $all[$key] = $node->nodeValue;
            $key = null;
        }
    }

    $clean[] = $all;
}

file_put_contents(
    "tracks.json", json_encode($clean)
);

Мы создаем объект DomDocument Этот файл имеет три уровня: корневой узел dictdictdict

Для каждого узла дорожки мы проходим каждый дочерний узел. Половина из них — key Поэтому мы храним каждый ключ до тех пор, пока не получим значение, соответствующее ему. Это немного взломать, но это делает работу. Нам нужно только запустить это один раз, чтобы получить хороший список треков, и RulerZ будет использовать его после этого!

Если вы хотите отладить этот код, я предлагаю вместо этого экспортировать списки воспроизведения (в виде файлов XML). Таким образом, вы можете иметь гораздо меньший файл library.xml Вы не хотите повторять это извлечение много раз, в большом списке. Доверьтесь мне…

Затем нам нужно создать форму для фильтров:

 $filterCount = 0;
$filtered = [];

function option($value, $label, $selected = null) {
    $parameters = "value={$value}";

    if ($value == $selected) {
        $parameters .= " selected='selected'";
    }

    return "<option {$parameters}>{$label}</option>";
}

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

Затем мы определяем функцию для рендеринга опционных элементов. Это сокращает работу, которую мы должны сделать позже. Ура! Далее идет разметка:

 <form method="post">
    <div>
        <select name="field[<?= $filterCount ?>]">
            <?= option("Name", "Name") ?>
            <?= option("Artist", "Artist") ?>
            <?= option("Album", "Album") ?>
            <?= option("Year", "Year") ?>
        </select>
        <select name="operator[<?= $filterCount ?>]">
            <?= option("contains", "contains") ?>
            <?= option("begins", "begins with") ?>
            <?= option("ends", "ends with") ?>
            <?= option("is", "is") ?>
            <?= option("not", "is not") ?>
            <?= option("gt", "greater than") ?>
            <?= option("lt", "less than") ?>
        </select>
        <input type="text" name="query[<?= $filterCount ?>]" />
    </div>
    <input type="submit" value="filter" />
</form>
<?php foreach ($filtered as $track): ?>
    <div>
        <?= $track["Artist"] ?>,
        <?= $track["Album"] ?>,
        <?= $track["Name"] ?>
    </div>
<?php endforeach; ?>

Здесь мы создали разметку для добавления одного фильтра. Поля имеют имена field[0]operator[0]query[0]

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

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

 {
    "Track ID": "238",
    "Name": "Broken Bones (Bonus Track)",
    "Artist": "CHVRCHES",
    "Album Artist": "CHVRCHES",
    "Composer": "CHVRCHES",
    "Album": "The Bones of What You Believe (Special Edition)",
    "Genre": "Alternative",
    "Kind": "Purchased AAC audio file",
    "Size": "7872373",
    "Total Time": "224721",
    "Disc Number": "1",
    "Disc Count": "1",
    "Track Number": "14",
    "Track Count": "16",
    "Year": "2013",
    "Date Modified": "2014-05-21T09:45:09Z",
    "Date Added": "2013-11-24T22:18:35Z",
    "Bit Rate": "256",
    "Sample Rate": "44100",
    "Play Count": "133",
    "Play Date": "3513745347",
    "Play Date UTC": "2015-05-05T20:22:27Z",
    "Skip Count": "1",
    "Skip Date": "2014-01-30T21:44:20Z",
    "Release Date": "2013-09-24T07:00:00Z",
    "Normalization": "1979",
    "Artwork Count": "1",
    "Sort Album": "Bones of What You Believe (Special Edition)",
    "Persistent ID": "B05B025A46F6F2BB",
    "Track Type": "File",
    "Purchased": "",
    "Location": "file://.../track.m4a",
    "File Folder Count": "5",
    "Library Folder Count": "1"
}

Помимо текстовых фильтров, которые мы уже добавили; мы можем добавить наши собственные пользовательские функции:

 $visitor->setOperator("my_is", function($field, $value) {
    return $field == $value;
});

$visitor->setOperator("my_not", function($field, $value) {
    return $field != $value;
});

$visitor->setOperator("my_contains", function($field, $value) {
    return stristr($field, $value);
});

$visitor->setOperator("my_begins", function($field, $value) {
    return preg_match("/^{$value}.*/i", $field) == 1;
});

$visitor->setOperator("my_ends", function($field, $value) {
    return preg_match("/.*{$value}$/i", $field) == 1;
});

$visitor->setOperator("my_gt", function($field, $value) {
    return $field > $value;
});

$visitor->setOperator("custom_lt", function($field, $value) {
    return $field < $value;
});

Мы можем использовать их в других текстовых запросах, таких как: my_contains(Artist, 'Glitch') Фактически, мы можем начать сшивать фильтры форм, используя эти:

 if (isset($_POST["field"])) {
    $fields = $_POST["field"];
    $operators = $_POST["operator"];
    $values = $_POST["query"];

    $query = "";

    foreach ($fields as $i => $field) {
        $operator = $operators[$i];
        $value = $values[$i];

        if (trim($field) && trim($operator) && trim($value)) {
            if ($query) {
                $query .= " and ";
            }

            $query .= "my_{$operator}({$field}, '{$value}')";
        }
    }

    $filterCount = count($fields);
}

Этот код проверяет, есть ли опубликованные фильтры. Для каждого размещенного фильтра мы получаем operatorquery Если это не пустые значения (это то, что мы используем для проверки trim

Мы также настраиваем $filterCount Наконец, нам нужно отфильтровать экспортированный список треков:

 $tracks = json_decode(
    file_get_contents("tracks.json"), true
);

$filtered = $rulerz->filter($tracks, $query);

Это берет экспорт iTunes, который мы сделали ранее, и фильтрует его согласно динамическому запросу, который мы только что сделали.

Отфильтрованные треки

Отображение опубликованных фильтров

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

 <form method="post">
<?php if ($fields): ?>
<?php for ($i = 0; $i < $filterCount; $i++): ?>
    <div>
        <select name="field[<?= $i ?>]">
            <?= option("Name", "Name", $fields[$i]) ?>
            <?= option("Artist", "Artist", $fields[$i]) ?>
            <?= option("Album", "Album", $fields[$i]) ?>
            <?= option("Year", "Year", $fields[$i]) ?>
        </select>
        <select name="operator[<?= $i ?>]">
            <?= option("contains", "contains", $operators[$i]) ?>
            <?= option("begins", "begins with", $operators[$i]) ?>
            <?= option("ends", "ends with", $operators[$i]) ?>
            <?= option("is", "is", $operators[$i]) ?>
            <?= option("not", "is not", $operators[$i]) ?>
            <?= option("gt", "greater than", $operators[$i]) ?>
            <?= option("lt", "less than", $operators[$i]) ?>
        </select>
        <input
            type="text"
            name="query[<?= $i ?>]"
            value="<?= $values[$i] ?>" />
    </div>
<?php endfor; ?>
<?php endif; ?>
    <div>
        <select name="field[<?= $filterCount ?>]">
            <?= option("Name", "Name") ?>
            <?= option("Artist", "Artist") ?>
            <?= option("Album", "Album") ?>
            <?= option("Year", "Year") ?>
        </select>
        <select name="operator[<?= $filterCount ?>]">
            <?= option("contains", "contains") ?>
            <?= option("begins", "begins with") ?>
            <?= option("ends", "ends with") ?>
            <?= option("is", "is") ?>
            <?= option("not", "is not") ?>
            <?= option("gt", "greater than") ?>
            <?= option("lt", "less than") ?>
        </select>
        <input type="text" name="query[<?= $filterCount ?>]" />
    </div>
    <input type="submit" value="filter" />
</form>

Это очень похоже на предыдущую форму, которую мы имели. Теперь мы основываем выбор параметров на опубликованных значениях.

Мы не удаляем пустые фильтры. Учтите, что упражнение оставлено читателю!

Вывод

Это был интересный проект для меня. Я не часто думаю о том, как что-то реализовано, через мой собственный код. RulerZ предоставил мне инструменты, необходимые для этого!

Можете ли вы вспомнить другие интересные способы использования механизма правил? Дай мне знать в комментариях!