ООП (Статей: 27)

Введение
Модульное тестирование это то, чем вы пользуетесь постоянно во время разработки и даже не замечаете этого. В процессе написания кода вы по многу раз запускаете его, чтобы убедится в том, что он работает ожидаемым образом. Так почему бы не автоматизировать этот процесс? Для этих целей применяется "Автоматизированное модульное тестирование" (АМТ) и "Модульные тесты".

АМТ решает следующие важные задачи:
* Позволяет выявить ошибку на раннем этапе разработки, до того, как код уйдет в работу. Своевременное выявление ошибки так же упрощает ее локализацию и исправление
* Позволяет быстро удостоверится в том, что вносимые в систему изменения не поломали старый код
* Облегчает процесс изучения системы новыми разработчиками

Но не следует забывать, что АМТ, как и код системы, требует поддержки и модернизации, что отнимает время у разработчика.

Фреймворк xUnit, часто применяется для создания АМТ во многих языках программирования. Для PHP так же существует его разновидность - PHPUnit. Работу с ним я и опишу в данной статье.

Установка PHPUnit
Для установки фреймворка PHPUnit воспользуйтесь следующей командой (для Linux систем):
wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
mv phpunit.phar /usr/local/bin/phpunit

Эта команда загрузит фреймворк, распространяемый в виде PHAR-пакета, предоставит ему права на выполнение и переместит его в соответствующий каталог.

Основы модульного тестирования
АМТ представляет собой множество классов, каждый из которых тестирует конкретную логику системы. Все эти классы должны наследоваться от базового класса PHPUnit_Framework_TestCase и содержать один или более методов, имя которых начинается с test.

Рассмотрим пример создания собственного класса для автоматизированного модульного тестирования. Представим что у нас есть следующий класс, реализующий простой стек:
class MyStack implements Countable{
  private $store;

  public function __construct(){
    $this->store = [];
  }

  public function push($value){
    array_push($this->store, $value);
  }

  public function pop(){
    return array_pop($this->store);
  }

  public function count(){
    return count($this->store);
  }
}

Для тестирования этого класса мы используем следующий модульный тест:

class MyStackTest extends PHPUnit_Framework_TestCase{
  public function testConstruct(){
    $stack = new MyStack();

    $this->assertEquals(0, count($stack));
  }

  public function testPush(){
    $stack = new MyStack();

    $stack->push(5);

    $this->assertEquals(1, count($stack));
  }

  public function testPop(){
    $stack = new MyStack();

    $stack->push(5);

    $this->assertEquals(5, $stack->pop());
    $this->assertEquals(0, count($stack));
  }
}

Все что вам нужно знать, для понимания этого кода, это метод assertEquals. Он является частью любого модульного теста и проверяет, равен ли первый аргумент второму, и если нет, то тест считается не пройденным.

Для запуска этого теста выполните команду:
phpunit MyStackTest.php

Шаблоны тестирования
Применяя АМТ старайтесь использовать следующие шаблоны:
* Структурируйте тесты - помните, что любой тест всегда состоит из предусловий, вызова и проверки постусловий
public function testPop(){
  // Предусловия
  $stack = new MyStack();

  // Вызов
  $stack->push(5);

  // Проверка постусловий
  $this->assertEquals(5, $stack->pop());
  $this->assertEquals(0, count($stack));
}
* Понятные имена для тестов - старайтесь именовать методы теста так, чтобы было понятно, что они проверяют. Так, метод testPop можно было бы переименовать в testPop_shouldSliceCurrentStackValue
* Применение фабрик в тесте - если в одном тесте вы часто создаете один и тот же объект, выделите его состояние в метод-фабрику. Так, код $stack = new MyStack можно было бы обернуть в метод createStack и использовать в тестах следующим образом: $stack = $this->createStack()
* Не проверяйте с помощью тестов внутреннюю логику класса, только постусловия. Другими словами тесту не нужно знать как именно работает класс, достаточно проверить, что при некоторых предусловиях и вызове всегда достигаются данные постусловия
* Не повторяйтесь. Как и при разработке приложения, старайтесь не заниматься "программированием через копипаст" во время написания тестов. Помните, что вы должны их сопровождать, а повторяющийся код сделает сопровождение сложным
* Пишите тесты так, чтобы после их прочтения было понятно, что делает тестируемый класс. Запомните - тесты это особая документация, отвечающая на вопрос: "Что должен и что не должен делать класс"

Послесловие
Я намерено упростил статью, выбросив из нее множество важных деталей, таких как Mock-объекты и тестирование исключений. Если вас заинтересовала тема модульного тестирования, обязательно обратитесь к следующей литературе:
* http://www.ozon.ru/context/detail/id/1501671/
* http://www.ozon.ru/context/detail/id/4127815/
Предыстория
Приветствую. Я уже не раз говорил, что язык JS я считаю если не самым любимым, то как минимум вторым (после C++). Это связано с тем, что благодаря этому, казалось бы, сложному языку, я постиг один из "дзенов" программирования, а именно великую парадигму прототипной ориентации объектно-ориентированного языка. Многие программисты, не знакомые с этим, не побоюсь этого слова, чистой реализацией, считают ее более сложной и даже не объектной, но на самом деле это не так, скорее наоборот, классическая объектная ориентация оказывается всего лишь надстройкой "сахара" над этой парадигмой.
Не так давно, я начал изучать язык скриптов известного редактора Vim, и сразу же обратил внимание на его объектную парадигму, и занесло...

