В MVC модель должна обрабатывать проверку?

25

Я пытаюсь реорганизовать веб-приложение, разработанное мной для использования шаблона MVC, но я не уверен, следует ли проверять правильность в модели или нет. Например, я настраиваю одну из моих моделей следующим образом:

class AM_Products extends AM_Object 
{
    public function save( $new_data = array() ) 
    {
        // Save code
    }
}

Первый вопрос: Итак, мне интересно, должен ли мой метод сохранения вызывать функцию проверки для $ new_data или предполагать, что данные уже были проверены?

Кроме того, если бы это было предложение проверки, я думаю, что часть кода модели для определения типов данных будет выглядеть так:

class AM_Products extends AM_Object
{
    protected function init() // Called by __construct in AM_Object
    {
        // This would match up to the database column `age`
        register_property( 'age', 'Age', array( 'type' => 'int', 'min' => 10, 'max' => 30 ) ); 
    }
}

Второй вопрос: каждый дочерний класс AM_Object будет запускать register_property для каждого столбца в базе данных этого конкретного объекта. Я не уверен, является ли это хорошим способом сделать это или нет.

Третий вопрос: если проверка должна обрабатываться моделью, должна ли она возвращать сообщение об ошибке или код ошибки, и должно ли представление использовать этот код для отображения соответствующего сообщения?

Брэндон Вамбольдт
источник

Ответы:

30

Первый ответ: ключевая роль модели заключается в поддержании целостности. Однако обработка пользовательского ввода является обязанностью контроллера.

То есть контроллер должен переводить пользовательские данные (которые в большинстве случаев являются просто строками) во что-то значимое. Это требует анализа (и может зависеть от таких вещей, как локаль, учитывая, что, например, существуют разные десятичные операторы и т. Д.).
Таким образом, фактическая проверка, как в «правильно ли сформированы данные?», Должна выполняться контроллером. Однако проверка, как в «данные имеют смысл?» должны быть выполнены в рамках модели.

Чтобы прояснить это на примере:
Предположим, что ваше приложение позволяет вам добавлять некоторые сущности с датой (например, проблема с тупиком). У вас может быть API, где даты могут быть представлены в виде простых меток времени Unix, а при переходе с HTML-страницы это будет набор различных значений или строка в формате MM / DD / YYYY. Вы не хотите эту информацию в модели. Вы хотите, чтобы каждый контроллер по отдельности пытался выяснить дату. Однако когда дата затем передается модели, модель должна сохранять целостность. Например, может иметь смысл не разрешать даты в прошлом или даты, которые относятся к праздникам / воскресеньям и т. Д.

Ваш контроллер содержит правила ввода (обработки). Ваша модель содержит бизнес-правила. Вы хотите, чтобы ваши бизнес-правила всегда соблюдались, независимо от того, что происходит. Предполагая, что у вас есть бизнес-правила в контроллере, вам придется их дублировать, если вы когда-нибудь создадите другой контроллер.

Второй ответ: подход имеет смысл, однако этот метод можно сделать более мощным. Вместо того, чтобы последний параметр был массивом, он должен быть экземпляром, IContstraintкоторый определяется как:

interface IConstraint {
     function test($value);//returns bool
}

И для чисел вы могли бы иметь что-то

class NumConstraint {
    var $grain;
    var $min;
    var $max;
    function __construct($grain = 1, $min = NULL, $max = NULL) {
         if ($min === NULL) $min = INT_MIN;
         if ($max === NULL) $max = INT_MAX;
         $this->min = $min;
         $this->max = $max;
         $this->grain = $grain;
    }
    function test($value) {
         return ($value % $this->grain == 0 && $value >= $min && $value <= $max);
    }
}

Также я не понимаю, что 'Age'значит представлять, если честно. Это фактическое имя свойства? Предполагая, что по умолчанию существует соглашение, параметр может просто идти до конца функции и быть необязательным. Если не задано, по умолчанию используется значение to_camel_case имени столбца БД.

Таким образом, пример вызова будет выглядеть так:

register_property('age', new NumConstraint(1, 10, 30));

Смысл использования интерфейсов в том, что вы можете добавлять все больше и больше ограничений по мере продвижения, и они могут быть настолько сложными, насколько вы хотите. Для строки, соответствующей регулярному выражению. На свидание должно быть не менее 7 дней. И так далее.

