Zend_Db_Table_Select Dynamic Finder

    Привет, Хабр! Dynamic Finder

    Написал класс, использующий Zend_Db_Table_Select и позволяющий использовать Dynamic Finder в моделях в проектах на Zend Framework. Статья о том, что этот класс умеет, а также ссылка на исходный код предлагаются вашему вниманию.

    Что это, зачем?



    Dynamic Finder – способ, позволяющий получать данные из таблицы БД, записывая названия искомых полей в виде названия метода класса, а значения этих полей — в качестве аргумента метода. Например, может использоваться в экземпляре класса модели, связанной с какой-либо таблицей БД.

    Dynamic Finder позволяет избежать написания ряда методов вида getById(…), getByLoginAndPassword(…), getAllByCountry(…) внутри модели в виде построения полноценных SQL-запросов и выборок. Вместо этого, в данной реализации достаточно подключить Dynamic Finder к модели должным образом, и, далее, программист может использовать эти методы модели непосредственно в контроллере или представлении. При этом реально эти методы в модели вообще не существуют.

    Таким образом, экономится время работы программиста.

    Dynamic Finder уже был реализован в том или ином виде в различных библиотеках и фреймворках, в частности, в Ruby on Rails.

    В данной реализации Dynamic Finder является надстройкой, использующей Zend_Db_Select / Zend_Db_Table_Select, и предназначен для выборок из только одной таблицы.


    Как этим пользоваться?



    Используется несколько видов синтаксиса. Простейший:
    $modelObj->getByTag($tag);


    getBy… идентичен getAllBy… – т.е. подразумевается запрос с множеством строк. Для получения только одной строки можно использовать getOneBy…

    Синтаксис имен запрашиваемых полей, используемый в названии метода, заключается в записывании названий этих полей в CamelCase – режиме. При этом поля с именем вида user_name должны быть записаны в названии метода как UserName (каждое слово, начинающееся с символа подчеркивания, заменяется на слово с большой буквы). Разные поля в имени «виртуального метода» разделяются логическими операторами And или Or.

    Например,
    $user->getOneByLoginAndPassword($login, $password);


    Синтаксис аргумента «виртуального метода» поддерживает две формы: краткую (в качестве аргументов через запятую записывают значения полей, по которым строится запрос, см. примеры выше) и полную. Полная форма требует в виде аргумента метода ассоциативный массив и выглядит в самом общем виде так:
     
    $data = $files->getAllByPaperId(
        array('values'=>array($paperId, ...),
        'options'=>array(
             'order'=>array('orig_filename ASC'),
             'offset' => $offset,
             'limit' => 10
        ) ) );
     


    Порядок записи значений полей (массив values, либо простейший случай) должен соответствовать порядку названий полей в названии «виртуального метода». Массив options и содержащие его пары ключ-значение являются необязательными, однако для них нет и каких-либо значений по умолчанию. В качестве аргументов можно использовать и экземпляры класса Zend_Db_Expr:
    $data = $tagObj->getAllByTag(new Zend_Db_Expr(sprintf(" LIKE '%s%%'" , $beginStr)));


    Подключение к модели.



    На мой взгляд, проще всего сам объект класса Dymanic Finder заводить в модели как private – свойство:
    /**
     * Dynamic Finder object
     * @var DynamicFinder
     */

    private $_dynFinder;
     
    public function  __construct() {        
        if (!class_exists('DynamicFinder',false)) {
            require_once  'path_to'. '/DynamicFinder.php';
        }
     
        $this->_dynFinder = new DynamicFinder();
    }
     


    Первичная обработка «виртуального метода»; получение данных из БД после генерации желаемого Zend_Db_Table_Select:
    public function __call($name, $arguments)
    {
        $this->_dynFinder->select = $this->getTable()->select();
        $this->_dynFinder->allowedFields = $this->getTable()->info(Zend_Db_Table_Abstract::COLS);
     
        $select =  call_user_func_array(array($this->_dynFinder, $name), $arguments);
        if (strpos($name,'getOneBy')===0){
            return $this->getTable()->fetchRow($select);
        } else {
            return $this->getTable()->fetchAll($select);
        }
    }
     


    $this->getTable() должна возвращать ассоциированный с моделью объект Zend_Db_Table.

    В принципе, всё может быть совсем по-другому. Например, вы можете скармливать DymanicFinder в качестве select каким-то образом предварительно подготовленный экземпляр Zend_Db_Table_Select (Zend_Db_Select), отвечающий именно вашей конкретной задаче. Так же, другим образом можно передать и массив-список доступных в таблице полей. Аналогично, по-другому могут быть и обработаны результаты преобразования объекта Zend_Db_Select() (вместо условий по проверке getOneBy). Именно ради возможных, нужных конечному программисту доработок, Dynamic Finder в данном случае и сделан в виде такого «полуфабриката». Ради всевозможной кастомизации.

    Также есть вариант внешнего разбора имени метода (вместо внутреннего _camelCase2underscore() ). Т.е. ваши правила именования колонок в таблицах и в именах метода для файндера будут отличаться. В таком случае где-то до вызова метода __call надо определить свойство $this->columnParseCallback в виде callback (как для call_user_func()), в котором вы и зададите внешнюю функцию-парсер вашего имени метода.

    Реализация.



    А внутри у нас php-код, который не станет работать, если ему не передать ни наследника Zend_Db_Table_Select (достаточно и Zend_Db_Select при условии работы, например, с одной таблицей), ни список допустимых полей. А то мало ли что впоследствии программист при написании виртуального метода учудит? ;)

    Исходный код класса доступен по ссылке.

    UPD: Баг со строкой 337 исправлен, также чуть доправлено форматирование. Спасибо пользователю dmitry_dvp
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 47
    • +2
      Мне кажется красивее будет расширить Zend_Db_Table. Тогда вы избавитесь от лишних строк в конструкторе и __call методе ;)
      • 0
        В моделях обычно используют композицию, поэтому сама модель как правиль но является наследницей Zend_Db_Table.

        В особо интересных случаях модель может использовать даже не одну таблицу и не один экземпляр Zend_Db_Table (хотя мне такая архитектура и кажется странной).
        • 0
          Вы неправильно меня поняли!

          Не надо модели наследовать код от тейбла. Просто ваш DynFinder будет расширять Zend_Db_Table и в последствии заменять его. Тогда не нужен код в конструкторе, а _call превратится в
          public function __call($name, $arguments)
          {
          return call_user_func_array(array($this->getTable(), $name), $arguments);
          }

          Правда в итоге она наследует все ф-ции таблицы (как и в вашем случае), но можно поставить проверку на то что имя ф-ции начинается на get…

          А насчет одна модель использует 2 и более таблиц — это вы зря, вполне нормальный подход.
          • 0
            В том-то и дело, что не все разработчики, с которыми работал, были бы в восторге от такого наследования.

            Мысль такая в голову, конечно, пориходила.
          • 0
            *«как правило» — ох уж эти опечатки :)
        • +2
          А картинка к статье лукавая… в актуальном Zend Framework тоже самое делается проще:
          public function getTagById($id)
          {
            $table = $this->getTable();
            $res = $table->getAdapter()->fetchRow(
              $table->select()
                    ->columns('tag')
                    ->where('id = ?', $id)
            );
          
          • +1
            ну а если ещё и предметную область проанализировать, то этот метод бы выглядел так:
            public function getTagById($id)
            {
              $table = $this->getTable();
              return $table->getAdapter()->fetchOne(
                $table->select()
                      ->columns('tag')
                      ->where('id = ?', $id)
              );
            }
            

            или даже может так:
            • 0
              public function getTagById($id)
              {
                $row = $this->getTable()->find($id)->current();
                return $row ? $row->tag : null;
              }
              
              • 0
                А тепрь, допустим, мы ищем не по Primary key…
            • 0
              ->getADapter() тоже не обязательно)
              • 0
                Исходный код достаточно старый, из проекта накотором я учился ZF. Тогда разобраться отдельно с кавычками было также актуально :)
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Я, к примеру, так делал со многими редакторами — просто менял у них цвета текста, цвет выделенного текста, ключевых слова, скобок, фона и т.д., которые обычно можно выставить в настройках редактора. Такое проделывал с gEdit, с MS Visiaul Studio и со всякими другими.
                  У меня на тёмном фоне видно чётче и визуальная навигация быстрее, но, по идее, быстрее устают глаза.
                  • 0
                    NetBeans, тема City Lights, местами доправленная до собственных нужд
                  • 0
                    Хм, а вообще такой механизм поддерживается в ZF и сделан аналогично rails'овому
                    Вы можете также запрашивать строки из родительской таблицы, используя «магический метод». Zend_Db_Table_Row_Abstract вызывает метод: findParentRow('', ''), если вы вызываете метод объекта Row, соответствующий одному из следующих шаблонов:

                    *

                    $row->findParent([Zend_Db_Table_Select $select])
                    *

                    $row->findParentBy([Zend_Db_Table_Select $select])


                    На сколько помню, есть так же и findBy
                    • 0
                      А если что-то не неализовано, то нужный класс всегда можно расширить и дать сообществу.
                      • 0
                        хабрапарсер порезал код… в общем, есть такие магические методы как getParent[TableClass]By[Something] и, get[TableClass]By[Something]

                        например, имеем статью $article и можем получить все её теги: $article->getParentTags();
                        • 0
                          Правильно, но в роли something должно быть некое rule, описанное в $_refrence_map? Или можно обойтись?

                          И, потом, допустим ли поиск по нескольким полям? С условиями And или Or?

                          Да и мне, собственно, и не надо получать данные от связанной модели, мне надо получить данные по данной конкретной модели, причем скорее по нескольким полям (т.к. в простейших случаях — да, find по primary keys вполне себе ищет).

                          Если же речь идет о самостоятельно созданном Select — так собственно мой класс для генерации этого Select главным образом и предназначен. Как далее его использовать (find… vs. getAll...) — ваше право.
                          • 0
                            >Или можно обойтись?
                            Вроде как нельзя

                            >И, потом, допустим ли поиск по нескольким полям?
                            Ну да, тут проигрывает rails, но когда нужен поиск по нескольким полям, кто мешает просто указать нужные where() ?)

                            >мне, собственно, и не надо…
                            Не оправдывайтесь) Ваше решение очень даже хорошо, просто всегда нужно понимать где что следует использовать в конкретной ситуации… Если у вас описаны все модели как наследники Zend_Db_Table_Abstract, прописаны все отношения, то думаю таки лучше использовать то, что уже реализовано в ZF…
                            • 0
                              Ну так, по сути, я и писал генератор этих самых where вне контекста отношений и поиска только по ним :). Логин, пароль и уникальных хэш юзера могут сами по себе ни к чему не относиться (и свойство этих колонок будет простым index в таблице). В то время как числовой id юзера — таки да.

                              Словом, решение задумывалось преимущественно не под поля, описанные в отношениях между таблицами, а, скорее, под выборки очень произвольных данных из таблицы. А пример на картинке — только пример :).

                              • 0
                                >и свойство этих колонок будет простым index в таблице

                                Речь шла об индексе по колонке, конечно. Ох уж этот конец дня.
                      • +1
                        Я использую такой вариант.
                        в Table_Abstract определен метод selecBy($field, $value), который возвращает такой же select.
                        А сам класс Db_Table_Select расширен методами fetchAll(), fetchRow(), getPaginator(), count(), max(), random() и т.п.

                        Пример использования.
                        $this->view->paginator = $table->selectBy('category', 'name')->getPaginator($page, $per_page);

                        Такой подход, ИМХО, наиболее гибкий. Фактически мы можем вызывать selectBy столько раз сколько нам нужно, а на выходе получать строку, набор, кол-во, paginator и т.п.
                        Хотя в контроллерах ничего сложнее selectBy (один вызов) лучше не делать. Для этого нужно писать соотвествующие методы в таблицах и покрывать их тестами.
                        • 0
                          Речь идет о вашем наследнике Zend_Db_Table (Table_Abstract)?
                          • 0
                            Да, у меня есть небольшая надстройка над Zend имеющая префикс Ext.
                        • 0
                          В исходнике, в строке 337 синтаксическая ошибка: не закрыта скобка

                          функционал _camelCase2underscore($column), если я не ошибаюсь, уже есть в Zend/Filter
                          • 0
                            и ещё: странно выглядят конструкции

                            • 0
                              Да что он сам отправляет-то?

                              странно выглядят конструкции $this->$columnParseCallback при том, что public $columnParseCallback = array();
                              • +1
                                Также надо быть внимательным: есть тонкий момент с кэшированием.
                                Как известно, Zend_Db_Table умеет кэшировать метадату.
                                Так вот если на живой сайт опубликовать одновременно новый столбец в БД и код на основе этого модуля, использующий этот столбец — будет 187: unset($args['options']['order'][$key]); — сортировка работать не будет до сброса кэша.
                                • 0
                                  Спасибо за замечание, задумывался над этим во время разработки.

                                  Будем надеяться, что на живых серверах при апгреде БД рзаработчики бдут помнить о необходимости и сбросить кэш.
                                • 0
                                  В статье есть на эту тему:
                                  ===
                                  Также есть вариант внешнего разбора имени метода (вместо внутреннего _camelCase2underscore() ). Т.е. ваши правила именования колонок в таблицах и в именах метода для файндера будут отличаться. В таком случае где-то до вызова метода __call надо определить свойство $this->columnParseCallback в виде callback (как для call_user_func()), в котором вы и зададите внешнюю функцию-парсер вашего имени метода.
                                  ===
                                  • 0
                                    ну я примерно так и понял, не понял только почему
                                    $this->$columnParseCallback
                                    а не
                                    $this->columnParseCallback
                                    • 0
                                      упс, спасибо, сейчас же исправлю
                              • 0
                                Спасибо, исправил
                              • 0
                                меня всегда удивляло, почему за два года развития в Zend Framework нет хотя бы какой-нибудь реализации Active Record. каждый раз нужно изобретать велосипед, вместо того, чтобы получить все из коробки, как допустим в Yii framework.
                                • 0
                                  Я думаю, этот вопрос скорее к идеологам ZF: где-то в документации даже было, что AR каждый может разработать по-своему, основываясь на предоставленных ZF классах. Насколько я вижу — преимущественно на базе Zend_Db_Table_Row и Zend_Db_Table_Rowset.
                                  • 0
                                    А вас никогда не удивляло то что можно с лёгкостью прикрутить Doctrine к ZF?
                                    • 0
                                      а зачем если есть Zend_Db и Database Table Gateway Pattern?
                                      • 0
                                        Doctrine это альтернатива вышеназванным компонентам. Сторонники Active Record прикручивают Doctrine, любители Database Table Gateway Pattern используют нативный компонент, ну а для маленьких проектов можно вообще только PDO использовать. Вооо какой выбор!

                                        Я лично использую Doctrine, так как для Zend_Db нужно писать много кода ручками, а Doctrine сама всё генерирует. Хотя конечно тяжела гадина, +7-20mb каждый php процесс отжирает, благо сервер мощный ксеон. Обещали ко второй версии исправить ситуацию.
                                        • 0
                                          для Zend_Db можно один раза написать и наследоваться от своего класса постоянно, поэтому я не вижу у доктрин преимуществ перед зенд диби. тут дело вкуса.
                                          • 0
                                            Тут дело паттерна. Кому нравится паттерн DTGP, а кому AR.
                                            • 0
                                              я впринципе это и имел ввиду=)
                                    • 0
                                      orm != active record
                                      active record = row gateway (Zend_Db_Table_Row) + бизнес логика. www.design-pattern.ru/patterns/active-record.html

                                      Так что в зенде с active record все в порядке, а вот orm функционала нет и это действительно бывает неудобно. Правда в zf2 нас ждет переход на doctrine2, но это конец 2010 года, если повезет.
                                      • 0
                                        Да кстати, протупил выше. Имелся ввиду ORM.
                                        Насчёт перехода на doctrine, уже вроде отличный адаптер есть, и для пагинации в том числе.
                                    • 0
                                      Мне такое не очень нравится, но если бы ты сделал генерацию классов с докблоками и т. п. было бы здорово. И положи код на www.zfsnippets.com/
                                      • 0
                                        А как насчет Code Completion? IMHO, здесь время девелопера никак не экономится, т.к. надо помнить некие правила и соглашения о том, как и что.

                                        Именно по этой же причине я крайне негативно отношусь к helper`ам во View, которые по тем же принципам устроены.

                                        Вот когда авторы ZF добавят к документации класса @magic`и, тогда можно считать, что штука время экономит.
                                        • 0
                                          в Zend Studio интеллисенс в вьюшках работает
                                        • 0
                                          если я не ошибаюсь, то во второй Доктрине от этого отказались.

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