Русское сообщество разработки на PHP-фреймворке Laravel.
Ты не вошёл. Вход тут.
Страницы 1
Рискну задать вопрос касательно шаблона mvc и не начать холивара.
На данном форуме много умных и отзывчивых людей, от которых я не раз получал дельные советы.
Все вокруг кричат об тонких контроллерах и толстых моделях, только вот мне не всегда ясно как именно этого добиться.
Для начала приведу краткий перечень общепринятых правил(если что поправьте) по которым должно
строится mvc приложение + свое личное мнение по каждому из пунктов.
1. Котроллеры — это:
1.1 Только логика, логика и еще раз логика — вроде все ясно.
1.2 Никаких echo или упаси бог html-вставок — тут вопросов никаких.
1.3 Никаких sql-запросов — данный пункт лично мне не совсем понятен, если уж так вышло что в определенном месте мне нужно получить
одно единственное поле из одной единственной таблицы и при этом все необходимые данные у меня есть,
то почему мне не сделать SQL-запрос тут же на месте? Неужели необходимо «звать» модель на помощь?
Если так, то заводить для этого динамический метод как то не очень, можно написать статический метод(хелпер) — вроде все хорошо,
НО если запрос выполняется в одном единственном методе одного единственного контроллера, то
модель просто «захламляется» подобными хелперами.
2. Модели — это:
2.1 Прослойка между контроллерами и бд, т.е. якобы любые обращения к базе должны обрабатывать — в теории все звучит разумно.
2.2 Хранилище для правил валидации и статических методов валидации — тоже все логично.
2.3 Класс который «накладывается» на таблицу и в котором определяются отношения между связанными таблицами — все чудесно!
Laravel содержит прекрасную ORM — отношения между связанными таблицами сэкономили мне действительно много времени.
2.4 Ну и конечно банальная фраза: «Модель содержит бизнес логику» – боюсь, не совсем понимаю, что именно под этим понимают?
3. Виды- это:
В основном гора html-кода оформленного правилами CSS и с вставками в определенных местах управляющих конструкций php PHP(if/else,switch/case,foreach)
— все логично, так и поступаю и никаких проблем не испытываю.
Следовательно, меня уже давно беспокоит следующий вопрос.
Можно ли считать вызов вида PHP$user = new User()/$user = User::find($id)...
в контроллере целесообразным?
На мой взгляд, все хорошо. Подобным образом удобно создавать/изменять записи в различных таблицах, а также
удобно вносить изменения если какие либо поля таблицы были добавлены/удалены(а это происходит довольно таки часто).
Но я столкнулся с «проблемой» когда стало необходимо работать с данными, представленными в виде односвязного списка, элементы которого разбросаны по различным таблицам. Код методов стал очень объемным, и местами появились строки повторяющегося кода,
кое-где удалось воспользоваться статическими методами моделей — и это действительно повысило читабельность и изящество кода.
Но в определенных методах это все равно не выход, так как по моему сделает код еще более запутанным, и когда я к нему вернусь через месяц вносить какие либо коррективы станет еще труднее. Возможно то то уже сталкивался с подобной задачей?
Но с созданием/изменением/удалением данных еще пол беды, вот когда необходимо выбрать данные(т.е. пройтись по цепочке данных расположенных в различных таблицах) в зависимости от определенных условий(условиями также служат определенные выбираемых записей)
я начинаю делать еще более неочевидные и странные вещи. Т.е. для формирования данных дабы избежать вложенных цыклов и множества переменных я формирую одну структуру(по сути массив объектов) вида:
[0] => obj {
'field' => 'val'
'field_two' => 'val_two'
}
[1] => obj {
'field' => 'val'
'field_two' => 'val_two'
}
Для ее формирования я использую такую фичу php как динамическое добавление свойств к объекту, т.е. вся конструкция выглядит следующим образом:
while($condition){
$result[$i] = new stdClass;
$q = DB::first('SELECT * FROM table WHERE condition = ?',condition);
switch($q->field){
case '1':
$result[$i]->name = $q->name;
$result[$i]->direction = $q->direction;
break;
case '2':
$result[$i]->second_name = $q->name;
$result[$i]->anbalog = $q->direction;
break;
}
$condition = $q->condition;
$i++
}
При данном способе нет лишних полей у объектов, т.е. каждый элемент массива содержит только то что необходимо.
Но в итоге размер одного метода контроллера выходит довольно внушительным. Возможно, есть способ более грамотно реализовать данную задачу?
Список затронутых мной вопросов велик, но они уже долгое время не дают мне покоя.
Хотелось бы услышать мнение участников форума, а также возможные советы/ссылки на литературу.
Заранее спасибо.
Не в сети
Мне тоже интересен этот вопрос, особенно от опытных разработчиков и с примерами
Не в сети
Оказалось, что я не подписан на этот раздел и не заметил новой темы. Запоздалый, но всё же ответ.
- одно единственное поле из одной единственной таблицы и при этом все необходимые данные у меня есть, то почему мне не сделать SQL-запрос тут же на месте? Неужели необходимо «звать» модель на помощь?
Я бы сказал да. Модель — это шлюз к данным. Сегодня ты используешь SQL, завтра NoSQL. Не имея модели тебе нужно будет пройти по всем контроллерам в поисках таких мелких и вроде бы незначащих запросов. Имея модель ты всегда уверен, что просмотрев и откорректировав её больше ничего не упадёт.
Не понятен вопрос о захламлении мелкими методами. У тебя есть ORM, она покрывает 70% всех простых запросов.
MyModel::where('field1', '=', $field)->get('field2');
Модель «позвали на помощь», дали указания, получили данные. Зачем для этого прибегать к SQL?
Может появиться вопрос, к чему все эти методы, написал SQL — и вперёд. Никаких обёрток.
SQL — это строка. Работать со строкой менее наглядно и удобно. Например, ты составил запрос
sqlSELECT field2 FROM table ORDER BY date
. Отлично. А затем в контроллер добавилась фильтрация по пользователю. Была одна строка, получается три:
$sql = 'SELECT field2 FROM table';
$user = Input::get('author') and $sql .= ' WHERE user = '.$user;
$sql .= ' ORDER BY date';
Вроде разобрались. А как же SQL-инъекции? Ну, допустим:
$sql = 'SELECT field2 FROM table';
$user = Input::get('author') and $sql .= ' WHERE user = '.mysql_real_escape_string($user);
$sql .= ' ORDER BY date';
Длинновато, но пока читабельно. Но ведь есть подготовленные запросы!
$sql = 'SELECT field2 FROM table';
$user = Input::get('author') and $sql .= ' WHERE user = :user';
$sql .= ' ORDER BY date';
$stmt = $pdo->prepare($sql);
$stmt->bindParam('user', $user);
Код растёт, но пока в норме. А если нужно фильтровать по дате?
$where = array();
$user = Input::get('author') and $where[] = 'user = :user';
$date = Input::get('date') and $where[] = 'date > :date';
$sql = 'SELECT field2 FROM table';
$where and $sql .= ' WHERE '.join(' AND ', $where);
$sql .= ' ORDER BY date';
$stmt = $pdo->prepare($sql);
$stmt->bindParam('user', $user);
$stmt->bindParam('date', $date);
Не напоминает ли это уже самописную микро-ORM? Именно отсюда они и пошли. Рано или поздно ты напишешь свою глючную и нестандартную виртуальную машину Lisp обёртку над SQL.
- т.е. якобы любые обращения к базе должны обрабатывать — в теории все звучит разумно.
Более чем в теории. Аналогичный вопрос встаёт у начинающего кодера: почему нельзя вставить один небольшой кусок HTML прямо из контроллера? Ведь он один, ма-аленький! Нитко не заметит. А дизайнер потом переделывает шаблон и обнаруживается, что его куски почему-то тут и там выпали и ему не доступны.
Тоже самое здесь. При развитии системы ты начинаешь перерабатывать структуру таблиц и оказывается, что половина контроллера захламлена явными обращениями и «оптимизациями». В итоге рефакторинг БД превращается в рефакторинг всего кода.
- 2.4 Ну и конечно банальная фраза: «Модель содержит бизнес логику» – боюсь, не совсем понимаю, что именно под этим понимают?
Я тоже не знаю. Я такие фразы пропускаю мимо ушей. В моём понимании модели работают с данными напрямую, причём могут также содержать частоиспользуемые методы более общего назначения, например, нормализацию каких-то полей или их проверку.
Проверку можно делать и в контроллере, но не все ведь поля привязаны к конкретному типу страницы — скажем, если при смене пароля пользователем мы должны принимать только пароли от 6 символов, то при смене пароля администратором ограничение может быть снято. Тем не менее, и для администратора, и для обычного пользователя дата регистрации (если она редактируется) должна быть правильной датой — ведь оба живут на Земле, формат времени одинаков для обоих.
Тем не менее здесь граница размыта и по большей части на совести программиста. Но вне зависимости от этого модель в первую очередь — единственный шлюз для работы с данными.
- Но я столкнулся с «проблемой» когда стало необходимо работать с данными, представленными в виде односвязного списка, элементы которого разбросаны по различным таблицам.
Здесь нужен пример, на словах не понятно. Обычно подобные вещи хорошо схватываются join-ами. В некоторых случаях задача действительно большая и просто требует большой работы с БД — это нормально, можно создать отдельный класс или классы, где это будет более изящно.
- вот когда необходимо выбрать данные(т.е. пройтись по цепочке данных расположенных в различных таблицах) в зависимости от определенных условий(условиями также служат определенные выбираемых записей)
Одним запросом здесь может и не обойтись и это нормально. В зависимости от объёма задачи можно либо сделать классы с перекрытыми методами (например, fill($row) — каждый класс берёт для себя то, что нужно, возможно делая дополнительные запросы), либо просто иметь набор массивов полей:
$fields = array(
1 => array('name' => 'name', 'direction' => 'direction'),
2 => array('second_name' => 'name', 'anbalog' => 'direction'),
);
while ($condition){
$q = DB::first('SELECT * FROM table WHERE condition = ?', $condition);
$result[] = $store = new stdClass;
foreach ($fields[$q->field] as $field => $table) {
$store->$field = $q->$table;
}
}
Не в сети
Изучаю чужой код, заглянул в контроллер https://github.com/davzie/Laravel-Boots … es.php#L85 здесь прямо в контроллере данные из базы получают, правильно ли я понял что в таком случае лучше создать метод в модели к примеру create($id) со всеми проверками, а уже в контроллере его использовать?
Не в сети
Нет, это вполне обычная работа в контроллере — получение элемента данных, его обработка и сохранение. Там ведь не используется SQL или DB::table(), обычный Model::find(). Работа с ORM.
Проверки можно выносить в модель или нет, это спорный вопрос — в данном случае проект небольшой и проверки можно делать в контроллере, так как они вряд ли будут нужны где-то ещё. Обычно я стараюсь их делать именно там, потому что в модели обычно и так достаточно других методов.
Не в сети
А как же тонкие контроллеры и толстые модели?
Не в сети
Почему я спросил с примером
Вот https://github.com/davzie/Laravel-Boots … es.php#L60
и вот https://github.com/davzie/Laravel-Boots … es.php#L85
Здесь явное дублирование кода, если вдруг изменится структура таблицы, еще одно поле добавили, придется ходить по контроллерам и дописывать везде где есть создание объекта новое свойство, не разумнее ли вынести отдельным методом в модель?
Не в сети
- А как же тонкие контроллеры и толстые модели?
Я не сторонник идеи придерживаться концепций ради их самих, лучше в каждом конкретном случае смотреть, стоит оно того или есть лучший вариант.
Конкретно по этим примерам. В целом ты прав и я бы сделал именно так — вынес присвоение полей в отдельный метод в модели. Другой вариант — использовать fill() и задать для него доступные поля; затем для свойств, где нужна особая обработка (slug, например), добавить методы (set_slug()), которые их приводят к нужному виду. Тогда весь код в post_create() сократится до:
$page = new Page(Input::get());
$page->created_by = $this->data['user']->id;
$page->save();
Page::create(Input:;get());
Не всегда применим именно такой подход. В целом, конечно хорошо, когда получается избежать дублирования и явного перечисления полей вне модели, но надо понимать, что данные сами по себе ведь не нужны, поэтому работать с ними вне модели придётся в любом случае (для этого и пишется собственно всё приложение). Если добавится новое поле или изменятся существующие — почти наверняка придется менять еще несколько мест, как минимум шаблоны. Какой-то код будет повторяться, это нормально; насколько много — отчасти вопрос вкуса.
Не в сети
Спасибо за развернутый ответ!
Не в сети
Спасибо за ответ. Чувствую что некоторые вещи для меня теперь прояснились! Отдельное спасибо за подробный ответ касательно
sqlSQL
в контроллерах, теперь начну понемногу избавляться от уже написанных запросов в контроллерах, и операции с базой теперь буду стараться делать через модель)))
Но я столкнулся с «проблемой» когда стало необходимо работать с данными, представленными в виде односвязного списка, элементы которого разбросаны по различным таблицам.
Постараюсь как нибудь привести пример кода, просто его там очень туева хуча много, и выбрать отдельный короткий фрагмент который бы демонстрировал всю суть проблемы довольно сложно.
В зависимости от объёма задачи можно либо сделать классы с перекрытыми методами (например, fill($row) — каждый класс берёт для себя то, что нужно, возможно делая дополнительные запросы)
Можно более подробно об этом? Боюсь не совсем понял о чем тут идет речь…можно ли пояснить хотя бы в виде псевдокода/ссылок для прочтения?
Не в сети
Я рад, что помог в понимании каких-то аспектов
- Можно более подробно об этом? Боюсь не совсем понял о чем тут идет речь…
Я в том же месте уже дал один вариант кода, если поле-критерий (PHP$q->field
) влияет только на названия полей, которые надо присвоить объектам. Если это более сложный случай можно сделать так:
abstract class Store {
abstract function fillFrom(array $row);
}
class StoreType1 extends Store {
function fillFrom(array $row) {
$this->name = $row->name;
$this->direction = $row->direction;
}
}
class StoreType2 extends Store {
function fillFrom(array $row) {
$this->second_name = $row->name;
$this->anbalog = $row->direction;
$this->from_db = DB::first('SELECT * FROM table2 WHERE x = ?', $row->x)->field;
}
}
while ($condition){
$q = DB::first('SELECT * FROM table WHERE condition = ?', $condition);
// можно для этого же использовать массив значение_field => имя класса
switch ($q->field) {
case 1: $store = new StoreType1; break;
case 2: $store = new StoreType2; break;
}
$store->fillFrom($q);
$result[] = $store;
}
Пример несколько надуманный, но логика должна быть понятна: мы выносим логику создания объекта из цикла в перегруженный метод этого самого объекта (используется вместо stdClass).
Не в сети
Имея уже отдельные объекты с разнесённой логикой мы можем делать более сложные вещи, не внося их в тело цикла. К примеру, если запросов наподобие from_db в StoreType2 будет делаться много можно их сделать за один запрос; для этого после цикла можно вставить такой код:
$accumulated = array();
foreach ($result as $store) {
$store[get_class($store)]-= accumulate($accumulated);
}
foreach ($accumulated as $class => $data) {
$class::finalize($data, $result);
}
А классы расширить (это уже больше псевдокод):
class StoreType2 {
function accumulate(array &$array) {
$array[] = $this->anbalog;
}
static function finalize(array $accumulated, array $stores) {
$allID = DB::query('SELECT * FROM table WHERE id IN (?, ...)', $accumulated);
foreach ($stores as $store) {
if ($store instanceof static) {
$store->from_db = $allID[$id];
}
}
}
}
Таким образом после цикла каждый класс собирает нужные ему данные для одного (или нескольких) завершающих запросов в один массив, а затем делает этот запрос один раз и распределяет полученные поля по созданным ранее объектам.
Решение получается в стиле ООП и потому гибкое, но более громоздкое — нужно смотреть насколько это нужно в конкретном случае.
Не в сети
Не в сети
Страницы 1