Показаны сообщения с ярлыком Php. Показать все сообщения
Показаны сообщения с ярлыком Php. Показать все сообщения

6 октября 2009 г.

Wordpress, "Read the rest of this entry"

Если в тексте поста в Wordpress есть специальный тэг <!--more-->, то при выводе списка постов Wordpress выводит только текст до этого тэга, а сам тэг заменяет на ссылку на пост, с текстом Read the rest of this entry. Эта ссылка содержит указатель на якорь, т.е. на то место, где находится тэг more. Зачем-то понадобилось убрать из этой ссылки указание на этот якорь. Т.е. было <a href="/blog/2009/10/my-blog-post/#more-123">My Blog Post</a>, надо <a href="/blog/2009/10/my-blog-post/">My Blog Post</a>. Ну, надо - так надо.

Содержимое поста, и ссылка Read the rest (если нужно) выводится функцией the_content. Включить/выключить добавление якоря в ссылку нельзя через настройки. Поэтому остаётся единственный вариант - получить строку контента и удалить якорь.

Функция the_content сразу выводит содержимое. Но есть аналогичная функция get_the_content, которая возвращает контент, а не выводит. Проблема в том, что результат этой функции не содержит переносов строк, т.е. текст идёт сплошным потоком. Я подсмотрел как работает the_content - она применяет фильтр the_content перед выводом. Т.е. нам надо сделать тоже самое.

Итого, получаем код, который удаляет из ссылки якорь:
<?php $content = get_the_content('Read the rest of this entry &raquo;'); ?>
<?php $content = preg_replace('/\/#more-\d+/', '/', $content); ?>
<?php $content = apply_filters('the_content', $content); ?>
<?php print $content;  ?>

2 октября 2009 г.

Баланс в телефоне

Решил реализовать такую интересную и простую задачку - следить за балансом на телефоне, что бы был красивый график. Я использую БВК, у них есть ИССА - Интернет Служба Сервиса Абонента, через которую можно узнать состояние своего счёта.

Задача разбивается на две подзадачи - посмотреть состояние счёта в интернете, сохраняя эти данные куда-нибудь; построить график на основе этих данных.

Получение состояния счёта

Я написал небольшой скрипт на питоне, который логинится в ИССА и узнаёт состояние счёта. Далее он записывает эти данные, вместе с текущей датой/временем, в текстовый файл. Для работы с интернетом используется mechanize, этот веб-клиент, написанный на питоне, поддерживает куки, что необходимо для логина.

mechanize не входит в стандартную поставку питона, его нужно установить:
easy_install.py mechanize

Поиск баланса ведётся с помощью регулярных выражений по строке "Баланс вашего лицевого счёта равен xx руб.".
# -*- coding:utf-8 -*-

from mechanize import Browser
from time import strftime
import re
import os, sys

LOGIN_URL = 'http://issa.bwc.ru/'
ACCOUNT_URL = 'http://issa.bwc.ru/cgi-bin/cgi.exe?function=is_account'
DATA_FILE = os.path.dirname(sys.argv[0]) + '/history.txt'
PHONE_NUMBER = <phone number>
PASSWORD = <issa password>

browser = Browser()

browser.open(LOGIN_URL)
browser.select_form(name = 'num')
browser['mobnum'] = PHONE_NUMBER
browser['Password'] = PASSWORD
browser.submit()
r = browser.open(ACCOUNT_URL)

page = r.read().decode('cp1251')

match = re.search(ur'Баланс вашего лицевого счета равен (?P<money1>\d+).(?P<money2>\d)', page, re.IGNORECASE)
if match:
    money = int(match.group('money1')) + float(match.group('money2')) / 10 
    f = open(DATA_FILE, 'a')
    f.write('%s, %.1f\n' % (strftime('%d.%m.%Y %H:%M'), + money))
    f.close()
    
    print('Phone balance: %.1f rub' % money)
else:
    print page
Я добавил задание в стандартный планировщик Windows, который запускает этот скрипт каждые два часа.

График

Для рисования графика я взял библиотеку от Google google.visualization.AnnotatedTimeLine. Ей нужно передать данные, она сама нарисует красивый график, используя Flash. Для разбора файла-лога с балансами я написал скрипт на PHP. Страница index.php, которая ответственна за вывод графика, имеет такой вид:

<?php
    define('HISTORY_FILE', 'd:\projects\python\issa.bwc\history.txt');

    $history = array();
    $hist = explode("\n", trim(file_get_contents(HISTORY_FILE)));
    foreach ($hist as $_h) {
        $tmp = split('[. ,:]', $_h);

        if (!$tmp[0])
            continue;

        $history[] = array('day' => $tmp[0],
                           'month' => $tmp[1],
                           'year' => $tmp[2],
                           'hours' => $tmp[3],
                           'minutes' => $tmp[4],
                           'value' => intval($tmp[6]) + intval($tmp[7]) / 10);
    }
?>

