Все ленты — последние статьи

Простой скрипт для сокращения URL-адреса

СКАЧАТЬ ИСХОДНИКИ

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

Требовалось встроить в PDF-документы ссылки, которые могли изменяться в будущем.

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

Эта проблема может быть решена с помощью скрипта для сокращения URL-адресов. Уже довольно давно существуют сервисы, предоставляющие такие возможности, но они не без недостатков. Вот некоторые из них:

  • Зависимость от внешнего сервиса — когда он не работает, ссылки тоже оказываются недоступными. Еще хуже, если сервис закрывается — сами понимаете к чему это приведет;
  • Вся статистика отслеживается сторонними сервисами;
  • Обычно, не разрешается изменять вид укороченных ссылок;
  • Некоторые сервисы позволяют выбрать свои псевдонимы, но большинство назначает их автоматически.

Итак, задача состоит в написании собственного PHP-скрипта, который был позволил решать задачу, описанную выше!

Идея

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

  • Короткие ссылки будут иметь вид http://example.com/l/short-link. Посещение данного URL-адреса будет переадресовываться на реальный адрес;
  • Весь скрипт будет располагаться в единственном файле — index.php, без внешних зависимостей;
  • Панель администратора с инструментами добавления и редактирования ссылок отсутствует. Все будет управляться с помощью простого текстового файла, расположенного на сервере, который очень легко отредактировать;
  • Идентификаторы не будут автоматически присваиваться ссылкам — вы сами будете вводить псевдоним.

Эти пункты упростят наш будущий скрипт. Чтобы сделать все еще более понятным, я решил хранить ссылки в INI-файле, потому что его легко отредактировать и PHP имеет их встроенную поддержку через функцию parse_ini_file (это убережет нас от необходимости читать содержимое файла и парсить его вручную, к тому же эта функция работает очень быстро).

 
 

INI-файл

INI-файл выглядит так:

links.ini

google = https://www.google.com/
fb = https://www.facebook.com/

Слева короткая ссылка, а справа — длинная. Все очень просто!

Реализация

Наш PHP-скрипт гениально прост:

index.php

$links = parse_ini_file('links.ini');
if(isset($_GET['l']) && array_key_exists($_GET['l'], $links)){
header('Location: ' . $links[$_GET['l']]);
} else{
header('HTTP/1.0 404 Not Found');
echo 'Unknown link.';
}

Скрипт ожидает получения псевдонима в массиве $_GET['l'], который выглядит например так: http://example.com/index.php?l=google.

Конечно это не сокращение, но мы можем улучшить это с помощью файла .htaccess (используется для запуска веб-сервера Apache).

.htaccess

RewriteEngine On
RewriteCond $1 !^(index\.php)
RewriteRule ^(.*)$ index.php?l=$1 [L]

Этот файл должен находиться в той же директории, что и index.php и links.ini. Его назначение состоит в том, чтобы перенаправлять каждый запрос, который идет НЕ к файлу index.php, на адрес вида index.php?l=xxx. Это защитит от циклических ссылок и сделает файл links.ini недоступным из браузера.

Для достижения наилучших результатов, поместите три этих файла в папку с коротким именем, к примеру «l», в корневую папку сайта, и получите короткий вид ссылок в форме http://example.com/l/google.

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

Заключение

Если вы не хотите хранить ссылки в отдельном файле, то можете поместить их в ассоциативный массив внутри скрипта.

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

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

Перевод статьи «Quick Tip — Create a Simple URL Shortener With 10 Lines of PHP» был подготовлен дружной командой проекта Сайтостроение от А до Я.

Шаблон проектирования: Хранилище

Определение шаблону проектирования Хранилище (Репозитарий) дано Эриком Эвенсом в его книге Domain Driven Design — по его мнению это один из самых полезных и наиболее широко применяемых шаблонов проектирования из когда-либо изобретенных.

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

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

Шаблон проектирования Фабричный метод

Как мы уже упоминали во вступлении, Хранилище будет объединяться с Фабричным методом через Шлюзы (сохранение). Это также шаблоны проектирования, и, если вы не знакомы с ними, то задачей данного раздела является как раз пролить свет на данный аспект.

Фабричный метод является простым шаблоном, который задает удобный способ создания объектов. Это класс или набор классов, отвечающих за создание объектов, в которых нуждается наша бизнес-логика. Фабричный метод обычно поддерживает метод «make()», с его помощью класс распознает, как принимать всю поступающую информацию, необходимую для создания объекта и возвращает объект уже готовым к применению согласно бизнес-логике.

Более подробно Шаблон Фабричный Метод был рассмотрен в предыдущей статье: Шаблоны проектирования: Пособие для начинающих. Если вы желаете еще более предметно разобраться в этой теме, советуем вам изучить статью Agile Design Patterns.

Шаблон Шлюз

Также известен под названием «Шлюз таблицы данных» — это простой шаблон, который обеспечивает связь между бизнес-логикой и непосредственно базой данных. Его основная задача заключается в том, чтобы создавать запросы к базе данных и предоставлять полученные данные, сообразуясь со структурой понятной языкам программирования (например, массив в PHP).

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

Шаблон проектирования Шлюз подробно рассмотрен и проиллюстрирован примерами в пособии Evolving Toward a Persistence Layer. Кроме того, в том же курсе Agile Design Patterns второй урок по шаблонам проектирования также посвящен данной тем.

Проблемы, которые нам нужно решить

Дублирование при обработке данных

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

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

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

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

Дублирование данных при перестройке логики

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

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

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

Дублирование данных в хранилищах при перестройке логики

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

Основные концепции

Хранилище для извлечения данных

Хранилище может функционировать в двух направлениях: извлечение данных и запись данных.

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

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

Шлюз в свою очередь предоставляет исходные данные объекта (массив со значениями). После этого Хранилище принимает эти данные, совершает необходимые преобразования и вызывает соответствующие методы Фабричного метода.

