- PVSM.RU - https://www.pvsm.ru -
Как можно было заметить из моей предыдущей статьи со сравнением библиотек LINQ для PHP [1], библиотек много, а качества мало: ленивые вычисления не реализованы ни в одной библиотеке, тесты есть в половине случаев, типы коллбэков ограничены, а иногда и вовсе незнамо что выдаётся за LINQ. Поэтому я написал свою библиотеку. Встречайте:
Возможности:
Пример кода:
// Отфильтровать продукты с ненулевым количеством, поместить в соответствующие категории,
// отсортированные по имени. Продукты отсортировать сначала по убыванию количества, потом по имени.
from($categories)
->orderBy('$v["name"]')
->groupJoin(
from($products)
->where('$v["quantity"] > 0')
->orderByDescending('$v["quantity"]')
->thenBy('$v["name"]'),
'$v["id"]', '$v["catId"]', 'array("name" => $v["name"], "products" => $e)'
);
Теперь рассмотрим приведённый выше пример детальнее. На самом деле существует несколько вариантов записи этого запроса: с помощью замыканий и с помощью строковых лямбд. У лямбд тоже два синтаксиса: можно использовать имена переменных по умолчанию (v и k для значения и ключа, соответственно), можно задать осмысленные.
Исходные данные (либо из базы, либо из какого-нибудь сервиса JSON пришёл, либо «железные» константы, либо ещё какой источник):
$products = array(
array('name' => 'Keyboard', 'catId' => 'hw', 'quantity' => 10, 'id' => 1),
array('name' => 'Mouse', 'catId' => 'hw', 'quantity' => 20, 'id' => 2),
array('name' => 'Monitor', 'catId' => 'hw', 'quantity' => 0, 'id' => 3),
array('name' => 'Joystick', 'catId' => 'hw', 'quantity' => 15, 'id' => 4),
array('name' => 'CPU', 'catId' => 'hw', 'quantity' => 15, 'id' => 5),
array('name' => 'Motherboard', 'catId' => 'hw', 'quantity' => 11, 'id' => 6),
array('name' => 'Windows', 'catId' => 'os', 'quantity' => 666, 'id' => 7),
array('name' => 'Linux', 'catId' => 'os', 'quantity' => 666, 'id' => 8),
array('name' => 'Mac', 'catId' => 'os', 'quantity' => 666, 'id' => 9),
);
$categories = array(
array('name' => 'Hardware', 'id' => 'hw'),
array('name' => 'Operating systems', 'id' => 'os'),
);
Собственно, задача: отфильтровать продукты с ненулевым количеством, поместить в соответствующие категории. Продукты отсортировать сначала по убыванию количества, потом по имени. Категории отсортировать по имени. Должно получиться следущее (для краткости переформатировал):
Array (
[hw] => Array (
[name] => Hardware
[products] => Array (
[0] => Array ( [name] => Mouse [catId] => hw [quantity] => 20 [id] => 2 )
[1] => Array ( [name] => CPU [catId] => hw [quantity] => 15 [id] => 5 )
[2] => Array ( [name] => Joystick [catId] => hw [quantity] => 15 [id] => 4 )
[3] => Array ( [name] => Motherboard [catId] => hw [quantity] => 11 [id] => 6 )
[4] => Array ( [name] => Keyboard [catId] => hw [quantity] => 10 [id] => 1 )
)
)
[os] => Array (
[name] => Operating systems
[products] => Array (
[0] => Array ( [name] => Linux [catId] => os [quantity] => 666 [id] => 8 )
[1] => Array ( [name] => Mac [catId] => os [quantity] => 666 [id] => 9 )
[2] => Array ( [name] => Windows [catId] => os [quantity] => 666 [id] => 7 )
)
)
)
Ниже приведён пример с использованием замыканий из PHP 5.3. Самая длинная запись, однако наилучшая поддержка в разнообразных IDE.
from($categories)
->orderBy(function ($cat) { return $cat['name']; })
->groupJoin(
from($products)
->where(function ($prod) { return $prod["quantity"] > 0; })
->orderByDescending(function ($prod) { return $prod["quantity"]; })
->thenBy(function ($prod) { return $prod["name"]; }),
function ($cat) { return $cat["id"]; },
function ($prod) { return $prod["catId"]; },
function ($cat, $prods) { return array("name" => $cat["name"], "products" => $prods); }
);
Запись с помощью строковых лямбд. Слева от оператора "==>" имена аргументов, справа — возвращаемое значение.
from($categories)
->orderBy('$cat ==> $cat["name"]')
->groupJoin(
from($products)
->where('$prod ==> $prod["quantity"] > 0')
->orderByDescending('$prod ==> $prod["quantity"]')
->thenBy('$prod ==> $prod["name"]'),
'$cat ==> $cat["id"]',
'$prod ==> $prod["catId"]',
'($cat, $prods) ==> array("name" => $cat["name"], "products" => $prods)'
);
И наконец самая краткая запись. Если нет оператора "==>", то используются имена переменных по умолчанию: v для значения, k для ключа, a и b для сравниваемых значений и т.п.
from($categories)
->orderBy('$v["name"]')
->groupJoin(
from($products)
->where('$v["quantity"] > 0')
->orderByDescending('$v["quantity"]')
->thenBy('$v["name"]'),
'$v["id"]', '$v["catId"]', 'array("name" => $v["name"], "products" => $e)'
);
Просто так один-в-один оригинальный LINQ не скопируешь: разные языки, разные возможности, разные особенности. Поэтому часто приходилось делать выбор. Насколько хороший или плохой — судить вам. Обсуждение приветствуется.
Начнём с самого сомнительного: ключи объявляются важной частью данных. Причина: они явно присутствуют в родных похапэшных итераторах, они важны в массивах, они важны при преобразовании в JSON. В общем и целом, в PHP повсеместно используются ключи, поэтому пренебрегать ими, как в некоторых других библиотеках, мне не хотелось бы.
Однако в оригинальном LINQ никаких ключей у последовательностей нет, поэтому приходится увеличивать количество аргументов как у коллбэков (теперь они все могут работать с ключом, если это возможно), так и у самих методов LINQ: resultSelector превращается в resultSelectorValue + resultSelectorKey. Однако в большинстве случаев разработчику об этом думать не нужно: коллбэки можно передавать с меньшим количеством аргументов, а у всех методов LINQ для аргументов типа resultSelectorKey заданы значения по умолчанию.
Другая проблема с ключами вытекает из необходимости их везде сохранять. Это значит, что по умолчанию при сортировке у элементов останутся прежние ключи. PHP обычно перечисляет массивы в порядке добавления элементов, поэтому преобразование в массив проблемой быть не должно, но мало ли.
Если вам информация о ключах не нужна, то есть два простых способа от них избавиться:
На втором месте по сомнительности решение о порядке аргументов в функциях и коллбэках. Они всегда идут в порядке от (теоретически) наиболее используемого к наименее используемому. В коллбэках поэтому на первом месте обычно идёт значение, а потом ключ, потому что значение важно почти всегда, а ключ — нет. Однако порядок аргументов, возможно, теперь сложнее запомнить. Например, в select сначала идёт выборка значения, а в toDictionary — выборка ключа.
Впрочем, нам, похапэшникам, к такому безобразию не привыкать — весь язык испещрён совершенно случайным порядком аргументов (тех же needle и haystack).
Неочевидное решение для тех, кто пользовался оригинальным LINQ: методы типа indexOf, elementAt работают с ключами, а не числовым положением элемента в перечилении. Если вам нужно положение, то предварительно вызовите toValues — ключи станут последовательными: 0, 1, 2, 3 и т.д. Также для методов типа select нет перегрузок с коллбэками, принимающими положение элемента. Аналогично, используйте toValues.
В библиотеке linq.js, которой я вдохновлялся при написании, у всех коллбэков аргументы называются $, $$, $$$, $$$$. В PHP такого не бывает. Можно сделать преобразование строк, но хотелось бы оставить код валидным, пусть даже он внутри строчки. Называть аргументы бессодержательно $a, $b, $c тоже не хочется. Поэтому принято решение использовать имена, соответствующие содержимому:
Недостаток: имена нужно знать. Впрочем, при детальной документации это не должно быть проблемой.
Класса List нет, метод toList возвращает то же самое, что toArray, только с последовательными ключами (0, 1, 2 и т.д.)
Класс Dictionary есть. Изначально задумывался исключительно как база для Lookup, но сейчас стал отдельной полноценной коллекцией. Отличие от обычных массивов — ключами могут быть объекты (возможно в оригинальном LINQ). Но фактически в самом LINQ объекты-ключи поддерживаются далеко не везде, потому что PHP не позволяет использовать объекты-ключи в foreach. Можно все циклы переписать, но насколько игра стоит свеч — вопрос.
Класс Lookup есть. По ключу возвращает список значений (или пустой массив, если ключа нет).
Обе коллекции поддерживают метод toArray, который возвращает внутренний массив.
Ко всем методам скопирована справка из MSDN, затем адаптирована под реалии порта. Где-то описания спёрты из других проектов. Где-то — написаны самостоятельно. Если найдёте ошибки — сообщайте.
В целом, справка получилась весьма солидная. У некоторых методов нехилые такие статьи.
Некоторые слова в PHP нагло захапаны самим языком, причём во всех регистрах. Даже empty нельзя использовать как имя метода. Поэтому, где есть конфликты, методы переименованы (в списке методов в начале статьи оригинальные имена методов даны в скобках). В частности, run/forEach стали call/each.
В PHP нет встроенных исключений, которые есть в .NET. Однако я постарался избежать создания ненужных классов. Так, вместо InvalidOperationException используется UnexpectedValueException. В конце концов, недопустимой операция становится при неожиданных значениях.
Покрытие юнит-тестами — практически 100%.
Лицензия — упрощённая BSD (двухпунктовая).
Требования — PHP 5.3.
Использование:
require_once __DIR__ . '/lib/Linq.php'; // заменить на свой путь
use YaLinqoEnumerable; // по вкусу укоротить имя
use YaLinqoEnumerable as E; // или так
// Можно вызывать или глобальную функцию from, или статический метод в Enumerable — разницы нет
Enumerable::from(array(1, 2, 3));
from(array(1, 2, 3));
Для наглядности добавил в таблицу ещё и библиотеки для JavaScript. Их сравнение будет в отдельной статье.
Легенда как в Википедии, но с дополнительным значением:
Прошу прощения за английский в таблице. По-русски слишком длинно получалось.
Работал на халяву, денег никто не даст. Если чувствуете приступ щедрости, можете просто проголосовать за эти фичи в PHP и PHPStorm. Авось заметят и использовать библиотеку станет приятнее.
Скачать Yet Another LINQ to Objects for PHP с GitHub [8]
P.S. Подскажите, пожалуйста, куда можно запостить аналогичную статью на английском.
Автор: Athari
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/11456
Ссылки в тексте:
[1] со сравнением библиотек LINQ для PHP: http://habrahabr.ru/post/147612/
[2] Image: http://habrastorage.org/storage2/b3c/95c/3f4/b3c95c3f427b11617bfe086504bc893b.png
[3] 45684: https://bugs.php.net/bug.php?id=45684
[4] WI-3477: http://youtrack.jetbrains.com/issue/WI-3477
[5] WI-2377: http://youtrack.jetbrains.com/issue/WI-2377
[6] WI-11110: http://youtrack.jetbrains.com/issue/WI-11110
[7] WI-8270: http://youtrack.jetbrains.com/issue/WI-8270
[8] Скачать Yet Another LINQ to Objects for PHP с GitHub: https://github.com/Athari/YaLinqo
Нажмите здесь для печати.