Третий ответ: у каждого объекта Model должен быть такой метод Result checkValue(string property, mixed value). Контроллер должен вызвать его до установки данных. Он Resultдолжен располагать всей информацией о том, провалилась ли проверка, и, если она прошла, указать причины, чтобы контроллер мог соответствующим образом передать их в представление.
Если в модель передается неправильное значение, модель должна просто ответить, вызвав исключение.

back2dos
источник
Спасибо за эту статью. Это прояснило многое о MVC.
AmadeusDrZaius
5

Я не полностью согласен с «back2dos»: я рекомендую всегда использовать отдельный слой формы / валидации, который контроллер может использовать для проверки входных данных перед их отправкой в ​​модель.

С теоретической точки зрения валидация модели работает с доверенными данными (внутренним состоянием системы) и в идеале должна повторяться в любой момент времени, тогда как валидация ввода явно выполняется один раз для данных, поступающих из ненадежных источников (в зависимости от варианта использования и привилегий пользователя).

Такое разделение позволяет создавать многократно используемые модели, контроллеры и формы, которые могут быть свободно связаны посредством внедрения зависимостей. Думайте о проверке входных данных как о проверке белого списка («принять заведомо хорошее»), а о проверке модели как о проверке черного списка («отклоните заведомо плохое»). Проверка белого списка более безопасна, в то время как проверка черного списка предотвращает чрезмерное ограничение уровня вашей модели для очень специфических вариантов использования.

Неверные данные модели всегда должны вызывать исключение (в противном случае приложение может продолжить работу, не замечая ошибки), в то время как неверные входные значения, поступающие из внешних источников, не являются неожиданными, а скорее распространены (если только у вас нет пользователей, которые никогда не делают ошибок).

Смотрите также: https://lastzero.net/2015/11/why-im-using-a-separate-layer-for-input-data-validation/

