Pull to refresh

Comments 77

Круто. Пойду внедрять.
Посоветуйте хорошую книгу по ZF?
Мне такая реализация не нравится, если честно.
Советую с книгой (Архитектура корпоративных программных приложений. Автор Фаулер М.)
Ну а потом с onPHP (ссылка на обзор)
Где бы собрать воедино все эти полезные финты.
это не какой-то специфический паттерн ведь, м?
нет, но само решение можно было свести к «Объекту запроса», которое является более гибким, чем приведенное
С одной стороны в Django всё тоже самое выглядит более красиво, с другой именно на Zend-е более ООП-но смотрится чтоль )))

Спасибо за пост.
Yii это вообще очень хороший способ познакомиться с фишками rails для тех кто не знает ruby или некогда его учить.
Да, заимствований из Rails в Yii прилично. Скоро ещё и миграции появятся.
еще бы именование методов было как в Rails то можно было бы сказать, что YII почти RAILS :)
А то этот «верблюжий» синтаксис укачивает :)
Зачем плодить столько кода?
Сделали бы один продуманный метод, независящий от данных.

Пример из своего кода:
$Object->Query('Sex=Female')->Intersect('Avatar=True');
Или так $Object->Query('Sex=Female;Avatar=True');
$Object->Query('Surname=Иванов')->Merge('Surname=Петров')

Плюс, удобно абстрагировать выборки вроде Top, Last, Random.
$Object->Query('All')->Substract('Random') — все, минус один лузер.
Для простых запросов это хорошо. А для чего-то посложнее уже не очень.
Например, выбрать только посты, помеченные тегами «PHP» и «Zend Framework» с кодом, приведённом в посте:
$post_table->select()->withTags('PHP', 'Zend Framework'))->fetchAll();

В Yii:
Post::model()->withTags('PHP', 'Yii')->findAll();

т.е. смысл как раз в куче разных методов, зависящих от данных и скрывающих сложную логику.
Чем это хуже к примеру
with ('Tags: PHP,Yii')
(псевдокод)
Плюс в том, что можно в with подставить другую модель, минус — немного более сложный API. Если попытаться сделать сверхуниверсальные методы, тяжко будет разбираться в том, какие к ним нужны параметры.
Я наверное, слишком привык к schemaless, потому не догоняю, ну да ладно =)
потому что тогда мне придется эту строку составлять. типа
->with('Tags: '. join(',', $tagsList));
вместо
->withTags($tagsList)
мне редко приходится в исходном коде писать названия, например, тегов

Можно использовать Zend_Db_Table_Select и не заморачиваться,
будет что то типа $select->where('sex = ?', 'female')->order('id DESC') и т.д.
Такой код больше подвержен ошибкам, потому что его нельзя атомарно протестировать. Так же он поощряет писать запросы прямо в контроллерах, что в итоге ведет к каше и при рефакторинге базы данных Вам придется переписывать все места, а не один select.
Где я указал, что запросы пишутся в контроллерах?
Запрос может приходить из роутера, или чего-то подобного.
Вообще-то, приведённый мной кусок кода, это не замаскированный под php sql — запрос, как раз таки при рефакторинге, вызовы Query не нужно будет менять.
Неудобно.

Например, если вы собираете условия «по пути», то придется конкатенировать какую-то переменную ($conditions). А если потом внезапно выяснится, что какое-то условие уже не нужно (опять же, во время прогона скрипта)? Намного удобнее использовать различные методы, вроде того же hasAvatar($hasAvatar = true).

А если нужно передать объект Select какому-то методу, чтобы он его по желанию изменил, тогда ссылку на строковую переменную придется передавать, что вообще бред, как я считаю.

Наглядно:

$conditions .= 'Avatar: true'; /* Ан нет, аватар не нужен */ $conditions = str_replace('Avatar: true', 'Avatar: false'); // Ппц

$select->hasAvatar()->hasAvatar(false);
* $conditions = str_replace('Avatar: true', 'Avatar: false', $conditions);
А если без переопределения селекта? Код не проверял, просто для примера.

Users extends Zend_Db_Table
{
 const STATUS_OFF = 0;
 const STATUS_ON = 1;
 
 protected $select;
 
 public function __construct ()
 {
  $this->select = $this->select();
 }
 
 public function getSelect ()
 {
  if (null === $this->select) {
   throw new Zend_Exception('Select object is not set.');
  }
  return $this->select;
 }
 
 public funtion setSelect (Zend_Db_Table_Select $select)
 {
  $this->select = $select;
 }
 
 public funtion status ($status = self::STATUS_ON)
 {
  $this->select->where('status = ?', $status);
  return $this->select;
 }
 
 public funtion hasAvatar ()
 {
  $this->select->where('avatar IS NOT NULL');
  return $this->select;
 }
}

$users = new Users();
$rows = $users->fetchAll($users->status()->hasAvatar());


