Kohana, Проекты → WordPress + Kohana

Сегодня расскажу как интегрировать kohana 3.2 в wordpress. Тема для меня интересная, т.к. последний опыт написания скриптов на чистом php мне не понравился =). Вообще, в инете инфу на данную тематику найти тяжело. Либо все ссылаются на плагин Kohana-for-Wordpress, либо инфа очень старая и не подходит для WP3 или/и KO3.2.

Для начала, немного истории о том, как я искал всю эту инфу =). Первым шагом, конечно-же, был поиск. Первым пунктом в выдаче идет блог уважаемого мной программиста Саши Купреева. На странице, которую выдал Яндекс, подробное описание использования плагина Kohana-for-Wordpress, о котором я говорил выше. И в этом описании видно, что плагин тестировался для ko3.0 и wp2.8.4. Те, кто следит за развитием kohana или wordpress поймут, что версии настолько устарели, что вряд ли вообще этот плагин будет работать. Так же подумал и я, но решил проверить…

После установки плагина пришлось поправить в его коде пару строк, т.к. некоторые функции из wp2.8 в wp3.2 являются depricated. Хорошо, что код плагина очень простой, да и функций для исправления мало. Сделал, но вот дальше пришел полный облом, который я не сразу понял, если честно. В блоге у Саши очень хорошо описано, как настроить плагин, а вот в админке wordpress нет этих настроек. Вообще нет. Потратил больше часа поисков в попытках заставить настройки выводиться без полного переписывания плагина, но результата так и не получил. Ок, вбиваем настройки в сам плагин, переустанавливаем. Не работает. Что и следовало ожидать, честно говоря…

Весь этот процесс проходил до написания QR-генератора. Поэтому, я на пару дней забросил это дело и принялся за написание генератора и его внедрения в страницу. Т.к. кохану я еще не интегрировал, пришлось написать скрипт, который выводит форму и обрабатывал запросы, а в WP создал страницу и вставил в него

shortcode [ include path="qr/generator.php" ]

Ну и в functions.php добавил вот такие строки:

function include_file($atts) {
	extract(shortcode_atts(array(
		'path'      => NULL,
	), $atts));
	include ABSPATH . $path;
}
add_shortcode('include', 'include_file');

Отвлекся, сорри =). Сегодня все-же решил вернуться к интеграции kohana в wordpress. Первая мысль — поступить как с qr-генератором: include-ом вызвать index.php коханы. Но тут появляются несколько проблем. Первая — роуты. Я так и не смог придумать, как правильно потом с ними работать. Вторая — шаблон блога. Можно было бы реализовать несколько вариантов, но у всех них будет один существенный недостаток — сложность обновления/замены. А мне хотелось полную интеграцию в блог. В связи с этим поиск продолжился…

Следующая страница, которая попала мне на глаза — официальный форум коханы. Пролистав ее ниже, можно увидеть ссылку на уже знакомый нам плагин. Но самое интересное в самом низу, как обычно =). Я уже было обрадовался, увидев код, но тут же пришло огорчение, увидев дату поста и версию коханы. Честно — даже пробовать не стал. Идем дальше…

Следующая страница, на которую я попал, описывает интеграцию ko3 в wordpress. Появилась надежда на успех, не смотря на то, что нигде не была указана версия wp и использовалась ko3.0. Надежда основывалась на том, что не используются никакие плагины, нет зависимости от месторасположения файлов ko3 и wp и пр… Оставив данную страницу открытой на потом, двинулся на поиски дальше.

Последнее, на данный момент, что нашел по теме интеграции kohana и wordpress — это их сборка в одном проекте. Не понравилось сразу, если честно, но решил все-таки протестировать. Скачал, обновил версии скриптов в сборке (т.к. устаревшие меня не интересовали) и установил все на локали. Все, что могу сказать — wordpress работает. Протестировать kohana не удалось, т.к. никакой информации по сборке я не нашел, а документации от создателя просто нет. На этом весь опыт со сборкой закончился.

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

Шаг 1: переименовать функцию __() коханы во что-то другое и заменить в коде фреймворка все вхождения. Не понравился мне этот шаг =). Оставив его на далекое будущее иду к следующему шагу.

Шаг 2: в functions.php подключаем index.php от kohana:

include ABSPATH . '/kohana/index.php';