Фабричный метод обеспечивает формирование объектов, созданных на основе данных, предоставленных Хранилищем. Также Хранилище будет объединять эти объекты и вернет их в виде набора объектов (например, массив объектов или набор объектов.

Хранилище для хранения данных

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

При использовании для хранения информации клиентского класса инициируется одно прямое обращение к Фабричному методу. Представьте себе ситуацию, когда в блоге появляется новый комментарий. Объект «Комментарий» создан для нашей бизнес-логики (класс Клиент), а затем отправлен в хранилище для хранения.

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

Точки соединения

 
 

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

В центре располагается наше Хранилище. Слева — интерфейс Шлюза, реализация и непосредственно процесс сохранения данных. Справа — интерфейс для Фабричного метода и непосредственно его реализация. И вверху показан класс Клиент.

Как можно видеть по направлению стрелок, зависимости здесь обоюдные. Хранилище связано только с абстрактными интерфейсами Фабричного метода и Шлюза. Шлюз связан со своим интерфейсом и процессом сохранения данных. Фабричный метод связан только с интерфейсом.

Клиент связан с Хранилищем, которое должно иметь соответствующий формат. Существует тенденция того, что Хранилище часто имеет не настолько корректную форму, как Клиент.

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

Управление комментариями к записям в блоге с помощью Хранилища

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

Комментарии

Начнем с теста, который даст нам информацию о том, что наш объект Комментария должен содержать:

class RepositoryTest extends PHPUnit_Framework_TestCase {
function testACommentHasAllItsComposingParts() {
$postId = 1;
$commentAuthor = "Joe";
$commentAuthorEmail = "Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.<script type="text/javascript">
/* <![CDATA[ */
(function(){try{var s,a,i,j,r,c,l,b=document.getElementsByTagName("script");l=b[b.length-1].previousSibling;a=l.getAttribute('data-cfemail');if(a){s='';r=parseInt(a.substr(0,2),16);for(j=2;a.length-j;j+=2){c=parseInt(a.substr(j,2),16)^r;s+=String.fromCharCode(c);}s=document.createTextNode(s);l.parentNode.replaceChild(s,l);}}catch(e){}})();
/* ]]> */
</script>";
$commentSubject = "Joe Has an Opinion about the Repository Pattern";
$commentBody = "I think it is a good idea to use the Repository Pattern to persist and retrieve objects.";
$comment = new Comment($postId, $commentAuthor, $commentAuthorEmail, $commentSubject, $commentBody);
}
}

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

На основании данного примера мы можем просто предположить, что это простой объект данных. Построенный с набором переменных:

class Comment {
}

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

require_once '../Comment.php';
class RepositoryTest extends PHPUnit_Framework_TestCase {
[ … ]
}

Но это еще далеко не совершенство. Наш тест по сути еще ничего не тестирует. Давайте пропишем все элементы, которые обращаются к этому классу:

function testACommentsHasAllItsComposingParts() {
$postId = 1;
$commentAuthor = "Joe";
$commentAuthorEmail = "Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.<script type="text/javascript">
/* <![CDATA[ */
(function(){try{var s,a,i,j,r,c,l,b=document.getElementsByTagName("script");l=b[b.length-1].previousSibling;a=l.getAttribute('data-cfemail');if(a){s='';r=parseInt(a.substr(0,2),16);for(j=2;a.length-j;j+=2){c=parseInt(a.substr(j,2),16)^r;s+=String.fromCharCode(c);}s=document.createTextNode(s);l.parentNode.replaceChild(s,l);}}catch(e){}})();
/* ]]> */
</script>";
$commentSubject = "Joe Has an Opinion about the Repository Pattern";
$commentBody = "I think it is a good idea to use the Repository Pattern to persist and retrieve objects.";
$comment = new Comment($postId, $commentAuthor, $commentAuthorEmail, $commentSubject, $commentBody);
$this->assertEquals($postId, $comment->getPostId());
$this->assertEquals($commentAuthor, $comment->getAuthor());
$this->assertEquals($commentAuthorEmail, $comment->getAuthorEmail());
$this->assertEquals($commentSubject, $comment->getSubject());
$this->assertEquals($commentBody, $comment->getBody());
}

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

class Comment {
private $postId;
private $author;
private $authorEmail;
private $subject;
private $body;
function __construct($postId, $author, $authorEmail, $subject, $body) {
$this->postId = $postId;
$this->author = $author;
$this->authorEmail = $authorEmail;
$this->subject = $subject;
$this->body = $body;
}
public function getPostId() {
return $this->postId;
}
public function getAuthor() {
return $this->author;
}
public function getAuthorEmail() {
return $this->authorEmail;
}
public function getSubject() {
return $this->subject;
}
public function getBody() {
return $this->body;
}
}

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

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

Эти тестовые методы и тестовые классы мы можем рассматривать в качестве класса «Клиент» из рассмотренной выше схемы.

Наш Шлюз для записи

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

require_once '../InMemoryPersistence.php';
class InMemoryPersistenceTest extends PHPUnit_Framework_TestCase {
function testItCanPerisistAndRetrieveASingleDataArray() {
$data = array('data');
$persistence = new InMemoryPersistence();
$persistence->persist($data);
$this->assertEquals($data, $persistence->retrieve(0));
}
}

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

В этом тесте мы создаем новый объект InMemoryPersistence и попытаемся записать и извлечь массив данных:

require_once __DIR__ . '/Persistence.php';
class InMemoryPersistence implements Persistence {
private $data = array();
function persist($data) {
$this->data = $data;
}
function retrieve($id) {
return $this->data;
}
}

Это простейший код, который оперирует входящей переменной $data и частными переменными и вызывает их через метод retrieve. На данной стадии в нем не предусмотрено отсылки переменной $id.

Это самое простое, что мы могли бы сделать, чтобы протестировать нашу модель. Мы также взяли на себя смелость представить и реализовать интерфейс под названием Persistence:

 
 
interface Persistence {
function persist($data);
function retrieve($ids);
}

Этот интерфейс определяет два метода, которые должны применяться для любого Шлюза. Запись и вывод. Как вы, наверное, уже догадались, наш Шлюз и является классом InMemoryPersistence, а физическое сохранение является частной переменной, обрабатывающей данные в регистрах памяти.

Но давайте вернемся к реализации этой записи:

function testItCanPerisistSeveralElementsAndRetrieveAnyOfThem() {
$data1 = array('data1');
$data2 = array('data2');
$persistence = new InMemoryPersistence();
$persistence->persist($data1);
$persistence->persist($data2);
$this->assertEquals($data1, $persistence->retrieve(0));
$this->assertEquals($data2, $persistence->retrieve(1));
}

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

require_once __DIR__ . '/Persistence.php';
class InMemoryPersistence implements Persistence {
private $data = array();
function persist($data) {
$this->data[] = $data;
}
function retrieve($id) {
return $this->data[$id];
}
}

Для теста нам пришлось немного изменить наш код. Теперь нам нужно добавить данные в массив, а не просто заменить их на те, что были отправлены на хранение в persists(). Мы также должны рассмотреть параметр $id и вернуть элемент по этому индексу.

Для объекта InMemoryPersistence этого достаточно. При необходимости мы можем вернуться к нему позже.

Наш Фабричный метод

У нас есть клиент (наши тесты), метод записи через Шлюз и объекты комментариев, которые нужно сохранить. Следующим недостающим звеном цепи является Фабрика (Фабричный метод).

Мы начали составлять наш код с файла RepositoryTest. Затем этот тест создал реальный объект Comment.

Теперь нам нужно создать тесты для проверки того, сможет ли наша Фабрика создавать такие объекты Comment. Похоже, что на предыдущем этапе мы немного ошиблись в предпосылках и наш тест скорее был предназначен для проверки Фабричного метода, нежели Хранилища в целом.

Мы можем переместить его в другой файл CommentFactoryTest:

require_once '../Comment.php';
class CommentFactoryTest extends PHPUnit_Framework_TestCase {
function testACommentsHasAllItsComposingParts() {
$postId = 1;
$commentAuthor = "Joe";
$commentAuthorEmail = "Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.<script type="text/javascript">
/* <![CDATA[ */
(function(){try{var s,a,i,j,r,c,l,b=document.getElementsByTagName("script");l=b[b.length-1].previousSibling;a=l.getAttribute('data-cfemail');if(a){s='';r=parseInt(a.substr(0,2),16);for(j=2;a.length-j;j+=2){c=parseInt(a.substr(j,2),16)^r;s+=String.fromCharCode(c);}s=document.createTextNode(s);l.parentNode.replaceChild(s,l);}}catch(e){}})();
/* ]]> */
</script>";
$commentSubject = "Joe Has an Opinion about the Repository Pattern";
$commentBody = "I think it is a good idea to use the Repository Pattern to persist and retrieve objects.";
$comment = new Comment($postId, $commentAuthor, $commentAuthorEmail, $commentSubject, $commentBody);
$this->assertEquals($postId, $comment->getPostId());
$this->assertEquals($commentAuthor, $comment->getAuthor());
$this->assertEquals($commentAuthorEmail, $comment->getAuthorEmail());
$this->assertEquals($commentSubject, $comment->getSubject());
$this->assertEquals($commentBody, $comment->getBody());
}
}

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

Мы хотим создать объект Factory, передать его массиву и попробовать создать для нас объект Comment:

require_once '../CommentFactory.php';
class CommentFactoryTest extends PHPUnit_Framework_TestCase {
function testACommentsHasAllItsComposingParts() {
$postId = 1;
$commentAuthor = "Joe";
$commentAuthorEmail = "Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.<script type="text/javascript">
/* <![CDATA[ */
(function(){try{var s,a,i,j,r,c,l,b=document.getElementsByTagName("script");l=b[b.length-1].previousSibling;a=l.getAttribute('data-cfemail');if(a){s='';r=parseInt(a.substr(0,2),16);for(j=2;a.length-j;j+=2){c=parseInt(a.substr(j,2),16)^r;s+=String.fromCharCode(c);}s=document.createTextNode(s);l.parentNode.replaceChild(s,l);}}catch(e){}})();
/* ]]> */
</script>";
$commentSubject = "Joe Has an Opinion about the Repository Pattern";
$commentBody = "I think it is a good idea to use the Repository Pattern to persist and retrieve objects.";
$commentData = array($postId, $commentAuthor, $commentAuthorEmail, $commentSubject, $commentBody);
$comment = (new CommentFactory())->make($commentData);
$this->assertEquals($postId, $comment->getPostId());
$this->assertEquals($commentAuthor, $comment->getAuthor());
$this->assertEquals($commentAuthorEmail, $comment->getAuthorEmail());
$this->assertEquals($commentSubject, $comment->getSubject());
$this->assertEquals($commentBody, $comment->getBody());
}
}

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

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

Данный тест слегка отличается от предыдущего, но не слишком. В нем мы попытаемся создать объект CommentFactory. Этого класса еще не существует.

Мы также попытаемся вызвать метод make() в нем вместе с массивом, в котором будут содержаться все данные о комментариях. Этот метод определяет интерфейс Фабрики:

interface Factory {
function make($data);
}

Это довольно распространенный интерфейс Фабрики. Он задает единственный необходимый для Фабрики метод — метод, который фактически создает нужные нам объекты:

require_once __DIR__ . '/Factory.php';
require_once __DIR__ . '/Comment.php';
class CommentFactory implements Factory {
function make($components) {
return new Comment($components[0], $components[1], $components[2], $components[3], $components[4]);
}
}

И CommentFactory успешно реализует интерфейс Factory. Применяя параметр $components для метода make(), он создает и извлекает новые объекты Comment со всей информаций.

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

Использование Хранилища для сохранения комментариев

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

Использование TDD — это (по большей части) проще, чем сперва запускать часть логики, предназначенной для записи, а затем использовать реализованный метод для проверки целостности данных:

require_once '../../../vendor/autoload.php';
require_once '../CommentRepository.php';
require_once '../CommentFactory.php';
class RepositoryTest extends PHPUnit_Framework_TestCase {
protected function tearDown() {
\Mockery::close();
}
function testItCallsThePersistenceWhenAddingAComment() {
$persistanceGateway = \Mockery::mock('Persistence');
$commentRepository = new CommentRepository($persistanceGateway);
$commentData = array(1, 'x', 'x', 'x', 'x');
$comment = (new CommentFactory())->make($commentData);
$persistanceGateway->shouldReceive('persist')->once()->with($commentData);
$commentRepository->add($comment);
}
}

Мы используем Эммуляцию, чтобы создать виртуальное хранилище, а затем извлечь из него виртуальный объект. Затем мы вызываем в хранилище метод add().

 
 

Данный метод имеет параметр Comment. Мы ожидаем, что из хранилища будет извлечен массив данных аналогичный $commentData:

require_once __DIR__ . '/InMemoryPersistence.php';
class CommentRepository {
private $persistence;
function __construct(Persistence $persistence = null) {
$this->persistence = $persistence ? : new InMemoryPersistence();
}
function add(Comment $comment) {
$this->persistence->persist(array(
$comment->getPostId(),
$comment->getAuthor(),
$comment->getAuthorEmail(),
$comment->getSubject(),
$comment->getBody()
));
}
}

Как вы можете видеть, метод Add () довольно интеллектуальный. Он инкапсулирует информацию о том, как преобразовать объект PHP в простой массив, совместимый с хранилищем. Помните, наш шлюз хранилища обычно является общим объектом для всех данных.

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

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

function testAPersistedCommentCanBeRetrievedFromTheGateway() {
$persistanceGateway = new InMemoryPersistence();
$commentRepository = new CommentRepository($persistanceGateway);
$commentData = array(1, 'x', 'x', 'x', 'x');
$comment = (new CommentFactory())->make($commentData);
$commentRepository->add($comment);
$this->assertEquals($commentData, $persistanceGateway->retrieve(0));
}

Конечно, если у вас нет встроенной реализации записи в хранилище, эмуляция является единственным разумным способом действий. Иначе ваш тест будет слабо применим из-за медленной скорости:

function testItCanAddMultipleCommentsAtOnce() {
$persistanceGateway = \Mockery::mock('Persistence');
$commentRepository = new CommentRepository($persistanceGateway);
$commentData1 = array(1, 'x', 'x', 'x', 'x');
$comment1 = (new CommentFactory())->make($commentData1);
$commentData2 = array(2, 'y', 'y', 'y', 'y');
$comment2 = (new CommentFactory())->make($commentData2);
$persistanceGateway->shouldReceive('persist')->once()->with($commentData1);
$persistanceGateway->shouldReceive('persist')->once()->with($commentData2);
$commentRepository->add(array($comment1, $comment2));
}

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

Шаблон Хранилище просто предусматривает то, что это обеспечит требования ваших пользователей и будет соответствовать нашей бизнес-логике.

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

function add($commentData) {
if (is_array($commentData))
foreach ($commentData as $comment)
$this->persistence->persist(array(
$comment->getPostId(),
$comment->getAuthor(),
$comment->getAuthorEmail(),
$comment->getSubject(),
$comment->getBody()
));
else
$this->persistence->persist(array(
$commentData->getPostId(),
$commentData->getAuthor(),
$commentData->getAuthorEmail(),
$commentData->getSubject(),
$commentData->getBody()
));
}

И самый простой способ проверить нашу модель — это просто протестировать, извлекаются ли параметры массивом или нет. Если они извлекаются массивом, мы сможем обработать каждый элемент и записать его через массив, который мы сгенерировали из одного объекта Comment.

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

function add($commentData) {
if (is_array($commentData))
foreach ($commentData as $comment)
$this->addOne($comment);
else
$this->addOne($commentData);
}
private function addOne(Comment $comment) {
$this->persistence->persist(array(
$comment->getPostId(),
$comment->getAuthor(),
$comment->getAuthorEmail(),
$comment->getSubject(),
$comment->getBody()
));
}

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

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

В результате этого наше Хранилище может иметь несколько реализаций: одну — через метод addOne(), вторую через — addMany ( ). Они обе являются вполне легитимными реализациями Шаблона проектирования Хранилища.

Вывод комментариев через Хранилище

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

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

function testItCanFindAllComments() {
$repository = new CommentRepository();
$commentData1 = array(1, 'x', 'x', 'x', 'x');
$comment1 = (new CommentFactory())->make($commentData1);
$commentData2 = array(2, 'y', 'y', 'y', 'y');
$comment2 = (new CommentFactory())->make($commentData2);
$repository->add($comment1);
$repository->add($comment2);
$this->assertEquals(array($comment1, $comment2), $repository->findAll());
}

Первый метод называется findAll(). Согласно нему должны получаться все объекты, за которые отвечает Хранилище. В нашем случае Comments. Его проверка осуществляется очень просто. Мы добавляем комментарий, затем еще один, а потом вызываем их через метод findAll() и получаем список, содержащий оба комментария.

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

function retrieveAll() {
return $this->data;
}

Вот и все. Мы просто добавили метод RetrieveAll (), который возвращает весь массив $data из класса. Просто и эффективно. Пора применить метод FindAll ( ) к CommentRepository:

function findAll() {
$allCommentsData = $this->persistence->retrieveAll();
$comments = array();
foreach ($allCommentsData as $commentData)
$comments[] = $this->commentFactory->make($commentData);
return $comments;
}

findAll() будет вызывать метод retrieveAll(). Этот метод создаст линейный массив данных. findAll() обработает каждый элемент и передаст необходимые данные Фабричному методу. Фабрика создаст один объект Comment. Будет создан массив с этим комментарием и через метод findAll() он будет выведен. Просто и эффективно.

Другой распространенный метод, который применяется для Хранилища — это поиск конкретного объекта или группы объектов по их характерным ключам. Например, все наши комментарии связаны с блогом через внутреннюю переменную $ PostId.

Я думаю, что согласно бизнес-логике нашего блога практически всегда нам нужно, чтобы все комментарии, связанные с конкретной записью, выводились на экран вместе с ней. Поэтому, по моему мнению, было бы вполне разумно для таких случаев применять метод findByPostId ( $ ID ):

function testItCanFindCommentsByBlogPostId() {
$repository = new CommentRepository();
$commentData1 = array(1, 'x', 'x', 'x', 'x');
$comment1 = (new CommentFactory())->make($commentData1);
$commentData2 = array(1, 'y', 'y', 'y', 'y');
$comment2 = (new CommentFactory())->make($commentData2);
$commentData3 = array(3, 'y', 'y', 'y', 'y');
$comment3 = (new CommentFactory())->make($commentData3);
$repository->add(array($comment1, $comment2));
$repository->add($comment3);
$this->assertEquals(array($comment1, $comment2), $repository->findByPostId(1));
}

Мы просто создаем три комментария. Первые два имеют одинаковое значение $ PostId — 1, третий имеет $ PostId = 3. Добавим все три записи в хранилище, после чего после запроса findByPostId () при $ PostId = 1 мы ожидаем, что будет выведен список из первых двух комментариев:

function findByPostId($postId) {
return array_filter($this->findAll(), function ($comment) use ($postId){
return $comment->getPostId() == $postId;
});
}

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

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

function testItCanFindCommentsByBlogPostId() {
$repository = new CommentRepository();
$commentData1 = array(1, 'x', 'x', 'x', 'x');
$comment1 = (new CommentFactory())->make($commentData1);
$commentData2 = array(1, 'y', 'y', 'y', 'y');
$comment2 = (new CommentFactory())->make($commentData2);
$commentData3 = array(3, 'y', 'y', 'y', 'y');
$comment3 = (new CommentFactory())->make($commentData3);
$repository->add(array($comment1, $comment2));
$repository->add($comment3);
$this->assertEquals(array($comment1, $comment2), $repository->findByPostId(1));
$this->assertEquals(array($comment3), $repository->findByPostId(3));
}

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

Эти простые дополнительные операторы или методы тестирования могут выявить скрытые проблемы. Как в нашем случае array_filter () не переиндексирует массив на выходе. И, так как у нас уже есть массив с корректными элементами, все индексы перепутались:

RepositoryTest::testItCanFindCommentsByBlogPostId
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
- 0 => Comment Object (…)
+ 2 => Comment Object (…)
)

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