* This source code was highlighted with Source Code Highlighter.
Тут допущена грубая ошибка. Вы в своих методах возвращаете селект, а методы определены в шлюзе. Как Вы будете строить цепочки?
Да, спасибо, не доглядел, надо делать return $this;
Напрашивается вариант с написанием обработчика __call для такого типа функций
public function status($status)
{
  return $this->where('status = ?', $status);
}
public function sex($sex)
{
  return $this->where('sex = ?', $sex);
}


* This source code was highlighted with Source Code Highlighter.

Правда нужно учесть что в Zend_Db_Select он уже есть и корректно скармливать ему если это не столбец
О, спасибо, что упомянули статью :)
А зачем под каждое условие писать свой метод?
Так ведь универсальнее?:
public function call($method, $params)
    {
      return $this->where($method." = ".$params);
    }


* This source code was highlighted with Source Code Highlighter.


Потому что для каждого условия могут быть свои операторы. Например для возраста может быть диапазон, для категории скажем IN (...)
Да и код более очевидный.
Ну если в таблице пара десятков столбцов — тогда, имхо, для многих можно использовать вызов через __call
А для некоторых условий выборки уже сделать свои методы.

_call можно расширить, например так:

public function call($method, $params)
    {
     if(is_array($params) {
      return $this->where($method." IN (".implode(',',$params).")");  
     } else {
      return $this->where($method." = ".$params);
     }
    }


* This source code was highlighted with Source Code Highlighter.
Мне кажется, что дальше будет только сложнее.

Как быть с ">", "<", «IS NULL» и др.?
А вот это другой разговор). У меня реализован метод selectBy($field, $value), который делает то что вы говорите. И это действительно удобно.
Это уже больше на функциональный код похоже. Но вообще да, такой подход используется скажем в magento для фильтрации коллекций. Метод addFieldToFilter(), в который подаются поле и возможные значения.
А еще Вы забыли учесть IS NULL, >, <, BETWEEN и много чего другого).
Я написал не готовый метод, а лишь дал понять, как можно было бы сделать.
Ведь не лучше написать 1 универсальный метод, в котором учесть многие условия выборки и им пользоваться?
А для более серьезных условий выборки уже написать отдельные методы.

Не лучше. Сложнее в тестировании и сопровождении, запутанный код, завязка на конкретную базу, невозможность учесть всех вариантов, тот кто будет пользоваться вообще не поймет что может этот код, отсутствие автокомплита.
В принципе, может Вы и правы. Хотя я видел подобные решения и, надо признать, лично мне такой вариант больше нравится.
Только не совсем понял, о какой завязке на конкретную базу идет речь. Ибо любой проект так или иначе завязывается на конкретную базу. Ваш пример тоже завязан.
100 процентной абстракции от базы никогда не получится, но в моем случае проще разобраться и поменять что то. Однажды мне пришлось переезжать с mysql на postgresql, когда уже было создано больше сотни таблиц и моделей. Благодаря использованию Zend_Db_Table_Select, многое не пришлось переписывать, хотя бы из-за автоматического квотирования селектом под каждую базу.
Да, но замена методов status() и sex() методом __call() никак не повлияло бы на перенос между базами данных.
Хотя действительно, внутри же используется тот же самый селект. Этот пункт можно вычеркнуть).
Это же очевидное решение. Добавление фильтров к объекту по мере необходимости.
Кстати метод

public function fetchRowIfExists($message = 'Данной страницы нет на сайте')

ужасен. Ужасен по нескольким причинам:
1) метод в зависимости от наличия строки возвращает разные типы данных (массив или строку ошибки)
2) логика обработки отсуствия строки в БД перенесена из контроллера в модель

По хорошему достаточно метода fetchRow(), который возвращает либо запись из БД, либо пустой массив (объект?), которй уже в контроллере проверяется на пустоту и выбрасывается соответствующее исключение, которое обработается центральным обработчиком исключений и покажет 404, либо вручную перенаправит на 404
Он не возвращает строку ошибки, а выкидывает исключение. Очень, кстати, удобный метод, который имеет аналоги во всех фреймворках (правда реализация может отличаться). После написания в тысячный раз
$row = где то взяли
if (!$row) throw exception <класс для 404 ошибки> // а еще все забывают это делать

мозг начинает искать способы упростить задачу.
выбрасывать исключения каждый раз, когда нет записи в БД — не кошерно. Иногда нужен действительно пустой массив, если нет данных.
Зачем каждый раз? Этот метод используется только в тех случаях когда нужно показать страницу с ошибкой 404. Во всех остальных случаях используется простой fetchRow.
да просто потом прийдется объяснять, почему метод называется именно так и почему в одних случаях случается магия (отображается 404), а в других — обычный поток выполнения
В общем то не вижу никакой магии. Этот метод давно и успешно применяется не вызывая ни у кого никаких противоречий. Если Вам нужно что то более очевидное то можно его переназвать так: fetchRowAndThrowExceptionIfNotExists(), ну или по короче).
В Django, аналогичная функция называется, если не ошибаюсь, get_object_or_404 — по-моему, название говорит само за себя. Есть также аналогичная функция для «набора записей» get_list_or_404
_or_404 уже лучше, но опять же смешиваеются слои приложения. Но это дело вкуса, конечно…
Не смешиваются, это не метод модели, а глобальная функция. Модель и параметры запроса передаются в неё в качестве параметров, из модели обычным путём делается выборка и, если выборка пуста, генерится исключение, к модели никак не относящееся — контроллер вызвал глобальную функцию, она сгенерировала исключение.
При этом нужно учитывать, что в python глобальная функция это не тоже самое что в php из-за пространств имен. В zf это можно оформить в виде хелпера. В разных проектах я использовал оба подхода.
Это да (по крайней мере пока популярные фреймворки не переписаны под 5.3 с его неймспейсами и прочими вкусностями), но главное, что использование подобных хелперов (а можно описать методом контроллера) не нарушает концепций MVC — модель ничего не знает о 404, а контроллеру не надо заботиться о пустом объекте или списке.
у меня контроллеры выглядят приблизительно так(суть).