Объекты в Vim
Забегая вперед скажу, что в этой статье речь пойдет о реализации прототипной ориентации в Vim(!), но все попорядку.
В Vim объекты создаются и используются следующим образом (" в Vim это начало строки комментария):

let obj = {'name': 'Artur'} " Создаем объект
function! obj.getName() dict " Создаем метод объекта
  return self.name
endfunction
echo obj.getName() " Вызываем метод объекта


Что мы видим из стандартного синтаксиса Vim?
* Классы не используются вообще, то есть нет возможности заранее определить структуру будущего объекта, его свойства и методы, объекты создаются сразу перечислением всех свойств и методов, а затем к ним добавляются методы.
* Нет возможности наследовать поведение, то есть мы не можем создать объект основываясь на структуре другого объекта, нужно явно копировать в новый объект все свойства и методы родителя.
* Снижается повторное использование кода, за счет отсутствия наследования, то есть возможности выносить логику в родительские объекты.
Этот список можно продолжать еще долго, но это есть то, с чего начинается любой объектно-ориентированный язык программирования - ассоциативный массив с возможностью передачи функций в качестве его элементов.
Как из всего этого сделать прототипную модель? Достаточно реализовать всего один метод!

Прототипирование
Сначала немного теории. Чтобы существующая в Vim объектная модель могла хоть немного походить на прототипную, достаточно реализовать возможность наследования реализации. В прототипной модели делается это довольно просто, но чтобы понять это, необходимо забыть о классической реализации объектности в языках программирования. Дело в том, что в прототипной модели нет понятия класса вообще, то есть за класс выступает обычный объект, называемый прототипом. Предположим нам нужно реализовать программу для магазина. Имеем два типа объектов: покупатели и сотрудники. И тот и другой тип объектов по сути является типом "Человек", то есть они отличаются только несколькими дополнительными свойствами и методами, а общие можно вынести в отдельный тип объектов. В качестве типа объектов "Человек" выступает объект, в качестве свойств которого задаются значения по умолчанию. В этот же объект выносятся и все общие методы.

let People = {'name': 0} " Прототип Человек
function! People.getName() dict
  return self.name
endFunction

Такой объект называется прототипом, так как на основании его будут создаваться другие типы объектов, а делается это путем создания ссылки на все методы прототипа из дочернего объекта (чтобы не копировать функции), а так же созданием ссылки на сам прототип (чтобы не копировать свойства):

" Функция создает объект на основе прототипа.
" 4 prototype - прототип.
" 4 properties - свойства создаваемого объекта.
function! Expand(prototype, properties)
  let obj = {}

  " Наследование свойств.
  " Наследование реализуется путем формирования ссылки на родительский объект в свойстве parent.
  let obj.parent = a:prototype
  
  " Наследование методов.
  " Наследование реализуется путем формирования ссылок на методы родительского класса в одноименных методах дочернего.
  for k in keys(a:prototype) " Проход по всем методам прототипа
    let t = type(a:prototype[k])
    if t == 2
      let obj[k] = a:prototype[k] " Создание ссылок на методы прототипа из дочернего объекта
    endif
  endfor

  " Формирование свойств.
  " Реализуется путем копирования переданного списка в создаваемый объект.
  for [k, v] in items(a:properties) " Формирование частных свойств объекта
    let t = type(v)
    if t == 3 || t == 4
      let obj[k] = deepcopy(v)
    else
      let obj[k] = v 
    endif
  endfor

  return obj 
endfunction

Вот собственно и вся реализация прототипной модели в Vim. С помощью данной функции можно создавать объекты наследуя структуру других объектов (прототипов). Как это использовать:

let s:Object = {} " Корневой прототип
function s:Object.getType() dict " Метод прототипа
  return 'prototype'
endfunction

" Создание объекта через прототип
let s:o1 = Expand(s:Object, {'name': 'Artur'}) 
function s:o1.getName() dict " Метод объекта
  return self.name
endfunction
echo s:o1.getType() " prototype - вызов метода прототипа
echo s:o1.getName() " Artur - вызов метода объекта

Реализация
Как вы могли заметить, прототипная модель это всего лишь механизм, позволяющий создавать ссылки между прототипом и объектом так, чтобы вторым было удобно пользоваться. В моем случае, в объекте создаются ссылки на методы прототипа, это позволяет вызывать их прямо из объекта, при этом не забивая память копиями методов. Так же создается свойство parent в объекте, ссылающееся на прототип, это позволяет обращаться к свойствам родителя без их копирования в объект, а так же переопределять методы родителя:

let s:o1 = Expand(s:Object, {'name': 'Artur'}) 
function s:o1.getType() dict " Переопределение метода
  return 'object'
endfunction
echo s:o1.getType() " object - вызов переопределенного метода

При переопределении метода всегда можно получить доступ к перегружаемому методы через ссылку parent:

echo s:o1.parent.getType() " prototype - вызов родительского метода


Итоги
Никогда не знаешь, сколько интересного можно найти в "стареньком" редакторе. Обязательно изучайте различные реализации давно известных вам решений, это позволяет взглянуть на одни и те же вещи под совершенно разными углами.
Всем привет))
Сегодня я опишу как можно организовать автоматическую загрузку классов с помощью пространств имен (namespace) точнее как это организовал я))
Итак как и везде наш проект имеет индескную страницу которая лежит в самом корне проекта (не зависимо от того в какой директории находится сам проект) так вот сначала мы и вычислим эту директорию. Для этого пропишем в нашем индексном файле следующее
<?php
define('DOCROOT', realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR);
?>
константа DOCROOT будет содержать полный путь к корневой директории.
затем нам нужно сохранить путь к корневой директории в include_path
<?php
set_include_path(DOCROOT);
?>
далее мы должны вызвать функцию регистрации автоматической загрузки классов
<?php
spl_autoload_register();
?>
но так как данный способ поддерживает автозагрузку классов только в нижнем регистре мы зарегистрируем "свою" функцию загрузки:
<?php
spl_autoload_register(function ($class) {
    include_once DOCROOT . str_replace('\\', '/', $class).'.php';   
});
?>
Давайте теперь проверим нашу функцию на работоспособность.
Для начала создадим в корневой директории проекта папку test и затем этой папке создадим класс Test*
<?php
namespase test;