<script type='text/javascript' src='http://www.google.com/jsapi'></script>
<script type='text/javascript'>
  google.load('visualization', '1', {'packages':['annotatedtimeline']});
  google.setOnLoadCallback(drawChart);
  function drawChart() {
    var data = new google.visualization.DataTable();
    data.addColumn('datetime', 'Date');
    data.addColumn('number', 'Phone balance');
    data.addRows([
    <?php foreach ($history as $_h) { ?>
      [new Date(<?php print $_h['year']; ?>, <?php print $_h['month']; ?>, <?php print $_h['day']; ?>, <?php print $_h['hours']; ?>, <?php print $_h['minutes']; ?>), <?php print $_h['value']; ?>]<?php if ($_h != end($history)) { ?>,<?php } ?>

    <?php } ?>
    ]);

    var chart = new google.visualization.AnnotatedTimeLine(document.getElementById('chart_div'));
    chart.draw(data, {displayAnnotations: true});
  }
</script>

<body>
    <div id='chart_div' style='width: 100%; height: 100%;'></div>
</body>

В результате работы скрипта получаем такую картинку. Этот скрипт работает всего пол дня, поэтому изменения совсем незаметные :)

Заключение

Практическая ценность такой игрушки весьма сомнительна :) Потом, когда у меня появится свой VDS :) это нужно будет переместить туда.

18 сентября 2009 г.

Интеграция Wordpress в Magento - ЧПУ, часть 2

После создания модуля, генерирующего реврайты для вордпресовских адресов, я подумал и решил, что такая идея не будет работать на 100%. Вот почему:
  • Кроме адресов постов, категорий, тэгов, и т.д., т.е. тех адресов, для которых мы делаем реврайты, есть другие адреса, такие, как action формы, отправляющей комментарий, адрес редактирования поста, адрес подписки на комментарии каждого поста. И это только те адреса, которые я заметил, при том все эти адреса ЧПУ, т.е. они просто не будут работать в Magento без реврайтов.
  • Некоторые страницы будут иметь так много постов, что они не будут влезать на одну страницу. Т.е. адреса "следующая страница с постами" не будут работать.
К счастью, я нашёл решение этих проблем, что не позволит выкинуть результат предыдущей работы с автосозданием реврайтов для блога :)

Тепер подробнее.

Проблема с ЧПУ

Сначала рассмотрим проблему со множеством ЧПУ.

Источник проблемы в том, что в настройках Wordpress указана ЧПУ схема адресов. Т.е. абсолютно все адреса, даже для которых мы не писали реврайты, будут ЧПУ. А зная, что такие адреса в Magento работать не будут, получается, что блог будет выдавать "страница не найдена" в неожиданных местах :)

Отсюда следует, что если поставить не-ЧПУ схему, все адреса станут не-ЧПУ, и начнут работать. Это и надо сделать.

Но мы же делали реврайты, т.е. некоторые адреса могут быть ЧПУ. Поэтому мы будем модифицировать шаблоны, выводящие адреса, меняя некоторые из них на ЧПУ.

Для этого я поступил не совсем красиво - вылез в файл index.php, что находится в самом корне Magento. Думаю, есть решение по-красивее, но я его не знаю :)

В этот файл я добавил следующий код (после включения хедеров вордпресса):
...
require_once $mageFilename;

define('WP_USE_THEMES', true);
require('wordpress/wp-blog-header.php');

// bof Blogrewrite module code
$oldPermalink = get_option('permalink_structure');

function ChangePermalink() {
    global $wp_rewrite, $oldPermalink;
    $wp_rewrite->set_permalink_structure("/%year%/%monthnum%/%postname%/");
}

function RestorePermalink() {
    global $wp_rewrite, $oldPermalink;
    $wp_rewrite->set_permalink_structure($oldPermalink);
}
// eof Blogrewrite module code

#Varien_Profiler::enable();
...
Т.е. сначала я запоминаю текущую схему адресов. Вообще-то это достаточно бесполезное занятие т.к. схема должна быть только "по-умолчанию", т.е. $oldPermalink == ''. Т.е. вместо запоминания текущей схемы можно использовать пустую строку. Сделал это, наверное, на будущее...

Затем я добавил две функции. Одна меняет схему адресов на ЧПУ (что автоматически меняет схемы адресов категорий, тэгов и т.д.), другая восставнавливает предыдущую схему (не-ЧПУ, пустая строка).

По вкусу можно добавить вызов RestorePermalink(); в конец файла :) Наверное так будет лучше.

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

После того, как мы поработаем с ЧПУ, вызываем RestorePermalink() - восстанавливаем изменённую схему адресов.

