YaLinqo (LINQ to Objects для PHP) — версия 2.0

a88804a0d87bfaf21b6817ade48e772b.pngЧто-что? LINQ — это штука, которая позволяет писать запросы, чем-то похожие на SQL, прямо в коде. LINQ to Objects, собственно, позволяет писать запросы к объектам, массивам и всему тому, чем вы оперируете в коде.

Это ещё зачем?

Если у вас есть база, то у вас есть любимый ORM (или любимый голый SQL — кому как по вкусу). Но иногда объекты приходят из веб-сервисов, из файлов, да и вообще тьма тьмущая объектов может требовать нетривиальной обработки: преобразование, фильтрация, сортировка, группировка, агрегация… Применить бы привычный ORM или SQL —, но базы-то нет. Тут на помощь приходит LINQ to Objects, в данном случае YaLinqo.

Что умеет?

Самый полный порт .NET LINQ на PHP, со многими дополнительными методами. Всего реализовано более 70 методов. Ленивые вычисления, текст исключений и многое другое, как в оригинальном LINQ. Детальная документация PHPDoc к каждому методу. Текст статей адаптирован из MSDN. 100% покрытие юнит-тестами. Коллбэки можно задавать замыканиями, «указателями на функцию» в виде строк и массивов, строковыми «лямбдами» с поддержкой нескольких синтаксисов. Ключам уделяется столько же внимания, сколько значениям: преобразования можно применять и к тем, и к другим; большинство коллбэков принимает на вход и то, и другое; ключи по возможности не теряются при преобразованиях. Минимальное изобретение велосипедов: для итерации используются Iterator, IteratorAggregate и др. (и их можно использовать наравне с Enumerable); исключения по возможности используются родные похапэшные и т.п. Поддерживается Composer, есть пакет на Packagist. Никаких внешних зависимостей. Что случилось? Прошёл год, как вышел PHP 5.5 со всякими вкусностями типа генераторов и исправленных итераторов. Так как на моей совести самый полноценный порт LINQ на PHP, то я решил, что настало время его обновить и воспользоваться новыми фичами языка.

Что нового?

Скорость новая. Выкинуты тонны кода (порядка 800 строк под подсчётам Git): не стало костылей вроде Enumerator для генерации Iterator’ов; не стало бесполезных коллекций, по сути единственным преимуществом которых было хранение объектов в ключах; не стало call_user_func… И самое главное — не стало кучи маловменяемого кода для генерации итераторов, который невозможно понять. Остался foreach и yield. Всё это в сумме дало нехилый прирост в скорости.

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

return new Enumerable (function () use ($self, $inner, $outerKeySelector, $innerKeySelector, $resultSelectorValue, $resultSelectorKey) { /** @var $self Enumerable */ /** @var $inner Enumerable */ /** @var $arrIn array */ $itOut = $self→getIterator (); $itOut→rewind (); $lookup = $inner→toLookup ($innerKeySelector); $arrIn = null; $posIn = 0; $key = null;

return new Enumerator (function ($yield) use ($itOut, $lookup, &$arrIn, &$posIn, &$key, $outerKeySelector, $resultSelectorValue, $resultSelectorKey) { /** @var $itOut \Iterator */ /** @var $lookup \YaLinqo\collections\Lookup */ while ($arrIn === null || $posIn >= count ($arrIn)) { if ($arrIn!== null) $itOut→next (); if (!$itOut→valid ()) return false; $key = call_user_func ($outerKeySelector, $itOut→current (), $itOut→key ()); $arrIn = $lookup[$key]; $posIn = 0; } $args = array ($itOut→current (), $arrIn[$posIn], $key); $yield (call_user_func_array ($resultSelectorValue, $args), call_user_func_array ($resultSelectorKey, $args)); $posIn++; return true; }); }); на лаконичное: return new Enumerable (function () use ($inner, $outerKeySelector, $innerKeySelector, $resultSelectorValue, $resultSelectorKey) { $lookup = $inner→toLookup ($innerKeySelector); foreach ($this as $ok => $ov) { $key = $outerKeySelector ($ov, $ok); if (isset ($lookup[$key])) foreach ($lookup[$key] as $iv) yield $resultSelectorKey ($ov, $iv, $key) => $resultSelectorValue ($ov, $iv, $key); } }); Кроме этого, я наконец-то добавил человеческие теги с версиями в репозиторий и описал алиасы веток в composer.json, поэтому работа с Composer теперь должна вызывать меньше боли.Так что это такое, в конец-то концов?

Допустим, у вас есть массивы:

$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'), ); Допустим, вам нужно разместить продукты с ненулевым количеством в соответствующие отсортированные по имени категории, и внутри категорий отсортировать продукты сначала по убыванию количества, потом по имени. Сейчас вы начинаете строить в уме трижды вложенные циклы, вызовы функций для массивов, пытаетесь вспомнить, какой префикс у подходящей функции сортировки… Вместо всего этого можно написать: $result = 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) ==> [ «name» => $cat[«name»], «products» => $prods ]' ); Если бы создатели PHP не были упёртыми ослами и не отказались от пулл-реквеста с лямбдами, можно было бы написать даже так: $result = 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) ==> [ 'name' => $cat['name'], 'products' => $prods, ] ); Так или иначе, на выходе мы получим: 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) ) ) ) Для эстетов и оптимизаторов есть возможность использовать анонимные функции, а не «строковые лямбды». Вместо '$prod ==> $prod[«quantity»] > 0' можете писать function ($prod) { return $prod['quantity'] > 0; }. Для диких обфускаторов есть возможность использовать имена аргументов по умолчанию (v — значение, k — ключ и т.п.), то есть можно писать просто '$v[«quantity»] > 0' (для сложных вложеных запросов не рекомендуется).А где LINQ to Database?

Да, вообще-то в мире .NET запросы LINQ используются и в ORM (кто-то скажет, что это вообще основное назначение), но конкретно эта фича в библиотеке отсутствует, потому что любая попытка её реализовать выльется в тонну костылей, тормозов и прочих неприглядных вещей из-за отсутствия поддержки на уровне языка (разбора выражений, в частности). LINQ to Objects-то не обошёлся без костылей в виде «строковых лямбд», а тут полноценный транслятор из PHP в SQL с полным разбором и тоннами оптимизаций понадобится — закат солнца вручную.

Давай!

P.S. Старая версия поддерживает PHP 5.3. Функционально не уступает, но несколько тормознее (итераторы-с).P.P. S. Наконец-то появился вменяемый конкурент (Ginq). В нём тонны SPL, Symfony и прочей арихитектуры, ноль комментариев, много тормозов (оверхед от x2 до x50 относительно моей версии). Бенчмарки в процессе, напишу в следующий раз.

© Habrahabr.ru