class Test {
   function __construct() {
       echo 'Все работает';
   }
}
?>
и теперь попробуем вызвать класс тест
<?php
new \test\Test();
?>
и перейдя по адресу vacsh_proect.ru вы должны увидеть
Все работает
Вот в принципе и все))
*UPD 03.08.14
все классы должны быть в отдельных файлах, а их ns должен повторять файловую структуру сайта
Доброго времени суток!
Вот решил написать простой и удобный класс для реализации мультиязычности для Вашего сайта.
Итак начнем!
Структура у нас будет следующего вида:
dir: [b]/[/b]
     - dir: [b]i18n[/b]
          - file: [b]ru.php[/b] // тут будут переводы
     - dir: [b]system[/b]
          - file: [b]config.php[/b] // тут будут настройки
          - file: [b]lang.php[/b] // доп. функционал
          - file: [b]i18n.php[/b] // тут собственно сам класс
     - file: [b]index.php[/b]
Создаем файл index.php.
Содержимое:
<?php
include_once ('system/lang.php'); // Подключаем класс
echo __('Test') . '<br />'; // проверка
?>
Создаем папку system.
Создаем в папке system файл config.php.
Содержимое:
<?php
$langs = array('ru', 'en'); // массив с доступными языками
$default_lang = 'ru'; // Язык по умолчанию 
?>
Теперь перейдем к самому классу.
Создаем в папке system файл i18n.php.
Содержимое:
<?php
class I18n {
    /** задаем язык классу **/
    public static $lang = 'en';
    /** задаем путь к папке где лежат файлы с языками **/
    public static $path = 'i18n';
    /** кеширование **/
    protected static $_cache = array();
    
    /** задаем язык. Пример I18n::lang('ru') **/
    public static function lang($lang) {
        I18n::$lang = $lang;
        return I18n::$lang;
    }
    
    /** функция отвечающая за вывод **/
    public static function get($string, $lang = NULL) {
        if (!$lang) {
            $lang = I18n::$lang;
        }
        $table = I18n::load($lang);
        if (isset($table[$string])) {
            return $table[$string];
        }
        return $string;
    }
    
    /** функция отвечающая за загрузку файла с переводами **/
    public static function load($lang) {
        if (isset(I18n::$_cache[$lang])) {
            return I18n::$_cache[$lang];
        }
        $table = array();
        $path = I18n::$path . DIRECTORY_SEPARATOR . $lang . '.php';

        if (file_exists($path)) {
            $table = array_merge_recursive($table, include($path));
        }

        return I18n::$_cache[$lang] = $table;
    }

}
?>

В папке system создаем файл lang.php

<?php
header('Content-Type: text/html; charset=utf-8'); // задаем кодировку (это я для теста писал)
include('config.php'); // подключаем настройки
include('i18n.php');

// Здесь переключение языков
if (!empty($_GET['lang']) && in_array($_GET['lang'], $langs)) {
    setcookie('lang', $_GET['lang']);
    $allowed_lang = $_GET['lang'];
} elseif (!empty($_COOKIE['lang']) && in_array($_COOKIE['lang'], $langs)) {
    $allowed_lang = $_COOKIE['lang'];
} else {
    $allowed_lang = $default_lang;
}

I18n::lang($allowed_lang);

if (!function_exists('__')) {
    // функция __() выводит переведенную строку
    // Если такой строки нету в словаре, то выводит значение $string
    function __($string) {
        return I18n::get($string);
    }

}
?>

Создаем папку i18n в ней файл ru.php
Содержание:
<?php
return array(
    'Test' => 'Тест',
);
?>

Ну вот и все. Все работает)
Следующая статья будет о том как установить и настроить локальный веб сервер на виртуальной машине VitrualBox (debian). P.S. сайты будут доступны и на самой хост машине (т.е. на Windows 7)
Добрый день!
Хочу представить Вам свой класс реализации мультиязычности для ваших проектов.

<?php
/**
 * @author KpuTuK
 * @link http://kputuk.ru
 * @version 1.0.1
 * @copyright (c) 2013
 */
class language {
    
    /**
     * Массив выбранного пакета
     * @var array $language
     */
    private $language = array();
    
    /**
     * Загрузка выбранного пакета
     * @param string $language
     * @throws InvalidArgumentException
     */
    function __construct($language) {
        if (is_string($language)) {
            if (file_exists('/language/'. $language .'.ini')) {
                $this->language = parse_ini_file('/language/'. $language .'.ini');
            } else {
                $this->language = parse_ini_file('/language/default.ini');
            }
        } else {
            throw new InvalidArgumentException('Недопустимый тип параметра. Ожидается [string] вместо ['. gettype($language) .'].');
        }
    }
    
    /**
     * Локализация текстовой строки
     * @param string $text Строка для локализации
     * @return string Локализированная строка
     * @throws InvalidArgumentException
     */
    public function replease($text) {
        if (is_string($text)) {
            $lang_rus = array_flip(parse_ini_file('/language/default.ini'));
            $key = $lang_rus[$text];
            if (isset($this->language[$key])) {
                return $this->language[$key];
            } else 
                return $text;
        } else {
            throw new InvalidArgumentException('Недопустимый тип параметра. Ожидается [string] вместо ['. gettype($text) .'].');
        }
    }
}
?>

создаем папку language
в которой содаем файл default.ini
который будет отвечать у нас за названия ключей в языковых пакетах (key => value) по умолчанию

содержимое файла default.ini:
test = тестируем мультиязычность

затем в той же папке создадим файл EN.ini
который будет отвечать за перевод строки на английский

содержимое файла EN.ini:
test = multilanguage test

Реализация класса:
создаем файл test.php
с содержанием:

<?php
$lng = new language('EN');
echo $lng->replease('тестируем мультиязычность');
?>

затем открываем наш файл в браузере и видим

multilanguage test

Вот впринципе и все smile
[/b]

[b]Предисловие

Я думаю многих заинтересует статья о безопасности web проектов, потому настало время. Я часто буду ссылаться на используемую мной архитектуру, но описываемые подходы можно использовать в любой системе (главное знать как).