Итак, файлы и код, которые используют эти функции:
  • Mage_Blogrewrite_Adminhtml_BlogrewriteController::makerewritesAction()

    Где-нибудь вначале метода нужно вызвать ChangePermalink, где-нибудь в конце - RestorePermalink.
  • Шаблон блока со списком постов/категорий/и т.д. находится в файле app/design/frontend/default/sunnyD/template/blog/menu.phtml. Т.к. мы создали реврайты для всех ссылок, которые выводятся в этом файле, то вызываем ChangePermalink в начале файла, RestorePermalink - в конце.
  • wordpress/wp-content/themes/Wordpress-theme/magento/magento.php

    Это файл-шаблон, используемый для отображения "контента", содержимого блога - списка постов, отдельного поста. Выводится по-середине страницы :)

    Те адреса, для которых мы писали реврайты, мы выводим с включёнными ЧПУ. Остальные адреса - с выключенными ЧПУ.

    Например, мы знаем, что ссылка на пост может быть ЧПУ. Поэтому код может выглядеть так:
    <?php wp_reset_query(); ?>
    
    <?php if (have_posts()) : ?>
    
      <?php while (have_posts()) : the_post(); ?>
    
       <div class="post" id="post-<?php the_ID(); ?>">
                    <?php ChangePermalink(); ?>
        <h2><a href="<?php the_permalink() ?>" rel="bookmark" title="Permanent Link to <?php the_title_attribute(); ?>"><?php the_title(); ?></a></h2>
                    <?php RestorePermalink(); ?>
    ...

Pagination

Ремонтирование разбиения на страницы оказалось проще. Когда мы получаем список категорий, постов, тэгов, месячных архивов, мы можем узнать количество постов, удовлетворяющих выбранному критерию. Ещё мы можем узнать выводимое количество постов на странице:
$postsPerPage = get_option('posts_per_page');
А, зная это, мы легко можем подсчитать количество страниц, необходимое для отображения всех постов, удовлетворяющих выбранному критерию, и добавить соответствующих реврайтов:
$pagesCount = ceil($totalPostsCount / $postsPerPage);
Вот как был модифицирован код:
  • Для категорий:
    // Update rewrites for categories
    $cats = get_categories();
    foreach ($cats as $_c) {
        $this->_makeRewrite('blog_cat/' . $_c->term_id,
            trim(substr(get_category_link($_c->term_id), strlen($baseUrl)), '/'),
            'blog/index/index/cat/' . $_c->term_id);
    
        // Also add rewrites for other pages, e.g. page/2, page/3 etc
        for ($i = 2; $i <= ceil($_c->count / $postsPerPage); $i++) {
            $pages++;
            $this->_makeRewrite('blog_cat/' . $_c->term_id . '/page/' . $i,
                trim(substr(get_category_link($_c->term_id), strlen($baseUrl)), '/') . "/page/$i",
                'blog/index/index/cat/' . $_c->term_id . '/page/' . $i);
        }
    }
  • Для тэгов:
    // Update rewrites for tags
    $tags = get_tags();
    foreach ($tags as $_t) {
        $this->_makeRewrite('blog_tag/' . $_t->term_id,
            trim(substr(get_tag_link($_t->term_id), strlen($baseUrl)), '/'),
            'blog/index/index/tag/' . $_t->term_id);
    
        // Also add rewrites for other pages, e.g. page/2, page/3 etc
        for ($i = 2; $i <= ceil($_t->count / $postsPerPage); $i++) {
            $pages++;
            $this->_makeRewrite('blog_tag/' . $_t->term_id . '/page/' . $i,
                trim(substr(get_category_link($_t->term_id), strlen($baseUrl)), '/') . "/page/$i",
                'blog/index/index/tag/' . $_t->term_id . '/page/' . $i);
        }
    }
  • Для месячных архивов:
    $query = "SELECT DISTINCT YEAR(post_date) AS `year`, MONTH(post_date) AS `month`, count(ID) as posts FROM $wpdb->posts $join $where GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC $limit";
    $archive = $wpdb->get_results($query);
    if ($archive) {
        $afterafter = $after;
        foreach ((array) $archive as $_a) {
            $id = sprintf("%4d%02d", $_a->year, $_a->month);
    
            $this->_makeRewrite('blog_month/' . $id,
                trim(substr(get_month_link($_a->year, $_a->month), strlen($baseUrl)), '/'),
                'blog/index/index/m/' . $id);
    
            // Also add rewrites for other pages, e.g. page/2, page/3 etc
            for ($i = 2; $i <= ceil($_a->posts / $postsPerPage); $i++) {
                $pages++;
                $this->_makeRewrite('blog_month/' . $id . '/page/' . $i,
                    trim(substr(get_month_link($_a->year, $_a->month), strlen($baseUrl)), '/') . "/page/$i",
                    'blog/index/index/m/' . $id . '/page/' . $i);
            }
        }
    }

Итог

Итак:
  • Выставляем схему адресов в Wordpress "по-умолчанию". Это гарантирует нам, что любые ссылки, сгенерированне вордпрессом будут работать сами по себе.
  • Во время генерации реврайтов для постов/категорий/тэгов/месячных архивов устанавливаем схему адресов в ЧПУ. Генерируем реврайты, не забываем про "многостраничные страницы". Благодаря реврайтам пользователь сможет зайти на блог по ЧПУ (это относится к постам, категориям, тэгам, месячным архивам).
  • Модифицируем шаблоны, генерирующие вордпресовские адреса, включая ЧПУ схему для тех элементов, для которых мы создали реврайт на предыдущем шаге, выключая ЧПУ для тех элементов, для которых реврайтов нет