На самом деле проблема с логикой была в CommentRepository.

function findByPostId($postId) {
return array_values(
array_filter($this->findAll(), function ($comment) use ($postId) {
return $comment->getPostId() == $postId;
})
);
}

Да, вот так вот просто. Мы просто перед выводом пропускаем результат через array_values(). И тогда наш массив красиво индексируется. Миссия выполнена.

Несколько слов напоследок

Вся большая миссия по построению нашего Хранилища (Репозитария) также успешно завершена. У нас есть класс, который может быть использован любым другим классом бизнес-логики. С его помощью можно довольно просто сохраняться и извлекать объекты.

Он также отделяет бизнес-логику от Шлюзов Фабрики и сохраняемых данных. С его помощью можно существенно уменьшить количество дублирований и значительно упростить операции по сохранению и выводу наших комментариев.

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

Хранилища разнятся между собой, в зависимости от типа объектов, с которыми они работают, нет какого-то общего шаблона.

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

И напоследок хочу отметить, что Хранилище может иметь свой собственный список объектов, а также может осуществлять локальное кэширование объектов. Если объект не может быть найден в локальном списке, мы извлекаем его из репозитария — в противном случае мы можем найти его в нашем списке.

Если используется кэширование, Хранилище можно успешно сочетать с Одиночным шаблоном проектирования. Как обычно, спасибо за ваше время. Я искренне надеюсь, что вы узнали сегодня что-то новое.

