Я знаю, как связать методы класса (с помощью «return $ this» и всех), но то, что я пытаюсь сделать, это связать их по-умному, взгляните на это:
$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');
То, что я понял из этого примера кода, состоит в том, что первые 3 метода (select, where, limit) строят оператор запроса, который будет выполнен, и последний (заказ) приходит, чтобы закончить оператор, а затем выполняет его и отбрасывает результат, правильно?
Но это не так, потому что я могу легко отказаться от любого из этих методов (кроме «выбрать», конечно) или, что более важно, изменить свой порядок, и ничто не пойдет не так! Это означает, что метод «select» обрабатывает работу, не так ли? Затем, как другие 3 метода добавляют / влияют на запрос после того, как метод «select» уже был вызван! ??
Нетрудно понять, что для достижения этого методы, которые должны быть связаны цепью, должны постепенно настраивать некоторую структуру данных, которая, наконец, интерпретируется каким-либо методом, который выполняет окончательный запрос. Но есть некоторые степени свободы относительно того, как это можно организовать.
Пример кода
$albums = $db->select('albums')->where('x', '>', '20')->limit(2)->order('desc');
Что мы видим здесь?
$db
является экземпляром, который предоставляет по крайней мере метод select
. Обратите внимание: если вы хотите полностью переупорядочить вызовы, этот тип должен выставлять методы со всеми возможными сигнатурами, которые могут принимать участие в цепочке вызовов. $db
. order
, который не выглядит правильным: мы хотим, чтобы он смог перенести его ранее в цепочке в конце концов. Давайте будем иметь это в виду. Поэтому мы можем разрушить то, что происходит в трех разных шагах.
Мы установили, что должен быть хотя бы один тип, который собирает информацию о плане запроса. Предположим, что тип выглядит следующим образом:
interface QueryPlanInterface { public function select(...); public function limit(...); // etc } class QueryPlan implements QueryPlanInterface { private $variable_that_points_to_data_store; private $variables_to_hold_query_description; public function select(...) { $this->encodeSelectInformation(...); return $this; } // and so on for the rest of the methods; all of them return $this }
QueryPlan
нуждается в соответствующих свойствах, чтобы помнить не только о том, какой запрос он должен выполнить, но и о том, куда направлять этот запрос, потому что это экземпляр этого типа, который у вас будет под рукой в конце цепочки вызовов; обе части информации необходимы для того, чтобы запрос был материализован. Я также предоставил тип QueryPlanInterface
; его значение будет разъяснено позже.
Означает ли это, что $db
имеет тип QueryPlan
? На первый взгляд вы можете сказать «да», но при ближайшем рассмотрении проблемы начинают возникать из-за такой договоренности. Самая большая проблема – это устаревшее состояние:
// What would this code do? $db->limit(2); // ...a little later... $albums = $db->select('albums');
Сколько альбомов будет извлечено? Поскольку мы не «перезагружали» план запроса, он должен быть 2. Но это не очевидно из последней строки, которая читается совсем по-другому. Это плохое соглашение, которое может привести к ненужным ошибкам.
Итак, как решить эту проблему? Один из вариантов был бы для select
сброса плана запроса, но в этом проблема противоположна: $db->limit(1)->select('albums')
теперь выбирает все альбомы. Это не выглядит красиво.
Этот вариант будет состоять в том, чтобы «запустить» цепочку, организовав первый вызов для возврата нового экземпляра QueryPlan
. Таким образом, каждая цепочка работает по отдельному плану запроса, и, хотя вы можете составлять план запроса по частям, вы больше не можете делать это случайно. Таким образом, вы могли бы:
class DatabaseTable { public function query() { return new QueryPlan(...); // pass in data store-related information } }
который решает все эти проблемы, но требует от вас всегда писать ->query()
впереди:
$db->query()->limit(1)->select('albums');
Что делать, если вы не хотите иметь этот дополнительный звонок? В этом случае класс DatabaseTable
должен также реализовать QueryPlanInterface
, с той разницей, что реализация будет создавать новый QueryPlan
каждый раз:
class DatabaseTable implements QueryPlanInterface { public function select(...) { $q = new QueryPlan(); return $q->select(...); } public function limit(...) { $q = new QueryPlan(); return $q->limit(...); } // and so on for the rest of the methods }
Теперь вы можете без проблем записывать $db->limit(1)->select('albums')
; компоновку можно описать как «каждый раз, когда вы пишете $db->something(...)
вы начинаете составлять новый запрос, который не зависит от всех предыдущих и будущих».
На самом деле это самая простая часть; мы уже видели, как методы QueryPlan
всегда return $this
чтобы включить цепочку.
Нам еще нужно сказать «ОК, я сочиняю, получаю результаты». Для этой цели можно использовать специальный метод:
interface QueryPlanInterface { // ...other methods as above... public function get(); // this executes the query and returns the results }
Это позволяет вам писать
$anAlbum = $db->limit(1)->select('albums')->get();
Нет ничего неправильного и многого с этим решением: очевидно, в какой момент выполняется фактический запрос. Но в этом вопросе используется пример, который, похоже, не работает так. Можно ли добиться такого синтаксиса?
Ответ и да и нет. Да в том, что это действительно возможно, но нет в том смысле, что семантика происходящего изменится.
У PHP нет возможности, позволяющей автоматически вызывать метод, поэтому должно быть что-то, что инициирует материализацию, даже если это что-то не похоже на вызов метода с первого взгляда. Но что? Ну, подумайте о том, что является, пожалуй, наиболее распространенным случаем использования:
$albums = $db->select('albums'); // no materialization yet foreach ($albums as $album) { // ... }
Можно ли это сделать? Конечно, если QueryPlanInterface
расширяет IteratorAggregate
:
interface QueryPlanInterface extends IteratorAggregate { // ...other methods as above... public function getIterator(); }
Идея здесь в том, что foreach
запускает вызов getIterator
, который, в свою очередь, создаст экземпляр еще одного класса, в который вводится вся информация, скомпилированная реализацией QueryPlanInterface
. Этот класс будет выполнять фактический запрос на месте и материализовать результаты по запросу во время итерации.
Я решил реализовать IteratorAggregate
а не Iterator
специально для того, чтобы итерационное состояние могло перейти в новый экземпляр, что позволяет выполнять несколько итераций по одному и тому же плану запроса без проблем.
Наконец, этот трюк foreach
выглядит аккуратно, но как насчет другого распространенного варианта использования (получение результатов запроса в массив)? Мы сделали это громоздким?
Не совсем, благодаря iterator_to_array
:
$albums = iterator_to_array($db->select('albums'));
Это требует много кода для написания? Наверняка. У нас есть DatabaseTable
, QueryPlanInterface
, QueryPlan
, а также QueryPlanIterator
мы описали, но не показаны. Кроме того, все кодированное состояние, в котором эти агрегаты классов, вероятно, должны храниться в экземплярах еще большего числа классов.
Стоит ли оно того? Вполне вероятно. Это потому, что это решение предлагает:
QueryPlan
хранит дескриптор в абстрактном хранилище данных, поэтому вы можете теоретически запросить что-либо из реляционных баз данных в текстовые файлы с использованием того же синтаксиса) QueryPlan
сейчас и продолжать делать это в будущем, даже в другом методе) QueryPlan
более одного раза) Совсем неплохой пакет.
Это требует очень элегантного решения.
Вместо того, чтобы изобретать колесо, загляните в существующую структуру (ы).
Я предлагаю Laravel с использованием Eloquent ORM. Вы сможете это сделать и многое другое.
Вероятно, вам понадобится метод, который ударяет по фактическому запросу, в то время как методы, такие как select
и order_by
просто сохраняют информацию до этой точки.
Вы можете сделать это неявным, хотя, если вы реализуете интерфейс Iterator и запускаете запрос, в первый раз rewind
или current
попадаете (думаю, foreach
) или счетчик , поэтому результат может быть сгенерирован вызовом count()
с объектом. Я лично не хотел бы использовать библиотеку, построенную таким образом, я бы скорее оценил явный вызов, чтобы я мог видеть, где запущены запросы.