В итоге в основном пользователь будет видеть ЧПУ. Остальные адреса, для которых мы не сделали реврайтов, будут не-ЧПУ. Т.е. теперь блог будет 100% рабочий, разве что не 100% ЧПУ.

PS. Ну и недостаток, который есть у данной схемы: смена структуры адресов не самая быстрая операция. При этом она может выполняться несколько десятков раз (при отображении 20 постов на одной странице смен будет 20 * 3 = 60). Ну, Magento сама по себе весьма не скоростная, будем надеяться эти новые смены никто не заметит :)

PPS. Ещё я добавил загрузку реврайта по Request Path, если реврайт не найден по Id Path. Это сделано для того, что бы код без проблем заработал в магазине, где уже есть некоторые реврайты, добавленные вручную.
protected function _makeRewrite($idPath, $requestPath, $targetPath, $permamentRedirect = false) {
    $this->_urlRewrite->loadByIdPath($idPath);
    if (!$this->_urlRewrite->getId())
        $this->_urlRewrite->loadByRequestPath($requestPath);
    ...
}
Код попытается найти реврайт по Id Path, скорее всего не найдёт (Id Path заполнялся вручную и не совпадает с генерируемым нашим модулем). Если затем не попробовать загрузить реврайт по Request Path, Magento упадёт при попытке создать новый реврайт с тем же Request Path, который уже есть в системе (Request Path однозначно идентифицирует реврайт, это уникальное поле). Добавляя дополнительную загрузку по Request Path мы уберегаем себя от подобных падений - вместо падения Magento загрузит уже существующий, добавленный вручную реврайт, и обновит его.

15 сентября 2009 г.

Интеграция Wordpress в Magento - ЧПУ

Уже давно у нас в в Magento был внедрён Wordpress, специальным модулем-плагином для Magento. Но, используя такой метод внедрения, есть проблема с адресами в Wordpress - можно использовать только простые адреса, вида ?p=<номер поста>, ?cat=<категория> и т.д. Это связано с тем, что обработку всех адресов берёт на себя Magento, т.е. все адреса должны быть в специальном формате - <module name>/<controller name>/<controller action>[/<param1>/<value1>/<param2>/<value2>...]. Если же в Wordpress включить ЧПУ адреса, например, вида <year>/<month>/<post title>, такой адрес придёт в Magento, которая попытается найти модуль <year>, в нём найти контроллер <month>, и выполнить метод <post title>. Естественно ничего такого в Magento нет, поэтому будет ошибка "страница не найдена".

Тем не менее, в Magento можно использовать ЧПУ. Для этого надо писать реврайты. Magento сама создаёт по реврайту на каждый продукт и категорию. Rewrite говорит, какой произвольный адрес перенаправить на какой корректный "Magento-адрес". Например, адрес /benq-2220hd нужно заменить адресом /catalog/product/view/id/496.

Реврайты бывают "внтуренние" и "с редиректом". Внутренние работают внутри :) Т.е. пользователь ввёл один адрес, он видит его в адресной строке, а Magento внутри себя вызывает другой адрес для обработки (более точно - другое действие другого контроллера). Реврайт с редиректом наоборот сразу бросается в глаза - браузер пользователя перенаправляется с одного адреса на другой.

Но даже с реврайтами есть проблема. Модуль, использующийся для интеграции Wordpress в Magento, не позволял использовать ЧПУ. Я его немного переделал, и теперь стало возможным написать адрес вида blog/index/index/param1/value1/param2/value2, и эти параметры будут переданы Wordpress, который на их основе выполнит запрос. Т.е. раньше приходилось писать реврайты с редиректом на не-ЧПУ адрес (например, на /blog/?p=123), сейчас же можно писать "внутренние" реврайты. А подсмотреть, какие нужно передавать параметры, можно в документации на Wordpress. Т.е., если перейти по адресу /blog/index/index/p/123, Wordpress покажет пост с id=123. Кроме p есть другие параметры, cat - для категорий, tag - для тэгов, m - для архивов, и т.д.

Сначала мы писали реврайты для постов вручную. Но это очень неудобно, медленно, не расширяемо и т.д., и я подумал что это дело можно автоматизировать. Сейчас будем этим заниматься :)

Мы напишем модуль, который сам будет создавать реврайты на все посты/категории/тэги/архивы блога/rss. Делать это будет специальная волшебная кнопка "пыщь" в админке :)

Создание модуля

Создадим модуль Blogrewrite в пространстве имён Mage. Что бы сделать свою страницу в админке нужно иметь такой config.xml:
<?xml version="1.0"?>
<config>
    <global>
        <helpers>
            <Blogrewrite>
                <class>Mage_Blogrewrite_Helper</class>
            </Blogrewrite>
        </helpers>
    </global>

    <admin>
        <routers>
            <Blogrewrite>
                <use>admin</use>
                <args>
                    <module>Mage_Blogrewrite</module>
                    <frontName>Blogrewrite</frontName>
                </args>
            </Blogrewrite>
        </routers>
    </admin>

    <adminhtml>
        <menu>
            <catalog module="catalog">

            <children>
                <Blogrewrite translate="title" module="Blogrewrite">
                    <title>Blog Rewrite</title>
                    <action>Blogrewrite/adminhtml_Blogrewrite</action>
                </Blogrewrite>
            </children>

            </catalog>
        </menu>
    </adminhtml>