Перевод статьи «The Repository Design Pattern» был подготовлен дружной командой проекта Сайтостроение от А до Я.

Подтверждение адреса электронной почты на PHP

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

Ниже приведен базовый урок, объясняющий, как создать базу данных и надлежащий код активации. Реализация выполнена с помощью методов mysqli_(), так как методы mysql_() уже потеряли свои позиции.

Скачать скрипт

Демо

База данных

Примерная таблица базы данных users содержит пять столбцов: идентификатор пользователя (uid), адрес электронной почты (email), пароль (password), активация (activation) и статус (status):

CREATE TABLE IF NOT EXISTS `users` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(300) NOT NULL UNIQUE,
`password` varchar(300) NOT NULL,
`activation` varchar(300) NOT NULL UNIQUE,
`status` enum('0','1') NOT NULL DEFAULT '0',
PRIMARY KEY (`uid`)
)

HTML код

Содержит простой HTML код:

<form action="" method="post">
<label>Email</label>
<input type="text" name="email" class="input" autocomplete="off"/>
<label>Password </label>
<input type="password" name="password" class="input" autocomplete="off"/><br/>
<input type="submit" class="button" value="Registration" />
<span class='msg'><?php echo $msg; ?></span>
</form>

db.php

Файл конфигурации базы данных, измените имя пользователя, пароль, базу данных и основные значения URL:

 
 
