Примеры шаблонов проектирования или как написать свой PHP framework. Часть 4: Итератор

В этой короткой статье я опишу реализацию шаблона проектирования итератор (Iterator). Как я и обещал, мы рассмотри класс ArrayList, упомянутый в прошлой статье. Давайте сначала рассмотрим некоторые особенности класса ArrayList, а потом я расскажу о реализации паттерна итератор.

Зачем нужен класс ArrayList? Многие умные дядьки скажут, что он и нафиг не нужен, и, и без этого можно обойтись. Конечно можно, но некоторым, таким как я, это вполне может показаться удобным на столько, что данный функционал можно просто использовать. Но не будем тратить лишние символы, которые и так на данный момент не сжимаются ни каким gzip-ом или чем-то типа того. Первое и, я считаю, главное, что делает данный класс - это нормальный обход генерации ошибки уровня E_NOTICE, при доступе к неинициализированному элементу массива. То есть, когда мы делаем так:


Listing №1 (PHP)

<?php
error_reporting(E_ALL);
$ar = array();
echo $ar[0]
?>


мы получаем ошибку вида:

результат
Notice: Undefined offset: 0 in <имя файла>

Чтобы этого не происходило, мы можем либо подавить возможную ошибку знаком @ либо проверить индекс массива. Или вообще не обращать внимания на ошибки уровня E_NOTICE. Но так как последнее считается плохой практикой, как, по моему мнению, также и собака, то остается лишь проверять индекс массива с помощью isset(). Я предлагаю спрятать это в функционале нашего ArrayList. Фактически ArrayList будет тем же самым обычным php-массивом, но с встроенной проверкой на присутствие индекса. Также вместо доступа к массиву через квадратные скобки, как то [идетификатор ключа], я предлагаю осуществлять доступ через магические методы __get() и __set(). То есть работа с массивом будет осуществляться через оболочку объекта. Следующий пример показывает эквивалентные обращения к стандартному массиву и к объектному массиву ArrayList:


Listing №2 (PHP)
//инициализация ссылки
$NativeArray = array();
$ObjectArray = new ArrayList();
//запись элемента
$NativeArray['Name'] = 'Vova';
$ObjectArray->Name = 'Vova';
//чтение элемента
if(isset($NativeArray['Name'])) {
  echo $NativeArray['Name'];
}
echo $ObjectArray->Name;


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


Listing №3 (PHP)
class ArrayList {
  /**
   * @var array
   */
  private $_Items;
 
  public function __construct()
  {
    $this->_Items = array();
  }
  /**
   * @param string $VarName
   * @return mixed
   */
  public function __get($VarName)
  {
    if(isset($this->_Items[$VarName])) {
      return $this->_Items[$VarName];
    }
    else {
      return NULL;
    }
  }
  /**
   * @param string $VarName
   * @param mixed $VarValue
   */
  public function __set($VarName, $VarValue)
  {
    $this->_Items[$VarName] = $VarValue;
  }


Теперь, если запрошенный элемент не присутствует в нашем ArrayList, то будет просто возвращен NULL без всяких генераций E_NOTICE. При этом не надо писать скобки и кавычки. Конечно массивы в php это очень мощный механизм и, я думаю, глупо бы было даже думать о реализации в нашем объекте их полного функционала. Поэтому, для класса ArrayList я предпочту сделать поддержку еще пока только одной операции - циклический проход по элементам ArrayList.

В этом месте мы плавно перешли к шаблону проектирования итератор (Iterator), суть которого и заключается в предоставлении последовательного доступа к каким-то данным сущности. Классически различают итератор и агрегатор итератора. Агрегатор и является той сущностью, обход элементов которой предоставляется итератором. То есть, попросту говоря, классически шаблон проектирования итератор формируется двумя объектами: объектом-агрегатором, данные которого необходимо циклически обойти, и самим объектом-итератором, который позволяет выполнить такой обход. В данном случае обычно объект-агрегатор предоставляет управляющей конструкции (циклу) доступ к своему объекту-итератору. Как же это реализовать в PHP?

Нам, можно сказать, повезло, что PHP 5 изначально скомпилирован с поддержкой интерфейса итератора (Iterator) и агрегатора (IteratorAggregate). Вам остается лишь указать, что объект реализует данный интерфейс и реализовать непосредственно его методы. Причем для обхода элементов в цикле foreach достаточно реализовать любой из интерфейсов. Если вы реализуете интерфейс агрегатора, то вам нужно будет реализовать один метод: getIterator(), который должен возвратить объект, реализующий интерфейс Iterator. Но зачем это делать, если объект может сразу реализовать интерфейс Iterator? Дело в том, что классы обычно служат для представления данных и методов взаимодействия с этими данными. Поэтому реализация интерфейса Iterator может просто не вписываться в основную функциональность класса. Кроме этого, во время итераций по объекту сведения об итерации (текущая позиция) сохраняются в самом объекте, что иногда делает невозможным организацию вложенных итераций. По этим причинам лучше возложить функциональность интерфейса Iterator на отдельный класс, а в основном классе реализовать интерфейс IteratorAggregate. Так как эти две причины практически не относятся к классу ArrayList, то мы просто реализуем в нем интерфейс Iterator.

Интерфейс итератора в PHP описывается так:


Listing №4 (PHP)
Iterator extends Traversable {
/* Methods */
  abstract public mixed current ( void )
  abstract public scalar key ( void )
  abstract public void next ( void )
  abstract public void rewind ( void )
  abstract public boolean valid ( void )
}


Таким образом, наш ArrayList должен реализовать пять методов:


Listing №5 (PHP)
class ArrayList implements Iterator {
  /**
   * @see Iterator
   */
  public function rewind()
  {
    reset($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function current()
  {
    return current($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function key()
  {
    return key($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function next()
  {
    return next($this->_Items);
  }
  /**
   * @see Iterator
   */
  public function valid()
  {
    return ($this->current() !== FALSE);
  }


Тогда мы можем так же, как и обычный массив, передать его в цикл foreach:


Listing №6 (PHP)
$Errors = new ArrayList();
$Errors->LoginError = 'Login is empty';
$Errors->PasswordError = 'Password is small';
foreach($Errors as $key=>$value) {
  echo "$key: \"$value\"<br>";
}


Тут можно подумать, почему просто нельзя реализовать интерфейс IteratorAggregate и вернуть массив Items:


Listing №7 (PHP)
class ArrayList implements IteratorAggregate {
 
  public function getIterator()
  {
    return $this->_Items;
  }


Этот код не будет работать, потому что обычный массив не является объектом, реализующим интерфейс Iterator, а конструкция foreach, при передачи ей объекта интерфейса IteratorAggregate, ожидает от его функции getIterator() именно объект, реализующий интерфейс Iterator. Если вы хотите сделать таким образом, то вам необходимо будет преобразовать массив в объект-итератор. Для этого вам поможет встроенный класс ArrayObject, который превращает массив в объект-итератор (и не только):


Listing №8 (PHP)
class ArrayList implements IteratorAggregate {
 
  public function getIterator()
  {
    return new ArrayIterator($this->_Items);
  }


Или же вам придется самим вызвать getIterator() или какой-то метод, возвращающий массив Items в коде foreach. Я, пожалуй, остановлюсь на варианте с реализацией Iterator. Помимо этого, давайте реализуем еще встроенный интерфейс Countable, который позволяет функции count подсчитывать количество элементов в объекте-реализаторе:


Listing №9 (PHP)
class ArrayList implements Iterator, Countable {
 
  /**
   * @see Countable
   */
  public function Count()
  {
    return count($this->_Items);
  }


Теперь мы можем спокойно вычислить количество элементов ArrayList:


Listing №10 (PHP)
$Errors = new ArrayList();
$Errors->LoginError = 'Login is empty';
$Errors->PasswordError = 'Password is small';
foreach($Errors as $key=>$value) {
  echo "$key: \"$value\"<br>";
}
echo 'Total errors: ' . count($Errors);


На этом данную тему итераторов прикрываю. Продолжение следует...


зы: а вообще всё это может делать встроенный ArrayObject

Добавлено: 08 Августа 2018 07:56:54 Добавил: Андрей Ковальчук

Примеры шаблонов проектирования или как написать свой PHP framework. Часть 3: Контроллер

Написать свой PHP framework становиться всё проще и проще, ведь я публикую очередную статейку об этом. Сейчас мы поговорим об архитектуре web-приложений, а точнее об одном из основных аспектов организации обработок команд пользователя. Конечно же, я буду обговаривать некоторые шаблоны проектирования. Здесь мы затронем такие паттерны как модель-представление-контроллер (Model View Controller), контроллер страниц (Page Controller), контроллер запросов (Front Controller), команда (Command), сценарий транзакции (Transaction Script) и некоторые другие.

Итак, с чего начнем? А начнем мы с того, что все только и твердят об MVC (Model View Controller) хотя многие понятия не имеют, что это такое и как это работает. Не смотря на то, знаете ли вы или нет, что скрывается за этими словами, я все же попытаюсь кратко объяснить суть, так как на основе последней будет строиться дальнейшее изложение. Конечно же, в нашем контексте я говорю, что MVC представляет собой шаблон проектирования. Но так как шаблоны проектирования как таковые могут абстрагировать довольно различные по охвату мысли и масштабу реализации какого-то функционала, то сказать, что MVC просто шаблон проектирования равносильно тому, что говорить о компьютере, как о простом наборе микросхем. Паттерн MVC не просто типовое решение - он скрывает в себе один из фундаментальных принципов проектирования программного обеспечения - разделение основных частей приложения, благодаря которому приложение легче адаптируется, масштабируется, тестируется, сопровождается и конечно же реализуется. Разделение происходит именно в тех местах, где это наиболее выгодно для всех только что перечисленных действий. Конкретно происходит отделение интерфейсной части приложения от логической. В нашем же случае - случае web-приложения, происходит разделение последнего на три составляющие: модель, представление и контроллер. Модель представляет собой бизнес-логику приложения. Представление характеризуется внешним видом, которое непосредственно наблюдается пользователем. Ну а контроллер управляет всем этим делом. Сегодня речь пойдет о контроллере и вариантах его реализации, на одном из которых мы и остановимся.

На данный момент в 90 процентах случаев взаимодействие пользователя с web-приложением проходит посредством переходов по ссылкам. Посмотрите сейчас на адресную строку браузера - по этой ссылке вы получили этот текст. По другим ссылкам, например, находящимся справа на этой странице, вы получите другое содержимое. Таким образом, значение ссылки - комбинация символов в ней - определяет конкретную команду web-приложению. Надеюсь, вы уже успели заметить, что у разных сайтов могут быть совершенные разные форматы построения адресной строки. Где-то можно выделить явные названия файлов, где-то нельзя, а где-то - вообще ничего вроде и не меняется. Каждый формат может отображать архитектуру web-приложения. Хотя это и не всегда так, но в большинстве случаев это явный факт. К чему я веду? Я просто хочу показать на примере адресной строки различие двух подходов в реализации (сразу скажем это слово) контроллера. Допустим, имеется два варианта адресной строки, по которым показывается какой-то текст:


http://www.domain1.com/article.php?id=3
http://www.domain2.com/index.php?article=3
Соответственно имеются также два варианта для показа профиля пользователя:


http://www.domain1.com/user.php?id=4
http://www.domain2.com/index.php?user=4
Как видно из примеров, логика обращения к web-приложению является разной. Для сайта domain1.com каждый сценарий отвечает за выполнение определённой команды, а для сайта domain2.com все обращения происходят в одном сценарии index.php. То есть для первого случая каждая страница сервера выполняет конкретное действие на сайте, а для второго - все действия реализуются как бы одним скриптом. За реальными примерам далеко ходить не надо. Подход с множеством точек взаимодействия вы можете наблюдать на любом форуме с движком phpBB (например, www.php.ru/forum): просмотр форума происходит через сценарий viewforum.php, просмотр топика через viewtopic.php и т. д. Второй же подход, с доступом через один физический файл сценария, можно наблюдать на этом сайте: все обращения проходят через index.php. Чтобы просмотреть статью нужен адрес http://itdumka.com.ua/index.php?cmd=shownode&node=1, а чтобы, допустим, зарегистрироваться нужно пройти по ссылке http://itdumka.com.ua/index.php?cmd=showregisterform. Как видите, это совершенно два различных подхода, которые, как это ни странно, находят отражение в контексте наших с вами шаблонов проектирования. Первый подход характерен для шаблона контроллер страниц (Page Controller), а второй подход реализуется паттерном контроллер запросов (Front Controller).

Контроллер страниц хорошо применять для сайтов с достаточно простой логикой. Форум phpBB имеет очень простую логику, поэтому контроллер страниц хорошо подошел для его реализации. В свою очередь, контроллер запросов объединяет все действия по обработке запросов в одном месте, что даёт ему дополнительные возможности, благодаря которым можно реализовать более трудные задачи, чем обычно решаются контроллером страниц. Я не буду вдаваться в подробности реализации контроллера страниц, а скажу лишь, что в рамках нашей задачи в роли контроллера будет разработан именно контроллер запросов. Мы же делаем фреймоврк, который должен решать большинство задач, поэтому контроллер страниц, к сожалению, тут не уместен. Но, я думаю, мы еще затронем его.

Вернемся немного к архитектуре MVC. Что же будет делать наш контроллер запросов? В первую очередь, он будет просматривать параметры запроса пользователя, и исходя из их значений, обращаться к соответствующим моделям приложения. Существуют разные способы реализации контроллера запросов, но все они основаны на схеме, в которой взаимодействуют две сущности: обработчик и действие. Обработчик извлекает из URL необходимую информацию, после чего решает, какое действие необходимо инициировать. После выбора конкретного действия он передает ему управление. То есть, в большинстве случаев обработчик является довольно простой программой, функции которой заключаются в выборе нужного действия. Действие представляет собой чистой воды шаблон проектирования, реализуемый в виде сценария транзакции (Transaction Script) или команды (Command).

В чем отличие между сценарием транзакции (Transaction Script) и командой (Command)? Эти два типовых решения практически одинаковы и решают одну и ту же задачу - обеспечить "логическую изоляцию" логики работы приложения по процедурам. Но, в связи с различной структурой реализации, эти типовые решения начинают приобретать различные свойства, обеспечивающие им довольно сильные отличия друг от друга, которые способствуют различным вариантам применения. Фактически, происходит реализация логически завершенных процедур, но конструктивно разными способами. Сценарий транзакций подразумевает расположения каждого логического действия в отдельной процедуре, которые могут существовать как разрознено, так и в пределах одного класса. Это уже решает сам разработчик исходя из своих собственных убеждений. Можно объединить в одном классе сценарии транзакции, реализующие более родственные операции. Но это, еще раз повторюсь, решает конкретный разработчик. В рамках контроллера запросов web-приложения я обычно не решаю такие вопросы, а использую шаблон проектирования команда. В отличие от сценария транзакции каждая логическая процедура реализуется отдельным классом, в котором присутствует полиморфный метод запуска процедуры. Этот метод объявлен в абстрактном классе и реализуется, в наследниках:


Listing №1 (PHP)

/**
 * Абстрактное действие
 */
abstract class Action {
  /**
   * Запускает действие
   * с определенными параметрами
   * @param ArrayList $Params
   */
  abstract function Run(ArrayList $Params);
}


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


Listing №2 (PHP)
/**
 * Объявляем класс действия
 * наследуемого от Action
 */
class regAction extends Action {
  /**
   * Реализуем логику модели в методе Run
   * @param ArrayList $Params
   * @return mixed
   */
  public function Run(ArrayList $Params)
  {
    // Создаем контейнер ошибок
    $Errors = new ArrayList();
    // Проверяем новые логин и пароль
    $Errors->LoginError = User::TestLogin(Request::Post('login'));
    if(!count($Errors) && User::Exists('Login', Request::Post('login'))) {
      $Errors->LoginError = 'Такой логин уже используется';
    }
    $Errors->PasswordError = User::TestPswd(Request::Post('password'));
    // Если нет ошибок создаём нового пользователя
    if(!count($Errors)) {
      $User = new User();
      $User->Login = Request::Post('login');
      $User->Password = Request::Post('password');
      // Пытаемся сохранить его
      if ($User->Save()) {
        // Если все прошло удачно,
        // то возвращаем сообщение об
        // успешной регистрации
        $View = new View();
        $View->SetTemplete('message');
        $View->Message = $User->Login . ' зарегистрирован';
        return $View;
      }
      else {
        // Возвращаем ошибку если
        // не можем сохранить пользователя
        $Errors->UnknownError = 'Ошибка регистрации';
        return $Errors;
      }
    }
    else {
      // Возвращаем ошибки входных данных
      return $Errors;
    }
  }
}


Как видите все очень просто. Класс конкретного действия, один метод - и обработчик готов. Данный паттерн называют по-разному - команда (Сommand), действие (Action) и иногда путают со сценарием транзакции (Transaction Script). Чаще всего я встречал название "команда" хотя, как видите, употребляю также слово "действие". Это происходит потому, что реализация самого шаблона проектирования чаще связана со слово Action. Думаю, ничего страшного не будет, если я буду использовать оба названия.

Что же такого хорошего в команде? Применяя это типовое решение, мы получаем следующие результаты:

Команда разрывает связь между объектом, инициирующим операцию, и объектом, имеющим информацию о том, как ее выполнить. То есть наш контроллер запросов будет как бы выполнять команды и ничего не знать о том, что конкретно в них происходит. Это позволяет добиваться высокой гибкости и динамически подменять команды
Команды - это самые настоящие объекты. Мы можем манипулировать ими и расширять точно так же, как в случае с любыми другими объектами
Из простых команд можно собирать составные
Добавлять новые команды очень легко, поскольку никакие существующие классы изменять не нужно. Необходимо только создать новый класс конкретной команды, реализовав в методе запуска соответствующую логику
Для завершения реализации контроллера запросов осталось дописать обработчик. Напомню, что он всего лишь должен выбрать нужную команду и запустить её. Тут стоит уточнить, что под словом "выбрать" подразумевается создание объекта команды, более модными словами - инстанцирование. Обработчик не должен знать, какой именно экземпляр команды он создаёт, но он точно знает название метода запуска команды. Ведь оно диктуется абстрактным классом, в нашем случае классом Action.

Итак, обработчик, назовем его диспетчер (Dispatcher), будет принимать на вход имя команды, которое будет совпадать с частью имени класса команды в нижнем регистре. К этой части имени мы прибавим строку 'Action' для констатации класса как класса команды (действия). Пример вы видели в листинге 2. Далее осуществляется полет фантазий и каждый летает, как хочет. Конечно же, более умные люди, скорее всего, опираются на задачу, но мы полетим довольно простым способом. Мы можем разделить команды по модулям, привязав название модулей к соответствующим директориям. В принципе, это довольно распространенный подход. Он осуществляется в основном с целью расположить классы команд более удобным способом и таким образом формировать непосредственно модули самого приложения. Таким образом, путь к классу конкретной команды будет выглядеть следующим образом:

Формат пути к классу команды:
/корневой путь/модуль родственных команд/конкретнаяКоманда.php
Как видно у нас будет всегда один и тот же корневой путь. Наверно мы будем по традиции инициализировать наш диспетчер им:


Listing №3 (PHP)
class Dispatcher {
  /**
   * Корневой путь
   * к модулям
   * @static
   * @var string
   */
  private static $_Path = '';
  /**
   * Задает путь к модулям приложения
   * @static
   * @param string $Path
   */
  public static function Init($Path)
  {
    self::$_Path = $Path;
  }


Теперь реализуем два основных метода. Один будет создавать конкретный экземпляр команды, а второй запускать команду на исполнение:


Listing №4 (PHP)
  /**
  * Возвращает инстанцированный
  * объект команды
  * @param string $ActionName
  * @param string $ModuleName
  * @return Action
  */
  private function _CreateAction($ActionName, $ModuleName)
  {
    //Формируем имя и путь подключаемого файла с классом команды
    $FileName = self::$_Path . $ModuleName . DIRECTORY_SEPARATOR .
                $ActionName . 'Action' . PHP_EXT;
    if(is_file($FileName)) {
      require_once $FileName;
      $ActionClassName = $ActionName . 'Action';
      return new $ActionClassName();
    }
    else {
      //Генерируем исключение если файл не найден
      throw new Exception('Action file "' . $FileName . '" does not exists');
    }
  }
  /**
  * Запускает команду и
  * возвращает результат ее действия
  * @param Action $Action
  * @param ArrayList $Params
  * @return mixed
  */
  private function _RunAction(Action $Action, ArrayList $Params)
  {
    return $Action->Run($Params);
  }


Как видим, последний метод не пропустит ничего кроме класса с типом Action, что декларирует обязательную реализацию интерфейса объектов команд через их абстрактного предка.

Но что делать, если пользователь будет запускать всё что не лень? Для ограничения его возможностей мы будет регистрировать в диспетчере те команды, которые может выполнить данный пользователь. Дописываем оставшиеся методы:


Listing №5 (PHP)
class Dispatcher {
  /**
   * Зарегистрированные
   * команды
   * @var array
   */
  private $_Actions;
 
  public function __construct()
  {
    $this->_Actions = array();
  }
  /**
   * Регистрирует команду модуля
   * @param string $ActionName
   * @param string $ModuleName
   */
  public function RegisterAction($ActionName, $ModuleName)
  {
    //Приводим переменные к нижнему регистру
    $ActionName = Str::Strtolower($ActionName);
    $ModuleName = Str::Strtolower($ModuleName);
    $this->_Actions[$ModuleName . ' ' . $ActionName] = TRUE;
  }
  /**
   * Возвращает TRUE если команда зарегистрирована
   * @param string $ActionName
   * @param string $ModuleName
   * @param bool $InDefault FALSE
   * @return bool
   */
  public function IsActionRegistered($ActionName, $ModuleName)
  {
    //Приводим переменные к нижнему регистру
    $ActionName = Str::Strtolower($ActionName);
    $ModuleName = Str::Strtolower($ModuleName);
    return (isset($this->_Actions[$ModuleName . ' ' . $ActionName]));
  }
  /**
   * Возвращает результат выполнения команды
   * или NULL если команда не зарегистрирована
   * @param string $ActionName
   * @param string $ModuleName
   * @param ArrayList|NULL $Params
   * @return mixed
   */
  public function GetResponse($ActionName, $ModuleName, $Params = NULL)
  {
    //Создаем пустые параметры команды если они не заданы
    if(!$Params) {
      $Params = new ArrayList();
    }
    //Приводим переменные к нижнему регистру
    $ActionName = Str::Strtolower($ActionName);
    $ModuleName = Str::Strtolower($ModuleName);
    if($this->IsActionRegistered($ActionName, $ModuleName)) {
      //Создаем неизвестный экземпляр команды
      $Action = $this->_CreateAction($ActionName, $ModuleName);
      //Запускаем команду и возвращаем результат
      return $this->_RunAction($Action, $Params);
    }
    else {
      return NULL;
    }
  }


Я думаю вы со мной согласитесь, что как то глупо просто возвращать NULL если сценарий запустился без параметров. Давайте реализуем запуск команды по умолчанию. Она будет запускаться для конкретного модуля. Если эта команда не будет найдена, то будем перенаправлять пользователя по адресу, заданному при создании диспетчера. Для этого изменим наш код. Чтобы не путаться я привожу окончательный вариант класса диспетчера:


Listing №6 (PHP)
class Dispatcher {
  /**
   * Корневой путь
   * к модулям
   * @static
   * @var string
   */
  private static $_Path = '';
  /**
   * Зарегистрированные
   * команды
   * @var array
   */
  private $_Actions;
  /**
   * Команды по умолчанию
   * @var array
   */
  private $_Default;
  /**
   * Модуль по умолчанию
   * @var string
   */
  private $_DefModule;
  /**
   * Адрес редиректа
   * @var string
   */
  private $_RedirectUrl;
  /**
   * Задает путь к модулям приложения
   * @static
   * @param string $Path
   */
  public static function Init($Path)
  {
    self::$_Path = $Path;
  }
  /**
   * @param string $DefModule
   * @param string $RedirectUrl
   */
  public function __construct($DefModule, $RedirectUrl = '/')
  {
    $this->_Actions = array();
    $this->_Default = array();
    $this->_RedirectUrl = Str::Strtolower($RedirectUrl);
    $this->_DefModule = Str::Strtolower($DefModule);
  }
  /**
   * Регистрирует команду модуля
   * @param string $ActionName
   * @param string $ModuleName
   */
  public function RegisterAction($ActionName, $ModuleName)
  {
    //Приводим переменные к нижнему регистру
    $ActionName = Str::Strtolower($ActionName);
    $ModuleName = Str::Strtolower($ModuleName);
    $this->_Actions[$ModuleName . ' ' . $ActionName] = TRUE;
  }
  /**
   * Регистрирует команду по умолчанию для модуля
   * @param string $ActionName
   * @param string $ModuleName
   */
  public function RegisterDefault($ActionName, $ModuleName)
  {
    //Приводим переменные к нижнему регистру
    $ActionName = Str::Strtolower($ActionName);
    $ModuleName = Str::Strtolower($ModuleName);
    $this->_Default[$ModuleName] = $ActionName;
  }
  /**
   * Возвращает TRUE если команда зарегистрирована
   * @param string $ActionName
   * @param string $ModuleName
   * @param bool $InDefault FALSE
   * @return bool
   */
  public function IsActionRegistered($ActionName, $ModuleName)
  {
    //Приводим переменные к нижнему регистру
    $ActionName = Str::Strtolower($ActionName);
    $ModuleName = Str::Strtolower($ModuleName);
    return (isset($this->_Actions[$ModuleName . ' ' . $ActionName]));
  }
  /**
   * Возвращает команду по умолчанию для модуля
   * или NULL если её нет
   * @param string $ModuleName
   * @return mixed
   */
  public function GetDefaultAction($ModuleName)
  {
    //Приводим переменную к нижнему регистру
    $ModuleName = Str::Strtolower($ModuleName);
    return (isset($this->_Default[$ModuleName])) ? $this->_Default[$ModuleName] : NULL;
  }
  /**
   * Возвращает результат выполнения команды
   * @param string $ActionName
   * @param string $ModuleName
   * @param ArrayList|NULL $Params
   * @return mixed
   */
  public function GetResponse($ActionName, $ModuleName, $Params = NULL)
  {
    //Создаем пустые параметры команды если они не заданы
    if(!$Params) {
      $Params = new ArrayList();
    }
    //Если не указан модуль устанавливаем тот что по умолчанию
    if(!$ModuleName) {
      $ModuleName = $this->_DefModule;
    }
    //Ищем команду в зарегистрированных
    if($this->IsActionRegistered($ActionName, $ModuleName)) {
      //Создаем неизвестный экземпляр команды
      $Action = $this->_CreateAction($ActionName, $ModuleName);
      //Запускаем команду и возвращаем результат
      return $this->_RunAction($Action, $Params);
    }
    //Ищем команду в зарегистрированных по умолчанию
    elseif (NULL !== $ActionName = $this->GetDefaultAction($ModuleName)) {
      //Создаем неизвестный экземпляр команды
      $Action = $this->_CreateAction($ActionName, $ModuleName);
      //Запускаем команду по умолчанию и возвращаем результат
      return $this->_RunAction($Action, $Params);
    }
    else {
      //Перенаправляем пользователя
      header('Location: ' . $this->_RedirectUrl);
      exit(0);
    }
  }
  /**
   * Возвращает инстанцированный
   * объект команды
   * @param string $ActionName
   * @param string $ModuleName
   * @return Action
   */
  private function _CreateAction($ActionName, $ModuleName)
  {
    //Приводим переменные к нижнему регистру
    $ActionName = Str::Strtolower($ActionName);
    $ModuleName = Str::Strtolower($ModuleName);
    //Формируем имя и путь подключаемого файла с классом команды
    $FileName = self::$_Path . $ModuleName . DIRECTORY_SEPARATOR .
                $ActionName . 'Action' . PHP_EXT;
    if(is_file($FileName)) {
      //Подключаем файл класса
      require_once $FileName;
      //Формируем имя класса команды (действия)
      $ActionClassName = $ActionName . 'Action';
      //Инстанцируем экземпляр команды и возвращаем его
      return new $ActionClassName();
    }
    else {
      //Генерируем исключение если файл не найден
      throw new Exception('Action file "' . $FileName . '" does not exists');
    }
  }
  /**
   * Запускает команду и
   * возвращает результат ее действия
   * @param Action $Action
   * @param ArrayList $Params
   * @return mixed
   */
  private function _RunAction(Action $Action, ArrayList $Params)
  {
    return $Action->Run($Params);
  }
}


Да, не особо простой оказался диспетчер. Некоторые методы я чуть изменил, но суть осталась та же: проверяем команду, если нет, то смотрим, установлена ли какая-либо по умолчанию, и, если и таковой нет, то выполняем редирект на адрес. Не остывая от кода, сразу напишем небольшой пример, в котором в зависимости от входных параметров, будет выполняться определенная команда. Сперва мы инициализируем некоторые константы и классы. Последние были разработаны в предыдущих частях №1 и №2. Затем зарегистрируем несколько команд и запустим диспетчер:


Listing №7 (PHP)
// Константа расширения для файлов команд
// да и для всех интерпретируемых файлов
define('PHP_EXT', '.php');
require_once './framework/Action' . PHP_EXT;
require_once './framework/ArrayList' . PHP_EXT;
require_once './framework/Dispatcher' . PHP_EXT;
require_once './framework/Request' . PHP_EXT;
require_once './framework/Str' . PHP_EXT;
//Инициализируем классы
Str::Init(Str::MODE_MULTIBYTE, 'UTF-8');
Request::Init();
Dispatcher::Init('./actions/');
//Посылаем заголовок
header('Content-type: text/html; charset="' . Str::GetCharset() . '"');
//Создаем диспетчер
$Dispatcher = new Dispatcher('standart');
//Регистрируем команду "login" в модуле "standart"
$Dispatcher->RegisterAction('login', 'standart');
//Регистрируем команду "reg" в модуле "standart"
$Dispatcher->RegisterAction('reg', 'standart');
//Регистрируем команду "user" в модуле "standart"
$Dispatcher->RegisterAction('user', 'standart');
//Также регистрируем ее как команду по умолчанию
$Dispatcher->RegisterDefault('user', 'standart');
//Запускаем диспетчер
$Response = $Dispatcher->GetResponse(Request::Get('cmd'), Request::Get('module'));
//Выводим результат
echo $Response;


Вот так с помощью шаблона проектирования контроллер запросов (Front Controller) мы объединяем все действия по обработке запросов в одном месте, распределяя их выполнение посредством диспетчера. Благодаря применению шаблона проектирования команда (Command) в составе контроллера запросов, мы с легкостью можем добавлять новые действия без особых усилий. Стоит лишь создать класс конкретной команды и подкинуть ее в директорию модуля. Давайте это и сделаем, добавив действия описанные в листинге7:


Listing №8 (PHP)
<?php //авторизация: файл ./actions/standart/loginAction.php
class loginAction extends Action {
 
  public function Run(ArrayList $Params)
  {
    return 'Запущена команда авторизации';
  }
}
?>
<?php //регистрация: файл ./actions/standart/regAction.php
class regAction extends Action {
 
  public function Run(ArrayList $Params)
  {
    return 'Запущена команда регистрации';
  }
}
?>
<?php //пользователь: файл ./actions/standart/userAction.php
class userAction extends Action {
 
  public function Run(ArrayList $Params)
  {
    return 'Запущена команда пользователя';
  }
}
?>


Надеюсь, с путями вы разберетесь. Конечно, данный диспетчер не претендует на идеальность, но я старался сделать как можно проще, чтобы показать суть решения. Естественно, вы можете допилить его до нужной кондиции - рефакторинг никогда не помешает!

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


Listing №9 (PHP)
class ArrayList {}


Можете считать, что перед вами зародыш фиктивной службы (Service Stub).


Было бы неплохо увидеть ваши комментарии! Продолжение следует...

Добавлено: 08 Августа 2018 07:54:34 Добавил: Андрей Ковальчук

Примеры шаблонов проектирования или как написать свой PHP framework. Часть 2: Объект запроса

Продолжаем писать свой PHP framework дальше. Раз уж я решился написать вторую часть, значит, думаю, дело пойдёт. В этой статье, как бы это банально не звучало, мы закрепим обсуждаемый в прошлой статье шаблон проектирования фасад (Facade) и опишем на его основе еще одну нужную в любом движке сущность, называемую объектом запроса.

Как известно, PHP содержит посланные пользователем данные в различных супеглобальных переменных, основными из которых являются $_GET, $_POST и $_COOKIE. На основании этих данных приложение "понимает", что от него хочет пользователь и соответственно пользовательскому запросу выполняет необходимые действия. Что же дают эти суперглобальные массивы нам, как программистам? На основе структуризации этих данным мы может строить логику взаимодействия интерфейса пользователя и непосредственно приложения. Например, мы используем данные из $_GET массива, чтобы определить какую функциональность требует пользователь в данный момент:


Listing №1 (PHP)

if(isset($_GET['mode']) && ($_GET['mode'] === 'reg')) {
  // Показываем форму регистрации...
}
else {
  // Перенаправляем на index.php
  header('Location: /index.php');
}


Приблизительно так же мы можем использовать данные из массивов $_POST, $_COOKIE и может даже остальных.

К чему я веду? В связи с теперешней архитектурой Интернета мы можем получить ответ лишь только если пошлём запрос. Поэтому, все web-приложения работают, так скажем, "по запросу". Грубо говоря - если пользователь ничего не делает, то ничего и не происходит. Если пользователь активирует свои действия, например, нажав на кнопку формы, то это непосредственно "чувствует" приложение. Какие действия выполнил пользователь с интерфейсом мы и определяем из вышеупомянутых суперглобальных массивов. Таким образом, последние являются единственной "отправной точкой" действия большинства web-приложений, написанных на PHP. Следовательно, можно рассматривать всю информацию, находящуюся в этих массивах, как некое отображение взаимодействия пользователя с программой - пользовательский запрос к логике программы, характеризующий обращение пользователя к web-интерфейсу в браузере.

Думаю, многие из вас могут согласиться с тем, что данные одной сущности неплохо было бы держать в одном месте и соответственно иметь одну точку доступа к ним для управления. Если вы так не думаете то попробуйте пересмотреть свои взгляды, или, хотя бы, примите моё мнение как одно из имеющих шанс на существование ;). Следуя вышесказанному, мы можем объединить данные из $_GET, $_POST и $_COOKIE массивов, инкапсулировав их в одном объекте. Сделав это, мы сконструируем уже знакомый нам фасад (Facade) к нескольким различным наборам данных, предоставив один общий интерфейс доступа к ним. Здесь в основном под интерфейсом доступа подразумевается не сам интерфейс доступа, а инкапсуляция доступа в одном объекте - одной точке доступа. Но так как эта точка доступа единственна и, естественно, имеет один интерфейс к различным наборам данных, то можно с уверенностью сказать, что это явный пример паттерна фасад (Facade). Но не спешите так закреплять свои знания об этом типовом решении. То, что я хочу сделать, очень похоже также на шлюз (Gateway) в том смысле, что мы получим основу для реализации фиктивной службы (Service Stub). Её смысл будет в эмуляции переданных пользователем параметров, предназначенной для тестирования приложения. Но так как интерфейс доступа, предоставляемый объектом фасада, будет отличаться от интерфейса, стоящего за ним объекта, мы будем говорить, что это именно фасад, а не шлюз, интерфейс которого может представлять собой точную копию инкапсулируемого интерфейса. Вообще это довольно сильно родственные шаблоны проектирования и на данный момент своей жизни мне самому иногда довольно трудно определить, что есть что.

Мартин Фаулер
Типовое решение фасад также как и шлюз упрощает работу с интерфейсом API, однако оно создаётся самим разработчиком внешней службы и предназначено для общего употребления. В свою очередь, шлюз разрабатывается клиентом для использования конкретным приложением.
Также хочу добавить, что фасад больше чем шлюз "предрасположен" предоставлять более удобный интерфейс доступа. Этим нам тоже надо будет заняться. Также важно, что фасад реализует общий интерфейс над несколькими службами - в нашем случае несколькими источниками данных, в то время как шлюз обычно скрывает за собой одну службу.

Не тратя лишних слов на дальнейшие разбирательства, приступим непосредственно к реализации. Естественно это будет статический класс, с методами доступа к нашим суперглобальным массивам. Эти методы должны быть довольно удобны - то есть должны скрывать некую логику доступа. Таким образом, я думаю, что они будут проверять наличие запрашиваемых нами данных и возвращать NULL если они отсутствуют, либо можно будет задать значение по умолчанию. При этом каждый метод будет отражать доступ к конкретной структуре:


Listing №2 (PHP)
class Request {
  /**
   * @static
   * @param string $VarName
   * @param string $Default NULL
   * @return mixed
   */
  public static function Post($VarName, $Default = NULL)
  {
    return isset($_POST[$VarName]) ? $_POST[$VarName] : $Default;
  }
  /**
   * @static
   * @param string $VarName
   * @param string $Default NULL
   * @return mixed
   */
  public static function Get($VarName, $Default = NULL)
  {
   return isset($_GET[$VarName]) ? $_GET[$VarName] : $Default;
  }
  /**
   * @static
   * @param string $VarName
   * @param string $Default NULL
   * @return mixed
   */
  public static function Cookie($VarName, $Default = NULL)
  {
    return isset($_COOKIE[$VarName]) ? $_COOKIE[$VarName] : $Default;
  }


Вот мы и сконцентрировали в одном месте данные о действиях пользователя. Теперь добавим методы эмуляции этих действий - установки значений соответствующих массивов:


Listing №3 (PHP)
class Request {
  /**
   * @static
   * @param string $VarName
   * @param string $Value
   */
  public static function SetPost($VarName, $Value)
  {
    $_POST[$VarName] = $Value;
  }
  /**
   * @static
   * @param string $VarName
   * @param string $Value
   */
  public static function SetGet($VarName, $Value)
  {
    $_GET[$VarName] = $Value;
  }
  /**
   * @static
   * @param string $VarName
   * @param string $Value
   */
  public static function SetCookie($VarName, $Value)
  {
    $_COOKIE[$VarName] = $Value;
  }


Метод SetCookie выглядит немного неуклюже, так как можно ошибочно представить, что он установит куку в браузере пользователя. Но так как это метод класса Request, то мы не будем обострять на этом внимание.

Ну что же, получилось всё довольно просто, поэтому я добавлю ещё одну особенность. Эта особенность так же будет немного выделять данное решение как реализацию шаблона фасад. Помимо претензий к разработчикам PHP, предъявляемым в прошлой статье, я хочу предъявить еще одну в этой. Это не совсем претензия, но некое недоразумение всё же присутствует. И имя ему Magic Quotes. Я надеюсь, читатель в курсе, что стоит за этими словами. Если нет, то я вкратце скажу, что если директива php.ini файла, называемая magic_quotes_gpc равна значению "On", то данные в наших суперглобальных массивах будут автоматически обработаны и одинарные, двойные кавычки, обратный слеш и NULL символы будут экранированы обратным слешем. Я считаю это функциональность бредовой, хотя бы потому, что аргументов против неё больше, чем аргументов за. В связи с этим я также как и в прошлый раз не хочу зависеть от каких-то там настроек php.ini и иметь на входе чистые данные. Так как директиву magic_quotes_gpc нельзя установить во время исполнения и данные пользователя могут прийти уже экранированными, то нам, возможно, придётся их "разэкранировать". Также я хочу по краям каждой входной переменной отрезать лишние пробелы, NUL-байты и вертикальные табуляции.

Для всего вышесказанного нам понадобиться сделать три метода. Один, для обработки одного суперглобального массива. Второй - для обработки всех массивов. Ну и третий - для инициализации объекта, при которой и будет проводиться зачистка. Третий будет схож с тем, что мы делали для класса обработки строк в прошлой статье:


Listing №4 (PHP)
class Request {
  /**
   * @static
   * @param mixed &$Param
   * @return mixed
   */
  private static function _StripSlash(&$Param)
  {
    $Param = (is_array($Param)) ? array_map(array('self', '_StripSlash'), $Param) : trim(stripslashes($Param), " \x0B\0");
    return $Param;
  }
  /**
   * @static
   */
  private static function _MagicFilter()
  {
    if (((function_exists('get_magic_quotes_gpc')) && get_magic_quotes_gpc()) ||
        (ini_get('magic_quotes_sybase'))) {
      self::_StripSlash($_GET);
      self::_StripSlash($_POST);
      self::_StripSlash($_COOKIE);
    }
    if (function_exists('get_magic_quotes_runtime') && get_magic_quotes_runtime()) {
      set_magic_quotes_runtime(FALSE);
    }
  }
  /**
   * @static
   */
  public static function Init()
  {
    self::_MagicFilter();
  }


Метод _MagicFilter также обрабатывает директивы magic_quotes_sybase и magic_quotes_runtime, что также способствует оперированию в дальнейшем "чистыми" данными. Теперь вначале сценария надо вызвать метод инициализации объекта запроса. Он будет находиться в том же месте, где инициализируется работа со строками:


Listing №5 (PHP)
Str::Init(Str::MODE_MULTIBYTE, 'UTF-8');
Request::Init();


На этом примере мы завершим текущее обсуждение. В следующей статье я расскажу о более "архитектурных" частях нашего фреймворка и затрону также несколько других, более интересных, шаблонов проектирования.


Продолжение следует...

Добавлено: 08 Августа 2018 07:50:27 Добавил: Андрей Ковальчук

Примеры шаблонов проектирования или как написать свой PHP framework. Часть 1: Строковый фасад

Каждый из Web-программистов может прийти к такому моменту своей жизни, когда захочет сделать свой собственный движок, цивилизованными словами - framework, для каких-то своих разработок. Я хочу помочь вам в этом и отдать свой опыт разработки в ваше распоряжение. Возможно, вы почерпнёте из этого материала хороший кусок знаний, так как сама разработка будет основана на применении различных шаблонов проектирования. В этой статье я расскажу о применении шаблона проектирования фасад (Facade) в контексте класса обработки срок. Что же это за класс и зачем он нужен?


Допустим, вы пишете русскоязычный сайт. Естественно при этом вы пользуетесь различными строковыми функция, такими как substr, strlen и т.д., которые работают с однобайтовой кодировкой, например Windows-1251. Вот вы написали сайт, запустили его, он работает и радует ваш глаз. Но вдруг вам понадобилось расширить локализацию сайта языком, алфавит которого не может описаться одним байтом. Тут, конечно же, вам на помощь придёт кодировка UTF-8. Как известно PHP нормально работает с многобайтовой кодировкой посредством расширения mbstring. И вы задаёте себе вопрос: заменять все строковые функции в проекте на mb_*? Конечно же нет. Для таких случаев уже придумали фичу, которая заключается в регулировании параметра php.ini mbstring.func_overload. По умолчанию значение этого параметра равно нулю, но если вы установите его в двойку, то строковые функции, работающие с однобайтовыми символами, перегрузятся функциями из расширения mbstring. То есть вместо substr будет вызвана mb_substr, хотя написана будет именно первая. Этот очень хороший механизм пригодится, когда вам необходимо внедрить код, написанный с помощью стандартных строковых функций в приложение, работающее в многобайтном режиме. Класс, который я собираюсь вам показать, попросту абстрагирует приложение от параметра mbstring.func_overload. Конечно же, многим этот подход может не понравиться, вы даже можете сказать, что это вообще не нужно делать. Но я люблю быть независимым от подобных настроек, поэтому и вам рекомендую пользоваться таким подходом, раз уж разработчики PHP не были в состоянии сделать сразу всё как полагается.


Итак, какие задачи будет решать этот класс? Первое, что он должен сделать, это инкапсулировать в себе строковый функционал, запретив доступ программиста к стандартным строковым функциям, будь то substr или mb_substr. В это же время он предоставит один интерфейс, то есть одни методы доступа, для вызова различных функций. Как я уже сказал выше, это будет явный представитель типового решения фасад (Facade), так как для доступа к двум интерфейсам функций мы предоставим доступ через один унифицированный. Для удобства он преобразует свой интерфейс в интерфейсы вызываемых функций. Перечислим обязанности нашего "строкового фасада":

1. Выбор режимов функционирования: однобайтовый / многобайтовый.

2. Детектирование настроек окружения и выбор соответствующих вызовов.

3. Статический интерфейс, совместимый с интерфейсом функций.

Давайте определим некоторые свойства класса и напишем метод, который будет определять, какого типа функцию нужно вызвать фасаду.


Listing №1 (PHP)

class Str {
 
  const MODE_ANSII = 0;
 
  const MODE_MULTIBYTE = 1;
  /**
   * Текущий режим работы
   * Принимает значения
   * MODE_MULTIBYTE для работы в многобайтовом режиме
   * или MODE_ANSII в однобайтовом
   * По умолчанию в многобайтовом
   * @static int
   */
  private static $_Mode = 1;
  /**
   * Текущая кодировка
   * По умолчанию UTF-8
   * @static stirng
   */
  private static $_Encoding = 'UTF-8';
  /**
   * Значение параметра mbstring.func_overload
   * @static array
   */
  private static $_OverloadBitmask = 0;
  /**
   * Возвращает TRUE если нужно вызвать mb_* функцию
   * @param int $OvelroadBitmask - флаг выбора возможных перезагруженных функций
   * @return bool
   */
  private static function _NeedUseMBFuncs($OvelroadBitmask)
  {
    return (self::$_Mode && self::$_OverloadBitmask) ? (!($OvelroadBitmask & self::$_OverloadBitmask)) : self::$_Mode;
  }


Как видите, мы выбираем конечную функцию в зависимости от нескольких параметров. Если режим работы многобайтовый и функции перегружены, то мы вызывает те функции, которые перегрузились, сравнивая побитно (обратите внимание на одинарный знак &) значение mbstring.func_overload с флагом, определяющим, в факте перегруженности каких функций мы сейчас заинтересованы.


Обычно реализуя какие-нибудь подобные вещи, я конструирую метод инициализации, который будет выполнять функцию конструктора для статического класса:


Listing №2 (PHP)
/**
 * @static
 * @param int $Mode self::MODE_MULTIBYTE
 * @param string $Encoding 'UTF-8'
 */
public static function Init($Mode = self::MODE_MULTIBYTE, $Encoding = 'UTF-8')
{
  self::$_Mode = $Mode;
  if (!function_exists('mb_substr')) {
    self::$_Mode = self::MODE_ANSII;
  }
  else {
    mb_internal_encoding($Encoding);
  }
  self::$_Encoding = $Encoding;
  self::$_OverloadBitmask = ini_get('mbstring.func_overload');
}


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


Listing №3 (PHP)
Str::Init(Str::MODE_MULTIBYTE, 'UTF-8');


Итак, всё готово. Теперь непосредственно реализуем то, для чего мы это всё затеяли. Не забываем согласовывать интерфейс с родными функциями. Конечно для паттерна фасад (Facade) это не обязательно, но в данном случае это хороший плюс. В итоге мы получаем приблизительно такую общую картину:


Listing №4 (PHP)
class Str {
 
  const MODE_ANSII = 0;
 
  const MODE_MULTIBYTE = 1;
  /**
   * Текущий режим работы
   * Принимает значения
   * MODE_MULTIBYTE для работы в многобайтовом режиме
   * или MODE_ANSII в однобайтовом
   * По умолчанию в многобайтовом
   * @static int
   */
  private static $_Mode = 1;
  /**
   * Текущая кодировка
   * По умолчанию UTF-8
   * @static stirng
   */
  private static $_Encoding = 'UTF-8';
  /**
   * Значение параметра mbstring.func_overload
   * @static array
   */
  private static $_OverloadBitmask = 0;
  /**
   * @static
   * @param int $Mode self::MODE_MULTIBYTE
   * @param string $Encoding 'UTF-8'
   */
  public static function Init($Mode = self::MODE_MULTIBYTE, $Encoding = 'UTF-8')
  {
    self::$_Mode = $Mode;
    if (!function_exists('mb_substr')) {
      self::$_Mode = self::MODE_ANSII;
    }
    else {
      mb_internal_encoding($Encoding);
    }
    self::$_Encoding = $Encoding;
    self::$_OverloadBitmask = ini_get('mbstring.func_overload');
  }
  /**
   * @static
   * @return string
   */
  public static function GetCharset()
  {
    return self::$_Encoding;
  }
  /**
   * @static
   * @param string $String
   * @param int $Start
   * @param int $Length
   * @return string
   */
  public static function Substr($String, $Start, $Length = NULL)
  {
    if (self::_NeedUseMBFuncs(2)) {
      if (NULL === $Length) {
        return mb_substr($String, $Start);
      }
      else {
        return mb_substr($String, $Start, (int)$Length, self::$_Encoding);
      }
    }
    else {
      if (NULL === $Length) {
        return substr($String, $String, $Length);
      }
      else {
        return substr($String, $String);
      }
    }
  }
  /**
   * @static
   * @param string $String
   * @return int
   */
  public static function Strlen($String)
  {
    if (self::_NeedUseMBFuncs(2)) {
      return mb_strlen($String, self::$_Encoding);
    }
    else {
      return strlen($String);
    }
  }
  /**
   * @static
   * @param string $String
   * @return string
   */
  public static function Strtolower($String)
  {
    return  (self::_NeedUseMBFuncs(2)) ? mb_strtolower($String, self::$_Encoding) : strtolower($String);
  }
  /**
   * @static
   * @param string $String
   * @return string
   */
  public static function Strtoupper($String)
  {
    return  (self::_NeedUseMBFuncs(2)) ? mb_strtoupper($String, self::$_Encoding) : strtoupper($String);
  }
  /**
   * @static
   * @param string $HayStack
   * @param string $Needle
   * @param int $Offset
   * @return int
   */
  public static function Strpos($Haystack, $Needle, $Offset = NULL)
  {
    if (self::_NeedUseMBFuncs(2)) {
      return mb_strpos($Haystack, $Needle, $Offset, self::$_Encoding);
    }
    else {
      return strpos($Haystack, $Needle, $Offset);
    }
  }
  /**
   * @static
   * @param string $HayStack
   * @param string $Needle
   * @param int $Offset
   * @return int
   */
  public static function Strrpos($Haystack, $Needle, $Offset = NULL)
  {
    if (self::_NeedUseMBFuncs(2)) {
      return mb_strrpos($Haystack, $Needle, $Offset, self::$_Encoding);
    }
    else {
      return strrpos($Haystack, $Needle, $Offset);
    }
  }
  /**
   * @static
   * @param string $Pattern
   * @param string $String
   * @param int $Limit = NULL
   */
  public static function Split($Pattern, $String, $Limit = NULL)
  {
    if (self::_NeedUseMBFuncs(2)) {
      if (NULL === $Limit) {
        return mb_split($Pattern, $String);
      }
      else {
        return mb_split($Pattern, $String, $Limit);
      }
    }
    else {
      if (NULL === $Limit) {
        return split($Pattern, $String);
      }
      else {
        return split($Pattern, $String, $Limit);
      }
    }
  }
  /**
   * @static
   * @param string $To
   * @param string $Subject
   * @param string $Message
   * @param string $AdditionalHeaders = NULL
   * @param string $AdditionalParameters
   * @return bool
   */
  public static function Mail($To, $Subject, $Message, $AdditionalHeaders = NULL, $AdditionalParameters = NULL)
  {
    if (self::_NeedUseMBFuncs(1)) {
      if (NULL === $AdditionalHeaders) {
        return mb_send_mail($To, $Subject, $Message);
      }
      else {
        if (NULL === $AdditionalParameters) {
          return mb_send_mail($To, $Subject, $Message, $AdditionalHeaders);
        }
        else {
          return mb_send_mail($To, $Subject, $Message, $AdditionalHeaders, $AdditionalParameters);
        }
      }
    }
    else
    {
      if (NULL === $AdditionalHeaders) {
        return mail($To, $Subject, $Message);
      }
      else {
        if (NULL === $AdditionalParameters) {
          return mail($To, $Subject, $Message, $AdditionalHeaders);
        }
        else {
          return mail($To, $Subject, $Message, $AdditionalHeaders, $AdditionalParameters);
        }
      }
    }
  }
  /**
   * Возвращает TRUE если нужно вызвать mb_* функцию
   * @param int $OvelroadBitmask - флаг выбора возможных перезагруженных функций
   * @return bool
   */
  private static function _NeedUseMBFuncs($OvelroadBitmask)
  {
    return (self::$_Mode && self::$_OverloadBitmask) ? (!($OvelroadBitmask & self::$_OverloadBitmask)) : self::$_Mode;
  }
}


Теперь, работа со строками будет проводиться в таком стиле:


Listing №5 (PHP)
$RequestString = Str::Strtolower($_SERVER['REQUEST_URI']);


Если вы заметили, я описал не все функции. Для большинства задач этих будет достаточно, но если вы настаиваете, то можете дописать их сами, обратившись к документации.

Многие функции, например str_replace, не попадают в нашу категорию, поэтому вы можете оставить её как есть или, для полной унификации интерфейса работы со строками, просто добавить в разработанный класс методы с их использованием. Это сделает наш фасад более "чистым" по отношению к предмету его действия. Также вы можете не вызывать каждый раз метод _NeedUseMBFuncs, а при инициализации заполнить необходимые свойства. Оставляю это на ваше усмотрение.


Продолжение следует...

Добавлено: 08 Августа 2018 07:48:52 Добавил: Андрей Ковальчук

Ядро. Легкая версия

ПРЕДИСЛОВИЕ

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

Представленный набор скриптов представляет из себя его облегченную, спартанскую версию настоящего ядра. Это просто попытка помочь более плавно адаптироваться в премудростях последующих скриптов.

Кто чувствует в себе силы сразу изучать более серьёзные вещи, может этот раздел пропустить.
В следующем придется начать все сначала. Хотя повторение - мать учения. Впрочем, выбирать Вам.
Шаг 1
Подготовительные работы.
С чего обычно начинается построение сайта? Вы думаете, что нужно сделать первый файл, допустим index.php, и пошло-поехало.
Ан нет.

Для того чтобы было быстрее и аккуратнее, доложна быть готовая рама, на которую мы и понавешаем нужный функционал, чтоб не повторяться.

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

Нужно сразу заметить, что ядром в нашем понимании, является не какие то догмы и рамки, которые обычно ставятся фреймворками и всевозможными CMS. У нас это просто структура, которая позволяет упорядочить скрипты и иные принадлежности сайта. Оставляя полную свободу выбора как и что реализовывать. И хотя "framework" и переводится как "основа" или "рабочая рама", в классическом понимании это больше язык программирования со своим синтаксисом и законами. Наша структура более точно подходит под определение "рама".

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

Начнем с того, что обычно оставляется "на потом", так как это кажется не очень важным. И зачастую это "потом" так и не наступает.

А именно с конфигурации и сопутствующих фишек.

Начнем со страниц ошибок. В корневой директории нужно сделать, как минимум, три файла.

401.html
403.html
404.html

Оформлять их не нужно, это дело верстальщиков. Нам будет достаточно этого:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
<html> 
<head> 
<title>404</title> 
</head> 
<body> 
<h2 style="color:#FF0000">404</h2> 
Запрашиваемая страница отсутствует на нашем сайте. 
</body> 
</html>

Еще там же в корне нужно сделать файл .htaccess с таким содержанием:
AddDefaultCharset UTF-8 
php_flag magic_quotes_gpc Off 
php_flag magic_quotes_runtime Off 
php_flag register_globals Off 
ErrorDocument 401 /401.html 
ErrorDocument 403 /403.html 
ErrorDocument 404 /404.html 
Options -Indexes

Что тут.

1. Кодировка. Все сайты мы будем делать в кодировке UTF-8.
2, 3. Убиваем магические кавычки.
4. Отключаем глобальные переменные
5-8. Обработка ошибок запросов. Вот тут и пригодятся странички. Обратите внимание на строку

Options -Indexes

Эта строка запретит вывод списка файлов и директорий, если в папке не окажется индекса. А директива ErrorDocument перенаправляет запросы на подготовленные страницы в случае ошибки сервера. Допустим набран адрес несуществующей страницы. В таком случае откроется 404.html

А пока что на данный момент должны иметь:

1. Всю структуру с прошлого занятия.
К ней добавили:
2. Три страницы с ошибками.
3. Файл .htacces в корневой директории.

Это точка отправления.

Шаг 2

Инициализация внешних переменных
Итак, начало у нас положено.
Продолжим-с.

Теперь у нас стоит задача выставить текущие и глобальные настройки сайта.
Основные и незыблемые две - кодировку и уровень ошибок, они прописаны первыми строчками индекса.
<?php
/** 
* We establish the charset and level of errors 
* Устанавливаем кодировку и уровень ошибок 
*/ 
    header("Content-Type: text/html; charset=utf-8"); 
    error_reporting(E_ALL);

Но понадобятся еще и текущие настройки. Их будет много, и чтобы не носиться по всему сайту в поисках вчерашнего дня, все настройки принято держать в одном месте, а именно в конфигурационном файле. Вот и мы не лыком шиты, у нас есть такой в корне - config.php

Еще в config.php добавим еще две константы (одна там уже есть). Обратите внимание на префиксы констант. Они нужны, чтобы невзначай не пересечься с предопределенными константами или константами сторонних скриптов, которые мы, может быть, будем интегрировать в систему.
<?php
/** 
* Configuration file 
* Конфигурационный файл 
* @author IT studio IRBIS-team 
* @copyright © 2009 IRBIS-team 
*/ 
///////////////////////////////////////////////////////// 
/**  
* Choice of language of a site  
* Выбор языка сайта  
*/   
    define('IRB_LANGUAGE', 'ru');  

/**   
* Establishes a physical path to a root directory of a script   
* Устанавливает физический путь до корневой директории скрипта   
*/   
    define('IRB_ROOT', str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']) .'/');   

/**   
* Establishes a path to a script root for HTTP   
* Устанавливает путь до корневой директории скрипта   
* по протоколу HTTP   
*/   
    define('IRB_HOST', 'http://'. $_SERVER['HTTP_HOST'] .'/');

Первая для указания полных физических путей, а вторая нужна для полных ссылок.

Для чего это нужно. А для того, чтобы скрипт можно было использовать не только в корне, а в отдельной папке, как дополнение или расширеник какого то готового сайта.
Для этого достаточно добавить в путь эту директорию.
/**  
* Establishes a path to a script root for HTTP  
* Устанавливает путь до корневой директории скрипта  
* по протоколу HTTP 
*/ 
    define('IRB_HOST', 'http://'. $_SERVER['HTTP_HOST'] .'/my_site/');

Теперь нужно все пути (к картинкам, стилям и т.д.) начинать с этого
<?php echo IRB_HOST ?>

и не бояться, что в определенный момент нужно будет перенести скрипт в другое место. Просто меняем путь в конфиге и насвистываем веселенький мотивчик.
Теперь ссылки в меню можно написать так:
<!-- skins/tpl/menu.tpl begin -->  
Сегодня <?php echo $date ?> года  
<ul>  
<li><a href="<?php echo IRB_HOST ?>?page=main">Главная страница</a></li>  
<li><a href="<?php echo IRB_HOST ?>?page=second">Вторая страница</a></li>  
</ul>  

<!-- skins/tpl/menu.tpl end -->

А ссылки - это важно. Потому что на гет-параметрах этих ссылок и будет строиться вся навигация и переключение модулей, так как все внутренние ссылки будут вести только на один файл (точку входа) - index.php

Это значит, что нам нужно бережно принять эти параметры и обработать их.

У нас уже был такой прецидент, мы инициализировали переменную $page в файле variables.php как раз для переключения модулей. Еще нам потребуется переменная для переключения разделов модуля (контроллеров). Назовем её $rem. Еще пригодится переменная для постранички. $num И переменная идентификатора чего-либо. Соответственно $id. А чтобы нам не греть голову, откуда появились в скрипте переменные, сложим их все в один массив $GET (не путать с $_GET). И теперь в любом месте будет ясна их принадлежность.
Ну а за одно сразу установим значения по умолчанию. Лишним никогда не будет.
variables.php
/**   
* Initialization of variables GET-parametres   
* Инициализация переменных GET-параметров   
*/   
    $GET['page'] = !empty($_GET['page']) ? $_GET['page'] : 'main'; 
    $GET['rem']  = !empty($_GET['rem']) ? $_GET['rem'] : 'read'; 
    $GET['num']  = !empty($_GET['num']) ? $_GET['num'] : 1;     
    $GET['id']   = !empty($_GET['id']) ? $_GET['id'] : 1;

В главном индексе нужно тоже изменить переменную, коль скоро они у нас все в массиве:
<?php

/** 
* The switch of modules 
* Переключатель страниц 
*/       
    switch($GET['page'])  
    {  
/**   
* Подключаем модуль приветствия   
* Includes the greeting module   
*/          
        case 'main':     
            include './modules/main/router.php';             
        break;

Еще нам понадобятся $_POST переменные. Мы же будем делать интерактивные приложения. В файле с переменными нужно просто указать несколько таких про запас. Если не хватит, потом добавим еще.
variables.php
<?php


/**  
* The block of initialization and processing of entering variables  
* Блок инициализации и обработки переменных  
* @author IT studio IRBIS-team  
* @copyright © 2009 IRBIS-team  
*/  
///////////////////////////////////////////////////////// 

/**   
* Initialization of variables GET-parametres   
* Инициализация переменных GET-параметров   
*/   
    $GET['page'] = !empty($_GET['page']) ? $_GET['page'] : 'main'; 
    $GET['rem']  = !empty($_GET['rem']) ? $_GET['rem'] : 'read'; 
    $GET['num']  = !empty($_GET['num']) ? $_GET['num'] : 1;     
    $GET['id']   = !empty($_GET['id']) ? $_GET['id'] : 1;     
     
/**   
* Initialization of variables POST   
* Инициализация переменных POST   
*/  
    
    $ok       = !empty($_POST['ok'])?true:false;   
    $delete   = !empty($_POST['delete'])?true:false; 
   
    $POST['value1'] = !empty($_POST['value1']) ? $_POST['value1'] : NULL;   
    $POST['value2'] = !empty($_POST['value2']) ? $_POST['value2'] : NULL;  
    $POST['value3'] = !empty($_POST['value3']) ? $_POST['value3'] : NULL;  
    $POST['value4'] = !empty($_POST['value4']) ? $_POST['value4'] : NULL;

Обратите внимание, мы не знаем, какие понадобятся переменные. Если приложение большое, то их может оказаться огромное количество. По этому, не мудря, просто пронумеруем их и будем использовать по порядку.

Еще в этот же файл сразу добавим функцию для удаления магических кавычек.
Весь файл variables.php теперь такой:
<?php


/**  
* The block of initialization and processing of entering variables  
* Блок инициализации и обработки переменных  
* @author IT studio IRBIS-team  
* @copyright © 2009 IRBIS-team  
*/  
///////////////////////////////////////////////////////// 

/**  
* We kill magic inverted commas  
* Убиваем магические кавычки  
*/          
    function stripslashesDeep($data)      
    {      
        if(is_array($data))       
            $data = array_map("stripslashesDeep", $data);       
        else     
            $data = stripslashes($data);       
        return $data;  
    }  

    if(get_magic_quotes_gpc())   
    {   
        $_GET = stripslashesDeep($_GET);    
        $_POST = stripslashesDeep($_POST);    
        $_COOKIE = stripslashesDeep($_COOKIE);  
        $_REQUEST = stripslashesDeep($_REQUEST);  
    } 
     
/**   
* Initialization of variables GET-parametres   
* Инициализация переменных GET-параметров   
*/   
    $GET['page'] = !empty($_GET['page']) ? $_GET['page'] : 'main'; 
    $GET['rem']  = !empty($_GET['rem']) ? $_GET['rem'] : 'read'; 
    $GET['num']  = !empty($_GET['num']) ? $_GET['num'] : 1;     
    $GET['id']   = !empty($_GET['id']) ? $_GET['id'] : 1;     
     
/**   
* Initialization of variables POST   
* Инициализация переменных POST   
*/  
    
    $ok       = !empty($_POST['ok'])?true:false;   
    $delete   = !empty($_POST['delete'])?true:false; 
   
    $POST['value1'] = !empty($_POST['value1']) ? $_POST['value1'] : NULL;   
    $POST['value2'] = !empty($_POST['value2']) ? $_POST['value2'] : NULL;  
    $POST['value3'] = !empty($_POST['value3']) ? $_POST['value3'] : NULL;  
    $POST['value4'] = !empty($_POST['value4']) ? $_POST['value4'] : NULL; 
     
     
/**   
* Other variables   
* Другие переменные   
*/     
   $info  = array();

Это основа всех наших дальнейших поползновений.

Шаг 3
Паттерн MVC. Модель.
Ну раз у нас есть гостевая книга - не пропадать же добру. Попробуем на её основе построить что то более менее приличное.
Начнем с данных. Как помнится, для этого у нас была организована директория data. Ну и тут сделаем тоже самое. Это будет частью модели, а именно хранилищем данных.

Вторая часть - скрипт получения этих данных из хранилища. У нас в гостевой книге за это отвечает функция сканирования директории.
Поместим её в файл libs/model.php
<?php


/**    
* Model functions   
* Функции модели    
* @author IT studio IRBIS-team    
* @copyright © 2009 IRBIS-team    
*/ 
////////////////////////////////////////////////////////////////// 


/**      
* Function of reading of a directory     
* Функция чтения директории 
* @param string      
* @return array     
*/  
    function dirScan($dir)  
    {   
        static $files;  
          
            if(empty($files))  
            {  
                $files = scandir($dir);  
                $files = array_diff($files, array('.', '..'));  
            }  
   
        $files = count($files) ? $files : array('1');  
                  
        return $files;  
    }


Ну и для полного счастья добавим туда же еще две функции. Получения и записи информации, хранимой в виде сериализованных массивов.
<?php


/**     
* Model functions    
* Функции модели     
* @author IT studio IRBIS-team     
* @copyright © 2009 IRBIS-team     
*/  
//////////////////////////////////////////////////////////////////  


/**       
* Function of reading of a directory      
* Функция чтения директории  
* @param string       
* @return array      
*/   
    function dirScan($dir)   
    {    
        static $files;   
           
            if(empty($files))   
            {   
                $files = scandir($dir);   
                $files = array_diff($files, array('.', '..'));   
            }   
    
        $files = count($files) ? $files : array('1');   
                   
        return $files;   
    } 
     
     
/**       
* Function of reception of the information    
* Функция получения информации  
* @param string       
* @return array       
*/   
    function getData($file)   
    {    
        if(file_exists($file)) 
            $string = trim(file_get_contents($file));  
        else 
            exit('No such file '. $file); 

         
        return !empty($string) ? unserialize($string) : array();  
              
    } 

/**       
* Data recording function    
* Функция записи информации 
* @param string       
* @return array      
*/   
    function setData($file, $array)   
    {    
        return file_put_contents($file, serialize($array));  
    }

Вот и вся модель. Тоесть хранилище (папка data в корне) и эти три функции, позволяющие с этими данными работать. То есть находить нужный файл, чтать его, десериализовать массив и выдавать результат. Ну или наоборот - сериализовать массив и складывать в файл по месту назначения.

Пока работы этих скриптов мы не увидим, это внутренняя кухня. А чтобы увидеть, нам потребуется продолжение банкета - вид и контроллер.

Шаг 4
Паттерн MVC. Контроллер
Начнем с контроллера. В нем, для наглядности, будет два блока. Один для вывода информации, другой для записи.

Гостевая книга, как ей и положено, будет находиться на второй странице.
Итак, открываем файл modules/second/read_controller.php и пишем туда это:
<?php


/**   
* The controller of a conclusion of the information   
* Контроллер вывода информации   
* @author IT studio IRBIS-team   
* @copyright © 2009 IRBIS-team   
*/   
/////////////////////////////////////////////////////////   


/**   
* We connect a file of the model functions   
* Подключаем файл функций модели  
*/     
   include './libs/model.php'; 


/**   
* The block of reading of the information   
* Блок чтения информации   
*/  
    $posts = getData('./data/'. $GET['num']);  
    include './libs/bb_tags.php'; 
     
    $rows = '';  
         
    foreach($posts as $id => $post)  
    {  
        $date  = $post['date'];          
        $name  = htmlspecialchars($post['name']);  
        $mess  = nl2br(bbTags(htmlspecialchars($post['mess'])));  
        $rows .= '<div class="guest_post">'. $date .' / <strong> 
                 '. $name .'</strong><hr width="30%" align="left" /> 
                 '. $mess .'</div>';        

    } 

Всё это мы уже проходили в гостевой книге, просто сейчас пытаемся привести в удобоваримый вид. По этому на самом функционале акцентироваться не будем.
Но несколько важных момента отметить надо.
1. Не все страницы и модули будут работать с данными. А значит файл с функциями модели мы будем подключать непосредственно в контроллер.
2.Весь вывод мы поместим в переменную $rows, а не отправим тут же в поток. За вывод отвечает часть паттерна MVC "вид", так что не будем нарушать принципов. И хоть у нас там затесались предательские html-теги, это будет идеологически верно. А с тегами мы разберемся чуть позже.
3. Теперь нет необходимости в контроллере проверять наличие файла, это сделает функция чтения, которую мы поместили в модель. Она нам пригодиться в той же админке или в другом модуле. Допустим можно сделать блок комментариев к статьям или даже модуль новостей. Принцип работы с данными останется прежним.
4. Функция bb-тегов нам понадобится и в админке. Поэтому мы вынесем её в отдельный файл и будем подключать там, где потребуется. Чтобы не переписывать одно и тоже по 10 раз.
libs/bb_tags.php
<?php


/**  
* Replacement function bb-tags and smilies        
* Функция замены bb-тегов и смайликов     
* @param string     
* @return string   
* @author IT studio IRBIS-team  
* @copyright © 2009 IRBIS-team  
*/  
///////////////////////////////////////////////////////// 


    function bbTags($text)   
    {      
        $bb = array(  
                    '[B]',   
                    '
',
'', '',
'
',   
                    '
',
'', '',
'[:)]',
'[:(]',
'[;)]',
'[:D]'
);

$tag = array(
'<b>',
'</b>',
'<i>',
'</i>',
'<s>',
'</s>',
'<u>',
'</u>',
'<img src="'. IRB_HOST .'skins/images/smiles/1.gif" />',
'<img src="'. IRB_HOST .'skins/images/smiles/2.gif" />',
'<img src="'. IRB_HOST .'skins/images/smiles/3.gif" />',
'<img src="'. IRB_HOST .'skins/images/smiles/4.gif" />'
);

return str_ireplace($bb, $tag, $text);
}[/PHP]
Ну скучно же делать что то, не получая тут же результата. Так давайте веселиться. Для начала выведем переменную $rows в шаблон:
<!-- skins/tpl/second/show.html begin --> 

<div style="padding:10px; background-color:#e6e6e6;"> 
<?php echo $rows ?> 
</div> 

<!-- skins/tpl/second/show.tpl end -->

Потом сделаем файл стилей и поместим на свое законное место: skins/css/style.css
/* CSS Document */ 
.guest_post{ 
    border:1px solid;  
    width:70%;  
    background-color:#66FFFF;  
    min-height:100px;  
    margin:5px;  
    padding:5px; 
}

Подключим его к главному шаблону: skins/tpl/index.tpl
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head>  
<title>Мой сайт</title>
 
    <link href="<?php echo IRB_HOST ?>skins/css/style.css" rel="stylesheet" type="text/css" />
 
</head> 
<body> 
<?php echo $content ?> 
</body> 
</html>

И - на старт! Внимание! Мааааарш!
Запускаем.

Вот так и будет работать дебаггер, если вдруг не окажется нужного файла с данными. Ну откуда ему взяться, если мы еще не сделали контроллер записи?
А вот отсюда можно взять пока прямо всю папку data

Теперь работает? Если нет, вот тут точно работает.

Ну а мы продолжим. Исправим ситуацию и соорудим блок записи.

Шаг 5
Контроллер. Продолжение.
Итак, нам нужно срочно добавить записей. Для этого нужен блок записи, но для начала разберемся с формой.
Вообще, форма - это тоже вещь, касаемая данных. И её тоже можно, с небольшой натяжкой, отнести к модели. Но суть в том, что как бы мы её не обзывали, формой она быть от этого не перестанет. Я вообще противник каких-либо ярлыков и рамок. Если комуто понравилось назвать это все MVC, ну и Бог ему судья. Нам же гораздо важнее, чтобы все безупречно работало. А назвать можно хоть MVC, хоть VCM, хоть Семен Семенычем, хоть крылатой ракетой "Першинг".

Итак, форма. Добавим её в шаблон модуля:
<!-- skins/tpl/second/show.html begin --> 

<div style="padding:10px; background-color:#e6e6e6;"> 
  
<strong style="color:#FF0000"><?php echo getInfo($info); ?></strong> 

<?php echo $rows ?> 
<form action="" name="post" id="post" method="post">    
Имя:<br />    
<input name="value1" type="text" value="<?php echo $POST['value1'] ?>"/>
<br />   
<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/bold.gif" 
alt="Жирный" onclick="tag('[b]','
')">

<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/italics.gif"
alt="Курсив" onclick="tag('','')">

<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/underline.gif"
alt="Подчеркнутый" onclick="tag('','')">

<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/strikethrough.gif"
alt="Зачеркнутый" onclick="tag('
','
')">

<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/1.gif" onclick="tag('[:)]','')">
<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/2.gif" onclick="tag('[:(]','')">
<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/3.gif" onclick="tag('[;)]','')">
<img style="cursor:pointer" src="<?php echo IRB_HOST ?>skins/images/smiles/4.gif" onclick="tag('[:D]','')">
<br />
Сообщение:<br />
<textarea name="value2" id="value2" cols="40" rows="10"><?php echo $POST['value2'];?></textarea><br />
<input name="ok" type="submit" />
</form>
</div>

<!-- skins/tpl/second/show.tpl end -->[/PHP]
Форма нам знакомая, изменились только названия полей и добавилась функция вывода ошибок. Для красоты.
<?php

/** 
* Information conclusion 
* Вывод информации         
*/   
    function getInfo($info) 
    { 
        if(count($info)) 
            return '<br>' . implode('<br>', $info); 
        else 
            return '&nbsp;'; 
    }

Добавилась она, как можно догадаться, в файл с общими функциями (libs/default.php)

И еще нужно взять смайлики и поместить их в папку skins/images
А вот для того, чтобы заработали эти смайлики и bb-теги, нужно в папку skins/js положить этот скрипт:
skins/js/script.js
function tag(text1, text2) 
{

    if ((document.selection)) 
    { 
        document.getElementById('value2').focus(); 

        document.post.document.selection.createRange().text = 
        text1+document.post.document.selection.createRange().text + text2; 

    } else if(document.forms['post'].elements['value2'].selectionStart != undefined) { 
        var element = document.forms['post'].elements['value2']; 
        var str = element.value; 
        var start = element.selectionStart; 
        var length = element.selectionEnd - element.selectionStart; 
        element.value = str.substr(0, start) + text1 + str.substr(start, length) 
        + text2 + str.substr(start + length); 

    }
}

и подключить в главный шаблон:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head>  
<title>Мой сайт</title> 
<link href="<?php echo IRB_HOST ?>skins/css/style.css" rel="stylesheet" type="text/css" />
 
    <script type="text/javascript" language="javascript" src="<?php echo IRB_HOST ?>skins/js/script.js"></script> 
 
</head> 
<body> 
<?php echo $content ?> 
</body> 
</html>

Вот теперь пора сделать и сам блок записи. Функционал его практически не изменился, такой же и был в гостевой книге.
modules/second/read_controller.php
<?php


/**   
* The controller of a guestbook   
* Контроллер гостевой книги   
* @author IT studio IRBIS-team   
* @copyright © 2009 IRBIS-team   
*/   
/////////////////////////////////////////////////////////   


/**   
* We connect a file of the model functions   
* Подключаем файл функций модели  
*/     
   include './libs/model.php'; 

/**   
* The block of recording of the information   
* Блок записи информации   
*/  
    if($ok)    
    {    
        if(!$POST['value1'])    
            $info[] = IRB_LANG_NO_NAME;  
                
        if(!$POST['value2'])    
            $info[] = IRB_LANG_NO_TEXT;;   
               
        if(count($info) == 0)    
        {    
               
             $file = max(dirScan('data'));  
             $posts = getData('data/'. $file);      
                  
             if(count($posts)) 
             {  
                 $keys = array_keys($posts);  
                 $id = max($keys) + 1;  
                 $num = count($posts);  
             }  
             else  
                 $num = $id = 1;  
                  
            if($num >= IRB_NUM_POSTS)  
            {  
                ++$file;  
                unset($posts);  
            }                     
                          
            $posts[$id]['date'] = date('d-m-Y');  
            $posts[$id]['name'] = $POST['value1'];  
            $posts[$id]['mess'] = $POST['value2'];                        
              
            setData('./data/'. $file, $posts);    

            header('location: '. IRB_HOST .'?page=second&num='. $file);    
            exit();    
        }    
    }  
     
     
/**   
* The block of reading of the information   
* Блок чтения информации   
*/  
    $posts = getData('./data/'. $GET['num']);  
    include './libs/bb_tags.php'; 
     
    $rows = '';  
         
    foreach($posts as $id => $post)  
    {  
        $date  = $post['date'];          
        $name  = htmlspecialchars($post['name']);  
        $mess  = nl2br(bbTags(htmlspecialchars($post['mess'])));  
        $rows .= '<div class="guest_post">'. $date .' / <strong> 
                 '. $name .'</strong><hr width="30%" align="left" /> 
                 '. $mess .'</div>';        

    }

Разница только в методах получения и записи информации (теперь это осуществляют специальные функции) и в редиректе. Мы в него подставили константу.

Кстати, о константах. Нужно добавить еще одну в конфигу. Она будет определять количество постов на странице.
<?php


/** 
* Configuration file 
* Конфигурационный файл 
* @author IT studio IRBIS-team 
* @copyright © 2009 IRBIS-team 
*/ 
///////////////////////////////////////////////////////// 

/**  
* Choice of language of a site  
* Выбор языка сайта  
*/   
    define('IRB_LANGUAGE', 'ru'); 
     
/**      
* Quantity of posts on page   
* Количество постов на странице    
*/  
   define('IRB_NUM_POSTS', 5);       

/**   
* Establishes a physical path to a root directory of a script   
* Устанавливает физический путь до корневой директории скрипта   
*/   
    define('IRB_ROOT', str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']) .'/');   

/**   
* Establishes a path to a script root for HTTP   
* Устанавливает путь до корневой директории скрипта   
* по протоколу HTTP   
*/   
    define('IRB_HOST', 'http://'. $_SERVER['HTTP_HOST'] .'/');

и еще парочку в языковой файл - для валидации данных формы:
language/ru.php (en.php)
<?php


/** 
* Варианты модуля приветствия   
* Variants of the module of a greeting 
*/ 
    define('IRB_WHO_FIRST', 'Мир');  
    define('IRB_WHO_SECOND','Солнце');  
    define('IRB_WHO_THIRD', 'Небо');  
    define('IRB_WHO_FOURTH','Вася');  
    define('IRB_WHO_FIFTH', 'Петя'); 
      
/** 
* Массив месяцев   
* The month array  
*/ 
    $month = array(  
                    '01' => 'января',  
                    '02' => 'февраля',  
                    '03' => 'марта',  
                    '04' => 'апреля',  
                    '05' => 'мая',  
                    '06' => 'июня',  
                    '07' => 'июля',  
                    '08' => 'августа',  
                    '09' => 'сентября',  
                    '10' => 'октября',  
                    '11' => 'ноября',  
                    '12' => 'декабря'  
                  ); 
                   
/** 
* Check data   
* Валидация  
*/                   
    define('IRB_LANG_NO_NAME', 'Нет имени');                   
    define('IRB_LANG_NO_TEXT', 'Текстовая область не заполнена.');

Ну вот теперь кажется и все, контроллер готов к бою. Можно пробовать.

Шаг 6
Паттерн MVC. Вид.
Ну а теперь займемся приведением в порядок отображения. У нас с ним далеко не все в порядке. Начнем с того, что уже проходили. А именно с возврата данных. Самые наблюдательные могли заметить, что у нас пропала обработка данных, которыен выводятся в элементы формы. Напомню:
<input name="value1" type="text" value="<?php echo $POST['value1'] ?>"/>

Тут отсутствует функция htmlspecialchars(). Это все неспроста.

Дело в том, что данные у нас хранятся не в разрозненных переменных, а в массиве $POST (не путать с $_POST). А раз они в массиве, то и обработать их можно одним махом. Для этого сочиним специальную функцию-обертку, которая заставит htmlspecialchars() работать с массивами.
Поместим её в файл libs/view.php, так как данные нужно обрабатывать всегда и повсеместно.
<?php


/** 
* Library of the views functions 
* Библиотека функций отображения 
* @author IT studio IRBIS-team 
* @copyright © 2009 IRBIS-team 
*/ 
/////////////////////////////////////////////////////////<br /> 

/**    
* Function of processing of variables for a conclusion in a stream    
* Функция обработки переменных для вывода в поток     
*/                                                        
    function htmlChars($data)       
    {       
        if(is_array($data))  // Если данные - массив, вызываем эту же функцию.             
            $data = array_map("htmlChars", $data);     
        else  // Если нет, обрабатываем  htmlspecialchars()               
            $data = htmlspecialchars($data);       
       // На выход                             
        return $data;    
    } 

/** 
* Date formatting 
* Форматирование даты  
*/ 
    $date = formatDate(date("Y-m-d"), false);

А теперь просто открываем вьюшку модуля и добавляем туда всего одну строчку:
modules/second/view.php
<?php


/** 
* View 
* Вид 
* @author IT studio IRBIS-team 
* @copyright © 2009 IRBIS-team 
*/ 
///////////////////////////////////////////////////////// 


   $POST = htmlChars($POST); 


/**   
* Подключаем шаблон   
* Includes a templates   
*/ 
    include './skins/tpl/second/show.tpl';

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

Поехали дальше. Блок чтения информации. Тут точно также можно обработать сразу весь массив:
/**   
* The block of reading of the information   
* Блок чтения информации   
*/  
    $posts = getData('./data/'. $GET['num']);  
    include './libs/bb_tags.php'; 
     
    $rows = '';  
         
    foreach($posts as $id => $post)  
    {  
        $post = htmlChars($post); 
        $date  = $post['date'];          
        $name  = $post['name'];  
        $mess  = nl2br(bbTags($post['mess']));  
        $rows .= '<div class="guest_post">'. $date .' / <strong> 
                 '. $name .'</strong><hr width="30%" align="left" /> 
                 '. $mess .'</div>';        

    }

Это удобно, но не столь важно. А важно то, что вот и попались нам эти бессовестные html теги, которым совсем не место в контроллере. Их нужно немедленно поместить в шаблон. Вот так:
<!-- skins/tpl/second/rows.html begin --> 

<div class="guest_post">          
<?php echo $date; ?> / <strong><?php echo $name; ?></strong>  
<hr width="30%" align="left" />  
<?php echo $mess; ?>  
</div> 

<!-- skins/tpl/second/rows.tpl end -->

А вот сам этот шаблон запихать в переменную $rows . Но как это сделать, ведь там переменные, и они должны меняться в каждом витке цикла... А для этого сочиним функцию, которая нам в этом поможет. Поместим её в файл libs/view.php
<?php


/** 
* Library of the views functions 
* Библиотека функций отображения 
* @author IT studio IRBIS-team 
* @copyright © 2009 IRBIS-team 
*/ 
///////////////////////////////////////////////////////// 

/**   
* Function of analysis of a template   
* Функция разбора шаблона   
*/        
    function parseTpl($file, $data = array())   
    {   
                       
            extract($data);   

            ob_start();   
                include $file;   
            $cont = ob_get_contents();     
            ob_end_clean();     

       return $cont;   
    }   
     
     
/**    
* Function of processing of variables for a conclusion in a stream    
* Функция обработки переменных для вывода в поток     
*/                                                        
    function htmlChars($data)       
    {       
        if(is_array($data))  // Если данные - массив, вызываем эту же функцию.             
            $data = array_map("htmlChars", $data);     
        else  // Если нет, обрабатываем  htmlspecialchars()               
            $data = htmlspecialchars($data);       
       // На выход                             
        return $data;    
    } 

/** 
* Date formatting 
* Форматирование даты  
*/ 
    $date = formatDate(date("Y-m-d"), false);

Что это за зверь такой. Ну вопервых, сразу оговорочка. Так делать вообще то нежелательно, потому что мы при каждой интерации (витке) цикла вынуждены обращаться к файловой системе. В следующем разделе я покажу, как этого избежать. А пока для наглядности так.
Но по порядку. Функция extract() разбирает массив на переменные. Теперь (хоть их и не видно) у нас внутри функции определены переменные с названиями, эдентичными ключам массива, и значениями соответственно. Тоесть, если мы передадим в неё массив
    array( 
            'name'  => 'Вася', 
            'mess'  => 'Привет!' 
         )

то внутри нашей функции, после обработки extract() образуются переменные
    $name = 'Вася'; 
    $mess = 'Привет!';

которые мы и подставим в подключенный шаблон. А дальше вступает в силу буферизация, и мы получаем шаблон с нужными значениями в виде строки. Которую и выдаем, как результат работы функции. Теперь нужно немного изменить контроллер (блок чтения) и все будет в порядке. Весь вывод - в шаблонах. А логика в контроллере.
/**   
* The block of reading of the information   
* Блок чтения информации   
*/  
    $posts = getData('./data/'. $GET['num']);  
    include './libs/bb_tags.php'; 
     
    $rows = '';  
         
    foreach($posts as $id => $post)  
    {  
        $post = htmlChars($post); 
        $post['mess']  = nl2br(bbTags($post['mess']));  
        $rows .= parseTpl('./skins/tpl/second/rows.tpl', $post);        

    }

Вот и всё. Основные принципы я показал. Если есть желание достроить эту гостевуху до полного ажура - теперь Вы знаете, как это делать.
Добавить в общую вьюшку функцию формирования ссылок постранички и организовать такой же модуль админки к примеру.
Лично я не стал бы на Вашем месте тратить на это время и перешел бы к более серьёзным занятиям - следующему разделу.

А вот итог этого занятия.

Добавлено: 11 Мая 2018 08:41:10 Добавил: Андрей Ковальчук

Представление Laravel

"Laravel гибкая и расширяемая библиотека PHP для веб разработки. Освобождая вас от спагетти кода, Laravel помогает вам создавать прекрасные приложения с помощью простого и выразительного синтаксиса. Разработка должна быть творческим процессом, который доставляет удовольствие, а не причиняет боль. Насладитесь свежим ветром."

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

Миграции
Сначала создадим базу данных. Приложение использует очень простую схему, единственная таблица с 5 столбиками для хранения ID, заголовка, описания и времени, когда задача была создана или обновлялась в последний раз.

Laravel имеет инструмент, который называется миграции (вы уже можете быть знакомы с ним, если использовали библиотеки подобные Ruby on Rails). Миграции - файлы, которые используются для обновления базы данных. При выполнении они изменяют схему базы данных таким образом, что вы легко можете применить обновление.

Для нашего приложения миграция выглядит так:

<?php
class Create_Todo_Table
{
    public function up() {
        Schema::table("todos", function($table) {
            $table->create();
            $table->increments("id");
            $table->string("title", 20);
            $table->text("description");
            $table->timestamps();
        });
    }
 
    public function down() {
        Schema::drop("todos");
    }
}

Метод up() вызывается, когда миграция выполняется. А метод down() вызывается для возврата в исходное состояние.

Модель
Так как Laravel использует схему MVC (модель-вид-контроллер), нам нужно создать модель. Как и в другие библиотеках MVC, модель является компонентом, который отвечает за связь с базой данных. Наша таблица проста и модель тоже будет простой:
<?php
class Todo extends Eloquent
{
    public static $timestamps = true;
}

Имя класса “Todo, и Laravel автоматически ассоциирует его с таблицей todos в базе данных. Eloquent - класс ORM для модели, который используется в Laravel. Он обеспечивает работу с объектами базы данных.

Свойство $timestamps получает значение true, что указывает на автоматическое обновление полей created_at и updated_at при создании или обновлении записи соответственно.

Контроллер
Теперь создадим контроллер. В нем содержится вся логика приложения и он должен обеспечивать функционал:

Возвращение всех записей в таблице в виде списка;
Возвращение информации определенной записи по указанному id;
Удаление записи по указанному id;
Представление формы для добавления новой записи:
Добавление новой записи в базу данных и представление сообщения о завершении операции.
Таким образом у нас будет следующий контроллер с пятью методами, которые будут вызываться для совершения действий. Объявление действия должно иметь префикс “action_”.
<?php
class Todo_Controller extends Base_Controller
{
    public function action_list() {
        $todos = Todo::all();
        return View::make("list")
                ->with("todos", $todos);
    }
 
    public function action_view($id) {
        $todo = Todo::where_id($id)->first();
        return View::make("view")
                ->with("todo", $todo);
    }
 
    public function action_delete($id) {
        $todo = Todo::where_id($id)->first();
        $todo->delete();
        return View::make("deleted");
    }
 
    public function action_new() {
        return View::make("add");
    }
 
    public function action_add() {
        $todo = new Todo();
        $todo->title = Input::get("title");
        $todo->description = Input::get("description");
        $todo->save();
        return View::make("success");
    }
}

Код достаточно простой и понятный. Первый метод action_list() получает все записи в базе данных. Здесь проявляется мощь Eloquent, который облегчает задачу, используя только Todo::all() для получения данных. Затем он возвращает вид со всеми записями, связанными с переменной $todos.

Другие методы также легко разобрать. Они выполняют манипуляции с объектом базы данных и возвращают вид со связанными данными. Последний метод может быть исключением, action_add() вызывается при передаче формы для добавления новой записи TODO. Input::get() используется для получения значений из переданной формы.

Вид
Теперь пришла пора заняться видом. Laravel использует собственный механизм шаблонов Blade, который дает нам простой и легко читаемый код. Мы приведем пример первого вида - список. С помощью выражения make/with при возвращении вида в методе action_list() мы поместили результат работы Todo::all() в $todos. Теперь можно использовать ее в виде:
<h2>Todo list</h2>
<p>{{ HTML::link_to_action("todo@new", "Add new todo") }}</p>
<ul>
@foreach($todos as $todo)
    <li>{{ HTML::link_to_action("todo@view", $todo->title, array($todo->id)) }} - {{ HTML::link_to_action("todo@delete", "Delete", array($todo->id)) }}</li>
@endforeach
</ul>

Сначала Blade создает ссылку на действие контроллера action_new(). Затем используется выражение foreach, которое очень похоже на оригинальное одноименное выражение PHP. Для каждой задачи мы создаем ссылку на вид записи. Вторым параметром является ссылка на текст $todo->title, а далее следуют любые параметры действия. Нам нужен ID записи, которую надо выводить - $todo->id. Также создаем ссылку на задачу удаления с использованием ID в качестве параметра.

Заключение
Действительно, создание приложения с помощью Laravel - простая задача. Код получается понятным и структурированным.

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

Добавлено: 08 Мая 2018 10:34:02 Добавил: Андрей Ковальчук

Создание Фрэймворка на PHP5 – Часть 3

CMS

Теперь, когда движок нашего фрэймворка готов, мы можем заняться другими вещами. К примеру, интеграцией дизайна в наш PHP фрэймворк. На данный момент, мы сконцентрируемся на оформлении фронт энда и попытаемся организовать этот процесс простым настолько, насколько это возможно.

Как всё собрать в кучу
Итак, у нас есть наши файлы с кодом, рассортированных в каталоги и несколько объектов, доступ к которым получаем из центра управления. Одним из этих объектов является обработчик шаблонов, который позволяет нам без особых сил генерировать HTML код. Также мы можем с его помощью выводить изображения, css и другие шаблоны, которые формируют дизайн нашего приложения.

Шаг 1: Что нам необходимо для дизайна
Дизайн фронт энда не должен быть слишком уж сложным. Будет круто, если структура HTML шаблона будет пригодна для всех фичей, которые поддерживаются нашим фрэймворком. Минимум, который нам нужен - это:

Основной блок, где будет располагаться инфа. Назовём его #content
Также нам понадобится несколько колонок, которые будут храниться в #content
Наверняка нам понадобится таблица.
Нумерованные и ненумерованные списки (и другие виды списков).
Изображение. Я привык использовать технику присвоения специального стиля для фоток в HTML коде; к примеру, вот так: <img class=”photo” src=”images/photo.jpg” alt=”Photograph” />.
Формы для получения данных.
<head>
А теперь настало время создать простую структуру нашей XHTML страницы. Начнём мы вот с чего:

	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
	<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
	<head>
	<title>{pagetitle}</title>
	<meta name="description" content="{metadesc}" />
	<meta name="keywords" content="{metakey}" />
	<style type="text/css" title="Default page style" media="screen"><!--@import "skins/fmwk/style.css";--></style>
	<link rel="icon" href="favicon.ico" type="image/x-icon" />
	<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
	</head>
	<body>

Если вы хотите, то можете поменять doctype на тот, который используете в своей повседневной практике. Также ничего не мешает вам отредактировать и другие параметры, например, такие как язык

lang. В этом же фрагменте кода мы подключаем наши основные таблицы стилей. В добавок, вписываем теги мета для ключевых слов и описания, которые будут меняться в зависимости от страницы. Заменитель {pagetitle} будет использоваться для того, чтобы вставлять название страницы, которую будет просматривать пользователь.

<body>
Теперь мы можем перейти непосредственно к телу нашей XHTML страницы, для того чтобы дополнить структуру дизайна нашего фрэймворка. Сейчас мы ничего не будем заполнять, а просто создадим несколько блоков, которые часто используются разработчиками. Добавим блоки header, content, column и footer.
	<div id="wrapper">
	<div id="header">
	 
	</div>
	<div id="content">
	 
	</div><!--/content-->
	<div id="column">
	 
	</div><!--/column-->
	 
	<div id="footer">
	 
	</div><!--/footer-->
	 
	</div><!--/wrapper-->
	</body>
	</html>

Шаг 2: основной контент
Как я и обещал, теперь мы перейдём к описанию основного контента, который будет состоять из тегов, которые так или иначе часто встречаются на веб страницах:
	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
	<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
	<head>
	<title>{pagetitle}</title>
	<meta name="description" content="{metadesc}" />
	<meta name="keywords" content="{metakey}" />
	<style type="text/css" title="Default page style" media="screen"><!--@import "skins/fmwk/style.css";--></style>
	<link rel="icon" href="favicon.ico" type="image/x-icon" />
	<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
	</head>
	<body>
	<div id="wrapper">
	<div id="header">
	<h2><a href="#" title="Website name">Website name</a></h2>
	</div>
	<div id="content">
	<h1>{pagetitle}</h1>
	<img class="photo" src="photo.jpg" alt="Photo test" />
	<p>
	Lorem ipsum dolor sit amet, <strong>consectetuer adipiscing elit</strong>. Quisque urna augue, fringilla quis, pulvinar non, feugiat in, pede. Curabitur vitae pede. Cras vehicula varius tellus. Sed consequat, enim tristique euismod volutpat, <em>tellus magna aliquet risus</em>, id aliquet eros metus at purus.
	</p>
	<h2>Secondary heading</h2>
	<p>
	Aliquam dictum, nibh eget <a href="#" title="Test link">ullamcorper condimentum</a>, magna turpis placerat pede, tempor facilisis tortor urna commodo turpis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras luctus cursus velit. Nullam imperdiet turpis.
	</p>
	<h3>Tertiary heading</h3>
	<table>
	<tr>
	<th>Heading</th>
	<td>Data</td>
	</tr>
	<tr>
	<th>Heading</th>
	<td>Data</td>
	</tr>
	</table>
	<p>
	<img src="image.jpg" alt="Generic image" />
	Cras a eros eget lorem fermentum malesuada. Phasellus condimentum libero vel lacus. Donec lectus nisl, adipiscing malesuada, sodales tincidunt, sagittis vitae, lacus. Proin nec pede. Maecenas adipiscing adipiscing risus.
	</p>
	</div><!--/content-->
	<div id="column">
	 
	<ul>
	<li>List item</li>
	<li>List item</li>
	<li>List item</li>
	</ul>
	 
	<ol>
	<li>List item</li>
	<li>List item</li>
	<li>List item</li>
	<li>List item</li>
	</ol>
	 
	</div><!--/column-->
	 
	<div id="footer">
	<p>
	© Website name, 2008.
	</p>
	</div><!--/footer-->
	 
	</div><!--/wrapper-->
	</body>
	</html>

Теперь наш контент готов к тому, чтобы мы его слегка оформили.

Шаг 3: стиль
В первую очередь, нам необходимо сбросить все начальные значения HTML элементов, такие как внешние и внутренние отступы:
	body, * {
	margin: 0;
	padding 0;
	}

Теперь оформим ещё несколько элементов:
	body {
	background: #FFF;
	color: #000;
	font-family: "helvetica", "arial", "verdana", sans-serif;
	font-size: 62.5%;
	}
	 
	a, a:active, a:link  {
	color: #1A64AC;
	text-decoration: underline;
	}
	 
	a:visited {
	color: #0D2F4F;
	}

Теперь нам надо сконцентрировать наше внимание на блоке #wrapper, а также добавить небольшую границу всем основным элементам страницы.
	#wrapper {
	margin: 0 auto;
	width: 950px;
	}
	 
	#wrapper, #header, #content, #column, #footer {
	border: 1px #DDD solid;
	}

Внимание! В Internet Explorer 6 возможны глюки. Давайте добавим ещё немного CSS, для того чтобы окончательно добить оформление структуры фронт энда нашего фрэймворка – всё остальное это просто позиционирование:
	#column, #content {
	float: left;
	font-size: 125%;
	padding: 5px;
	}
	 
	#column {
	width: 200px;
	}
	 
	#content {
	margin-left 5px;
	width:  725px;
	}
	 
	#header, #footer {
	clear: both;
	}

Всё что осталось сделать, так это написать парочку стилей для изображений:
	#column img, #content img {
	border: 2px #DDD solid;
	float: left;
	margin: 0 5px 0 10px;
	}
	img.photo {
	background: #DDD;
	float: right !important;
	padding: 25px 2px;
	}

И вот что у нас в конечном итоге получилось:

Конечно же тут всё может быть намного сложнее, однако это не было целью данного урока. Мне нужно было вам показать, как внедрить XHTML и CSS в наш новоиспечённый фрэймворк.

Шаг 4: XHTML шаблоны
Следующее, что нам нужно разъяснить, так это передачу XHTML, CSS и изображений в наш PHP фрэймворк. Для этого нам необходимо разбить XHTML на три части: header, main и footer. Благодаря системе, которую мы построили за эти несколько уроков, наша страница может строиться из нескольких шаблонов.

Шаблон Header для PHP фрэймворка (skins/default/templates/header.tpl.php)
Данный шаблон будет хранить следующую информацию:
	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
	<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
	<head>
	<title>{pagetitle}</title>
	<meta name="description" content="{metadesc}" />
	<meta name="keywords" content="{metakey}" />
	<style type="text/css" title="Default page style" media="screen"><!--@import "style.css";--></style>
	<link rel="icon" href="favicon.ico" type="image/x-icon" />
	<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
	</head>
	<body>
	<div id="wrapper">
	<div id="header">
	<h2><a href="#" title="Website name">Website name</a></h2>
	</div>

Шаблон Main для PHP фрэймворка (skins/default/templates/main.tpl.php)
В этом шаблоне должны размещаться блоки div, которые будут содержать основной контент страницы. Теперь вместо вставки какого-то случайного текста, мы можем вставить специальные заменители, которые будут замещаться текстом.

Нам потребуется:

{pagetitle} для заголовка страницы.
{maincontent} для основного контента страницы.
{btitle} и {bcontent} заголовки и контент для дополнительной инфы.
	<div id="content">
	<h1>{pagetitle}</h1>
	{maincontent}
	</div><!--/content-->
	 
	<div id="column">
	<!-- START rcolumn -->
	<h2>{btitle}</h2>
	{bcontent}
	<!-- END rcolumn -->
	</div><!--/column-->

Шаблон Footer для PHP фрэймворка (skins/default/templates/footer.tpl.php)
И наконец, нам осталось создать шаблон для нижней части страницы. Обычно в этой части люди пишут копирайт и другую информацию.
	<div id="footer">
	<p>
	© Website name, 2008.
	</p>
	</div><!--/footer-->
	 
	</div><!--/wrapper-->
	</body>
	</html>

Я приношу свои извинения, т.к. немного изменил тему данной статьи. Думаю, в следующих частях данной серии мы рассмотрим вопросы безопасности, отправки email и возможности расширения данного фрэймворка.

Добавлено: 02 Мая 2018 12:29:39 Добавил: Андрей Ковальчук

Создание Фрэймворка на PHP5 – Часть 2

CMS

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

MVC
В первой части данного урока мы создали каталог с именем controllers для того, чтобы хранить в нём всю бизнес логику нашего приложения. Однако, если быть до конца честным, это не самое лучшее место, где можно хранить всю бизнес логику. На самом деле, для этих целей больше подойдёт каталог model. Разделение работы с базой данных и основной логики упрощает понимание кода и одновременно делает наш фрэймворк более мощным и расширяемым.

Так что же такое MVC? MVC - это шаблон проектирования (такой же как Singleton и Registry, которые мы рассмотрели в первой части). Это аббревиатура от Model View Controller. Главная цель MVC заключается в разделении бизнес логики и пользовательского интерфейса друг от друга. Теперь пришло самое время немного изменить структуру нашего проекта и создать каталог “models”. В моделях будет содержаться вся бизнес логика, а контроллеры будут взаимодействовать с ней и пользователем (допустим обрабатывать введённые данные). Метод __autoload изменять не нужно.

Обработчик для работы с БД
Многие сайты, которые написаны на PHP, также работают с различными системами базами данных, самая популярная из них - это MySQL. Если мы будет хранить всё, что связано с БД в одном месте, то (в теории) мы с лёгкостью можем сменить используемую технологию. Также мы сможем упростить некоторые задачи, такие как вставка, редактирование и удаление записей из базы.

Итак, что же будет делать наш обработчик:

Управление подключениями к базе данных
Попытка абстрагироваться от самой базы данных
Кэшировать запросы для многократного использования
Упростить основные операции взаимодействия с базой
Теперь давайте взглянем на код.

	<?php
	 
	class database {
	 
	    /**
	     * Массив для множества подключений.
	     * Используется редко, но бывает очень необходим
	     */
	    private $connections = array();
	 
	    /**
	     * Сообщает БД, какое подключение использовать
	     * setActiveConnection($id) для изменения
	     */
	    private $activeConnection = 0;
	 
	    /**
	     * Кэш для запросов
	     */
	    private $queryCache = array();
	 
	    /**
	     * Кэш для сформированных данных
	     */
	    private $dataCache = array();
	 
	    /**
	     * Запись последнего запроса
	     */
	    private $last;
	 
	    public function __construct()
	    {
	 
	    }
	 
	    /**
	     * Создание нового подключения
	     * @param String называние хоста
	     * @param String пользователь
	     * @param String пароль
	     * @param String имя базы данных
	     * @return int id нового подключения
	     */
	    public function newConnection( $host, $user, $password, $database )
	    {
	        $this->connections[] = new mysqli( $host, $user, $password, $database );
	        $connection_id = count( $this->connections )-1;
	        if( mysqli_connect_errno() )
	        {
	            trigger_error('Error connecting to host. '.$this->connections[$connection_id]->error, E_USER_ERROR);
	        }  
	 
	        return $connection_id;
	    }
	 
	    /**
	     * Закрытие активного подключения
	     * @return void
	     */
	    public function closeConnection()
	    {
	        $this->connections[$this->activeConnection]->close();
	    }
	 
	    /**
	     * Переключение между активными подключениями
	     * @param int новый id
	     * @return void
	     */
	    public function setActiveConnection( int $new )
	    {
	        $this->activeConnection = $new;
	    }
	 
	    /**
	     * Запись запроса в кэш для дальнейшего использования
	     * @param String строка запроса
	     * @return кэш
	     */
	    public function cacheQuery( $queryStr )
	    {
	        if( !$result = $this->connections[$this->activeConnection]->query( $queryStr ) )
	        {
	            trigger_error('Error executing and caching query: '.$this->connections[$this->activeConnection]->error, E_USER_ERROR);
	            return -1;
	        }
	        else
	        {
	            $this->queryCache[] = $result;
	            return count($this->queryCache)-1;
	        }
	    }
	 
	    /**
	     * Достать набор строк из кэша
	     * @param int указатель на кэш
	     * @return int количество строк
	     */
	    public function numRowsFromCache( $cache_id )
	    {
	        return $this->queryCache[$cache_id]->num_rows;
	    }
	 
	    /**
	     * Получить данные из кэша
	     * @param int указатель на кэш
	     * @return array строка
	     */
	    public function resultsFromCache( $cache_id )
	    {
	        return $this->queryCache[$cache_id]->fetch_array(MYSQLI_ASSOC);
	    }
	 
	    /**
	     * Записать данные в кэш
	     * @param array данные
	     * @return int указатель на данные в кэше
	     */
	    public function cacheData( $data )
	    {
	        $this->dataCache[] = $data;
	        return count( $this->dataCache )-1;
	    }
	 
	    /**
	     * Достаём данные из кэша
	     * @param int указатель
	     * @return array данные
	     */
	    public function dataFromCache( $cache_id )
	    {
	        return $this->dataCache[$cache_id];
	    }
	 
	    /**
	     * Удалить данные из базы
	     * @param String таблица
	     * @param String условие удаления
	     * @param int количество строк для удаления
	     * @return void
	     */
	    public function deleteRecords( $table, $condition, $limit )
	    {
	        $limit = ( $limit == '' ) ? '' : ' LIMIT ' . $limit;
	        $delete = "DELETE FROM {$table} WHERE {$condition} {$limit}";
	        $this->executeQuery( $delete );
	    }
	 
	    /**
	     * Обновление записей
	     * @param String название таблицы
	     * @param array значения в виде field => value
	     * @param String условие
	     * @return bool
	     */
	    public function updateRecords( $table, $changes, $condition )
	    {
	        $update = "UPDATE " . $table . " SET ";
	        foreach( $changes as $field => $value )
	        {
	            $update .= "`" . $field . "`='{$value}',";
	        }
	 
	        $update = substr($update, 0, -1);
	        if( $condition != '' )
	        {
	            $update .= "WHERE " . $condition;
	        }
	 
	        $this->executeQuery( $update );
	 
	        return true;
	 
	    }
	 
	    /**
	     * Вставка в базу
	     * @param String название таблицы
	     * @param array данные в виде field => value
	     * @return bool
	     */
	    public function insertRecords( $table, $data )
	    {
	        $fields  = "";
	        $values = "";
	 
	        foreach ($data as $f => $v)
	        {
	 
	            $fields  .= "`$f`,";
	            $values .= ( is_numeric( $v ) && ( intval( $v ) == $v ) ) ? $v."," : "'$v',";
	 
	        }
	 
	        $fields = substr($fields, 0, -1);
	        $values = substr($values, 0, -1);
	 
	        $insert = "INSERT INTO $table ({$fields}) VALUES({$values})";
	        $this->executeQuery( $insert );
	        return true;
	    }
	 
	    /**
	     * Выполнение запроса
	     * @param String запрос
	     * @return void
	     */
	    public function executeQuery( $queryStr )
	    {
	        if( !$result = $this->connections[$this->activeConnection]->query( $queryStr ) )
	        {
	            trigger_error('Error executing query: '.$this->connections[$this->activeConnection]->error, E_USER_ERROR);
	        }
	        else
	        {
	            $this->last = $result;
	        }
	 
	    }
	 
	    /**
	     * Получить строки из последнего выполненного запроса (исключая закэшированные)
	     * @return array
	     */
	    public function getRows()
	    {
	        return $this->last->fetch_array(MYSQLI_ASSOC);
	    }
	 
	    /**
	     * Извлечение числа полученных строк прошлого запроса
	     * @return int число строк
	     */
	    public function affectedRows()
	    {
	        return $this->$this->connections[$this->activeConnection]->affected_rows;
	    }
	 
	    /**
	     * Фильтрация данных
	     * @param String данные на фильтрацию
	     * @return String отфильтрованные данные
	     */
	    public function sanitizeData( $data )
	    {
	        return $this->connections[$this->activeConnection]->real_escape_string( $data );
	    }
	 
	    /**
	     * Деконструкция объекта
	     * закрытие всех подключений
	     */
	    public function __deconstruct()
	    {
	        foreach( $this->connections as $connection )
	        {
	            $connection->close();
	        }
	    }
	}
	?>

Перед тем как перейти к более подробному описанию, я должен признаться, что функционал данного обработчика довольно-таки прост. Мы полностью абстрагировались от выполнения запросов напрямую. И это круто!

Методы для удаления, обновления и вставки новых записей позволят нам более простым способом решать некоторые задачи (к тому же, их функционал всегда можно расширить). Все что нам нужно - это только передать название таблицы, массив с данными, число для лимита и условие. Запросы также можно кэшировать, чтобы использовать их в дальнейшем. Рассмотрим пример использования.

	// вставка записи
	$registry->getObject('db')->insertRecords( 'testTable', array('name'=>'Michael' ) );
	// обновление записи
	$registry->getObject('db')->updateRecords( 'testTable', array('name'=>'MichaelP' ), 'ID=2' );
	// удаление записи
	$registry->getObject('db')->deleteRecords( 'testTable', "name='MichaelP'", 5 );

Также мы без особых затруднений можем работать с несколькими подключениями (к примеру следующий фрагмент кода позволяет нам удалить данные из двух разных баз).
	// создаём второе подключение (представим, что одно у нас уже есть)
	$newConnection = $registry->getObject('db')->newConnection('localhost', 'root', 'password', 'secondDB');
	// удаляем из первой базы
	$registry->getObject('db')->deleteRecords( 'testTable', "name='MichaelP'", 5 );
	// меняем подключение
	$registry->getObject('db')->setActiveConnection( $newConnection );
	// удаляем из второй базы
	$registry->getObject('db')->deleteRecords( 'testTable', "name='MichaelP'", 5 );
	// снова меняем подключение
	$registry->getObject('db')->setActiveConnection( 0 );

Как и что можно добавить в этот класс?

Полную абстракцию
Применить наследование. Создать для каждой технологии баз данных свой отдельный интерфейс.
Хранить ID подключения на ряду с закэшированным запросом
Улучшить фильтрацию данных (согласно каким-то критериям)
Менеджер шаблонов
Менеджер шаблонов будет взаимодействовать с процессом вывода информации, он должен уметь работать с различными файлами шаблонов, метками для замены (я буду называть их теги) и конечно же уметь взаимодействовать с предыдущим классом, который мы написали.

Для того чтобы не усложнять нашу задачу, мы организуем работу так, чтобы менеджер шаблонов мог управлять объектом страницы.
	<?php
	 
	// устранение доступа напрямую
	if ( ! defined( 'PCAFW' ) )
	{
	    echo 'This file can only be called via the main index.php file, and not directly';
	    exit();
	}
	 
	/**
	 * Класс менеджера шаблонов
	 */
	class template {
	 
	    private $page;
	 
	    public function __construct()
	    {
	        include( APP_PATH . '/PCARegistry/objects/page.class.php');
	        $this->page = new Page();
	 
	    }
	 
	    /**
	     * Добавляем шаблон на страницу
	     * @param String $tag тег где будет вставлен шаблон {hello}
	     * @param String $bit путь или имя к файлу с шаблоном
	     * @return void
	     */
	    public function addTemplateBit( $tag, $bit )
	    {
	        if( strpos( $bit, 'skins/' ) === false )
	        {
	            $bit = 'skins/' . PCARegistry::getSetting('skin') . '/templates/' . $bit;
	        }
	        $this->page->addTemplateBit( $tag, $bit );
	    }
	 
	    /**
	     * Вставка шаблонов на страницу
	     * Происходит обновление контента страницы
	     * @return void
	     */
	    private function replaceBits()
	    {
	        $bits = $this->page->getBits();
	        foreach( $bits as $tag => $template )
	        {
	            $templateContent = file_get_contents( $bit );
	            $newContent = str_replace( '{' . $tag . '}', $templateContent, $this->page->getContent() );
	            $this->page->setContent( $newContent );
	        }
	    }
	 
	    /**
	     * Замена тегов на контент
	     * @return void
	     */
	    private function replaceTags()
	    {
	        $tags = $this->page->getTags();
	 
	        foreach( $tags as $tag => $data )
	        {
	            if( is_array( $data ) )
	            {
	 
	                if( $data[0] == 'SQL' )
	                {
	                    $this->replaceDBTags( $tag, $data[1] );
	                }
	                elseif( $data[0] == 'DATA' )
	                {
	                    $this->replaceDataTags( $tag, $data[1] );
	                }
	            }
	            else
	            {
	                $newContent = str_replace( '{' . $tag . '}', $data, $this->page->getContent() );
	                $this->page->setContent( $newContent );
	            }
	        }
	    }
	 
	    /**
	     * Замена контента на странице данными из БД
	     * @param String $tag тег области замены
	     * @param int $cacheId ID закэшированного запроса
	     * @return void
	     */
	    private function replaceDBTags( $tag, $cacheId )
	    {
	        $block = '';
	        $blockOld = $this->page->getBlock( $tag );
	 
	        while ($tags = PCARegistry::getObject('db')->resultsFromCache( $cacheId ) )
	        {
	            $blockNew = $blockOld;
	            foreach ($tags as $ntag => $data)
	            {
	                $blockNew = str_replace("{" . $ntag . "}", $data, $blockNew);
	            }
	            $block .= $blockNew;
	        }
	        $pageContent = $this->page->getContent();
	        $newContent = str_replace( '<!-- START ' . $tag . ' -->' . $blockOld . '<!-- END ' . $tag . ' -->', $block, $pageContent );
	        $this->page->setContent( $newContent );
	    }
	 
	    /**
	     * Замена контента страницы данными из кэша
	     * @param String $tag тег области замены
	     * @param int $cacheId ID закэшированных данных
	     * @return void
	     */
	    private function replaceDataTags( $tag, $cacheId )
	    {
	        $block = $this->page->getBlock( $tag );
	        $blockOld = $block;
	        while ($tags = PCARegistry::getObject('db')->dataFromCache( $cacheId ) )
	        {
	            foreach ($tags as $tag => $data)
	            {
	                $blockNew = $blockOld;
	                $blockNew = str_replace("{" . $tag . "}", $data, $blockNew);
	            }
	            $block .= $blockNew;
	        }
	        $pageContent = $this->page->getContent();
	        $newContent = str_replace( $blockOld, $block, $pageContent );
	        $this->page->setContent( $newContent );
	    }
	 
	    /**
	     * Получить объект страницы
	     * @return Object
	     */
	    public function getPage()
	    {
	        return $this->page;
	    }
	 
	    /**
	     * Отобразить содержание страницы в зависимости от числа шаблонов
	     * @return void
	     */
	    public function buildFromTemplates()
	    {
	        $bits = func_get_args();
	        $content = "";
	        foreach( $bits as $bit )
	        {
	 
	            if( strpos( $bit, 'skins/' ) === false )
	            {
	                $bit = 'skins/' . PCARegistry::getSetting('skin') . '/templates/' . $bit;
	            }
	            if( file_exists( $bit ) == true )
	            {
	                $content .= file_get_contents( $bit );
	            }
	 
	        }
	        $this->page->setContent( $content );
	    }
	 
	    /**
	     * Сконвертировать массив с данными в теги
	     * @param array данные
	     * @param string префикс
	     * @return void
	     */
	    public function dataToTags( $data, $prefix )
	    {
	        foreach( $data as $key => $content )
	        {
	            $this->page->addTag( $key.$prefix, $content);
	        }
	    }
	 
	    public function parseTitle()
	    {
	        $newContent = str_replace('<title>', '<title>'. $page->getTitle(), $this->page->getContent() );
	        $this->page->setContent( $newContent );
	    }
	 
	    /**
	     * Формируем страницу
	     * @return void
	     */
	    public function parseOutput()
	    {
	        $this->replaceBits();
	        $this->replaceTags();
	        $this->parseTitle();
	    }
	 
	}
	?>

Данный класс создаёт объект страницы в зависимости от шаблона. Объект страницы содержит контент и информацию, которая необходима для создания HTML страницы. Затем мы используем метод buildFromTemplate(‘templatefile.tpl.php’, ‘templatefile2.tpl.php’) для получения начального содержания нашей страницы. Данный метод принимает целый набор названий шаблонов и производит последовательную обработку.

Управляет содержанием страницы, помогая объекту страницы вести учёт данных, которые будут заменены, а также применение новых шаблонов, которые добавляются при помощи (addTemplateBit(‘userbar’,'usertoolsbar.tpl.php’)).

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

Объект страницы
Объект страницы управляется менеджером шаблонов и предназначен для всей информации о странице. Разделение этих объектов упрощает их использование, что опять же открывает нам возможность расширять функциональность данных классов.
	<?php
	 
	/**
	 * Это объект страницы
	 * Предназначен для добавления какой-то новой функциональности
	 * К примеру, добавления пароля, специальных файлов css/js, и т.д.
	 */
	class page {
	 
	    private $css = array();
	    private $js = array();
	    private $bodyTag = '';
	    private $bodyTagInsert = '';
	 
	    // будующая функциональность
	    private $authorised = true;
	    private $password = '';
	 
	    // элементы страницы
	    private $title = '';
	    private $tags = array();
	    private $postParseTags = array();
	    private $bits = array();
	    private $content = "";
	 
	    /**
	     * Конструктор...
	     */
	    function __construct() { }
	 
	    public function getTitle()
	    {
	        return $this->title;
	    }
	 
	    public function setPassword( $password )
	    {
	        $this->password = $password;
	    }
	 
	    public function setTitle( $title )
	    {
	        $this->title = $title;
	    }
	 
	    public function setContent( $content )
	    {
	        $this->content = $content;
	    }
	 
	    public function addTag( $key, $data )
	    {
	        $this->tags[$key] = $data;
	    }
	 
	    public function getTags()
	    {
	        return $this->tags;
	    }
	 
	    public function addPPTag( $key, $data )
	    {
	        $this->postParseTags[$key] = $data;
	    }
	 
	    /**
	     * Получить все теги после первого анализа
	     * @return array
	     */
	    public function getPPTags()
	    {
	        return $this->postParseTags;
	    }
	 
	    /**
	     * Добавления шаблона, но не содержания
	     * @param String куда добавить шаблон
	     * @param String имя файла с шаблоном
	     * @return void
	     */
	    public function addTemplateBit( $tag, $bit )
	    {
	        $this->bits[ $tag ] = $bit;
	    }
	 
	    /**
	     * Получает шаблоны присутствующие на странице
	     * @return array массив тегов
	     */
	    public function getBits()
	    {
	        return $this->bits;
	    }
	 
	    /**
	     *Получить фрагмент содержимого страницы
	     * @param String блок ( <!-- START tag --> block <!-- END tag --> )
	     * @return String контент блока
	     */
	    public function getBlock( $tag )
	    {
	        preg_match ('#<!-- START '. $tag . ' -->(.+?)<!-- END '. $tag . ' -->#si', $this->content, $tor);
	 
	        $tor = str_replace ('<!-- START '. $tag . ' -->', "", $tor[0]);
	        $tor = str_replace ('<!-- END '  . $tag . ' -->', "", $tor);
	 
	        return $tor;
	    }
	 
	    public function getContent()
	    {
	        return $this->content;
	    }
	 
	}
	?>

Загрузка объектов
Теперь самое время сообщить нашему фрэймворку о новой функциональности, которую мы только что написали. Я создал специальный метод для объекта PCARegistry с именем loadCoreObjects, который загружает объекты. Это необходимо для того, чтобы мы могли иметь доступ к объектам из файла index.php.
	public function storeCoreObjects()
	{
	    $this->storeObject('database', 'db' );
	    $this->storeObject('template', 'template' );
	}

Какие-то данные
Для того чтобы продемонстрировать работу нашего фрэймворка, нам необходимо создать базу данных и таблицу. Сейчас нам подойдёт самая простая структура: ID, имя, email.

Честно говоря, нам нужно всего несколько строк из таблицы!

Шаблон на скорую руку
Для того чтобы продемонстрировать работу фрэймворка в полной мере, создадим небольшой шаблон.
	<html>
	<head>
	    <title> Powered by PCA Framework</title>
	</head>
	<body>
	<h1>Our Members</h1>
	<p>Below is a list of our members:</p>
	<ul>
	<!-- START members -->
	<li>{name} {email}</li>
	<!-- END members -->
	</ul>
	</body>
	</html>

START и END - это блок (он будет выбираться при помощи метода getBlock()). Как раз в этом месте менеджер шаблона будет вставлять данные из базы.

Используем фрэймворк
Теперь соберём всё вместе в нашем index.php:
	require_once('PCARegistry/pcaregistry.class.php');
	$registry = PCARegistry::singleton();
	 
	$registry->storeCoreObjects();
	 
	$registry->getObject('db')->newConnection('localhost', 'root', '', 'pcaframework');
	 
	$registry->storeSetting('default', 'skin');
	 
	// подключаем шаблон
	$registry->getObject('template')->buildFromTemplates('main.tpl.php');
	 
	// кэшируем запрос
	$cache = $registry->getObject('db')->cacheQuery('SELECT * FROM members');
	 
	// добавляем тег
	$registry->getObject('template')->getPage()->addTag('members', array('SQL', $cache) );
	 
	// назначаем заголовок страницы
	$registry->getObject('template')->getPage()->setTitle('Our members');
	 
	// парсим и выводим
	$registry->getObject('template')->parseOutput();
	print $registry->getObject('template')->getPage()->getContent();

Если мы откроем этот файл в браузере, то увидим следующие результаты:

Грядёт третья часть…
В третьей части мы начнём создание небольшого приложения. Как раз там и посмотрим на то, как работают все эти классы более подробно. До встречи!

Добавлено: 02 Мая 2018 12:22:34 Добавил: Андрей Ковальчук

Создание Фрэймворка на PHP5 – Часть 1

CMS

Всё больше и больше нам приходится создавать динамических и интерактивных сайтов. Именно поэтому множество разработчиков начинают применять в своей работе различные фрэймворки. Существует немало хороших фрэймворков, однако собственный экземпляр может быть подстроен полностью под ваши нужды и значительно увеличить скорость разработки проекта, конечно же, если вы знаете, как с ним взаимодействовать. В этой небольшой серии уроков, мы покажем вам, как создать собственный фрэймворк на PHP5! В этом уроке мы начнём с планирования и закончим реализацией структуры.

*Заметка: при запуске демо вы должны увидеть “PCA Framework version 0.1 the HTML output”, что продемонстрирует успешную работу нашего приложения на первой стадии его разработки.

Шаг 1: Немного об этой серии уроков
В этой серии уроков мы будем создавать свой собственный фрэймворк на PHP5 с нуля. Он будет включать в себя все основные фишки системы управления контентом. Создавать мы его будем для нужд небольшой организации, ориентирующейся на веб разработку.

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

В течении нескольких недель мы рассмотрим следующие темы:

Создание обработчика аутентификации, уровень абстракции базы данных и менеджер шаблонов
Объединение трёх объектов перечисленных выше воедино
Использование фрэймворка для управления содержимым сайта
Создание фантастического дизайна для лицевой и внутренней части сайта
Проектирование и реализация процесса входа в систему
Возможность расширения сконструированного фрэймворка

Шаг 2: Шаблоны проектирования и их использование в нашем фрэймворке
Когда речь идёт о создании какой-то крупной электронной системы, будь то десктоп программа или веб приложение, то всегда будет вставать ребром вопрос о том, какую архитектуру применить для её имплементации.

Шаблоны проектирования - это как раз то, к чему нужно обратиться при появлении подобной задачи. Для того чтобы подобрать необходимый шаблон для нашего проекта, нужно перебрать целую кучу подобных инструментов функциональной основы. Всё это нужно для того чтобы наша система была флексибильна! Ладно, больше не томлю! В этом уроке мы рассмотрим шаблоны проектирования под названием Singleton и Registry.

Шаг 3: Файлы и папки
Первое, что мы должны сделать, прежде чем приступим к реализации нашего фрэймворка, так это определиться с структурой каталогов, в которых будут храниться наши скрипты и прочие файлы. Мы готовим что-то вроде полигона, на котором будут располагаться наши войска. Большинство разработчиков используют следующие каталоги для подобных целей:

Каталог для хранения классов или файлов с функциями
Каталог для бизнес логики
Каталог для элементов дизайна
Для нас эта инфа - прекрасная отправная точка, потому как файлы, которые относятся к одной из этих 3х категорий, можно объединить в группы. Взгляните на структуру папок, которая приведена ниже. Чуть позже мы всё это детально обсудим.

Учтите, что каталоги .settings .project были созданы IDE, который я использую, и не претендуют на то, чтобы вы их создавали у себя

Базовые функции и объекты, такие как подключение к базе данных, аутентификация, обработчик шаблонов, объекты работающие с электронными сообщениями, объекты, обрабатывающие электронные сообщения, должны располагаться все вместе в каталоге objects, который в свою очередь располагается в папке PCARegistry. Это позволит нам отделить логику, относящуюся к Registry (по которому мы пробежимся вскользь) от самого объекта Registry.

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

Элементы дизайна сайта и его шаблоны будут храниться в каталоге skins. Т.к. в перспективе мы хотим использовать несколько дизайнов (например, для того чтобы пользователи сами выбирали, в каком стиле они хотят видеть сайт, или различный дизайн в зависимости от времён года или в честь какого-то праздника), то каждый из этих шаблонов будет храниться в отдельном каталоге.

Шаг 4: Registry
Сердцевина нашего фрэймворка будет состоять из метода, взаимодействующего с базами данных, пользовательской авторизации и т.д. Имплементация шаблона проектирования Registry позволит нам хранить все эти методы централизованно, предоставляя лёгкий способ взаимодействия нашего фрэймворка с другими системами.

Шаблон проектирования Registry использует ссылки на объекты и в своём функционале очень похож на телефонную книгу, которая хранит и извлекает контактные данные. Этот механизм мы будем использовать, для того чтобы иметь доступ к объектам, настройкам системы, а также при необходимости передавать данные и другую информацию в любую часть приложения.

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

Ниже, в файле registry.class.php мы побыстренькому пройдёмся по тому, как это всё работает.

	<?php
	/**
	 * Объект PCARegistry
	 * Имплементация шаблонов проектирования Registry и Singleton
	 *
	 * @версия 0.1
	*/
	class PCARegistry {
	 
	    /**
	     * Массив для объектов
	     * @access private
	     */
	    private static $objects = array();
	 
	    /**
	     * Массив для настроек
	     * @access private
	     */
	    private static $settings = array();
	 
	    /**
	     * Название фрэймворка понятное для человека
	     * @access private
	     */
	    private static $frameworkName = 'PCA Framework version 0.1';
	 
	    /**
	     * Экземпляр нашего класса
	     * @access private
	     */
	    private static $instance;
	 
	    /**
	     * Закрытый конструктор (для того чтобы нельзя было вызвать напрямую)
	     * @access private
	     */
	    private function __construct()
	    {
	 
	    }
	 
	    /**
	     * метод singleton для получения доступа к объекту
	     * @access public
	     * @return
	     */
	    public static function singleton()
	    {
	        if( !isset( self::$instance ) )
	        {
	            $obj = __CLASS__;
	            self::$instance = new $obj;
	        }
	 
	        return self::$instance;
	    }
	 
	    /**
	     * запрещаем клонирование объекта
	     */
	    public function __clone()
	    {
	        trigger_error( 'Cloning the registry is not permitted', E_USER_ERROR );
	    }
	 
	    /**
	     * Подключаем класс
	     * @param String $object имя объекта
	     * @param String $key ключ для массива
	     * @return void
	     */
	    public function storeObject( $object, $key )
	    {
	        require_once('objects/' . $object . '.class.php');
	        self::$objects[ $key ] = new $object( self::$instance );
	    }
	 
	    /**
	     * Получаем объект
	     * @param String $key ключ для массива
	     * @return object
	     */
	    public function getObject( $key )
	    {
	        if( is_object ( self::$objects[ $key ] ) )
	        {
	            return self::$objects[ $key ];
	        }
	    }
	 
	    /**
	     * Передаём настройки
	     * @param String $data
	     * @param String $key для массива
	     * @return void
	     */
	    public function storeSetting( $data, $key )
	    {
	        self::$settings[ $key ] = $data;
	 
	    }
	 
	    /**
	     * Получаем настройки
	     * @param String $key ключ для массива
	     * @return void
	     */
	    public function getSetting( $key )
	    {
	        return self::$settings[ $key ];
	    }
	 
	    /**
	     * Получаем название фрэймворка
	     * @return String
	     */
	    public function getFrameworkName()
	    {
	        return self::$frameworkName;
	    }
	 
	}
	 
	?>

Итак, как же работает Registry?

Все объекты хранятся в массиве.

Когда в Registry передаётся новый объект, то автоматически подключается класс, где хранится функциональность данного объекта.

Доступ к данным объектам осуществляется при помощи “ключа” объекта, передаваемого в метод getObject.

Вы спросите: ну как же происходит предотвращение создания копии объекта Registry?

Наш конструктор имеет модификатор доступа private, для того чтобы предотвратить создание объекта класса напрямую.

Попытка клонировать объект приведёт к возникновению ошибки.

Если нам необходимо получить доступ к объекту в какой-то другой части нашего фрэймворка, или по какой-либо причине не можем достучаться к нему напрямую, мы можем использовать метод singleton ( PCARegistry::singleton() ) для получения экземпляра Registry.

Шаг 5: index.php
Перед тем как объяснить, каким образом мы будем приписывать функциональность в будущих уроках, давайте взглянем на то, как мы можем достучаться до Registry, и работать с нашим фрэймворком из одной точки – файла index.php.

ЧПУ или другими словами, человеко-понятные урлы можно встретить уже практически в любом веб приложении. Единственный способ реализации такого же эффекта, заключается в том, чтобы все запросы шли через файл index.php. В будущих уроках, мы создадим специальный файл .htaccess, для того чтобы любая часть нашего приложения перенаправляла пользователя на файл index.php.

Ниже вы можете увидеть код файла index.php. Конечно же это не вся его функциональность. Это то, что нужно нам на данный момент.
	<?php
	// Первое, что нужно сделать - начать сессию
	session_start();
	 
	// определим некоторые константы
	// такие как путь к корню нашего приложения
	define( "APP_PATH", dirname( __FILE__ ) ."/" );
	// а эту для того, чтобы убедиться, что скрипты не будут вызываться за пределами нашего фрэймворка
	define( "PCAFW", true );
	 
	/**
	 * Магическая функция autoload
	 * используется для того чтобы подключить нужный нам контроллер
	 * @param String название класса
	 */
	function __autoload( $class_name )
	{
	    require_once('controllers/' . $class_name . '/' . $class_name . '.php' );
	}
	 
	// подключаем то, что создали ранее
	require_once('PCARegistry/pcaregistry.class.php');
	$registry = PCARegistry::singleton();
	 
	// выведем название фрэймворка, для того чтобы убедиться, что всё работает
	print $registry->getFrameworkName();
	 
	exit();
	 
	?>

Итак…что же делает этот файл на данный момент?

Вызывает функцию start_session, для того чтобы мы могли получать доступ к необходимым данным из любой части фрэймворка (данную функцию нужно вызывать в самом начале файла)
Далее определяем константу, которая будет хранить путь к корневой части нашего фрэймворка, для того чтобы мы могли ссылаться на него из любой части приложения. В добавок создаём ещё одну константу для страховки, чтобы никакие посторонние скрипты не могли обращаться к нашим файлам.
Далее реализуем функцию autoload, для того чтобы определить, откуда подкачивать необходимые классы. В нашем случае, это путь к каталогу с контроллерами, поскольку именно там мы храним всю бизнес логику.
Потому мы подключаем класс, созданный в начале данного урока
В конце мы выводим название нашего фрэймворка, чтобы убедиться, что он находится в рабочем состоянии
На то, как работает объект Registry в нашем фрэймворке, мы создадим dummy класс. Мы можем это продемонстрировать благодаря классу template.class.php, который хранится в каталоге PCARegsitry/objects. Для этого необходимо добавить новый код в файл index.php.

После того, как мы создали $registry, можете вписать следующее:
	$registry->storeObject('template','template');

Если в классе, который мы только что упомянули реализован метод generateOutput, то мы без особых затруднений можем вызывать его в index.php:
	$registry->getObject('template')->generateOutput();

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

Добавлено: 02 Мая 2018 12:05:57 Добавил: Андрей Ковальчук

CMS своими руками. mod_rewrite. ЧПУ на сайте

CMS

Практически в каждом современном движке есть поддержка так называемой технологии ЧПУ.
ЧПУ или как на английском SEF расшифровывается, как «числово програмное управление», английский аналог несколько отличается «search engine friendly». Иногда встречается не коректрася расшифровка как «человекопонятный урл», но этот перевод отражает только смысл.
Ниже на простом примере я хочу показать как использовать этот самый мод

А использовать его следующим образом:
создаём на сервере файл .htaccess (если он ещё не создан) и дописываем в него следующие строки

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
#Замена ссылок на страницы
RewriteRule ^(.*)/(.*)/$ index.php?modul

если уже нечто подобное есть не страшно, допишите ещё… это будет дополнительное правило.
создайте файл index.php с таким текстом:
<?php
print_r($_GET);

теперь если вы напишете урл вида sitename.ru/news/1/ то вам скрипт выдаст следующее:
Array ( [module] => news [id] => 1 )

тоесть GET параметр $_GET[module] будет равен = news , а $_GET[id] = 1
А теперь немного о том как это всё работает

RewriteEngine On — говорит о том, что мы включаем mod_rewrite
RewriteBase / — говорит точку от которой начинать монтировать урлы
RewriteRule ^(.*)/(.*)/$ index.php?module=$1&id=$2 — это правило монтирования, оно и отвечает за преобразования урлов обратите внимание, данное правило разбито на 2 части, а именно ^(.*)/(.*)/$ и index.php?module=$1&id=$2
первая часть это обычное rejex выражение, какое вы скорее всего уже раньше использовали например в preg_replace. Оно определяет какие части урла выбираются, а именно выбирается часть урла по селектору (.*). Каждый такой селектор имеет свой индекс (номер по порядку) по значению которого можно вывести результат. Так например для первого (.*) результат будет присвоен в $1 для второго (.*) в $2, для третего (.*) в $3 и т.д.

Добавлено: 02 Мая 2018 11:42:16 Добавил: Андрей Ковальчук

CMS своими руками. Кэширование

CMS

Думаю ни одна современная цмска не может обойтись без системы кэширования. Кэширование как известно помогает обеспечить быстродействие. Для этого используется буферизация вывода с помощью ob_start();
Принцип кэширования следующий:
- сначала проверяется наличие уже сохранённой странички
- если такая страничка найдена, то она инклудится и дальнейшее выполнение кода не происходит, если её не существует, то начинается буферизация вывода
- конечный результат вывода записывается в файл и инклудится.

А теперь приступим к реализации данного механизма. Все действия я подробно закаментирую в коде.

<?php 

class Cache
{//Begin class
//Название класса
public $name="CachePage";
//Версия класса кэширования
public $version="_v_1.0";
//Хэш код файла
public $hash_code="";
//Переменная содержащая дату последнего кэширования, которую получаем в результате вычислений
public $modifi_file;
//Время последней модификации
//Устанавливается в секундах, по умолчанию 10 мин
public $time_file_cache=600;
//Хранение значения контента файла
//При буферизации
public $CONTENT;
/*
Главная функция кэшируемых файлов
*/
public function haupt_cache()
{
$this->code_file(); //Получение хэш кода файла
$this->clean_content(); //Получение и отчистка буфера
if($this->file_and_katalog_isset()==FALSE) $this->create_cache_file(); //Создания файла кэша если его нет
else $this->cache_file(); //Проверка последней модификации
if($this->modifi_file>$this->time_file_cache) $this->mod_file_cache(); //Модифицирование файла кэша
}
/*
Функция проверки существования файла и каталога кэширования
*/
public function file_and_katalog_isset()
{
if(file_exists($this->dir_cache."/".$this->hash_code.".php")) return TRUE; //Проверяет существование файла в каталоге кэширования
else return FALSE;
}
/*
Функция кэширования контента файла
*/
public function cache_content()
{
ob_start(); //Кэшируем контент файла
$this->dir_cache=DOC_ROOT."/vision/cache";
}
/*
Функция преобразования url файла в хэш код
*/
public function code_file()
{
$url=$GLOBALS['REQUEST_URI']; //Получаем url файла
$hashcode_file=md5($url); //Хэш код файла
$this->hash_code=$hashcode_file;
}
/*
Функция создания файла кэша если его нет в каталоге кэширования
*/
public function create_cache_file()
{
$open=fopen($this->dir_cache."/".$this->hash_code.".php", "at") or die(trigger_error("Невозможно открыть файл ".$this->hash_code.".php [".$this->name.$this->version."]", E_USER_ERROR)); //Создаём файл с кэшем
flock($open, LOCK_EX);//Блокируем файл
rewind($open); //Установка позиции в начало файла
$time_create_file=time(); //Время создания кэшируемого файла
$write_file=$this->CONTENT.""; //Формируем контент файла для записи
fwrite($open, $write_file); //Записываем данные
fclose($open); //Закрываем файл
readfile($this->dir_cache."/".$this->hash_code.".php");
}
/*
Функция проверки последнего кэширования файла
*/
public function cache_file()
{
$fopen=fopen($this->dir_cache."/".$this->hash_code.".php", "rt") or die(trigger_error("Невозможно открыть файл ".$this->hash_code.".php [".$this->name.$this->version."]", E_USER_ERROR)); //Открываем файл
$fread=fread($fopen, filesize($this->dir_cache."/".$this->hash_code.".php")); //Прочитываем контент файла
$time_mod=filectime($this->dir_cache."/".$this->hash_code.".php");
//preg_match_all("/()/i", $fread, $time_mod); //Ишем дату последней модификации файла
fclose($fopen);//Закрываем файл с кэшем
$mod_file=time()-$time_mod;
//$mod_file=time()-$time_mod[2][0]; //Время последнего кэширования файла
$this->modifi_file=$mod_file; //Присваиваем модификацию файла переменной
if($this->modifi_file<$this->time_file_cache) //Проверяем время последнего кэширования
{
readfile($this->dir_cache."/".$this->hash_code.".php"); //Подключаем файл
}
}
/*
Функция модификации файла если время полседнего кэширования истекло
*/
public function mod_file_cache()
{
unlink($this->dir_cache."/".$this->hash_code.".php"); //Удаляем кэшируемый файл так как время последней модификации истекло
$open=@fopen($this->dir_cache."/".$this->hash_code.".php", "at") or die(trigger_error("Невозможно открыть файл ".$this->hash_code.".php [".$this->name.$this->version."]", E_USER_ERROR)); //Открываем файл
flock($open, LOCK_EX);//Блокируем файл
rewind($open); //Установка

А теперь немного о том, как всем этим пользоваться
Для начала нам надо инициировать класс, делаем мы это следующим образом
$CACHE=new Cache;

Далее нам необходимо в начале кэшируемого участка вставить код
$CACHE->cache_content();

и в конце кода
$CACHE->haupt_cache();

для очистки кэша можно воспользоваться функцией
cleaning_cache_katalog("all");

если надо удалить только один файл кэша, то
cleaning_cache_katalog("file", стран

Добавлено: 02 Мая 2018 11:39:22 Добавил: Андрей Ковальчук

Создание простой MVC-системы на PHP 5

Предисловие
В этом руководстве Вы узнаете, как построить простую систему по архитектуре MVC (Model-View-Controller, Модель-Отображение-Контроллер) на PHP 5.1 с использованием возможностей библиотеки SPL (Standard PHP Library, Стандартная Библиотека PHP).

Введение

Добро пожаловать в первое полноценное руководство для PHP 5 на PHPit. Вам понадобится PHP 5.1 с установленной библиотекой SPL, так как мы воспользуемся некоторыми из самых последних возможностей PHP 5.

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

Одна точка входа

Одной из важных вещей в MVC является одна точка входа в приложение вместо кучи PHP-файлов, делающих примерно следующее:

<?php
include ('global.php'); 

// Здесь код страницы

?>


У нас будет один файл, обрабатывающий все запросы. Это значит, что нам не придётся мучиться с подключением global.php каждый раз, когда нам нужно создать новую страницу. Эта «одна точка входа» будет называться index.php и на данный момент будет такой:

<?php


// Тут чего-нибудь делаем


?>


Как Вы можете заметить, этот скрипт пока ещё ничего не делает, но погодите минутку.

Чтобы направить все запросы на главную страницу, мы воспользуемся mod_rewrite и установим в .htaccess директиву RewriteRule. Вставим следующий код в файл .htaccess и сохраним его в той же директории, что и index.php:

RewriteEngine on 

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule ^(.*)$ index.php?route=$1 [L,QSA]


Сперва мы проверяем, существует ли запрашиваемый файл, используя директиву RewriteCond, и, если нет, то перенаправляем запрос на index.php. Такая проверка на существование файла необходима, так как иначе index.php будет пытаться обрабатывать все запросы к сайту, включая запросы на изображения. А это нам как раз и не надо.

Если у Вас нет возможности использовать .htaccess или mod_rewrite, то Вам придётся вручную адресовать все запросы к index.php. Другими словами, все ссылки должны будут иметь вид «index.php?route=[здесь-идёт-запрос]». Например, «index.php?route=chat/index».

Теперь, когда все запросы идут через одну точку входа, мы можем начать написание скрипта index.php. Первая вещь, которую мы должны сделать, это инициализация системы. Создадим директорию includes, а в ней файл startup.php (он будет у нас файлом инициализации). Вставим следующий код в index.php:

<?php

error_reporting (E_ALL);

if (version_compare(phpversion(), '5.1.0', '<') == true) { die ('PHP5.1 Only'); }


// Константы:

define ('DIRSEP', DIRECTORY_SEPARATOR);


// Узнаём путь до файлов сайта

$site_path = realpath(dirname(__FILE__) . DIRSEP . '..' . DIRSEP) . DIRSEP;

define ('site_path', $site_path);


В этом примере мы объявляем некоторую константу, узнаём, где лежат файлы системы, а также проверяем, что версия PHP, ну, хотя бы, 5.1.

Следующая вещь, которую необходимо сделать, это объект Registry (журнал, реестр) для хранения глобальных значений. Он будет передаваться в отдельные объекты системы и использоваться для доступа к глобальным значениям, причём без необходимости обозначать переменные как «global» или обращаться к массиву $GLOBALS. Почитайте статью«Использование глобальных значений в PHP» для более подробной информации об объекте реестра.

Добавьте следующий код в файл startup.php после того кода, что приведён в предыдущем примере:

$registry = new Registry;


Если сейчас попробовать запустить систему, то можно увидеть следующую ошибку:

Fatal error: Class 'Registry' not found in g:\Projects\PHPit\content\simple mvc php5\demo\includes\startup.php on line 12


Это, конечно, не большой сюрприз для нас, ведь мы ещё не написали сам класс Registry. Файл с классом можно было бы просто подключить, используя функцию include() (Прим. пер.: кстати говоря, include() не такая уж и функция, а всё-таки выражение языка, управляющая структура, если смотреть по ману), но давайте воспользуемся одной из новых возможностей PHP 5: __autoload().

Волшебная функция __autoload() используется для динамической загрузки классов. Когда PHP обнаруживает несуществующий класс, он сначала вызывает функцию __autoload() и только затем выдаёт ошибку. Мы можем воспользоваться такой возможностью для загрузки классов «на лету».

Вставьте этот код перед кодом из предыдущего примера:

// Загрузка классов «на лету»

function __autoload($class_name) {

        $filename = strtolower($class_name) . '.php';

        $file = site_path . 'classes' . DIRSEP . $filename;


        if (file_exists($file) == false) {

                return false;

        }


        include ($file);

}


Наша функция __autoload() берёт имя класса, переданное ей как аргумент, и проверяет, существует ли файл с похожим именем в директории с классами. Если файла нет, то функция просто вернёт false и выскочит фатальная ошибка. Но если файл существует, он будет загружен. Т.е. объявится необходимый класс, и никакой ошибки не будет.

Мы ещё не создали сам класс Registry, поэтому ошибка всё ещё будет появляться. Давайте же займёмся этим.

Создание класса Registry

Класс Registry используется для передачи глобальных значений между отдельными объектами. Это на самом деле довольно простой класс, в котором нужно реализовать несколько маленьких методов.

Для начала создадим директорию classes и в ней файл registry.php. Вставим следующий код в registry.php:

<?php


Class Registry {

        private $vars = array();


}


?>


Теперь у нас есть «скелет» класса Registry и нужно нагрузить его методами. Напишем 2 метода: set(), чтобы устанавливать значения и get(), чтобы значения получать. Также можно написать метод remove() для удаления значений. Добавим эти методы в класс Registry:

function set($key, $var) {

        if (isset($this->vars[$key]) == true) {

                throw new Exception('Unable to set var `' . $key . '`. Already set.');

        }


        $this->vars[$key] = $var;

        return true;

}


function get($key) {

        if (isset($this->vars[$key]) == false) {

                return null;

        }


        return $this->vars[$key];

}


function remove($var) {

        unset($this->vars[$key]);

}


?>


Эти методы простые, они устанавливают, получают и удаляют элементы из массива $vars, который является атрибутом класса. В методе set() мы заодно проверяем, не существует ли уже значение с указанным ключом, и, если существует, то мы генерируем исключение. Это нужно, чтобы избежать случайной перезаписи значений.

Теперь у нас есть полноценный класс Registry, но мы не будем останавливаться на этом. Воспользуемся одной из возможностей библиотеки SPL: ArrayAccess. SPL (сокращённо от Standard PHP Library, Стандартная Библиотека PHP) — это коллекция интерфейсов и классов, предназначенных для решения стандартных проблем. Один из интерфейсов SPL, ArrayAccess, может быть использован, чтобы предоставить доступ к объекту, как к обычному массиву. Посмотрим на такой пример:

<?php


$registry = new Registry;


// Устанавливаем некоторое значение

$registry->set ('name', 'Dennis Pallett');


// Получаем значение, используя get()

echo $registry->get ('name');


// Получаем значение, используя доступ как к массиву

echo $registry['name']


?>


Фокус в том, что $registry становится как бы массивом, хотя на самом деле это объект. Конечно, ArrayAccess не даёт никаких особых преимуществ, но он позволяет сократить объём кода, так как не придётся каждый раз писать «->get()». Чтобы воспользоваться этим интерфейсом, нужно исправить первую строчку класса («Class Registry») таким образом:

Class Registry Implements ArrayAccess {


Ключевое слово «Implements» говорит интерпретатору, что этим классом мы реализуем интерфейс, чем на самом деле ArrayAccess и является.

Класс, реализующий интерфейс ArrayAccess, должен иметь следующие методы:

function offsetExists($offset) {

        return isset($this->vars[$offset]);

}


function offsetGet($offset) {

        return $this->get($offset);

}


function offsetSet($offset, $value) {

        $this->set($offset, $value);

}


function offsetUnset($offset) {

        unset($this->vars[$offset]);

}


Эти методы должны быть понятны сами по себе. Дополнительную информацию можно найти в документации SPL.

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

<?php


$registry = new Registry;


// Устанавливаем некоторое значение

$registry['name'] = 'Dennis Pallett';


// Получаем значение, используя get()

echo $registry->get ('name');


// Получаем значение, используя доступ как к массиву

echo $registry['name']


?>


Класс Registry теперь завершён, и, если попробовать запустить систему, всё должно заработать (хотя ещё ничего не будет выводиться). Мы закончили с файлом инициализации и можно приступать к следующему шагу написания нашей MVC-системы: реализация доступа к базе данных, что в архитектуре MVC называется «Model» («Модель»).

Модель

«M» или модель – часть MVC-системы, которая отвечает за запросы к базе данных (или другому внешнему источнику) и предоставление информации контроллеру. Можно было бы загружать необходимую модель в зависимости от запроса, но я предпочитаю немного стереть границы между моделью и контроллером именно в этом месте, т.е. контроллер работает с БД непосредственно через библиотеку взаимодействия с БД, нежели чем через отдельную модель. Может быть, Вам захочется сделать это по-другому, тут дело вкуса.

Нам нужно написать код, необходимый для установки соединения с БД и поместить его в index.php. Существует множество замечательных библиотек для работы с БД (включая мою собственную, AutoCRUD), но в PHP 5 уже есть такая библиотека – PDO. Поэтому нет нужды использовать какую-либо другую.

Вставим следующий код в файл index.php (после подключения файла инициализации):

# Соединяемся с БД

$db = new PDO('mysql:host=localhost;dbname=demo', '[user]', '[password]');

$registry->set ('db', $db);


В этом примере мы сначала создаём новый экземпляр библиотеки PDO и соединяемся с нашей БД MySQL. Потом делаем переменную $db доступной глобально при помощи нашего класса Registry.

Модельная компонента нашей системы готова, поэтому давайте перейдём к написанию контроллера.

Написание контроллера подразумевает также и написание класса Router, ответственного за загрузку нужного контроллера в зависимости от запроса (вспомните, в index.php через URL передаётся переменная $route).

Класс Router

Класс Router будет разбирать запрос, а потом загружать требуемый контроллер. Создадим «скелет» класса:

<?php


Class Router {

        private $registry;

        private $path;

        private $args = array();


        function __construct($registry) {

                $this->registry = $registry;

        }


}


?>


Затем добавим следующие строки в index.php:

# Загружаем router

$router = new Router($registry);

$registry->set ('router', $router);


Мы добавили класс Router в нашу MVC-систему, но он пока что ничего не делает, поэтому давай добавим в него методы, необходимые для работы.

Первая вещь, которую мы напишем, это метод setPath() для установки директории, где будут лежать все наши контроллеры. Метод выглядит следующим образом и должен быть добавлен в класс Router:

function setPath($path) {

        $path = trim($path, '/\\');

        $path .= DIRSEP;


        if (is_dir($path) == false) {

                throw new Exception ('Invalid controller path: `' . $path . '`');

        }


        $this->path = $path;

}


Потом добавим следующие строки в index.php:

$router->setPath (site_path . 'controllers');


Теперь, когда мы установили путь до наших контроллеров, напишем сам метод, ответственный за загрузку контроллера. Этот метод будет называться delegate(), он и будет анализировать запрос. Первый кусочек этого метода такой:

function delegate() {

        // Анализируем путь

        $this->getController($file, $controller, $action, $args);


Как Вы можете видеть, он использует ещё один метод, getController(), чтобы получить название контроллера и несколько других переменных. Этот метод выглядит так:

private function getController(&$file, &$controller, &$action, &$args) {

        $route = (empty($_GET['route'])) ? '' : $_GET['route'];


        if (empty($route)) { $route = 'index'; }


        // Получаем раздельные части

        $route = trim($route, '/\\');

        $parts = explode('/', $route);


        // Находим правильный контроллер

        $cmd_path = $this->path;

        foreach ($parts as $part) {

                $fullpath = $cmd_path . $part;


                // Есть ли папка с таким путём?

                if (is_dir($fullpath)) {

                        $cmd_path .= $part . DIRSEP;

                        array_shift($parts);

                        continue;

                }


                // Находим файл

                if (is_file($fullpath . '.php')) {

                        $controller = $part;

                        array_shift($parts);

                        break;

                }

        }


        if (empty($controller)) { $controller = 'index'; };


        // Получаем действие

        $action = array_shift($parts);

        if (empty($action)) { $action = 'index'; }


        $file = $cmd_path . $controller . '.php';

        $args = $parts;

}


Пробежимся по этому методу. Сначала он берёт значение переменной $route из запроса, потом разбивает его на части с помощь функции explode(). Например, запрос «members/view» преобразуется в такой массив: array(‘members’, ‘view’).

Потом при помощи цикла foreach он проходит по каждой части и проверяет, является ли эта часть директорией. Если является, то он приписывает её к пути до файла и проверяет следующую часть. Это позволяет поместить контроллеры в поддиректориях и, таким образом, получить иерархию контроллеров. Если же текущая часть запроса не является директорией, но является файлом, она сохраняется в переменную $controller, и мы выходим из цикла, так как нашёлся контроллер, который нам нужен.

После цикла мы проверяем переменную с именем контроллера. Если она пустая, то используем контроллер «index», который будет у нас контроллером по умолчанию. Потом метод определяет действие, которое необходимо выполнить. Контроллер – это класс, который состоит из нескольких методов. Действие же указывает на конкретный метод. Если действие не указано, будем использовать «index» — действие по умолчанию.

И, наконец, получаем полный путь до файла контроллера, объединяя три переменные: путь, имя контроллера и расширение «php».

Теперь, когда мы проанализировали запрос, пора вызывать метод delegate() для загрузки контроллера и выполнения действия. Полностью метод delegate() выглядит так:

function delegate() {

        // Анализируем путь

        $this->getController($file, $controller, $action, $args);


        // Файл доступен?

        if (is_readable($file) == false) {

                die ('404 Not Found');

        }


        // Подключаем файл

        include ($file);


        // Создаём экземпляр контроллера

        $class = 'Controller_' . $controller;

        $controller = new $class($this->registry);


        // Действие доступно?

        if (is_callable(array($controller, $action)) == false) {

                die ('404 Not Found');

        }


        // Выполняем действие

        $controller->$action();

}


Проанализировав запрос при помощи метода getController(), мы проверяем, существует ли в действительности файл, и, если нет, то возвращаем простое сообщение об ошибке.

После этого мы подключаем файл с контроллером и создаём экземпляр его класса, называться который должен «Controller_[имя]». Чуть позже мы поговорим о контроллерах более подробно.

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

Написав полностью метод delegate(), добавим следующую строчку в файл index.php:

$router->delegate();


Если попробовать сейчас запустить систему, то мы увидим следующую ошибку (разумеется, если директории controllers ещё нет):

Fatal error: Uncaught exception 'Exception' with message 'Invalid controller path: `g:\Projects\PHPit\content\simple mvc php5\demo\controllers\`' in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php:18 Stack trace: #0 g:\Projects\PHPit\content\simple mvc php5\demo\index.php(13): Router->setPath('g:\Projects\PHP...') #1 {main} thrown in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php on line 18


Или же мы увидим ошибку «404 Not Found», так как ещё нет ни одного контроллера. Но этим-то мы сейчас и займёмся.

Контроллер

Контроллеры в нашей MVC-системе будут достаточно простыми и потребуют совсем немного времени. Во-первых, удостоверимся, что директория controllers существует. Создадим файл controller_base.php в директории classes и вставим в него следующий код:

<?php


Abstract Class Controller_Base {

        protected $registry;


        function __construct($registry) {

                $this->registry = $registry;

        }


        abstract function index();

}


?>


Этот абстрактный класс будет родительским классом для всех наших контроллеров. Он будет делать всего лишь две вещи: сохранять локальную копию класса Registry и при помощи абстрактного метода index() заставлять все дочерние контроллеры реализовывать этот метод.

Напишем наш первый контроллер. Создадим файл index.php в директории controllers и вставим в него такой код:

<?php


Class Controller_Index Extends Controller_Base {


        function index() {

                echo 'Hello from my MVC system';

        }


}


?>


Это означает, что класс Router выполнил свою работу и запустил требуемое действие из требуемого контроллера. Давайте напишем ещё один контроллер, который будет соответствовать запросу «members/view». Создадим файл members.php в директории контроллеров и вставим в него такой код:

<?php


Class Controller_Members Extends Controller_Base {


        function index() {

                echo 'Default index of the `members` controllers';

        }


        function view() {

                echo 'You are viewing the members/view request';

        }


}


?>


Теперь зайдём в нашу MVC-систему по запросу «members/view» или же «index.php?route=members/view».

Только лишь написанием нового контроллера и добавлением в него метода, мы смогли создать новую страницу, и ничего не пришлось менять в самой системе. Кроме того, нашим контроллерам не нужно подключать файл global.php или делать что-нибудь в таком роде.

Теперь, когда у нас есть контроллеры, осталась лишь одна вещь: «V» или «View» («Отображение»).

Отображение

Как и в случае с моделями, есть несколько различных вариантов создания компоненты View в MVC-системе. Мы могли бы научить класс Router автоматически загружать ещё один файл, названный как-нибудь так: «view_{имя}.php». Но чтобы сделать руководство более понятным, напишем класс Template, который будет заниматься выводом шаблонов.

Сначала создадим файл template.php в директории classes и вставим в него следующий код:

<?php


Class Template {

        private $registry;

        private $vars = array();


        function __construct($registry) {

                $this->registry = $registry;

        }


}


?>


Теперь у нас есть основная структура нашего класс Template. Следующим шагом добавим такой код в файл index.php прямо перед строками, связанными с классом Router:

# Создаём объект шаблонов

$template = new Template($registry);

$registry->set ('template', $template);


Так как нам понадобится использовать значения из моделей и контроллеров, то напишем метод set() для установки переменных, доступных в шаблонах. Посмотрим на пример:

function set($varname, $value, $overwrite=false) {

        if (isset($this->vars[$varname]) == true AND $overwrite == false) {

                trigger_error ('Unable to set var `' . $varname . '`. Already set, and overwrite not allowed.', E_USER_NOTICE);

                return false;

        }


        $this->vars[$varname] = $value;

        return true;

}


function remove($varname) {

        unset($this->vars[$varname]);

        return true;

}


Методы set() и remove() достаточно простые и используются, соответственно, для установки и удаления переменных.

Займёмся написанием метода show(), который будет отображать шаблоны. Простейший путь – это создать отдельную директорию templates, где хранить все файлы шаблонов, и использовать include() для вывода шаблона. Разумеется, Ваш собственный метод show() может быть совершенно другим и загружать шаблоны из базы данных или делать что-нибудь ещё. Посмотрим на кусо

Добавлено: 27 Апреля 2018 17:52:40 Добавил: Андрей Ковальчук

Как веб-мастера CMS выбирали

Начальство решило полностью переделать корпоративный сайт – взамен старого стационарного хочет получить динамический сайт с новостями, каталогом статей, поддержкой комментариев и опросов. Даже дали добро на покупку недорогой коммерческой CMS. Наш отдел АСУ провел тестирование имеющихся на рынке продуктов и вот что получилось.

UMI.CMS

Стоимость: 2990-4490 руб. (версия Small and Home Office) и 8990-29990 руб. (версия Professional)
Сайт: http://www.umi-cms.ru

Добротная CMS, о ней часто упоминают в Сети. При более близком рассмотрении вариантов поставки оказалось, что версии Small and Home Office подходят только для самых простеньких сайтов. Для этой редакции присутствует ограничение 1 сайт/домен на 1 лицензию. Планировали делать еще и корпоративный блог, значит, придется брать более дорогую версию.

А так – стандартные возможности коммерческих CMS. Редакции отличаются только вспомогательным функционалом и количеством поддерживаемых модулей. В версии Professional есть доступ к исходному коду системы.

Для себя отметили мощную встроенную подсистему статистики. Очень огорчил примитивный встроенный форум и неудобная система навигации по административной панели. Админка сделана в виде дерева, двигаясь по узлам которого можно настраивать модули, подсистемы, получать доступ к шаблонам и данным системы. На наш взгляд – запутанно и неудобно.

1С-Битрикс: Управление сайтом

Стоимость: 4900-249900 руб.
Сайт: http://www.1c-bitrix.ru

Битрикс – настоящий гигант на рынке CMS. В основу системы положены очень мощные и гибкие механизмы администрирования, проектирования и поддержки системы. Сайт буквально "свинчивается" с отдельных компонентов. Есть возможность редактировать разделы как в визуальном режиме (с помощью окон настройки), так и в редакторе кода.

Во все версии системы входит мощный модуль управления информационными блоками, на основе которого можно сделать раздел статей, новостей и другие. Встроенный поиск по сайту работает с динамическими и статическими разделами, поддерживает индексацию данных и Sitemap. Есть встроенные инструменты для SEO-оптимизации контента и поддержка многосайтовости.

В более продвинутые версии включен форум, блоги, рассылки, веб-аналитика, интернет-магазин и механизмы взаимодействия с 1С:Предприятие.

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

HostCMS

Стоимость: 5 900 - 29 900 руб., есть бесплатная версия
Сайт: http://host-cms.ru

В наличие удобная админка с полным набором инструментов для визуальной настройки сайта и модулей. Для каждого модуля предусмотрена отдельная страница с кучей настроек, включая SEO, визуальное отображение, Google Sitemap и многое другое.
Бесплатная редакция под названием "Халява" абсолютно не предназначена для создания серьезных ресурсов. В ней отсутствуют модули поиска по сайту, компрессии страниц и резервирования информации. Платные редакции отличаются друг от друга только вспомогательным функционалом и модулями: форум, рассылки, реклама, журнал, статистика посещаемости.
После исследования кода мы пришли к выводу, что внедрение нужного нам функционала, который выходит за рамки модулей HostCMS, окажется трудной технической задачей.

PHP-Nuke

Стоимость: 12$, все предыдущие версии бесплатны
Сайт: http://phpnuke.org

PHP-Nuke – это известная портальная система с большим русскоязычным сообществом. Русская локализация проекта стала доступна благодаря усилиям рядовых пользователей системы.

От Open Source решения не стоит ждать функционала на все случаи жизни. Тем не менее, базовые возможности присутствует – статьи, новости, FAQ, каталог файлов, каталог ссылок. В Сети много бесплатных тем оформления и дополнительных плагинов.

В PHP-Nuke отсутствуют какие-либо инструменты для визуального проектирования интерфейса. Для создания полноценного корпоративного сайта нужно хорошо разбираться в коде системы. Для создания интернет-магазина или форума придется интегрировать в CMS сторонние разработки.

По заявлению веб-мастеров, PHP-Nuke отличается повышенной "прожорливостью", что нужно учитывать при выборе хостинга.

A-CMS

Стоимость: 90-270$, есть бесплатная версия (ограничения на 2 раздела)
Сайт: http://www.a-cms.ru

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

Во-первых, это концепция универсальных модулей, с помощью которых можно создавать разные разделы сайта. Например, "Каталог материалов" – это родитель для новостной ленты, каталога статей и блога. Такие возможности появились благодаря очень гибкой системе настроек каждого из модулей.

Блоки отвечают за визуальное отображение информации из разделов сайта. С помощью административного интерфейса можно настроить порядок расположение блоков и получить доступ к шаблонам с HTML-разметкой. Для создания шаблонов используется шаблонизатор Smarty, что позволило вынести весь код в скрипты модулей, а в шаблонах оставить только разметку.

Еще нам очень понравились инструментарий для импорта/экспорта настроек разделов и блоков. На сайте также доступны готовые конфигурации (интернет-магазин, блог и корпоративный сайт). По словам разработчиков, если есть готовый дизайн, то интеграция типовой конфигурации занимает максимум 1-2 часа.

Очень впечатлил специальный Windows-клиент, который выполняет функции центра управления группами сайтов! Мастер установки берет на себя всю работу по закачке системы на хостинг и импорту настроек. Нужно только задать FTP-доступ и базу MySQL. Можно даже выбрать одну из предложенных готовых конфигураций и сразу приступить к наполнению сайта.

Выводы

Мы протестировали ряд интересных CMS-решения и пришли к выводу, что нашим требованиям наиболее полно соответствуют два продукта - 1С-Битрикс: Управление сайтом и A-CMS. Первая CMS – очень мощная, гибкая в настройке, но отличается высокой ценой и системными требованиями. A-CMS – очень гибкий в использовании продукт, который нам полностью подходит по цене. Наличие готовых конфигураций и поддержка Smarty – это еще один плюс этой CMS.

Добавлено: 18 Февраля 2015 07:56:21 Добавил: Андрей Ковальчук

Мультиязычный сайт на Yii: Элементы интерфейса и URL



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

Постановка задачи

Итак, на нашем сайте необходимо:

Научиться извлекать текущий язык из адреса;
Заготовить варианты всех служебных надписей для элементов интерфейса (например, заголовки портлетов, attributeLabels полей моделей, текста уведомлений и т. д.) на разных языках;
Научить все необходимые модели хранить несколько вариантов заголовков и текстов для каждого языка;
Разместить ссылки для переключения на разные языки.

Использование интернационализации

Для вывода вариантов перевода надписей интерфейса достаточно ознакомиться с описанием интернационализации в руководстве.

Чтобы у нас всё работало корректно, необходимо указать приложению исходный и текущий язык:

return array(
    'sourceLanguage'=>'en',
    'language'=>'ru',
    ...
)


Пусть у нас есть модель Post с надписями:

class Post extends CActiveRecord
{
    public function attributeLabels()
    {
        return array(
            'title' => 'Заголовок',
            'text' => 'Текст',
            'category_id' => 'Категория',
        );    
    }
}


И есть портлет для вывода последних записей:

<?php $this->widget('LatestPostsPortlet', array('title' => 'Последние записи')); ?>


Теперь в каталоге protected/messages/ru нужно создать файл blog.php со списком переводов на русский язык:

return array(
    'Title' => 'Заголовок',
    'Text' => 'Текст',
    'Category' => 'Категория',
    'Latest posts' => 'Последние записи',
    ...
);


И везде в коде перейти на использование функции Yii::t:

class Post extends CActiveRecord
{
    public function attributeLabels()
    {
        return array(
            'title' => Yii::t('blog', 'Title'),
            'text' => Yii::t('blog', 'Text'),
            'category_id' => Yii::t('blog', 'Category'),
        );    
    }
}



<?php $this->widget('LatestPostsWidget', array('title' => Yii::t('blog', 'Latest posts'))); ?>


Аналогично нужно к этому переводу en→ru подготовить переводы en→de, en→fr и так далее. Переводить надписи фреймворка не надо, так как у него все свои переводы уже есть.

Теперь для вывода надписей нашего сайта на другом языке нужно изменить значение параметра language в конфигурационном файле:

return array(
    'sourceLanguage'=>'en',
    'language'=>'fr',
    ...
)


Теперь переводы будут браться из аналогичного файла blog.php из каталога protected/messages/fr.

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

Yii::t('blog', 'Title');


используйте имя класса модуля:

Yii::t('BlogModule.blog', 'Title');


Теперь можно сложить файлы blog.php в языковые поддиректории папки protected/modules/blog/messages.
Теперь рассмотрим автоматическое переключение языка.

Указание языка в адресе


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

Yii::app()->language = 'fr';
Теперь нам необходимо обеспечить извлечение языка из URL текущей страницы и произвести присвоение Yii::app()->language.

Решений, на самом деле много. В большинстве своём они сводятся к указанию языка в виде префикса адреса.

'rules'=>array(
    '<language:(ru|en|de)>' => 'site/index',
    '<language:(ru|en|de)>/<action:(contact|login|logout)>' => 'site/<action>',
    '<language:(ru|en|de)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
    '<language:(ru|en|de)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
    '<language:(ru|en|de)>/<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
),


то есть в начале масок всех правил необходимо добавить место для языка

<language:(ru|en|de)>.


Теперь где-нибудь до выполнения наших экшенов нужно указать текущий язык приложению. Чаще всего это делают при инициализации контроллера:

class Controller extends CController
{
    public function init()
    {    
        if (!empty($_GET['language']))
            Yii::app()->language = $_GET['language'];
        parent::init();
    }
}


А при генерации адресов (используя переопределённый в своём классе метод UrlManager::createUrl) нужно форсировать добавление языка:

return array(
    'components'=>array(
        'urlManager'=>array(
            'class'=>'UrlManager',
            ...
        ),
        ...
    ),
    ...
);


class UrlManager extends CUrlManager
{
    public function createUrl($route, $params=array(), $ampersand='&')
    {
        if (empty($params['language'])) {
            $params['language'] = Yii::app()->language;
        }
        return parent::createUrl($route, $params, $ampersand);
    }
}


Теперь можно заходить на свой сайт с указанием языка в начале адреса:

http://site.ru/ru
http://site.ru/en
http://site.ru/ru/blog
http://site.ru/en/blog


Здесь и проявляется первый недостаток такого решения. А именно, мы бы хотели заходить на страницы нашего сайта без указания языка «ru»:

http://site.ru
http://site.ru/en
http://site.ru/blog
http://site.ru/en/blog


Чтобы указание языка сделать необязательным, потребуется продублировать все правила:

'rules'=>array(
    '<language:(ru|en|de)>' => 'site/index',
    '/' => 'site/index',
 
    '<language:(ru|en|de)>/<action:(contact|login|logout)>' => 'site/<action>',
    '<action:(contact|login|logout)>' => 'site/<action>',
 
    '<language:(ru|en|de)>/<controller:\w+>/<id:\d+>'=>'<controller>/view',
    '<controller:\w+>/<id:\d+>'=>'<controller>/view',
 
    '<language:(ru|en|de)>/<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
    '<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
 
    '<language:(ru|en|de)>/<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
    '<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
),


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

class Controller extends CController
{
    public function init()
    {    
        if (empty($_GET['language']))
            $_GET['language'] = 'ru';
 
        Yii::app()->language = $_GET['language'];
        parent::init();
    }
}


И при генерации ссылок не надо дописывать язык, если он русский:

class UrlManager extends CUrlManager
{
    public function createUrl($route, $params=array(), $ampersand='&')
    {
        if (empty($params['language']) && Yii::app()->language !== 'ru') {
            $params['language'] = Yii::app()->language;
        }
        return parent::createUrl($route, $params, $ampersand);
    }
}


Согласитесь, что это не очень простой подход, так как для него требуется перерабатывать все правила маршрутизации и немного «засорять» базовый контроллер.

Прозрачная работа с языковыми адресами

Попробуем разрешить проблему языковой адресации без добавления кода в контроллер и указания языка в правилах маршрутизации. В этом нет ничего сложного.

Во-первых, оставим в покое правила маршрутизации, убрав из них язык:

'rules'=>array(
    '/' => 'site/index',
    '<action:(contact|login|logout)>' => 'site/<action>',
    '<controller:\w+>/<id:\d+>'=>'<controller>/view',
    '<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
    '<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
),


Раньше мы указывали языки прямо в коде:

'<language:(ru|en|de)>'
...
$_GET['language'] = 'ru';
...
Yii::app()->language !== 'ru'


В нашем новом коде этого не будет, так как это не очень красиво и не гибко. Но теперь мы не знаем, сколько языков поддерживаем. Исправим этот недостаток:

return array(
    'sourceLanguage'=>'en',
    'language'=>'ru',
 
    ...
 
    'params'=>array(
        'translatedLanguages'=>array(
            'ru'=>'Russian',
            'en'=>'English',
            'de'=>'Deutsch',
        ),
        'defaultLanguage'=>'ru',
    ),
);


Мы добавили два параметра для хранения списка поддерживаемых языков и для указания языка, используемого по умолчанию. Кстати, имена их не случайны и мы к этому ещё вернёмся.

Для продолжения обычной работы стандартных правил маршрутизации нам нужно в CHttpRequest::getRequestUri перехватывать и удалять префикс языка, а при создании адреса через CUrlManager::createUrl – добавлять префикс текущего языка снова.

Другими словами, по какому бы адресу мы ни зашли:

http://site.ru/blog
http://site.ru/en/blog
http://site.ru/de/blog


метод Yii::app()->request->getUrl() должен отбрасывать язык и возвращать в любом случае:

/blog
/blog
/blog


А генератор адресов при вызове

Yii::app()->createUrl('blog/index');


в зависимости от текущего значения Yii::app()->language должен генерировать ссылки вида

/blog
/en/blog
/de/blog


То есть, фактически, система внутри кроме доступа к параметру Yii::app()->language вообще не должна понимать, в окружении какого языка она находится на данным момент.

Реализация обработки адресов

Напишем теперь всё, что мы имели в виду в предыдущем пункте. Переопределим стандартные классы своими наследниками.

Парсер адресов:

class DLanguageHttpRequest extends CHttpRequest
{
    private $_requestUri;
 
    public function getRequestUri()
    {
        if ($this->_requestUri === null)
            $this->_requestUri = DMultilangHelper::processLangInUrl(parent::getRequestUri());
 
        return $this->_requestUri;
    }
 
    public function getOriginalUrl()
    {
        return $this->getOriginalRequestUri();
    }
 
    public function getOriginalRequestUri()
    {
        return DMultilangHelper::addLangToUrl($this->getRequestUri());
    }
}


Генератор адресов:

class DLanguageUrlManager extends CUrlManager
{
    public function createUrl($route, $params=array(), $ampersand='&')
    {
        $url = parent::createUrl($route, $params, $ampersand);
        return DMultilangHelper::addLangToUrl($url);
    }
}


Теперь эти классы нужно указать в конфигурационном файле:

return array(
    'sourceLanguage'=>'en',
    'language'=>'ru',
 
    'components'=>array(
        'request'=>array(
            'class'=>'DLanguageHttpRequest',
            ...
        ),
        'urlManager'=>array(
            'class'=>'DLanguageUrlManager',
            ...
        ),
    ),
 
    ...
 
    'params'=>array(
        'translatedLanguages'=>array(
            'ru'=>'Russian',
            'en'=>'English',
            'de'=>'Deutsch',
        ),
        'defaultLanguage'=>'ru',
    ),
);


Если в приложении уже имеются свои классы UrlManager и HttpRequest

class UrlManager extends CUrlManager {...}
class HttpRequest extends СHttpRequest {...}


то вы можете отнаследоваться прямо от них:

class DLanguageUrlManager extends UrlManager {...}
class DLanguageHttpRequest extends HttpRequest {...}


Теперь, собственно, сам DMultilangHelper:

/**
 * @author ElisDN <mail@elisdn.ru>
 * @link http://www.elisdn.ru
 */
class DMultilangHelper
{
    public static function enabled()
    {
        return count(Yii::app()->params['translatedLanguages']) > 1;
    }
 
    public static function suffixList()
    {
        $list = array();
        $enabled = self::enabled();
 
        foreach (Yii::app()->params['translatedLanguages'] as $lang => $name)
        {
            if ($lang === Yii::app()->params['defaultLanguage']) {
                $suffix = '';
                $list[$suffix] = $enabled ? $name : '';
            } else {
                $suffix = '_' . $lang;
                $list[$suffix] = $name;
            }
        }
 
        return $list;
    }
 
    public static function processLangInUrl($url)
    {
        if (self::enabled())
        {
            $domains = explode('/', ltrim($url, '/'));
 
            $isLangExists = in_array($domains[0], array_keys(Yii::app()->params['translatedLanguages']));
            $isDefaultLang = $domains[0] == Yii::app()->params['defaultLanguage'];
 
            if ($isLangExists && !$isDefaultLang)
            {
                $lang = array_shift($domains);
                Yii::app()->setLanguage($lang);
            }
 
            $url = '/' . implode('/', $domains);
        }
 
        return $url;
    }
 
    public static function addLangToUrl($url)
    {
        if (self::enabled())
        {
            $domains = explode('/', ltrim($url, '/'));
            $isHasLang = in_array($domains[0], array_keys(Yii::app()->params['translatedLanguages']));
            $isDefaultLang = Yii::app()->getLanguage() == Yii::app()->params['defaultLanguage'];
 
            if ($isHasLang && $isDefaultLang)
                array_shift($domains);
 
            if (!$isHasLang && !$isDefaultLang)
                array_unshift($domains, Yii::app()->getLanguage());
 
            $url = '/' . implode('/', $domains);
        }
 
        return $url;
    }
}


Он и будет обрабатывать все адреса и на основе префикса адреса устанавливать язык приложения. Также мы добавили сюда два вспомогательных метода enabled() и suffixList(). Второй пригодится нам дальше.

Если теперь попробовать зайти по разным адресам

http://site.ru/
http://site.ru/en/
http://site.ru/de/


то строка

<?php echo Yii::app()->language; ?>


должна правильно выводить текущий язык, а выражение

<?php echo Yii::app()->createUrl('blog/view', array('id'=>1)); ?>


должно генерировать URL с префиксом, совпадающим с текущим языком.

Также мы добавили методы для получения «настоящего» адреса текущей страницы. Это может быть полезно, например, для реализации защиты от дубликатов страниц:

public function actionView($id)
{
    $model = $this->loadModel($id);
 
    if (Yii::app()->request->originalUrl !== $model->getUrl())
        $this->redirect($model->getUrl());
 
    $this->render('view', array('model'=>$model));
}


Виджет переключения языков

В нашем случае метод Yii::app()->request->getUrl() возвращает адрес без префикса, так что можно собрать ссылки на другие языки простой конкатенацией префиксов:

class LanguageSwitcherWidget extends CWidget
{
    public function run()
    {
        $currentUrl = ltrim(Yii::app()->request->url, '/');
        $links = array();
        foreach (DMultilangHelper::suffixList() as $suffix => $name){
            $url = '/' . ($suffix ? trim($suffix, '_') . '/' : '') . $currentUrl;
            $links[] = CHtml::tag('li', array('class'=>$suffix), CHtml::link($name, $url));
        }
        echo CHtml::tag('ul', array('class'=>'language'), implode("\n", $links)); 
    }
}


Теперь достаточно вывести этот виджет в шаблоне:

<?php $this->widget('LanguageSwitcherWidget'); ?>


чтобы вывести меню переключения любой страницы на все поддерживаемые языки.

Добавлено: 16 Февраля 2015 20:47:00 Добавил: Андрей Ковальчук

OpenCart URL Rewrite: Критическая доработка в контроллере SEO URL

Недавно удалось обнаружить серьёзный недочет в работе стандартного контроллера OpenCart SEO URL. Один из заказчиков пожаловался на выпадение из поисковой выдачи целого ряда категорий и связанных с ними товаров. Владелец магазина не особо себя утруждает работой с контентом через панель администрирования, обновление данных настроено через выгрузку из базы данных популярной программы для ведения финансовой и торговой деятельности. Слишком много товаров и категорий, чтобы за ними следить вручную.

Как и ожидалось, проблемы были видны и в самом интерфейсе. Подкатегории некоторых категорий имели такие же ссылки, которые были закреплены за родительскими категориями. Если взять один из участков дерева категорий, то он выглядел следующим образом:
Мебель и предметы интерьера
http://domain.com/furniture
Мебель и предметы интерьера -> Кухонная мебель
http://domain.com/furniture
Мебель и предметы интерьера -> Кухонная мебель -> Стулья
http://domain.com/furniture
Мебель и предметы интерьера -> Кухонная мебель -> Столы
http://domain.com/furniture
Видно, что для всех категорий "заданы" одни и те же ссылки. Ни поисковый робот, ни посетители больше не могут попасть в подкатегории.

Ранее владелец магазина пытался оптимизировать структуру категорий, в том числе задать канонические имена (заполнить поля SEO URL). Начал с основных категорий, видимо в какой-то момент это дело наскучило и оптимизация была отложена (категорий было очень много).

Собственно причина была очевидна, если родительским категориям задать ссылки, а дочерние категории оставить без изменений (ссылки-идентификаторы по умолчанию), то при при включенном режиме SEO URL формироваться ссылки для подкатегорий не будут.

Чтобы это исправить, в оригинальном файле ./catalog/controller/common/seo_url.php необходимо выполнить следующее:

В теле функции index() найти строку foreach ($parts as $part) { и заменить на строку foreach ($parts as $id => $part) {Ниже найти строку $this->request->get['path'] .= '_' . $url[1]; и заменить на if ($id) { $this->request->get['path'] .= '_' . $url[1]; } else { $this->request->get['path'] = $url[1] . '_' . $this->request->get['path']; }
В теле функции rewrite() в условии } elseif ($key == 'path') { найти и удалить строку unset($data[$key]); Здесь же строку $url .= '/' . $query->row['keyword']; заменить на следующий код: $part = preg_replace('/^' . $category . '_?/', '', $data[$key]); if ($data[$key] != $part) { $url .= '/' . $query->row['keyword']; if ($part) { $data[$key] = $part; } else { unset($data[$key]); } }
После такой доработки наш выделенный участок из общей структуры категорий примет следующий вид:
Мебель и предметы интерьера
http://domain.com/furniture
Мебель и предметы интерьера -> Кухонная мебель
http://domain.com/furniture?path=40
Мебель и предметы интерьера -> Кухонная мебель -> Стулья
http://domain.com/furniture?path=40_41
Мебель и предметы интерьера -> Кухонная мебель -> Столы
http://domain.com/furniture?path=40_42
Первый пункт доработки обеспечивает понимание таких ссылок, а второй их формирование. Соответствующие исправления необходимо сделать и для оптимизированной версии контроллера (для тех кто использует решение по сокращению количества запросов к базе данных в режиме SEO URL), но уже в другом виде.

Добавлено: 13 Февраля 2015 06:48:02 Добавил: Андрей Ковальчук