$topic = $this->getTopic(); // Topic or Null

if ($topic) {
    return $this->render($topic);
} else {
    return $this->http(404);
}
Вот, а представьте что у вас таких мест пара тройка десятков. Сделайте хелпер который можно использовать так

тьфу, случайно нажал.

$topic = $this->getTopic(); // Topic or Null
return $this->renderOrHttp($topic, 404)

А внутри уже ваш иф.
function port ($argv) {
  if (count($argv) != 1 || !$this->isPortCode($argv[0])) {
    return $this->http(404);
  }
  
  $port = $this->getPort($argv[0]);
  
  if (!$port) {
    return $this->http(404);
  }

  return $this->renderPort($port);
}

function portsList ($argv) {
  if ($argv) {
    return $this->http(404);
  }
  return $this->renderPortsList();
}


Ну это грубо. Каждый раз могут быть другие причины появления 404
Если Вы так хотите этого нововведения, то почему это запостили на хабр, а не в maling list Zend. Если в таблице уже так сильно нужно что бы был доступ к селекту, попросите и в следующем релизе это добавят, а если нет — то Вам объяснят почему это делать нельзя и чем плох Ваш подход.
Из всей статьи вы заметили только то что нужно переопределить метод select? Это полезная плюшка сама по себе на которую уже есть давно есть тикет в трекере zf. Только статья о другом…
Тикет тикетом, а предложить патч. И что бы эта функция была по умолчанию в Zend_Db_Table?

Я просто похожий механизм использую в своём классе. И не пользуюсь Zend_Db_Table.
Я давно туда не заглядывал, но насколько помню там все было как надо и патч и объяснение. Сейчас в любом случае этого никто делать не будет т.к. в zf1 только багфиксы, а все силы на zf2. Во втором zf возможно это и не понадобится.

Кстати, не обязательно переопределять стандартный метод, можно написать свой и дергать через него. В любом случае это мелочь и каждый сам сможет это реализовать как ему удобнее.
Если в Rails мы можем писать так

User.active

то аналогичный код в PHP/Zend будет выглядеть в 2 строки

$u = User.new
$u->select()->active();

В принципе никто не мешает в PHP создать фабрику и вызывать что-то вроде $u = User::create()->select()->active() или даже $u = UserSelect::create->active()
Я сделал так:

public function __call($name, $arguments) {
$actionName = '';
foreach (array('fetchRowBy', 'fetchAllBy', 'deleteBy', 'countBy') as $_a) {
if (strpos($name, $_a) === 0) {
$actionName = $_a;
break;
}
}

if (empty($actionName)) throw new Model_DbTable_Exception(«Undefined method $name»);
$fetchField = substr($name, strlen($actionName));
$arguments = $this->_splitCallArguments($fetchField, $arguments);
return call_user_func_array(array($this, $actionName), $arguments);
}

protected function _splitCallArguments($fetchField, $arguments) {
$fetchFields = explode('And', $fetchField);
if (count($fetchFields) == 1) {
array_unshift($arguments, strtolower(Zend_Filter::filterStatic($fetchField, 'Word_CamelCaseToUnderscore')));
}
else {
$args = array_values($arguments);
$arguments = array();
for ($i=0;$i
Почему-то кусок кода обрезало:

protected function _splitCallArguments($fetchField, $arguments) {
$fetchFields = explode('And', $fetchField);
if (count($fetchFields) == 1) {
array_unshift($arguments, strtolower(Zend_Filter::filterStatic($fetchField, 'Word_CamelCaseToUnderscore')));
}
else {
$args = array_values($arguments);
$arguments = array();
for ($i=0;$i < count($fetchFields);$i++) {
if (array_key_exists($i, $args)) array_push($arguments, array(
strtolower(Zend_Filter::filterStatic($fetchFields[$i], 'Word_CamelCaseToUnderscore')),
$args[$i]
));
}
$arguments = array($arguments);
}
return $arguments;
}

Далее возможны вызовы типа: fetchAllByGenderAndStatus('female', 'active')

Но ваш метод тоже интересен. Нужно будет к нему присмотреться.
Sign up to leave a comment.

Articles