Вы не вошли.
А вот про noindex я не согласен. Лента -она пости как карта, в плане - я с удивлением обнаружил отсутствие некоторых вполне полезных вещей с samovarchik.info в гугле. я думаю что если паук приходит на главную, и на ней это фигурировало, пусть и несколько часов/пару дней - то шансов попасть с индекс google этого больше. И тут уж не очень важно перебрасывало это с пагинации на пагинацию или нет... Хотя уже в 2-х колоночном дизайне перебрасывать появляться и исчезать с 1-й страницы будет только тема в "прямом эфире". А в основной ленте. с пагинацией, материалы будут упорядочены по времени создания.
Немного не понял. Не было в гугле совсем, или только на первых страницах? Поиск с параметром site:samovarchik.info их тоже не отображал? И при этом они были в открытых для гостей разделах?
Насколько я понимаю алгоритм работы поисковых роботов: робот заходит на главную, индексирует страницу, видит на ней ссылки, помещает их в очередь для обхода, если какие-то из ссылок есть в списке "только что проиндексированных", они отбрасываются. Он по очереди проходит по всем ссылкам из очереди и повторяет с ними ту же процедуру - индексирует и складывает в очередь найденные ссылки.
Пометка "NOINDEX, FOLLOW" говорит, что "данную конкретную страницу индексировать не нужно, но вот по всем ссылочкам с нее пройдись и проиндексируй их". Таким образом, робот до всех остальных доступных гостю разделов сайта спокойно должен добраться как через главное меню "Мы / 1С / Web / ...", так и по всем страницам (пагинация) главной пройдется и ссылки с них соберет.
На собственном опыте ни один раз натыкался на то, что в поисковике на запрос выдается не сама статья или иная информация, а страница со списком ссылок на статьи/иные информации, разделенным на страницы, и переход из поисковика происходит не на ту страницу, где сейчас ссылка на статью, а на ту, где она когда-то была. При этом не всегда очевидно в какую сторону и как далеко листать. То ли статья потонула и с 5 страницы нужно идти на 9, то ли наоборот всплыла, и сейчас ближе к 1. Пользователь нужную информацию не нашел, закрыл сайт и пошел искать на других.
Поэтому и предлагаю пометку. Возможно есть смысл помещать ее только на страницы в пагинации отличные от самой первой.
С другой стороны, если поисковик будет достаточно часто обновлять свой индекс, чтобы выдаваемые им URL адреса не устаревали, то это не такая уж большая проблема. Хотя лично я бы предпочел чтобы Гугл направлял меня сразу на нужную тему, а не на страницу со ссылкой на нее, будь она viewboard.php или index.php.
В любом случае, если все будут против такого подхода, то не буду его добавлять (это дело одной строчки c <META>-тэгом).
На остальные пункты в том числе в теме о теге [ cut ] отвечу чуть попозже.
Поэтому я и задаю наводящие вопросы, чтобы не слепить совсем не то, что требовалось.
5. с 5-м пунктом я думаю надо быть осторожнее. пагинация же, когда устоится, индексируется... а если её взять и изменить - т.е. ссылки потеряются на страницы на главной... Хотя они в любом случае будут теряться ели переключать режимы главной...
Идея увеличить количество тем на одной странице была связана с мыслью об одноколоночной ленте. Чтобы заходишь на сайт и перед тобой сразу много-много тем, которые видно без необходимости сразу же начинать перелистывать. С другой стороны, если отображать не только заголовки, но и часть статьи, то даже 30 сообщений на страницу могут превратиться в довольно объемную страницу. На Хабре, например, лента ограничена 10 сообщениями на страницу.
Кроме того, я не предлагал постоянно менять количество тем на страницу, а взять изначально некоторый коэффициент и использовать его постоянно. Правда сейчас считаю, что лучше уж тогда в явном виде задавать где-нибудь в настройках это число, а не опираться на настройки viewboard.php, чтобы не было неявных зависимостей.
На счет индексации - темы и так будут прыгать со страницы на страницу, особенно если будут всплывающие. Я бы вообще главную страницу отдавал бы с пометкой "NOINDEX, FOLLOW", как отдается страница поиска. Т.е. все темы, на которые есть ссылки будут проиндексированы и доступны из поисковиков, а конкретные страницы на которых эти ссылки находились N часов назад - не запоминать. Сейчас кажется странным, что viewboard.php не отдается похожим образом, ведь создание одной новой темы заставляет K (=числу страниц доски) тем переместиться со страницы на страницу. Но возможно в этом есть какой-то другой, незаметный мне сейчас смысл.
4. обязательно нужна! это вообще щас для меня очень нужная вещь
Если брать одноколоночную ленту, или двухколоночную, но набор досок для них одинаковый, то в принципе решаемо добавлением нового поля в таблицу досок в базе данных и чекбокса в списке досок в Админка -> Доски.
3. да - хлебные крошки добавить надо. И нет - мало просто списка тем, нужно вовсю пользовать тег [ cut ]
+ во второй ленте - где ответы, выводить надо вырезку из нового ответа а не из первого сообщения темы, ато будет одно и тоже - а ответ то другой...
Хорошо. Нужно будет доработать метод парсера, создающий "вырезки" статей, чтобы он и на сообщения без тэга [ cut ] реагировал более-менее адекватно. Тогда можно будет и "вырезки" ответов создавать, и для тех статей, у которых автор забыл про тег, тоже будет что-нибудь отображаться.
2. Есть 2 сущности - новое и всплывшее. они обе - актуальны (новое - потому что новое, а всплывшее - потому что кому то оно стало актуально, поэтому он задал вопрос и оно всплыло.
я пока не понял надо ли их разделять.
Первый вариант (без разделения) - без разделения: это кто то написал новыую статью - и она попала на главную. Потом, кто то кому то ответил в какой то теме и она попала в ту же ленту на главную.
второй вариант: с разделением. чтобы новые темы (статьи) не путались с мелкими обсуждениями/вопросами. Тут надо основную ленту для новых статей, и дополнительную.. более узкую, чтоли... - для ответов на старые темы - т.е всплывающие вопросы как бы в отдельной ленте, т.к. они менее важны в сравнении с новыми статьями.. или - точнее не менее важны, а они - другая сущность.
И отсюда ответ на твой вопрос №2: если одна лента - то да. как только кто то ответил на какую то тему, она должна всплыть. Но по хорошему должна быть лента "новостей"
- где по дате старта темы, и вторая лента "всплывших", подчиненная - где по ответам - назвать официально надо "Лента обсуждений".и хорошо бы если можно переключать эти 2 режима - типа выбор в админке: одноколоночная главная/двухколоночная главная.
В случае одноколоночной ленты, пока считаю более удобным вариант с "всплытием" тем. Если кто-то что-то спросил в старой теме, она всплыла, ее заметили, ответили. Если больше она ни у кого интереса не вызывает, она утонула обратно. Новые темы тоже сразу же оказываются вверху и потому тоже заметны. Горячо обсуждаемое тоже висит где-то сверху и привлекает внимание.
В случае двух колонок, для меня пока загадкой остается вопрос пагинации и перелистывания страниц. Можно, конечно, синхронно листать сразу обе колонки... но это как-то не очень мне нравится. О динамической подгрузке для одной/каждой из них сейчас речи не идет, т.к. для меня на данный момент это нереально сделать быстро. Можно вывести 2 колонки без пагинации (скажем, по 20 тем/ответов), и к каждой ссылку вида "Все темы" или что-то типа того.
И все-таки это - баг PHP.
Интересно, что самый ранний тикет о нем, который я увидел, создан еще в 2010 году, и был закрыт разработчиками под предлогом "так и должно быть".
В последнем же тикете, после моего комментария активировалась дискуссия между разработчиками и создателем тикета, в результате которой баг вроде бы признали, но сам тикет пока остается закрытым.
Буду следить за развитием событий :)
может он там "тюненный" какой - на хостинге?
Я на своем компьютере это тоже воспроизвожу.
Больше похоже на "особенность" PHP. Позже вернусь к этому вопросу, сегодня и так весь день убил на него (будет обидно, если решение элементарное и лежит на поверхности :) ). Оно не сильно критично на данный момент - что так, что так действие по сохранению параметров в профиле оканчивалось бы фатально, разница только в том, что при выбросе исключения было бы более-менее понятное сообщение об ошибке в браузер, а так пришлось лезть в логи.
Исправил.
Сама по себе проблема была банальной - отсутствие префикса пространства имен перед именем класса. Но это только вершина айсберга. Куда более странным было то, что вместо исключения от автозагрузчика классов мы имели "белую страницу смерти" из-за того, что скрипт завершался фатальной ошибкой. Из документации следует, что для PHP >= 5.3.0 такого быть не должно. Но оно есть!
Простейший код, воспроизводящий ошибку:
<?php
set_exception_handler(function (Exception $ex) {
echo $ex->getMessage().'<br />';
});
spl_autoload_register(function ($className) {
echo $className.'<br/>';
if (!class_exists($className, false))
throw new Exception('Error: '.$className.' does not exist!');
});
Test::foo();
?>
Выведет нам только:
Test
Загвоздка тут именно в том, что мы вызываем статический метод класса.
Если вызов метода заменить на создание класса:
$test = new Test();
то мы, как и ожидалось, увидим:
Test
Error: Test does not exist!
Обращение к константе несуществующего класса даст тот же эффект - фатальную ошибку, а вот обращение к статической переменной выбросит исключение.
Адекватного решения проблемы пока не вижу.
Исправил.
Не отображается информация о копиях и их оригиналах.
История урезана. Отображается только часть истории файла для его нового имени.
Список файлов проекта. Пометки о копировании.
Diff проекта перед коммитом.
История проекта. Список файлов в наборе изменений. Копированные файлы.
Diff между оригинальным файлом и измененной копией.
История изменений одного файла.
История изменений одного файла. Diff.
История изменений с именованными ветками.
Ярлыки с именем ветви не отображаются только у наборов изменений, принадлежащих ветви, выбранной "ветвью по умолчанию" в настройках репозитория.
Если нажать на ярлык, то можно увидеть историю изменений для выбранной ветви.
История изменений ветви с именем "experimental"
История изменений ветви с именем "default"
Кроме того, благодаря интерфейсу для cron, появилось такое понятие, как пул сообщений для рассылок. Т.е. возможность рассылать сообщения не все сразу в момент создания темы или ответа на тему, а порциями по расписанию.
В админке на странице Опции для этого появились 2 настройки - "Использовать пул сообщений" и "Размер пакета сообщений".
(Для того, чтобы пул работал, нужно сначала настроить cron, простого включения галочки в админке недостаточно)
Что изменилось за это время.
RSS/Atom-подписки на категорию и вид контента были добавлены.
Так, например, выглядит URL адрес Atom-подписки на категорию Web на нашем сайте:
http://samovarchik.info/extern.php?action=feed&cid=2&type=atom
А так - подписка на все статьи сайта:
http://samovarchik.info/extern.php?action=feed&kind=1&type=atom
Соответствующие ссылки появились в подвале страницы index.php.
Через <link>-тэги на страницах теперь указываются оба вида подписок - и RSS и Atom -, так что продвинутый пользователь может сам выбрать (через средства, предоставляемые браузером).
Появились email-подписки на категорию.
Для каждой подписки теперь можно указать, присылать ли уведомления на email или нет. Если уведомления отключены, то новости о подписке все-равно можно будет узнать в ленте в профиле (когда она будет сделана).
Также для подписок на доску и на категорию можно указать, чтобы следить не только за новыми темами, но и за ответами в уже существующих темах.
Появилась секция "Подписки" в профиле. Отображаются все подписки, разбитые по типам.
По-прежнему доступна ссылка "показать все подписки", доставшаяся от FluxBB, и показывающая только подписки на темы.
В движке появилась возможность запускать некоторые задания периодически посредством cron, который установлен и доступен на большинстве хостингов.
За это отвечает скрипт protected/cron.php.
Т.к. он находится в защищенной директории, то извне доступ к нему закрыт. Но это не мешает работать с ним изнутри сервера.
Для того, чтобы скрипт начал выполнять задания, нужно добавить его в список работ cron'а (crontab). Как именно - зависит от вашего хостинга. Например, из командной строки через ssh, либо через web-интерфейс панели управления.
Задание должно выглядеть примерно так:
*/5 * * * * php -f /path/to/root/protected/cron.php 2>&1 > /dev/null
Тут мы запускаем cron.php на выполнение каждые 5 минут.
Для того, чтобы добавить новые задания на выполнение, нет необходимости модифицировать сам protected/cron.php. Достаточно просто указать их в файле настроек protected/config/cron.php.
Что для этого нужно:
Создаем новый класс для вашего задания, наследуем его от класса PeComponent, сохраняем в директории protected/. (Либо, если вы уже добавляли свой класс с заданиями, можете воспользоваться им и добавить ему новый метод)
Пишете метод класса, который будет выполнять нужную работу.
Методу не должно передаваться каких-либо переменных, и он не должен ничего возвращать вызывающему коду.
Если периодичность выполнения работы не совпадает с периодичностью запуска cron.php, добавьте соответствующую проверку. Например, можно хранить дату и время предыдущего запуска в файле в директории cache/ или в базе данных, и сравнивать с ней.
Добавьте правила инициализации объекта вашего класса в protected/config/cron.php, как это описано в этой статье
Там же в protected/config/cron.php добавьте в массив Jobs строку вида:
'jobName' => array('component' => 'objectName', 'method' => 'methodName'),
(С запятой в конце строки)
Здесь нужно заменить jobName на название, которое вы присвоите своему заданию, objectName - на название, которое вы назначили инициализированному объекту вашего класса на предыдущем шаге, methodName - на название метода, который должен выполняться.
Все, после сохранения protected/config/cron.php уже при следующем запуске будет выполнена новая задача.
P.S. Про отладку.
Если при работе вашего кода будет вызвано исключение, то сообщение об ошибке и трассировку (debug trace) можно посмотреть в логе в cache/cron_error.log.
Если при работе кода произошла фатальная ошибка, то искать сообщение нужно в логах сервера, и тут все зависит от настроек вашего хостинга (хотя я надеюсь, что на хостинг вы будете выкладывать уже отлаженный код).
Во внутреннем устройстве движка FluxBB.PE довольно заметную роль играет класс Pe.
Основное его предназначение - это инициализация и выдача ресурсов по запросу. Типы ресурсов любые - числа, строки, массивы любой сложности, объекты и прочее. Примеры возвращаемых ресурсов - массив параметров конфигурации сайта, объект базы данных, парсер, шаблонизатор и т.д.
Изначально реализовывая паттерн реестр, он совмещает в себе и другие связанные паттерны и функции.
Данное руководство расскажет с точки зрения программиста, как пользоваться классом Pe и настраивать правила инициализации объектов.
Класс Pe был получен от artoodetoo и используется c некоторыми дополнениями.
Оглавление:
Класс Pe определяется в файле protected/pebase.php
Класс статический, т.е. нет необходимости создавать экземпляры класса, для того, чтобы пользоваться его функционалом.
В момент подключения класса (include/require) автоматически производится первичная инициализация - вызывается метод Pe::init(). В этот момент происходит вычисление пути к корню движка (в дальнейшем это значение можно получить при помощи Pe::get('root')), назначение класса Pe как обработчика автозагрузки классов и обработчика исключений.
Класс защищен от повторной инициализации - все последующие вызовы Pe::init() игнорируются.
В абсолютном большинстве случаев вам не придется заботиться о подключении и инициализации класса Pe. Все скрипты движка в начале своего выполнения подключают файл protected/bootstrap.php, в начале которого происходит подключение protected/pebase.php и дальнейшее конфигурирование класса Pe. (Подробнее о конфигурировании читайте ниже.)
Класс Pe осуществляет автозагрузку классов.
Это значит, что для использования какого-либо класса, вам не нужно его предварительно подключать с помощью include/require.
Если файл с классом лежит в директории protected/ и имя файла совпадает с именем класса, записанным всеми маленькими буквами, то Pe автоматически найдет и подключит такой файл.
Например, файл protected/myclass.php:
<?php
class MyClass { ... }
При выполнении кода:
$mc = new MyClass();
если класс с именем MyClass не существует, PHP обращается к классу Pe, чтобы тот обработал автозагрузку (метод Pe::_handleClass()), при этом ему передается имя класса. Pe преобразует это имя так, чтобы оно содержало только строчные буквы, после чего проверяет существование файла protected/myclass.php. Если такой файл существует, он будет подключен. В противном случае выбрасывается исключение.
При возникновении исключения, управление передается методу Pe::handleException(), который делегирует обработку исключения классу ErrorHandler. Класс ErrorHandler является оберткой над модифицированной функцией error() из FluxBB 1.4, и описание этого класса выходит за рамки статьи.
В реестре класса Pe могут храниться именованные данные различной природы (числа, строки, массивы, объекты).
Для ручного помещения данных в реестр используется метод Pe::set(). Первым параметром передается имя, по которому будут доступны данные, вторым - сами данные. Если данные с указанным именем уже существуют в реестре, они будут замещены новыми.
Пример использования:
Pe::set('digit', 42);
Pe::set('string', 'Hello, World!');
Pe::set('list', array(1, 2, 3, 4, 5));
Pe::set('myclass', new MyClass());
На имена хранимых данных не накладывается каких-либо ограничений, но в большинстве случаев удобнее, если они удовлетворяют требованиям PHP по именованию переменных.
Извлечение данных из реестра выполняется методом Pe::get(), которому параметром передается имя извлекаемого ресурса.
Извлечение данных является более сложным процессом:
Если данные с указанным именем существуют в реестре, они извлекаются и возвращаются методом.
Если данных нет, проверяется набор правил инициализации на предмет наличия там правила для указанного имени ресурса. (О правилах читайте ниже). При наличии правил, данные инициализируются, сохраняются в реест с нужным именем и возвращаются методом.
Если 1 и 2 условия не выполняются, метод возвращает null
Пример использования:
$pun_config = Pe::get('pun_config');
Что происходит в этой строке? Предположим, что ранее Pe::get('pun_config') не вызывалось и Pe::set('pun_config', ...) тоже никто не делал. Тогда, в соответствии с текущими настройками движка, происходит следующее:
Класс Pe не находит в реестре записи с именем 'pun_config' и обращается к списку правил.
В списке правил инициализации находится запись, которая указывает, что 'pun_config' можно получить из кэша с помощью класса CacheLoader и его метода getData. Если быть более точным, то там содержится только имя некоторого ресурса, который тоже должен храниться в реестре, и его метод. К какому классу принадлежит ресурс, какого типа данные вернет его метод и откуда эти данные получены, классу Pe неважно.
Класс Pe ищет в реестре объект с именем 'cacheLoader' (так записано в правилах инициализации). Предположим, что не находит.
Класс Pe ищет правила инициализации для объекта 'cacheLoader'. Здесь-то как раз и указано, что этот объект принадлежит классу CacheLoader, а также указано, какие параметры необходимо передать конструктору класса. Объект создается.
В этот момент, где-то между new и ; срабатывает автозагрузка и подключает php-файл с исходным кодом класса CacheLoader.
Созданный объект сохраняется в реестре под именем 'cacheLoader'. При следующем обращении к нему, уже не придется его создавать.
Вызывается метод getData() которому в качестве параметра передается имя того, что нужно извлечь из кэша. Предположим, что конфигурация уже была ранее помещена в кэш. В противном случае внутри метода getData() был бы вызов Pe::get('cacheGenerator')->generateConfigCache(), который извлекает конфиг из базы данных и сохраняет в кэше. (В качестве упражнения, можете построить последовательность действий, которая при этом произошла бы).
Данные о конфигурации сохраняются в реестр под именем 'pun_config' и при следующем обращении будут взяты уже оттуда.
Полученные данные возвращаются методом get() и сохраняются в переменной $pun_config.
Далее будет описано как настроить Pe на выполнение всего этого.
Рассмотренный ранее метод Pe::set() позволяет загружать только один именованный элемент данных, загружаемые данные при этом должны быть уже инициализированы. Но что если мы хотим загрузить сразу большое количество именованных данных или вообще снять с себя необходимость каждый раз вручную инициализировать некоторые из них?
На этот случай в классе Pe предусмотрено конфигурирование реестра. Оно выполняется методом Pe::config(). В качестве параметра этому методу передается массив следующего вида:
array(
'rules' => array( ... ),
'name1' => %value1%,
'name2' => %value2%,
...
'nameN' => %valueN%
)
Где name1, ..., nameN - это произвольные имена, которые вы сами выбираете для ваших данных. Как уже упоминалось выше, на имена не накладывается каких-либо ограничений, но в большинстве случаев желательно, чтобы они удовлетворяли требованиям PHP по именованию переменных. Если вы не планируете использовать эти данные для инициализации других данных или объектов, то подойдут любые символы, которые можно напечатать на клавиатуре. Но в правилах инициализации можно использовать ссылки на другие элементы реестра только если их имена удовлетворяют ограничениям. %value1%, ..., %valueN% - произвольные значения, которые позже будут помещены в реестр.
'rules' - это особый элемент конфигурационного массива, который задает правила для инициализации данных.
Он подробнее будет рассмотрен ниже.
Здесь же отмечу, что сам этот элемент в реестр не помещается. Т.е. Pe::get('rules') вернет null. Если поместить элемент с таким именем в реестр через Pe::set('rules', ...), то это не добавит новых правил для инициализации, а сами данные, передаваемые в метод set(), будут помещены в реестр в том виде, как вы их передали.
Файлы конфигурации движка хранятся в директории protected/config/
Из себя они представляют обычный PHP-файл, единственным предназначением которого является создание и возврат конфигурационного массива, описанного ранее.
Почему я говорю именно "создания", а не, например, "хранения"?. Дело в том, что ничто не мешает нам конструировать такой массив динамически.
Пример из protected/config/main.php:
// {$start} is set in protected/bootstrap.php
'pun_debug' => defined('PUN_DEBUG') ? array('component' => 'punPage', 'method' => 'getPunDebug', 'start' => '{$start}') : null,
'saved_queries' => defined('PUN_SHOW_QUERIES') ? array('component' => 'punPage', 'method' => 'getSavedQueries') : null,
При необходимости, логика создания конфигурационного массива может быть и более сложной.
Файлы конфигурации движка имеют следующую структуру:
<?php
return array(
'rules' => array(
...
),
...
);
А использовать их можно следующим образом:
Pe::config(include(Pe::get('root') . 'protected/config/main.php'));
На данный момент в движке используются только три файла конфигурации:
protected/config/main.php - основные настройки и правила инициализации для движка. Здесь вы найдете 98% настроек и правил используемых при повседневной работе скриптов.
protected/config/config.php - этот файл создается после установки и содержит те же самые данные, что и config.php в FluxBB ветки 1.4. Например, там хранится пароль от базы данных.
protected/config/install.php - версия конфигурации, используемой во время установки движка. После установки можете смело удалить этот файл, он больше не понадобится.
Еще небольшая часть конфигурации задается методом Pe::set() в protected/bootstrap.php. Например, там можно увидеть такие строчки:
// Define standard date/time formats
Pe::set(
'forum_time_formats',
array($pun_config['o_time_format'], 'H:i:s', 'H:i', 'g:i:s a', 'g:i a')
);
Pe::set(
'forum_date_formats',
array($pun_config['o_date_format'], 'Y-m-d', 'Y-d-m', 'd-m-Y', 'm-d-Y', 'M j Y', 'jS M Y')
);
Правила инициализации в файлах конфигурации сгруппированы по их назначению, каждая такая группа подписана.
DataBase section - работа с базой данных.
User section - пользователь и все что с ним связано (cookies, пометка тем прочитанными).
Cache section - работа с кэшем (генерация кэша редко изменяемых, но часто требуемых данных, получение данных из кэша).
Language section - работа с языковыми файлами.
Template section - шаблонизатор и то, что с ним связано.
Parser - парсер.
File upload - класс FileUpload.
Controllers - тут собраны все классы-контроллеры (см. модель MVC).
Несколько правил, которые не подходят под другие категории.
Группа правил, которые в FluxBB ветки 1.4 определялись через if (!defined(...)) define(...); в include/common.php.
В конфигурационных файлах присутствуют также и просто данные. Их не так много, если сравнивать с количеством строк, занятого правилами инициализации.
Например, там определяются такие константные значения, как виды контента (kind), требуемая ревизия базы данных, виды пользователей (админ, модератор, гость и т.д.) и прочее.
Выше мы рассмотрели конфигурирование реестра, где упоминалось о том, как передать правила инициализации данных. Теперь рассмотрим, что же из себя представляют эти данные.
Элемент 'rules' является массивом, структура которого напоминает структуру самого конфигурирующего массива:
Pe::config(array(
'rules' => array(
'name1' => %value1%,
...
'nameK' => %valueK%
),
...
));
name1, ..., nameK - произвольные имена, которые вы задаете для правил. Впоследствии это будут имена элементов данных в реестре. Т.е. правило для Pe::get('pun_config') должно иметь имя 'pun_config', иначе класс Pe не найдет правило и вернет null. %value1%, ..., %valueK% - либо особым образом параметризованная строка, либо массив определенной структуры.
Пример параметризации строки:
'forum_cache_dir' => '{$root}cache/',
На моем компьютере вызов Pe::get('forum_cache_dir'); вернет "/var/www/flux/pe/cache/".
Параметр задается при помощи открывающей фигурной скобки {, имени переменной (знак $ + имя элемента данных в реестре) и закрывающей фигурной скобки }. Это похоже на то, как можно средствами PHP параметризовать строку в двойных кавычках:
$a = 'Hello';
$b = 'World';
$str = "{$a}, {$b}!"; // "Hello, World!"
В нашем же случае кавычки используются одинарные, чтобы не пришлось экранировать символы с особым значением. И сама интерпретация параметров у нас происходит не в момент когда строка объявляется, а намного позже - при первом использовании строки (перед возвращением из Pe::get() или перед передачей в качестве параметра конструктору класса или методу (подробнее см. ниже)).
Параметров в строке может быть несколько.
Если в строке нет ни одного параметра, она возвращается в неизменном виде.
...
'str' => 'Just a string.',
...
Если присутствует только один параметр и кроме параметра нет больше ни одного символа, будет предпринят поиск в реестре по имени параметра, и вместо строки будет возвращены хранящиеся под этим именем данные в том виде, как они хранятся. Т.е. это вполне может быть объект или массив.
...
'rootDir' => '{$root}',
'db' => '{$db}',
...
Сейчас строки вида 'db' => '{$db}' могут показаться бессмысленными, и действительно, вне контекста такая строчка является даже ошибочной. Такой подход в основном использется при описании параметров, передаваемых конструктору класса.
Другая строчка примера - 'rootDir' => '{$root}' - по сути задает псевдоним для элемента данных с именем 'root'.
Если присутствует несколько параметров, либо если помимо единственного параметра в строке есть еще и текст, по именам параметров будет предпринят поиск в реестре и в строку вставлены соответствующие значения. Все эти значения должны быть представимы в виде строки, в противном случае вы с большой долей вероятности получите не тот результат, что ожидали.
...
'a' => 'Hello',
'b' => 'World',
'str' => '{$a}, {$b}!', // 'Hello, World!'
...
Пример правила для инициализации объекта:
'langLoader' => array('class' => 'LangLoader',
'rootDir' => '{$root}',
'pun_user' => '{$pun_user}'),
Вызов Pe::get('langLoader'); вернет инициализированный объект класса LangLoader.
Наверное массив, стоящий справа от 'langLoader' => показался вам очень знакомым. Действительно, он имеет ту же структуру, что и конфигурационный массив, с той только разницей, что "особым" элементом теперь является 'class', который указывает, какой класс нужно использовать. Все остальные элементы будут переданы конструктору класса параметром-массивом. При этом все параметризованные строки в этом массиве будут обработаны в соответствии с правилами, описанными выше.
Если вы пишете собственный класс с нуля, можете получить доступ к этим параметрам (из примера с LangLoader), например, так:
public function __construct(array $params)
{
echo $params['rootDir'] . "\n";
echo ($params['pun_user']['is_guest'] ? 'Гость' : 'Не гость') . "\n";
}
или так:
public function __construct(array $params)
{
extract($params);
echo $rootDir . "\n";
echo ($pun_user['is_guest'] ? "Гость" : "Не гость") . "\n";
}
Если же вы наследуетесь от PeComponent, то его конструктор автоматически сохраняет переданные ему параметры в качестве полей объекта, с именами, совпадающими с ключами в массиве правила инициализации. Вы можете заранее определить для них уровень доступности (в противном случае, они будут объявлены как public):
class MyClass extends PeComponent
{
protected
$rootDir, $pun_user;
public function printVars()
{
var_dump($this->rootDir, $this->pun_user);
}
}
Если обобщить все выше сказанное, то вызов $ll = Pe::get('langLoader'); равносилен следующей конструкции:
require Pe::get('root') . 'protected/langloader.php'; // Не забыли про автозагрузку классов?
$ll = new LangLoader(array(
'rootDir' => Pe::get('root'),
'pun_user' => Pe::get('pun_user')
));
Pe::set('langLoader', $ll);
Вернемся к примеру с CacheLoader и $pun_config (см. выше в описании Pe::get()), вот строчка из protected/config/main.php:
'pun_config' => array('component' => 'cacheLoader', 'method' => 'getData', 'data' => 'config'),
Ключевыми являются элементы 'component' и 'method'. Первый указывает имя объекта в реестре, второй метод этого объекта. Все остальные элементы передаются в виде массива этому методу. Слово 'component' как бы предполагает, что объект принадлежит классу, унаследованному от PeComponent. И на самом деле, все подобные объекты в движке унаследованы от PeComponent. Но использовать объекты любого другого класса не запрещается. В случае, если элемент реестра не является объектом, или у него нет метода с указанным именем, будет выброшено исключение.
В упрощенном виде вызов $pun_config = Pe::get('pun_config'); можно представить следующим образом:
$component = Pe::get('cacheLoader');
$pun_config = $component->getData(array('data' => 'config'));
Pe::set('pun_config', $pun_config);
В движок планируется включить такую полезную вещь как черновики тем.
Возможно есть смысл ограничить такую возможность только блогами и статьями (нужны ли еще и форумы?). Однозначно не нужны черновики комментариев к файлам и галерей.
Черновики ответов к чему-либо считаю не нужными. Да и в большинстве случаев, за исключением разве что полных ответов, претендующих на то, чтобы быть отдельной статьей, они бессмысленны: либо кто-нибудь уже выскажет схожую мысль, либо пользователь сам забудет про сохраненный черновик, либо ответ станет не актуальным по прошествии времени.
Возникли разночтения в том, как должны быть реализованы черновики. Поэтому ниже опишу все варианты.
Мы нажимаем "создать тему", пишем сообщение, в какой-то момент возникает необходимость прерваться и сохранить изменения, жмем на "сохранить в черновики", и после этого сообщение сохраняется...
Черновики представляют собой отдельную сущность.
Почему мне нравится этот подход:
Черновики сразу исключаются из результатов поиска по сайту
Мы можем в одной строке одной таблицы хранить атрибуты, присущие как теме (таблица topics) так и сообщению (таблица posts), и при этом исключить лишнюю информацию (например, количество просмотров и ответов, последнее сообщение в теме и т.д.)
Т.к. черновик заранее привязан к конкретному пользователю, то проверка есть доступ/нет доступа сводится к проверке id пользователя. И такая проверка необходима лишь в местах, непосредственно работающих с черновиками (profile.php, post.php, edit.php)
Лично мне кажется более естественным подход, когда после публикации черновика, он перестает быть черновиком и является полноценной статьей. Статью позже можно дополнять и исправлять. И прятать ее вновь в черновики мне кажется нелогичным.
Мы создаем отдельный тип контента, который имеет особый путь доступа (как, например, комментарии к файлам).
Предполагается возможность тему сделать вновь черновиком.
Подводные камни:
Нужно позаботиться об исключении всех черновиков (и комментариев к ним) из поиска по сайту.
Необходимо где-то хранить дополнительную информацию, в какую доску переместить черновик при публикации (физически он будет располагаться в доске черновиков).
Что делать со счетчиками сообщений у пользователей успевших ответить на тему до того, как она вернулась в состояние черновика?
Позаботиться о запрете доступа к черновикам другими пользователями.
Предусмотреть средство для админа по работе с черновиками, оставшимися от удаленного пользователя.
Для каждой темы мы добавляем свойство "черновик/не черновик".
Черновики сохраняются также как и обычные темы, только имеют особую пометку.
Предполагается возможность тему сделать вновь черновиком.
Подводные камни:
Нужно позаботиться об исключении всех черновиков (и комментариев к ним) из поиска по сайту.
Учитывать черновики при обновлении счетчиков (количество тем/сообщений доски, количество сообщений пользователя). Кроме того, что делать при переносе темы обратно в черновики - уменьшать количество сообщений у доски и у пользователя(лей)?
Позаботиться о запрете доступа к черновикам другими пользователями.
Предусмотреть средство для админа по работе с черновиками, оставшимися от удаленного пользователя.
Что делать с черновиками в доске, которую по какой-либо причине удаляют? (в двух других случаях черновик физически лежит вне такой доски, а значит пользователь может просто самостоятельно выбрать новую доску в момент публикации)
Все-равно потребуется отдельный инструмент для самого пользователя, чтобы видеть все свои черновики. Кроме того, думаю будет логично, если даже сам пользователь не будет видеть своих же черновиков в досках, куда он их собирался постить
_________________________________
Во всех трех случаях также возникает вопрос, должен ли админ видеть черновики?
Теоретически - нет.
Но если в первом случае с этим не возникает вопросов, то во втором и третьем вариантах такой подход противоречит правилу: админ может видеть все темы на сайте.
Добавил в движок тэг [ hide].
Применение довольно нишевое - скрыть кусок сообщения от всех пользователей, за исключением тех, которые входят в группы, которым разрешено видеть скрытое содержимое (не путать со [ spoiler]).
Использовать, думаю, интуитивно понятно как:
Видимый текст
[hide]
Спрятанный текст
[/hide]
Видимый текст
Админы всегда видят содержимое данного тэга, гости - никогда. Для остальных групп нужно в админке включить переключатель "Видеть скрытый текст", чтобы они тоже могли видеть его (по-умолчанию выключено).
Содержимое тэга не индексируется, т.е. его нельзя найти через поиск, встроенный в движок (внешним поиском - тем более).
Кроме того, оно вырезается из сообщения при его цитировании.
Вы долго пользовались движком и в один прекрасный день решили, что вам недостаточно функциональности встроенных в движок BBCode-тэгов?
Если вы решили самостоятельно расширить функционал парсера, эта статья поможет вам разобраться, как это сделать.
Парсер используется почти в чистом виде такой же, как в FluxBB ветки 1.4. Он лишь превращен из набора функций в класс и немного адаптирован под наш движок. Поэтому если вы хорошо знакомы с парсером FluxBB, вы не почувствуете разницы.
При написании данной статьи предполагалось, что пользователь знает HTML, CSS и хотя бы основы регулярных выражений. Еще лучше, если пользователь немного знаком с PHP.
Для добавления нового BBCode-тэга, нужно совершить несколько шагов:
Определиться с синтаксисом самого тэга: будет ли это пара [ tag]...[/ tag] или одиночный тэг, можно ли ему будет передать параметр [ tag=something]...[/ tag] или нет, и что этот параметр означает.
Определиться, как будет выглядеть результирующая HTML-разметка.
Здесь трудно дать какой-то конкретный совет.
Помните, что если вы делаете что-то блочное, то в самом начале вашей разметки закройте параграф текста, а в конце - откройте: </p><div class="newfeature">....</div><p>. Так устроен парсер, что в любой момент обработки BBCode тэгов, вы всегда находитесь внутри некоторого параграфа <p>, который был открыт ранее.
Для внутристрочных тэгов (типа [ b] или [ i]) скорее всего разметка будет на основе HTML-тэга <span>
Можно ли ваш тэг использовать внутри подписи пользователя? От этого будет зависеть дальнейшая его настройка в парсере
Заставляем парсер преобразовывать ваши тэги в HTML-разметку.
Для этого находим класс Parser (protected/parser.php), в нем метод _doBBcode(). Этот метод формирует пары шаблон/замена и в конце при помощи метода preg_replace() выполняет преобразование текста сообщения в HTML-разметку.
Шаблоны добавляются в массив $pattern, разметка на замену в массив $replace. Очень важен порядок, в котором в эти массивы помещаются элементы. Чтобы не запутаться, проще всего просто поместить эти строки рядом:
$pattern[] = '...';
$replace[] = '...';
Если вы не хотите, чтобы ваш BBCode-тэг можно было использовать в подписях пользователей, заключите формирование пары шаблон/замена в условие:
if (!$is_signature)
{
$pattern[] = '...';
$replace[] = '...';
}
Для описания шаблона используется синтаксис регулярных выражений PCRE (Perl Compatible Regular Expressions). Описание регулярных выражений выходит за рамки данной статьи, приведу лишь пару примеров:
$pattern[] = '#\[b\](.*?)\[/b\]#ms';
$pattern[] = '#\[url\]([^\[]*?)\[/url\]#e';
$pattern[] = '#\[colou?r=([a-zA-Z]{3,20}|\#[0-9a-fA-F]{6}|\#[0-9a-fA-F]{3})](.*?)\[/colou?r\]#ms';
Если регулярное выражение получается сложным и медленным, или используется несколько вариантов для одного и того же тэга, иногда может оказаться эффективнее сначала убедиться что такой тэг присутствует в тексте сообщения, выполнив обычный текстовый поиск:
if (strpos($text, '[mytag') !== false)
{
$pattern[] = '...';
$replace[] = '...';
}
где "mytag" заменяем на название вашего тэга.
Более-менее достоверно сказать, что такой предварительный поиск действительно увеличит производительность, можно только проведя сравнительное тестирование. (Да, это значит спарсить обоими способами несколько тысяч тестовых сообщений, как содержащих, так и не содержащих внутри себя ваш тэг, и сравнить среднее время обработки таких сообщений. Думаю для этого без навыков PHP не обойтись.) Как именно тестировать - выходит за рамки данной статьи, по сути это все эвристика.
То, на что мы должны заменить текст, при совпадении с шаблоном, является специально сформированной строкой. Подробности можно прочитать здесь. Приведу примеры:
$pattern[] = '#\[b\](.*?)\[/b\]#ms';
$replace[] = '<strong>$1</strong>';
$pattern[] = '#\[search\]([^\[]*?)\[/search\]#e';
$replace[] = '$this->_handleSearchTag(\'$1\')';
$pattern[] = '#\[colou?r=([a-zA-Z]{3,20}|\#[0-9a-fA-F]{6}|\#[0-9a-fA-F]{3})](.*?)\[/colou?r\]#ms';
$replace[] = '<span style="color: $1">$2</span>';
В некоторых сложных случаях, требующих дополнительной обработки или проверки каких-либо условий, рационально будет создать метод, обабатывающий ваш BBCode-тэг и возвращающий сформированную разметку в виде строки (пример сильно упрощен):
private function _handleMytagTag($text)
{
return '</p><div class="newfeature">' . $text . '</div><p>';
}
Такие методы делаются скрытыми (private), т.к. они являются внутренними для парсера и нигде вне его не используются. Имя обычно начинается с "_handle", затем идет название вашего тэга, и завершается "Tag". Количество и имена параметров вы выбираете сами, в зависимости от ваших потребностей. Обычно их нужно не более 2.
Для того, чтобы ваш метод использовался для формирования HTML-разметки, необходимо в конце шаблона указать модификатор "е", а вместо разметки для замены, указать имя функции (как в примере выше, для тэга [ search]).
Настраеваем препарсер.
Это нужно для того, чтобы парсер мог оценить правильность использования вашего тэга в сообщении.
Находим в классе Parser метод preparseBBcode().
Если ваш тэг нельзя использовать в подписи пользователя, добавьте его имя в разделенный вертикальной чертой | список тэгов в условии if (preg_match(...)), которое находится внутри условия if ($is_signature) в самом начале метода.
Находим метод _preparseTags().
Для начала помещаем имя вашего BBCode-тэга в массив $tags - список всех тэгов, обрабатываемых парсером.
Затем, помещая или наоборот не помещая в другие массивы имени вашего тэга, настраиваем другие параметры:
$tags_selfclosed - тэги, которые не имеют закрывающего тэга. Например [ thumb] или [ cut]
$tags_nested - тэги, которые могут содержаться внутри себя. Формат массива: array('tag' => $val), где 'tag' - это имя тэга, а вместо $val может быть любое вырадение, формирующее число, и это число означает допустимый уровень вложенности тэга внутрь самого себя. Если вложить тэг большее число раз, препарсер выдаст пользователю сообщение об ошибке.
$tags_ignore - тэги, чье содержимое полностью игнорируется препарсером. На данный момент это только [ code]
$tags_block - блоковые тэги. Это как раз те, о которых я выше говорил, чтобы вы не забыли закрыть параграф </p> до них, и открыть новый после.
$tags_inline - не смотря на название, это не обязательно внутристрочные (inline) тэги, это скорее тэги, которые не допускают внутри себя символов перевода строки. Препарсер разбивает их следующим образом:
[b]Первая строка.
Вторая строка.[/b]
превращается в:
[b]Первая строка.[/b]
[b]Вторая строка.[/b]
$tags_trim - в таких тэгах пробельные символы (пробелы, табуляция, переводы строки) между обычным текстом и BBCode тэгами удаляются. Нужно помнить, что такие символы удаляются не только после открывающего тэга и перед закрывающим, но и перед и после любого другого BBCode-тэга внутри вашего.
$tags_quotes - тэги, из аргументов которых мы удаляем кавычки. Т.е. тэг вида:
[tag="something"]...[/tag]
Превращается в:
[tag=something]...[/tag]
$tags_limit_bbcode - все тэги, не включенные в этот список, могут содержать внутри себя любые другие ббкоды. Если мы включаем сюда какой-либо тэг, то мы указываем для него белый лист из других тэгов, разрешая только этим тэгам содержаться внутри него. Формат: 'mytag' => array('tag1', 'tag2', ...).
$tags_fix - для данных тэгов мы позволяем препарсеру самостоятельно исправить порядок открывающих и закрывающих тэгов, если пользователем они указаны неверно. Например:
[b][i]something[/b][/i]
превращается в
[b][i]something[/i][/b]
Обработка пустых тэгов.
Обычно BBCode-тэги без содержимого оставляются пользователем случайно, и на самом деле не нужны.
Если ваш тэг не самозакрывающийся (т.е. имеет помимо открывающего также закрывающий тэг), найдите метод _stripEmptyBBcode(), в нем первый из циклов while(). Если ваш тэг бесполезен пустым не зависимо от наличия у него параметра, поместите его в первый из preg_replace() внутри while. Если при наличии параметра, тэг остается функциональным даже при отсутствии внутри него текста, поместите его во второй preg_replace() внутри while.
Например, [ url=http://example.org][/ url] обрабатывается также как и [ url]http://example.org[/ url], и поэтому помещен во второй preg_replace(). В то время как [ color=#a00][/ color] бесполезен, и поэтому помещен в первый preg_replace().
Формирование поискового индекса.
Для того, чтобы новое сообщение могло быть найдено средствами встроенного в движок поиска, это сообщение должно быть проиндексировано. Перед индексацией из сообщения удаляются все тэги BBCode и остается только полезная информация. Для этого код, занимающийся индексированием должен знать о существовании вашего тэга и о том, как его корректно удалить.
Находим класс SearchIndex (protected/searchindex.php), в нем метод splitWords(). Добавляем имя вашего тэга в самый первый preg_replace() в список тэгов, разделенных вертикальной чертой |.
В большинстве случаев этого достаточно. Но иногда к тэгу требуется особый подход. Например, может потребоваться оставить для индексации параметр тэга. В таком случае находим в этом же классе метод _stripBBcode() и добавляем в массив $patterns шаблон поиска (регулярное выражение) и строку замены. Пример:
$patterns = array(
'%\[img=([^\]]*+)\]([^[]*+)\[/img\]%' => '$2 $1', // Оставить URL и описание
'%\[anchor\]([a-zA-Z_0-9\-/\.:~]++)\[/anchor\]%' => ' ', // Удалить ID якоря
);
Проверить, что все работает, как вы планировали.
[ Сгенерировано за 0.027 сек, 9 запросов выполнено - Использовано памяти: 2.31 MiB (Пик: 2.41 MiB) ]