Уровни безопасности
Многие программисты задумываясь о безопасности системы представляют единый, универсальный механизм, позволяющий защитить всю систему путем вызова (на пример) функции toDoWell или метода $security->work(), но к сожалению, такой подход не оправдывает себя, в результате получается комок грязного кода, который не защищает систему совсем. Подобная проблема имеет те же корни, что и все комки грязного кода в программировании - божественный объект. Данный антипаттерн говорит, что не следует использовать один механизм для решения несвязанных задач, даже если во всех задачах присутствует слово - безопасность.

Давайте, предварительно, разделим возможные опасности, которые грозят системе, на части:
1. Аппаратный отказ - выход из строя серверов, линий связи, DDoS и т.д. Никакой скрипт вам тут не поможет, нужно либо правильно настраивать сервер, либо обращаться за помощью к администраторам. Об этой проблеме в этой статье мы говорить не будем;
2. Подмена данных - отсутствие верификации (фильтрации) поступающих от пользователя данных, что часто приводит к xss, sql inj и другим не приятным проблемам;
3. Доступ - получение пользователем права выполнять операцию, которую ему выполнять нельзя, на пример изменять пароль другого пользователя;
4. Видимость - данный вид опасности наименее опасный, он скорее служит для улучшения восприятия информации пользователем. Сюда относится показ разным пользователям разной информации, на пример администратор видит рычаги управления системой, а пользователь только упрощенный интерфейс без лишних кнопок.

Последние три проблемы могут и должны быть решены программными средствами. Начнем с начала.

Доступность
Если мы взглянем на клиент-серверную систему (web сайт) как на клиент-серверную систему, то заметим, что здесь участвует два механизма: клиент - который передает данные; и сервер - который эти данные принимает и обрабатывает. Передаваемые от клиента серверу данные назовем командой. Так, команда addUser Baskka 123 переданная на сервер позволяет создать нового пользователя с логином Bashka и паролем 123, а команда removeUser Bashka удалить этого пользователя. Имя команды и передаваемые с ней параметры назовем семантикой команды. Зная семантику команды, мы можем определить данные, которые эта команда должна принимать, и данные, которые эта команда принимать не должна. На пример команда addUser в первом параметре должна принимать логин пользователя, при этом необходимо заранее определить, какие символы могут входить в логин, а какие нет. На пример мы запретим пользователям регистрировать учетные записи с символами, отличными от A-Za-z_0-9. Чем меньше символов могут использовать пользователи (особенно системных), тем защищеннее получится система. Попробуйте вставить sql inj или xss используя только A-Za-z0-9 ;) Назовем доступный набор символов параметра его маской верификации. Верификацией же назовем процесс проверки данных на соответствие маске верификации. Если данные содержат символы, которых нет в маске верификации, верификация считается не пройденной.

Сольем полученное:
1. Команда - обращение к серверу;
2. Параметры команды - передаваемые серверу данные;
3. Маска верификации параметра - символы, которые могут входить в параметр;
4. Верификация параметра - проверка параметра на предмет наличия в нем символов, отсутствующих в маске верификации.

Запишем команду в более расширенном виде:
Login = [A-Za-z_0-9]
Pass = [A-Za-z_0-9*?]
addUser Login Pass
Видно, что команда принимает только определенные данные. Если мы попробуем передать в нее данные недопустимого типа, система вернет ошибку без вреда для себя.

Как реализовать этот подход на практике? В моем случае все реализовано следующим образом:
Существуют классы данных: Integer, String, Float, Alias, FileName, FileAddress и т.д. Каждый класс включает в себя метод isReestablish, который принимает данные и выполняет их верификации в соответствии со своей маской. На пример маской верификации для Boolean является: true|false
Каждый модуль имеет свой контроллер, при чем пользователь может вызывать только методы этих контроллеров. Каждый метод контроллера имеет параметры (как и любой другой метод класса), при чем эти параметры строготипизированы, на пример:
class Controller{
public function addUser(Login $login, Pass $pass){
...
}
}
Когда пользователь передает в систему запрос, он попадает в центральный контроллер. В запросе присутствует следующая информация: имя модуля, имя метода, параметры. Центральный контроллер находит нужный пользователю модуль, получает его контроллер, затем получает метод контроллера и проходя по всем параметрам метода определяет какому типу должны соответствовать данные. Если в процессе этой проверки оказывается, что метод ожидает одни параметры, а пользователь передал другие (путем вызова методы isReestablish), то центральный контроллер возвращает ошибку.

Заметьте, мне не нужно выполнять ручную проверку входящих данные через if и preg_match, все делается автоматически. Так же я могу повторно использовать маски верификации в других контроллерах, а не переписывать код верификации. Безопасность тоже учтена, так как пользователь не сможет передать данные, которые не ожидает метод модуля. Миссия выполнена.

Доступ
Вернемся к клиент-серверной архитектуре. Кашерная (читать - годная) система чаще всего имеет модульную архитектуру (или пакетную), при этом каждый модуль отвечает за свои задачи и обрабатывает свои команды. Очевидно, что не все пользователи должны иметь доступ к имеющимся в системе модулям (как минимум не ко всем командам модуля), потому встает вопрос - как запретить пользователю доступ к модулю?

Существует несколько способов ограничения доступа и все они сводятся к одному - давать или запрещать доступ одному пользователю к одной операции. Выглядит это примерно так: Bashka !addUser,removeUser - пользователю Bashka запрещен доступ к операциям addUser и removeUser. Конечно процесс разграничения доступа пользователям к командам превратиться в муку, если не объединить правила в роли: User !addUser,removeUser; Admin - роли User не доступные команды создания и удаления пользователей, а для админа запретов нет. Теперь достаточно дать определенные роли определенным пользователям и дело сделано! Если пользователь передаст команду серверу, тот должен проверить, разрешена ли обработка команды сервером или установлен запрет.

Как реализовать этот подход на практике? В моем случае все реализовано следующим образом:
Существуют модули Users и Access. Первый отвечает за учет пользователей системы, второй за разграничения прав. Модуль Access включает такие сущности как Role и Rule. Первая определяет роли, которые можно назначить пользователю, вторая права доступа - что пользователю делать нельзя (имя модуля и имя метода, который нельзя вызывать данному пользователю). Так, если мы создадим Rule вида module:Users,action:addUser и расширим ей Role User, то все пользователи с данной ролью не смогут вызывать метод addUser модуля Users.

Центральный контроллер системы (как уже было сказано ранее), получает от пользователя имя модуля и метод, к которому он обращается, но прежде чем передать модулю команду, центральный контроллер проверяет, разрешен ли пользователю доступ к этому модулю (путем проверки его ролей), если доступ закрыт, то возвращается ошибка без вреда для сервера.

Видимость
Что будет, если пользователь откроет сайт и увидит кнопку "Настроить" или "RemoveAllUser"? Думаю он на нее нажмет! Не важно что система никак на это не отреагирует (пользователю ведь не дан доступ к ненужным методам модулей?!), само наличие лишних кнопок напрягает. Потому необходимо реализовать механизм сокрытия ненужных компонентов интерфейса от пользователя. Реализуется это несколькими способами, самый простой из которых - проверять какие роли определены для пользователи и в зависимости от этого скрывать или показывать компоненты экрана. Пользователь конечно сможет окольными путями увидеть запрещенные компоненты, но это никак не навредит системе, так как на уровне сервера доступ к командам пользователю закрыт.

Как реализовать этот подход на практике? К сожалению описать подробно не успею, потому предложите вариант в комментариях ;)

Послесловие
Защищать систему можно многими способами, но запомните - каждая опасность это отдельная задача, которую должен решать свой механизм.
Удачи!
Предисловие
Почему ООП? Думается, для реализации мультиязычности в web ООП подходит отлично. Я думаю все читатели понимают, что под мультиязычностью в данной статье я буду понимать именно локализацию тех или иных компонентов системы, сюда не будет входить информация о создании переводчиков и подобного, это отдельная тема.

Немного теоретических размышлений
Изучая одну открытую платформу, обратил внимание на отличный подход к локализации (переводу) данных и решил "поковыряться поглубже". Если говорить о локализации, стоит выделить три основных типа данных, которые следует локализовать:
1. Сущности - то есть объекты, с которыми мы работаем. Сюда можно отнести такие понятия как login - логин; password - пароль; rule - право доступа и т.д. Другими словами, речь идет о классах и их свойствах, которые фигурируют в системе;
2. Компоненты пользовательского интерфейса - когда работает программист, записи вида msg://helloMessage это приемлемо, но когда за работу берется дизайнер, подобного рода записи, часто необходимые для локализации, начинают сильно мешать. Посмотрите сами, какой из шаблонов страницы лучше:
<div>
<h1>msg://TitleMessage</h1>

<p>%Content%</p>
</div>

или

<div>
<h1 dLocal="text">Hello people!</h1>

<p dInit="contentLoad">Test content. More text...</p>
</div>
Мне нравиться второй вариант. По моему мнению, здесь содержание шаблона отделяется от его форматирования. В первом случае при запуске такой страницы в браузере, мы увидим ненужную для нас, программную информацию, что частенько раздражает дизайнеров, и я их понимаю, мне тоже не хотелось бы задумываться обо всех этих программных примочках, а просто писать дизайн и пример страниц.
3. Динамический, "тяжелый контент" - к примеру сообщения пользователей или новостные сообщения, локализация которых часто очень сложна или занимает много времени (данный тип мы рассматривать не будем).

Все указанные типы данных следует локализовывать отдельно, иначе мы получим тяжеловесные, неуклюжие системы, сопровождать которые будет очень сложно.

Как я это вижу
При написании модулей локализации в своей платформе, я задался вопросом - как это лучше сделать?
Выделил следующие обязательные условия, которым должна соответствовать система локализации:
1. При локализации сущностей, следует беспокоиться о размерах файлов локализации, дабы не перебирать огромные массивы данных с целью поиска нужной нам записи;
2. Не следует смешивать процесс локализации с процессом программирования сущностей. Программисту и так в голове нужно держать целую проектную модель, на кой ему еще и беспокоится о написании локализации? Нужно сделать так, чтобы отдельные люди могли заниматься локализацией, при этом они не обязаны знать программирование!
3. То же самое, что и п. 2, только относительно дизайнеров!
4. Следует обеспечить дизайнерам возможность писать примеры страниц с тестовым содержимым, чтобы заказчик мог оценить результат как он есть, вместо записей на экране вида %contentID%!
5. При работе с пользовательским интерфейсом, вопрос объемов данных для локализации стоит еще более остро чем в п. 1;
6. Сделать все это удобным для работы!

Как видите, условия довольно строгие, но справедливые!

Еще немного теории перед началом
Для того, чтобы вы могли понять подход, применяемый мной для мультиязычности, следует немного сказать о платформе. Платформа имеет модульную структуру, это означает, что ее функциональность зависит от модулей, которые в ней устанавливаются. Так существуют модули Users - пользователи; Access - права доступа; TapeNews - лента новостей и т.д. Каждый модуль включает сущности и логику, строго ограниченную задачами данного модуля.
Помимо модулей, в систему можно устанавливать экраны. Это специальные компоненты, отвечающие за визуализацию модулей. Каждый экран состоит из html, css и js файлов, в первом содержится разметка, во втором дизайн, в третьем логика. Используется PHP (для модулей) и JS (для экранов), а так же объектно-ориентированный подход.
Все что мне нужно, это реализовать локализацию модулей, при чем так, чтобы любой сторонний разработчик мог без проблем использовать ее для своих модулей, а так же реализовать локализацию экранов с теми же условиями.

Локализация модулей
Для понимание принципов локализации модулей рассмотрим пример модуля Access. Данный модуль отвечает за предоставление прав доступа к другим модулям пользователям и имеет следующую структуру:
1. Controller - стандартный программный интерфейс доступа к модулю. Нам он не интересен так как нечего локализовывать;
2. AccessManager, EssenceManager - бизнес-логика модуля. Так же нечего локализовывать;
3. Role - сущность! Представляет роль пользователя типа: админ, модератор, пользователь, гость. Каждому пользователю может быть назначено сколько угодно ролей, они и определяют что пользователь может, а что нет. Есть простор для локализации;
4. Rule - тоже сущность! Право доступа (или точнее не доступа), которое определяет модуль и его метод, к которому не может получить доступ с данным правом доступа. Можно локализовать.

Теперь взглянем на сущности:
class Role{
  /**
   * Наименование роли.
   * @var string
   */
  protected $name;

  /**
   * Множество правил доступа, связанных с данной ролью.
   * @var \PPHP\tools\patterns\database\associations\LongAssociation
   */
  protected $rules;
}
class Rule{
  /**
   * Имя модуля, на которого распространяется правило.
   * @var string
   */
  protected $module;

  /**
   * Имя запрещаемого метода.
   * @var string
   */
  protected $action;
}

Что же тут локализовывать? Представьте что вы админ и вам нужно добавить новую роль в систему, на пример "VIP-пользователь". Перед вами таблица, которая называется Role (а вам бы хотелось Роли), в которой поля key, name и rules (а вам бы хотелось Ключ, Имя роли, Права) и список уже существующих ролей. Есть что локализовать, не правда ли? При чем локализовать нам нужно имя класса и его свойств.

Реализуется это у меня следующим образом. Так как каждый класс находится в отдельном файле, а каждый файл одного модуля в каталоге этого модуля, то системе не составляет проблем найти файл локализации класса, она просто ищет файл, имя которого равно имени класса с суффиксом локали (en, ru и т.д.). В данном файле в виде ini структуры записывается локализация класса. Пример для класса Role:
Role_ru.ini
Role=Роли
key=Ключ
name=Имя роли
rules=Права доступа
Очень маленький и простой файл. Правила локализации модулей поймет даже не программист. Нужно локализовать класс? Создай файл с тем же именем, что и класс, добавь через нижнее подчеркивание имя локали и записывай в файл имя локализуемого свойства и его локализацию через знак равенства. Нужно локализовать имя самой сущности? Пиши имя класса в этот файл и локализуй!
Как же выполняется локализация? Когда мы передаем объект некоторого класса пользователю (на уровень экранов), система в тайне(!) от программистов предварительно ищет файл локализации, считывает из него данные и локализует объект, после чего передает его пользователю, потому на уровне модуля мы работаем с объектами, а экраны получают уже локализованные данные (ведь если пользователь открывает сайт для ru, зачем ему получать от сервера данные на инглише, а потом их локализовывать? Зачем я спрашиваю?!).

Локализация экранов
С локализацией экранов немного сложнее. Говорю я значит своему дизайнеру:
- Хочешь локализовать заголовок страницы, запиши в этот тег msg://имяЗаписи и потом запишешь локализацию этой записи в этот вот файл указав имя записи и ее значение! - а он на меня смотрит и... все!
Понял что дело не ладно и решил, что дизайнеры не должны локализовывать ничего! Пусть пишут по старинке:
<h1>Тестовый заголовок</h1>
<p>Какое то там содержимое. йух!</p>
а система пусть сама все локализовывать при помощи специальных файлов. Заметьте, что чаще всего локализовывать нужно либо:
1. Содержимое тегов;
2. Содержимое атрибутов тегов (как на пример атрибут value у тега <input type="text").

Экраны у нас тоже содержаться в отдельных каталогах экранов, потому файлы локализации можно писать в них и никаких огромных файлов с текстом не нужно (да и искать легко).
Вот пример экрана TapeNews:
TapeNews/browse
browse.html
browse.css
browse.js
Сюда же помещаем файл, имя которого соответствует имени экрана (browse) + суффикс локали (en, ru и т.д.): browse_ru.ini
Теперь дизайнер пишет структуру экрана в html файле:
<div>
<h1>Test</h1>
<p>Какое то там содержимое.</p>
</div>
После добавляем управляющие атрибуты (дизайнеру о них знать и не нужно):
<div>
<h1 dLocal="text">Test</h1>
<p dInit="contentLoader">Какое то там содержимое.</p>
</div>
dLocal - следует локализовать содержимое тега (то есть текст "Test");
dInit - следует обратиться к методу contentLoader контроллера экрана, и получить от него нужные данные для тега (уже локализованные).

Теперь остается записать в файл локализации следующее:
Test=Тестовый заголовок
Все! Теперь перед показом экрана пользователю, система автоматически выберет все теги с атрибутом dLocal, найдет файлы локализации, переведет данные и покажет пользователю результат.

Делов то?!
Краткая модель уровневой мультиязычности мной расписана. Думаю получилось довольно просто и понятно, а главное удобно! Заниматься локализацией могут даже отдельные специалисты, которым не нужно углубляться не в программирование, не в верстку и дизайн.
Удачи в программировании!
высмотрел в DCMS 7 легонький шаблонизатор и немножко его изменил под себя:
<?php
define('BASE_DIR', $_SERVER["DOCUMENT_ROOT"]);
/**
* Класс шаблонизатора 
*/

class template {
	/*
	* $tpl_dir папка шаблонов (по умолчанию /tpl/)
	* $tpl_ras расширение шаблонов (по умолчанию .php)
	* $tpl_cache кеш шаблона
	* $tpl_var_value массив со значениями и переменными которые мы будем передавать шаблону
	*/
	public $tpl_dir = '/tpl/';
	public $tpl_ras = '.php';
	protected $tpl_cache = null;
	protected $tpl_var_value = array();
	
	/*
	* $name название переменной которой задаем значения(е)
	* $value значения(е) которые мы задаем переменной
	*/
	public function assign($name, $value = null) {
		if (is_array($name)) {
      foreach ($name as $key => $value) {
        $this->assign($key, $value);
      }
    return;
    }
    $this->tpl_var_value[$name] = $value;
  }
	
	/*
	* $tpl_name имя загружаемого шаблона
	*/
	public function display($tpl_name) {
		if (file_exists(BASE_DIR . $this->tpl_dir . $tpl_name . $this->tpl_ras)) {
			$this->tpl_cache = file_get_contents(BASE_DIR . $this->tpl_dir . $tpl_name . $this->tpl_ras);
			ob_start();
			extract($this->tpl_var_value);
			eval('?>' . $this->tpl_cache);
			return ob_get_contents();
		} else echo 'Шаблон '. $tpl_name .' не найден в '. $this->tpl_dir;
	}
	
}
?>

использованние:

<?php

 $doc = new template();
$el = array();
$el[] = array('a' => 'тестовое значение 1');
$doc->assign('el', $el);
$doc->assign('aa', 'тестовое значение 2');
$doc->display('test.tpl');

?>

содержание test.tpl.php:

<?
echo $aa;
?>
<br />
<?
foreach ($el AS $element) {
 echo $element['a']; 
 }
 ?>

ЗЫ ООП еще только осваиваю)))
Немного теории
Абстракция в ООП тесно связана с такими важными характеристиками, как наследование и полиморфизм. В частности, к абстракции прибегают с целью упрощения объекта, выделения общих, важных в данном контексте закономерностей и отвлечения от несущественных деталей.
Во время проектирования класса, часто можно столкнуться с необходимостью вынесения общей структуры нескольких классов в один общий, но не законченный для полноценного использования класс. Для этих целей хорошо подойдет абстрактный класс, который будет содержать некоторую общую структуру (свойства и методы), но не достаточную, для его полноценного использования (инстанциирования).

Абстракция не только позволяет выделить общие части структуры классов в родительский объект, но и обеспечить дальнейшую переносимость за счет полиморфизма. Если некоторые классы представляют одну и ту же сущность, то наследование их от абстрактного класса позволяет задать эту ассоциацию программно. Такой подход позволит заменить один конкретный класс другим, не изменяя код. Часто это применяется в том случае, когда необходимо работать с общей структурой, а их частная часть не требуется.

Немного практики
В качестве примера обоих типов абстрагирования, приведу следующую задачу:
Задача: Реализовать классы шахматных фигур для игры в шахматы.
Предметная область: Движение шахматные фигуры подчиняются правилам шахматной игры. В частности отличительными характеристиками является форма движения и число доступных ходов. Среди шахматных фигур нет таких, которые не могут ходить в игре, т.е. форма движения и число доступных ходов - это свойства, которые есть у всех шахматных фигур, различны только значения.
Реализация:
Данная реализация упрощена для примера.
Как можно заметить из предметной области, все классы шахматных фигур имеют общие корни в виде абстрактного понятия - шахматная фигура. Для программирования этой ассоциации достаточно реализовать абстрактный класс, который будет являться родительским, по отношению ко всем фигурам.
<?php
// Инстанциирование (создание экземпляра) класса "Шахматная фигура" не имеет смысла, потому он объявлен как абстрактный
// Вынесение общей структуры в абстрактный класс
abstract class Chessman{
  // Метод служит для изменения положения шахматной фигуры. В частности он заставляет шахматную фигуру "пойти" из текущей клетки доски, в заданную клетку $cell
  // Метод объявлен как абстрактный, потому что его реализация зависит от конкретной шахматной фигуры, то есть тело метода на данном уровне абстракции определить невозможно
  public abstract function move(Cell $cell);

  // Метод определяет, доступно ли движение на клетку $cell для данной фигуры
  public abstract function isMovementAvailable(Cell $cell);
}

class Horse extends Chessman{
  public function move(Cell $cell){
    // Реализация движения фигуры "Конь"
  }

  public function isMovementAvailable(Cell $cell){
    ...
  } 
}

// Любой метод или функция теперь может работать не с конкретными фигурами, а их общей часть - классом Chessman
// Обеспечение полиморфности
function isMovementAvailable(Chessman $chessman, Cell $cell){
  // Не важно какая именно фигура передана, так как все дочерние классы Chessman реализуют метод isMovementAvailable
  return $chessman->isMovementAvailable($cell);
}
[/color]

[color=#0000ff]Введение
Как часто вас преследует чувство дежавю в программировании? Многие программисты не догадываются насколько важно это чувство. Оно свидетельствует о том, что пришло время обобщать классы для повторного использования кода!
Толчком, побудившим меня к написанию этой статьи, стала сегодняшняя ночь, а именно то самое чувство.

Предыстория
Несколько недель назад мне понадобилось написать простенький модуль аутентификации клиента. Я не стал использовать сложные алгоритмы безопасности и включать множество полей, таких как IP или логин пользователя, просто в этом не было никакой необходимости. Все клиенты делились на: неавторизированных пользователей и администраторов - а значит, достаточно было
использовать только два поля: идентификатор и пароль. Когда администратор регистрируется, он передает в систему пароль, на что система отвечает ему идентификатором. В будущем администратору необходимо ввести идентификатор и пароль, что позволит ему войти в систему и получить необходимые полномочия.
Архитектура, как и логика модуля, проста до невозможности, и я даже представить не мог, что ее можно еще больше упростить.

Первые шаги
И так я взялся за дело. Сначала реализовал сущность, представляющую пользователя. Все очень просто и лаконично:
<?php
calss User extends LongObject{
  protected $pass;

  function getPass(){...}
  function setPass($pass){...}
}
Родительский класс LongObject используется здесь потому, что все его дочерние классы могут быть сохранены в БД. Этот класс и содержит идентификатор OID.
Менеджер, содержащий логику аутентификации пользователя, был несколько сложнее, но по сравнению с другими менеджерами системы, он был тривиален. Приведу его
краткий интерфейс:
<?php
interface AuthManager{
  /* Метод возвращает объект User, восстановленный из сессии. 
То есть, если пользователь был уже аутентифицирован, 
то метод вернет его состояние*/
  function identify();
  // Метод выполняется при отключении администратора от системы
  function closeSession();
  // Метод регистрирует нового администратора, возвращая ему идентификатор
  function register();
  /* Метод пытается аутентифицировать клиента по полученной 
от него ключевой паре - идентификатору и паролю */
  function authenticate($OID, $pass);
}
Все предельно просто. За пару часов я полностью реализовал модуль, написал под него тесты (в действительности на написание тестов ушло больше времени, чем на разработку и реализацию модуля D ), документацию и интерфейс. Все работало прекрасно.

Дежавю
Сегодня ночью, скачивая кое-что из известного файлового обменника, я задумался о механизме, позволяющем предоставлять клиентам файлы только по паролю. Мне стало интересно, как это проще всего реализовать, и тут ба-бах - Дежавю! Я понял, что написанный мною ранее модуль аутентификации использует тот же механизм!
Посмотрим внимательно на механизм ограничения доступа к файлам по паролю: доступ к файлу предоставляется только тогда, когда клиент предоставит как информацию о запрашиваемом файле, так и пароль к этому файлу. Модуль аутентификации работает аналогично: доступ к сессии предоставляется только тогда, когда клиент предоставит как информацию о сессии, так и пароль к ней. Замечаете связь?
В эту ночь у меня не было никаких причин переписывать модуль аутентификации, но программист во мне требовал чистоты кода! Тогда я решил прибегнуть к "Обобщению классов".

Обобщение
Для начала я выделил все то общее, что было между моим модулем и механизмом файлового обменника:
1. Идентификатор;
2. Пароль;
3. Доступ только при правильной ключевой паре (идентификатор + пароль).
Казалось бы, что загвоздка в идентификаторе, в моем случае это ID пользователя, а в случае файла, может показаться, что его физический адрес, но это не так! Достаточно вспомнить, что в ООП все является объектом, как становится ясно, что идентификатором файла может являться не только физический адрес, но и ID, как у пользователя:
<?php
class File extends LongObject{
protected $address;
...
}
То есть поиск в БД будет произведен по OID, а после восстановления файла из БД можно найти его по физическому адресу ($address).
Теперь, чтобы обобщить общее этих двух механизмов, мне достаточно использовать один-два класса, инкапсулирующих те общие части, что присутствуют в обоих механизмах. Отмечу сразу, что модуль аутентификации у меня отличается от механизма ограничения доступа к файлам тем, что он добавляет логику работы с сессией, для сохранения состояния сеанса после аутентификации пользователя.
Пришло время обобщать!
<?php
/*
 Класс представляет аутентифицируемую сущность. Все дочерние классы могут быть  
восстановлены из БД только если клиент предоставит правильный идентификатор и  
пароль.
Так как все аутентифицируемые сущности работают с базой данных, правильным  
решением было вынести уровень наследования от LongObject в данный класс.
*/
abstract class AuthenticatedEntity extends LongObject{
  protected $password; // Общее для всех сущностей свойство - пароль

  // Setters и Getters 
  ...
}
Так же можно обобщить и менеджер, реализующий логику аутентификации:
<?php
class AuthenticationManager implements Singleton{
  protected $db; // Интерфейс взаимодействия с БД
  // Всяческие вспомогательные методы
  ...

  /* Общий метод, позволяющий определить, верна ли ключевая пара
     Если ключевая пара верна, сущность автоматически восстанавливается из БД
     а метод возвращает true */
  public function authenticate(AuthenticatedEntity &$entity){
    try{
      // Попытка восстановления сущности из БД по ключевой паре
      $this->dataMapper->recoverFinding($entity, ['OID' => $OID, 'password' =>  
$password]);
      // Если исключений не выброшено, значит, аутентификация успешна
      return true;
    }
    /* Выброс данного исключения свидетельствует о том, что сущность не  
восстановлена, а значит, аутентификация не пройдена */
    catch(UncertaintyException  
$e){
      return false;
    }
  }
}
Как видно, классы получились довольно общими, и использовать их без наследования и частных сущностей не представляется возможным, но зато они позволяют повторно использовать код. Ниже пример обновленного модуля аутентификации с использованием обобщенных классов:
<?php
/*
  Наследование свидетельствует о том, что User может восстановить свое  
состояние из БД только при правильной ключевой паре
*/
class User extends AuthenticatedEntity{
}
Как видно, все внутренности User наследуются от AuthenticatedEntity.
Запомните, часто класс может вообще не иметь тела, но само его наличие создает в системе новую сущность.
<?php
class SessionManager implements Singleton{
  /* Класс использует обобщенный менеджер аутентификация для делегирования ему  
части обязанностей */
  protected $authManager;
  /* Класс использует менеджер сессий для делегирования ему части обязанностей */
  protected $sessionProvider;

  function identify(){}

  function closeSession(){}

  function register(){}

  function authenticate($OID, $pass){
$user = new User();
    $user->setOID($OID);
    $user->setPassword($password);
    // Использование обобщенного менеджера аутентификации
    if($this->authManager->authenticate($user)){
      // Аутентификация успешна
      // Запись данных в сессию
      $this->sessionProvider->start();
$this->sessionProvider->set('SessionManager::OID',  
$user->getOID());
      return $user;
    }
    else{
      // Аутентификация неудачна
      throw new  
AuthentifyException( 
'Аутентификация не пройдена.', 1);
    }
  }
}
Как видно, интерфейс класса совершенно не поменялся, но теперь часть его логики вынесена в обобщенный менеджер аутентификации, и все схожие классы, такие как менеджер доступа к файлам, могут использовать тот же принцип без дублирования кода!

Заключение
При использовании ООП, важно помнить одно золотое правило - если вам кажется, что этот код вы уже когда то писали - настало время обобщать!
Облако тегов / Авторы