<?php
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
define('DB_DATABASE', 'database');
$connection = @mysqli_connect(DB_SERVER,DB_USERNAME,DB_PASSWORD,DB_DATABASE);
$base_url='http://www.youwebsite.com/email_activation/';
?>

index.php

Содержит PHP код, который сохраняет данные регистрации пользователя в таблицу users.

Здесь же находится генерация кода активации с помощью MD5 шифрования:

<?php
include 'db.php';
$msg='';
if(!empty($_POST['email']) && isset($_POST['email']) && !empty($_POST['password']) && isset($_POST['password']) )
{
// имя пользователя и пароль отправлены из формы
$email=mysql_real_escape_string($_POST['email']);
$password=mysql_real_escape_string($_POST['password']);
// регулярное выражение для проверки написания адреса электронной почты
$regex = '/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/';
if(preg_match($regex, $email))
{
$password=md5($password); // encrypted password
$activation=md5($email.time()); // encrypted email+timestamp
$count=mysqli_query($connection,"SELECT uid FROM users WHERE email='$email'");
// проверка адреса электронной почты
if(mysqli_num_rows($count) < 1)
{
mysqli_query($connection,"INSERT INTO users(email,password,activation) VALUES('$email','$password','$activation')");
// отправка письма на электронный ящик
include 'smtp/Send_Mail.php';
$to=$email;
$subject="Подтверждение электронной почты";
$body='Здравствуйте! <br/> <br/> Мы должны убедиться в том, что вы человек. Пожалуйста, подтвердите адрес вашей электронной почты, и можете начать использовать ваш аккаунт на сайте. <br/> <br/> <a href="'.$base_url.'activation/'.$activation.'">'.$base_url.'activation/'.$activation.'</a>';
Send_Mail($to,$subject,$body);
$msg= "Регистрация выполнена успешно, пожалуйста, проверьте электронную почту.";
}
else
{
$msg= 'Данный адрес электронный почты уже занят, пожалуйста, введите другой. ';
}
}
else
{
$msg = 'Адрес, введенный вами, неверен. Пожалуйста, попробуйте еще раз.';
}
}
// HTML часть
?>