lastzero
источник
Для простоты предположим, что существует семейство классов Validator и что все проверки выполняются со стратегической иерархией. Конкретные дети валидатора могут также состоять из специальных валидаторов: электронной почты, номера телефона, токенов формы, кода, пароля и других. Проверка ввода контроллеров бывает двух видов: 1) проверка существования контроллера и метода / команды, и 2) предварительная проверка данных (т. Е. Метод HTTP-запроса, сколько входных данных (слишком много? Слишком мало?).
Энтони Ратледж
После того, как количество входных данных проверено, вам нужно знать, что правильные HTML-элементы управления были отправлены по имени, учитывая, что количество входных данных на запрос может варьироваться, поскольку не все элементы управления HTML-формы представляют что-либо, если оставить их пустыми ( особенно галочки). После этого последняя предварительная проверка - это проверка размера ввода. На мой взгляд, это должно быть рано , а не поздно. Выполнение проверки количества, имени элемента управления и базового размера входных данных в валидаторе контроллера будет означать наличие валидатора для каждой команды / метода в контроллере. Я чувствую, что это делает ваше приложение более безопасным.
Энтони Ратледж,
Да, валидатор контроллера для команды будет тесно связан с аргументами (если таковые имеются), необходимыми для метода модели , но сам контроллер не будет, за исключением ссылки на упомянутый валидатор контроллера . Это достойный компромисс, так как не следует идти вперед с предположением, что большинство исходных данных будет законным. Чем раньше вы сможете остановить незаконный доступ к вашему приложению, тем лучше. Выполнение этого в классе валидатора контроллера (количество, имя и максимальный размер входов) избавляет вас от необходимости создавать экземпляр всей модели, чтобы отклонять явно вредоносные HTTP-запросы.
Энтони Ратледж,
Тем не менее, прежде чем приступить к решению проблем с максимальным размером входных данных, необходимо убедиться в правильности кодировки. Учитывая все это, это слишком много для модели, даже если работа заключена в капсулу. Отказ от вредоносных запросов становится излишне дорогим. Таким образом, контроллер должен взять на себя больше ответственности за то, что он отправляет в модель. Сбой уровня контроллера должен быть фатальным, без информации о возврате запрашивающей стороне, кроме 200 OK. Журнал активности. Брось роковое исключение. Прекратить всю деятельность. Остановите все процессы как можно скорее.
Энтони Ратледж
Минимальные элементы управления, максимальные элементы управления, правильные элементы управления, кодировка ввода и максимальный размер ввода - все это относится к характеру запроса (так или иначе). Некоторые люди не определили эти пять основных вещей, как определение того, следует ли удовлетворить запрос. Если все эти вещи не удовлетворены, почему вы отправляете эту информацию в модель? Хороший вопрос.
Энтони Ратледж
3

Да, модель должна выполнить проверку. Пользовательский интерфейс также должен подтвердить ввод.

Очевидно, что ответственность за определение действительных значений и состояний лежит на модели. Иногда такие правила часто меняются. В этом случае я бы кормил модель из метаданных и / или украшал ее.

сокол
источник
Как насчет случаев, когда намерение пользователя явно злонамеренное или ошибочное? Например, конкретный HTTP-запрос должен иметь не более семи (7) входных значений, но ваш контроллер получает семьдесят (70). Вы действительно собираетесь разрешить десятикратное (в 10 раз) количество допустимых значений для модели, когда запрос явно поврежден? В этом случае речь идет о состоянии всего запроса, а не о каком-либо конкретном значении. Стратегия глубокой защиты предполагает, что природа HTTP-запроса должна быть изучена перед отправкой данных в модель.
Энтони Ратледж,
(продолжение) Таким образом, вы не проверяете, являются ли указанные пользователем конкретные значения и состояния действительными, а что весь запрос действителен. Пока нет необходимости углубляться в это. Масло уже на поверхности.
Энтони Ратледж,
(продолжение) Невозможно форсировать проверку интерфейса. Нужно учитывать, что автоматизированные инструменты могут использоваться интерфейсом с вашим веб-приложением.
Энтони Ратледж
(После размышления) Допустимые значения и состояния данных в модели важны, но то, что я описал, затрагивает намерение запроса, поступающего через контроллер. Пропуск проверки намерения делает ваше приложение более уязвимым. Намерение может быть только хорошим (игра по вашим правилам) или плохим (выход за пределы ваших правил). Намерение может быть проверено с помощью основных проверок на входе: минимальные элементы управления, максимальные элементы управления, правильные элементы управления, кодировка ввода и максимальный размер ввода. Это предложение «все или ничего». Все проходит, или запрос недействителен. Не нужно ничего отправлять на модель.
Энтони Ратледж
2

Отличный вопрос!

Что касается разработки всемирной паутины, то что, если вы спросите следующее, также.

«Если неправильный пользовательский ввод поступает в контроллер из пользовательского интерфейса, должен ли контроллер обновлять представление в виде циклического цикла, заставляя команды и входные данные быть точными перед их обработкой ? Как? Как представление обновляется при нормальном режиме? Условия? Является ли представление тесно связано с моделью? Является ли проверка ввода пользователя основной бизнес-логикой модели или она является предварительной по отношению к ней и, следовательно, должна происходить внутри контроллера (поскольку пользовательские входные данные являются частью запроса)?

(В сущности, можно и нужно ли откладывать создание экземпляра модели до тех пор, пока не будет получен хороший ввод?)

Мое мнение таково, что модели должны управлять чистыми и нетронутыми обстоятельствами (насколько это возможно), не обремененными базовой проверкой входных данных HTTP-запроса, которая должна происходить до создания экземпляра модели (и, безусловно, до того, как модель получит входные данные). Поскольку управление данными состояния (постоянными или иными) и связями API - это мир модели, пусть базовая проверка ввода HTTP-запроса происходит в контроллере.

Подводя итоги.

1) Проверьте ваш маршрут (проанализированный по URL), так как контроллер и метод должны существовать, прежде чем что-либо еще может продолжаться. Это определенно должно произойти в области фронт-контроллера (класс маршрутизатора), прежде чем попасть к настоящему контроллеру. Duh. :-)

2) Модель может иметь много источников входных данных: HTTP-запрос, база данных, файл, API и, да, сеть. Если вы собираетесь поместить все свои входные проверки в модель, то вы считаете, проверку ввода HTTP-запроса часть бизнес-требований для программы. Дело закрыто.

3) Тем не менее, это близоруко, чтобы пройти через создание множества объектов, если ввод HTTP-запроса бесполезен! Вы можете знать, хорошо ли ** ввод HTTP-запроса ** ( который пришел с запросом ), проверив его перед созданием экземпляра модели и всех ее сложностей (да, возможно, еще больше валидаторов для данных ввода-вывода API и БД).