Шаг 3: настраиваем kohana в bootstrap.php

Kohana::init(array(
	'base_url'   => rtrim(get_bloginfo('home'), '/') . '/',
	'index_file' => '',
));

Шаг 4: дописываем пару строк в functions.php:

if (!is_admin()):
remove_filter('template_redirect', 'redirect_canonical');
add_filter('template_redirect', 'Kohana::redirect_canonical');
endif;

Шаг 5: добавляем файл в application/classes/kohana.php:

<?php defined('SYSPATH') or die('No direct script access.');

class Kohana extends Kohana_Core {
	
	public static function redirect_canonical($requested_url = NULL, $do_redirect = TRUE) {
		if (is_404() && self::test_url()) {
			echo Request::instance() -> execute() -> send_headers() -> response;
			exit;
		}
		redirect_canonical($requested_url, $do_redirect);
	}

	public static function test_url($url = NULL) {
		if (NULL === $url)
			$url = trim(str_replace('?' . $_SERVER['QUERY_STRING'], '', $_SERVER['REQUEST_URI']), '/');

		foreach (Route::all() as $route) {
			if ($params = $route -> matches($url)) {
				$controller = 'controller_';
				if (isset($params['directory']))
					$controller .= strtolower(str_replace('/', '_', $params['directory'])) . '_';
				$controller .= $params['controller'];
				$action = isset($params['action']) ? $params['action'] : Route::$default_action;
				
				if (!class_exists($controller))
					return FALSE;
				if (!(method_exists($controller, 'action_' . $action) || method_exists($controller, '__call')))
					return FALSE;
				return TRUE;
			}
		}
		return FALSE;
	}

}

Шаг 4: проверяем. Не работает =(.

Т.к. больше вариантов уже не было, пришлось разбираться что и почему не работает. Начинаем с ko. Для начала, в kohana.php правим redirect_canonical в соответствии с ko3.2:

public static function redirect_canonical($requested_url = NULL, $do_redirect = TRUE) {
	if (is_404() && self::test_url()) {
		echo Request::factory()
			-> execute()
			-> send_headers()
			-> body();
		exit;
	}
	redirect_canonical($requested_url, $do_redirect);
}

Проверяем — не работает. Смотрим, что выдает — жалуется на /wp-includes/user.php в строке 660. Правим, создавая объект вручную. Снова проверяем — не работает. Теперь идут жалобы на is_a, которая depricated =(. Поняв, что правка wordpress до добра не доведет (потом все-равно же буду обновлять версии, не могу же я постоянно в ручную изменять файлы после каждого обновления; да и плагины еще есть — мало ли), бросаю эту затею. На странице с инструкцией говориться, что надо блокировать вывод E_STRICT, что и было сделано (заодно и E_DEPRECATED заглушил) в index.php коханы:

error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);

Без этого, кстати, никак. Уж очень большой хвост зависимостей от прежних версий тянет за собой wordpress. Запускаем — блог работает! Уже хорошо, но вверху блога выводит «hello, world!», что не хорошо. Вспоминаем, что в ko3.2 вывод идет в index.php, а не в bootstrap.php, как в 3.0. Комментируем нижние 4 строки и проверяем снова — работает как и должно. Теперь очередь kohana. Открываем дефолтный роут — работает как задуманно. Отлично. Отдыхаем, пьем чай =).

Последний этап — интеграция шаблона wordpress в ko3.2. Вообще-то, интегрировать их не особо сложно: создаем вьюшку в кохане примерно следующего содержания:

<?php get_header(); ?> 
	<div id="content">
		<div class="post">
			<?php echo $content; ?>
		</div>
	</div>
<?php get_sidebar(); ?> 
<?php get_footer(); ?> 

После этого можно использовать примерно так:

$content = View::factory('layout') -> set('content', 'Kohana from WordPress');
$this -> response -> body($content);

Но тут меня не устраивали 2 момента:

1. Мне больше нравится использовать template.
2. WordPress не знает о вьюшке и ko3 ничего, отсюда вытекают несколько глюков. Например, title страницы будет всегда от 404-й =(.

В моем понимании, эти пункты между собой связанны. Создаем application/classes/template.php примерно следующего содержания:

<?php defined('SYSPATH') or die('No direct script access.');

class Template extends Controller_Template {

	public $template = 'layout';
	
	protected $post;
	protected $title       = 'НЕ ЗАБЫВАЕМ ПРО TITLE!';
	protected $description = '';
	protected $keywords    = '';
	protected $content     = '';
	
	public function get_title($title) {
		return $this -> title;
	}
	
	public function __construct(Request $request, Response $response) {
		add_filter('wp_title', array($this, 'get_title'));
		return parent::__construct($request, $response);
	}
	
	public function before() {
		$result = parent::before();
		
		$this -> post = Validation::factory($_POST);
		
		if ($this -> request -> is_initial() AND !$this -> request -> is_ajax())
			$this -> template
				-> bind('title', $this -> title)
				-> bind('keywords', $this -> keywords)
				-> bind('description', $this -> description);
		
		View::bind_global('post', $this -> post);
		
		return $result;
	}

	public function after() {
		if ($this -> request -> is_initial() AND !$this -> request -> is_ajax())
			$this -> template -> content = $this -> content;
		
		return parent::after();
	}
	
}

Данный класс, в последствии, буду улучшать, добавляя различный функционал. Например, первые на очереди — простановка мета-тегов keywords и description в шаблон страницы. Использование значительно упростилось:

<?php defined('SYSPATH') or die('No direct script access.');

class Controller_Welcome extends Template {

	public function action_index() {
		$this -> content = 'Kohana from WordPress';
	}

} // End Welcome

Все-бы хорошо, кроме одного момента. Title так и не менялся =(. Убил почти 2 часа, пока понял причину: в блоге используется плагин all-in-seo, который тоже подменяет title. В процессе просмотра кода плагина увидел интересный момент:

function template_redirect() {
	global $wp_query;
	global $aioseop_options;

	$post = $wp_query->get_queried_object();

	if( $this->aioseop_mrt_exclude_this_page()){
		return;
	}

	if (is_feed()) {
		return;
	}

	if (is_single() || is_page()) {
	    $aiosp_disable = htmlspecialchars(stripcslashes(get_post_meta($post->ID, '_aioseop_disable', true)));
	    if ($aiosp_disable) {
	    	return;
	    }
	}

	if ($aioseop_options['aiosp_rewrite_titles']) {
		ob_start(array($this, 'output_callback_for_title'));
	}
}

Решение пришло само собой — притвориться страницей или полным постом. Первый вариант правильнее логически, на нем и остановился. Теперь начались поиски, как именно притвориться. Пока реализовал в виде грязного хака, в начале конструктора Template добавил 2 строки:

global $wp_query;
$wp_query -> is_page = TRUE;

Все, после этого наша вьюшка — это страница блога. Попутно закомментировал в плагине all-in-seo еще несколько строк:

echo "\n<!-- All in One SEO Pack $this->version by Michael Torbert of Semper Fi Web Design";
if ($this->ob_start_detected) {
	echo "ob_start_detected ";
}
echo "[$this->title_start,$this->title_end] ";
echo "-->\n";
echo "<!-- /all in one seo pack -->\n";

Ну вроде все работает, все выводится как надо, можно использовать. Но, как оказалось, не все так радужно. Помните, в начале я забил на шаг 1, в котором надо было переименовать в кохане функцию __()? Так вот, без этого некоторые функции ko3 не работают. Например, не будут работать Exception-ы.

Беглый осмотр показал, что для работы критичны пара классов: Exception и Validation. Ну плюс еще пара вьюшек: вывод ошибок (system/views/kohana/error.php) и профайлер (system/views/profiler/stats.php). Модули не рассматриваю в данный момент (тем более, что там только один — userguide, который я не использую никогда). Решил подменить нужные методы своими:

1. Добавляем новый метод в application/classes/kohana.php:

public static function translate($string, array $values = NULL, $lang = 'en-us') {
	if ($lang !== I18n::$lang)
		$string = I18n::get($string);
	return empty($values) ? $string : strtr($string, $values);
}

2. Добавляем файл application/classes/kohana/exception.php:

<?php defined('SYSPATH') or die('No direct script access.');

class Kohana_Exception extends Kohana_Kohana_Exception {
	
	public function __construct($message, array $variables = NULL, $code = 0) {
		if (defined('E_DEPRECATED'))
			Kohana_Exception::$php_errors[E_DEPRECATED] = 'Deprecated';
		
		$message = Kohana::translate($message, $variables);
		parent::__construct($message, (int) $code);
		$this -> code = $code;
	}
	
}

3. Добавляем класс application/classes/validation.php:

<?php defined('SYSPATH') or die('No direct script access.');

class Validation extends Kohana_Validation {

	public function errors($file = NULL, $translate = TRUE) {
		if ($file === NULL)
			return $this -> _errors;

		$messages = array();

		foreach ($this -> _errors as $field => $set) {
			list($error, $params) = $set;
			$label = $this -> _labels[$field];
			if ($translate) {
				if (is_string($translate))
					$label = Kohana::translate($label, NULL, $translate);
				else
					$label = Kohana::translate($label);
			}

			$values = array(
				':field' => $label,
				':value' => Arr::get($this, $field),
			);

			if (is_array($values[':value']))
				$values[':value'] = implode(', ', Arr::flatten($values[':value']));

			if ($params) {
				foreach ($params as $key => $value) {
					if (is_array($value))
						$value = implode(', ', Arr::flatten($value));
					elseif (is_object($value))
						continue;

					if (isset($this -> _labels[$value])) {
						// Use the label as the value, eg: related field name for "matches"
						$value = $this -> _labels[$value];

						if ($translate) {
							if (is_string($translate))
								$value = Kohana::translate($value, NULL, $translate);
							else
								$value = Kohana::translate($value);
						}
					}
					$values[':param'.($key + 1)] = $value;
				}
			}

			if ($message = Kohana::message($file, "{$field}.{$error}"))
			{
				// Found a message for this field and error
			}
			elseif ($message = Kohana::message($file, "{$field}.default"))
			{
				// Found a default message for this field
			}
			elseif ($message = Kohana::message($file, $error))
			{
				// Found a default message for this error
			}
			elseif ($message = Kohana::message('validation', $error))
			{
				// Found a default message for this error
			}
			else
			{
				// No message exists, display the path expected
				$message = "{$file}.{$field}.{$error}";
			}

			if ($translate) {
				if (is_string($translate))
					$message = Kohana::translate($message, $values, $translate);
				else
					$message = Kohana::translate($message, $values);
			} else
				$message = strtr($message, $values);

			$messages[$field] = $message;
		}

		return $messages;
	}

}

4. Добавляем файл application/views/kohana/error.php:

<?php

// Unique error identifier
$error_id = uniqid('error');

?>
<style type="text/css">
#kohana_error { background: #ddd; font-size: 1em; font-family:sans-serif; text-align: left; color: #111; }
#kohana_error h1,
#kohana_error h2 { margin: 0; padding: 1em; font-size: 1em; font-weight: normal; background: #911; color: #fff; }
	#kohana_error h1 a,
	#kohana_error h2 a { color: #fff; }
#kohana_error h2 { background: #222; }
#kohana_error h3 { margin: 0; padding: 0.4em 0 0; font-size: 1em; font-weight: normal; }
#kohana_error p { margin: 0; padding: 0.2em 0; }
#kohana_error a { color: #1b323b; }
#kohana_error pre { overflow: auto; white-space: pre-wrap; }
#kohana_error table { width: 100%; display: block; margin: 0 0 0.4em; padding: 0; border-collapse: collapse; background: #fff; }
	#kohana_error table td { border: solid 1px #ddd; text-align: left; vertical-align: top; padding: 0.4em; }
#kohana_error div.content { padding: 0.4em 1em 1em; overflow: hidden; }
#kohana_error pre.source { margin: 0 0 1em; padding: 0.4em; background: #fff; border: dotted 1px #b7c680; line-height: 1.2em; }
	#kohana_error pre.source span.line { display: block; }
	#kohana_error pre.source span.highlight { background: #f0eb96; }
		#kohana_error pre.source span.line span.number { color: #666; }
#kohana_error ol.trace { display: block; margin: 0 0 0 2em; padding: 0; list-style: decimal; }
	#kohana_error ol.trace li { margin: 0; padding: 0; }
.js .collapsed { display: none; }
</style>
<script type="text/javascript">
document.documentElement.className = document.documentElement.className + ' js';
function koggle(elem)
{
	elem = document.getElementById(elem);

	if (elem.style && elem.style['display'])
		// Only works with the "style" attr
		var disp = elem.style['display'];
	else if (elem.currentStyle)
		// For MSIE, naturally
		var disp = elem.currentStyle['display'];
	else if (window.getComputedStyle)
		// For most other browsers
		var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');

	// Toggle the state of the "display" style
	elem.style.display = disp == 'block' ? 'none' : 'block';
	return false;
}
</script>
<div id="kohana_error">
	<h1><span class="type"><?php echo $type ?> [ <?php echo $code ?> ]:</span> <span class="message"><?php echo html::chars($message) ?></span></h1>
	<div id="<?php echo $error_id ?>" class="content">
		<p><span class="file"><?php echo Debug::path($file) ?> [ <?php echo $line ?> ]</span></p>
		<?php echo Debug::source($file, $line) ?>
		<ol class="trace">
		<?php foreach (Debug::trace($trace) as $i => $step): ?>
			<li>
				<p>
					<span class="file">
						<?php if ($step['file']): $source_id = $error_id.'source'.$i; ?>
							<a href="#<?php echo $source_id ?>" onclick="return koggle('<?php echo $source_id ?>')"><?php echo Debug::path($step['file']) ?> [ <?php echo $step['line'] ?> ]</a>
						<?php else: ?>
							{<?php echo Kohana::translate('PHP internal call') ?>}
						<?php endif ?>
					</span>
					&raquo;
					<?php echo $step['function'] ?>(<?php if ($step['args']): $args_id = $error_id.'args'.$i; ?><a href="#<?php echo $args_id ?>" onclick="return koggle('<?php echo $args_id ?>')"><?php echo Kohana::translate('arguments') ?></a><?php endif ?>)
				</p>
				<?php if (isset($args_id)): ?>
				<div id="<?php echo $args_id ?>" class="collapsed">
					<table cellspacing="0">
					<?php foreach ($step['args'] as $name => $arg): ?>
						<tr>
							<td><code><?php echo $name ?></code></td>
							<td><pre><?php echo Debug::dump($arg) ?></pre></td>
						</tr>
					<?php endforeach ?>
					</table>
				</div>
				<?php endif ?>
				<?php if (isset($source_id)): ?>
					<pre id="<?php echo $source_id ?>" class="source collapsed"><code><?php echo $step['source'] ?></code></pre>
				<?php endif ?>
			</li>
			<?php unset($args_id, $source_id); ?>
		<?php endforeach ?>
		</ol>
	</div>
	<h2><a href="#<?php echo $env_id = $error_id.'environment' ?>" onclick="return koggle('<?php echo $env_id ?>')"><?php echo Kohana::translate('Environment') ?></a></h2>
	<div id="<?php echo $env_id ?>" class="content collapsed">
		<?php $included = get_included_files() ?>
		<h3><a href="#<?php echo $env_id = $error_id.'environment_included' ?>" onclick="return koggle('<?php echo $env_id ?>')"><?php echo Kohana::translate('Included files') ?></a> (<?php echo count($included) ?>)</h3>
		<div id="<?php echo $env_id ?>" class="collapsed">
			<table cellspacing="0">
				<?php foreach ($included as $file): ?>
				<tr>
					<td><code><?php echo Debug::path($file) ?></code></td>
				</tr>
				<?php endforeach ?>
			</table>
		</div>
		<?php $included = get_loaded_extensions() ?>
		<h3><a href="#<?php echo $env_id = $error_id.'environment_loaded' ?>" onclick="return koggle('<?php echo $env_id ?>')"><?php echo Kohana::translate('Loaded extensions') ?></a> (<?php echo count($included) ?>)</h3>
		<div id="<?php echo $env_id ?>" class="collapsed">
			<table cellspacing="0">
				<?php foreach ($included as $file): ?>
				<tr>
					<td><code><?php echo Debug::path($file) ?></code></td>
				</tr>
				<?php endforeach ?>
			</table>
		</div>
		<?php foreach (array('_SESSION', '_GET', '_POST', '_FILES', '_COOKIE', '_SERVER') as $var): ?>
		<?php if (empty($GLOBALS[$var]) OR ! is_array($GLOBALS[$var])) continue ?>
		<h3><a href="#<?php echo $env_id = $error_id.'environment'.strtolower($var) ?>" onclick="return koggle('<?php echo $env_id ?>')">$<?php echo $var ?></a></h3>
		<div id="<?php echo $env_id ?>" class="collapsed">
			<table cellspacing="0">
				<?php foreach ($GLOBALS[$var] as $key => $value): ?>
				<tr>
					<td><code><?php echo HTML::chars($key) ?></code></td>
					<td><pre><?php echo Debug::dump($value) ?></pre></td>
				</tr>
				<?php endforeach ?>
			</table>
		</div>
		<?php endforeach ?>
	</div>
</div>

5. Добавляем файл application/views/profiler/stats.php:

<?php defined('SYSPATH') or die('No direct script access.') ?>

<style type="text/css">
<?php include Kohana::find_file('views', 'profiler/style', 'css') ?>
</style>

<?php
$group_stats      = Profiler::group_stats();
$group_cols       = array('min', 'max', 'average', 'total');
$application_cols = array('min', 'max', 'average', 'current');
?>

<div class="kohana">
	<?php foreach (Profiler::groups() as $group => $benchmarks): ?>
	<table class="profiler">
		<tr class="group">
			<th class="name" rowspan="2"><?php echo Kohana::translate(ucfirst($group)) ?></th>
			<td class="time" colspan="4"><?php echo number_format($group_stats[$group]['total']['time'], 6) ?> <abbr title="seconds">s</abbr></td>
		</tr>
		<tr class="group">
			<td class="memory" colspan="4"><?php echo number_format($group_stats[$group]['total']['memory'] / 1024, 4) ?> <abbr title="kilobyte">kB</abbr></td>
		</tr>
		<tr class="headers">
			<th class="name"><?php echo Kohana::translate('Benchmark') ?></th>
			<?php foreach ($group_cols as $key): ?>
			<th class="<?php echo $key ?>"><?php echo Kohana::translate(ucfirst($key)) ?></th>
			<?php endforeach ?>
		</tr>
		<?php foreach ($benchmarks as $name => $tokens): ?>
		<tr class="mark time">
			<?php $stats = Profiler::stats($tokens) ?>
			<th class="name" rowspan="2" scope="rowgroup"><?php echo HTML::chars($name), ' (', count($tokens), ')' ?></th>
			<?php foreach ($group_cols as $key): ?>
			<td class="<?php echo $key ?>">
				<div>
					<div class="value"><?php echo number_format($stats[$key]['time'], 6) ?> <abbr title="seconds">s</abbr></div>
					<?php if ($key === 'total'): ?>
						<div class="graph" style="left: <?php echo max(0, 100 - $stats[$key]['time'] / $group_stats[$group]['max']['time'] * 100) ?>%"></div>
					<?php endif ?>
				</div>
			</td>
			<?php endforeach ?>
		</tr>
		<tr class="mark memory">
			<?php foreach ($group_cols as $key): ?>
			<td class="<?php echo $key ?>">
				<div>
					<div class="value"><?php echo number_format($stats[$key]['memory'] / 1024, 4) ?> <abbr title="kilobyte">kB</abbr></div>
					<?php if ($key === 'total'): ?>
						<div class="graph" style="left: <?php echo max(0, 100 - $stats[$key]['memory'] / $group_stats[$group]['max']['memory'] * 100) ?>%"></div>
					<?php endif ?>
				</div>
			</td>
			<?php endforeach ?>
		</tr>
		<?php endforeach ?>
	</table>
	<?php endforeach ?>

	<table class="profiler">
		<?php $stats = Profiler::application() ?>
		<tr class="final mark time">
			<th class="name" rowspan="2" scope="rowgroup"><?php echo Kohana::translate('Application Execution').' ('.$stats['count'].')' ?></th>
			<?php foreach ($application_cols as $key): ?>
			<td class="<?php echo $key ?>"><?php echo number_format($stats[$key]['time'], 6) ?> <abbr title="seconds">s</abbr></td>
			<?php endforeach ?>
		</tr>
		<tr class="final mark memory">
			<?php foreach ($application_cols as $key): ?>
			<td class="<?php echo $key ?>"><?php echo number_format($stats[$key]['memory'] / 1024, 4) ?> <abbr title="kilobyte">kB</abbr></td>
			<?php endforeach ?>
		</tr>
	</table>
</div>

Все, после этих действий получаем полную интеграцию ko3 в wp3.