Send_Mail.php

В этом файле находится функция отправки сообщения, измените только SMTP хост, имя пользователя и пароль.

 
 

Здесь вы можете использовать элементы GMail SMTP для тестирования:

<?php
function Send_Mail($to,$subject,$body)
{
require 'class.phpmailer.php';
$from = "Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.";
$mail = new PHPMailer();
$mail->IsSMTP(true); // используем протокол SMTP
$mail->IsHTML(true);
$mail->SMTPAuth = true; // разрешить SMTP аутентификацию 
$mail->Host = "tls://smtp.yourwebsite.com"; // SMTP хост
$mail->Port = 465; // устанавливаем SMTP порт
$mail->Username = "SMTP_Username"; //имя пользователя SMTP 
$mail->Password = "SMTP_Password"; // SMTP пароль
$mail->SetFrom($from, 'From Name');
$mail->AddReplyTo($from,'From Name');
$mail->Subject = $subject;
$mail->MsgHTML($body);
$address = $to;
$mail->AddAddress($address, $to);
$mail->Send();
}
?>

activation.php

Содержит PHP код, в котором статус пользователя, основанный на коде активации, обновляется со значения 0 до 1.

<?php
include 'db.php';
$msg='';
if(!empty($_GET['code']) && isset($_GET['code']))
{
$code=mysql_real_escape_string($_GET['code']);
$c=mysqli_query($connection,"SELECT uid FROM users WHERE activation='$code'");
if(mysqli_num_rows($c) > 0)
{
$count=mysqli_query($connection,"SELECT uid FROM users WHERE activation='$code' and status='0'");
if(mysqli_num_rows($count) == 1)
{
mysqli_query($connection,"UPDATE users SET status='1' WHERE activation='$code'");
$msg="Ваш аккаунт активирован";
}
else
{
$msg ="Ваш аккаунт уже активирован, нет необходимости активировать его снова.";
}
}
else
{
$msg ="Неверный код активации.";
}
}
?>
//HTML часть
<?php echo $msg; ?>

Подтверждение адреса электронной почты

.htaccess

Скрипт перенаправления URL, который превращает

http://website.com/activation.php?code=ACTIVATION_CODE

в

http://website.com/activation/ACTIVATION_CODE

RewriteEngine On
RewriteRule ^activation/([a-zA-Z0-9_-]+)$ activation.php?code=$1
RewriteRule ^activation/([a-zA-Z0-9_-]+)/$ activation.php?code=$1

CSS код

body
{
font-family: "Helvetica",Arial,sans-serif;
font-weight: 500;
color:#333;
}
label
{
width:100px;
display:block;
font-weight:bold;
color:#666666;
}
#main
{
margin:0 auto;
width:800px;
}
.input
{
padding:10px;
font-size:14px;
border:1px solid #999999;
width:200px;
margin-bottom:10px;
}
.button {
padding:10px;
background-color: #5fcf80 !important;
border-color: #3ac162 !important;
}
.msg
{
font-size:11px;
color:#666;
padding:10px;
}

Перевод статьи «PHP Email Verification Script» был подготовлен дружной командой проекта Сайтостроение от А до Я.

Календарь на AJAX и PHP

Календарь является неотъемлемой частью дизайна многих сайтах. Часто он представляет собой один из многочисленных плагинов JQuery для календаря. Но он также может быть реализован с помощью PHP.

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

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

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

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

Демонстрация

Скачать источник

Структура папок

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

  • CSS — для всех файлов CSS;
  • изображения — для всех возможных изображений;
  • шаблоны — для всех файлов шаблонов.

Шаг 1. HTML

Я не собираюсь использовать какую-либо конкретную системы шаблонов (например, Smarty), я буду использовать только простые шаблоны HTML с собственными ключами.

Шаблоны / index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>PHP AJAX Calendar</title>
<!-- add styles and scripts -->
<link href="css/styles.css" rel="stylesheet" type="text/css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
</head>
<body>
<div id="calendar">
__calendar__
</div>
</body>
</html>

Это очень простой шаблон для нашей индексной страницы, в нем мы задали базовый контейнер календаря.

 
 

Второй шаблон будет использоваться в качестве внутреннего контейнера:

Шаблоны / calendar.html:

<div class="navigation">
<a class="prev" href="index.php?month=__prev_month__" onclick="$('#calendar').load('index.php?month=__prev_month__&_r=' + Math.random()); return false;"></a>
<div class="title" >__cal_caption__</div>
<a class="next" href="index.php?month=__next_month__" onclick="$('#calendar').load('index.php?month=__next_month__&_r=' + Math.random()); return false;"></a>
</div>
<table>
<tr>
<th class="weekday">sun</th>
<th class="weekday">mon</th>
<th class="weekday">tue</th>
<th class="weekday">wed</th>
<th class="weekday">thu</th>
<th class="weekday">fri</th>
<th class="weekday">sat</th>
</tr>
__cal_rows__
</table>

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

Шаг 2 . PHP

Пора действовать.

index.php

// Устанавливаем текущий год, месяц и день
list($iNowYear, $iNowMonth, $iNowDay) = explode('-', date('Y-m-d'));
// Устанавливаем текущий год, месяц в зависимости от возможных параметров GET 
if (isset($_GET['month'])) {
list($iMonth, $iYear) = explode('-', $_GET['month']);
$iMonth = (int)$iMonth;
$iYear = (int)$iYear;
} else {
list($iMonth, $iYear) = explode('-', date('n-Y'));
}
// Получаем названия и количество дней в конкретном месяце
$iTimestamp = mktime(0, 0, 0, $iMonth, $iNowDay, $iYear);
list($sMonthName, $iDaysInMonth) = explode('-', date('F-t', $iTimestamp));
// Получаем предыдущий год и месяц 
$iPrevYear = $iYear;
$iPrevMonth = $iMonth — 1;
if ($iPrevMonth <= 0) {
$iPrevYear--;
$iPrevMonth = 12; // set to December
}
// Получаем следующий год и месяц
$iNextYear = $iYear;
$iNextMonth = $iMonth + 1;
if ($iNextMonth > 12) {
$iNextYear++;
$iNextMonth = 1;
}
// Получаем количество дней в предыдущем месяце
$iPrevDaysInMonth = (int)date('t', mktime(0, 0, 0, $iPrevMonth, $iNowDay, $iPrevYear));
// Получаем числовое представление дней недели от первого дня конкретного (текущего) месяца.
$iFirstDayDow = (int)date('w', mktime(0, 0, 0, $iMonth, 1, $iYear));
// С этого дня начинается предыдущий месяц 
$iPrevShowFrom = $iPrevDaysInMonth — $iFirstDayDow + 1;
// Если предыдущий месяц
$bPreviousMonth = ($iFirstDayDow > 0);
// Тогда первый день
$iCurrentDay = ($bPreviousMonth) ? $iPrevShowFrom : 1;
$bNextMonth = false;
$sCalTblRows = '';
// Генерируем строки календаря
for ($i = 0; $i < 6; $i++) { // 6-weeks range
$sCalTblRows .= '<tr>';
for ($j = 0; $j < 7; $j++) { // 7 days a week
$sClass = '';
if ($iNowYear == $iYear && $iNowMonth == $iMonth && $iNowDay == $iCurrentDay && !$bPreviousMonth && !$bNextMonth) {
$sClass = 'today';
} elseif (!$bPreviousMonth && !$bNextMonth) {
$sClass = 'current';
}
$sCalTblRows .= '<td class="'.$sClass.'"><a href="javascript: void(0)">'.$iCurrentDay.'</a></td>';
// Следующий день
$iCurrentDay++;
if ($bPreviousMonth && $iCurrentDay > $iPrevDaysInMonth) {
$bPreviousMonth = false;
$iCurrentDay = 1;
}
if (!$bPreviousMonth && !$bNextMonth && $iCurrentDay > $iDaysInMonth) {
$bNextMonth = true;
$iCurrentDay = 1;
}
}
$sCalTblRows .= '</tr>';
}
// Готовим замену ключей и генерируем календарь
$aKeys = array(
'__prev_month__' => "{$iPrevMonth}-{$iPrevYear}",
'__next_month__' => "{$iNextMonth}-{$iNextYear}",
'__cal_caption__' => $sMonthName . ', ' . $iYear,
'__cal_rows__' => $sCalTblRows,
);
$sCalendarItself = strtr(file_get_contents('templates/calendar.html'), $aKeys);
// AJAX-запрос — выводит календарь
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] =='XMLHttpRequest' && isset($_GET['month'])) {
header('Content-Type: text/html; charset=utf-8');
echo $sCalendarItself;
exit;
}
$aVariables = array(
'__calendar__' => $sCalendarItself,
);
echo strtr(file_get_contents('templates/index.html'), $aVariables);

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

 
 

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

Через запрос Ajax мы выводим только внутреннее содержание ($sCalendarItself), в противном случае — мы показываем всю страницу.

Шаг 3. CSS

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

CSS / styles.css

/* стили календаря */
#calendar {
-moz-user-select: none;
border: 1px solid #EEEEEE;
border-radius: 6px 6px 6px 6px;
color: #333333;
font-family: Arial,sans-serif;
font-size: 1.1em;
margin: 10px auto;
padding: 0.4em;
width: 90%;
}
#calendar .navigation {
background-color: #CC0000;
border: 1px solid #E3A1A1;
border-radius: 6px 6px 6px 6px;
color: #FFFFFF;
font-weight: bold;
padding: 1px;
position: relative;
}
#calendar .navigation .title {
background: none repeat scroll 0 0 transparent;
border-color: rgba(0, 0, 0, 0);
color: inherit;
line-height: 1.8em;
margin: 0 2.3em;
text-align: center;
}
#calendar .navigation .prev, #calendar .navigation .next {
background-image: url(../images/nav.png);
height: 24px;
opacity: 0.9;
position: absolute;
top: 4px;
width: 24px;
}
#calendar .navigation .prev {
background-position: 0 0;
left: 4px;
}
#calendar .navigation .next {
background-position: -24px 0;
right: 4px;
}
#calendar .navigation .prev:hover, #calendar .navigation .next:hover {
opacity: 1;
}
#calendar table {
border-collapse: collapse;
font-size: 0.9em;
table-layout: fixed;
width: 100%;
}
#calendar table th {
border: 0 none;
font-weight: bold;
padding: 0.7em 0.3em;
text-align: center;
}
#calendar table td {
border: 0 none;
padding: 1px;
}
#calendar table td a {
background-color: #EEEEEE;
border: 1px solid #D8DCDF;
color: #004276;
display: block;
font-weight: normal;
opacity: 0.7;
padding: 0.2em;
text-align: right;
text-decoration: none;
}
#calendar table td a:hover {
background-color: #F6F6F6;
border: 1px solid #CDD5DA;
color: #111111;
}
#calendar table td.current a {
font-weight: bold;
opacity: 1;
}
#calendar table td.today a {
background-color: #FBF8EE;
border: 1px solid #FCD3A1;
color: #444444;
font-weight: bold;
opacity: 1;
}

Шаг 4. Изображения

В стилях нашего календаря используется только одно небольшое изображение: nav.png

Демонстрация

Скачать источник

Заключение

Вот и все на сегодня, мы только что создали стильный адаптивный календарь.

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

Перевод статьи «PHP AJAX Calendar» был подготовлен дружной командой проекта Сайтостроение от А до Я.

Полезные PHP сниппеты

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

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

Расчет расстояния между двумя точками по координатам

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

Есть две функции с соответственными названиями:

function haversineGreatCircleDistance($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000) {
//конвертируем градусы в радианы
$latFrom = deg2rad($latitudeFrom);
$lonFrom = deg2rad($longitudeFrom);
$latTo = deg2rad($latitudeTo);
$lonTo = deg2rad($longitudeTo);
$latDelta = $latTo — $latFrom;
$lonDelta = $lonTo — $lonFrom;
$angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));
return $angle * $earthRadius;
}
public static function vincentyGreatCircleDistance($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000) {
// конвертируем градусы в радианы
$latFrom = deg2rad($latitudeFrom);
$lonFrom = deg2rad($longitudeFrom);
$latTo = deg2rad($latitudeTo);
$lonTo = deg2rad($longitudeTo);
$lonDelta = $lonTo — $lonFrom;
$a = pow(cos($latTo) * sin($lonDelta), 2) +
pow(cos($latFrom) * sin($latTo) — sin($latFrom) * cos($latTo) * cos($lonDelta), 2);
$b = sin($latFrom) * sin($latTo) + cos($latFrom) * cos($latTo) * cos($lonDelta);
$angle = atan2(sqrt($a), $b);
return $angle * $earthRadius;
}

Обе функции используют следующие параметры:

 
 

float $latitudeFrom — широта начальной точки (в градусах и в виде десятичной дроби);
float $longitudeFrom — долгота начальной точки (в градусах и в виде десятичной дроби);
float $latitudeTo — широта конечной точки (в градусах и в виде десятичной дроби);
float $longitudeTo — долгота конечной точки (в градусах и в виде десятичной дроби);
float $earthRadius — среднее значение радиуса Земли в милях.

Функции возвращают значение типа float, являющееся расстоянием между точками в милях (также как earthRadius).

Оповещение по email об ошибках PHP кода

function errorHandler($sMessage = '', $aVars = array()) {
$sScript = $_SERVER['PHP_SELF'];
$sParams = print_r($_REQUEST, true);
$sVars = print_r($aVars, true);
$aBackTrace = debug_backtrace();
unset($aBackTrace[0]);
$sBackTrace = print_r($aBackTrace, true);
$sExplanation = <<<EOF
<p>Дополнительная информация: {$sMessage}</p>
<p>Дополнительные переменные: <pre>{$sVars}</pre></p><hr />
<p>Вызванный скрипт: {$sScript}</p>
<p>Параметры запроса: <pre>{$sParams}</pre></p><hr />
<p>Трассировка отладки:</p><pre>{$sBackTrace}</pre>
EOF;
$sHeader = "Subject: Error occurred\r\nContent-type: text/html; charset=UTF-8\r\n";
error_log($sExplanation, 1, 'Этот адрес электронной почты защищен от спам-ботов. У вас должен быть включен JavaScript для просмотра.', $sHeader);
}

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

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

string $sMessage — пользовательское сообщение;
array $aVars — дополнительный массив, который будет отправлен по email.

Конвертирование PDF в JPG

function pdfToJpg($pdf, $jpg) {
$im = new Imagick();
$im->setResolution(300,300);
$im->readimage($pdf);
$im->setImageFormat('jpeg');
$im->writeImage($jpg);
$im->clear();
$im->destroy();
}

Эта функция нужна, чтобы конвертировать PDF файлы в изображение. Она принимает два параметра:

 
 

string $pdf — путь к исходному PDF файлу;
string $jpg — путь к файлу изображения.

Получение возраста по дате рождения

function getAge($birthdate = '0000-00-00') {
if ($birthdate == '0000-00-00') return 'Unknown';
$bits = explode('-', $birthdate);
$age = date('Y') — $bits[0] — 1;
$arr[1] = 'm';
$arr[2] = 'd';
for ($i = 1; $arr[$i]; $i++) {
$n = date($arr[$i]);
if ($n < $bits[$i])
break;
if ($n > $bits[$i]) {
++$age;
break;
}
}
return $age;
}

Эта функция нужна для того, чтобы узнать возраст по дню рождения (заданному в формате ГГГГ-ММ-ДД).

Извлечение файлов из архива ZIP

function unzipArchive($file, $destinationFolder){
// создаем объект ZipArchive 
$zip = new ZipArchive() ;
// открываем архив
if ($zip->open($file) !== TRUE) {
die ('Could not open archive');
}
// извлекаем содержимое в папку назначения 
$zip->extractTo($destinationFolder);
// закрываем архив
$zip->close();
}

Эта функция принимает два параметра:

string $file — путь к исходному ZIP файлу;
string $destinationFolder — путь к папке назначения для файлов

Заключение

На сегодня — все. Спасибо за ваше внимание, не забывайте навещать нас время от времени.

Перевод статьи «Useful PHP Snippets» был подготовлен дружной командой проекта Сайтостроение от А до Я.