Проверьте следующее:

а) Метод HTTP-запроса (GET, POST, PUT, PATCH, DELETE ...)

б) Минимальный контроль HTML (вам достаточно?).

в) Максимальный контроль HTML (у вас слишком много?).

г) Правильные HTML-элементы управления (у вас есть правильные?).

e) Входное кодирование (обычно это кодировка UTF-8?).

f) Максимальный размер ввода (какой-либо из входных данных выходит за границы?).

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

То, что я описал здесь, попадает в цель запроса, поступающего через контроллер. Пропуск проверки намерения делает ваше приложение более уязвимым. Намерение может быть только хорошим (игра по вашим основным правилам) или плохим (выход за пределы ваших основных правил).

Намерение для запроса HTTP является предложением «все или ничего». Все проходит, или запрос недействителен . Не нужно ничего отправлять на модель.

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

Да, это означает, что даже ввод файла должен соответствовать вашим попыткам внешнего интерфейса проверить и сообщить пользователю максимальный допустимый размер файла. Только HTML? Нет JavaScript? Хорошо, но пользователь должен быть проинформирован о последствиях загрузки файлов, которые слишком велики (главным образом, они потеряют все данные формы и будут выгнаны из системы).

4) Означает ли это, что входные данные HTTP-запроса не являются частью бизнес-логики приложения? Нет, это просто означает, что компьютеры являются конечными устройствами, а ресурсы должны использоваться с умом. Имеет смысл прекратить вредоносную деятельность раньше, а не позже. Вы платите больше вычислительных ресурсов за ожидание, чтобы остановить его позже.

5) Если ввод HTTP-запроса плохой, весь запрос плохой . Вот как я на это смотрю. Определение правильного ввода HTTP-запроса получено из бизнес-требований модели, но должна быть некоторая точка разграничения ресурсов. Как долго вы оставите плохой запрос в живых, прежде чем убить его и сказать: «Эй, неважно. Плохой запрос».

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

6) Таким образом, за мои деньги HTTP-запрос (МЕТОД, URL / маршрут и данные) либо ВСЕ хорош, либо НИЧЕГО не может продолжаться. У надежной модели уже есть задачи проверки, о которых нужно заботиться, но хороший пастор ресурсов говорит: «Мой путь или верный путь. Приходите правильно или не приходите вообще».

Это ваша программа, хотя. «Существует более одного способа сделать это». Некоторые способы стоят больше времени и денег, чем другие. Проверка данных HTTP-запроса позже (в модели) должна стоить дороже в течение всего жизненного цикла приложения (особенно в случае увеличения или уменьшения).

Если ваши валидаторы являются модульными, проверка правильности ввода * HTTP-запроса ** в контроллере не должна быть проблемой. Просто используйте стратегический класс Validator, в котором валидаторы иногда также состоят из специализированных валидаторов (электронная почта, телефон, токен формы, капча, ...).

Некоторые считают, что это совершенно неправильно, но HTTP был в зачаточном состоянии, когда «Банда четырех» написала « Шаблоны проектирования: элементы многократно используемого объектно-ориентированного программного обеспечения» .

================================================== ========================

Теперь, поскольку это относится к обычной проверке пользовательского ввода (после того, как HTTP-запрос был признан действительным), он обновляет представление, когда пользователь ошибается, о чем вам нужно подумать! Этот вид проверки ввода пользователя должен происходить в модели.

У вас нет гарантии JavaScript на фронтэнде. Это означает, что у вас нет возможности гарантировать асинхронное обновление пользовательского интерфейса вашего приложения со статусами ошибок. Истинное прогрессивное улучшение будет также охватывать случай синхронного использования.

Учет синхронного варианта использования - это искусство, которое теряется все больше и больше, потому что некоторые люди не хотят тратить время и хлопоты на отслеживание состояния всех своих приемов пользовательского интерфейса (показать / скрыть элементы управления, отключить / включить элементы управления , сообщения об ошибках, сообщения об ошибках) на стороне сервера (обычно по состоянию отслеживания в массивах).

Обновление : на диаграмме я говорю, что Viewссылка должна Model. Нет. Вы должны передать данные Viewот, Modelчтобы сохранить слабую связь. введите описание изображения здесь

Энтони Ратледж
источник