</config>
В секции admin мы говорим, что наш модуль будет доступен по адресу Blogrewrite (www.example.com/Blogrewrite). Т.о. контроллер нашего модуля - Mage_Blogrewrite_Adminhtml_BlogrewriteController.

В секции adminhtml мы добавляем новый пункт меню, в меню Catalog, и говорим, какое действие нужно выполнить, когда пользователь нажмёт на этом пункте меню - модуль Blogrewrite, контроллер Mage_Blogrewrite_Adminhtml_BlogrewriteController, действие по-умолчанию - index.

Действие index всего лишь отображает блок Mage_Blogrewrite_Block_Adminhtml_Blogrewrite:
public function indexAction() {
    $this->_initAction();
    $this->getLayout()->getBlock('head')
         ->setCanLoadRulesJs(true);
    $this->_addContent($this->getLayout()->createBlock('Blogrewrite/adminhtml_Blogrewrite')
         ->setCanLoadRulesJs(true));
    $this->renderLayout();
}
Этот блок находится в файле Block/Adminhtml/Blogrewrite.php:
<?php
class Mage_Blogrewrite_Block_Adminhtml_Blogrewrite extends Mage_Adminhtml_Block_Template {
    public function __construct() {
        parent::__construct();
        $this->setTemplate('Blogrewrite/index.phtml');
    }
}
Блок всего лишь выводит шаблонный файл index.phtml, который должен быть в app/design/adminhtml/default/default/template/Blogrewrite/index.phtml. В шаблоне - одна кнопка, которая вызывает действие makerewrites нашего контроллера:
<div class="content-header">
    <table cellspacing="0">
        <tr>
            <td>
                <h3 class="head-dashboard"><?php echo $this->__('Blog Rewrite') ?></h3>
            </td>
        </tr>
    </table>
</div>

<form action="<?php print $this->getUrl('*/*/makerewrites'); ?>">
    <button type="submit">Make Blog Rewrites</button>
</form>
Итак, модуль готов, он виден в админке, форма с кнопкой отображается. Надо делать собственно добавление реврайтов

Создаём rewrites

Как создать реврайт? Для этого есть модель core/url_rewrite, мы можем получить её так:
Mage::getModel('core/url_rewrite');
Можно загрузить данные из базы по некоторым параметрам, нам вполне хватит загрузки по Id Path:
Mage::getModel('core/url_rewrite')->loadByIdPath($idPath);
Если запись с таким Id Path есть в базе, она загрузится в модель, иначе нет :)

После того как мы загрузили данные из базы (или если данных нет), нужно задать параметры реврайта:
Mage::getModel('core/url_rewrite')
    ->loadByIdPath($idPath)
    ->setIdPath($idPath)
    ->setRequestPath($requestPath)
    ->setTargetPath($targetPath)
    ->setDescription('Automagically generated, Blogrewrite module')
    ->setIsSystem(0);
И, наконец, сохраняем реврайт:
->save();
Для удобства вынесем создание/обновление реврайта в отдельную функцию:
protected function _makeRewrite($idPath, $requestPath, $targetPath, $permamentRedirect = false) {
    $this->_urlRewrite->loadByIdPath($idPath);
    $this->_urlRewrite->setIdPath($idPath)
          ->setRequestPath($requestPath)
          ->setTargetPath($targetPath)
          ->setOptions($permamentRedirect ? 'RP' : '')
          ->setDescription('Automagically generated, Blogrewrite module')
          ->setIsSystem(0);
    $this->_urlRewrite->save();
}
Ещё я добавил новый параметр $permanentRedirect, который, соответственно, делает реврайт редиректом (по-умолчанию реврайты создаются "внутренними").

Если сохранить реврайт так, как выше, без указания магазина (Store Id), он добавится для всех магазинов. Это хорошо :)

makerewritesAction

Создание реврайтов будет происходить в действии makerewrites контроллера Mage_Blogrewrite_Adminhtml_BlogrewriteController:
<?php
class Mage_Blogrewrite_Adminhtml_BlogrewriteController extends Mage_Adminhtml_Controller_Action {
...
    public function makerewritesAction() {
...
    }
...
}
?>
Что бы создать реврайт, нужно знать, для чего его создавать. Т.е. нужно перебрать все посты, категории, архивы, и т.д., узнать их адреса, и создать по реврайту, перенаправляя эти адреса на правильные.

Сейчас будет в основном Wordpress часть.

Wordpress

Мы будем пользоваться функциями Wordpress для получения ЧПУ постов/категорий и т.д. Отсюда следует два замечения:
  • Пермалинки в настройках Wordpress должны быть настроены на ЧПУ. Иначе мы будем делать ревайты на адреса вида ?p=123. Нам этого не надо, эти адреса и так работают.

    С другой стороны, схема ЧПУ может быть любой. Всё что нужно сделать после смены вида пермалинков - нажать кнопку "пыщь" на странице нашего модуля :)
  • Все функции, возвращающие адреса, возвращают полные, абсолютные адреса. Реврайты же нужно создавать на относительные адреса (иначе они просто не будут работать). Поэтому нужно удалять доменную часть. Для этого получаем базовый адрес Magento - вызываем метод Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_WEB), и получаем, например, http://www.example.com/. Далее при получении адреса для реврайта, будем отсекать эту часть:
    trim(substr(get_permalink(), strlen($baseUrl)), '/')
    Кроме того, нужно убрать последний слэш, без него реврайты тоже не сработают. trim это делает.

Перебор постов

Для перебора постов можно воспользоваться "циклом", loop. Но по-умолчанию Wordpress загружает лишь несколько постов в цикл. А нам надо все. Это делается вызовом метода query_posts. После этого запускаем цикл, перебирающий все посты. На каждой итерации добавляем/обновляем реврайт:
// Query all published posts
query_posts(array('post_status' => 'publish', 'showposts' => -1));
while (have_posts()) {
    the_post();

    // get_permalink returns full absolute url. We need to remove domain info.
    // Also remove trailing slash, it's important.
    // E.g. was http://www.example.com/blog/2009/02/title
    //   become                                             blog/2009/02/title
    $this->_makeRewrite('blog/' . get_the_ID(),
        trim(substr(get_permalink(), strlen($blogUrl) - 4), '/'),
        'blog/index/index/p/' . get_the_ID());

    $posts++;
}

Перебор категорий

Для получения всех категорий достаточно вызвать функцию get_categories. Для получения адреса категории есть функция get_category_link.
// Update rewrites for categories
$cats = get_categories();
foreach ($cats as $_c) {
    $this->_makeRewrite('blog_cat/' . $_c->term_id,
        trim(substr(get_category_link($_c->term_id), strlen($blogUrl) - 4), '/'),
        'blog/index/index/cat/' . $_c->term_id);
}

Перебор тэгов

Тэги так же доступны вызовом всего одной функции get_tags, а для получения адреса есть функция get_tag_url:
// Update rewrites for tags
$tags = get_tags();
foreach ($tags as $_t) {
    $this->_makeRewrite('blog_tag/' . $_t->term_id,
        trim(substr(get_tag_link($_t->term_id), strlen($blogUrl) - 4), '/'),
        'blog/index/index/tag/' . $_t->term_id);
}

Перебор архивов

Я посмотрел, как архивы выводятся у нас на сайте. Это задаётся в шаблонном файле app/design/frontend/default/sunnyD/template/blog/menu.phtml, а именно - вызов функции wp_get_archives('type=monthly'). Эта функция возвращает уже отформатированный html. К сожалению, нет нормального способа выбрать нужные мне ссылки. Пришлось скопировать код этой функции к себе и немного его переделать:
// Update rewrites for archive (from Wordpress core file wp-includes\general-template.php,
// function wp_get_archives, from line 753)
global $wpdb;
$defaults = array(
    'type' => 'monthly', 'limit' => '',
    'format' => 'html', 'before' => '',
    'after' => '', 'show_post_count' => false,
    'echo' => 1
);
$r = wp_parse_args('', $defaults);
$where = apply_filters('getarchives_where', "WHERE post_type = 'post' AND post_status = 'publish'", $r);
$join = apply_filters('getarchives_join', "", $r);
$query = "SELECT DISTINCT YEAR(post_date) AS `year`, MONTH(post_date) AS `month`, count(ID) as posts FROM $wpdb->posts $join $where GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC $limit";
$archive = $wpdb->get_results($query);
if ($archive) {
    $afterafter = $after;
    foreach ((array) $archive as $_a) {
        $this->_makeRewrite('blog_month/' . $_a->year . $_a->month,
            trim(substr(get_month_link($_a->year, $_a->month), strlen($blogUrl) - 4), '/'),
            'blog/index/index/m/' . $_a->year . $_a->month);
    }
}

RSS

Последние ссылки без реврайтов - подписка на новости. Их две - подписка на новые посты, и новые комментарии.
$feeds = trim(substr(get_bloginfo('rss2_url'), strlen($baseUrl)), '/');
$feedsComments = trim(substr(get_bloginfo('comments_rss2_url'), strlen($baseUrl)), '/');

$this->_makeRewrite('blog_feeds', $feeds, 'blog/index/index/feed/rss2');
$this->_makeRewrite('blog_comments_feeds', $feedsComments, 'blog/index/index/feed/comments-rss2');

Заключение

Итак, мы сделали по реврайту на посты, категории, тэги, месячные архивы и rss. Теперь пользователи будут видеть ЧПУ, относящиеся к блогу, в адресной строке браузера. Так же адреса, связанные с Wordpress, выводимые на странице магазина, будут ЧПУ (т.к. это указано в настройках Wordpress).

Но всё же есть одна неприятность. Страницы просмотра категории, или тэга, могут иметь несколько страниц. Например, blog/my-category/page/2. Такой адрес выдаст страницу "не найдено" :(

11 сентября 2009 г.

Magento, подписка на новости во время чекаута

Задача - добавить галочку "Получать новости" к одному из шагов чекаута (checkout - "проход через кассу").



Сразу стало ясно, что придётся писать новый модуль, т.к. модификации дизайнерских файлов здесь не хватит. Как можно добавить своё поле к какому-либо шагу чекаута, можно подсмотреть у модулей Desitex Checkoutnewsletter (он добавляет галочку "подписать на новости" во второй шаг, где надо указать billing address) и у Biebersdorf CustomerOrderComment (он добавляет поле для добавления комментария в последний шаг - страницу подтверждения заказа).

Опускаю процесс копания в указанных модулях и поиска решения :) В итоге, что бы всё получилось, нужно:
  • Модифицировать дизайнерский файл, который рисует нужную страницу чекаута - добавить туда галочку;
  • Каким-либо образом подписаться на событие "сохранение страницы чекаута", что бы:
  • Запомнить состояние галочки в текущей сессии
  • Обработать событие "оформление заказа", возникающее когда пользователь уже оформил заказ, перед тем как сайт перенаправит его на сайт для оплаты (например, PayPal, AlliedWallet). Здесь надо извлечь сохранённое значение из сессии и подписать пользователя на рассылку, если он этого хочет.

Создание модуля

Прежде всего создадим модуль. Пусть он будет в пространстве имён Mage, а называться будет NewsletterSubscribe.

Сначала нужно сказать Magento, что наш модуль есть - создаём файл Mage_NewsletterSubscribe.xml в папке app/etc/modules:
<?xml version="1.0"?>
<config>
    <modules>
        <Mage_NewsletterSubscribe>
            <active>true</active>
            <codePool>local</codePool>
        </Mage_NewsletterSubscribe>
    </modules>
</config>
Согласно XML, модуль активен и находится в пуле local.

Далее создаём папку где будет находится новый модуль - app/code/local/Mage/NewsletterSubscribe, и файл конфигурации config.xml в папке app/code/local/Mage/NewsletterSubscribe/etc:
<?xml version="1.0"?>
<config>
    <global>
        <helpers>
            <newslettersubscribe>
                <class>Mage_NewsletterSubscribe_Helper</class>
            </newslettersubscribe>
        </helpers>
    </global>
</config>
Без хелпера модуль не будет работать как надо, а будет вместо этого падать. Поэтому дадим Magento хелпер, пусть и пустой - файл Data.php в папке Helper:
<?php

class Mage_NewsletterSubscribe_Helper_Data extends Mage_Core_Helper_Abstract {

}

Модификация страницы чекаута

Файл, рисующий нужную страницу чекаута - app\design\frontend\default\sunnyD\template\checkout\onepage\payment\methods.phtml. Добавляем галочку:
...
<?php /* bof Subscribe for newsletter checkbox */ ?>
<dt>Join Our Mailing List</dt>
<dd>
    <input type="checkbox" name="NewsletterSubscribe" id="NewsletterSubscribe" checked="checked" />
    <label for="NewsletterSubscribe"><?php echo Mage::helper('newslettersubscribe')->__('I would like to receive the Century Supplements newsletter') ?></label>
</dd>
<?php /* eof Subscribe for newsletter checkbox */ ?>
...
Да, теперь мы видим нашу галочку на странице выбора метода оплаты. Но почему же она неактивна? А потому что она сделана неактивной JS кодом, расположенным в конце файла:
<script type="text/javascript">payment.init();</script>
Не разбирался зачем он нужен, но в данном случае он делает неактивными все тэги <input>. Выходит, нам надо активировать нашу галочку после выполнения этого кода:
<script type="text/javascript">payment.init();</script>

<script type="text/javascript">$('NewsletterSubscribe').disabled = false;</script>
Теперь галочка стала активна, идём дальше.

Событие "сохранение страницы чекаута"

Я подсмотрел как это делает Checkoutnewsletter. В конфигурационном файле модуля есть строки, которые видимо перехватывают действия, связанные со всем чекаутом, всеми его страницами:
<?xml version="1.0"?>
<config>
    ...
    <global>
        <models>
         <checkout>
          <rewrite>
           <type_onepage>Desitex_Checkoutnewsletter_Model_Checkout_Type_Onepage</type_onepage>
          </rewrite>
         </checkout>
    ...
</config>
Стандартный класс Mage_Checkout_Model_Type_Onepage заменяется классом модуля Desitex_Checkoutnewsletter_Model_Checkout_Type_Onepage (который наследуется от оригинального класса Mage_Checkout_Model_Type_Onepage). В этом классе переопределён всего один метод:
<?php

