vakorovin
@vakorovin
Разработчик

Несколько вопросов к толковым разработчикам относительно mvc и php?

Доброго времени суток. Очевидно, что для кого-то вопрос нубский, но для меня актуальный.



У нас есть своя наработка, надстройка над биллингом. Изначально был набор скриптов, который постепенно заменил и существенно дополнил существовавшую web-морду биллинга. Так вот, сейчас вычленяем похожие запросы, разбиваем на классы. Хочу прийти к MVC, но вчитываясь в шаблоны проектирования Банды Четырех, статьи на Хабре и в Википедии, вижу, что web-разработчики искажают классическую модель MVC, навешивая на контроллер то, что ему делать не положено. И что самое главное, я иду тем же путем, наступая на те же грабли. Грубо говоря, сейчас я занят тем, что разбиваю функционал системы на контроллеры и модели (практически для каждого элемента базы данных создана модель). Не касаясь представлений (используется смарти, но речь не о темплейтах), объясните дураку, как убрать те косяки, которые я ниже перечислю.



Начну с того, что в процессе разработки я пришел к такой схеме:



index.php — точка входа

class/controller/*Controller.php — Контроллеры

class/model/*Model.php — Модели.



Остальное пока не важно. Цели, которые ставились, когда шел к этой схеме были просты: — Из всех наработанных скриптов убрать вывод данных в темплейты;

— Убрать все запросы в БД в отдельные классы (включая проверки на валидность и сообщения в лог изменений);

— Получить массив необходимых повсеместно данных (тарифные планы, населенные пункты и прочее);

— Привести работу с каждым элементом данных к нескольким вариациям:

index, create, update, remove плюс/минус специфичные действия (примерно как в yii);

— Авторизация. Уйти от прав доступа старого биллинга к правам доступа в новом.



Все это осложняется обратной совместимостью со старой веб-мордой, но это решили.



Так вот, то, к чему стремился, подхожу. Но работая в этом направлении уже сейчас знаю, что переписывать надо еще раз. Ведь создавая модель, я представлял себе объект-массив с методами для обновления данных. А в контроллере предполагал всю логику. Да, сократить код получилось неплохо. Но это снова говнокод.



Смотрите, у меня практически каждая модель — это класс, реализующий интерфейс ArrayAccess (для работы с объектом класса как с массивом). Но. Для обновления данных созданы методы. К примеру:



$user=new user(array(‘id’=>$uid)); # создание объекта
echo $user[‘name’]; # имя пользователя
$user->update_name(‘Vasya’); # обновление имени - проверка, изменение и запись в лог



Уже здесь у меня есть вопросы. Мне приходится при создании объекта user передавать массив. Почему? Потому что в большинстве случаев мы работаем с пользователем по id, но пополняет к примеру свой баланс абонент, указывая логин (ровно как и заходит в личный кабинет). Как эту строку можно оптимизировать? И нужно ли?



Второе. В классе же модели теперь у меня есть еще по 2 статических метода (далеко не во всех моделях): search($params) и get_all().

search используется для поиска элементов серди разрешенных. Например сотрудники одного филиала не могут видеть абонентов/тарифы другого. search ищет с учетом “прав видимости”. В тоже время get_all необходим при настройке системы. В админке те, кому доступ разрешен, должны видеть все элементы данных. Смотрите, для того, чтобы найти абонентов, у которых тарифный план с id=39 и населенный пункт Удовольск, приходится формировать такой запрос:



$params=array();
...
$tariff=get_param(‘tariff’, ‘int’, 0);
if ($tariff>0) $params[]=“tariff=$tariff”;

$cities=get_param(‘city’, ‘int_array’, array());
if (!empty($cities)) $params[]=”city IN (”.implode(‘, ’, $cities).”)”;
...
$users=user::search($params);



Если не вдаваться в подробности, что такое get_param, мне не по душе, что я формирую уже часть запроса здесь. По факту же, как мне кажется, контроллер не должен заниматься таким вот анализом. Как нормальные разработчики решают аналогичную задачу?



В тоже время возвращаясь к функции get_param, мне кажется она тоже какой-то уродливый рудимент, но как избавиться от нее пока не представляю. get_param принимает 3 параметра: название переменной в запросе (get/post), тип и значение по умолчанию. Собственно, возвращает либо найденное значение, либо значение по умолчанию. Приведу пример контроллера city:



...
function update(){
$core=core::getInstance();

$id=get_param('id', 'int', 0);
...
$city=new city(array('id'=>$id));
….

$do=get_param('do', 'string', '');

if ($do=='update'){
$name=get_param('name', 'string', $city['name']);
$phone_code=get_param('phone_code', 'int', $city['phone_code']);

$city->update_name($name);
$city->update_phone_code($phone_code);

header("Location: ?route=city/update&id={$city['id']}");
exit;
}

...


Когда данных в форме много, вот этих get_param разрастается такое количество, что хочется пойти устроиться разнорабочим на стройку. Опять-таки, как уйти от такого уродства?



Буду очень, очень и очень признателен тем, кто вникнет в описанные мной ошибки и направит в нужное русло. Весь код показывать стыдно, но может найдется кто, кто сможет выделить некоторое время и на консультации в личной переписке в плане ошибок, которые я допускаю.
  • Вопрос задан
  • 6134 просмотра
Пригласить эксперта
Ответы на вопрос 9
Fesor
@Fesor
Full-stack developer (Symfony, Angular)
M — модели… если упростить — это просто проекция вашего хранилища данных (будь то ORM/ODM или обычные файлы) на объекты. И все… Бизнес логику они содержать не должны. Так же модель никак не должна знать как именно она будет сохраняться в хранилище, ибо тогда при смене хранилища начнутся приключения. В этом смысле концепция AR в том виде, в котором она реализована в Yii мне не нравится. Какой-то класс CActiveRecord который знает кучу всего о том что и как он будет сохранять, и от него надо наследовать нашу простенькую модельку содержащую в себе всего пару полей. Опять же есть разные реализации паттерна ActiveRecord, хотя мне больше по душе DataMapper (например Doctrine), при котором у вас есть отдельный компонент, знающий о том что и куда пихать. Логика по выборкам в этом случае выносится в репозитории, эдакие менеджеры записей. Плюсы этого способа — очень легко в системе заменить тип хранилища, расширять и поддерживать за счет слабой связанности компонентов. Контроллеры знают только о том что есть репозитории, которые имеют для каждой записи свой интерфейс (аля getPendingPayments()), и они не знают откуда эти модели выходят, из базы, висят в памяти или еще чего.

V — тут все просто… шаблоны призваны отвязать представление от логики работы с данными, что бы изменение первого не вызывало редактирование какого-то сервиса/контроллера.

C — тут сложнее. Есть две концепции, толстые и тонкие контроллеры. Последние наиболее распространены, и включают в себя два слоя: сами контроллеры и сервисный слой. Контроллеры разруливают какое действие нужно произвести с данными в зависимости от пришедших параметров, прав пользователя и т.д. Они же разруливают ситуации когда нету исхомых данных, различные ошибки и т.д. Вся бизнес логика же выносится в сервисный слой. Сервисы должны минимально знать о контексте использования и заниматься только тем, для чего они были написанны. Скажем, сервис валидации данных ничего не должен знать о сервисе для обработки платежей, или о правах доступа и т.д. Ему на вход приходят данные, и он их проверяет по правилам, описанным для этой модели.

Так же есть некоторые рекомендации по поводу использования тонких контроллеров: дублирование кода контроллеров — это нормально. Если у вас есть несколько контроллеров, реализующих примерно одно и тоже. То есть если отличия минимальны, скажем права пользователей разные, разные вьюшки, разные модели (актуально для простых CRUD операций), то лучше уж пусть будет много очень похожих методов, чем городить какой-то один базовый контроллер и внутри городить логику для обработки всего этого. Так как вся бизнес логика вынесена в сервисы, дублирования именно ее нету. А за время жизни проекта многое может и поменяться, и намного удобнее вносить изменения в отдельные методы а не в один здоровенный супер класс.

Так же при работе с сервисами очень помогает использование контейнера зависимостей. Благо готовых реализаций (как простых, как Pimple так и сложных, как например DiC в Symfony2 или Zend2) достаточно много.

p.s. Мое мнение не претендует на истину. Если кто-то увидел что-то в таком подходе что-то не корректное, буду рад если укажите.
Ответ написан
Комментировать
@rozhik
В данный момент Вы переливаете из пустого в порожнее (простите).
Хочу прийти к MVC… web-разработчики искажают классическую модель MVC — не зря искажают, в классически-чистом виде она не подходит для вэб. Вместо нее используют модификации. Которые, как правило отличаются пониманием того, что не есть model и не есть view.
Перед тем как рефакторить — нужно поставить цели рефакторинга к примеру (простота расширения, производительность… ), а уж после этого смотреть на
паттерны итп (но ни как не наоборот). У меня сложилось впечатление, что это рефакторинг ради рефакторинга, и красивого слова MVC. Я вам их еще красивым много назову: MVVP, ORM, Observer…
То что Вы описываете имеет право на жизнь, но оно разнородно, и разностильно. По этому я очень рекомендовал бы перед тем как писать, определить всё таки цели, и дополнить ими вопрос. Мне почему-то кажется, что ответ Вы найдете на вопросы при их написании.
Ответ написан
PopeyetheSailor
@PopeyetheSailor
Получение существующей записи
$user=new user(array(‘id’=>$uid));

Создание новой
$user=user->create($data);

Уже как-то непонятно. Если идти по такому пути, то получение существующей записи сделать так:
$user=user->find_by('id', $uid);


Дальше
$user->update_name(‘Vasya’); # обновление имени - проверка, изменение и запись в лог

А если нужно обновить (установить) значение для другого поля, email, например, то создавать еще 1 метод?
$user->update_email(‘vasya@example.com’); # так не пойдет

Я бы посоветовал сделать что-то вроде
$user->update('email', ‘vasya@example.com’);


Вот тут
$data=array(
'name'=>get_param('name', 'string', ''),
…
'tariff'=>get_param('tariff', 'int', 0),
);


Незнаю, такого рода вы хотели советы, или что-то другое. Но а если смотреть глобально — какая у вас цель? Может быть стоит воспользоваться готовыми решениями? Легкий фреймворк, вроде CodeIgniter, или возможно Yii или даже Zend 2/Symfony 2.
Ответ написан
SunDrop
@SunDrop
Касательно работы с моделями, посмотрите в сторону www.phpactiverecord.org/ — AR паттерн.
Ответ написан
AmdY
@AmdY
PHP и прочие вебштучки
Забейте на MVC, он просто предлагает делить зоны отвественности между слоями, сами буковки для красоты и примера.

вот смотри, у тебя есть модель tariff и ты ей там же через массив передаёшь параметры.
ты пишешь $tariff = Tariff::findById(1); // SELECT * FROM tariff WHERE id =?
затем тебе нужно скривать старые тарифы.
$tariff = Tariff::findById(1); // SELECT * FROM tariff WHERE id =? AND hide <> 0
Затем у тебя тариф привязывается к городу и т.д.
Все эти правки ты делать водном месте в методе Tariff::findById, а плюсами пользуешься по всему коду.

Контроллер, у твоём варианте ты пока делаешь валидацию и работу с данными формы через него, а затем это надоест и создашь классы validator и form и постепенно через рефакторинг нафигачишь десяток слоёв, которые будут хорошо делать только свою задачу. Но не надо имея легаси код сразу продумывать как ты разделишь классы, нужно рефакторить и плесать от того, что получается.
Ответ написан
JekaRu
@JekaRu
Советую в качестве ORM взять Propel propelorm.org/ или Doctrine doctrine-project.org/
Проверенные временем проекты.
Ответ написан
@Masterme
Привет.

Вот так удобно работать с данными:

Открываем страницу /articles/, либо /articles/123, либо /articles/?date=2013-10-22

class ArticlesController extends BaseController {
  # для страницы /articles/
  function index ($get_params){
    if ($get_params->date){
      $articles = $this->Articles->find_by_date($get_params->date); // выбираем все статьи за заданную дату
    } else {
      $articles = $this->Articles->all(); // выбираем все статьи из таблицы
    }
    return compact('articles'); // этот массив пойдёт в шаблонизатор
  }

  # для /articles/123
  function item($get_params){
    if ($article = $this->Articles->find_first_by_id($get_params->id)){
      return compact('article');
    } else {
      throw new Exception404;
    }
  }
}


$this->Articles вызывает __get класса BaseController, в нём ленивая загрузка, если первая буква аргумента большая — то модель создаётся на лету. Аналогично, find_by_* и find_first_by_* — обращаются к __call родительской модели и преобразуются в SELECT.

Ещё примеров.
Создать пустую запись в таблице
$this->Article->create();


но обычно создают не пустую, а с данными, потому (в контексте контроллера)
$article = $this->post('article); // массив
if ($id = $this->Articles->create($article)){
  $article['id'] = $id;
}


Найти статьи и пользователей, их создавших:
$articles = $this->Articles->find_by_date('2013-04-15');
if ($authors_ids = array_map(function($article) {return $article['author_id'];}, $articles)){
  $authors = $this->Authors->find_by_id(array_unique($authors_ids));
}
return compact('articles', 'authors');

В модели эти команды преобразуются в
SELECT * FROM `articles` WHERE `date`='2013-04-15'
SELECT * FROM `authors` WHERE `id` IN (1, 2, 3, 4, 5);
if ($authors_ids = ...) — проверка, не пустой ли получится массив

Чем модель отличается от датасета.
$article = array('id' => 123, 'title' => 'Зима в деревне!', 'content' => 'Однажды в студёную зимнюю пору лошадка примёрзла ...', 'date' => '2014-06-01', 'author_id' => 1);
это датасет

а
$article = new ArticleModel(array('id' => 123, 'title' => 'Зима в деревне!', 'content' => 'Однажды в студёную зимнюю пору лошадка примёрзла ...', 'date' => '2014-06-01', 'author_id' => 1))
это модель

Разница в том, что на модель можно навешать методов и юзать такой синтаксис
$articles = $this->Articles->all();
foreach ($articles as $article){
print $article->author['name'];
}
т.е. в этом случае мы прописываем классу ArticleModel метод author, который возвращает значение типа AuthorModel и т.д.

$article->author->articles; // все статьи автора, написашего данную статью.

иногда так удобнее, иногда нет. с моей точки зрения разница невелика. я пользуюсь датасетами и примерно таким синтаксисом
$articles = $this->Articles->find_by_author_id($article['author_id']);
на один запрос меньше.

— примечание: есть модель в терминологии MVC, означает программный слой. а есть модель — как объект, соответствующий строке таблицы. я говорил про вторую. а первая модель это
$this->Articles
$this->Authors
Ответ написан
7workers
@7workers
Не бойтесь (научитесь?) перемещать методы из одного класса в другой. Помните, что PHP это уже в какой-то мере фрэймворк и парадигма. Собирайте код (структурно) так, как будет удобно Вам, как разработчику, а потом уже решайте, что у Вас получилось M, V или C.
Ответ написан
Комментировать
KEKSOV
@KEKSOV
Просто как идея — возьмите Phalcon и с его помощью создайте модель/контроллер для какой-нибудь из ваших сущностей и посмотрите, насколько удобно/быстро работать с этим фреймворком. Я недавно начал его изучение и пока в полном восторге. Для меня ключевым моментом в выборе именно этой платформы для своего очередного проекта стало то, что этот фреймворк является расширением PHP, написанным на С, иными словами, он обгоняет по скорости/памяти любое другое решение на чистом PHP. Ну и все плюшки MVC в нем есть, плюс поддержка одновременно работы с MySQL и PostgeSQL. Кроме того, на горизонте есть Zephir, который позволит писать компилируемые расширения для PHP. Может я и не прав, но для биллинга важна производительность, пользователей-то всегда много…

ПС. Если возникнет желание потестить его под виндой, то напишите в личку, там есть нюансы с установкой.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы