Найти текст, заключенный в какой-то тег и заменить его на другой тег

Например: <TITLE> ... </TITLE> заменить аналогично на <МОЙ_ТЕГ> ... </МОЙ_ТЕГ> в HTML-файле:

preg_replace("!<title>(.*?)</title>!si","<МОЙ_ТЕГ>\\1</МОЙ_ТЕГ>",$string);

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

Простые правила работы с UTF-8 кодировкой в PHP

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

Я думаю, не надо рассказывать, что UTF-8 уже является стандартом для большинства web-приложений и пока не планируется особых замен. Но на текущий момент множество начинающих "познавателей" PHP сталкиваются с проблемами в этом месте и постят кучу топиков в связи с тем, что PHP пока не поддерживает кодировку UTF-8 "из коробки" и приносит вышеуказанным людям кучу непонимания и проблем. Кракозяблики, кракозяблики, кракозяблики... Проблема с UTF-8... Что делать? Помогите - слышно тут и там... На самом деле всё очень просто. Ниже я опишу несколько правил, которые позволяют избавиться от "проблем" с UTF-8 при разработке приложений на PHP.

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

1. В начале работы скрипта необходимо указать кодировку входа и выхода с помощью функции mb_internal_encoding:

Listing №1 (PHP)

mb_internal_encoding('UTF-8');

Также это позволит определить кодировку по умолчанию для всех mb-функций, которым нужно её указывать.

2. При работе со строками, преобразованиями символов, необходимо использовать функции работы со строками из расширения php_mbstring. Это, например, такие как mb_substr, mb_strpos, mb_strlen и т.п. или же те функции, которые безопасны для обработки данных в двоичной форме, такие как explode, str_replace и т.д. Эта особенность указана в мануале по этим функциям. Иногда нельзя просто заменить функцию на её mb-аналог, потому как такой там просто нет. Например, ucfirst, которая преобразует первый символ строки в верхний регистр. Решение тут очень простое - изящной комбинацией mb_substr и mb_strtoupper получаем необходимый результат. Попробуйте проделать это сами )). Также в этот пункт стоит отнести такие функции как htmlspecialchars, htmlentities и т.п., которые производят различные преобразования строк. Пожалуйста, передавайте в них третий аргумент $charset в виде строки 'UTF-8'.

3. Так как мы используем строки символов в самих скриптах, то строго необходимо сохранять их в UTF-8 кодировке. Это также относиться ко всем остальным файлам, которые вы используете - HTML шаблоны, различные подключаемые файлы, текстовые и т.д. Это всё должен делать ваш редактор кода. Согласуйте с ним эти вещи. Но смотрите внимательно, что бы он кодировал или преобразовывал файлы в UTF-8 без особой метки, называемой BOM. Так будет намного лучше.

4. Немаловажным фактором, является работа браузера с кодировкой UTF-8, а точнее с данными, которые вы посылаете ему, и теми данными, которые он отсылает вам. Для того, что бы браузер правильно понимал в какой кодировке вы отправили ему данные нужно отсылать заголовок с указанием кодировки. Это можно сделать так:

Listing №2 (PHP)
header('Content-Type: text/html; charset=UTF-8');

Только не забудьте отправлять этот заголовок до какого либо вывода с помощью echo, print или просто пробелов в начале скрипта до тега <?php или где то между парой этих тегов ?> <?php. Ну, а для того, чтобы браузер отсылал вам данные в нужной кодировке, нужно помечать сам html документ соответствующим мета-тегом. Например таким:

Listing №3 (HTML)
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

5. Наверное больше всего трудностей вызывает работа с базой данных. Обычно используется MySQL СУРБД. Тут всё тоже очень просто. Перед тем как послать и принимать данные нужно сказать серверу базы данных, что вы будете отсылать ей данные в кодировке UTF-8. Не вдаваясь в особые подробности, скажу лишь, что перед любым обращением к базе лишь единожды, т.е. один раз, нужно выполнить два запроса. Вот они:

Listing №4 (SQL)
SET NAMES "utf8"
-- и
SET CHARACTER SET "utf8"

Поверьте, она с радостью воспримет ваш призыв. Но не только обмениваться, а и хранить данные нужно соответственно. Посему изначально создавайте поля, хранящие строки в UTF-8 кодировке. Я думаю, вам не составит труда указать это при создании таблицы ;)


Ну вот и всё. Придерживаясь этих правил, и, я думаю, вы больше не будете встречать каких-то проблем с UTF-8 в PHP, по крайней мере до выхода шестой его версии. Хотя я знаю некоторые ситуации, но эта заметка не про них.

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

Примеры шаблонов проектирования или как написать свой 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 Добавил: Андрей Ковальчук

Мой родной PHP шаблонизатор

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

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

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

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


Listing №1 (PHP)

class Template {
  /**
   * @var array
   */
  private $_Vars;
  /**
   * @var string
   */
  private $_Name;
  /**
   * @var string
   */
  private $_Path;
 
  public function __construct()
  {
    $this->_Name = '';
    $this->_Vars = array();
    $this->_Path = '';
  }
  /**
   * @param string $VarName имя переменной
   * @param string $VarValue значение переменной
   * @return Template
   */
  public function AddVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
    return $this;
  }
  /**
   * @param string $Varname имя переменной
   * @return mixed
   */
  public function __get($VarName)
  {
    if (array_key_exists($VarName, $this->_Vars)) {
      if ($this->_Vars[$VarName] instanceof Template) {
        return $this->_Vars[$VarName]->Prepare();
      }
      else{
        return $this->_Vars[$VarName];
      }
    }
    return NULL;
  }
  /**
   * @var string $Name имя шаблона
   */
  public function SetName($Name)
  {
    $this->_Name = $Name;
  }
  /**
   * @var string $Path путь к шаблону(ам)
   */
  public function SetPath($Path)
  {
    $this->_Path = $Path;
  }
}


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


Listing №2 (PHP)
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeHtml($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeHtml($VarItem);
      }
    }
    else {
      $Var = htmlspecialchars($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeUrl($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeUrl($Var);
      }
    }
    else {
      $Var = htmlentities($Var, ENT_QUOTES);
    }
    return $Var;
  }


Идем дальше.

Что я подразумеваю под словами "режим отладки"? Это, скорее всего, больше похоже на режим удобного чтения. В таком режиме на выходе мы получим код шаблона в том же виде, что и до его парсинга, то есть со всеми табуляциями, переводами строк и прочей нечистью. Это довольно удобно, потому что native шаблоны могут сложно восприниматься и то и дело приходится всё табулировать и переносить. В противоположность этому режиму будет обычный рабочий режим, где ненужные символы будут удалены за ненадобностью. За это будет отвечать метод _Zip, а за переключение между режимами статический метод SetDebug:


Listing №3 (PHP)
  /**
   * Свойство хранящее текущий режим
   * @static
   * @var bool
   */
  private static $_Debug = FALSE;
  /**
   * Вкл/выкл дебаг режим
   * @static
   * @param bool $TrueOrFalse TRUE
   */
  public static function SetDebug($TrueOrFalse = TRUE)
  {
    self::$_Debug = $TrueOrFalse;
  }
  /**
   * Возвращает $Text с удаленными "\t", "\n" и "\r"
   * @param  string $Text
   * @return string
   */
  private function _Zip($Text)
  {
    return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  }


Теперь нам нужен метод Prepare, который выполнит всю основную работу, но не выведет данные, а возвратит их строкой. Это нам понадобиться для реализации вставки шаблона в шаблон, которая и происходит при извлечении переменной в шаблоне (метод __get). Думаю, без буферизации вывода тут не обойтись:


Listing №4 (PHP)
  /**
   * @return string
   */
  public function Prepare()
  {
    if (file_exists($this->_Path . $this->_Name)) {
      ob_start();
      ${__CLASS__} = $this;
      include $this->_Path . $this->_Name;
      unset(${__CLASS__});
      return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
    }
    else {
      throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
    }
  }


Сначала проверяем наличие шаблона. Затем "по родному" парсим шаблон и возвращаем контент из буфера, очистив ненужные символы, если это необходимо. Также, меня очень напрягает писать в шаблонах $this, поэтому я решил писать там $Template. Мне так очень нравится.

Наконец, добавив метод непосредственного вывода, получаем нужный "полный фарш":


Listing №5 (PHP)
class Template {
  /**
   * @var string
   */
  private $_Name;
  /**
   * @var array
   */
  private $_Vars;
  /**
   * @var string
   */
  private $_Path;
  /**
   * @static
   * @var bool
   */
  private static $_Debug = FALSE;
  /**
   * @static
   * @param bool $TrueOrFalse TRUE
   */
  public static function SetDebug($TrueOrFalse = TRUE)
  {
    self::$_Debug = $TrueOrFalse;
  }
 
  public function __construct()
  {
    $this->_Name = '';
    $this->_Vars = array();
    $this->_Path = '';
  }
  /**
   * @param string $VarName
   * @param string $VarValue
   * @return Template
   */
  public function AddVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
    return $this;
  }
  /**
   * @param string $Varname
   * @return mixed
   */
  public function __get($VarName)
  {
    if (array_key_exists($VarName, $this->_Vars)) {
      if ($this->_Vars[$VarName] instanceof Template) {
        return $this->_Vars[$VarName]->Prepare();
      }
      else{
        return $this->_Vars[$VarName];
      }
    }
    return NULL;
  }
  /**
   * @var string $Name
   */
  public function SetName($Name)
  {
    $this->_Name = $Name;
  }
  /**
   * @var string $Path
   */
  public function SetPath($Path)
  {
    $this->_Path = $Path;
  }
  /**
   * @return string
   */
  public function Prepare()
  {
    if (file_exists($this->_Path . $this->_Name)) {
      ob_start();
      ${__CLASS__} = $this;
      include $this->_Path . $this->_Name;
      unset(${__CLASS__});
      return (self::$_Debug) ? ob_get_clean() : $this->_Zip(ob_get_clean());
    }
    else {
      throw new Exception('Template file "' . $this->_Path . $this->_Name . '" does not exists');
    }
  }
 
  public function Display()
  {
    print ($this->Prepare());
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeHtml($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeHtml($VarItem);
      }
    }
    else {
      $Var = htmlspecialchars($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param mixed $Var
   * @return mixed
   */
  protected function EscapeUrl($Var)
  {
    if(is_array($Var)) {
      foreach($Var as &$VarItem) {
        $VarItem = $this->EscapeUrl($Var);
      }
    }
    else {
      $Var = htmlentities($Var, ENT_QUOTES);
    }
    return $Var;
  }
  /**
   * @param  string $Text
   * @return string
   */
  private function _Zip($Text)
  {
    return (empty($Text)) ? $Text : str_replace(array("\t", "\n", "\r"), '', $Text);
  }
}


Ну и напоследок, небольшой пример использования.

Допустим, у нас есть два шаблона - главная страница page.php и список пользователей userslist.php. Нужно отобразить этот список вместе с информацией на главной странице. Поскупившись на контент, рисуем главную:


Listing №6 (PHP)
<div style="border: 1px solid #E0E3F3;">
  <?php echo $Template->UserList;?>
</div>

<a href="<?php echo $Template->EscapeUrl($Template->Link)?>">About</a>

А также список пользователей:


Listing №7 (PHP)
<div>
  <?php foreach($Template->EscapeHtml($Template->UserNames) as $UserName) { ?>
  <span style="color: #FF0000;"><?php echo $UserName;?></span><br>
  <?php } ?>
</div>


За логику отвечает некая модель:


Listing №8 (PHP)
//Подключаем шаблонизатор
require 'Template.php';
//Устанавливаем режим отладки включенным для всех шаблонов
Template::SetDebug(TRUE);
//Инициализируем подшаблон списка пользователей
$UsersListView = new Template();
$UsersListView->SetName('userslist.html');
//Список пользователей
$UserNames = array('Den Rights©', 'Dasha Nasha', 'Samuil Kakojto');
$UsersListView->AddVar('UserNames', $UserNames);
//Инициализируем главный шаблон страницы
$PageView = new Template();
$PageView->SetName('page.html');
$PageView->AddVar('UserList', $UsersListView);
$PageView->AddVar('Link', 'http://www.itdumka.com.ua/index.php?cmd=shownode&node=1');
$PageView->Display();



В результате работы с включенным режимом отладки, в отличие от обычного режима, где все будет в одну строку, мы увидим такую картину:


Listing №9 (HTML)
<div style="border: 1px solid #E0E3F3;">
  <div>
    <span style="color: #FF0000;">Den Rights©</span><br>
    <span style="color: #FF0000;">Dasha Nasha</span><br>
    <span style="color: #FF0000;">Samuil Kakojto</span><br>
  </div>
</div>
<a href="http://www.itdumka.com.ua/index.php?cmd=shownode&node=1">About</a>


Вот так.

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

Кеширование в PHP - теперь немного лучше, чем просто кеш

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


Итак, что же мне не нравилось в моей тривиальности, и что я хотел бы улучшить?

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

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

Сначала я проверял актуальность кешированных данных и если они таковыми не являлись, то проводил операцию получения данных и записывал "свежий" кеш на диск. Иначе - если кеш был актуален - просто брал данные от туда. Вот пример из моего старенького шаблонизатора:


Listing №1 (PHP)

<?php
if ($this->_Cache->IsActual()) {
  return $this->_Cache->Read();
}
else {
  $Content = $this->Parse($File);
  $this->_Cache->Write($Content);
  return $Content;
}
?>


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


Listing №2 (PHP)
<?php
public function IsActual()
{
  clearstatcache();
  if (!file_exists($this->_Id)) {
    return FALSE;
  }
  if (!($CreateTime = filemtime($this->_Id))) {
    return FALSE;
  }
  if (($CreateTime + $this->_Expired) < time()) {
    @unlink($this->_Id);
    return FALSE;
  }
  return TRUE;
}
?>


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

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

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

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

Для начала сконструируем интерфейс с нужными функциями:


Listing №3 (PHP)
<?php
interface Cachebackend {
  /**
   * @param string $Key
   */
  public function GetCache($Key);
  /**
   * @param string $Key
   * @param int    $ExpiredPeriod
   */
  public function IsActual($Key, $ExpiredPeriod);
  /**
   * @param string $Key
   * @param int    $ExpiredPeriod
   * @param int    $SoonPeriod
   * @param string $SoonFuseKeyPostfix
   */
  public function IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix);
  /**
   * @param string $Key
   * @param string $Data
   */
  public function PutCache($Key, $Data);
  /**
   * @param stirng $PreparedKey
   * @param string $RenameKey
   * @return bool
   */
  public function Rename($PreparedKey, $RenameKey);
}
?>


Теперь сконструируем непосредственный каркас с нашим алгоритмом:


Listing №4 (PHP)
<?php
class Cacher {
  /**
   * @var Cachebackend
   */
  private static $_Backend = NULL;
  /**
   * @var mixed
   */
  private static $_CallbackSignature = array();
  /**
   * @var array
   */
  private static $_CallbackArguments = array();
  /**
   * @static
   * @param string $Tag
   * @param string $Key
   * @param int    $ExpiredPeriod
   * @param int    $SoonPeriod
   * @param sting  $SoonFuseKeyPostfix '.next'
   * @return mixed
   */
  public static function GetData($Tag, $Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix = '.next')
  {
    $Key = CACHE_PATH . strtolower($Tag) . '_' . strtolower($Key) . CACHE_EXT;
    if (NULL === self::$_Backend) {
      self::$_Backend = new Cachefilebackend;
    }
    if (self::$_Backend->IsActual($Key, $ExpiredPeriod)) {
      $Data = self::$_Backend->GetCache($Key);
      if(self::$_Backend->IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix)) {
        $CallbackData = self::_GetCallbackResult();
        self::$_Backend->PutCache($Key . $SoonFuseKeyPostfix, $CallbackData);
      }
      return $Data;
    }
    else {
      if(self::$_Backend->IsActual($Key . $SoonFuseKeyPostfix, $ExpiredPeriod)) {
        $Data = self::$_Backend->GetCache($Key . $SoonFuseKeyPostfix);
        self::$_Backend->Rename($Key . $SoonFuseKeyPostfix, $Key);
        return $Data;
      }
      else {
        $CallbackData = self::_GetCallbackResult();
        self::$_Backend->PutCache($Key, $CallbackData);
        return $CallbackData;
      }
    }
  }
  /**
   * @static
   * @param string $CallbackFunction
   * @param array $CallbackArguments
   * @param mixed $CallbackObject NULL
   */
  public static function SetCallback($CallbackFunction, $CallbackArguments, $CallbackObject = NULL)
  {
    if ($CallbackObject) {
      self::$_CallbackSignature = array($CallbackObject, $CallbackFunction);
    }
    else {
      self::$_CallbackSignature = $CallbackFunction;
    }
    self::$_CallbackArguments = $CallbackArguments;
  }
  /**
   * @static
   * @return mixed
   */
  private static function _GetCallbackResult()
  {
    self::_CheckCallback();
    return call_user_func_array(self::$_CallbackSignature, self::$_CallbackArguments);
  }
  /**
   * @static
   */
  private static function _CheckCallback()
  {
    if(!is_callable(self::$_CallbackSignature, FALSE, $CallableName)) {
      throw new Exception($CallableName . ' is not correct callback');
    }
    if(!is_array(self::$_CallbackArguments)) {
      throw new Exception('Callback arguments must be an array');
    }
  }
}
?>


Давайте немного разберемся, что к чему.

Итак, основной метод - это метод GetData. Что мы передаем ему:

$Tag - идентификатор группы кеш-данных. Предназначен для управления целой группой кеш-данных.
$Key - идентификатор конкретных кеш-данных. Однозначно идентифицирует кешируемые данные.
$ExpiredPeriod - время жизни кеша. Это время можно даже назвать как время "псевдо" жизни, ведь новый кеш появится до истечения этого времени, хотя старый и удалиться именно по этому значению.
$SoonPeriod - период времени, который определяет временной интервал между появлением нового кеша и удалением старого: T(удаления старого кеша) - T(создания нового кеша) = $SoonPeriod
$SoonFuseKeyPostfix - параметр по умолчанию, обозначающий расширение файлов нового кеша, чтобы не путать их с еще существующими файлами старого кеша.
Константы CACHE_PATH и CACHE_EXT определяют кеш-директорию и расширение кеш-файлов соответственно. Метод GetData создает объект backend-а и выполняет соответствующие действия по управлению кешем:

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

Функция SetCallback может принимать как метод объекта, так и функцию для получения кешируемых данных. Остальные две функции выполняют внутреннюю работу.

Теперь опишем файловый backend, который следует вышеобъявленному интерфейсу:


Listing №5 (PHP)
<?php
class Cachefilebackend implements Cachebackend {
  /**
   * @see Cachebackend
   */
  public function GetCache($Key)
  {
    clearstatcache();
    if (file_exists($Key)) {
      $Content = '';
      $Content = @file_get_contents($Key);
      return unserialize((string)$Content);
    }
    else {
      throw new Exception('Cached keyfile "' . $Key. '" does not exists');
    }
  }
  /**
   * @see Cachebackend
   */
  public function IsActual($Key, $ExpiredPeriod)
  {
    clearstatcache();
    if (file_exists($Key)) {
      if (time() - filemtime($Key) > $ExpiredPeriod) {
        @unlink($Key);
        return FALSE;
      }
      else {
        return TRUE;
      }
    }
    return FALSE;
  }
  /**
   * @see Cachebackend
   */
  public function IsNeedCreateNewCache($Key, $ExpiredPeriod, $SoonPeriod, $SoonFuseKeyPostfix)
  {
    clearstatcache();
    if (file_exists($Key)) {
      if ((time() - filemtime($Key) > $ExpiredPeriod - $SoonPeriod) && (
          (!file_exists($Key . $SoonFuseKeyPostfix)))) {
        return TRUE;
      }
      else {
        return FALSE;
      }
    }
    else {
      return FALSE;
    }
  }
  /**
   * @see Cachebackend
   */
  public function PutCache($Key, $Data)
  {
    if($Key) {
      @file_put_contents($Key, serialize($Data));
    }
    else {
      throw new Exception('Cache key is empty');
    }
  }
  /**
   * @see Cachebackend
   */
  public function Rename($PreparedKey, $RenameKey)
  {
    return @rename($PreparedKey, $RenameKey);
  }
}
?>


Думаю теперь надо привести пример использования. Допустим, у вас есть объект $UserData, который возвращает одни данные c помощью метода GetData(), и функция GetUserText(), возвращающая другие данные. В обе функции передается параметр - идентификатор пользователя. Результат их работы складывается как строки и отдается на съедение браузеру. Мы закешируем эти данные на день и подготовим кеш объекта за час до конца дня, а кеш функции за два часа.


Listing №6 (PHP)
<?php
//Инициализируем пользователя
$User = new User();
//Инициализируем данные пользователя
$UserData= new UserData($User->Id);
//Настраиваем кеширование данных пользователя
Cacher::SetCallback('GetData', array($User->Id), $UserData);
//Получаем данные пользователя
$Response = Cacher::GetData('userdata', md5($User->Id), 24 * 60 * 60, 60 * 60);
//Настраиваем кеширование текста пользователя
Cacher::SetCallback('GetUserText', array($User->Id));
//Получаем текст пользователя
$Response .= Cacher::GetData('usertext', md5($User->Id), 24 * 60 * 60, 120 * 60);
//Отдаем результат
echo $Response;
?>


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

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

Зачем нужен prepared statements API в PHP mysqli extension?

PHP

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

Однажды в PHP появилось расширение под названием mysqli, которое пришло на замену привычному mysql и победило его словом Improved (улучшенный), расставив некоторые точки над i. С тех пор, если вы используете MySQL-сервер версии 4.1.3 и выше, разработчики PHP строго рекомендуют использовать mysqli-расширение. Основными аргументами в пользу последнего были улучшенный протокол обмена между клиентов и сервером, а так же поддержка следующих и не только фич:

Самое, что было крутое так это поддержка объектно-ориентированного интерфейса. Коннект вешался на объект - и все становились счастливыми. Мы можем наследовать от класса mysqli, перезагружать методы и т. д. Стало намного удобнее, не надо тягать за собой ресурс и всё такое.
Поддержка Multiple Statements - фактически можно выполнить несколько запросов за одно обращение. Да, это хорошо - уменьшение обращений к серверу открывает новые возможности обращения )). Это позволило открыть некоторым небольшую дыру, но у кого руки не крюки - у тех всё сработало.
Поддержка Prepared Statements - третья основная фича в mysqli. Надеюсь, вы знаете, что это за механизм prepared statements, а если не знаете, то, может быть, узнаете.
Конечно, было еще несколько нововведений, но я их упущу чтобы конкретнее остановиться на последнем пункте. Итак, сторона разработчиков оперирует следующими фактами об использовании механизма prepared statement. Следующие три абзаца - это почти прямое цитирование Харисона Фиска (ссылка на оригинал) .

Использование prepared statements дает множество преимуществ как для безопасности так и для производительности. Prepared statements могут способствовать повышению безопасности путём отделения логики SQL запроса, от данных, подставляемых в него. Такое разделение логики и данных может помочь предотвратить внедрение SQL-инъекций. Обычно, когда вы используете запросы, в которых используются данные пришедшие от пользователей, вы должны быть очень осторожными. Для этого вы используете функции, которые экранируют проблемные символы, такие как одинарные и двойные кавычки, обратный слешь. Эти операции не обязательно необходимо выполнять, когда вы будете использовать prepared statements. Отделение данных от логики запроса SQL позволяет MySQL автоматически обработать эти символы и не прибегать к помощи специальных функций.

Увеличение производительности при использовании prepared statements может быть связано со следующими некоторыми факторами. Во-первых разбор строки SQL запроса происходит лишь единожды. На начальном этапе prepared statements (этап подготовки параметров) MySQL обработает каждый параметр, для проверки синтаксиса, а затем создаст запрос для непосредственного запуска. Затем, если мы будет выполнять этот запрос много раз, это уже не будет требовать расходов на новый синтаксический анализ его логики. Такой предварительный анализ, приводит к увеличению скорости, если нужно выполнить один и тот же запрос много раз, например, когда производиться вставка нескольких строк с помощью INSERT. Во-вторых, при использовании механизма prepared statements применяется совсем другой, так называемый, двоичный протокол обмена. Стандартный протокол MySQL перед отправкой данных по сети всегда преобразует их в строки. Это означает, что клиент преобразует данные в строки, которые зачастую становятся больше чем исходные данные, отправляет их серверу, который, в свою очередь, декодирует пришедшие строки обратно в правильный тип данных. Двоичный протокол отправляет все данный в родной бинарной форме, тем самым уменьшая использование ресурса процессора на конвертации данных и, естественно, сокращает затраты на передачу по сети.

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

Я решил немного измерить процесс работы. Для теста я написал две функции - одна работает чисто без prepared statements, а другая только с prepared statements. Так же я взял "небольшие" несколько запросов и комбинировал их в разных вариациях друг с другом. Вот один из вариантов тестирования:


Listing №1 (PHP)

error_reporting(E_ALL);
 
function workWithoutPS(mysqli $db)
{
  $startTime = microtime(TRUE);
  /**
   * Select 1
   */
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $login = "'" . $db->real_escape_string($login) . "'";
  $email = "'" . $db->real_escape_string($email) . "'";
  $result = $db->query('Select ID from USERS where LOGIN = ' . $login . ' and EMAIL = ' . $email);
  while($row = $result->fetch_assoc()) {
    $a = $row;
  }
  $result->close();
  /**
   * Select 2
   *
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $login = "'" . $db->real_escape_string($login) . "'";
  $email = "'" . $db->real_escape_string($email) . "'";
  $result = $db->query('Select ID from USERS where LOGIN = '. $login . ' and EMAIL = ' . $email . ' LIMIT 10 OFFSET 100' );
  while($row = $result->fetch_assoc()) {
    $a = $row;
  }
  $result->close();
  /**
   * Select 3
   *
  $login = "mylogin'mm";
  $login = "'" . $db->real_escape_string($login) . "'";
  $result = $db->query('Select ID from USERS where LOGIN = '. $login . '  LIMIT 30 OFFSET 100');
  while($row = $result->fetch_assoc()) {
    $a = $row;
  }
  $result->close();*/
  /**
   * Insert 1
   */
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $login = "'" . $db->real_escape_string($login) . "'";
  $email = "'" . $db->real_escape_string($email) . "'";
  $db->query('Insert into USERS (LOGIN, EMAIL) values ('. $login . ',' . $email . ')');
  /**
   * Insert 2
   *
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $login = "'" . $db->real_escape_string($login) . "'";
  $email = "'" . $db->real_escape_string($email) . "'";
  $db->query('Insert into USERS (LOGIN, EMAIL) values ('. $login . ',' . $email . ')');*/
  return microtime(TRUE) - $startTime;
}
 
function workWithPS(mysqli $db)
{
  $startTime = microtime(TRUE);
  /**
   * Select 1
   */
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $stmt = $db->prepare('Select ID from USERS where LOGIN = ? and EMAIL = ?');
  $stmt->bind_param('ss', $login, $email);
  $result = $stmt->execute();
  $stmt->bind_result($id);
  while($stmt->fetch()) {
    $a = $id;
  }
  $stmt->close();
  /**
   * Select 2
   *
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $stmt = $db->prepare('Select ID from USERS where LOGIN = ? and EMAIL = ?  LIMIT 10 OFFSET 100');
  $stmt->bind_param('ss', $login, $email);
  $result = $stmt->execute();
  $stmt->bind_result($id);
  while($stmt->fetch()) {
    $a = $id;
  }
  $stmt->close();
  /**
   * Select 3
   *
  $login = "mylogin'mm";
  $stmt = $db->prepare('Select ID from USERS where LOGIN = ? LIMIT 30 OFFSET 100');
  $stmt->bind_param('s', $login);
  $result = $stmt->execute();
  $stmt->bind_result($id);
  while($stmt->fetch()) {
    $a = $id;
  }
  $stmt->close();*/  
  /**
   * Insert 1
   */
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $stmt = $db->prepare('Insert into USERS (LOGIN, EMAIL) values (?, ?)');
  $stmt->bind_param('ss', $login, $email);
  $result = $stmt->execute();
  /**
   * Insert 2
   *
  $login = "mylogin'mm";
  $email = "myemail%@em.com";
  $stmt->bind_param('ss', $login, $email);
  $result = $stmt->execute();*/
  return microtime(TRUE) - $startTime;
}
 
$db = new mysqli('localhost', 'test', 'test', 'test');
$count = 1000;
$time = 0;
for ($i = 0; $i < $count; $i++) {
  $time += workWithoutPS($db); //workWithPS($db)
}
$time /= $count;
echo  $time . '</br>';


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


Listing №2 (unknown)
workWithoutPS workWithPS

$count=1000
Select 1 + Insert 1
libmysql mysqlnd libmysql mysqlnd
0.0150 0.0153 0.0172 0.0178
0.0248 0.0277 0.0249 0.0277
0.0368 0.0415 0.0334 0.0383
______ ______ ______ ______
0.0255 0.0282 0.0252 0.0279

Select 1 + Insert 1 + Insert 2
libmysql mysqlnd libmysql mysqlnd
0.0261 0.0287 0.0275 0.0298
0.0507 0.0566 0.0460 0.0513
0.0738 0.0834 0.0636 0.0731
______ ______ ______ ______
0.0502 0.0562 0.0457 0.0514

$count=10000
Select 2 + Select 3
libmysql mysqlnd libmysql mysqlnd
- 0.00659 - 0.01000
- 0.00653 - 0.01040
- 0.00649 - 0.01010
______ _______ _______ _______
- 0.00654 - 0.01020

Из таких результатов я делаю следующий вывод: использование повсеместно запросов с prepared statements может замедлить работу с базой данных в полтора раза. Реальная выгода происходить только лишь при запуске подряд одинаковых запросов с разными параметрами. Также я вижу, что mysqlnd не совсем оправдывает ожидания. Как говорят разработчики, mysqlnd пришел заменить libmysql в связи с тем, что libmysql оптимизирована для работы с приложениями, написанными на С, а не PHP. И настоятельно предлагают использовать mysqlnd, потому что он оптимизирован для работы из PHP и по сравнению с libmysql имеет множество усовершенствований по работе с памятью и скоростью работы. Но, как мы можем наблюдать, mysqlnd все время проигрывает libmysql, хотя стоит заметить, что я не измерял используемую память и ресурс процессора. Это что касается производительности.

Что же с нашей безопасностью? Что лучше использовать prepared statements или real_escape_string? Многие говорят что real_escape_string это костыль, он что-то там недофильтровывает и всё такое. Я считаю, что он такой же костыль как и prepared statements, который не может подставлять переменные абсолютно во все места SQL запроса. Обычно логика вокруг real_escape_string такая же костыльная как и вокруг prepared statements, причём вокруг последних бывает побольше всякой хрени типа call_user_func_array. В связи с эти, я делаю следующий вывод: что безопасность, обеспечиваемая при использовании расширения mysqli, одинаково удобна, костыльна и "безопасна" как при real_escape_string так и при prepared statements. Другое дело использование prepared statements в PDO, где они поддерживаются на клиентской части и эмулируются, если сервер базы данных не поддерживает prepared statements. Слой абстракции PDO явно оправдывает подстановку параметров в запросы к разным СУБД.

Но зачем нужны prepared statements в mysqli? Допустим, вы вставляете несколько записей с помощью INSERT. Но ведь есть варианты, как вставить много записей с помощью одного запроса. Допустим, вы удаляете записи, но это тоже может быть довольно специфичной ситуацией. Некоторые аргументы в пользу повсеместного использования prepared statements явно пестрят рассказами о том, что MySQL-сервер кеширует подготовленные запросы идущие не подряд. То есть подготовив и выполнив запрос, можно подготовить и выполнить другой, отличный по структуре запрос, не переживая о том, что он собственно другой по структуре. Разбор первого сохранится в каком-то кеше и подхватится при повторном его использовании. Хотя я сильно не искал, но в документации я такого не нашел, да и почему-то слабо вериться в такое, хотя с другой стороны, вроде как, MySQL-сервер должен быть довольно всемогущ.

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

Как бы там ни было, как говориться, каждому своё, и для каждого обращения к базе данных можно выбрать свой способ. А сосредоточится на использовании лишь одного способа - вовсе не выход, хотя есть много случаев, где это будет нормальным решением. Например, не использовать prepared statements API в mysqli, потому что пока всё указывает на то, что он там не нужен и способен замедлить работу с базой данных.

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

Делим код пополам или представление по шаблону в PHP


Зачем все это? - возмущенно спросил Артём, широко раскрыв глаза. У нас и так все прекрасно работает! Я знаю, где мне надо поменять код, чтобы новости отображались в три колонки, а не в две. А Тане я потом покажу, где поменять теги...
Если вы понимаете, какие проблемы у Тани с Артёмом, то это уже хорошо. Как некоторые догадываются Артём - PHP программист, Таня - дизайнер-верстальщик. У них есть общая проблема - файл, который формирует ленту новостей. Редактируют они его по очереди и сам черт ногу сломит в нём. А все потому, что PHP код Артёма уже давно зависит от HTML кода Татьяны и наоборот. И нет им покоя длительное время, если что-то надо поменять в этом файле. Если бы Артём с Татьяной знали о представлении по шаблону, то они бы не тратили кучу нервов и времени на столь простую функцию.

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

Представление по шаблону (Template View) или шаблонизация - это механизм, позволяющий заменять в статической HTML странице ее динамические части, то есть, простым языком говоря, в Таниной HTML страничке заменять данные, генерируемые PHP кодом Артёма. При этом Артём работает со своим файлом, в котором не видит ни одного Таниного тега, а Таня со своим. С этим механизмом Артёму и Тане очень удобно. Им больше не режет глаза чужой листинг, и их идеи в построении кода не зависят друг от друга. Таким образом, выделим два основных, почти смежных, достоинства шаблонизации:

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

Для начала маленький пример:

Listing №1 (PHP)

<?php
  date_default_timezone_set('Europe/Kiev');
  $UserMessage = 'Артём';
  if (date('md') === '0509') {
    $UserMessage .= ' С Днём Победы!';
  }
?>
<html>
<head>
<title>Пример1</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
Привет, <?php echo $UserMessage; ?>
</body>
</html>

Поспешу вас обрадовать - перед вами простейший код, выполняющий шаблонизацию. Да, может вы и не знали, но PHP сам по себе отличный шаблонизатор. Как видно из примера, в результате выполнения сценария в статической части (шаблоне - код черного цвета) будет заменена ее динамическая часть, которую PHP сгенерирует с помощью команды echo. Это самый простой случай привожу для того, чтобы вы запомнили, что PHP - сам по себе шаблонизатор. Но как бы там ни было, такой подход не решает проблемы Артёма и Татьяны.

Разделим код на два файла: model.php и template.php. Первый отдадим Артёму, а второй Татьяне.

model.php

Listing №2 (PHP)
<?php
  date_default_timezone_set('Europe/Kiev');
  $UserMessage = 'Артём';
  if (date('md') === '0509') {
    $UserMessage .= ' С Днём Победы!';
  }
  include 'template.php';
?>

template.php

Listing №3 (PHP)
<html>
<head>
<title>Пример2</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
Привет, <?php echo $UserMessage; ?>
</body>
</html>

Как же теперь обрадовался Артём - нет ни одного Татьяниного тега. Чистый PHP код. Татьяне тоже повезло. Хотя ее немного смущает кусочек PHP кода в теле страницы, но она не сильно обращает на это внимание, потому как знает, что в этом месте просто будет написано сообщение пользователю.

Заметьте, обычным разделением кода мы намного облегчили работу Артёма и Татьяны. Теперь статическая часть страницы, то есть интерфейс пользователя отделен от логики работы приложения. Не только Татьяна, но и любой другой человек, не владеющий программированием на PHP, но имеющий опыт в дизайне и верстке, теперь легко сможет изменить внешний вид сайта. Артем в любой момент может поменять логику работы приложения, например, заменив ее на поздравление "С Новым Годом". При этом ему не будет мозолить глаза вся эта Татьянина писанина и, пока Таня ваяет дизайн, он быстренько все поменяет и пойдет домой смотреть Футураму.

Таким образом мы отделили интерфейс от логики работы скрипта. Мы создали статический шаблон (файл template.php), в который поместили маркер PHP кода (<?php echo $UserMessage; ?>), обращающийся к скрипту для получения динамической информации - строки с приветствием пользователя. Такой метод шаблонизации называется native (родной) или прямой, потому как команда include 'template.php' фактически вставила template.php в model.php и PHP шаблонизировал все сам по себе, заменив свой вызов в шаблоне значением переменной $UserMessage.

Раз уж у нас есть такой хороший способ шаблонизации, то спрячем логику его работы в простой класс:

class.view.php

Listing №4 (PHP)
<?php
/**
 * Класс шаблонизатор
 */
class View {
  /**
   * Файл шаблона
   * @var string
   */
  private $_File;
  /**
   * Массив переменных-маркеров шаблона
   * @var array
   */
  private $_Vars;
  /**
   * Конструктор, инициализируем свойства класса
   */
  public function __construct()
  {
    $this->_File = '';
    $this->_Vars = array();
  }
  /**
   * Устанавливает файл шаблона
   * @param string $File
   */
  public function SetTemplate($File)
  {
    $this->_File = $File;
  }
  /**
   * Связывает переменные скрипта с переменными-маркерами
   * @param string $VarName
   * @param string $VarValue
   */
  public function AssignVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
  }
  /**
   * Отображает шаблон
   */
  public function Display()
  {
    if (file_exists($this->_File)) {
      extract($this->_Vars, EXTR_PREFIX_ALL, '');
      include $this->_File;
    }
  }
}
?>

Теперь попробуем его использовать. Поместим наш класс в файл class.view.php и перепишем классы model.php и template.php:


template.php

Listing №5 (PHP)
<html>
<head>
<title>Пример3</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
Привет, <?php echo $_UserMessage; ?>
</body>
</html>


model.php

Listing №6 (PHP)
<?php
require_once 'class.view.php';
date_default_timezone_set('Europe/Kiev');
$UserMessage = 'Артём';
if (date('md') === '0509') {
  $UserMessage .= ' С Днём Победы!';
}
//Создаем объект представления интерфейса
$Veiw = new View;
//Устанавливаем шаблон представления
$Veiw->SetTemplate('template.php');
//Установка значения переменной-маркера сообщения пользователю
$Veiw->AssignVar('UserMessage', $UserMessage);
//Отображение интерфейса
$Veiw->Display();
?>


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

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

class.view.php

Listing №7 (PHP)
<?php
/**
 * Класс шаблонизатор
 */
class View {
  /**
   * Файл шаблона
   * @var string
   */
  private $_File;
  /**
   * Массив переменных-маркеров шаблона
   * @var array
   */
  private $_Vars;
  /**
   * Конструктор, инициализируем свойства класса
   */
  public function __construct()
  {
    $this->_File = '';
    $this->_Vars = array();
  }
  /**
   * Устанавливает файл шаблона
   * @param string $File
   */
  public function SetTemplate($File)
  {
    $this->_File = $File;
  }
  /**
   * Связывает переменные скрипта с переменными-маркерами
   * @param string $VarName
   * @param string $VarValue
   */
  public function AssignVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
  }
  /**
   * Отображает шаблон
   */
  public function Display()
  {
    if (file_exists($this->_File)) {
      include $this->_File;
    }
  }
  /**
   * Функция доступа к элементу массива
   * переменных-маркеров шаблона
   * @param string $Varname
   * @return mixed
   */
  public function __get($VarName)
  {
    if (array_key_exists($VarName, $this->_Vars)) {
      return $this->_Vars[$VarName];
    }
    return NULL;
  }
}
?>


И немного изменим шаблон:


template.php

Listing №8 (PHP)
<html>
<head>
<title>Пример4</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
Привет, <?php echo $this->UserMessage; ?>
</body>
</html>

Все стало лучше и проще. Заметьте, в файле model.php мы ничего не меняем - опять же, благодаря тому, что мы скрыли сложности реализации шаблонизации в классе. Скрывайте сложность - это избавит вас от многих проблем и не только при программировании на PHP.

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

Теперь самое время вспомнить наших Артёма с Татьяной и познакомить вас с еще одним методом шаблонизации - методом замены. Как бы мы не старались угодить Артёму с его скриптом, Татьяну все же немного напрягает присутствие чужого кода в своем милом HTML 4.01 Transitional. Дело в том, что полностью Татьяну удовлетворить нельзя, но мы можем максимально приблизить такой момент как раз с помощью метода замены. Для этого нам понадобиться что? Правильно - новый класс.

class.view.php

Listing №9 (PHP)
<?php
/**
 * Класс шаблонизатор
 */
class View {
  /**
   * Файл шаблона
   * @var string
   */
  private $_File;
  /**
   * Массив переменных маркеров шаблона
   * @var array
   */
  private $_Vars;
  /**
   * Конструктор, инициализируем свойства класса
   */
  public function __construct()
  {
    $this->_File = '';
    $this->_Vars = array();
  }
  /**
   * Устанавливает файл шаблона
   * @param string $File
   */
  public function SetTemplate($File)
  {
    $this->_File = $File;
  }
  /**
   * Связывает переменные скрипта с переменными-маркерами
   * @param string $VarName
   * @param string $VarValue
   */
  public function AssignVar($VarName, $VarValue)
  {
    if ($VarName !== '') {
      $this->_Vars[$VarName] = $VarValue;
    }
  }
  /**
   * Отображает шаблон
   */
  public function Display()
  {
    echo $this->_Prepare();
  }
  /**
   * Загружает шаблон и
   * заменяет в нем маркеры замены
   */
  private function _Prepare()
  {
    $Content = '';
    if (file_exists($this->_File)) {
      $Content = file_get_contents($this->_File);
      foreach ($this->_Vars as $VarName=>$VarValue) {
        $Content = str_replace('{' . $VarName . '}', $VarValue, $Content);
      }
    }
    return $Content;
  }
}
?>

Посмотрим теперь на наш шаблон:


template.php

Listing №10 (HTML)
<html>
<head>
<title>Пример5</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
Привет, {UserMessage}
</body>
</html>


Да он уже и не PHP совсем! Хотя в нем и нет вообще PHP кода, в нем присутствует некий псевдокод. Но для Татьяны, дизайнера-верстальщика, это просто набор символов {UserMessage}, который довольно прост в восприятии и практически ее не отвлекает. Разве что она знает, что там, в итоге, будет имя пользователя.

Стоит заметить, что данная конструкция не разрушает никакие HTML теги и ее удобно наблюдать в WYSIWYG редакторах, в отличие от шаблонов, реализованных для native метода, которые часто могут исказить вид шаблона в "неподготовленных" к этому редакторах. Заметно, что метод замены намного удобнее для Тани. Да и Артём, как обычно, не сильно напрягается. Таким образом, мы практически полностью отделили Танин HTML код от PHP кода Артёма, а также логику вида интерфейса от логики работы приложения. Теперь, с помощью такого шаблонизатора можно написать приложение на PHP, описать маркеры в шаблонах и тогда любой дизайнер, даже понятия не имеющий, что есть PHP, сможет эффективно поработать.

Но у всего хорошего всегда есть какие-то недостатки. И этот метод не исключение. Во-первых, метод замены несколько, а порой и намного, медленнее native метода. Это естественно - мы же считываем файл с диска, ищем и заменяем в нем одни значения на другие. Во-вторых, при написании всего кода, можно теоретически совершит больше ошибок. Например, не связав переменную скрипта с переменной-маркером, мы можем получить надпись "Привет , {UserMessage}", в то время как в предыдущем подходе мы просто увидим "Привет, ". Я думаю, первое выглядит похуже. Вот такими недостатками мы можем платить за комфорт Татьяны. Поэтому, Татьяны со знаниями базовых конструкций PHP обычно ценятся подороже обычных Татьян. Но не надо так сильно останавливать внимание на недостатках. В конце концов, решающим фактором является не количество недостатков, а относительная их доля в количестве достоинств. Существует масса классов использующих, именно этот метод. Одним из самых распространенных является Smarty. Его используют миллионы сайтов и не замечают данных недостатков.

То же самое относиться и к native методу. Он не избавляет нас от PHP кода в шаблоне. Зато он намного быстрее и предоставляет более эффективное управление логикой представления, благодаря именно PHP коду внутри шаблона. Не надо стараться извлечь весь PHP код из шаблона - главное разделить логику представления и приложения. И если PHP код в шаблоне управляет логикой представления, то это есть норма и ничего более менять уже не надо. Разве что необходимость в эффективной работе дизайнеров или требования заказчика или личные предпочтения заставят вас все же использовать метод замены.

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

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

Вариант реализации паттерна Registry или глобальная фабрика

PHP

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

Думаю, неплохо получилось, так что комментировать мне уже нечего. Но вы можете это сделать в комментариях.



Listing №1 (PHP)

<?php
 
//интерфейс базы данных
interface Db {
  function foo();
}
 
//база данных
class RealDb implements Db {
 
  function foo()
  {
    echo 'real db';
  }
}
 
//фиктивная служба базы данных
class TestDb implements Db {
  function foo()
  {
    echo 'test db';
  }
}
 
//Реестр
class Registry {
 
  private static $_instance = null;
 
  protected $_db;
 
  public static function init($instance)
  {
    self::$_instance = new $instance();
    if (!self::$_instance instanceof Registry) {
      throw new Exception('');
    }
  }
 
  private static function getInstance()
  {
    if (!self::$_instance) {
      self::init('Registry');
    }
   
    return self::$_instance;
  }
 
  public static function getDb()
  {
    return self::getInstance()->_db;
  }
   
  protected function __construct() {
    $this->_db = new RealDb();
  }
 
  private function __clone() {}
}
 
//Реестр для тестирования
class TestRegistry extends Registry {
 
  protected function __construct() {
    $this->_db = new TestDb();
  }
}
 
////Пример
 
//Инициализация для тестирования
//Registry::init('TestRegistry');
 
$db = Registry::getDb();
$db->foo();


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

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

Учимся работать с Excel файлами

Всем привет! Сегодня наш урок будет посвящен использованию Excel'я в Delphi. А именно я рассажу как вывести табличку из Excel'я в компонент DbGrid.

Итак начинаем.....

Таблица содержит 3 столбика: Имя, Фамилия, Должность. Обратите внимание что таблицу я создал на листе с Именем Лист1 (в дальнейшем нам это пригодиться). Так далее, я добавил в неё первую запись (Олег, Иванов, Менеджер). Всё начальные приготовления завершены, осталось только сохранить наш документ, я сохраню его на диске C:\ с именем 2.xls

Теперь открываем Delphi и кидаем на форму 4 компонента: ADOConnection, ADOQuery с закладки ADO, DataSource с закладки Data Access и DBGrid с закладки DataControls. Для того что-бы вывести документ Excel в компонент DbGrid нам нужно с начала подключиться к этому документу, для этих целей будем использовать компонент ADOConnection. Выделяем его и в свойстве ConnectionString пишем вот такой код

Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\2.xls;Extended Properties=Excel 8.0;


В Свойстве LoginPromt и Connected установите значение false.

Далее у нас на очереди компонент DataSource, В Свойство DataSet поставте ADOQuery1.
А в компоненте ADOQuery свойство Connection установите на ADOConnection1.

Ну что со связями между компонентами вроде бы разобрались, теперь добавьте на форму 2 Компонента Edit и один компонент Button с закладки Standart. В первом Edit'e будет храниться путь до excel файла,
во втором edit'e мы будем писать SQL запрос, а при нажатии на кнопку программа будет подключаться к Excel файлу и выводить таблицу в компонент DbGrid.

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


procedure ConnectToExcel;  

И нажимаем комбинацию клавиш Ctrl+Shift+C

Delphi автоматически генерирует нам шаблончик для будущей процедуры.


procedure TForm1.ConnectToExcel;  
begin  
  
end;  
end; 

Посмотрите теперь как должна выглядеть эта процедура и допишите не достающие строки.


procedure TForm1.ConnectToExcel;  
var strConn: widestring;  
begin  
strConn:='Provider=Microsoft.Jet.OLEDB.4.0;' +  
'Data Source=' +Edit1.Text+ ';' +  
'Extended Properties=Excel 8.0;';  
AdoConnection1.Connected:=False;  
AdoConnection1.ConnectionString:=strConn;  
try  
AdoConnection1.Open;  
except  
ShowMessage('Не могу соединиться с Excel книгой, которая расположена по адресу: '+Edit1.Text+' !');  
raise;  
end;  
end;  

С помощью этой процедуры мы будем устанавливать соединение между Excel файлом и Нашей программой.

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


procedure FetchData;  

И нажимаем комбинацию клавиш Ctrl+Shift+C

Полный листинг процедуры:


procedure TForm1.FetchData;  
begin  
ConnectToExcel;  
AdoQuery1.Close;  
AdoQuery1.SQL.Text:=Edit2.Text;  
try  
AdoQuery1.Open;  
except  
ShowMessage( 'Не могу выполнить Sql запрос ' + Edit1.Text +'!');  
raise;  
end;  
end;  

Двигаемся дальше, создаем обработчик событий OnClick на кнопке.
Между begin ... end пишем имя второй функции: FetchData;

Все запускаем программу, в первом Edit'e прописываем полный путь до нашего excel документа (у меня это C:\2.xls), а во втором Edite пишем SQL запрос на выборку всех полей из таблицы Лист1:


SELECT * FROM [Лист1$]  

Жмем на кнопку.., ОПА табличка вывелась в компонент DBGRID

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

Создание непрямоугольных форм в Delphi

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

SetWindowsRgn(Form1.Handle, True);
Правда, перед этим потребуется создать подходящий регион. Большинство из тех, кто работает на Delphi, не знают, что такое регион, главным образом потому, что эта штука не нашла своего отражения в VCL. :-)

Документация утверждает, что регион, это "прямоугольник, многоугольник, эллипс или комбинация двух или более фигур из приведённого списка". Регионы используются для "заливки, отсечения (то, что по английски называется clipping)" и других, не менее полезных операций.

Для создания регионов существуют такие функции (с очевидным назначением), как CreateRectRgn, CreateEllipticRgn, CreatePolygonRgn и несколько других. Объединять регионы между собой можно при помощи функции CombineRgn.

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

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

Это правда. Насколько мне известно, Windows не умеет этого делать, то есть в ней нет функции CreateBitmapRgn. Тем не менее, можно создавать и такие регионы. Для этого необходимо пробежаться по всей картинке сверху вниз, в каждой строчке найти непрозрачные области и сделать из них прямоугольные регионы (эти прямоугольники будут высотой в 1 пиксель). Затем мы объединяем эти регионы, и, вуаля — вот он, искомый регион!

Готов поспорить, вы думаете, что это слишком сложно... :-) Проверяем...


function BitmapToRegion(Bitmap: TBitmap; TransColor: TColor): HRGN;  
var  
  X, Y: Integer;  
  XStart: Integer;  
begin  
  Result := 0;  
  with Bitmap do  
    for Y := 0 to Height - 1 do  
    begin  
    X := 0;  
    while X < Width do  
    begin  
      // Пропускаем прозрачные точки  
      while (X < Width) and (Canvas.Pixels[X, Y] = TransColor) do  
        Inc(X);  
      if X >= Width then  
        Break;  
      XStart := X;  
      // Пропускаем непрозрачные точки  
      while (X < Width) and (Canvas.Pixels[X, Y] <> TransColor) do  
        Inc(X);  
      // Создаём новый прямоугольный регион и добавляем его к региону всей картинки  
      if Result = 0 then  
        Result := CreateRectRgn(XStart, Y, X, Y + 1)  
      else  
        CombineRgn(Result, Result, CreateRectRgn(XStart, Y, X, Y + 1), RGN_OR);  
    end;  
  end;  
end;  

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

type  
TFormMain = class(TForm)  
private  
  { Private declarations }  
  procedure WMLButtonDown(var Msg: TMessage); message WM_LBUTTONDOWN;  
public  
  { Public declarations }  
end;  

В разделе реализации эта функция выглядит так:

procedure TFormMain.WMLButtonDown(var Msg: TMessage);  
begin  
  Perform(WM_NCLBUTTONDOWN, HTCAPTION, Msg.LParam);  
end;  

Форма посылает самой себе сообщение WM_NCLBUTTONDOWN с wParam равным HTCAPTION, то есть эмулирует ситуацию, когда пользователь нажимает левую кнопку мыши на заголовке формы. После этого форму можно спокойно перемещать за всю её область.

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

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