Академический Документы
Профессиональный Документы
Культура Документы
Руководство программиста
Содержание
1 Вступление............................................................................................................................4
1.1 Описание ...................................................................................................................................5
1.1.1 Компоненты .........................................................................................................................................5
1.2 Установка ..................................................................................................................................6
2 Пример создания приложения...........................................................................................8
2.1 Начальные приготовления ..................................................................................................10
2.1.1 Модель данных ..................................................................................................................................10
2.1.2 Создание базы данных ......................................................................................................................10
2.1.3 Конфигурирование приложения ......................................................................................................10
2.2 Управление товарами ...........................................................................................................12
2.2.1 Создание таблицы products...............................................................................................................12
2.2.2 Создание приложения для управления товарами ...........................................................................12
2.2.3 Модель................................................................................................................................................14
2.2.4 Контроллер.........................................................................................................................................16
2.2.5 Представление ...................................................................................................................................19
2.2.6 Проверка данных ...............................................................................................................................19
2.2.7 Улучшение интерфейса.....................................................................................................................22
2.2.8 Удаление товара.................................................................................................................................26
2.2.9 Добавление нового атрибута ............................................................................................................27
2.3 Каталог товаров .....................................................................................................................30
2.4 Создание покупательской корзины ...................................................................................33
2.4.1 Сессии.................................................................................................................................................33
2.4.2 Новая модель данных ........................................................................................................................33
2.4.3 Создание покупательской корзины..................................................................................................35
2.4.4 Пример обработки ошибок ...............................................................................................................41
2.4.5 Пример рефакторинга .......................................................................................................................41
2.5 Покупка ...................................................................................................................................43
2.5.1 Оформление заказа ............................................................................................................................43
2.5.2 Вывод содержимого корзины при оформлении заказа ..................................................................49
2.6 Отправка заказов...................................................................................................................52
2.6.1 Просмотр новых заказов ...................................................................................................................52
2.6.2 Связь «один-ко-многим»...................................................................................................................55
2.6.3 Отправка заказов ...............................................................................................................................56
2.7 Управление администраторами..........................................................................................57
2.7.1 Создание администратора.................................................................................................................57
2.7.2 Просмотр администраторов..............................................................................................................61
2.7.3 Вход в систему...................................................................................................................................61
2.7.4 Ограничение доступа ........................................................................................................................65
2.7.5 Удаление администраторов ..............................................................................................................66
2.7.6 Выход из системы..............................................................................................................................67
3 Многоязычные приложения .............................................................................................69
3.1 Установка и конфигурация .................................................................................................70
3.1.1 Установка ...........................................................................................................................................70
3.1.2 Создание базы данных ......................................................................................................................70
3.1.3 Конфигурирование базы данных .....................................................................................................71
3.2 Создание приложения ...........................................................................................................73
3.2.1 Структура ...........................................................................................................................................73
3.2.2 Переводчик.........................................................................................................................................74
3.2.3 Многоязычные шаблоны...................................................................................................................75
3.2.4 Выбор текущего языка ......................................................................................................................78
3.2.5 Перевод приложения .........................................................................................................................78
2
3.2.6 Кэширование......................................................................................................................................83
3.2.7 Визуальное редактирование переводов ...........................................................................................84
3
1 Вступление
4
1.1 Описание
1.1.1 Компоненты
PHP on Rails включает в себя реализацию следующих компонент:
5
1.2 Установка
6
127.0.0.1 rails.loc
NameVirtualHost 127.0.0.1:80
<VirtualHost rails.loc:80>
ServerAdmin webmaster@rails.loc
DocumentRoot <www path>/rails_demo/public
ServerName rails.loc
ErrorLog logs/rails-error_log
CustomLog logs/rails-access_log common
</VirtualHost>
7
2 Пример создания приложения
8
В этой главе объясняется, как при помощи PHP on Rails быстро создать небольшое
приложение, и демонстрируются основные возможности фреймворка. В качестве
демонстрации было выбрано приложение Depot из книги «Agile Web Development with
Rails», где главным инструментом разработки является фреймворк Ruby on Rails.
9
2.1 Начальные приготовления
Поскольку уже несколько раз говорилось о заказах, то, очевидно, что это ещё один
претендент в модель данных - Order. Заказ содержит информацию о покупателе и оплате.
10
сообщений об ошибках. Переменная $BASE_URL должна соответствовать базовому URL
приложения с символом “/” на конце. В случае данного приложения это http://rails.loc/.
<Databases>
<!--
This database is used by Site Builder
Do not delete it
-->
<Database name="server">
<Driver>MySQL</Driver>
<Host>localhost</Host>
<Port>3306</Port>
<User>root</User>
<Password></Password>
</Database>
<Database name="database">
<Driver>MySQL</Driver>
<Host>localhost</Host>
<Port>3306</Port>
<User>root</User>
<Password></Password>
<Database>database</Database>
</Database>
<Mapping>
<MapDir>./config/ormmapping/</MapDir>
</Mapping>
</Databases>
Первую запись лучше не трогать, как это указано в комментариях. Только если
пароль для пользователя root у вас не пустой, то укажите его между соответствующих
тегов. Вторую запись измените так, чтобы она соответствовала параметрам соединения с
базой depot. Она должна выглядеть следующим образом.
<Database name="depot_db">
<Driver>MySQL</Driver>
<Host>localhost</Host>
<Port>3306</Port>
<User>depot_user</User>
<Password>paroli</Password>
<Database>depot</Database>
</Database>
Каждый элемент Database в корне XML документа имеет атрибут name, который
является индексом базы данных, чем-то вроде уникального имени базы внутри
приложения. Не может быть нескольких элементов Database с одинаковыми
индексами. Индекс базы данных depot равен “depot_db”.
11
2.2 Управление товарами
12
Сгенерируйте код и наберите в браузере адрес http://rails.loc/admin/. Вы увидите
страницу с таблицей просмотра товаров и ссылку “Add new product” («Добавить новый
товар»).
13
Как видите, не написав ни строчки кода, вы создали работающее приложение.
Конструктор сайта выполнил за вас следующие действия:
2.2.3 Модель
Вспомните, как вы заполняли форму “Scaffold”. Перед вами был выбор: создать
новую модель или использовать уже существующую.
14
2.2.3.1 «Карта» модели
Файл Product.hbm.xml находится в каталоге config/ormmapping. Этот файл
представляет собой «карту», описывающую таблицу products, класс Product и связь
между ними. Именно благодаря этой «карте» PHP on Rails знает, что экземпляр класса
Product надо извлекать из таблицы products, знает о её структуре и автоматически
конструирует SQL запросы для манипулирования объектом. Файлы *.hbm.xml были
позаимствованы из ORM библиотеки для языка Java – Hibernate (hbm в названии файла об
этом и говорит).
<hibernate-mapping>
<class name="Product" database="depot_db" table="products">
</class>
</hibernate-mapping>
В корне документа находится элемент class. Его атрибуты name, table и database
определяют соответственно имена класса, таблицы и индекса базы данных. Очень
важно!!! Атрибут name элемента Database из файла конфигурации должен быть
равен атрибуту database элемента class из «карты». Индекс базы данных применяется
для того, чтобы в случае её переименования, не пришлось изменять атрибут database во
всех «картах», которых в приложении могут быть десятки. Достаточно будет изменить
имя базы данных в конфигурационном файле application.cfg.xml.
Таким образом, атрибуты элемента class говорят, что экземпляры класса Product
извлекаются из базы данных с индексом depot_db, из таблицы products.
15
1 integer integer, smallint, int, tinyint, mediumint, case, bool, integer
bigint, timestamp, year
2 float numeric, decimal, float, real, double, dec double
3 date date class DateTime
4 time time class DateTime
5 datetime datetime class DateTime
6 string char, varchar, tinyblob, tinytext, blob, text, mediumblob, string
mediumtext, longblob, longtext, enum, set
Для каждого своего атрибута (id, title, description, price) класс Product имеет пару
set/get методов. Методы load(), save(), remove(), getList() предназначены для извлечения,
сохранения и удаления объектов из базы данных. Как их использовать, будет описано
дальше.
2.2.4 Контроллер
Контроллер предназначен для реализации бизнес-логики приложения. Так вся
логика, связанная с управлением товарами, размещена в контроллере admin. В PHP on
Rails контроллер представляет собой класс. Например, контроллер admin реализован в
классе AdminController.
16
1. создал класс AdminController,
2. в директории public создал поддиректорию admin с файлом index.php, который
вызывается веб-сервером на запросы по URL http://rails.loc/admin/.
<?PHP
require_once('../../app.init.php');
require_once(CONTROLLERS_DIR . 'AdminController.class.php');
require_once(SYSTEM_UTILS_DIR . 'Request.class.php');
try {
$controller = new AdminController();
$controller->run(Request::get('action', 'listProducts'));
}
catch (Exception $exception) {
………
}
require_once('../../app.finalize.php');
?>
Обратите внимание на операторы внутри блока try. В нём создаётся объект класса
AdminController и вызывается его метод run() с единственным параметром
Request::get(‘action’, ‘listProducts’). Метод get() класса Request возвращает элемент
массива $_GET[‘action’], если он задан, или значение по умолчанию ‘listProducts’.
Значение параметра action определяет, какую страницу должен обработать контроллер.
Фактически метод run() принимает в качестве единственного аргумента имя страницы,
запрошенной пользователем.
Например, запрос
http://rails.loc/admin/index.php?action=editProduct&productId=2
17
Внутри метода actionListProducts() находится следующий код.
/**
* Display template
*/
$this->smarty->assign('pagination', $pagination);
$this->smarty->assign('message', $this->getFlash('message'));
$this->smarty->display('listProducts.tpl');
}
2.2.5 Представление
После комментария Display template контроллер передаёт объекту представления
$this->smarty данные и указывает, какой шаблон использовать для отображения.
Например, функция
выводит текст
Очевидно, это не то, что вы хотели. Скорее всего, вам нужен механизм проверки
формы перед сохранением объекта. Библиотека PHP on Rails позволяет задавать
ограничения на свойства объектов. Для этого снова используется XML «карта».
19
Откройте файл config/ormmaping/Product.hbm.xml. Чтобы задать ограничения,
нужно в корень XML документа добавить новый элемент validator. Его дочерние
элементы задают ограничения свойств.
<hibernate-mapping>
<class name="Product" database="depot_db" table="products">
………
</class>
<validator>
<required property="title">Field title is required</required>
<required property="description">Field description is required</required>
<required property="price">Field price is required</required>
<numeric property="price">Price must be numeric</numeric>
<nonzero property="price">Price is nonzero</nonzero>
</validator>
</hibernate-mapping>
задаёт ограничения:
Теперь, пока форма не будет правильно заполнена, новый товар не будет сохранён
в базе данных.
if (Request::post('save')) {
$product->setProperties($_POST);
$errors = array();
$result = Product::getManager()->validate($_POST,
Product::getClassName());
if ($result !== true) {
20
$errors = array_merge($errors, $result);
}
if (count($errors) == 0) {
$product->save();
$this->setFlash('message', 'Product was saved');
Response::locate(array('action' => 'listProducts'));
}
}
/**
* Display template
*/
$this->smarty->assign('product', $product);
if (isset($errors)) {
$this->smarty->assign('errors', $errors);
}
$this->smarty->display('editProduct.tpl');
}
$product->setProperties($_POST);
После этого происходит проверка, чтобы все обязательные поля формы были
заполнены, а поле price имело числовое ненулевое значение.
Если проверка прошла успешно, то объект сохраняется в базе данных. Для этого
используется метод save(). После чего пользователь пересылается вновь на страницу
просмотра товаров, где видит сообщение “Product was saved” (Товар был сохранён). Для
переадресации страницы используется метод Response::locate(), который посылает
браузеру заголовок Location со значением, равным адресу страницы “listProducts”.
21
Аргумент метода locate – это либо строка URL, либо массив. Элементы массива
задают контроллер, страницу и дополнительные параметры GET. В данном случае
используется тот же контроллер, поэтому он не задан явно. Ключ action указывает на
страницу просмотра товаров.
2.2.6.1 Флеш-параметры
Перед тем как перезагрузить страницу приложение устанавливает флеш-параметр
message.
public/css/style.css
22
body {
background:#636958;
color:#333;
font:70% Verdana,Tahoma,Arial,sans-serif;
margin:0;
padding:0;
}
#wrapper {
background:#f5f5f5;
border-left:5px solid #53584A;
border-right:5px solid #53584A;
color:#000;
margin:0 auto;
padding:0;
width:550px;
}
#top{
background:#69C;
color:#fff;
height:40px;
margin:0;
padding:0;
}
#navigation ul,#navigation li {
margin:0;
padding:0;
}
#navigation {
background:#71A70B;
color:#fff;
font-size:1em;
height:2em;
line-height:2em;
}
#navigation li {
float:left;
list-style:none;
white-space:nowrap;
}
#navigation li a {
background:inherit;
color:#fff;
display:block;
font-weight:bold;
23
padding:0 15px;
text-decoration:none;
text-transform:uppercase;
}
#content {
background:#fff;
float:left;
padding:15px 10px;
width:450px;
color:#000;
}
#footer {
background:#71A70B;
clear:both;
color:#fff;
font-size:.9em;
height:1.8em;
line-height:1.8em;
padding:0;
text-align:center;
}
HTML код страницы можно разделить на три логические части: заголовок с меню,
содержательную и нижнюю. Для всех страниц контроллера admin первая и последняя
части одинаковы, поэтому их можно поместить в отдельные файлы: header.tpl и footer.tpl.
app/views/templates/admin/header.tpl
<body>
<div id="wrapper">
<div id="navigation">
<ul>
<li>
24
{link controller="admin" action="listProducts" _class="selected"}
Просмотр товаров
{/link}
</li>
</ul>
</div>
<div id="content">
app/views/templates/admin/footer.tpl
</div>
<div id="footer">© 2006 Depot</span></div>
</div>
</body>
</html>
Теперь остаётся только подключить эти шаблоны к уже созданным страницам. Для
этого добавьте в начало файлов listProducts.tpl и editProduct.tpl строку
{include file="header.tpl"}
, а в конец
{include file="footer.tpl"}
25
Старайтесь выделять общие фрагменты дизайна в отдельные файлы, как это было
сделано с header.tpl и footer.tpl, чтобы их легко можно было использовать повторно.
26
Метод actionRemoveProduct не использует представление, и поэтому для него нет
шаблона removeProduct.tpl.
Чтобы приложение знало о новом поле в таблице products и умело с ним работать,
нужно добавить в «карту» config/ormmapping/Product.hbm.xml строку
, которая определяет атрибут типа date, а в класс Product - два новых метода.
{include file="header.tpl"}
………
<tr align="center">
<TD bgcolor="#90C090" colspan="7">
<STRONG>Products</STRONG>
………
</TD>
</tr>
{if $message}
<tr align="center">
<TD bgcolor="#FFFFFF" colspan="7">
………
</TD>
</tr>
{/if}
<tr align="center">
27
………
<TD bgcolor="#90C090"><STRONG>Price</STRONG></TD>
<TD bgcolor="#90C090"><STRONG>Available Date</STRONG></TD>
………
</tr>
<tr valign="top">
………
<TD bgcolor="#FFFFFF">
{$product->getPrice()|escape}
</TD>
<TD bgcolor="#FFFFFF">
{if $product->isNull('availableDate')}
не указана
{else}
{assign var="availableDate" value=$product->getAvailableDate()}
{$availableDate->getTimestamp()|date_format:"%A, %B %e, %Y"}
{/if}
</TD>
………
</tr>
{foreachelse}
<tr>
<TD bgcolor="#FFFFFF" colspan="7" align="center">
No products
</TD>
</tr>
{/foreach}
<tr>
<TD bgcolor="#FFFFFF" colspan="7" align="center">
{assign var="currentRange" value=$pagination->getCurrentRange()}
{foreach from=$pagination->getRanges() item=range}
………
{/foreach}
</TD>
</tr>
</table>
{include file="footer.tpl"}
{include file="header.tpl"}
<FORM method="post">
<table border="0" cellpadding="4" cellspacing="1" align="center"
bgcolor="#90C090">
………
<TR>
<TD bgcolor="#90C090"><STRONG>Price</STRONG></TD>
<TD bgcolor="#FFFFFF">
28
<INPUT type="text" name="price" value="{$product->getPrice()|escape}"/>
</TD>
</TR>
<TR>
<TD bgcolor="#90C090"><STRONG>Available date</STRONG></TD>
<TD bgcolor="#FFFFFF">
{if $product->isNull('availableDate')}
{html_select_date field_array="availableDate"}
{else}
{assign var="availableDate" value=$product->getAvailableDate()}
{html_select_date field_array="availableDate" time=$availableDate-
>getTimestamp()}
{/if}
</TD>
</TR>
………
</table>
</FORM>
{include file="footer.tpl"}
29
2.3 Каталог товаров
/**
* Display template
*/
$this->smarty->assign('products', Product::salableItems());
$this->smarty->display('index.tpl');
}
Метод getList() возвращает массив объектов класса Product. Его первый параметр
определяет условие выборки товаров из базы данных. Он может быть строкой, массивом
пар поле => значение или экземпляром класса Expression.
app/views/templates/root/index.tpl
30
{include file="header.tpl" pageTitle='Каталог товаров'}
<ul>
{foreach from=$products item=product}
<li>
<strong>{$product->getTitle()|escape}</strong>
<br/>
{$product->getDescription()|escape}
<br/>
Цена: {$product->getPrice()|string_format:"%0.2f"} $
{link action="add2cart" productId=$product->getId()}
Добавить в корзину
{/link}
<br/><br/>
</li>
{/foreach}
</ul>
{include file="../admin/footer.tpl"}
app/views/templates/root/header.tpl
<body>
<div id="wrapper">
<div id="navigation">
<ul>
<li>
{link _class="selected"}
Каталог товаров
{/link}
</li>
</ul>
</div>
<div id="content">
31
Просмотр каталога товаров готов.
32
2.4 Создание покупательской корзины
2.4.1 Сессии
Просматривая каталог товаров, пользователь выбирает некоторые из них для
покупки. Выбранные товары заносятся в виртуальную Покупательскую корзину. Корзина
– это список товаров, их количество и цена. Веб-приложение должно как-то сохранять
содержимое Покупательской корзины во время навигации пользователя по сайту. Для
этого отлично подходит сессия. Сессия, открытая для первого запроса пользователя,
доступна и для последующих запросов. Здесь не описывается механизм работы сессий в
PHP. Предполагается, что читатель знаком с ним.
33
`unitPrice` decimal(10,2) NOT NULL,
PRIMARY KEY (`id`),
KEY `productId` (`productId`),
CONSTRAINT `productId` FOREIGN KEY (`productId`) REFERENCES `products`
(`id`)
)
Теперь необходимо создать модель для этой таблицы. Для этого используйте
конструктор сайта http://rails.loc/builder/. Зайдите в меню “Models”, выберите ссылку
“Generate *.hbm.xml”, а затем “Generate item” -, как показано ниже. Заполните первую
форму.
на
34
<many-to-one name="product" column="productId" type="integer" not-null="true"
class="Product"/>
Теперь каждый объект класса LineItem имеет свойство product по имени связи.
echo $lineItem->product->getTitle();
$lineItem->setProductId(1);
echo $lineItem->getProductId();
Cart.class.php
<?php
class Cart {
?>
35
{link action="add2cart" productId=$product->getId()}
Добавить в корзину
{/link}
36
Сначала проверяется, был ли такой же товар уже помещён в корзину. Если да, то
просто увеличивается на единицу его количество. Если же нет, то создаётся новый объект
LineItem и помещается в корзину. После этого стоимость товара прибавляется к общей
стоимости корзины.
/**
* Display template
*/
$this->smarty->assign('cart', $this->findCart());
$this->smarty->display('displayCart.tpl');
}
app/views/templates/root/displayCart.tpl
<ul>
37
{$unitPrice*$quantity|string_format:"%0.2f"}$
<br/><br/>
</li>
{/foreach}
</ul>
<hr/>
<strong>Общая стоимость</strong>:
{$cart->getTotalAmount()|string_format:"%0.2f"}$
{include file="../admin/footer.tpl"}
38
<div id="navigation">
<ul>
<li>
{if !$smarty.request.action ||
$smarty.request.action == 'index'}
{link _class="selected"}
Каталог товаров
{/link}
{else}
{link}
Каталог товаров
{/link}
{/if}
</li>
<li>
{if $smarty.request.action == 'displayCart'}
{link action="displayCart" _class="selected"}
Просмотр корзины
{/link}
{else}
{link action="displayCart"}
Просмотр корзины
{/link}
{/if}
</li>
</ul>
</div>
<div id="content">
/**
* Display template
*/
$this->smarty->assign('cart', $cart);
$this->smarty->display('displayCart.tpl');
}
app/controllers/RootController.class.php
/**
* Display template
*/
$this->smarty->assign('message', $this->getFlash('message'));
$this->smarty->assign('products', Product::salableItems()$products);
39
$this->smarty->display('index.tpl');
}
app/views/templates/root/index.tpl
{if $message}
<h3 align="left">
<font color="Blue">{$message|escape}</font>
</h3>
{/if}
………
{include file="../admin/footer.tpl"}
app/views/templates/root/displayCart.tpl
………
{include file="../admin/footer.tpl"}
app/models/Cart.class.php
<?php
40
class Cart {
………
public function _empty() {
$this->items = array();
$this->totalAmount = 0.0;
}
………
}
?>
<?PHP
………
class RootController extends AppController {
………
public function actionAdd2cart() {
try {
}
catch (ItemNotFound $e) {
41
$this->redirect2index('Товар с идентификатором ' .
Request::integerParam('productId').
' не найден');
}
}
?>
42
2.5 Покупка
После того, как был реализован интерфейс для просмотра каталога товаров и
заполнения Покупательской корзины, необходимо дать возможность клиенту сделать
заказ, а администратору магазина этот заказ принять. При оформлении заказа клиент
должен указать свою контактную информацию и данные об оплате. Реализация какой-
либо системы оплаты не является целью этого руководства, поэтому процедура оплаты
будет простой.
ALTER TABLE `line_items` ADD COLUMN `orderId` int(10) unsigned NOT NULL;
CREATE INDEX iOrderId ON `line_items`(`orderId`);
ALTER TABLE `line_items` ADD CONSTRAINT `orderId` FOREIGN KEY (`orderId`)
REFERENCES `orders` (`id`);
43
В «карту» LineItem.hbm.xml нужно добавить определение свойства orderId.
config/ormmapping/LineItem.hbm.xml
<hibernate-mapping>
</hibernate-mapping>
Теперь необходимо добавить в класс LineItem методы set и get для нового атрибута.
А можно просто сгенерировать класс LineItem снова. Для этого достаточно установить
опцию “Rewrite if exists (Перезаписать, если существует)” в конструкторе сайта.
44
public function actionCheckout() {
$cart = $this->findCart();
/**
* Display template
*/
$this->smarty->assign('paymentTypes', Order::$paymentTypes);
$this->smarty->display('checkout.tpl');
}
Order.class.php
<?PHP
?>
45
</select>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" name="checkout" value="Заказать"/>
</td>
</tr>
</table>
</form>
{include file="../admin/footer.tpl"}
Адрес, на который указывает элемент form, задаётся при помощи ещё одной
дополнительной функции Smarty: url. Эта функция формирует URL в пределах
приложения. В качестве аргументов она принимает контроллер controller (можно
опускать, если используется текущий), имя страницы action и другие дополнительные
параметры, передающиеся в URL.
/**
* Display template
*/
$this->smarty->assign('paymentTypes', Order::$paymentTypes);
if (!is_null($errors)) {
$this->smarty->assign('errors', $errors);
}
$this->smarty->display('checkout.tpl');
}
$driver->startTransaction();
$order = new Order($_POST);
$order->save();
$cart = $this->findCart();
$items = $cart->getItems();
foreach ($items as $item) {
/*@var $item LineItem*/
$item->setOrderId($order->getId());
$item->save();
}
$driver->commit();
$cart->_empty();
$this->redirect2index('Спасибо за покупку');
}
catch (SqlException $e) {
$driver->rollback();
46
$errors = array('Заказ не был сохранён');
}
}
else {
$errors = $result;
}
$this->actionCheckout($errors);
}
<validator>
<required property="name">Имя обязательно</required>
<required property="email">E-mail обязателен</required>
<email property="email">Неправильный формат e-mail</email>
<required property="address">Адрес обязателен</required>
<required property="paymentType">Выберите метод оплаты</required>
</validator>
При регистрации заказа в базе данных сначала сохраняется объект $order, чтобы
присвоенный ему первичный автоинкрементный ключ, можно было указать для всех
объектов класса LineItem. Все SQL команды выполняются в рамках одной транзакции. Так
что, если ваша база данных поддерживает транзакции (для этого в СУБД MySQL
создавайте таблицы типа InnoDB), то в случае провала одной SQL команды,
откатываются все изменения в базе данных.
app/views/templates/root/checkout.tpl
47
<form action="index.php?action=saveOrder" method="post">
{if isset($errors)}
{/if}
</form>
{include file="../admin/footer.tpl"}
48
2.5.2 Вывод содержимого корзины при оформлении заказа
Клиенту было бы удобно видеть на странице оформления заказа список,
покупаемых им товаров. Для этого достаточно было бы передать представлению
экземпляр корзины, а в шаблон страницы добавить необходимый HTML код. Передать
представлению объект корзины можно, добавив в метод actionCheckout() строку
$this->smarty->assign('cart', $cart);
<ul>
49
{assign var="quantity" value=$item->getQuantity()}
<li>
<strong>{$product->getTitle()|escape}</strong>
<br/>
{$product->getDescription()|escape}
<br/>
<strong>Цена единицы товара</strong>:
{$unitPrice|string_format:"%0.2f"}$
<strong>Количество</strong>:
{$quantity}
<strong>Всего</strong>:
{$unitPrice*$quantity|string_format:"%0.2f"}$
<br/><br/>
</li>
{/foreach}
</ul>
app/views/templates/root/displayCart.tpl
{include file="cartContent.tpl"}
<hr/>
<strong>Общая стоимость</strong>:
{$cart->getTotalAmount()|string_format:"%0.2f"}$
{include file="../admin/footer.tpl"}
app/views/templates/root/checkout.tpl
{include file="cartContent.tpl"}
{include file="../admin/footer.tpl"}
50
51
2.6 Отправка заказов
После того, как клиент оформил заказ, администратор магазина должен иметь
возможность просмотреть его и переслать товар по указанному адресу после оплаты. В
данном приложении не рассматривается сам процесс оплаты. Считается, что
администратору известно, что покупатель уже расплатился. Он должен лишь пометить
заказ как отправленный.
52
/**
* Display template
*/
$this->smarty->assign('pendingOrders', Order::pendingOrders());
$this->smarty->display('ship.tpl');
}
app/views/templates/admin/ship.tpl
{include file="header.tpl"}
<DIV align="center">
<strong>Отправка товаров</strong>
</DIV>
{if $message}
<tr align="center">
<TD bgcolor="#FFFFFF" colspan="3">
<FONT size="-2" color="Blue">
<STRONG>
{$message|escape}
</STRONG>
</FONT>
</TD>
</tr>
{/if}
<tr valign="top">
<TD bgcolor="#FFFFFF" align="center" valign="middle">
<input type="checkbox" name="orders[]" value="{$order->getId()}"/>
</TD>
<TD bgcolor="#FFFFFF">
<strong>{$order->getName()|escape}</strong>
<br/>
{$order->getEmail()|escape}
<br/>
<em>Адрес: {$order->getAddress()|escape}</em>
</TD>
<TD bgcolor="#FFFFFF">
<ul>
{foreach from=$order->items item=item}
<li>
{$item->getQuantity()}
53
x
{$item->getUnitPrice()|string_format:"%0.2f"}$
{$item->product->getTitle()|escape}
</li>
{/foreach}
</ul>
</TD>
</tr>
{/foreach}
<tr align="center">
<TD bgcolor="#FFFFFF" colspan="3">
<input type="submit" name="ship" value="Отправить"/>
</TD>
</tr>
</table>
</form>
{include file="footer.tpl"}
app/views/templates/admin/header.tpl
<!DOCTYPE ………>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
………
<body>
………
<div id="navigation">
<ul>
<li>
{if !$smarty.request.action ||
$smarty.request.action == 'listProducts'}
{link controller="admin" action="listProducts" _class="selected"}
Просмотр товаров
{/link}
{else}
{link controller="admin" action="listProducts"}
Просмотр товаров
{/link}
{/if}
</li>
<li>
{if $smarty.request.action == 'ship'}
{link controller="admin" action="ship" _class="selected"}
Отправка заказов
{/link}
{else}
{link controller="admin" action="ship"}
Отправка заказов
{/link}
{/if}
</li>
</ul>
</div>
<div id="content">
54
2.6.2 Связь «один-ко-многим»
Ранее был приведён пример реализации связи «многие-к-одному» в PHP on Rails.
Текст шаблона ship.tpl содержит пример связи «один-ко-многим».
</class>
</hibernate-mapping>
55
2.6.3 Отправка заказов
Осталось только реализовать саму отправку заказов. При нажатии на кнопку
«Отправить» приложению передаётся параметр orders, содержащий массив первичных
ключей выбранных заказов. Это достигается благодаря строке
/**
* Display template
*/
$this->smarty->assign('message', $this->getFlash('message'));
$this->smarty->assign('pendingOrders', Order::pendingOrders());
$this->smarty->display('ship.tpl');
}
56
2.7 Управление администраторами
Кроме первичного ключа таблица users содержит всего два поля: login,
идентифицирующее пользователя, и password, хранящее хешированный пароль. Логин
является уникальным. Для хеширования пароля будет использоваться алгоритм MD5.
57
После того, как была создана новая модель User, можно реализовывать функции
контроллера login. Лучше начать с метода actionAddUser().
if (count($errors) == 0) {
try {
$user->save();
$this->redirect2userList("Пользователь $login был
добавлен");
}
catch (DuplicateEntry $e) {
$errors[] = "Пользователь с именем $login уже
существует";
}
}
}
/**
* Display template
*/
$this->smarty->assign('errors', @$errors);
$this->smarty->assign('user', $user);
$this->smarty->display('addUser.tpl');
}
58
$this->setFlash('message', $message);
}
Response::locate(array('action' => 'listUsers'));
}
В первой строке создаётся новый объект User. Затем проверяется, была ли нажата
кнопка «Сохранить». Если кнопка была нажата, то проверяется, был ли указан логин. Если
ошибок найдено не было, то объект сохраняется в базе данных. При этом вызов метода
save() заключён в конструкцию try…catch. Если пользователь с указанным логином уже
существует в базе данных, то будет выброшено исключение DuplicateEntry, которое
является потомком класса SqlException. В этом случае на странице выводится
соответствующее сообщение. Использование исключения DuplicateEntry позволяет
избежать дополнительного запроса SELECT для поиска в таблице users пользователя с
указанным логином.
app/views/templates/login/addUser.tpl
{include file="../admin/header.tpl"}
<FORM method="post">
<tr align="center">
<TD bgcolor="#90C090" colspan="2">
<strong>Создать пользователя</strong>
</TD>
</tr>
<TR>
<TD bgcolor="#90C090"><STRONG>Логин</STRONG></TD>
<TD bgcolor="#FFFFFF">
<INPUT type="text" name="login" value="{$user->getLogin()|escape}"/>
</TD>
</TR>
<TR>
<TD bgcolor="#90C090"><STRONG>Пароль</STRONG></TD>
<TD bgcolor="#FFFFFF">
<INPUT type="password" name="password"/>
59
</TD>
</TR>
<tr align="center">
<TD bgcolor="#FFFFFF" colspan="2">
<INPUT type="submit" name="save" value="Save"/>
</TD>
</tr>
</table>
</FORM>
{include file="../admin/footer.tpl"}
2.7.1.1 Методы-события
Если вы попробуйте создать администраторов системы и затем посмотрите в
таблицу users, то увидите, что все пароли сохранены в явном виде. Это происходит,
потому что свойству password присваивается значение одноимённого поля формы, и
именно в таком виде оно пишется в базу данных. В PHP on Rails существуют методы-
события, вызываемые в определённые моменты. К ним относятся методы beforeSave() и
afterSave(), которые вызываются менеджером объектов до и после сохранения каждого
объекта в базе данных.
60
}
}
Если свойство password было изменено, то считается, что ему было присвоено
новое нехешированное значение. В этом случае применяется функция mp5().
app/views/templates/login/listUsers.tpl
{include file="../admin/header.tpl"}
{link action="addUser"}Создать{/link}
{if $message}
<div align="center">
<font color="Blue">
{$message|escape}
</font>
</div>
{/if}
<ul>
{foreach from=$users item=user}
<li>
<strong>{$user->getLogin()|escape}</strong>
{link action="removeUser" userId=$user->getId()}
Удалить
{/link}
</li>
{/foreach}
</ul>
{include file="../admin/footer.tpl"}
61
Эти действия должны быть реализованы в методе actionLogin() контроллера
LoginController. В контроллер был добавлен атрибут $this->adminSession. Это экземпляр
класса Session. Он представляет собой сессию, открываемую после успешного входа в
систему. Факт, что эта сессия открыта, является гарантом правильной аутентификации
при последующей навигации пользователя по административной части приложения.
<?PHP
………
class LoginController extends AppController {
/**
* Display template
*/
$this->smarty->display('login.tpl');
}
………
/**
* Сессия администратора
*
* @var Session
*/
protected $adminSession = null;
}
?>
62
return self::loadWhere(array('login' => $login,
'password' => md5($password)));
}
app/views/templates/login/login.tpl
<TR>
<TD bgcolor="#90C090"><STRONG>Логин</STRONG></TD>
<TD bgcolor="#FFFFFF">
<INPUT type="text" name="login"/>
</TD>
</TR>
<TR>
<TD bgcolor="#90C090"><STRONG>Пароль</STRONG></TD>
<TD bgcolor="#FFFFFF">
<INPUT type="password" name="password"/>
</TD>
</TR>
<tr align="center">
<TD bgcolor="#FFFFFF" colspan="2">
<INPUT type="submit" name="loginBtn" value="Войти"/>
</TD>
</tr>
</table>
</FORM>
{include file="../admin/footer.tpl"}
app/views/templates/admin/header.tpl
……..
<div id="top" align="center">
<h1>Администрирование магазина</h1>
</div>
{if !$dontShowMenu}
<div id="navigation">
<ul>
………
</ul>
63
</div>
{/if}
<div id="content">
/**
* Display template
*/
$this->smarty->assign('totalOrderCount', Order::count());
$this->smarty->assign('pendingOrderCount', Order::pendingCount());
$this->smarty->display('index.tpl');
}
app/views/templates/login/index.tpl
{include file="../admin/header.tpl"}
<ul>
<li>Всего {$totalOrderCount} заказов</li>
<li>Ждут отправки {$pendingOrderCount} заказов</li>
</ul>
{include file="../admin/footer.tpl"}
64
2.7.4 Ограничение доступа
Аутентификация пользователей на входе в систему была реализована. Однако по-
прежнему любой человек имеет доступ к административной части сайта. Необходимо
ограничить доступ пользователей к методам контроллеров admin и login.
Как было сказано выше, гарантом того, что пользователь успешно прошёл
аутентификацию, является открытая сессия администратора.
Класс Session имеет два метода для открытия сессии: start() и restore(). Первый
метод либо использует уже существующую сессию с указанным в конструкторе именем,
либо создаёт новую. Второй метод может использовать только уже открытую ранее
сессию. В противном случае он вернёт false.
При этом проверяется правильность сессии: IP адрес и т.д. Если сессия была
открыта клиентом по одному адресу, а после происходит попытка использовать её
клиентом с другого адреса, то метод restore() возвращает false, а метод start() очищает все
данные сессии. Таким образом, гарантируется безопасность данных, хранящихся в
сессиях на сервере.
65
Фильтр может проверять, открыта ли сессия администратора, и в зависимости от
результата либо разрешать дальнейшую обработку запроса, либо перебрасывать
пользователя на страницу «Вход в систему». Вот его примерная реализация в классе
LoginController.
Фильтр проверяет для всех страниц, кроме login («Вход в систему»), была ли
открыта сессия администратора. Если нет, то пользователь отсылается на прохождение
процедуры аутентификации.
Так же, как и для сохранения, так и для операции удаления объекта существуют два
метода-события: beforeRemove() и afterRemove(). В данном случае нужно использовать
первый метод.
66
$user->remove();
$this->redirect2userList("User was removed");
}
catch (OrmException $e) {
$this->redirect2userList($e->getMessage());
}
}
app/views/templates/admin/header.tpl
<div id="navigation">
<ul>
<li>
{if !$smarty.request.action ||
$smarty.request.action == 'listProducts'}
{link controller="admin" action="listProducts" _class="selected"}
Товары
{/link}
{else}
{link controller="admin" action="listProducts"}
Товары
{/link}
{/if}
<li>
{if $smarty.request.action == 'ship'}
{link controller="admin" action="ship" _class="selected"}
Заказы
{/link}
{else}
{link controller="admin" action="ship"}
Заказы
{/link}
{/if}
</li>
<li>
{if $smarty.request.action == 'listUsers'}
{link controller="login" action="listUsers" _class="selected"}
Администраторы
{/link}
{else}
{link controller="login" action="listUsers"}
Администраторы
{/link}
{/if}
</li>
<li>
67
{link controller="login" action="logout"}
Выход
{/link}
</li>
</ul>
</div>
68
3 Многоязычные приложения
69
3.1 Установка и конфигурация
3.1.1 Установка
Распакуйте архив с PHP on Rails в какую-либо директорию и настройте ваш веб-
сервер так, как это было сделано в части 2. Для удобства будем считать, что приложение
доступно по адресу http://translator.loc/. Укажите этот адрес в качестве базового URL в
файле конфигурации config/config.php.
Эта база данных будет состоять из четырёх таблиц. Их структура в виде SQL
находится в файле system/com/translator/sql/sql_without_foreign_keys.sql. Создайте эти
таблицы.
-- ----------------------------
-- Structure of the translator
-- database
-- ----------------------------
-- ----------------------------
-- Table structure for languages
-- ----------------------------
CREATE TABLE `languages` (
`id` tinyint(3) unsigned NOT NULL auto_increment,
`code` char(2) NOT NULL,
`name` varchar(20) NOT NULL,
`keepingCharset` char(20) NOT NULL,
`displayingCharset` char(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sections
-- ----------------------------
CREATE TABLE `sections` (
`id` int(10) unsigned NOT NULL auto_increment,
`name` char(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
);
-- ----------------------------
-- Table structure for keywords
-- ----------------------------
CREATE TABLE `keywords` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`sectionId` int(11) NOT NULL,
70
`name` char(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `section_keyword` (`sectionId`,`name`),
KEY `sectionId` (`sectionId`)
);
-- ----------------------------
-- Table structure for translations
-- ----------------------------
CREATE TABLE `translations` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`keywordId` bigint(20) unsigned NOT NULL,
`languageId` int(10) unsigned NOT NULL,
`text` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `keyword_language` (`keywordId`,`languageId`),
KEY `keywordId` (`keywordId`),
KEY `languageId` (`languageId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
<Database name="translator_demo">
<Driver>MySQL</Driver>
<Host>localhost</Host>
<Port>3306</Port>
71
<User>root</User>
<Password></Password>
<Database>translator_demo</Database>
</Database>
72
3.2 Создание приложения
3.2.1 Структура
Главная и единственная задача данного приложения – это отображение
информации на нескольких языках.
Сайт содержит две страницы. Первая страница имеет два варианта: по одному на
каждый язык. Это значит, что страница имеет один шаблон, содержание которого
выводится в зависимости от выбранного языка. Поскольку в контроллере root по
умолчанию присутствует страница index, то она и будет использоваться в этих целях. На
второй странице выводится текст на обоих языках сразу.
Для начала нужно создать вторую страницу в контроллере root. Как всегда
используйте конструктор сайта, который расположен по адресу http://translator.loc/builder/.
Выберите меню “Controllers”, ссылку “Add action” и добавьте страницу index2.
app/views/templates/root/index.tpl
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; windows-1251" />
<title>Заголовок</title>
</head>
<body>
<div align="center">
[{link action='index' lang='ru'}Русский{/link}]
[{link action='index' lang='en'}English{/link}]
[{link action='index2'}Многоязычная страница{/link}]
</div>
<h2 align="center">
Это текст страницы
</h2>
</body>
</html>
app/views/templates/root/index2.tpl
73
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; windows-1251" />
<title>Заголовок (рус) Title (en)</title>
</head>
<body>
<div align="center">
[{link action='index' lang='ru'}Русский{/link}]
[{link action='index' lang='en'}English{/link}]
</div>
<h2 align="center">
Это текст страницы на русском
</h2>
<h2 align="center">
This is text in english
</h2>
</body>
</html>
3.2.2 Переводчик
Переводами, как правило, занимаются профессиональные переводчики. Класс-
переводчик, используемый в PHP on Rails, настоящий полиглот. Один его экземпляр
может работать одновременно с произвольным количеством языков.
setDefaultLanguageCode($langCode)
Если же язык по умолчанию не задан или для него тоже не был найден перевод, то
метод getString() возвращает null.
74
3.2.3 Многоязычные шаблоны
3.2.3.1 Смарти
Вывод содержимого веб-страницы – это занятие для представления, которое в PHP
on Rails реализовано при помощи сторонней библиотеки Smarty. Для поддержки
многоязычных страниц класс Smarty был расширен классом TranslatorSmarty, который
использует объект-переводчик для перевода содержимого страниц. Его конструктор
принимает единственный аргумент – объект типа Translator. Метод
setCurrentLanguageCode($langCode)
setDefaultLanguageCode($langCode)
задаёт язык по умолчанию, который будет использоваться в случае, если перевод для
текущего языка не был найден (вызывает одноимённый метод переводчика).
{translate section=”sectionName”}
keyword
{/translate}
require_once(SYSTEM_TRANSLATOR_DIR . 'TranslatorSmarty.class.php');
75
app/controllers/RootController.class.php
3.2.3.2 Шаблоны
Теперь необходимо заменить весь переводимый текст в шаблонах на вызов
блоковой функции translate.
Первая страница содержит заголовок, три ссылки и основной текст. Первые две
ссылки «Русский» и “English” не нуждаются в переводе. Таким образом, нужно лишь три
ключевых слова. Пусть это будут title, multilingual_page_link и content.
app/views/templates/root/index.tpl
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; windows-1251" />
76
<title>
{translate section="page1"}
title
{/translate}
</title>
</head>
<body>
<div align="center">
[{link action='index' lang='ru'}Русский{/link}]
[{link action='index' lang='en'}English{/link}]
[{link action='index2'}
{translate section="page1"}
multilingual_page_link
{/translate}
{/link}]
</div>
<h2 align="center">
{translate section="page1"}
content
{/translate}
</h2>
</body>
</html>
Секция page2 содержит всего два ключевых слова: title и content -, а шаблон
соответствующей страницы выглядит следующим образом.
app/views/templates/root/index2.tpl
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; windows-1251" />
<title>
{translate section="page2" lang="ru"}
title
{/translate}
-
{translate section="page2" lang="en"}
title
{/translate}
</title>
</head>
<body>
<div align="center">
[{link action='index' lang='ru'}Русский{/link}]
[{link action='index' lang='en'}English{/link}]
</div>
<h2 align="center">
{translate section="page2" lang="ru"}
content
{/translate}
</h2>
<h2 align="center">
{translate section="page2" lang="en"}
content
{/translate}
</h2>
</body>
</html>
77
На этой странице заголовок (title) и основной текст (content) отображаются дважды:
на русском и английском языках. Поэтому в блоковую функцию translate передаётся
дополнительный параметр lang, который содержит код языка, на который должно быть
переведено содержимое блока.
Заметьте, что секции page1 и page2 содержат одинаковые ключевые слова: title и
content. Однако поскольку эти ключевые слова находятся в разных секциях, то они не
имеют ничего общего.
/**
* Display template
*/
$this->smarty->setCurrentLanguageCode($langCode);
$this->smarty->display('index.tpl');
}
………
}
config/config.php
78
…
$BASE_URL = 'http://admin.translator.loc/';
…
config/application.cfg.xml
<Database name="translator">
<Driver>MySQL</Driver>
<Host>localhost</Host>
<Port>3306</Port>
<User>root</User>
<Password></Password>
<Database>translator_demo</Database>
</Database>
Как видите, для настройки приложения нужно лишь указать правильный базовый
URL и параметры соединения к базе данных переводов. Это соединение доступно в
приложении по индексу “translator”.
3.2.5.2 Языки
Наше приложение поддерживает два языка: английский и русский. Чтобы добавить
язык, нужно выбрать ссылку "Add new language” («Добавить новый язык») в разделе
“Languages” («Языки»).
-- ----------------------------
-- Table structure for translations
-- ----------------------------
CREATE TABLE `translations` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`keywordId` bigint(20) unsigned NOT NULL,
`languageId` int(10) unsigned NOT NULL,
79
`text` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `keyword_language` (`keywordId`,`languageId`),
KEY `keywordId` (`keywordId`),
KEY `languageId` (`languageId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Добавьте в базу данных русский язык. Его код – ru. Имя – Russian. Кодировка
хранения – utf-8 как у таблицы translations. А кодировка отображения – windows-1251, т.к.
она используется в HTML-странице.
3.2.5.3 Секции
Выберите в меню раздел «Секции» (“Sections”). Ссылка «Добавить новую секцию»
(“Add a new sections”) приведёт на страницу с формой из одного поля, в котором надо
указать имя секции. Не забывайте, что имена секций уникальны! Добавьте секции page1 и
page2.
80
3.2.5.4 Ключевые слова
Здесь же в разделе «Секции» напротив имени каждой секции указаны количество
ключевых слов в ней (столбец “Count of keywords”) и ссылка на их список (“List
keywords”).
Откройте список ключевых слов для секции page1. Перейдите по ссылке «Добавить
ключевое слово» (“Add a new keyword”). Добавьте в секцию три ключевых слова: title,
multilingual_page_link и content.
3.2.5.5 Переводы
Напротив каждого ключевого слова есть ссылка в колонке «Переводы»
(“Translations”). Перейдите по такой ссылке для ключевого слова title в секции page1. Вы
увидите перевод этого слова для каждого языка или фразу «не найден» (“not found”), если
перевода нет. Выберите ссылку «добавить» (“add”) напротив русского языка и введите в
поле «Текст» (“Text”) содержание заголовка страницы. Например,
Таким способом можно перевести все ключевые слова на все языки. Однако есть
более простой способ. Зайдите в раздел «Экспорт». В этом разделе вы можете
экспортировать переводы в файл формата XML Spreadsheet, который поддерживается MS
Excel. Выберите язык (например, русский), секцию (все-all) и нажмите кнопку «Экспорт»
(“Export”). Сохраните полученный документ на жёстком диске и откройте его.
81
Повторите эту процедуру для английского языка.
Теперь у вас есть сайт, полностью переведённый на два языка. Вот результаты
ваших трудов.
82
Страница с русским и английским текстом
3.2.6 Кэширование
Для начала нужно определиться, как объект-переводчик извлекает переводы из
базы данных. Здесь становится понятным для чего нужно разделение ключевых слов по
секциям.
Однако при более сложном разделении слов по секциям (не в случае, каждой
странице своя секция) число запросов к базе данных возрастает и может достигнуть
десяти-двадцати. Чтобы снизить нагрузку на базу данных, можно воспользоваться двумя
видами кэширования:
83
Первый аргумент – это булево значение. Если он равен true, то включается
кэширование, иначе оно отключается, а остальные аргументы игнорируются. Второй
аргумент – это время секундах, в течение которого кэш будет актуальным, третий –
директория кэширования.
app/views/templates/root/index.tpl
<html>
<head>
………
<!-- Overlib -->
<script type="text/javascript" src="overlib/overlib.js"/>
<script type="text/javascript" src="overlib/overlib_exclusive.js"/>
<script type="text/javascript" src="overlib/overlib_draggable.js"/>
………
</head>
………
</html>
app/views/templates/root/index2.tpl
84
<html>
<head>
………
<!-- Overlib -->
<script type="text/javascript" src="overlib/overlib.js"/>
<script type="text/javascript" src="overlib/overlib_exclusive.js"/>
<script type="text/javascript" src="overlib/overlib_draggable.js"/>
</head>
………
</html>
85
На иллюстрации видна ошибка в заголовке страницы. Поскольку заголовок не
может содержать JavaScript, то его использование приведёт к ошибке, а редактировать
перевод, конечно, будет нельзя. То же самое относится к атрибутам alt и title тэга img и
т.д. Поэтому для них логично было бы запретить включать редактирование. Для этого
достаточно передать в блоковую функцию translate параметр edit со значением 0.
<title>
{translate section="page1" edit=0}
title
{/translate}
</title>
<title>
{translate section="page2" lang="ru" edit=0}
title
{/translate}
-
{translate section="page2" lang="en" edit=0}
title
{/translate}
</title>
86