class Desitex_Checkoutnewsletter_Model_Checkout_Type_Onepage extends Mage_Checkout_Model_Type_Onepage
{
    public function saveBilling($data, $customerAddressId)
    {
        if (isset($data['is_subscribed']) && !empty($data['is_subscribed'])){
            $this->getCheckout()->setCustomerIsSubscribed(1);
        }
        else {
            $this->getCheckout()->setCustomerIsSubscribed(0);
        }
        return parent::saveBilling($data, $customerAddressId);
    }
}
Очевидно, действие saveBilling возникает когда пользователь переходит со страницы ввода billing address (нажимает кнопку Continue). Здесь модуль сохраняет значение своей галочки "подписываться ли на новости" в текущей сессии (или чекауте...). После этого вызывает оригинальный метод стандартного класса.

Мы поступим подобным образом - сделаем класс, отнаследуем его от стандартного, и переопределяем только один метод, возникающий при сохранении формы на нашей странице. Метод будет сохранять значение нашей галочки. Класс поместим в файл Model/Onepage.php. :
<?php

class Mage_NewsletterSubscribe_Model_Onepage extends Mage_Checkout_Model_Type_Onepage {
    public function savePayment($data) {
        if (isset($_POST['NewsletterSubscribe'])){
            $this->getCheckout()->setNewsletterSubscribe((bool) $_POST['NewsletterSubscribe']);
        }
        else {
            $this->getCheckout()->setNewsletterSubscribe(false);
        }
        return parent::savePayment($data);
    }
}

Здесь меня немного настиг ступор. Значение галочки находится среди значений формы, но из текущего места у меня нет доступа к этим переменным. Т.е. доступа к объекту Magento Request, хранящему все GET и POST переменные. Доступны разные интересные объекты типа Quote, Checkout и т.д., с разным интересными данными, но не значениями формы. Я почти отчаялся, соображая что переписывать код ядра очень плохо, но потом вспомнил, что это Php, а значит в любом месте доступны переменные $_GET и $_POST :) Проблема была решена.

Теперь скажем Magento, что бы вместо стандартного класса брал наш. Редактируем etc/config.xml:
<?xml version="1.0"?>
<config>
    <global>
        <models>
            <checkout>
                <rewrite>
                    <type_onepage>Mage_NewsletterSubscribe_Model_Onepage</type_onepage>
                </rewrite>
            </checkout>
        </models>
    ...
</config>

Здесь готово. Только видимо есть одно ограничение - переопределить стандартный класс может только один модуль. Мой метод не вызывался, пока я не убрал переопределение у модуля Checkoutnewsletter. Т.е. это не обычное событие, на которое может подписаться произвольное количество слушателей. Потенциальные трудноотлаживаемые проблемы в будущем :(

Событие "оформление заказа"

В отличие от предыдущего "события", оформление заказа это "настоящее" событие checkout_type_onepage_save_order. Что бы подписаться на него нужно изменить конфиг модуля etc/config.xml:
<?xml version="1.0"?>
<config>
    <global>
        ...
        <events>
            <checkout_type_onepage_save_order>
                <observers>
                    <mage_newslettersubscribe_observer>
                        <type>singleton</type>
                        <class>newslettersubscribe/observer</class>
                        <method>onOrderSave</method>
                    </mage_newslettersubscribe_observer>
                </observers>
            </checkout_type_onepage_save_order>
        </events>
    </global>
</config>
Здесь мы указали какой метод у какого класса вызвать (Mage_NewsletterSubscribe_Model_Observer::onOrderSave), когда пользователь оформит заказ. Теперь создадим этот класс и метод - файл /Model/Observer.php:
<?php
class Mage_NewsletterSubscribe_Model_Observer extends Mage_Core_Helper_Abstract {
    public function onOrderSave($observer) {
        $isCustomerSubscribed = (bool) Mage::getSingleton('checkout/session')->getNewsletterSubscribe();
        if ($isCustomerSubscribed) {
            $quote = $observer->getEvent()->getQuote();
            $session = Mage::getSingleton('core/session');
            try {
                $status = Mage::getModel('newsletter/subscriber')->subscribe($quote->getBillingAddress()->getEmail());
                if ($status == Mage_Newsletter_Model_Subscriber::STATUS_NOT_ACTIVE){
                    $session->addSuccess(Mage::helper('checkoutnewsletter')->__('Confirmation request has been sent regarding your newsletter subscription'));
                }
            }
            catch (Mage_Core_Exception $e) {
                $session->addException($e, Mage::helper('checkoutnewsletter')->__('There was a problem with the newsletter subscription: %s', $e->getMessage()));
            }
            catch (Exception $e) {
                $session->addException($e, Mage::helper('checkoutnewsletter')->__('There was a problem with the newsletter subscription'));
            }
        }

        return $this;
    }
}
Этот код я взял из модуля Checkoutnewsletter, только переделал его что бы он работал :) К счастью в Magento есть класс, позволяющий подписывать пользователей на новости. По-сути всё что нужно сделать - вызвать Mage::getModel('newsletter/subscriber')->subscribe(<user email>);

Итог

В итоге получился небольшой модуль, выполняющий поставленную задачу :)