а что делает объект Context ? (можно просто ссылкой)
Это контейнер (возможно, лучше было бы его именно так назвать) для других объектов плюс в нем собраны методы, которым я не нашел места в других классах. Проще всего объяснить кодом:
my $c = WCP::Context->new();
$c->{displayer} = WCP::Displayer::TT->new();
$c->{config} = WCP::Config::Base->new();
$c->{response} = WCP::Response->new();
$c->{request} = WCP::Request::CGI->new;
my $fc = WCP::FC->new();
$fc->process($c);
То есть валидация и CRUD-операции не будут находиться в одном (доменном) классе ?
Я планирую сделать валидацию параметров, полученных от пользователя, отдельным этапом (как в Struts). Т.е. сначала выполняется метод validate класса-команды и лишь затем, в случае успеха, execute. Т.е. при передаче параметров в доменный класс, последний знает, что параметры гарантировано проверены. С точки зрения безопасности это не лучшее решение, но зато очень гибкое. Механизм валидации почти такой же, как в WACT. Вот пример (то что под оберткой):
sub validate
{
my ($self, $c) = @_;
my $validator = WCP::Validator->new();
my $md = WCP::Validator::MessageDictionary->new();
$validator->addRule(\'title\', \'title\', \'Length\', [3, 10]);
# То же самое, что $validator->addRule(\'title\', \'title\', WCP::Validator::Rule::Length->new(3, 10));
$validator->addRule(\'title\', \'title\', \'Regexp\', [ \'^[a-zA-Z ]+\' ]);
$validator->addRule(\'categoryid\', \'categoryid\', \'Regexp\', [ \'^\\d+$\' ]);
$validator->addRule(\'type\', \'type\', \'Regexp\', [ \'^\\d+$\' ]);
$md->registerMessage(\'Недопустимые символы.\', \'REGEXP_FAILED\');
$md->registerMessage(\'Слишком короткий.\', \'LENGTH_MIN\');
$md->registerMessage(\'Слишком длинный.\', \'LENGTH_MAX\');
my %params = $c->request->getParams();
if ($validator->validate(\\%params)) {
return 1;
} else {
$c->setErrors($validator->getMessageList($md));
return;
}
}
Принцип работы прост: при регистрации правила первым параметром указывается имя поля (или ссылка на массив имен полей), которые участвуют в проверке, вторым - имя группы, а третьим и четвертым - имя класса и параметры для конструктора. Правило имеет метод validate, которое возвращает булево значение. Если валидация не удалась, посредством метода getCodes можно получить массив констант, обозначающих неудавшиеся проверки (например, LENGTH_MIN, LENGTH_MAX и т.д.). После регистрации произвольного числа правил и валидации, в случае возникновения ошибок для каждой группы сохраняется массив (в порядке, указанном при регистрации правил) кодов ошибок. Грубо говоря:
{
\'title\' => [
\'LENGTH_MIN\',
\'REGEXP_FAILED\'
],
\'categoryid\' => [
\'REGEXP_FAILED\'
]
};
Потом в словарь сообщений добавляются тексты: или общий для каждого типа ошибки (например, для REGEXP_FAILED "Недопустимые символы") или для каждого типа ошибки и группы (например для title + REGEXP_FAILED "Поле title содержит недопустимые символы").
Отображение выполняется при помощи следующего шаблона:
[% INCLUDE header.tpl title="Добавление документа"%]
[% BLOCK field %]
[% IF errors.${fld_name} %]
[% errors.${fld_name}.0 %] |
[% END %]
[% IF errors.${fld_name} %][% fld_title %]: [% ELSE %][% fld_title %]:[% END %] |
[% END %]
Т.е. если при заполнении какого-то поля были допущены ошибки, поле выделяется цветом, а под ним пишется текст первой ошибки (или можно вывести список всех ошибок сразу же).
Все немного сумбурно, т.ч. если будут какие-то конкретные вопросы - спрашивайте. По валидатору у меня есть документация, немного доработаю и выложу.
Решил выложить на обозрение весь механизм валидации.
По правилам оформил небольшой обзор:
Документация по правилам (http://feotast.net/wcp/rule.html)
Но правила - это только основа всего механизма. Остальное продокументирую потом, а пока можно скачать архив (http:// [url=http://feotast.net/wcp/wcp.rar) и посмотреть на реализацию. Код довольно чистый, я вложил пример т.ч. все должно быть понятно. Имейте в виду, что это черновой вариант =) Работа с сообщениями и MessageDictionary точно будут перерабатываться.
Повесил блог, писать буду там, а комментировать можно здесь.
Блог: http://2netfly.com/blog/ .
Отписал довольно много аналитики, сумбурно, конечно, но общие принципы уже уловить можно.
По поводу http://2netfly.com/blog/?p=6
1. По поводу get/set - методов.
Про set - согласен. А вот get-методы ИМХО могут быть полезны. Код буду приводить на ПХП (мне он ближе)
Возьмем типичную новость. Ее надо показать на странице юзеру, ее надо показать в админке, возможно где-нибудь еще. Например дату новости на всех этих страницах надо привести к одному формату. В тексте новости надо символы перевода строк заменить на
а теги <> на & lt; & gt;
Если все это делать в шаблонах или ручкми после получения, то получиться избыточный код. Поэтому имхо удобнее сделать get-методы :
function getDate() {
return date(\'Y.m.d\', $this->fields[\'date\']);
}
а вшаблонах вызывать $news->getDate(); - хотя это уже зависит от уровня представления.
2. Все-таки в 95% случаев данные добавляются через POST.
if (POST_METHOD) {
if ($news->add()) {
redirect(\'news_added.php\');
} else {
$errors = $news->errors; // получаем массив ошибок
}
} метод add() сам загружает данные из пост, проверяет и записывает в БД (я все таки за проверку данных внутри класса)
3. мне кажется более логичным такое разделение классов :
- один базовый класс(DBObject) и все его наследники занимаются отображением одного объекта. В основном он занимается CRUD-операциями + по желанию (например если объектом является юзер, то занимается его авторизацией). Причем READ-операция делает только чтение одного объекта (не списка)
- один базовый класс (DBObjectList) и все его наследники занимаются работой со списком объектов. Это все выборки из БД и все что с этим связано - постраничная разбивка, определение кол-ва найденных объектов, некоторые манипуляции с полем ORDER BY.
while ($obj = &$list->getOne()) {
/// $obj - объект типа DBObject
}
4. и немного о проблеме модификации класса при добавлении новых связей.
Я в своем классе сделал поле $_fields, в которое помещаются все неиспользованые данные. Например объект новостей получил хеш-массив:
$fields = array(
"id"=> 12,
"subject"=>"....",
"text"=>"....",
"blablabla"=>"XXX", /// <<<
"date"=> "....")
Допустим объект новостей ничего не знает о поле "blablabla", тогда это поле и его значение будут помещены в $_fields (на всякий случай) и к нему я всегда смогу обратиться.
Теперь если мне в готовом проекте вдруг понадобилось подключить к новостям комментарии и выводить их кол-во, то я меняю запрос :
SELECT news.*, [b]COUNT(comments.*) as comments_amount[/b] FROM ..... LEFT JOIN .....
а в шаблоне напишу =intval($news->_fields[\'comments_amount\']);?>
Но эта фича скорее для "грязных хаков", чем для постоянного использования :)
Во-первых, спасибо за ответ, наконец-то появилась возможность подискутировать на интересную мне тему =) Во-вторых, если не сложно, продублируй свой ответ в блоге - его читаю пользователи других форумов, возможно они тоже захотят принять участие в дискуссии. Теперь ответы по пунктам.
1. Я довольно скептически отношусь к использованию объектов в шаблонах и, как следствие, к представлению, например, новости, как объекта. В моем понимании новость - это ассоциативный массив, ключи которого дублируют поля таблицы, и, если необходимо, некоторые дополнительные значения, которые предварительно устанавливаются доменным объектом. А список новостей - это массив таких хэшей. Может в будущем я изменю свое мнение, но сейчас мне кажется, что объектам в представлении не место (за исключением статических методов построения блоков информации).
Касательно замены определенных символов в некоторых полях, приведения даты к стандартному формату и прочего - я размышлял над этим, но пока не пришел к окончательному выводу, хотя уже вижу два пути реализации:
а) изменять данные непосредственно после выборки и хранить их в новом формате.
б) делать то же самое, но не в коде, а при помощи xml конфига. Пример подобного конфига был приведен в отчете о PHP конференции (доклад посвященный ORM).
2. У меня существует отдельный класс Request который занимается парсингом параметров и он умеет преобразовывать, например, /news/show/123/ в action=show&id=123. Использования такого класса упрощает модификацию обработки и пасинга параметров путем просто переопределения нужных методов.
3. Раньше все так и было. Все классы наследовали Object (в нем методы select, update, create, delete, getAll), зачастую в классах-наследниках указывался только путь к xml конфигурационному файлу (в нем определились короткие синонимы для полей, указывался первичный ключ, его тип и пр.), а так же был статический метод list (find) который занимался работой со списком. Однако вскоре стали появляться большие проблемы. Например, все документы я храню в одной таблице, информацию об авторах - в другой, иконку для документа - в третьей, информацию о источнике - в четвертой и т.д.. Я так и не смог придумать, как правильно реализовать метод выбора одной новости и списка новостей в этом случае и в какой класс его поместить. С одной стороны загромождать класс Document неправильно, с другой - больше эти методы и деть-то некуда.
Вот такой вот был запрос для списка (немного упрощенный =):
my $query = "
SELECT *
FROM $TBL{document}{_} AS doc
LEFT JOIN $TBL{author}{_} AS a ON a.$TBL{author}{id} = doc.$TBL{document}{authorid}
LEFT JOIN $TBL{category}{_} AS cat ON cat.$TBL{category}{id} = doc.$TBL{document}{categoryid}
LEFT JOIN $TBL{source}{_} AS s ON s.$TBL{source}{id} = doc.$TBL{document}{sourceid}" .
($self->SQLOrderBy($sparam->{OrderBy})) .
$self->SQLLimit(@{$sparam->{Limit}});
А так я вынес все методы отображения в отдельный класс и теперь доменные классы практически не подвергаются правке. Ну и в шаблоне это дело удобно использовать:
[% News::List("tmp_1", LimitForm => 0, LimitTo => 3, OrderBy => ‘date’, OrderType => ‘DESC’) %]
[% News::List("tmp_2", LimitForm => 3, LimitTo => 10) %]
tmp_1, tmp2 - это тройные шаблоны (альтернатива foreach).
Все, конечно, упрощенно. Можно обрабатывать ситуации, когда список пуст и т.д., но общая идея должна быть понятна.
Кстати, весьма интересная реализация работы с шаблонами есть в WordPress (блоге). Я думаю что для PHP (с перлом другая история) шаблонизаторы типа смарти - это излишество.
2NetFly:
Во-первых, спасибо за ответ, наконец-то появилась возможность подискутировать на интересную мне тему =)
ну насчет этого особо не радуйся, времени на обдумывание и дискуссию у меня мало. Я в последнее время мало ORM занимаюсь и нет/жаль свободного времени на это.
По поводу блога - подними эту тему к выходным, я все тексты туда переброшу. У тебя там какой-нибудь ББ-код есть для подсветки синтаксисиа ?
2NetFly:
1. Я довольно скептически отношусь к использованию объектов в шаблонах и
2NetFly:
Ну и в шаблоне это дело удобно использовать:
[% News::List("tmp_1", LimitForm => 0, LimitTo => 3, OrderBy => ‘date’, OrderType => ‘DESC’) %] [% News::List("tmp_2", LimitForm => 3, LimitTo => 10) %]
:) Или на статические вызовы методов в шаблонах ты смотришь иначе ?
3. Да я с таким тоже сталкивался и как правило все делал через Ж...
Славу богу таких случаев было не так много.
Вообще я считаю что такой фреймворк должен упрощать разработку простых скриптов и хотя бы не усложнять разработку сложных вещей. И я не вижу ничего плохого в том, чтобы иногда отказываться от ORM, когда связей слишком много и они затрудняют работу.
Что касается твоего примера, то у меня давно крутится в голове идея, но до реализации пока так и не дошло. Суть примерно такая.
Пишем класс - назовем его SQLLoader. Назначение - он получает SQL-запрос, список пустых объектов (может быть список классов, но интуитивно объекты мне нравятся больше), выполняет этот запрос и возвращает объекты. Покажу на примере :
$a_list = new AuthorList(); // список авторов
$c_list = new CommentList(); // список комментов
$loader = new SQLLoader();
$loader->setSQL("SELECT a.*, c.* FROM authors a, comments c WHERE c.author_id = a.author_id AND c.article_id = 12");
$loader->setObjects($a_list, $c_list);
l$loader->load();
дальше в зависимости от реализации (например если объекты были переданы по ссылке) их можно уже использовать
Пока вижу здесь 2 проблемы
- обеспечить ссылки между связаными объектами (именно это я пока не продумал)
- неудобно работать, если в связанных таблицах будут поля с одинаковыми именами полей
Кстати, в таком случае ИМХО есть преимущества в том, что "объект" и "список объектов" - это 2 разных класса.
$author = new Author(); // один автор
$comment = new Comment(); // один коммен
$loader = new SQLLoader();
$loader->setSQL("SELECT a.*, c.* FROM authors a, comments c WHERE c.author_id = a.author_id AND c.article_id = 12");
$loader->setObjects($a_list, $c_list);
while (l$loader->load()) {
....// здесь используем объекты $author и $comment
....// на каждой новой итерации они будут иметь новые значения
.... /// в этом случае и со ссылками проблем не будет
}
Но это все вымышленный код, пока нет времени (да и потребности) реализовывать его.
Или на статические вызовы методов в шаблонах ты смотришь иначе?
У этих статических методов иное назначение, без них обойтись нельзя. Я имел в виду объекты как сущность (новость, комментарии и т.д.).
Касательно объектов и коллекций объектов в представлении - я наконец-то вспомнил, почему в свое время от них отказался. Представь себе, что тебе нужно вывести список 100 пользователей с подробной информацией о каждом (допустим, 10 полей). Тебе во-первых придется создать коллекцию из 100 объектов Пользователь, а во-вторых сделать тысячу вызовов методов! Я не думаю, что это будет очень быстро =) По крайней мере обход структуры займет в тысячи раз меньше времени. Поэтому я все больше и больше склоняюсь к мнению, что дополнительный слой нужен и он не должен быть связан с доменными классами.
2NetFly:
Касательно объектов и коллекций объектов в представлении - я наконец-то вспомнил, почему в свое время от них отказался. Представь себе, что тебе нужно вывести список 100 пользователей с подробной информацией о каждом (допустим, 10 полей). Тебе во-первых придется создать коллекцию из 100 объектов Пользователь, а во-вторых сделать тысячу вызовов методов! Я не думаю, что это будет очень быстро =) По крайней мере обход структуры займет в тысячи раз меньше времени.
1. Поскольку в большинстве случаев такие коллекции исползуются в скрипте всего один раз и почти всегда это обход коллекции в одну сторону, то не проблема реализовать их, незагружая сразу в массив (типа Lazy Load), а работая с одним объектом из коллекции.
2. такие массивы данных (как ты описал) обычно выводятся только в админках. А там вполне можно пожертвовать производительностью.
3. Всегда есть куча способов оптимизировать скрипт (компиляция шаблонов, кеширование, статика, акселераторы и т.п.) Так что это не проблема.
1. Не совсем понял, что имеется в виду.
2. При разработке mp3 архива подобные запросы мне приходилось выводить довольно часто: список исполнителей на определенную букву, лейблов, дат выхода альбомов и т.д.
3. Я бы сказал, что всегда есть куча способов усложнить программу =) В данном случае статика и прочее - это лишь усложнение, цель которого решить проблемы, которые решаются другими более простыми способами.
И все равно мне кажется, что объектам модели в преставлении не место. Доменными объектами можно (и нужно) манипулировать в классах-обработчиках, а представление (по определению) должно манипулировать данными, а не методами доменных объектов. Процитирую одного из участников дискуссии на форуме phpclub, мнение которого я полностью разделяю: "Собственно, зачем нам необходимо полностью загружать доменные объекты, если всего лишь требуется отобразить некоторую информацию во View?".
2NetFly:
1. Не совсем понял, что имеется в виду.
Обычно список сущностей нужно просто получить из базы и вывести на странице. Поэтому даже если используются коллекции объектов, не надо сразу загружать все данные коллекции.
Задача класса DBObjectList не в том, чтобы сразу загрузить в объект все данные (это можно делать, а можно не делать - по желанию разработчика), а в том, чтобы стандартизировать работу со списками:
$list = new NewsList();
$list->getNewsForCategory($category_id);
if (!$list->empty) {
while ($news = &$list->get()) {
/// работаем с одной новостью
}
}
Насчет представления - не считаю этот вопрос принципиальным.
Я не делаю никаких ограничений на представление - все на совести разработчика.
2NetFly:
манипулировать в классах-обработчиках
вообще про отдельные классы-обработчики я еще подумаю на досуге.
Идея хорошая.
Я раньше использовал в качестве обработчиков методы классов (например, класс News метод add), но потом перешел на отдельные классы-обработчики. Преимуществ довольно много, основное из которых - возможность построения иерархии обработчиков и повторное использование кода. Т.е. создаешь common обработчики для добавления, удаления, модификации, а затем лишь указываешь на нужный доменный объект, с которыми они будут работать.
Приобрел неплохую книгу по паттернам в J2EE. Перевод ужасный, но довольно информативно. Прокомментировал несколько паттернов на блоге. Так же связался с разработчиками LIMB, они придумали очень интересный механизм организации команд в конечный автомат.