<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>SimpleCoding.org</title>
	<atom:link href="https://www.simplecoding.org/feed" rel="self" type="application/rss+xml" />
	<link>https://www.simplecoding.org</link>
	<description>Блог о программировании</description>
	<lastBuildDate>Mon, 01 May 2017 14:56:51 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>dbForge Studio &#8212; менеджер MySQL баз данных</title>
		<link>https://www.simplecoding.org/dbforge-studio-menedzher-mysql-baz-dannyx.html</link>
					<comments>https://www.simplecoding.org/dbforge-studio-menedzher-mysql-baz-dannyx.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Wed, 15 Oct 2014 18:17:05 +0000</pubDate>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Web разработка]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1511</guid>

					<description><![CDATA[<p>Некоторое время назад я начал пользоваться dbForge Studio for MySQL, программа оказалась очень неплохой, с бесплатной версией для некоммерческого использования и интересными возможностями, о которых я и хочу рассказать. Кроме того, разработчики пошли на встречу и согласились сделать небольшой подарок для всех читателей этого блога, но о нём в конце статьи. На сегодняшний день существует...  <a href="https://www.simplecoding.org/dbforge-studio-menedzher-mysql-baz-dannyx.html" title="Read dbForge Studio &#8212; менеджер MySQL баз данных">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/dbforge-studio-menedzher-mysql-baz-dannyx.html">dbForge Studio — менеджер MySQL баз данных</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Некоторое время назад я начал пользоваться <a href="http://www.devart.com/ru/dbforge/mysql/studio/">dbForge Studio for MySQL</a>, программа оказалась очень неплохой, с бесплатной версией для некоммерческого использования и интересными возможностями, о которых я и хочу рассказать. Кроме того, разработчики пошли на встречу и согласились сделать небольшой подарок для всех читателей этого блога, но о нём в конце статьи.</p>
<p>На сегодняшний день существует множество <strong>инструментов для работы с MySQL базами данных</strong>. Основные возможности, такие как просмотр и редактирование данных, реализованы во всех инструментах, но дополнительные функции отличаются довольно сильно и очень часто именно они экономят наше время.</p>
<p>Условно все программы такого типа можно разделить на два класса: с web интерфейсом и без него (десктопные).<br />
<span id="more-1511"></span></p>
<p>Наиболее известным представителем первого класса является <a href="http://www.phpmyadmin.net/home_page/index.php">phpMyAdmin</a>. Так или иначе, с ним сталкивались практически все web разработчики. Дело в том, что web приложения для администрирования баз (и не только) очень популярны на shared хостингах, т.к. для работы с базой не нужен SSH доступ и довольно легко устанавливать различные ограничения. С точки зрения пользователей тоже есть положительные моменты:</p>
<ul>
<li>не нужно ничего устанавливать и настраивать на своём компьютере;</li>
<li>получить доступ к базе можно с любого устройства, подключённого к интернет;</li>
<li>в phpMyAdmin реализованы практически все необходимые возможности (просмотр и редактирование данных, поиск, выполнение SQL запросов, экспорт, импорт и т.д.).</li>
</ul>
<p>Но, как только вы переезжаете на <strong>VPS или выделенный сервер</strong>, ситуация меняется. И проблемы в использовании web приложений становятся заметнее:</p>
<ol>
<li>Ограничения времени выполнения и ресурсов скриптов создают проблемы при экспорте/импорте данных. Т.е. для экспорта/импорта больших баз придётся использовать другие инструменты.</li>
<li>Т.к. приложение доступно из интернета, нужно следить за его безопасностью. В случае VPS или выделенного сервера это полностью ваша проблема, а не хостера.</li>
<li>Не получится работать одновременно с несколькими базами данных, расположенными на разных серверах.</li>
</ol>
<p>Поэтому в таких случаях использовать десктопные приложения гораздо удобнее.</p>
<h2>Подключение к базе данных</h2>
<p>Тут очень важно, чтобы была возможность подключиться через SSH. Если её нет, то должны быть другие, очень веские причины для использования такого MySQL менеджера. Конечно, вы можете открыть доступ к MySQL серверу снаружи, но при этом придётся обеспечивать его безопасность (например, создавать списки IP адресов, с которых можно к нему подключаться), а это дополнительная работа.</p>
<p>В dbForge подключение через SSH реализовано, нужные настройки находятся на вкладке «Безопасность».</p>
<div id="attachment_1512" class="wp-caption alignnone"><img fetchpriority="high" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/1_connection.png" alt="connection" width="463" height="532" class="size-full wp-image-1512" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/1_connection.png 463w, https://www.simplecoding.org/wp-content/uploads/2014/10/1_connection-450x517.png 450w" sizes="(max-width: 463px) 100vw, 463px" /><p class="wp-caption-text">&nbsp;</p></div>
<div id="attachment_1513" class="wp-caption alignnone"><img decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/2_connection_ssh.png" alt="connection ssh" width="463" height="532" class="size-full wp-image-1513" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/2_connection_ssh.png 463w, https://www.simplecoding.org/wp-content/uploads/2014/10/2_connection_ssh-450x517.png 450w" sizes="(max-width: 463px) 100vw, 463px" /><p class="wp-caption-text">&nbsp;</p></div>
<p><em>Примечание</em>. Скриншоты для этой статьи я сделал с локального сервера, установленного в VirtualBox, поэтому использована аутентификация с помощью пароля. Для рабочих серверов лучше использовать ключи и запретить вход под root’ом.</p>
<h2>Основные возможности</h2>
<p>Под ними я понимаю: <em>просмотр</em>, <em>изменение</em>, <em>удаление</em> и <em>добавление</em> данных, а также <em>поиск</em>. Т.к. CRUD операции реализованы практически одинаково во всех подобных программах, я просто покажу скриншот из dbForge. Вы выбираете нужную таблицу, и программа покажет информацию о таблице и первые 1000 строк данных. Естественно, вы можете использовать постраничную навигацию, редактировать данные, структуру таблицы, создавать индексы и т.п.</p>
<div id="attachment_1514" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/3_table_data.png"><img decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/3_table_data-450x327.png" alt="table data" width="450" height="327" class="size-medium wp-image-1514" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/3_table_data-450x327.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/3_table_data-700x509.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/3_table_data.png 1069w" sizes="(max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p>Для создания правил фильтрации данных предусмотрен «Конструктор фильтра». Открывается из меню <code>«Данные» -> «Фильтр» -> «Условие...»</code>. Конструктор сделан достаточно удобно, есть календарь для ввода дат, выпадающие списки с перечнем полей таблицы и условиями сравнения.</p>
<div id="attachment_1515" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/4_filter.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/4_filter-450x327.png" alt="filter" width="450" height="327" class="size-medium wp-image-1515" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/4_filter-450x327.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/4_filter-700x509.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/4_filter.png 1069w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p>В целом, интерфейс достаточно приятный. Но если вы раньше пользовались другими MySQL клиентами, то нужно будет привыкнуть.</p>
<p>Переходим к более «продвинутым» возможностям.</p>
<h2>Создание запросов</h2>
<p>В dbForge Studio реализовано два режима создания запросов:</p>
<ol>
<li>редактор SQL;</li>
<li>дизайнер запросов.</li>
</ol>
<p>Для того чтобы создавать запросы, вы должны хотя бы на базовом уровне знать SQL. В противном случае никакие инструменты вам не помогут. Инструменты могут только сделать работу комфортнее. Например, выделить цветом ключевые слова, добавить отступы и алиасы для таблиц, показать варианты автодополнения. Кстати, автодополнение сделали в dbForge очень удобно (см. скриншот), они сгруппировали списки полей по таблицам.</p>
<div id="attachment_1516" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/5_sql_editor.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/5_sql_editor-450x327.png" alt="sql editor" width="450" height="327" class="size-medium wp-image-1516" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/5_sql_editor-450x327.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/5_sql_editor-700x509.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/5_sql_editor.png 1069w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p>Дизайнер запросов на первый взгляд очень напоминает аналогичный инструмент в Access. Но всё-таки он ближе к SQL режиму, чем реализация в Access. Это хорошо видно по представлению информации на вкладке «Соединения» (я специально показал её на скриншоте). Формат, в котором представлена связь, практически совпадает с записью в SQL режиме.</p>
<div id="attachment_1518" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/6_sql_constructor.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/6_sql_constructor-450x327.png" alt="sql constructor" width="450" height="327" class="size-medium wp-image-1518" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/6_sql_constructor-450x327.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/6_sql_constructor-700x509.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/6_sql_constructor.png 1069w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p>Также есть возможность переключиться в SQL режим и обратно.</p>
<h2>Профилирование запросов</h2>
<p>Ещё одна удобная функция. Вы указываете запрос, а программа вам показывает вывод</p>
<pre class="brush: sql; gutter: true; first-line: 1; highlight: []; html-script: false">EXPLAIN ...request...
SHOW PROFILE FOR QUERY ...</pre>
<p>и</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">SHOW SESSION STATUS;</pre>
<p>Всю эту информацию можно получить с помощью обычных SQL запросов, но dbForge позволяет выбрать несколько результатов и сравнить их. Данные выводятся в соседних колонках (показано на скриншоте), которые можно отсортировать по возрастанию или убыванию.</p>
<div id="attachment_1517" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/6_1_profiling.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/6_1_profiling-450x277.png" alt="profiling" width="450" height="277" class="size-medium wp-image-1517" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/6_1_profiling-450x277.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/6_1_profiling-700x432.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/6_1_profiling.png 836w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<h2>Создание диаграмм</h2>
<p>Этот режим полезен для разработки структуры MySQL баз. Вы можете либо добавлять новые таблицы на диаграмму, создавать поля и устанавливать связи, либо сформировать диаграмму из существующих таблиц. Последний вариант удобно использовать для подготовки документации и статей для блога <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<div id="attachment_1519" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/7_diagramm.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/7_diagramm-450x423.png" alt="diagramm" width="450" height="423" class="size-medium wp-image-1519" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/7_diagramm-450x423.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/7_diagramm.png 639w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p><strong>Обратите внимание</strong>. При создании связей между таблицами на диаграмме у вас есть возможность получить запрос, который эту связь создаёт. Для этого нажмите кнопку «Скрипт изменений» (показана на скриншоте). </p>
<div id="attachment_1520" class="wp-caption alignnone"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/8_creating_foreign_keys.png" alt="creating foreign keys" width="460" height="492" class="size-full wp-image-1520" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/8_creating_foreign_keys.png 460w, https://www.simplecoding.org/wp-content/uploads/2014/10/8_creating_foreign_keys-450x481.png 450w" sizes="auto, (max-width: 460px) 100vw, 460px" /><p class="wp-caption-text">&nbsp;</p></div>
<p>Для данного примера вы получите следующий скрипт:</p>
<pre class="brush: sql; gutter: true; first-line: 1; highlight: []; html-script: false">USE workshop;

--
-- Изменить таблицу &quot;table2&quot;
--
ALTER TABLE table2
  ADD CONSTRAINT FK_table2_table1_id FOREIGN KEY (t_id)
    REFERENCES table1(id) ON DELETE NO ACTION ON UPDATE NO ACTION;</pre>
<h2>Сравнение баз данных</h2>
<p>В идеальном мире эта функция не нужна, т.к. структуры баз данных у всех разработчиков и в продакшене должны совпадать. Многие современные фреймворки поддерживают миграции, предназначенные для удобного обновления структуры баз. Но в реальной жизни получается по-разному <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> поэтому возможность быстро сравнить две базы совсем не лишняя и может сэкономить много времени.</p>
<p>Результаты сравнения в <strong>dbForge Studio</strong> выглядят следующим образом</p>
<div id="attachment_1521" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/9_schema_comparison.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/9_schema_comparison-450x231.png" alt="schema comparison" width="450" height="231" class="size-medium wp-image-1521" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/9_schema_comparison-450x231.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/9_schema_comparison-700x359.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/9_schema_comparison.png 790w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p>Как видите, в данном случае в БД, которая расположена справа, отсутствует таблица <code>wp_td_terms</code> и используется движок <code>MYISAM</code> вместо <code>INNODB</code>.</p>
<p>Также можно сравнить и содержимое баз.</p>
<h2>Резервное копирование и восстановление данных</h2>
<p>Ради эксперимента я создал копию базы размером около 800 МБ. Процесс прошел довольно быстро и без ошибок. При создании копии можно указать множество настроек, например, заблокировать таблицы и т.п.</p>
<div id="attachment_1522" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/10_backup.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/10_backup-450x356.png" alt="backup" width="450" height="356" class="size-medium wp-image-1522" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/10_backup-450x356.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/10_backup-700x553.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/10_backup.png 747w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p>При восстановлении нужно только правильно указать кодировку дампа.</p>
<p>Также есть возможность экспорта данных в разные форматы, вроде CSV и XML.</p>
<h2>Создание отчётов</h2>
<p>Это ещё одна возможность, которая сильно напомнила Access. Отчеты создаются с помощью мастера, который предлагает либо использовать существующие таблицы, либо создать запрос. Также предлагается выбрать вариант оформления, группировку и сортировку. После завершения работы мастера открывается дизайнер отчета в котором можно сделать окончательные настройки.</p>
<div id="attachment_1523" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/11_report_designer.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/11_report_designer-450x187.png" alt="report designer" width="450" height="187" class="size-medium wp-image-1523" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/11_report_designer-450x187.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/11_report_designer-700x291.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/11_report_designer.png 813w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<p>Результат можно распечатать или экспортировать в один из поддерживаемых форматов (HTML, XLS, JPEG, PDF).</p>
<div id="attachment_1524" class="wp-caption alignnone"><a href="https://www.simplecoding.org/wp-content/uploads/2014/10/12_report_final.png"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/12_report_final-450x187.png" alt="report final" width="450" height="187" class="size-medium wp-image-1524" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/12_report_final-450x187.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/12_report_final-700x291.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/12_report_final.png 813w" sizes="auto, (max-width: 450px) 100vw, 450px" /></a><p class="wp-caption-text">&nbsp;</p></div>
<h2>Заключение</h2>
<p>В этом обзоре я останавливался в основном на возможностях, которые показались мне наиболее интересными, и доступны в версии для некоммерческого использования. А вообще dbForge распространяется в трёх редакциях: Express, Standard и Professional, которые отличаются ценой и набором поддерживаемых функций. Сравнить их можно <a href="http://www.devart.com/ru/dbforge/mysql/studio/editions.html">здесь</a>. При этом использование любой из версий упрощает MySQL разработку.</p>
<h2>Подарок</h2>
<p>Специально для вас компания <a href="http://www.devart.com/ru/">Devart</a> предоставлет 20% скидку на продукты линейки MySQL версий <a href="http://www.devart.com/ru/dbforge/mysql/">Standart и Professional</a>.<br />
Для того, чтобы ей воспользоваться, при заказе товара введите промо код</p>
<p><code>simplecoding</code></p>
<p>Промо код действителен до 20.11.2014.</p>
<p><strong>Успехов!</strong></p><p>The post <a href="https://www.simplecoding.org/dbforge-studio-menedzher-mysql-baz-dannyx.html">dbForge Studio — менеджер MySQL баз данных</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/dbforge-studio-menedzher-mysql-baz-dannyx.html/feed</wfw:commentRss>
			<slash:comments>887</slash:comments>
		
		
			</item>
		<item>
		<title>Logentries, Debian и логи Nginx</title>
		<link>https://www.simplecoding.org/logentries-debian-i-logi-nginx.html</link>
					<comments>https://www.simplecoding.org/logentries-debian-i-logi-nginx.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Tue, 07 Oct 2014 17:57:35 +0000</pubDate>
				<category><![CDATA[Hosting]]></category>
		<category><![CDATA[Web разработка]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1503</guid>

					<description><![CDATA[<p>В этой заметке речь пойдёт об использовании сервиса для анализа логов под названием Logentries. Мы разберём основные способы отправки данных в Logentries и подробно рассмотрим настройку сервера с Debian 7 и связкой Nginx + PHP-FPM. Но, прежде всего, несколько замечаний о самом Logentries. На сегодняшний день это далеко не единственный сервис анализа логов, но он...  <a href="https://www.simplecoding.org/logentries-debian-i-logi-nginx.html" title="Read Logentries, Debian и логи Nginx">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/logentries-debian-i-logi-nginx.html">Logentries, Debian и логи Nginx</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>В этой заметке речь пойдёт об использовании сервиса для анализа логов под названием <a href="https://logentries.com/">Logentries</a>. Мы разберём основные способы отправки данных в Logentries и подробно рассмотрим настройку сервера с <strong>Debian 7</strong> и связкой <strong>Nginx + PHP-FPM</strong>.</p>
<p>Но, прежде всего, несколько замечаний о самом <strong>Logentries</strong>.</p>
<p>На сегодняшний день это далеко не единственный сервис анализа логов, но он один из самых популярных. Кроме того, у таких сервисов основные возможности вроде поиска, фильтрации, создания уведомлений и т.п. очень похожи. Обычно отличается интерфейс, тарифные планы и набор дополнительных функций.</p>
<p>В бесплатном варианте Logentries предоставляет возможность отправить до 5ГБ логов в месяц и при этом время хранения составляет 7 дней. В платном варианте эти лимиты увеличиваются, а также появляются дополнительные функции (<a href="https://logentries.com/pricing/">подробнее</a>).<br />
<span id="more-1503"></span></p>
<p>Если вы раньше не пользовались подобными сервисами, то очень рекомендую посмотреть <a href="https://logentries.com/doc/video-tutorials/">видеоролики на официальном сайте</a>. Они небольшие, но дают хорошее представление о возможностях системы.</p>
<h2>Способы отправки логов</h2>
<p>Logentries поддерживает несколько методов передачи данных:</p>
<p><strong>1) С помощью специальной программы (агента)</strong>. Это один из самых простых вариантов с точки зрения настройки. Вы только устанавливаете программу и выбираете логи, которые нужно отправлять в Logentries. Но устанавливать на «боевом» сервере сторонний софт не желательно и, кроме того, нужно будет следить за его обновлениями.</p>
<p><strong>2) С помощью syslog (rsyslog)</strong>. Эта утилита установлена в большинстве linux-дистрибутивов. Тут вы только настраиваете отправку нужных логов.</p>
<p><strong>3) С помощью библиотек</strong>. В этом случае вы сможете отправлять сообщения прямо из приложения, без сохранения в файловой системе. Библиотеки написаны для большинства распространённых языков, в том числе Java, PHP, Python, JavaScript/HTML5 и других.</p>
<p>Первые два варианта можно подойдут, если вы используете VPS или выделенный сервер. Т.е. у вас должен быть доступ к консоли, иначе вы просто не сможете сделать нужных настроек. Правда, сейчас у многих PaaS сервисов (например, <a href="https://www.heroku.com/">Heroku</a>, <a href="https://www.appfog.com/">AppFog</a>, <a href="https://www.cloudcontrol.com/">CloudControl</a>) уже реализована поддержка Logentries, и вам нужно её просто активировать в админке.</p>
<p>В общем, с серьёзными ограничениями вы столкнётесь только на shared хостинге. В этом случае у вас будет возможность отправлять сообщения из приложения, логи веб сервера отправить не получится.</p>
<p><em>Примечание</em>. Подробнее почитать об отправке данных можно в статье  <a href="https://blog.logentries.com/2013/12/log-management-101-where-do-logs-come-from/">Log Management 101 – Where Do Logs Come From?</a>.</p>
<h2>Рассмотрим подробно второй вариант – отправку с помощью rsyslog</h2>
<p>Примеры настройки показаны для Debian 7, но для большинства linux дистрибутивов подход будет аналогичным.</p>
<p><strong>Шаг 1. Регистрируемся в Logentries.</strong></p>
<p>Это бесплатно <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><strong>Шаг 2. Создаём логи.</strong></p>
<div id="attachment_1507" class="wp-caption alignnone"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/1_create_log.png" alt="create log" width="354" height="203" class="size-full wp-image-1507" /><p class="wp-caption-text">&nbsp;</p></div>
<p>Здесь я имею в виду, что нужно нажать кнопку «Add new log» в админке Logentries, а не создать log файл на сервере. Обычно каждому log файлу соответствует отдельный лог в админке, хотя можно данные из нескольких log файлов «слить» в один лог.</p>
<p><strong>Шаг 3. Выбираем syslog</strong></p>
<div id="attachment_1508" class="wp-caption alignnone"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/2_select_syslog.png" alt="select syslog" width="401" height="199" class="size-full wp-image-1508" /><p class="wp-caption-text">&nbsp;</p></div>
<p>После этого появится инструкция по настройке rsyslog на вашем сервере.</p>
<div id="attachment_1504" class="wp-caption alignnone"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/3_configuration-450x285.png" alt="configuration" width="450" height="285" class="size-medium wp-image-1504" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/3_configuration-450x285.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/3_configuration-700x444.png 700w, https://www.simplecoding.org/wp-content/uploads/2014/10/3_configuration.png 716w" sizes="auto, (max-width: 450px) 100vw, 450px" /><p class="wp-caption-text">&nbsp;</p></div>
<p>В ней предлагается внести правки в <code>/etc/rsyslog.conf</code>, но, на мой взгляд, удобнее создать файл в директории <code>/etc/rsyslog.d/</code>. Я обычно создаю отдельные конфигурационные файлы под каждый лог, в этом случае они не содержат много настроек и с ними удобно работать.</p>
<p><strong>Шаг 4. Создаём конфигурационный файл для лога Nginx.</strong></p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">sudo touch /etc/rsyslog.d/nginx_access.conf</pre>
<p>и копируем в него настройки из инструкции. Обратите внимание, эти настройки уже содежрат <code>token</code> нового лога.</p>
<p><code>token</code> определяет в какой лог будут попадать сообщения от вашего сервера. Т.е. если вы по каким-то причинам хотите чтобы данные из нескольких логов попадали в один лог Logentries, то нужно просто указать для этих файлов один и тот же <code>token</code>.</p>
<p><strong>Шаг 5. Проверяем передачу данных</strong></p>
<p>Для этого перезапускаем rsyslog и отправляем тестовое сообщение</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">sudo service rsyslog restart
logger -t test Hello there Logentries</pre>
<p>Через некоторое время (обычно меньше минуты) страница (в админке Logentries) обновиться и вы попадёте на страницу просмотра логов.<br />
Последним сообщением в логе будет «test: Hello there Logentries».</p>
<p>Теперь мы убедились, что rsyslog передаёт сообщения Logentries.</p>
<p><em>Примечание</em>. Logentries автоматически формирует названия для логов. Их можно изменить на вкладке Settings.</p>
<div id="attachment_1505" class="wp-caption alignnone"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/4_options-450x250.png" alt="options" width="450" height="250" class="size-medium wp-image-1505" srcset="https://www.simplecoding.org/wp-content/uploads/2014/10/4_options-450x250.png 450w, https://www.simplecoding.org/wp-content/uploads/2014/10/4_options.png 677w" sizes="auto, (max-width: 450px) 100vw, 450px" /><p class="wp-caption-text">&nbsp;</p></div>
<p><strong>Шаг 6. Выбираем лог файл.</strong></p>
<p>В файле <code>/etc/rsyslog.conf</code> (перед подключением <code>rsyslog.d</code>) добавляем строку:</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">$Modload imfile</pre>
<p>И заменяем содержимое <code>/etc/rsyslog.conf/nginx_access.conf</code></p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">$template NginxTemplate,&quot;LOGENTRIES_ACCESS_LOG_TOKEN %HOSTNAME% %syslogtag%%msg%\n&quot;
$InputFileName /var/log/nginx/access.log
$InputFileTag
$InputFileStateFile stat-nginx-access
$InputFileSeverity info
$InputFileFacility local4
$InputFilePollInterval 10
$InputRunFileMonitor
local4.* @@data.logentries.com:10000;NginxTemplate</pre>
<p>Убедитесь, что лог файл находится именно в директории <code>/var/log/nginx/</code>.</p>
<p>Вместо <code>LOGENTRIES_ACCESS_LOG_TOKEN</code> нужно указать реальный token. Его всегда можно посмотреть в админке Logentries. </p>
<div id="attachment_1509" class="wp-caption alignnone"><img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2014/10/log_token.png" alt="log token" width="369" height="104" class="size-full wp-image-1509" /><p class="wp-caption-text">&nbsp;</p></div>
<p>Перезапускаем rsyslog</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">sudo service rsyslog restart</pre>
<p>И через некоторое время у вас должны появиться данные в админке Logentries.</p>
<p><strong>Шаг 7. Подключаем error.log</strong></p>
<p>Повторяем шаги 2-6. Только теперь в создаём конфиг файл для лога с ошибками <code>/etc/rsyslog.conf/nginx_error.conf</code></p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">$template NginxError,&quot;LOGENTRIES_ERROR_LOG_TOKEN %HOSTNAME% %syslogtag%%msg%\n&quot;
$InputFileName /var/log/nginx/error.log
$InputFileTag
$InputFileStateFile stat-nginx-error
$InputFileSeverity info
$InputFileFacility local6
$InputFilePollInterval 1
$InputRunFileMonitor
local6.* @@data.logentries.com:10000;NginxError</pre>
<p>Не забываем перезапустить rsyslog</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">sudo service rsyslog restart</pre>
<p><em>Примечание</em>. Если у вас возникли проблемы с настройкой rsyslog, я очень рекомендую посмотреть <a href="https://logentries.com/doc/rsyslog/">официальную документацию</a>. Возможно, в будущем разработчики Logentries сделают какие-то изменения.</p>
<h2>Заключение</h2>
<p>Как видите, использование rsyslog не зависит от типа логов, которые вы передаёте. Т.е. вы спокойно можете заменить логи nginx из данного примера на логи apache. Главное правильно укажите размещение файлов и token’ы.</p>
<p><strong>Happy logging</strong> <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p><p>The post <a href="https://www.simplecoding.org/logentries-debian-i-logi-nginx.html">Logentries, Debian и логи Nginx</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/logentries-debian-i-logi-nginx.html/feed</wfw:commentRss>
			<slash:comments>93</slash:comments>
		
		
			</item>
		<item>
		<title>Управление созданием правил перезаписи URL в плагинах WordPress</title>
		<link>https://www.simplecoding.org/upravlenie-sozdaniem-pravil-perezapisi-url-v-plaginax-wordpress.html</link>
					<comments>https://www.simplecoding.org/upravlenie-sozdaniem-pravil-perezapisi-url-v-plaginax-wordpress.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Sat, 31 May 2014 14:18:24 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web разработка]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1483</guid>

					<description><![CDATA[<p>В WordPress входит группа функций, которые позволяют определять собственную структуру URL. К ним относятся add_rewrite_rule(), add_rewrite_tag(), flush_rules() и т.п. Использование этих функций достаточно подробно описано в документации, но у движка есть особенности, которые могут привести к неожиданным проблемам. В большинстве случаев дополнительные правила перезаписи URL создаются в плагинах. Например, если вам необходимо на основе какого-то...  <a href="https://www.simplecoding.org/upravlenie-sozdaniem-pravil-perezapisi-url-v-plaginax-wordpress.html" title="Read Управление созданием правил перезаписи URL в плагинах WordPress">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/upravlenie-sozdaniem-pravil-perezapisi-url-v-plaginax-wordpress.html">Управление созданием правил перезаписи URL в плагинах WordPress</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>В <strong>WordPress</strong> входит группа функций, которые позволяют определять собственную структуру URL. К ним относятся <a href="http://codex.wordpress.org/Rewrite_API/add_rewrite_rule">add_rewrite_rule()</a>, <a href="http://codex.wordpress.org/Rewrite_API/add_rewrite_tag">add_rewrite_tag()</a>, <a href="http://codex.wordpress.org/Rewrite_API/flush_rules">flush_rules()</a> и т.п. Использование этих функций достаточно подробно описано в документации, но у движка есть особенности, которые могут привести к неожиданным проблемам.</p>
<p>В большинстве случаев дополнительные правила перезаписи URL создаются в плагинах. Например, если вам необходимо на основе какого-то параметра изменять параметры запроса к базе данных.</p>
<p>Для того, чтобы добавить правило нужно:<br />
<span id="more-1483"></span><br />
1) Вызвать функцию <code>add_rewrite_rule</code> и передать ей регулярное выражение для разбора параметров.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">function add_my_rules() {
    add_rewrite_rule( &#039;my_param/(\d+)?$&#039;, &#039;index.php?my_param=$matches[1]&#039;, &#039;top&#039; );
}</pre>
<p>2) Добавить новую переменную в объект <a href="http://codex.wordpress.org/Class_Reference/WP_Query">WP_Query</a>. Для этого используется фильтр <code>query_vars</code>.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">add_filter( &#039;query_vars&#039;, &#039;add_query_vars&#039; );
function add_query_vars( $vars ) {
    $vars[] = &#039;my_param&#039;;
    return $vars;
}</pre>
<p>В первом параметре функция <code>add_query_vars</code> получит массив со всеми переменными <code>WP_Query</code> и мы сможем добавить в этот массив имя нашего параметра (<code>my_param</code>).</p>
<p>После этого можно получить значение параметра с помощью <code>get_query_var()</code>. Например,</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">$my_param_value = get_query_var(&#039;my_param&#039;);
...</pre>
<p>Т.е. мы сможем использовать полученное значение, например, при выборке постов (в обработчике события <code>pre_get_posts</code>) или прямо в шаблонах темы. Тут возможности ограничены только вашей фантазией и задачами.</p>
<p>Остаётся только один вопрос: <em>«Когда вызывать add_my_rules?»</em></p>
<p>Если правила добавляются в плагине, то логично для этих целей использовать события активации и деактивации. Т.е. сделать что-то вроде:</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">register_activation_hook( __FILE__, &#039;my_plugin_setup&#039; );
function my_plugin_setup() {
    add_my_rules();
    flush_rewrite_rules();
}

register_deactivation_hook( __FILE__, &#039;deactivate&#039; );
function deactivate() {
    flush_rewrite_rules();
}</pre>
<p>В результате всё будет работать, т.к. при активации плагина правила добавляются, а при деактивации &#8212; происходит перезапись правил, без вызова функции <code>add_rewrite_rules</code>, т.е. без новых правил.</p>
<p>Проблема в том, что любой вызов <code>flush_rewrite_rules()</code> будет отключать новые правила. Т.е., например, при входе на страницу настроек постоянных ссылок правила будут отключены.</p>
<p><strong>Обратите внимание.</strong> Для повышения производительности WP кэширует правила. Поэтому для того, чтобы перезаписать правила обязательно нужно вызвать <code>flush_rewrite_rules</code> (сбрасывает кэш). При этом, <code>flush_rewrite_rules</code> может вызываться самим движком (например, при сохранении ЧПУ) и может вызываться другими плагинами.</p>
<p><strong>Решение.</strong></p>
<p>Очевидно, что <code>register_activation_hook</code> для установки правил не подходит, т.к. вызывается только один раз при активации плагина. Нам нужно использовать событие, которое возникает при каждом пересоздании правил.</p>
<p>Изменим наш код следующим образом.</p>
<p>Убираем вызов <code>add_my_rules</code> из <code>register_activation_hook</code> и добавляем обработчик события <code>generate_rewrite_rules</code></p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">add_action( &#039;generate_rewrite_rules&#039;, &#039;add_my_rules&#039; );</pre>
<p>Теперь правила остаются активными при вызовах <code>flush_rewrite_rules</code>, но они не удаляются при деактивации плагина. Происходит это потому, что при каждом вызове <code>flush_rewrite_rules</code>, не зависимо от того кто этот вызов сделал, происходит добавление наших правил.</p>
<p>Исправить проблему можно следующим образом.</p>
<p>Добавляем переменную</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">$is_deactivating = false;
</pre>
<p>устанавливаем её равной true при вызове <code>deactivate</code></p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">function deactivate() {
    $this-&gt;is_deactivating = true;
    flush_rewrite_rules();
}
</pre>
<p>и добавляем проверку при установке правила</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">function add_my_rules() {
    if ($this-&gt;is_deactivating === false) {
        add_rewrite_rule( &#039;my_param/(\d+)?$&#039;, &#039;index.php?my_param=$matches[1]&#039;, &#039;top&#039; );
    }
}</pre>
<p>Теперь при деактивации плагина будет проверяться значение переменной <code>$is_deactivating</code> и если оно равно <code>true</code>, то наши правила добавляться не будут.</p>
<p>Как видите, решение довольно простое, но при условии, что вы понимаете, как работает движок. Помнить обо всех нюансах очень сложно, но есть общее правило – если вы используете встроенные возможности WP, в первую очередь ищите соответствующие события.</p>
<p><strong>Успехов!</strong></p><p>The post <a href="https://www.simplecoding.org/upravlenie-sozdaniem-pravil-perezapisi-url-v-plaginax-wordpress.html">Управление созданием правил перезаписи URL в плагинах WordPress</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/upravlenie-sozdaniem-pravil-perezapisi-url-v-plaginax-wordpress.html/feed</wfw:commentRss>
			<slash:comments>71</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress: как получить медленный запрос с помощью метаданных и WP_Query</title>
		<link>https://www.simplecoding.org/wordpress-kak-poluchit-medlennyj-zapros-s-pomoshhyu-metadannyx-i-wp_query.html</link>
					<comments>https://www.simplecoding.org/wordpress-kak-poluchit-medlennyj-zapros-s-pomoshhyu-metadannyx-i-wp_query.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Tue, 14 Jan 2014 08:07:18 +0000</pubDate>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web разработка]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1462</guid>

					<description><![CDATA[<p>Запросы к базе данных часто оказываются основной причиной снижения скорости приложения. В некоторых случаях эта проблема имеет объективный характер, но иногда она возникает из-за использования «универсальных инструментов». Тут я сразу хочу оговориться, что в 90% случаев такие инструменты отлично работают и экономят время, но когда снижается скорость, желательно понимать что именно они делают и как...  <a href="https://www.simplecoding.org/wordpress-kak-poluchit-medlennyj-zapros-s-pomoshhyu-metadannyx-i-wp_query.html" title="Read WordPress: как получить медленный запрос с помощью метаданных и WP_Query">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/wordpress-kak-poluchit-medlennyj-zapros-s-pomoshhyu-metadannyx-i-wp_query.html">WordPress: как получить медленный запрос с помощью метаданных и WP_Query</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Запросы к базе данных часто оказываются основной причиной снижения скорости приложения. В некоторых случаях эта проблема имеет объективный характер, но иногда она возникает из-за использования «универсальных инструментов». Тут я сразу хочу оговориться, что в 90% случаев такие инструменты отлично работают и экономят время, но когда снижается скорость, желательно понимать что именно они делают и как исправить ситуацию.</p>
<p>Рассмотрим в качестве примера работу с метаданными в <strong>WordPress</strong>. Допустим, у каждой статьи на сайте есть несколько атрибутов, например, <em>рейтинг</em> и <em>количество проголосовавших посетителей</em>. Нам нужно создать фильтры, которые позволят выбирать статьи по этим параметрам, т.е. что-то вроде <em>рейтинг – от 3 до 5</em> и <em>количество голосов – больше 100</em>.</p>
<p>Такую информацию удобно хранить в таблице метаданных <code>wp_postmeta</code>. Это логичное решение, т.к. таблица <code>wp_postmeta</code> связана с <code>wp_posts</code> отношением «многие-к-одному». И мы можем для любой статьи хранить практически не ограниченное количество метаданных. Кроме того, для разных статей можно сохранять собственные наборы полей и это не приведёт к появлению пустых (<code>NULL</code>) значений в таблицах.<br />
<span id="more-1462"></span><br />
<em>Примечание</em>. Такой способ хранения данных очень распространён и используется в большинстве CMS. WordPress в качестве примера я взял только потому, что он широко распространён. Описанные ниже проблемы могут касаться любой системы.</p>
<p>Проблема возникает <strong>при поиске по значениям нескольких метаполей стандартными средствами WP</strong>. Такие запросы могут выполняться очень медленно. Давайте разберемся почему так происходит.</p>
<h2>Таблица wp_postmeta и класс WP_Query</h2>
<p>Таблица <code>wp_postmeta</code> содержит 4 поля:</p>
<ol>
<li><code>meta_id</code> – первичный ключ;</li>
<li><code>post_id</code> – bigint, индексированное;</li>
<li><code>meta_key</code> – varchar, индексированное;</li>
<li><code>meta_value</code> – longtext, не индексированное.</li>
</ol>
<p>Такая структура позволяет хранить практически любые значения метаданных, т.к. тип <code>meta_value</code> – <code>longtext</code>.</p>
<p>При этом поиск по ключу (<code>meta_key</code>) выполняется быстро, т.к. поле индексировано. Т.е. функция </p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">get_post_meta( $post_id, $key, $single );</pre>
<p>работает быстро, т.к. поиск выполняется по двум индексированным полям.</p>
<p>А вот поиск постов на основе <strong>значений</strong> этих полей – совсем другое дело.</p>
<p>В WordPress для выборки постов используется класс <a href="http://codex.wordpress.org/Class_Reference/WP_Query">WP_Query</a>, который поддерживает поиск по значениям метаданных.</p>
<p>Делается это следующим образом.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">$args = array(
     ...
     &#039;relation&#039; =&gt; &#039;AND&#039;,
     &#039;meta_query&#039; =&gt; array(
          array(
               &#039;key&#039; =&gt; &#039;rating&#039;,
               &#039;value&#039; =&gt; array(3, 4),
               &#039;type&#039; =&gt; &#039;numeric&#039;,
               &#039;compare&#039; =&gt; &#039;BETWEEN&#039;,
          ),
          array(
               &#039;key&#039; =&gt; &#039;votes&#039;,
               &#039;value&#039; =&gt; 100,
               &#039;type&#039; =&gt; &#039;numeric&#039;,
               &#039;compare&#039; =&gt; &#039;&gt;=&#039;,
          )
     )
);
$query = new WP_Query($args);</pre>
<p>Такой запрос вернёт все статьи у которых значение рейтинга находится в диапазоне от 3-х до 4-х, а число проголосовавших – больше или равно 100.</p>
<p>Обратите внимание, что мы задали тип полей <code>numeric</code>, т.е. мы хотим, чтобы значения, записанные в поле <code>meta_value</code>, считались числовыми, а не текстовыми.</p>
<p>В результате мы получим запрос вроде</p>
<pre class="brush: sql; gutter: true; first-line: 1; highlight: []; html-script: false">SELECT SQL_CALC_FOUND_ROWS  wp_posts.ID FROM wp_posts
     INNER JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id)
     INNER JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
          WHERE 1=1
               AND wp_posts.post_type IN (&#039;post&#039;, &#039;page&#039;)
               AND (wp_posts.post_status = &#039;publish&#039;)
               AND (
                    (wp_postmeta.meta_key = &#039;rating&#039; AND CAST(wp_postmeta.meta_value AS SIGNED) BETWEEN &#039;3&#039; AND &#039;4&#039;)
                    AND
                    (mt1.meta_key = &#039;votes&#039; AND CAST(mt1.meta_value AS SIGNED) &gt;= &#039;100&#039;)
               )
     GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 10</pre>
<p>Для каждого условия создается внутреннее объединение (<code>INNER JOIN</code>) с таблицей <code>wp_postmeta</code>. Кроме того, при фильтрации значений используется функция <code>CAST</code>, которая преобразует значения <code>wp_postmeta.meta_value</code> в числовой тип.</p>
<p>В реальном запросе будут еще дополнительные условия и объединения, например, с таблицей <code>wp_term_relationships</code> для того, чтобы выбрать посты, которые относятся к определённой категории.</p>
<p>В результате мы скорость выполнения запроса резко падает. И основная причина &#8212; <code>INNER JOIN</code>.</p>
<p>Конкретные значения времени выполнения запроса будут зависеть от размера БД (количества записей в таблицах wp_posts, wp_postmeta) и производительности сервера. Я сталкивался с ситуациями, когда время выполнения запроса с 4-мя такими условиями при 100к+ записях в таблице wp_postmeta (около 5000 постов с примерно 20-ю метаданными на пост) доходило до нескольких минут. Более мощное железо в какой-то степени улучшит ситуацию, но проблему не решит.</p>
<p>Дело в том, что MySQL должен перебрать все возможные сочетания метаданных, а их количество экспоненциально увеличивается с каждым объединением.</p>
<p class="wp-caption"><script type="text/javascript"><!--
google_ad_client = "ca-pub-1195314499431105";
/* simplecoding content */
google_ad_slot = "2021931265";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="https://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</p>
<h2>Варианты решения проблемы</h2>
<h3>1) Создать дополнительную таблицу с нужными полями и хранить данные в ней.</h3>
<p>Тип связи новой таблицы с <code>wp_posts</code> – «один-к-одному». Как вариант, можно добавить дополнительные поля в таблицу <code>wp_posts</code>.</p>
<p>Выборка будет выполняться быстро, т.к. для полей можно указать числовой тип и будет только одно объединение, при этом количество записей в новой таблице будет меньше, чем в <code>wp_postmeta</code>.</p>
<p>Недостатки:</p>
<ol>
<li>Если мы в будущем захотим использовать дополнительные метаданные, то нужно будет либо менять структуру новой таблицы, либо хранить их в wp_postmeta.</li>
<li>Работать с такой таблицей сложнее, т.к стандартные функции WP тут ничем не помогут.</li>
<li>Какая-то часть данных в такой таблице может иметь значение null. Например, если у вас для разных типов постов нужны разные метаполя.</li>
</ol>
<p>Из собственного опыта могу сказать, что это очень эффективное решение. Особенно, если нужно не только проверить значения метаданных. Конечно, некоторое время уходит на то, что написать необходимый код. Но зато запросы выполняются быстро.</p>
<h3>2) Разбить запрос с объединениями на несколько.</h3>
<ol>
<li>Выполняем поиск в таблице wp_postmeta отдельно для каждого условия.</li>
<li>Получаем массивы, содержащие ID постов.</li>
<li>Находим пересечение (или объединение) этих массивов.</li>
<li>В основной запрос вместо условий поиска по метаданным, подставляем ID постов (<code>WHERE ID IN (...)</code>).</li>
</ol>
<p>Количество запросов будет на один больше количества необходимых объединений. Но за счёт того, что выполняются они быстрее, получаем выигрыш в скорости.</p>
<p>Хоть скорость всё-равно остаётся довольно низкой из-за вызова <code>CAST</code>, время выполнения запроса значительно меньше, чем у запроса с объединениями. В одном из моих экспериментов, этим способом получилось уменьшить время с 27 до 0,3 сек.</p>
<h3>3) Добавить индекс для поля meta_value</h3>
<pre class="brush: sql; gutter: true; first-line: 1; highlight: []; html-script: false">ALTER TABLE `wp_postmeta` ADD INDEX  USING BTREE (meta_value(255));</pre>
<p>Этот совет я взял <a href="http://www.nathanfranklin.com.au/coding/experiments-with-meta">отсюда</a>. Решение далеко не идеальное, автору той статьи тоже не нравится.</p>
<p>Эффект есть, но недостаточный. В моём случае время выполнения запроса уменьшилось примерно в 3,5 раза, но всё-равно это слишком медленно.</p>
<h3>4) Для ограниченного набора дискретных значений использовать таксономии вместо метаданных.</h3>
<p>Это не решение данной проблемы, просто иногда метаполя используются там, где нужны таксономии.</p>
<p>Для рейтинга или количества проголосовавших такое решение не подходит, но для таких параметров как цвет, размер (XL, XXL, &#8230;) и т.п. (т.е. любых данных которые имеют фиксированный набор значений) его можно использовать.</p>
<p>При поиске по терминам таксономий тоже используются объединения. Но он выполняется быстрее, чем по метаполям, т.к. в большинстве случаев таблица <code>wp_terms</code> содержит гораздо меньше значений, и вместо <code>BETWEEN</code> будет использоваться <code>IN</code> или <code>=</code>.</p>
<h2>Заключение</h2>
<p>С подобными проблемами можно столкнуться при использовании более-менее сложных библиотек для работы с базой данных. Такие библиотеки, как и любой универсальный инструмент имеют свои ограничения и недостатки, которые могут проявиться при определённых условиях.</p>
<p>Но если вы потратили некоторое время на изучение <strong>SQL</strong> и знаете как посмотреть запросы, которые формирует ваша библиотека, то сможете решить все проблемы <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p><strong>Happy querying!</strong></p><p>The post <a href="https://www.simplecoding.org/wordpress-kak-poluchit-medlennyj-zapros-s-pomoshhyu-metadannyx-i-wp_query.html">WordPress: как получить медленный запрос с помощью метаданных и WP_Query</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/wordpress-kak-poluchit-medlennyj-zapros-s-pomoshhyu-metadannyx-i-wp_query.html/feed</wfw:commentRss>
			<slash:comments>68</slash:comments>
		
		
			</item>
		<item>
		<title>Personal Maps: локализация и интернационализация. Часть 10</title>
		<link>https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html</link>
					<comments>https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Sat, 26 Oct 2013 18:04:41 +0000</pubDate>
				<category><![CDATA[AngularJS]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web разработка]]></category>
		<category><![CDATA[Yii]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1449</guid>

					<description><![CDATA[<p>Приветствую! Это заключительная статья цикла о разработке web приложения с использованием фреймворков Yii и AngularJS. На данный момент у нас есть полностью работающее приложение, и остаётся добавить возможность перевода интерфейса на разные языки. Примечание. Ссылки на все предыдущие статьи вы найдёте в конце этой страницы. Вообще создание многоязычного интерфейса – задача довольно тривиальная. В большинство...  <a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html" title="Read Personal Maps: локализация и интернационализация. Часть 10">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html">Personal Maps: локализация и интернационализация. Часть 10</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p><strong>Приветствую!</strong> Это заключительная статья цикла о разработке web приложения с использованием фреймворков <strong>Yii</strong> и <strong>AngularJS</strong>. На данный момент у нас есть полностью работающее приложение, и остаётся добавить возможность перевода интерфейса на разные языки.</p>
<p><em>Примечание</em>. Ссылки на все предыдущие статьи вы найдёте в конце этой страницы.</p>
<p>Вообще создание многоязычного интерфейса – задача довольно тривиальная. В большинство фреймворков (и Yii здесь не исключение) входят соответствующие библиотеки. Но в нашем случае ситуация немного сложнее из-за того, что приложение состоит из клиентской и серверной частей. При этом для обоих фреймворков (Yii и AngularJS) есть собственные средства для работы с переводами.<br />
<span id="more-1449"></span><br />
В принципе, можно работать с двумя библиотеками. Но обычно удобнее, собрать все переводы в одном месте, либо на клиенте, либо на сервере. Т.к. передача данных от сервера клиенту (браузеру) проще, то мы будем использовать библиотеку Yii в качестве основной. А при формировании главной страницы приложения, передадим переводы AngularJS.</p>
<p><em>Напоминаю</em>. Вы можете посмотреть исходный код приложения на GitHub и поэкспериментировать с демо-версией.</p>
<a href="https://github.com/vladimir-s/personal-maps" class="src-button">Source</a>
<a href="http://personal-maps.simplecoding.org" class="demo-button">Demo</a>
<h2>Интернационализация серверной части (Yii)</h2>
<p>В официальном руководстве есть <a href="http://www.yiiframework.com/doc/guide/1.1/ru/topics.i18n">подробная статья на эту тему</a>, повторять её я не буду, а остановлюсь только на тех моментах, которые относятся к нашему приложению.</p>
<p>Прежде всего, необходимо выбрать тип источника сообщений. Yii поддерживает три типа таких источников:</p>
<ul>
<li>обычные PHP массивы (CPhpMessageSource);</li>
<li>файлы формата GNU Gettext (CGettextMessageSource);</li>
<li>базу данных (CDbMessageSource).</li>
</ul>
<p>Я остановился на первом варианте (PHP массивы), но принципиальной разницы нет.</p>
<p>Важно другое. Для передачи переводов клиентской части нам очень желательно передать сразу все переводы, иначе их придётся загружать AJAX запросами, а это занимает время и не лучшим образом скажется на внешнем виде приложения. Но класс <a href="http://www.yiiframework.com/doc/api/1.1/CPhpMessageSource">CPhpMessageSource</a> не позволяет получить все переводы сразу, точнее нужный нам метод <code>loadMessages</code> объявлен защищённым (protected).</p>
<p>Поэтому мы создадим компонент, который наследует <code>CPhpMessageSource</code> и будем использовать его в качестве источника сообщений.</p>
<p><code>protected/components/PhpMessageSource.php</code></p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">class PhpMessageSource extends CPhpMessageSource {
    public function getAllMessages($category, $lang) {
        return $this-&gt;loadMessages($category, $lang);
    }
}</pre>
<p>Мы объявили один метод <code>getAllMessages</code>, который просто вызывает <code>loadMessages</code>, т.е. возвращает массив переводов для указанного языка.</p>
<p>Подключаем наш компонент в <code>config/main.php</code></p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">return array(
	...
    &#039;language&#039;=&gt;&#039;ru&#039;,

	...
	// application components
	&#039;components&#039;=&gt;array(
		...
        &#039;messages&#039;=&gt;array(
            &#039;class&#039;=&gt;&#039;PhpMessageSource&#039;,
        ),
	),
	...
);</pre>
<p>Файлы с переводами находятся в папке <code>protected/messages</code>.</p>
<pre class="brush: text; gutter: true; first-line: 1; highlight: []; html-script: false">messages/
	en/
		frontend.php
		...
	ru/
		frontend.php
		...</pre>
<p>Сами переводы выглядят следующим образом:</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">return array(
    &#039;CREATE_PLACE&#039; =&gt; &#039;Create place&#039;,
    &#039;UPDATE_PLACE&#039; =&gt; &#039;Update place&#039;,
	...
);

return array(
    &#039;CREATE_PLACE&#039; =&gt; &#039;Создать объект&#039;,
    &#039;UPDATE_PLACE&#039; =&gt; &#039;Изменить объект&#039;,
	...
);</pre>
<p class="wp-caption"><script type="text/javascript"><!--
google_ad_client = "ca-pub-1195314499431105";
/* simplecoding content */
google_ad_slot = "2021931265";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="https://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</p>
<h2>Интернационализация клиентской части (AngularJS)</h2>
<p>Прежде всего, нам нужно передать переводы браузеру. Для этого в представление, которое создаёт главную страницу приложения (<code>protected/views/places/index.php</code>), добавим следующий код:</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">Yii::app()-&gt;clientScript-&gt;registerScriptFile(Yii::app()-&gt;baseUrl.&#039;/js/angular-translate.min.js&#039;, CClientScript::POS_END);
Yii::app()-&gt;clientScript-&gt;registerScript(
    &#039;langScript&#039;
    , &#039;
    var lang = &quot;&#039;.Yii::app()-&gt;getLanguage().&#039;&quot;;
    var translations = &#039;.CJSON::encode(Yii::app()-&gt;messages-&gt;getAllMessages(&#039;frontend&#039;, Yii::app()-&gt;getLanguage())).&#039;;&#039;
    , CClientScript::POS_HEAD
);</pre>
<p>В первой строке мы подключаем <a href="http://ngmodules.org/modules/angular-translate">Angular translate</a>. Это модуль, предназначенный для интернационализации приложений на <strong>Angular</strong>.</p>
<p>Затем мы создаём две JS переменные:</p>
<p><code>lang</code> – содержит название языка;<br />
<code>translations</code> – содержит массив с переводами.</p>
<p>Этих данных нам достаточно для того, чтобы настроить приложение <code>public_html/js/app.js</code></p>
<p>Мы указываем модуль <code>pascalprecht.translate</code> в списке зависимостей приложения.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">var app = angular.module(&#039;personalmaps&#039;, [&#039;ui.bootstrap&#039;, &#039;pascalprecht.translate&#039;])
    .value(&#039;lang&#039;, lang);</pre>
<p>В результате через систему внедрения зависимостей (Dependency injection &#8212; DI) станет доступен сервис <code>$translateProvider</code>, которому мы передаём массив с переводами.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">app.config([&#039;$translateProvider&#039;, function($translateProvider) {
    // add translation table
    $translateProvider.translations(translations);
}]);</pre>
<p>Также через DI будет доступна переменная <code>lang</code>. Вообще мы можем обойтись без неё, но я решил просто показать пример использования переменных. Т.е. получить к ней доступ можно, например, так:</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">app.controller(&#039;PlacesListController&#039;
    , [&#039;$scope&#039;, &#039;$rootScope&#039;, &#039;Places&#039;, &#039;$dialog&#039;, &#039;lang&#039;
    , function($scope, $rootScope, Places, $dialog, lang) {

    $scope.curLang = lang;

	...
}]);</pre>
<p>Возвращаемся к модулю <strong>Angular translate</strong>.</p>
<p>На официальном сайте предлагается загрузить все варианты переводов.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">app.config(function ($translateProvider) {
  $translateProvider.translations(&#039;en&#039;, {
    TITLE: &#039;Hello&#039;,
	...
  });
  $translateProvider.translations(&#039;de&#039;, {
    TITLE: &#039;Hallo&#039;,
	...
  });
  $translateProvider.preferredLanguage(&#039;en&#039;);
});</pre>
<p>Такой способ имеет смысл использовать, если вы хотите предоставить пользователю возможность переключать языки прямо из интерфейса приложения, т.е. выполнять следующий код.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$scope.changeLanguage = function (key) {
	$translate.uses(key);
};</pre>
<p>Но в нашем приложении такая возможность не предусматривается. Язык приложения указывается в конфигурационном файле <code>main.php</code>, поэтому отправлять браузеру все переводы нет смысла. Мы просто один раз вызываем метод <code>translations</code> и передаём ему массив с переводами на выбранный язык.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$translateProvider.translations(translations);</pre>
<p>Для использования переводов в модуль angular translate входит специальный фильтр – <code>translate</code>. Т.е. теперь в шаблонах мы можем написать что-то вроде (<code>partials/list.html</code>):</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;span ng-show=&quot;isEmpty()&quot;&gt;{{ &#039;NO_PLACES&#039; | translate }}&lt;/span&gt;

&lt;div&gt;
    &lt;a href=&quot;places/index#/add&quot; class=&quot;btn btn-success&quot;&gt;{{ &#039;ADD_PLACE&#039; | translate }}&lt;/a&gt;
&lt;/div&gt;</pre>
<p>В фигурных скобках мы указываем имя сообщения (ключ в массиве <code>translations</code>) и через вертикальную черту – название фильтра. В результате мы получим значение из массива <code>translations</code>, т.е. сообщение на нужном языке.</p>
<h2>Заключение</h2>
<p>Этот цикл получился довольно объемным, и публикация растянулась на два месяца. Но мне кажется, что такой формат полезнее, чем отдельные статьи, потому что в них сложно рассмотреть взаимодействие нескольких технологий между собой. Даже в этом цикле многие вещи пришлось упростить, чтобы не перегружать код различными проверками и дополнительными функциями. Всё-таки основная цель заключалась в том, чтобы показать, как компоненты взаимодействуют между собой, а не создать приложение для продакшена (хотя приложение вполне можно использовать).</p>
<p>И отдельно хочу поблагодарить всех читателей, которые присылали вопросы и замечания. Вы помогли сделать этот цикл лучше.</p>
<p><strong>Успехов!</strong></p>
<h3>Содержание</h3>
<div id="serial-posts-wrapper">
<h3 class="serial-posts-heading"></h3>
<ul class="serial-posts">
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ispolzuem-yii-i-angularjs-dlya-razrabotki-web-prilozheniya-chast-1.html" title="Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.">Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ustanavlivaem-i-nastraivaem-yii-proektiruem-strukturu-bazy-dannyx-chast-2.html" title="Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.">Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-client-side-app-structure-part-3.html" title="Personal Maps: главная страница и структура клиентской части приложения. Часть 3.">Personal Maps: главная страница и структура клиентской части приложения. Часть 3.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html" title="Personal Maps: создаём сервис AngularJS. Часть 4.">Personal Maps: создаём сервис AngularJS. Часть 4.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html" title="Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html" title="Personal Maps: контроллеры и представления в AngularJS. Часть 6.">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html" title="Personal maps: создаём директиву для подключения Google Maps. Часть 7.">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html" title="Personal maps: REST интерфейс. Часть 8.">Personal maps: REST интерфейс. Часть 8.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html" title="Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</a></li>
<li class="serial-posts-list-item current-inactive">Personal Maps: локализация и интернационализация. Часть 10</li>
</ul>
</div><p>The post <a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html">Personal Maps: локализация и интернационализация. Часть 10</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html/feed</wfw:commentRss>
			<slash:comments>60</slash:comments>
		
		
			</item>
		<item>
		<title>Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</title>
		<link>https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html</link>
					<comments>https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Sat, 19 Oct 2013 10:21:41 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web разработка]]></category>
		<category><![CDATA[Yii]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1445</guid>

					<description><![CDATA[<p>В предыдущих статьях этого цикла мы практически завершили создание нашего web приложения (ссылки на предыдущие статьи вы найдёте внизу этой страницы). Мы можем создавать и редактировать объекты, отображать их на карте. Но для полноценной работы приложения нам необходимо создавать пользователей с различными уровнями доступа, а также проверять их права при обращении к приложению. Прежде чем...  <a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html" title="Read Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>В предыдущих статьях этого цикла мы практически завершили создание нашего web приложения (ссылки на предыдущие статьи вы найдёте внизу этой страницы). Мы можем создавать и редактировать объекты, отображать их на карте. Но для полноценной работы приложения нам необходимо создавать пользователей с различными уровнями доступа, а также проверять их права при обращении к приложению.</p>
<p>Прежде чем переходить к системе разграничения доступа, давайте разберемся, какие типы пользователей нужны для нашего приложения.</p>
<p>Предполагается, что каждый пользователь будет только со своими объектами, то нам будет достаточно двух уровней доступа:</p>
<ul>
<li>user – обычный пользователь, может работать только со своими объектами;</li>
<li>admin – администратор, может работать со своими объектами, а также создавать, изменять и удалять других пользователей.</li>
</ul>
<p>Т.е. администратор имеет те же самые права, что и обычный пользователь, плюс несколько дополнительных. Реализовать такую систему разграничения доступа удобнее всего с помощью <a href="http://www.yiiframework.com/doc/guide/1.1/ru/topics.auth#sec-7">RBAC</a> (Role Based &#8212; Управление доступом на основе ролей).<br />
<span id="more-1445"></span></p>
<p><em>Примечание</em>. Исходный код приложения размещён на GitHub, также доступна демоверсия приложения.</p>
<a href="https://github.com/vladimir-s/personal-maps" class="src-button">Source</a>
<a href="http://personal-maps.simplecoding.org" class="demo-button">Demo</a>
<h2>Структура правил RBAC</h2>
<p>В Yii фреймворк входит удобная библиотека для реализации такого управления доступом, которая поддерживает три элемента авторизации: операции (<code>operations</code>), задачи (<code>tasks</code>) и роли (<code>roles</code>). Роль может включать в себя несколько задач, а каждая задача – несколько операций.</p>
<p>Рассмотрим, как эти элементы авторизации связаны между собой в нашем приложении.</p>
<img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2013/10/rbac.png" alt="rbac structure" width="640" height="741" class="alignnone size-full wp-image-1447" srcset="https://www.simplecoding.org/wp-content/uploads/2013/10/rbac.png 640w, https://www.simplecoding.org/wp-content/uploads/2013/10/rbac-450x521.png 450w" sizes="auto, (max-width: 640px) 100vw, 640px" />
<p>В первую очередь мы определяем операции пользователей (левая колонка на рисунке). Т.к. приложение у нас достаточно простое, операций немного. Фактически мы определили только CRUD операции для объектов типа <code>Place</code> и <code>User</code>.</p>
<h2>Авторизация пользователей</h2>
<p>Процедура авторизации предполагает проверку прав пользователя на доступ к запрошенному ресурсу или выполнение какой-то операции.</p>
<p>Эта проверка выполняется в методах контроллера (до того как мы что-либо отправили пользователю) с помощью метода <code>checkAccess</code>:</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">public function actionIndex() {
    if (Yii::app()-&gt;user-&gt;checkAccess(&#039;viewPlaces&#039;)) {
        $this-&gt;render(&#039;index&#039;);
    }
    else {
        $this-&gt;redirect(array(&#039;site/login&#039;));
    }
}</pre>
<p>Если текущий пользователь имеет соответствующие права, метод <code>checkAccess</code> вернёт <code>true</code> и будет показана главная страница приложения, если нет – произойдёт редирект на страницу с формой ввода логина и пароля.</p>
<p>Немного сложнее <strong>использование задач</strong>. В нашем приложении у пользователя не должно быть возможности удалить чужие объекты. Т.е. операция удаления должна быть ему доступна, но при этом нужно проверить принадлежит ли объект ему. Для этого используются задачи (tasks). При их создании мы определяем так называемое «бизнес правило». В нём мы должны сравнить <code>id</code> текущего пользователя со значением поля <code>p_user_id</code> выбранного объекта.</p>
<p>Т.е. бизнес правило будет выглядеть так:</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">return Yii::app()-&gt;user-&gt;id==$params[&quot;place&quot;]-&gt;p_user_id;</pre>
<p>При этом <code>$params</code> содержит данные, которые передаются во втором параметре <code>checkAccess</code>. В нашем случае это будет объект, который нужно удалить. Теперь рассмотрим метод удаления объекта.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">public function actionDelete($id)
{
	$place = Places::model()-&gt;findByPk($id);
	if (null === $place) {
		$this-&gt;_sendResponse(404, CJSON::encode(array(&#039;message&#039;=&gt;&#039;Could not find place with id = &#039;.$id)));
		return;
	}
	if (!Yii::app()-&gt;user-&gt;checkAccess(&#039;deletePlace&#039;, array(&#039;place&#039;=&gt;$place))) {
		$this-&gt;_sendResponse(403);
		return;
	}
	if ($place-&gt;delete()) {
		$this-&gt;_sendResponse(200, CJSON::encode($place));
	}
	else {
		$this-&gt;_sendResponse(500, CJSON::encode(array(
			&#039;message&#039;=&gt;&#039;Could not delete place&#039;,
			&#039;errors&#039;=&gt;$place-&gt;getErrors(),
		)));
	}
}</pre>
<p>Запрос на удаление выглядит следующим образом</p>
<p><code>/api/places/id</code></p>
<p>В первую очередь мы ищем объект, который имеет указанный <code>id</code>. Если объект не найден, отправляем 404-ую ошибку.</p>
<p>Затем выполняем проверку прав пользователя. Обратите внимание, в методе <code>checkAccess</code> мы проверяем права на выполнение операции <code>deletePlace</code>, но при этом передаём во втором параметре массив с найденным объектом. Т.к. для операции <code>deletePlace</code> определена задача <code>viewOwnPlace</code> при проверке доступа будет использовано бизнес правило этой задачи. И если значение <code>p_user_id</code> не совпадает с <code>id</code> пользователя, метод вернёт <code>false</code>.</p>
<h2>Создание правил RBAC</h2>
<ol>
<li>Правила, которые используются для проверок, должны где-то храниться. Это может быть как файл, так и база данных. Мы используем второй вариант.</li>
<li>Правила нужно создать. Для этого можно реализовать web интерфейс, который позволит редактировать правила для разных групп пользователей. Но т.к. у нас не предполагается возможность изменения связей между ролями, операциями и задачами, мы создадим их с помощью консольной команды.</li>
</ol>
<p>Сначала создадим таблицы, в которых будут храниться правила. Их схема хранится в файле <code>framework/web/auth/schema-mysql.sql</code>. Просто импортируем его в базу и в результате у нас появятся три новые таблицы: <code>authassignment</code>, <code>authitem</code> и <code>authitemchild</code>.</p>
<p>Теперь нам нужно указать Yii, что мы хотим хранить правила в базе. Для этого в <code>config/main.php</code> настроим компонент <code>authManager</code>.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">&#039;components&#039;=&gt;array(
	...
	&#039;authManager&#039;=&gt;array(
		&#039;class&#039;=&gt;&#039;CDbAuthManager&#039;,
		&#039;connectionID&#039;=&gt;&#039;db&#039;,
	),
	...
),</pre>
<p>Напишем консольную команду.</p>
<p>Для этого создаём файл <code>protected/commands/AccessCommand.php</code> с двумя методами.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">class AccessCommand extends CConsoleCommand
{
    public function actionAddRules() {
	...
    }

    public function actionAddAdminUser() {
	...
    }
}</pre>
<p>Первый метод создаёт правила, второй – добавляет пользователя с правами администратора. Для того чтобы их вызвать нужно из консоли (из папки <code>protected</code>) выполнить команды</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">yiic access addRules
yiic access addAdminUser</pre>
<p>Рассмотрим <strong>создание правил</strong></p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">public function actionAddRules() {
	$auth=Yii::app()-&gt;authManager;

	$auth-&gt;createOperation(&#039;addPlace&#039;,&#039;create place&#039;);
	$auth-&gt;createOperation(&#039;viewPlace&#039;,&#039;view place&#039;);
	$auth-&gt;createOperation(&#039;updatePlace&#039;,&#039;update place&#039;);
	$auth-&gt;createOperation(&#039;deletePlace&#039;,&#039;delete place&#039;);
	$auth-&gt;createOperation(&#039;viewPlaces&#039;,&#039;view places&#039;);

	$auth-&gt;createOperation(&#039;addUser&#039;,&#039;create user&#039;);
	$auth-&gt;createOperation(&#039;viewUser&#039;,&#039;view user&#039;);
	$auth-&gt;createOperation(&#039;updateUser&#039;,&#039;update user&#039;);
	$auth-&gt;createOperation(&#039;deleteUser&#039;,&#039;delete user&#039;);
	$auth-&gt;createOperation(&#039;viewUsers&#039;,&#039;view users&#039;);

	$bizRule=&#039;return Yii::app()-&gt;user-&gt;id==$params[&quot;place&quot;]-&gt;p_user_id;&#039;;
	$task=$auth-&gt;createTask(&#039;viewOwnPlace&#039;, &#039;view own place&#039;, $bizRule);
	$task-&gt;addChild(&#039;viewPlace&#039;);
	$task=$auth-&gt;createTask(&#039;updateOwnPlace&#039;, &#039;edit own place&#039;, $bizRule);
	$task-&gt;addChild(&#039;updatePlace&#039;);
	$task=$auth-&gt;createTask(&#039;deleteOwnPlace&#039;, &#039;delete own place&#039;, $bizRule);
	$task-&gt;addChild(&#039;deletePlace&#039;);

	$role=$auth-&gt;createRole(&#039;user&#039;);
	$role-&gt;addChild(&#039;addPlace&#039;);
	$role-&gt;addChild(&#039;viewOwnPlace&#039;);
	$role-&gt;addChild(&#039;updateOwnPlace&#039;);
	$role-&gt;addChild(&#039;deleteOwnPlace&#039;);
	$role-&gt;addChild(&#039;viewPlaces&#039;);

	$role=$auth-&gt;createRole(&#039;admin&#039;);
	$role-&gt;addChild(&#039;user&#039;);
	$role-&gt;addChild(&#039;addUser&#039;);
	$role-&gt;addChild(&#039;viewUser&#039;);
	$role-&gt;addChild(&#039;updateUser&#039;);
	$role-&gt;addChild(&#039;deleteUser&#039;);
	$role-&gt;addChild(&#039;viewUsers&#039;);
}</pre>
<p>Сначала мы получаем <code>authManager</code> с помощью которого выполняется создание операций, задач и ролей. Каждая операция создаётся с помощью метода <code>createOperation</code>, в первом параметре которого мы указываем имя операции, во втором &#8212; описание.</p>
<p>Затем с помощью <code>createTask</code> создаём задачи, для каждой из них указываем бизнес правило. И с помощью метода <code>addChild</code> «привязываем» операцию к задаче. Теперь при проверке прав доступа к операции будет использована соответствующая задача. Т.е. в первом параметре <code>checkAccess</code> мы в любом случае указываем название операции, а не задачи.</p>
<p>Для создания роли используется метод <code>createRole</code>. После этого к роли с помощью метода <code>addChild</code> «привязываем» операции, задачи и другие роли.</p>
<p><em>Обратите внимание</em>. К роли <code>admin</code> мы не «привязываем» операции вроде <code>viewPlace</code>, т.к. у нас администратор должен получить все права пользователя мы «привязываем» роль <code>user</code>. На рисунке каждая стрелка соответствует вызову метода <code>addChild</code>, т.е. «привязке» одного объекта к другому. В результате получается иерархическое наследование прав.</p>
<p class="wp-caption"><script type="text/javascript"><!--
google_ad_client = "ca-pub-1195314499431105";
/* simplecoding content */
google_ad_slot = "2021931265";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="https://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</p>
<h2>Аутентификация пользователей</h2>
<p>Процедура аутентификации предполагает проверку подлинности пользователя. В нашем случае она заключается в проверке логина и пароля.</p>
<p>Прежде всего, создадим класс <code>UserIdentity</code> с методом <code>authenticate</code> (<code>protected/components/UserIdentity.php</code>)</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">class UserIdentity extends CUserIdentity
{
	private $_id;

	public function authenticate()
	{
		$record = Users::model()-&gt;findByAttributes(array(&#039;u_name&#039; =&gt; $this-&gt;username));
		if ($record === null) {
			$this-&gt;errorCode = self::ERROR_USERNAME_INVALID;
		} else if ($record-&gt;u_pass !== crypt($this-&gt;password, $record-&gt;u_pass)) {
			$this-&gt;errorCode = self::ERROR_PASSWORD_INVALID;
		} else {
			$this-&gt;_id = $record-&gt;id;
			$this-&gt;errorCode = self::ERROR_NONE;
		}
		return !$this-&gt;errorCode;
	}

	public function getId() {
		return $this-&gt;_id;
	}

	/**
	 * Generate a random salt in the crypt(3) standard Blowfish format.
	 *
	 * @param int $cost Cost parameter from 4 to 31.
	 *
	 * @throws Exception on invalid cost parameter.
	 * @return string A Blowfish hash salt for use in PHP&#039;s crypt()
	 */
	public static function blowfishSalt($cost = 13)
	{
		if (!is_numeric($cost) || $cost &lt; 4 || $cost &gt; 31) {
			throw new Exception(&quot;cost parameter must be between 4 and 31&quot;);
		}
		$rand = array();
		for ($i = 0; $i &lt; 8; $i += 1) {
			$rand[] = pack(&#039;S&#039;, mt_rand(0, 0xffff));
		}
		$rand[] = substr(microtime(), 2, 6);
		$rand = sha1(implode(&#039;&#039;, $rand), true);
		$salt = &#039;$2a$&#039; . sprintf(&#039;%02d&#039;, $cost) . &#039;$&#039;;
		$salt .= strtr(substr(base64_encode($rand), 0, 22), array(&#039;+&#039; =&gt; &#039;.&#039;));
		return $salt;
	}
}</pre>
<p>В методе <code>authenticate</code> (строка 10) мы проверяем пароль с помощью функции <a href="http://www.php.net/manual/en/function.crypt.php">crypt</a>. На сегодняшний день, использование <code>md5</code> для хеширование паролей считается не надёжным, поэтому необходимо генерировать хеши на основе более сложных алгоритмов.</p>
<p>Функция <code>crypt</code> поддерживает несколько алгоритмов хеширования, в том числе blowfish, который мы используем в данном приложении. В первом параметре <code>crypt</code> передаётся пароль, а во втором – соль. При этом, формат соли определяет алгоритм хеширования. Функция вернет хеш пароля, который мы сравниваем с хешем, который хранится в базе данных.</p>
<p>Также в данный класс мы добавили метод <code>blowfishSalt</code>, который формирует хеш пароля в по алгоритму blowfish.</p>
<p><strong>Важно!</strong> В Yii 1.1.14 появился класс <a href="http://www.yiiframework.com/doc/api/1.1/CPasswordHelper">CPasswordHelper</a>, который можно использовать для выполнения этой же задачи. Алгоритмы будут использоваться те же самые, но ваш код будет короче. В частности, не придётся реализовывать метод <code>blowfishSalt</code>. Подробности вы можете почитать в статье <a href="http://www.yiiframework.com/wiki/425">Use crypt() for password storage</a>. Кстати, код <code>blowfishSalt</code> я взял из старой версии этой статьи.</p>
<p>Теперь добавим команду, которая будет <strong>создавать аккаунт администратора</strong>.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">class AccessCommand extends CConsoleCommand
{
	...

    public function actionAddAdminUser() {
        $auth=Yii::app()-&gt;authManager;

        $user = new Users();
        $user-&gt;u_name = &#039;admin&#039;;
        $user-&gt;u_pass = &#039;admin&#039;;
        $user-&gt;u_email = &#039;admin@site.loc&#039;;
        $user-&gt;u_role = &#039;admin&#039;;
        $user-&gt;save();
    }
}</pre>
<p>Для того, чтобы эта команда правильно работала, нам нужно создать модель <code>Users</code>.</p>
<h2>Контроллер и модель для работы с пользователями</h2>
<p>Модель создаём с помощью gii и добавляем в неё два метода.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">class Users extends CActiveRecord
{
	...
	
    public function beforeSave() {
        $this-&gt;u_pass = crypt($this-&gt;u_pass, UserIdentity::blowfishSalt());
        return parent::beforeSave();
    }

    public function afterSave() {
        $assignments = Yii::app()-&gt;authManager-&gt;getAuthAssignments($this-&gt;id);
        if (!empty($assignments)) {
            foreach ($assignments as $key =&gt; $assignment) {
                Yii::app()-&gt;authManager-&gt;revoke($key, $this-&gt;id);
            }
        }
        Yii::app()-&gt;authManager-&gt;assign($this-&gt;u_role, $this-&gt;id);
        return parent::afterSave();
    }
}</pre>
<p>В методе <code>beforeSave</code> мы с помощью <code>blowfishSalt</code> хешируем пароль пользователя перед сохранением его в БД.</p>
<p>Метод <code>afterSave</code> используется для связей между пользователем и правами доступа. Это необходимо для того, чтобы администратор мог изменить права для уже созданных пользователей.</p>
<p>В первую очередь с помощью <code>getAuthAssignments</code> мы получаем массив всех связей между указанным пользователем и элементами авторизации RBAC. Далее удаляем все существующие связи и затем, с помощью метода <code>assign</code>, создаём новую связь. Новая связь устанавливается на основе значения поля <code>u_role</code>. Т.е. к конкретному пользователю мы привязываем роли, а не операции или задачи.</p>
<p><em>Обратите внимание</em>. Пользователь с ролью администратор фактически имеет две роли: <code>admin</code> и <code>user</code>, т.к. роль <code>admin</code> наследует <code>user</code>. Поэтому мы храним «главную» роль пользователя в таблице <code>pm_users</code>. И именно к этой роли «привязываем» пользователя с помощью <code>assign</code>.</p>
<p>Контроллер и представления для модели Users мы также создаём с помощью gii.</p>
<p>Затем добавляем проверку прав пользователя в методы контроллера. Например,</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">public function actionView($id)
{
	if (Yii::app()-&gt;user-&gt;checkAccess(&#039;viewUser&#039;)) {
		$this-&gt;render(&#039;view&#039;, array(
			&#039;model&#039; =&gt; $this-&gt;loadModel($id),
		));
	}
	else {
		throw new CHttpException(403);
	}
}</pre>
<p>И так далее для всех методов, названия которых начинаются с <code>action</code>. Таким образом, использовать этот контроллер сможет только администратор.</p>
<p>Соответственно в файле <code>views/layouts/yiistrap.php</code> настроим меню таким образом, чтобы пункт «Пользователи» отображался только для администратора.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;?php $this-&gt;widget(&#039;zii.widgets.CMenu&#039;,array(
	&#039;items&#039;=&gt;array(
		array(&#039;label&#039;=&gt;Yii::t(&#039;app&#039;, &#039;PLACES&#039;), &#039;url&#039;=&gt;array(&#039;/places/index&#039;), &#039;visible&#039;=&gt;!Yii::app()-&gt;user-&gt;isGuest),
		array(&#039;label&#039;=&gt;Yii::t(&#039;app&#039;, &#039;USERS&#039;), &#039;url&#039;=&gt;array(&#039;/users/index&#039;), &#039;visible&#039;=&gt;Yii::app()-&gt;user-&gt;checkAccess(&#039;viewUsers&#039;)),
		array(&#039;label&#039;=&gt;Yii::t(&#039;app&#039;, &#039;ABOUT&#039;), &#039;url&#039;=&gt;array(&#039;/site/page&#039;, &#039;view&#039;=&gt;&#039;about&#039;)),
		array(&#039;label&#039;=&gt;Yii::t(&#039;app&#039;, &#039;CONTACT&#039;), &#039;url&#039;=&gt;array(&#039;/site/contact&#039;)),
		array(&#039;label&#039;=&gt;Yii::t(&#039;app&#039;, &#039;LOGIN&#039;), &#039;url&#039;=&gt;array(&#039;/site/login&#039;), &#039;visible&#039;=&gt;Yii::app()-&gt;user-&gt;isGuest),
		array(&#039;label&#039;=&gt;Yii::t(&#039;app&#039;, &#039;LOGOUT&#039;).&#039; (&#039;.Yii::app()-&gt;user-&gt;name.&#039;)&#039;, &#039;url&#039;=&gt;array(&#039;/site/logout&#039;), &#039;visible&#039;=&gt;!Yii::app()-&gt;user-&gt;isGuest)
	),
	&#039;activeCssClass&#039;=&gt;&#039;active&#039;,
	&#039;htmlOptions&#039;=&gt;array(
		&#039;class&#039;=&gt;&#039;nav&#039;,
	),
)); ?&gt;</pre>
<p>В строке 4 мы с помощью <code>checkAccess</code> проверяем, может ли данный пользователь просматривать список пользователей (<code>/users/index</code>). </p>
<p>И в качестве завершающего штриха немного исправим форму создания (редактирования) пользователя (файл <code>views/users/_form.php</code>)</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;?php echo $form-&gt;errorSummary($model); ?&gt;

		&lt;?php echo $form-&gt;textFieldControlGroup($model,&#039;u_name&#039;,array(&#039;span&#039;=&gt;5,&#039;maxlength&#039;=&gt;255)); ?&gt;

		&lt;?php echo $form-&gt;textFieldControlGroup($model,&#039;u_email&#039;,array(&#039;span&#039;=&gt;5,&#039;maxlength&#039;=&gt;255)); ?&gt;

		&lt;?php echo $form-&gt;passwordFieldControlGroup($model,&#039;u_pass&#039;,array(&#039;span&#039;=&gt;5,&#039;maxlength&#039;=&gt;255,&#039;value&#039;=&gt;&#039;&#039;)); ?&gt;

		&lt;?php echo $form-&gt;passwordFieldControlGroup($model,&#039;u_pass_repeat&#039;,array(&#039;span&#039;=&gt;5,&#039;maxlength&#039;=&gt;255,&#039;value&#039;=&gt;&#039;&#039;)); ?&gt;

		&lt;?php echo $form-&gt;dropDownListControlGroup($model, &#039;u_role&#039;, array(&#039;user&#039;=&gt;&#039;user&#039;, &#039;admin&#039;=&gt;&#039;admin&#039;), array(&#039;class&#039; =&gt; &#039;span5&#039;)); ?&gt;

	&lt;div class=&quot;form-actions&quot;&gt;
	&lt;?php echo TbHtml::submitButton($model-&gt;isNewRecord ? Yii::t(&#039;app&#039;, &#039;SAVE&#039;) : Yii::t(&#039;app&#039;, &#039;UPDATE&#039;), array(
		&#039;color&#039;=&gt;TbHtml::BUTTON_COLOR_PRIMARY,
		&#039;size&#039;=&gt;TbHtml::BUTTON_SIZE_LARGE,
	)); ?&gt;
&lt;/div&gt;

&lt;?php $this-&gt;endWidget(); ?&gt;</pre>
<p>В частности, мы использовали метод <code>dropDownListControlGroup</code> для того, чтобы создать выпадающий список с ролями пользователей. Gii создаёт обычное текстовое поле.</p>
<h2>Заключение</h2>
<p>Использования RBAC особой сложности не представляет. Самое главное, изначально продумать систему ролей таким образом, чтобы её было легко и удобно расширять. И помнить, что проверять в контроллерах нужно операции, а роли «привязывать» к пользователям. Тогда вы сможете создавать новые роли с любым набором прав, и при этом не нужно будет изменять проверки в контроллерах и другие роли.</p>
<p>Также советую почитать статью <a href="http://habrahabr.ru/post/177873/">RBAC Авторизация в YII и LDAP</a>. В ней более подробно описывается использование RBAC в Yii без привязки к конкретному приложению.</p>
<p>Если есть вопросы или замечания, пишите.<br />
<strong>Успехов!</strong></p>
<h3>Содержание</h3>
<div id="serial-posts-wrapper">
<h3 class="serial-posts-heading"></h3>
<ul class="serial-posts">
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ispolzuem-yii-i-angularjs-dlya-razrabotki-web-prilozheniya-chast-1.html" title="Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.">Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ustanavlivaem-i-nastraivaem-yii-proektiruem-strukturu-bazy-dannyx-chast-2.html" title="Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.">Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-client-side-app-structure-part-3.html" title="Personal Maps: главная страница и структура клиентской части приложения. Часть 3.">Personal Maps: главная страница и структура клиентской части приложения. Часть 3.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html" title="Personal Maps: создаём сервис AngularJS. Часть 4.">Personal Maps: создаём сервис AngularJS. Часть 4.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html" title="Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html" title="Personal Maps: контроллеры и представления в AngularJS. Часть 6.">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html" title="Personal maps: создаём директиву для подключения Google Maps. Часть 7.">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html" title="Personal maps: REST интерфейс. Часть 8.">Personal maps: REST интерфейс. Часть 8.</a></li>
<li class="serial-posts-list-item current-inactive">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html" title="Personal Maps: локализация и интернационализация. Часть 10">Personal Maps: локализация и интернационализация. Часть 10</a></li>
</ul>
</div><p>The post <a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html/feed</wfw:commentRss>
			<slash:comments>32</slash:comments>
		
		
			</item>
		<item>
		<title>Personal maps: REST интерфейс. Часть 8.</title>
		<link>https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html</link>
					<comments>https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Tue, 08 Oct 2013 08:45:57 +0000</pubDate>
				<category><![CDATA[Ajax]]></category>
		<category><![CDATA[AngularJS]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web разработка]]></category>
		<category><![CDATA[Yii]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1442</guid>

					<description><![CDATA[<p>Приветствую, это очередная статья о разработке web приложения с использованием под названием Personal maps. В прошлый раз мы закончили разработку клиентской части приложения, а сегодня займемся созданием REST интерфейса. Ссылки на все предыдущие части вы найдёте внизу этой страницы. Форматы запросов клиентской части к серверу и его ответов у нас уже определены. Фактически мы это...  <a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html" title="Read Personal maps: REST интерфейс. Часть 8.">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html">Personal maps: REST интерфейс. Часть 8.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Приветствую, это очередная статья о разработке web приложения с использованием под названием <strong>Personal maps</strong>. В <a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html">прошлый раз</a> мы закончили разработку клиентской части приложения, а сегодня займемся созданием <strong>REST интерфейса</strong>. Ссылки на все предыдущие части вы найдёте внизу этой страницы.</p>
<p>Форматы запросов клиентской части к серверу и его ответов у нас уже определены. Фактически мы это сделали когда <a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html">тестировали сервис Places</a>. Теперь нам нужно реализовать поддержку этих запросов серверной частью приложения.</p>
<h2>Создание REST сервисов с помощью Yii фреймворка</h2>
<p><em>Примечание</em>. Если вы не знакомы с общими принципами создания REST сервисов, то рекомендую прочитать статью <a href="http://www.gen-x-design.com/archives/create-a-rest-api-with-php/">Create a REST API with PHP</a>.</p>
<p><em>Также напоминаю,</em> что исходный код приложения размещён на GitHub и доступна демоверсия приложения.<br />
<span id="more-1442"></span><br />
<a href="https://github.com/vladimir-s/personal-maps" class="src-button">Source</a></p>
<a href="http://personal-maps.simplecoding.org" class="demo-button">Demo</a>
<p>Обычные web приложения работают с двумя типами HTTP запросов – <code>GET</code> и <code>POST</code>, т.к. вы не можете отправить другие типы запросов с помощью стандартной HTML формы. Но при использовании JavaScript ситуация изменяется. AngularJS, как и большинство других фреймворков, позволяет использовать дополнительные типы запросов, в нашем случае это <code>PUT</code> и <code>DELETE</code>.</p>
<p>Таким образом, для создания REST сервиса нам нужно:</p>
<ul>
<li>написать контроллер, который будет обрабатывать запросы;</li>
<li>создать правила для роутера.</li>
</ul>
<h2>Контроллер REST сервиса</h2>
<p>Прежде всего, давайте вспомним, какие запросы мы можем получить от клиентской части.</p>
<ul>
<li><code>GET api/places</code> – в ответ мы должны вернуть массив, содержащий все объекты данного пользователя;</li>
<li><code>GET api/places/id_объекта</code> – ищем объект с данным <code>id</code> и возвращаем только его;</li>
<li><code>POST api/places</code> – этот запрос указывает, что необходимо создать новый объект, информация о нём передаётся в параметрах запроса;</li>
<li><code>PUT api/places/id_объекта</code> – изменение объекта с указанным <code>id</code>, как и в случае с <code>POST</code> информация об объекте будет отправлена в параметрах запроса;</li>
<li><code>DELETE api/places/id_объекта</code> – этот запрос говорит о том, что необходимо удалить объект с указанным <code>id</code>.</li>
</ul>
<p>Т.е. мы можем создать обычный контроллер с методами, которые будут соответствовать этим запросам. Но, т.к. наш <code>REST</code> сервис должен отправлять ответы в json формате с правильными заголовками, то будет удобнее создать абстрактный базовый класс <code>RestController</code> и на его основе – контроллер сервиса.</p>
<p>В документации Yii есть хорошая статья о <a href="http://www.yiiframework.com/wiki/175/how-to-create-a-rest-api/">создании REST сервисов</a>. Я использовал приведённый в ней пример в качестве основы для <code>RestController</code>, но немного его упростил с учётом особенностей данного приложения.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">abstract class RestController extends Controller
{
    // Members
    /**
     * Default response format
     * either &#039;json&#039; or &#039;xml&#039;
     */
    private $format = &#039;json&#039;;
    /**
     * @return array action filters
     */
    public function filters()
    {
        return array();
    }

    // Actions
    abstract public function actionList();

    abstract public function actionView($id);

    abstract public function actionCreate();

    abstract public function actionUpdate($id);

    abstract public function actionDelete($id);

    protected function _sendResponse($status = 200, $body = &#039;&#039;, $content_type = &#039;text/html&#039;)
    {
        // set the status
        $status_header = &#039;HTTP/1.1 &#039; . $status . &#039; &#039; . $this-&gt;_getStatusCodeMessage($status);
        header($status_header);
        // and the content type
        header(&#039;Content-type: &#039; . $content_type);

        // pages with body are easy
        if($body != &#039;&#039;)
        {
            // send the body
            echo $body;
        }
        // we need to create the body if none is passed
        else
        {
            // create some body messages
            $message = &#039;&#039;;

            // this is purely optional, but makes the pages a little nicer to read
            // for your users.  Since you won&#039;t likely send a lot of different status codes,
            // this also shouldn&#039;t be too ponderous to maintain
            switch($status)
            {
                case 401:
                    $message = &#039;You must be authorized to view this page.&#039;;
                    break;
                case 404:
                    $message = &#039;The requested URL &#039; . $_SERVER[&#039;REQUEST_URI&#039;] . &#039; was not found.&#039;;
                    break;
                case 500:
                    $message = &#039;The server encountered an error processing your request.&#039;;
                    break;
                case 501:
                    $message = &#039;The requested method is not implemented.&#039;;
                    break;
            }

            // servers don&#039;t always have a signature turned on
            // (this is an apache directive &quot;ServerSignature On&quot;)
            $signature = ($_SERVER[&#039;SERVER_SIGNATURE&#039;] == &#039;&#039;) ? $_SERVER[&#039;SERVER_SOFTWARE&#039;] . &#039; Server at &#039; . $_SERVER[&#039;SERVER_NAME&#039;] . &#039; Port &#039; . $_SERVER[&#039;SERVER_PORT&#039;] : $_SERVER[&#039;SERVER_SIGNATURE&#039;];

            // this should be templated in a real-world solution
            $body = &#039;
&lt;!doctype html&gt;
&lt;html lang=&quot;en-US&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;&#039; . $status . &#039; &#039; . $this-&gt;_getStatusCodeMessage($status) . &#039;&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;&#039; . $this-&gt;_getStatusCodeMessage($status) . &#039;&lt;/h1&gt;
    &lt;p&gt;&#039; . $message . &#039;&lt;/p&gt;
    &lt;hr /&gt;
    &lt;address&gt;&#039; . $signature . &#039;&lt;/address&gt;
&lt;/body&gt;
&lt;/html&gt;&#039;;

            echo $body;
        }
        Yii::app()-&gt;end();
    }

    protected function _getStatusCodeMessage($status)
    {
        // these could be stored in a .ini file and loaded
        // via parse_ini_file()... however, this will suffice
        // for an example
        $codes = Array(
            200 =&gt; &#039;OK&#039;,
            400 =&gt; &#039;Bad Request&#039;,
            401 =&gt; &#039;Unauthorized&#039;,
            402 =&gt; &#039;Payment Required&#039;,
            403 =&gt; &#039;Forbidden&#039;,
            404 =&gt; &#039;Not Found&#039;,
            500 =&gt; &#039;Internal Server Error&#039;,
            501 =&gt; &#039;Not Implemented&#039;,
        );
        return (isset($codes[$status])) ? $codes[$status] : &#039;&#039;;
    }
}</pre>
<p>В строках 18-26 мы объявляем пять абстрактных методов, которые соответствуют всем нашим запросам. Т.е. класс, который будет наследовать <code>RestController</code> должен будет определить эти методы.</p>
<p>Также у нас есть два вспомогательных метода.</p>
<p><code>_getStatusCodeMessage</code> – в нём просто определены описания HTTP статусов, которые будут отправляться в заголовках ответов.</p>
<p><code>_sendResponse</code> – формирует ответ сервера, т.е. устанавливает HTTP заголовки и отправляет данные.</p>
<h3>Теперь создадим PlacesController</h3>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">class PlacesController extends RestController
{
    public function actionIndex() {
        if (Yii::app()-&gt;user-&gt;checkAccess(&#039;user&#039;)) {
            $this-&gt;render(&#039;index&#039;);
        }
        else {
            $this-&gt;redirect(array(&#039;site/login&#039;));
        }
    }

    public function actionList()
    {
        if (!Yii::app()-&gt;user-&gt;checkAccess(&#039;user&#039;)) {
            $this-&gt;_sendResponse(403);
            return;
        }
        //searching only for current users places (defaultScope returns appropriate condition)
        $places = Places::model()-&gt;findAll();
        echo CJSON::encode($places);
    }

    public function actionView($id)
    {
    }

    public function actionCreate()
    {
        if (!Yii::app()-&gt;user-&gt;checkAccess(&#039;user&#039;)) {
            $this-&gt;_sendResponse(403);
            return;
        }
        $data = CJSON::decode(file_get_contents(&#039;php://input&#039;));
        $place = new Places();
        $place-&gt;attributes = $data;
        if ($place-&gt;save()) {
            $this-&gt;_sendResponse(200, CJSON::encode($place));
        }
        else {
            $this-&gt;_sendResponse(500, CJSON::encode(array(
                &#039;message&#039;=&gt;&#039;Could not save place&#039;,
                &#039;errors&#039;=&gt;$place-&gt;getErrors(),
            )));
        }
    }

    public function actionUpdate($id)
    {
        $data = CJSON::decode(file_get_contents(&#039;php://input&#039;));
        $place = Places::model()-&gt;findByPk($id);
        if (!Yii::app()-&gt;user-&gt;checkAccess(&#039;user&#039;, array(&#039;place&#039;=&gt;$place))) {
            $this-&gt;_sendResponse(403);
            return;
        }
        if (null === $place) {
            $this-&gt;_sendResponse(404, CJSON::encode(array(&#039;message&#039;=&gt;&#039;Could not find place with id = &#039;.$id)));
        }
        $place-&gt;attributes = $data;
        if ($place-&gt;save()) {
            $this-&gt;_sendResponse(200, CJSON::encode($place));
        }
        else {
            $this-&gt;_sendResponse(500, CJSON::encode(array(
                &#039;message&#039;=&gt;&#039;Could not save place&#039;,
                &#039;errors&#039;=&gt;$place-&gt;getErrors(),
            )));
        }
    }

    public function actionDelete($id)
    {
        $place = Places::model()-&gt;findByPk($id);
        if (null === $place) {
            $this-&gt;_sendResponse(404, CJSON::encode(array(&#039;message&#039;=&gt;&#039;Could not find place with id = &#039;.$id)));
            return;
        }
        if (!Yii::app()-&gt;user-&gt;checkAccess(&#039;user&#039;, array(&#039;place&#039;=&gt;$place))) {
            $this-&gt;_sendResponse(403);
            return;
        }
        if ($place-&gt;delete()) {
            $this-&gt;_sendResponse(200, CJSON::encode($place));
        }
        else {
            $this-&gt;_sendResponse(500, CJSON::encode(array(
                &#039;message&#039;=&gt;&#039;Could not delete place&#039;,
                &#039;errors&#039;=&gt;$place-&gt;getErrors(),
            )));
        }
    }
}</pre>
<p>Этот класс наследует <code>RestController</code>, т.е. в нём мы должны определить все абстрактные методы (<code>actionList</code>, <code>actionView</code> и т.д.). Во всех этих методах мы в первую очередь с помощью <code>Yii::app()->user->checkAccess</code> проверяем права текущего пользователя и если проверка не пройдена, отправляем 403 ошибку.</p>
<p><em>Примечание</em>. Подробно систему разграничения доступа мы рассмотрим в следующий раз.</p>
<p>После этого, мы выполняем соответствующую операцию. Т.е. либо возвращаем список объектов (<code>actionList</code>), либо создаём, изменяем или удаляем указанную модель. Во всех методах <code>id</code> модели передаётся в первом параметре, т.к. его определяет роутер при разборе URL. Для получения остальных данных запроса используется функция <code>file_get_contents</code>.</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">$data = CJSON::decode(file_get_contents(&#039;php://input&#039;));</pre>
<p>Т.к. данные приходят в JSON формате, мы их декодируем с помощью класса <code>CJSON</code>.</p>
<p>Ещё одни момент касается обработки ошибок. Если у пользователя не достаточно прав для выполнения операции – отправляем 403-ю со стандартным сообщением (задано в методе <code>_getStatusCodeMessage</code> класса <code>RestController</code>).</p>
<p>Если модель не найдена (методы <code>actionUpdate</code> и <code>actionDelete</code>) – возвращаем 404-ю с собственным сообщением, т.к. по стандартному сложно понять, что именно не было найдено.</p>
<p>Наконец, если операцию выполнить не удалось, возвращаем 500-ую. В описание этих ошибок входят два параметра:<br />
<code>message</code> – содержит название операции, которую не удалось выполнить;<br />
<code>errors</code> – содержит массив с ошибками.</p>
<p>Проверить отправку этих ошибок вы можете следующим образом. Запустите приложение и дождитесь загрузки списка объектов. После этого остановите сервер базы данных и попробуйте изменить какой-нибудь объект. В форме вы увидите соответствующие ответы. И обратите внимание, что после того как вы снова запустите сервер базы, пользователь сможет продолжить работу без необходимости перезагружать страницу и не потеряет введённые данные.</p>
<p class="wp-caption"><script type="text/javascript"><!--
google_ad_client = "ca-pub-1195314499431105";
/* simplecoding content */
google_ad_slot = "2021931265";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="https://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</p>
<h2>Правила для роутера</h2>
<p>Формат запросов к API отличается от стандартного формата запросов Yii (имя_контроллера/имя_метода), поэтому добавим в файл в файл <code>config/main.php</code> правила для каждого из наших запросов.</p>
<p>Обратите внимание, что для каждого правила, мы указываем параметр <code>verb</code>. Это необходимо, т.к. шаблоны для запросов на просмотр, удаление и обновление объекта не отличаются ничем кроме типа (GET, PUT, DELETE).</p>
<pre class="brush: php; gutter: true; first-line: 1; highlight: []; html-script: false">&#039;components&#039;=&gt;array(
	...
	&#039;urlManager&#039;=&gt;array(
		&#039;urlFormat&#039;=&gt;&#039;path&#039;,
		&#039;rules&#039;=&gt;array(
			// REST patterns
			array(&#039;places/list&#039;, &#039;pattern&#039;=&gt;&#039;api/places&#039;, &#039;verb&#039;=&gt;&#039;GET&#039;),
			array(&#039;places/view&#039;, &#039;pattern&#039;=&gt;&#039;api/places/&lt;id:\d+&gt;&#039;, &#039;verb&#039;=&gt;&#039;GET&#039;),
			array(&#039;places/update&#039;, &#039;pattern&#039;=&gt;&#039;api/places/&lt;id:\d+&gt;&#039;, &#039;verb&#039;=&gt;&#039;PUT&#039;),
			array(&#039;places/delete&#039;, &#039;pattern&#039;=&gt;&#039;api/places/&lt;id:\d+&gt;&#039;, &#039;verb&#039;=&gt;&#039;DELETE&#039;),
			array(&#039;places/create&#039;, &#039;pattern&#039;=&gt;&#039;api/places&#039;, &#039;verb&#039;=&gt;&#039;POST&#039;),
			// regular patterns
			&#039;&lt;controller:\w+&gt;/&lt;id:\d+&gt;&#039;=&gt;&#039;&lt;controller&gt;/view&#039;,
			&#039;&lt;controller:\w+&gt;/&lt;action:\w+&gt;/&lt;id:\d+&gt;&#039;=&gt;&#039;&lt;controller&gt;/&lt;action&gt;&#039;,
			&#039;&lt;controller:\w+&gt;/&lt;action:\w+&gt;&#039;=&gt;&#039;&lt;controller&gt;/&lt;action&gt;&#039;,
		),
		&#039;showScriptName&#039;=&gt;false,
	),
	...
),</pre>
<p>Важно помнить, что роутер разбирает правила сверху вниз и использует первое же правило, которое соответствует данному запросу. В данном случае если мы добавим новые правила в конец списка, то запросы вроде <code>api/list</code> будут обрабатываться стандартными правилами и Yii попытается найти класс <code>ApiController</code> и вызвать его метод <code>actionList</code>.</p>
<p>Как видите, создание REST API особой сложности не представляет. Естественно, этот пример очень простой и в реальном приложении у вас будет гораздо больше методов. Кроме того, может возникнуть необходимость одновременно поддерживать несколько версий API. В таких случаях контроллер и правила роутера станут сложнее, но общий принцип останется тем же.</p>
<p>В следующей части мы рассмотрим разграничение прав пользователей с помощью RBAC.</p>
<p>Если есть вопросы или замечания, пишите. <strong>Успехов!</strong></p>
<h3>Содержание</h3>
<div id="serial-posts-wrapper">
<h3 class="serial-posts-heading"></h3>
<ul class="serial-posts">
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ispolzuem-yii-i-angularjs-dlya-razrabotki-web-prilozheniya-chast-1.html" title="Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.">Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ustanavlivaem-i-nastraivaem-yii-proektiruem-strukturu-bazy-dannyx-chast-2.html" title="Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.">Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-client-side-app-structure-part-3.html" title="Personal Maps: главная страница и структура клиентской части приложения. Часть 3.">Personal Maps: главная страница и структура клиентской части приложения. Часть 3.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html" title="Personal Maps: создаём сервис AngularJS. Часть 4.">Personal Maps: создаём сервис AngularJS. Часть 4.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html" title="Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html" title="Personal Maps: контроллеры и представления в AngularJS. Часть 6.">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html" title="Personal maps: создаём директиву для подключения Google Maps. Часть 7.">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</a></li>
<li class="serial-posts-list-item current-inactive">Personal maps: REST интерфейс. Часть 8.</li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html" title="Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html" title="Personal Maps: локализация и интернационализация. Часть 10">Personal Maps: локализация и интернационализация. Часть 10</a></li>
</ul>
</div><p>The post <a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html">Personal maps: REST интерфейс. Часть 8.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html/feed</wfw:commentRss>
			<slash:comments>21</slash:comments>
		
		
			</item>
		<item>
		<title>Personal maps: создаём директиву для подключения Google Maps. Часть 7.</title>
		<link>https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html</link>
					<comments>https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Sun, 29 Sep 2013 16:46:07 +0000</pubDate>
				<category><![CDATA[AngularJS]]></category>
		<category><![CDATA[HTML]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Web разработка]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1439</guid>

					<description><![CDATA[<p>В этой части мы рассмотрим разработку и использование директивы AngularJS, которая позволит добавить карту в интерфейс нашего приложения. Ссылки на предыдущие статьи вы найдете в конце страницы. Директивы в AngularJS предназначены для работы с DOM (Document Object Model – объектная модель документа) и позволяют создавать новые компоненты или добавлять существующим новые свойства. В Angular есть...  <a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html" title="Read Personal maps: создаём директиву для подключения Google Maps. Часть 7.">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>В этой части мы рассмотрим разработку и использование директивы <a href="http://angularjs.org/">AngularJS</a>, которая позволит добавить карту в интерфейс нашего приложения. Ссылки на предыдущие статьи вы найдете в конце страницы.</p>
<p>Директивы в <strong>AngularJS</strong> предназначены для работы с DOM (Document Object Model – объектная модель документа) и позволяют создавать новые компоненты или добавлять существующим новые свойства.</p>
<p>В Angular есть множество встроенных директив, часть из них мы уже использовали при создании представлений. Например, <code>ngHide</code> позволяет скрыть элемент в зависимости от значения модели, т.е. изменяет CSS стили элемента.</p>
<p>Тем не менее, наиболее впечатляющий эффект производит возможность создания собственных директив. Они позволяют значительно упростить разметку и, самое главное, создавать новые элементы, которые не входят в HTML.</p>
<p>Естественно, создание собственных директив сложнее, чем использование стандартных. Нужно понимать, как решить задачу, и учитывать требования AngularJS к директивам. Но при правильном использовании директивы упрощают разработку приложения, особенно если оно сложное и содержит компоненты с похожим поведением.<br />
<span id="more-1439"></span><br />
Возвращаемся к нашей задаче.</p>
<p><em>Напомню</em>. Исходный код приложения размещён на GitHub, также доступна демоверсия приложения.</p>
<a href="https://github.com/vladimir-s/personal-maps" class="src-button">Source</a>
<a href="http://personal-maps.simplecoding.org" class="demo-button">Demo</a>
<p>На главной странице нашего приложения нужно показать <strong>карту с объектами, добавленными пользователем</strong>. Карта должна реагировать на работу пользователя со списком объектов. Например, клик по названию объекта в списке должен приводить к отображению подробного описания этого объекта на карте. Т.е. наша директива должна реагировать на события приложения. Рассмотрим их подробнее.</p>
<h2>События, с которыми работает директива</h2>
<ul>
<li><code>places:updated</code> – это событие возникает когда сервис <a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html">Places</a> (описание в 4-ой части) получает обновлённый список объектов.
</li>
<li><code>place:updated</code> – возникает когда изменяются данные (координаты, заголовок или описание) какого-нибудь объекта.
</li>
<li><code>place:added</code> – возникает при создании нового объекта.
</li>
<li><code>place:show</code> – возникает, когда пользователь выбирает объект в списке.
</li>
<li><code>place:deleted</code> – возникает при удалении объекта.
</li>
<li><code>map:pointSelected</code> – это событие отправляет директива, когда пользователь кликает по карте.
</li>
</ul>
<p>Обработчикам всех событий кроме <code>places:updated</code> передается информация об объекте.</p>
<h2>Создание директивы</h2>
<p>Прежде всего, создаём файл с кодом директивы – <code>js/directives/pm-google-map.js</code></p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">app.directive(&#039;pmGoogleMap&#039;, function factory($window, $rootScope, Places) {
    return {
    ...
    }
});</pre>
<p>В первом параметре метода <code>directive</code> мы указываем название директивы. AngularJS использует специальное соглашение при формировании этих имён. При использовании названия директивы в HTML разметке, нужно преобразовать все заглавные буквы в прописные и поставить перед ними дефис. Т.е. добавить нашу директиву на страницу можно следующим образом:</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div class=&quot;span9 map&quot; pm-google-map&gt;</pre>
<p>Если вы хотите чтобы HTML валидатор не показывал ошибок, можно использовать один из следующих вариантов:</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div class=&quot;span9 map&quot; data-pm-google-map&gt;</pre>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div class=&quot;span9 map&quot; x-pm-google-map&gt;</pre>
<h3>Структура директивы</h3>
<p>Во втором параметре метода <code>directive</code> передаётся функция, которая получает список зависимостей (это компоненты, которые будут доступны внутри директивы) и должна вернуть JS объект, создающий саму директиву. Подробное описание этого объекта вы найдёте в официальной <a href="http://docs.angularjs.org/guide/directive">документации</a>, а сейчас мы рассмотрим только те параметры, которые нужны в данном случае.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">app.directive(&#039;pmGoogleMap&#039;, function factory($window, $rootScope, Places) {
    return {
        restrict: &#039;A&#039;,
        link: function(scope, element, attrs) {
        ...
        }
    }
});</pre>
<p>Параметр <code>restrict</code> указывает где именно можно использовать данную директиву. <code>A</code> означает <code>attribute</code>, т.е. атрибут любого элемента. Также можно использовать <code>E</code> (element), <code>C</code> (class) и <code>M</code> (comment). Например, если разрешить использование директивы в названии элемента</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">restrict: &#039;EA&#039;,</pre>
<p>то можно будет подключить директиву следующим образом</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;pm-google-map class=&quot;span9 map&quot;&gt;</pre>
<p>Для нашего приложения принципиальной разницы нет, но для директив вроде <code>ngHide</code> использование в качестве элемента смысла не имеет.</p>
<p>Параметр <code>link</code> получает функцию, которая выполняет работу с DOM и обработку событий. Эта функция автоматически вызывается при создании директивы. И в нашем случае весь код директивы находится именно в этой функции.</p>
<p>Полностью код директивы вы можете посмотреть на <a href="https://github.com/vladimir-s/personal-maps/blob/master/public_html/js/directives/pm-google-map.js">GitHub</a>, а в этой статье мы будем разбирать его по частям.</p>
<h2>Инициализация карты</h2>
<p>Для данного приложения я выбрал <a href="https://developers.google.com/maps/documentation/javascript/?hl=ru">Google Maps</a>, но точно также можно было использовать и <a href="http://api.yandex.ru/maps/">Яндекс.Карты</a>. Кстати, если вы захотите подключить Яндекс.Карты, то заменить придётся только данную директиву. Все остальные компоненты приложения останутся без изменений.</p>
<p><em>Примечание</em>. Существуют готовые директивы для подключения Google maps, например, <a href="http://nlaplante.github.io/angular-google-maps/">эта</a>. Но в данном случае, удобнее создать собственную директиву, которая будет реагировать на события данного приложения. Тем более, что API карт удобный.</p>
<p>Прежде всего, функция <code>link</code> должна инициализировать карту.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">link: function(scope, element, attrs) {
	function setMapHeight() {
		var mapHeight = $window.innerHeight - $(&#039;header&#039;).height() - $(&#039;footer&#039;).height() - 20;
		element.css(&#039;height&#039;, mapHeight);
	}
	setMapHeight();

	scope.map = null;

	function initialize() {
		var defaults = {
			center: new google.maps.LatLng(-34.397, 150.644),
			zoom: 8,
			panControl: true,
			zoomControl: true,
			scaleControl: true,
			mapTypeId: google.maps.MapTypeId.ROADMAP
		};
		scope.map = new google.maps.Map(element[0], defaults);
	}
	if (scope.map === null) {
		initialize();
	}
	
	...
	
}</pre>
<p>С помощью функции <code>setMapHeight</code> мы устанавливаем высоту карты. Здесь используется сервис <code>$window</code>, который является ссылкой на стандартный объект <code>window</code>. Но т.к. использование <code>window</code> вызовет проблемы при тестировании, использовать рекомендуется именно <code>$window</code>.</p>
<p>Затем с помощью <code>initialize</code> мы создаём карту. Код практически совпадает с примерами в документации к Google Maps API. Но обратите внимание на следующие моменты:</p>
<ol>
<li>Для директивы автоматически создаётся собственный объект <code>scope</code>. Его назначение и использование мы рассматривали в <a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html">предыдущей части</a>. В данном случае мы используем его для хранения ссылок на карту и маркеры.</li>
<li>Во втором параметре функция <code>link</code> получает ссылку на объект, для которого установлена директива. Нам он нужен для того, чтобы установить высоту карты так, чтобы она занимала большую часть окна приложения.</li>
</ol>
<h2>Обработка событий</h2>
<p>Рассмотрим обработку события <code>places:updated</code></p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">function getDescription(place) {
	return &#039;&lt;h4&gt;&#039; + place.p_title + &#039;&lt;/h4&gt;&#039; + markdown.toHTML(place.p_description);
}

function setMarkerOnClickEvent(marker) {
	google.maps.event.addListener(marker, &#039;click&#039;, function(event) {
		var place = Places.get(marker.pid);
		var latlng = new google.maps.LatLng(place.p_lat, place.p_lng);
		infoBox.setContent(getDescription(place));
		infoBox.setPosition(latlng);
		infoBox.open(scope.map);
	});
}

scope.markers = [];
var infoBox = new google.maps.InfoWindow();

$rootScope.$on(&#039;places:updated&#039;, function() {
	scope.markers = [];
	infoBox.close();
	var bounds = new google.maps.LatLngBounds();
	angular.forEach(Places.getAll(), function(place) {
		var latlng = new google.maps.LatLng(place.p_lat, place.p_lng);
		bounds.extend(latlng);
		var marker = new google.maps.Marker({
			position: latlng,
			map: scope.map,
			title: place.p_title,
			pid: place.id
		});
		setMarkerOnClickEvent(marker);
		scope.markers.push(marker);
	});
	scope.map.fitBounds(bounds);
});</pre>
<p>Сам обработчик устанавливается в строке 18, но для его работы нам нужна пара дополнительных функций.</p>
<p><code>getDescription</code> – формирует описание объекта. Наше приложение позволяет пользователям вводить описание объектов с использованием синтаксиса markdown. Для этих целей мы используем библиотеку <a href="https://github.com/evilstreak/markdown-js">markdown-js</a>. Преобразование из markdown в HTML выполняется с помощью метода <code>markdown.toHTML</code> (строка 2).</p>
<p><code>setMarkerOnClickEvent</code> – устанавливает обработчик события click для маркеров объектов. Клик по маркеру центрирует карту и открывает <code>infoBox</code> (<code>google.maps.InfoWindow</code>) в котором отображается название и описание объекта.</p>
<p>Появление события <code>places:updated</code> означает, что сервис <code>Places</code> получил новый список объектов, поэтому нам необходимо сформировать новый массив с маркерами и показать его на карте. Для этого, мы получаем с помощью <code>Places.getAll()</code> новый список объектов (строка 22), для каждого из них создаём маркер, устанавливаем для него обработчики событий и показываем их на карте (строки 23-32).</p>
<p>Затем позиционируем карту таким образом, чтобы были видны все маркеры (строка 34).</p>
<p>Принцип работы обработчиков событий <code>place:updated</code>, <code>place:added</code> и <code>place:deleted</code> очень похож. Все их обработчики во втором параметре получают информацию об объекте, для которого нужно выполнить соответствующую операцию (создать, удалить или изменить). Рассмотрим в качестве примера обработчик события <code>place:updated</code>.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$rootScope.$on(&#039;place:updated&#039;, function(event, place) {
	infoBox.close();
	angular.forEach(scope.markers, function(marker) {
		if (marker.pid == place.id) {
			var latlng = new google.maps.LatLng(place.p_lat, place.p_lng);
			marker.setTitle(place.p_title);
			marker.setPosition(latlng);
			scope.map.setCenter(latlng);
			infoBox.setContent(getDescription(place));
			infoBox.setPosition(latlng);
			infoBox.open(scope.map);
			return false;
		}
	});
});</pre>
<p>Т.к. каждому объекту соответствует маркер на карте, мы в цикле перебираем все маркеры и ищем тот, у которого атрибут <code>pid</code> совпадает с <code>id</code> объекта. Если маркер найден, устанавливаем для него новые параметры и открываем <code>infoBox</code>.</p>
<p>Обработчики остальных методов вы найдете на <a href="https://github.com/vladimir-s/personal-maps/blob/master/public_html/js/directives/pm-google-map.js">GitHub</a>.</p>
<p>Последний шаг в создании директивы – установка обработчика события <code>click</code> карты.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">google.maps.event.addListener(scope.map, &#039;click&#039;, function(event) {
	scope.$apply(function() {
		$rootScope.$broadcast(&#039;map:pointSelected&#039;, {
			p_lat: Math.round(event.latLng.lat() * 100) / 100,
			p_lng: Math.round(event.latLng.lng() * 100) / 100
		});
	});
});</pre>
<p>Этот обработчик выполняет только одну операцию – отправляет событие <code>map:pointSelected</code> с координатами точки, по которой кликнул пользователь. Т.к. вызов обработчика происходит вне контекста AngularJS, необходимо обернуть отправку события в вызов <code>scope.$apply</code>. Если с этим моментом возникают вопросы, советую почитать статью <a href="http://jimhoskins.com/2012/12/17/angularjs-and-apply.html">AngularJS and scope.$apply</a>.</p>
<p class="wp-caption"><script type="text/javascript"><!--
google_ad_client = "ca-pub-1195314499431105";
/* simplecoding content */
google_ad_slot = "2021931265";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="https://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</p>
<h2>Тестирование директивы</h2>
<p>Как вы, наверное, помните из предыдущих частей, непосредственно тестирование компонентов AngularJS достаточно простое. Основная проблема заключается в подготовке «окружения» перед выполнением теста.</p>
<p>Решается эта задача с помощью функции <code>beforeEach</code>, которая вызывается перед выполнением каждого теста, и в ней нужно создать все необходимые объекты.</p>
<p>Тесты для нашей директивы не должны зависеть от других компонентов приложения. Поэтому мы должны создать mock объект для сервиса <code>Places</code>.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">beforeEach(module(&#039;personalmaps&#039;, function ($provide) {
	var Places = {
		&#039;get&#039;: function() {},
		&#039;getAll&#039;: function() {},
		&#039;add&#039;: function() {},
		&#039;update&#039;: function() {},
		&#039;delete&#039;: function() {},
		&#039;save&#039;: function() {}
	};
	spyOn(Places, &#039;getAll&#039;).andReturn([
		{
			id: 1,
			p_title: &#039;111&#039;,
			p_description: &#039;test 1&#039;,
			p_lat: 0,
			p_lng: 0
		},
		{
			id: 2,
			p_title: &#039;222&#039;,
			p_description: &#039;test 2&#039;,
			p_lat: 10,
			p_lng: 10
		}
	]);
	$provide.provider(&#039;Places&#039;, {
		$get: function() {
			return Places;
		}
	});
	$provide.value(&#039;lang&#039;, &#039;en_us&#039;);
}));</pre>
<p>Этот объект содержит те же самые методы, что и настоящий сервис, но они ничего не выполняют (строки 3-8). Также с помощью функции <code>spyOn</code> мы настраиваем отслеживание вызова метода <code>getAll</code> и формируем массив с данными, которые он должен вернуть (строки 10-25).</p>
<p>Затем мы должны сделать так, чтобы наша директива подключила новый объект через механизм внедрения зависимостей (DI – dependency injection). Для этого используется сервис <a href="http://docs.angularjs.org/api/AUTO.$provide">$provide</a>. Мы вызываем метод <code>provider</code> и передаём ему название сервиса – <code>Places</code>, и объект, содержащий метод <code>$get</code>. Этот метод будет автоматически вызван сервисом <code>$injector</code>, который управляет внедрением зависимостей.</p>
<p>Т.к. в списке зависимостей директивы указан сервис <code>Places</code>, <code>$injector</code> найдёт соответствующий провайдер и попытается получить объект с помощью метода <code>$get</code>. В результате <code>$injector</code> получит наш объект.</p>
<p>У директив есть ещё одна особенность – они «привязаны» к разметке страницы. Во время выполнения теста страницы как таковой у нас нет, поэтому мы должны создать тег вручную и «привязать» нашу директиву к нему. Сделать это можно следующим образом:</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">beforeEach(inject(function($injector, $rootScope, $compile, Places) {

	spyOn($rootScope, &#039;$broadcast&#039;);

	elm = angular.element(
		&#039;&lt;div class=&quot;span9 map&quot; pm-google-map&gt;&lt;/div&gt;&#039;
	);

	scope = $rootScope;
	$compile(elm)(scope);
	scope.$digest();

	spyOn(scope.map, &#039;setCenter&#039;);
}));</pre>
<p><em>Примечание</em>. Функцию <code>beforeEach</code> можно использовать несколько раз.</p>
<p>Обратите внимание на строки 5-7. В них мы создаём точно такой же тег <code>div</code>, как и на странице приложения. Затем компилируем его и передаём в качестве аргумента <code>scope</code> (строка 10). Эти же самые операции выполняет AngularJS при инициализации приложения.</p>
<p>Ещё один момент. Мы отслеживаем вызов метода <code>setCenter</code> (строка 13). Именно по этой причине у нас карта присвоена свойству объекта <code>scope</code>. Если бы в функции link мы записали <code>var map = ...</code>, то отследить центрирование карты было бы невозможно.</p>
<p>Теперь в качестве примера рассмотрим один из тестов</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">it(&#039;should call map.setCenter on place:show&#039;, inject(function($compile, $rootScope) {
	$rootScope.$emit(&#039;place:show&#039;, {
		id: 1,
		p_title: &#039;111&#039;,
		p_description: &#039;test&#039;,
		p_lat: 0,
		p_lng: 0
	});
	expect(scope.map.setCenter).toHaveBeenCalled();
}));</pre>
<p>Здесь мы инициируем событие, которое при работе приложения отправляется, когда пользователь кликает на название объекта в списке. Мы проверяем, что при возникновении этого события директива отцентрирует карту.</p>
<p>Ещё один пример.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">it(&#039;should remove marker from array on place:deleted&#039;, inject(function($compile, $rootScope) {
	$rootScope.$emit(&#039;places:updated&#039;);
	$rootScope.$emit(&#039;place:deleted&#039;, {id: 1});
	expect(scope.markers.length).toBe(1);
}));</pre>
<p>Здесь мы проверяем, что событие <code>place:deleted</code> приводит к уменьшению количества маркеров. Естественно, перед тем как отправлять <code>place:deleted</code> нужно отправить <code>places:updated</code>, чтобы массив маркеров не был пустым.</p>
<p>Остальные тесты работают по такому же принципу, посмотреть вы их можете в файле <a href="https://github.com/vladimir-s/personal-maps/blob/master/public_html/protected/tests/unit/js/directives/pm-google-map.spec.js">pm-google-map.spec.js</a>.</p>
<p>В следующей части цикла мы вернёмся к разработке серверной части приложения и рассмотрим создания REST API.</p>
<h3>Содержание</h3>
<div id="serial-posts-wrapper">
<h3 class="serial-posts-heading"></h3>
<ul class="serial-posts">
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ispolzuem-yii-i-angularjs-dlya-razrabotki-web-prilozheniya-chast-1.html" title="Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.">Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ustanavlivaem-i-nastraivaem-yii-proektiruem-strukturu-bazy-dannyx-chast-2.html" title="Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.">Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-client-side-app-structure-part-3.html" title="Personal Maps: главная страница и структура клиентской части приложения. Часть 3.">Personal Maps: главная страница и структура клиентской части приложения. Часть 3.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html" title="Personal Maps: создаём сервис AngularJS. Часть 4.">Personal Maps: создаём сервис AngularJS. Часть 4.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html" title="Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html" title="Personal Maps: контроллеры и представления в AngularJS. Часть 6.">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</a></li>
<li class="serial-posts-list-item current-inactive">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html" title="Personal maps: REST интерфейс. Часть 8.">Personal maps: REST интерфейс. Часть 8.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html" title="Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html" title="Personal Maps: локализация и интернационализация. Часть 10">Personal Maps: локализация и интернационализация. Часть 10</a></li>
</ul>
</div><p>The post <a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html/feed</wfw:commentRss>
			<slash:comments>11</slash:comments>
		
		
			</item>
		<item>
		<title>Personal Maps: контроллеры и представления в AngularJS. Часть 6.</title>
		<link>https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html</link>
					<comments>https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Sun, 22 Sep 2013 18:00:55 +0000</pubDate>
				<category><![CDATA[AngularJS]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Web разработка]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1435</guid>

					<description><![CDATA[<p>Эта статья шестая в цикле о создании небольшого web приложения под названием Personal Maps. Ссылки на все предыдущие части вы найдёте внизу страницы, а сейчас мы продолжим разработку клиентской части приложения и займёмся созданием контроллеров и представлений. Когда я начинал разрабатывать более-менее сложные приложения на JavaScript, создание этих компонентов было одним из самых сложных моментов....  <a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html" title="Read Personal Maps: контроллеры и представления в AngularJS. Часть 6.">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Эта статья шестая в цикле о создании небольшого web приложения под названием <strong>Personal Maps</strong>. Ссылки на все предыдущие части вы найдёте внизу страницы, а сейчас мы продолжим разработку клиентской части приложения и займёмся созданием контроллеров и представлений.</p>
<p>Когда я начинал разрабатывать более-менее сложные приложения на JavaScript, создание этих компонентов было одним из самых сложных моментов. Проблема была в том, что при создании клиентской части я автоматически пытался применять подходы, которые используются на сервере, и из-за этого возникали проблемы. Сбивало то, что и <strong>PHP</strong>, и <strong>JavaScript</strong> фреймворки используют модели, представления и контроллеры (MVC архитектура), но архитектура всего приложения при этом отличается.</p>
<p>Когда серверный фреймворк обрабатывает запрос, он с помощью роутера находит нужный метод (action) контроллера и вызывает его. В этом методе мы обрабатываем данные с помощью модели и выводим результат с помощью представления. Для следующего запроса процесс повторяется.</p>
<p>Но JavaScript фреймворк не получает HTTP запросов. <span id="more-1435"></span>Приложение создаётся один раз и работает до тех пор, пока пользователь не перезагрузит страницу или не закроет браузер. Т.е. на странице могут одновременно работать несколько контроллеров, каждый из которых отвечает за свою часть страницы. При этом взаимодействие между ними может быть организовано как с помощью событий, так и напрямую (один контроллер вызывает методы другого). Естественно, последний способ использовать не желательно, т.к. он увеличивает количество зависимостей в приложении. И изменение одного из контроллеров потребует переписывания кода в других.</p>
<p>Кроме того, есть ещё специфика конкретного JS фреймворка. Если в большинстве PHP фреймворков создание контроллеров, моделей и представлений практически не отличается, то разница между созданием этих же компонентов, например, в <a href="http://backbonejs.org/">Backbone</a> и <a href="http://angularjs.org/">AngularJS</a> очень заметна.</p>
<p>Поэтому, прежде чем переходить к коду нашего приложения, рассмотрим общий принцип создания MVC компонентов в AngularJS.</p>
<p><em>Примечание</em>. Исходный код размещён на GitHub, также доступна демоверсия приложения.</p>
<a href="https://github.com/vladimir-s/personal-maps" class="src-button">Source</a>
<a href="http://personal-maps.simplecoding.org" class="demo-button">Demo</a>
<h2>Создание контроллеров в AngularJS</h2>
<p>Фактически контроллером может быть любая JavaScript функция. Достаточно её объявить и указать её название в атрибуте <code>ng-controller</code> какого-нибудь тега.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">function MyController() { ... }
</pre>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div ng-controller=&quot;MyController&quot;&gt;...&lt;/div&gt;</pre>
<p>Эту форму записи не желательно использовать в реальных приложениях, но, тем не менее, она вполне рабочая. Ниже мы рассмотрим другой способ создания контроллера, а сейчас нам важно разобраться, что делает AngularJS.</p>
<p>Для каждого контроллера фреймворк создаёт так называемый <a href="http://docs.angularjs.org/guide/scope">Scope</a>. Это объект, который ссылается на модель приложения. Контроллер нужен для того, чтобы инициализировать <code>Scope</code> и добавить ему «поведения» (например, обработчики событий).</p>
<p>Передача данных между контроллером, моделью и представлениями, в том числе и двунаправленное связывание данных, происходит именно с помощью объекта <code>scope</code>.</p>
<h2>Модели в AngularJS</h2>
<p>В AngularJS моделями являются любые данные, которые хранятся в свойствах объекта <code>scope</code>.</p>
<p>Т.к. scope сам по себе является JavaScript объектом, то его свойствам можно присвоить любые другие объекты любой сложности. Например,</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$scope.hello = &#039;Привет&#039;;</pre>
<p>или</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$scope.delete = function(id) { ... };</pre>
<h2>Представления в AngularJS</h2>
<p>Представлением в AngularJS является <strong>DOM</strong> (Document Object Model – объектная модель документа) или её часть. Когда мы подключаем контроллер, то указываем для какого-нибудь тега в разметке страницы атрибут <code>ng-controller</code> или <code>ng-view</code>. Этот тег и всё его содержимое и являются представлением для данного контроллера.</p>
<p>Представление может содержать выражения, заключённые в двойные фигурные скобки. AngularJS обрабатывает эти выражения, и показывает пользователю результат. Например,</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div&gt;{{ hello }}&lt;/div&gt;</pre>
<p>будет преобразовано в</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div&gt;Привет&lt;/div&gt;</pre>
<p>При этом в контроллере мы должны установить значение свойства <code>hello</code> для объекта <code>$scope</code>. Указывать <code>$scope</code> в представлении не нужно.</p>
<p>Таким образом, представление «знает» о контроллере за счёт атрибутов <code>ng-controller</code> или <code>ng-view</code> и может получать значение атрибутов объекта <code>scope</code>.</p>
<h2>Роутер в AngularJS</h2>
<p>Роутер является не обязательным компонентом. Он позволяет переключать контроллеры в зависимости от текущего значения URL, точнее той его части, которая идёт после символа <code>#</code>.</p>
<p>В нашем приложении центральная часть страницы разделена на две области. В левой находится карта, она должна отображаться постоянно. А в правой – либо список объектов, либо форма создания нового объекта.</p>
<img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2013/09/5_main_page_structure.png" alt="main_page_structure" width="682" height="362" class="alignnone size-full wp-image-1426" srcset="https://www.simplecoding.org/wp-content/uploads/2013/09/5_main_page_structure.png 682w, https://www.simplecoding.org/wp-content/uploads/2013/09/5_main_page_structure-450x238.png 450w" sizes="auto, (max-width: 682px) 100vw, 682px" />
<p>И список объектов, и форма, друг от друга не зависят, поэтому удобно для каждого из этих компонентов использовать свои собственные контроллер и представления. При этом, нам нужно переключаться между этими компонентами. Для решения этой задачи и предназначен роутер.</p>
<p>Нам нужно, чтобы:</p>
<ul>
<li>для <code>#/list</code> отображался список объектов;</li>
<li>для <code>#/add</code> отображалась форма создания нового объекта;</li>
<li>а для <code>#/edit/id</code> отображалась форма редактирования объекта, который имеет указанный <code>id</code>.</li>
</ul>
<h3>Настроим роутер (файл app.js)</h3>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">var app = angular.module(&#039;personalmaps&#039;, [&#039;ui.bootstrap&#039;, &#039;pascalprecht.translate&#039;])
    .value(&#039;lang&#039;, lang);

app.config([&#039;$routeProvider&#039;, function($routeProvider) {
    $routeProvider.when(&#039;/list&#039;, {
        templateUrl: &#039;partials/list.html&#039;,
        controller: &#039;PlacesListController&#039;
    });
    $routeProvider.when(&#039;/add&#039;, {
        templateUrl: &#039;partials/form.html&#039;,
        controller: &#039;PlacesFormController&#039;
    });
    $routeProvider.when(&#039;/edit/:placeId&#039;, {
        templateUrl: &#039;partials/form.html&#039;,
        controller: &#039;PlacesFormController&#039;
    });
    $routeProvider.otherwise({
        redirectTo: &#039;/list&#039;
    });
}]);</pre>
<p>Как видите, после создания приложения (строка 1) мы вызываем метод <code>config</code>, которому передаём функцию, настраивающую роутер.</p>
<p>В первом параметре эта функция получает <code>$routeProvider</code> – объект, который используется для настройки сервиса <code>$route</code> (стандартный сервис <strong>AngularJS</strong>).</p>
<p>Для каждого варианта URL, который нам нужно обрабатывать, вызываем метод <code>when</code>, в котором передаём два параметра:</p>
<ol>
<li>Шаблон URL. Если URL содержит параметры, то перед ними нужно поставить двоеточие. Для каждого параметра создаётся свойство в объекте <code>$routeParams</code>. Например, <code>$routeParams.placeId</code>.
</li>
<li>Хеш с настройками. <code>templateUrl</code> содежрит URL шаблона, а <code>controller</code> – имя контроллера.</li>
</ol>
<p>Метод <code>$routeProvider.otherwise</code> позволяет установить параметры для всех остальных вариантов URL.</p>
<p>Для того чтобы роутер заработал нам нужно выполнить ещё один шаг – указать область на странице, для которой будут устанавливаться контроллеры. Делается это с помощью директивы <code>ng-view</code>. В нашем случае разметка выглядит так:</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div class=&quot;row-fluid&quot;&gt;
    &lt;div class=&quot;span9 map&quot; pm-google-map&gt;&lt;/div&gt;

    &lt;div class=&quot;span3&quot; ng-view&gt;&lt;/div&gt;
&lt;/div&gt;</pre>
<p>Роутер заменит <code>ng-view</code> на соответствующий контроллер и вставит шаблон внутрь данного тега. Например, для URL <code>#/list</code> мы получим что-то вроде:</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div class=&quot;span3&quot; ng-controller=&quot;PlacesListController&quot;&gt;… содержимое шаблона partials/list.html …&lt;/div&gt;</pre>
<p>Теперь нам нужно написать контроллеры для списка и формы.</p>
<h2>Контроллер списка</h2>
<p>Файл <code>js/controllers/list.js</code></p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">app.controller(&#039;PlacesListController&#039;
    , [&#039;$scope&#039;, &#039;$rootScope&#039;, &#039;Places&#039;, &#039;$dialog&#039;, &#039;lang&#039;
    , function($scope, $rootScope, Places, $dialog, lang) {

    $scope.curLang = lang;

    $scope.places = Places.getAll();

    $rootScope.$on(&#039;places:updated&#039;, function() {
        $scope.places = Places.getAll();
    });

    $scope.isEmpty = function() {
        if ($scope.places.length === 0) {
            return true;
        }
        return false;
    }

    $scope.confirm = function(place) {
        var title = &#039;Confirm&#039;;
        var msg = &#039;Do you really want to delete this place?&#039;;
        var btns = [{result:&#039;no&#039;, label: &#039;No&#039;}, {result:&#039;yes&#039;, label: &#039;Yes&#039;, cssClass: &#039;btn-danger&#039;}];

        $dialog.messageBox(title, msg, btns)
            .open()
            .then(function(result){
                if (result === &#039;yes&#039;) {
                    $scope.delete(place);
                }
            });
    }

    $scope.delete = function(place) {
        Places.delete(place);
    }

    $scope.show = function(place) {
        $rootScope.$broadcast(&#039;place:show&#039;, place);
    }
}]);</pre>
<p>Здесь для создания контроллера используется метод <code>controller</code>, который получает название контроллера, список зависимостей и функцию, которая и создаёт контроллер. Т.е. контроллер в любом случае создаётся с помощью JS функции, но в отличие от первого варианта, показанного в начале статьи, данный контроллер находится внутри модуля приложения и не «засоряет» глобальное пространство имён.</p>
<p>В списке зависимостей мы указали:</p>
<ul>
<li>Сервис <code>Places</code>, который создали в <a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html">4-ой части</a>. Напомню, этот сервис выполняет всю работу по взаимодействию с серверной частью приложения. И с помощью его методом мы можем прочитать, изменить или создать объект.
</li>
<li>Объект <code>$scope</code> – его автоматически создаёт AngularJS и через него осуществляется передача данных в представление.
</li>
<li><code>$rootScope</code> – глобальный scope, который мы используем для отправки и получения событий.</li>
<li><code>$dialog</code> – <a href="http://angular-ui.github.io/bootstrap/#/modal">сторонний компонент</a>, который мы используем для создания модального окна. Используется для подтверждения удаления объекта.
</li>
<li><code>lang</code> – содержит название текущего языка.
</li>
</ul>
<p>Т.к. контроллер отвечает за отображение списка объектов, то мы читаем их с помощью метода <code>Places.getAll()</code> и присваиваем свойству <code>$scope.places</code> (чтобы к ним могло получить доступ представление).</p>
<p>Когда список объектов изменяется, сервис <code>Places</code> инициирует событие <code>places:updated</code>. Поэтому мы добавляем соответствующий обработчик – строки 9-11.</p>
<p>И нам нужно несколько методов, которые будет вызывать представление.</p>
<ul>
<li>$scope.isEmpty – проверяет, является ли список объектов пустым.
</li>
<li>$scope.confirm – открывает диалог с просьбой подтвердить удаление объекта. И если подтверждение получено, вызывает метод $scope.delete.
</li>
<li>$scope.delete – вызывает Places.delete для удаления объекта.
</li>
<li>$scope.show – отправляет событие place:show, уведомляющее другие компоненты о том, что пользователь кликнул на объекте.</li>
</ul>
<h3>Теперь рассмотрим шаблон partials/list.html</h3>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;div id=&quot;placesList&quot;&gt;

    &lt;ul&gt;
        &lt;li ng-repeat=&quot;place in places&quot;&gt;
            &lt;a href=&quot;&quot; ng-click=&quot;show(place)&quot;&gt;{{ place.p_title }}&lt;/a&gt;
            &lt;a href=&quot;&quot; ng-click=&quot;confirm(place)&quot; class=&quot;pull-right&quot; rel=&quot;tooltip&quot; title=&quot;{{ &#039;DELETE&#039; | translate }}&quot;&gt;&lt;i class=&quot;icon-trash&quot;&gt;&lt;/i&gt;&lt;/a&gt;
            &lt;a href=&quot;places/index#/edit/{{place.id}}&quot; class=&quot;pull-right&quot; rel=&quot;tooltip&quot; title=&quot;{{ &#039;UPDATE&#039; | translate }}&quot;&gt;&lt;i class=&quot;icon-pencil&quot;&gt;&lt;/i&gt;&lt;/a&gt;
        &lt;/li&gt;
    &lt;/ul&gt;

    &lt;span ng-show=&quot;isEmpty()&quot;&gt;{{ &#039;NO_PLACES&#039; | translate }}&lt;/span&gt;

    &lt;div&gt;
        &lt;a href=&quot;places/index#/add&quot; class=&quot;btn btn-success&quot;&gt;{{ &#039;ADD_PLACE&#039; | translate }}&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;</pre>
<p>Для вывода списка объектов используем директиву <code>ng-repeat</code> (аналог <code>foreach</code> цикла). В качестве её значения указываем <code>place in places</code>. Значение <code>places</code> – это массив, который мы сохранили в <code>$scope</code> при инициализации контроллера.</p>
<p>Сразу после старта приложения <code>$scope.places</code> будет пуст, т.к. сервис <code>Places</code> вряд ли успеет получить от сервера ответ. Поэтому список будет пустым, и мы увидим соответствующее сообщение (строка 11). Обратите внимание на директиву <code>ng-show</code>. Она устанавливает CSS правило <code>display</code> в зависимости от результата, который возвращает функция <code>isEmpty</code>.</p>
<p>Как только сервис <code>Places</code> получит ответ сервера, он инициирует событие <code>places:updated</code>. Наш контроллер «слушает» это событие и как только оно появляется, ещё раз вызывает метод <code>getAll</code> сервиса. Т.е. в <code>$scope.places</code> будет сохранён массив с объектами. Напомню, <code>$scope.places</code> является моделью и AngularJS отслеживает изменения в её состоянии. Когда <code>$scope.places</code> изменяется, автоматически запускается рендеринг представления, связанного с данным контроллером. В результате будет сформирован список объектов с кнопками «Изменить» и «Удалить».</p>
<p>Обратите внимание на то, как формируется ссылка «Изменить» (строка 7). В атрибуте <code>href</code> мы указываем <code>places/index#/edit/{{place.id}}</code>, где <code>place.id</code> – id текущего объекта. В результате, клик по этой ссылке приведёт к срабатыванию роутера, который загрузит <code>PlacesFormController</code> с соответствующим шаблоном. Аналогично работает и клик по кнопке «Создать объект».</p>
<p class="wp-caption"><script type="text/javascript"><!--
google_ad_client = "ca-pub-1195314499431105";
/* simplecoding content */
google_ad_slot = "2021931265";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="https://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</p>
<h2>Контроллер формы</h2>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">app.controller(&#039;PlacesFormController&#039;
    , [&#039;$scope&#039;, &#039;$rootScope&#039;, &#039;Places&#039;, &#039;$routeParams&#039;, &#039;$location&#039;
    , function($scope, $rootScope, Places, $routeParams, $location) {

    $scope.place = {};
    $scope.isNew = true;
    $scope.saving = false;

    $scope.showErrors = false;
    $scope.errors = [];

    if ($routeParams.placeId !== undefined) {
        $scope.place = Places.get($routeParams.placeId);
        if (undefined === $scope.place || null === $scope.place) {
            //place with this id not found
            $location.path(&#039;/add&#039;).replace();
        }
        $scope.isNew = false;
    }

    $scope.save = function() {
        Places.save($scope.place);
        $scope.saving = true;
        $scope.showErrors = false;
    }

    $rootScope.$on(&#039;place:updated&#039;, function() {
        $scope.saving = false;
    });

    $rootScope.$on(&#039;place:added&#039;, function(event, data) {
        $scope.saving = false;
        $location.path(&#039;/list&#039;).replace();
    });

    $rootScope.$on(&#039;place:error&#039;, function(event, data) {
        $scope.showErrors = true;
        $scope.errors = [];
        angular.forEach(data.errors, function(error) {
            if (typeof error == &#039;object&#039;) {
                angular.forEach(error, function(err) {
                    $scope.errors.push(err);
                });
            }
            else {
                $scope.errors.push(error);
            }
        });
        $scope.saving = false;
    });

    $rootScope.$on(&#039;map:pointSelected&#039;, function(event, data) {
        $scope.place.p_lat = data.p_lat;
        $scope.place.p_lng = data.p_lng;
    });
}]);</pre>
<p>Форма может использоваться как для создания нового объекта, так и для редактирования существующего. Модель объекта храниться в <code>$scope.place</code>. Во время инициализации контроллер проверяет, указан ли <code>id</code> объекта и пытается его получить (строки 12-19). Если объект найден, <code>$scope.isNew</code> будет равен <code>false</code>.</p>
<p>Процесс сохранения объекта. Когда пользователь нажимает кнопку «Создать», приложение (сервис <code>Places</code>) отправляет асинхронный запрос с данными объекта. Ответ на этот запрос придёт через некоторое время, поэтом нам важно исключить возможность повторного нажатия на кнопку «Создать». Кроме того, пользователь должен видеть, что процесс сохранения запущен.</p>
<p>Для этого в методе <code>$scope.save</code> мы устанавливаем <code>$scope.saving = true;</code> (строка 23). Также устанавили обработчики событий <code>place:updated</code> и <code>place:added</code> (строки 27-34). Эти события инициирует сервис <code>Places</code>, когда получает сообщения об успешном обновлении или создании объекта. Обработчики устанавливают <code>$scope.saving = false</code>. Теперь в шаблоне формы мы можем использовать <code>$scope.saving</code> для того, чтобы показать пользователю, например, картинку с загрузчиком.</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;span ng-show=&quot;saving&quot;&gt;&lt;img ng-src=&quot;images/ajax-loader.gif&quot; alt=&quot;loader&quot;&gt;&lt;/span&gt;</pre>
<p>Во время сохранения может произойти какая-нибудь ошибка. Информацию о ней мы должны показать пользователю. Для этого устанавливаем обработчик <code>place:error</code>. Это также событие сервиса <code>Places</code>, вместе с которым передаются описания ошибок. Описания мы сохраняем в массиве <code>$scope.errors</code>. Таким образом, мы сможем отобразить их в шаблоне.</p>
<p>Обработчик события <code>map:pointSelected</code> (строки 52-55) предназначен для установки координат. Это событие инициирует директива (её мы рассмотрим в следующей части) когда пользователь кликает по карте. Обработчик просто сохраняет соответствующие свойства объекта $scope.place.</p>
<h3>Шаблон формы</h3>
<p>Файл <code>partials/form.html</code></p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;form name=&quot;placeForm&quot; novalidate&gt;

    &lt;h4 ng-show=&quot;isNew&quot;&gt;{{ &#039;CREATE_PLACE&#039; | translate }}&lt;/h4&gt;
    &lt;h4 ng-hide=&quot;isNew&quot;&gt;{{ &#039;UPDATE_PLACE&#039; | translate }}&lt;/h4&gt;

    &lt;label&gt;{{ &#039;TITLE&#039; | translate }} *:&lt;br&gt;&lt;input type=&quot;text&quot; ng-model=&quot;place.p_title&quot; name=&quot;pTitle&quot; required class=&quot;span12&quot;&gt;
    &lt;span class=&quot;text-error&quot; ng-show=&quot;placeForm.$dirty &amp;&amp; placeForm.pTitle.$error.required&quot;&gt;{{ &#039;FIELD_NOT_EMPTY&#039; | translate }}.&lt;/span&gt;&lt;/label&gt;

    &lt;label&gt;{{ &#039;DESCRIPTION&#039; | translate }} (&lt;a href=&quot;http://daringfireball.net/projects/markdown/&quot; target=&quot;_blank&quot;&gt;Markdown&lt;/a&gt; {{ &#039;ENABLED&#039; | translate }}):&lt;br&gt;
        &lt;textarea ng-model=&quot;place.p_description&quot; class=&quot;span12&quot;&gt;&lt;/textarea&gt;
    &lt;/label&gt;

    &lt;label for=&quot;placeLat&quot;&gt;{{ &#039;LATITUDE&#039; | translate }}:&lt;/label&gt;
    &lt;div class=&quot;input-append&quot;&gt;
        &lt;input type=&quot;text&quot; id=&quot;placeLat&quot; ng-model=&quot;place.p_lat&quot; placeholder=&quot;{{ &#039;CLICK_ON_THE_MAP&#039; | translate }}&quot; disabled&gt;
        &lt;span class=&quot;add-on&quot;&gt;&amp;deg;&lt;/span&gt;
    &lt;/div&gt;

    &lt;label for=&quot;placeLng&quot;&gt;{{ &#039;LONGITUDE&#039; | translate }}:&lt;/label&gt;
    &lt;div class=&quot;input-append&quot; class=&quot;span12&quot;&gt;
        &lt;input type=&quot;text&quot; id=&quot;placeLng&quot; ng-model=&quot;place.p_lng&quot; placeholder=&quot;{{ &#039;CLICK_ON_THE_MAP&#039; | translate }}&quot; disabled&gt;
        &lt;span class=&quot;add-on&quot;&gt;&amp;deg;&lt;/span&gt;
    &lt;/div&gt;

    &lt;div class=&quot;alert alert-error&quot; ng-show=&quot;showErrors&quot;&gt;
        &lt;button type=&quot;button&quot; class=&quot;close&quot; data-dismiss=&quot;alert&quot;&gt;&amp;times;&lt;/button&gt;
        &lt;h4&gt;{{ &#039;ERRORS&#039; | translate }}:&lt;/h4&gt;
        &lt;ul&gt;
            &lt;li ng-repeat=&quot;error in errors&quot;&gt;{{ error }}&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/div&gt;

    &lt;button type=&quot;submit&quot; ng-disabled=&quot;placeForm.$invalid || saving&quot; ng-click=&quot;save()&quot; class=&quot;btn btn-primary&quot;&gt;
        &lt;span ng-show=&quot;isNew&quot;&gt;{{ &#039;CREATE&#039; | translate }}&lt;/span&gt;
        &lt;span ng-hide=&quot;isNew&quot;&gt;{{ &#039;UPDATE&#039; | translate }}&lt;/span&gt;
        &lt;span ng-show=&quot;saving&quot;&gt;&lt;img ng-src=&quot;images/ajax-loader.gif&quot; alt=&quot;loader&quot;&gt;&lt;/span&gt;
    &lt;/button&gt;

    &lt;a href=&quot;places/index#/list&quot; class=&quot;btn&quot;&gt;{{ &#039;CANCEL&#039; | translate }}&lt;/a&gt;

&lt;/form&gt;</pre>
<p>Заголовков формы у нас два (Создать или Изменить форму). Мы выбираем нужный с помощью директив <code>ng-show</code> и <code>ng-hide</code> (строки 3, 4). Эти директивы можно рассматривать как аналог условий.</p>
<p>Разметка формы достаточно стандартная. Используются классы <a href="http://getbootstrap.com/2.3.2/">Twitter Bootstrap</a>. Значения поля формы получают напрямую из моделей с помощью директив <code>ng-model</code>. В значении директивы нужно указать модель или её свойство. Например, модель <code>place</code> ($scope.place) является объектом, поэтому для поля с заголовком объекта мы указываем <code>ng-model="place.p_title"</code>.</p>
<p>Использование <code>ng-model</code> автоматически подключает двунаправленное связывание данных. Т.е. если пользователь изменит значение в поле, оно будет сразу же записано в <code>$scope.place</code>. И наоборот, любое изменение <code>$scope.place</code> или его атрибутов будет отображаться в соответствующем поле. Таким образом, в явном виде задавать атрибут <code>value</code> для тегов <code>input</code> не нужно.</p>
<h3>Отображение ошибок формы</h3>
<p>Мы уже рассмотрели контроллер формы и знаем, что он обрабатывает сообщение <code>place:error</code>. Но это сообщение передаёт список ошибок сервера, т.е. пользователь увидит его только после того, как попытается сохранить форму. Но мы можем выполнить проверку с помощью JavaScript, и AngularJS предоставляет довольно удобный механизм для этих целей.</p>
<p>Для каждой формы автоматически создаётся объект с названием, совпадающим со значением атрибута name формы. Этот объект хранит информацию о состоянии формы и наличии ошибок в её полях.</p>
<p>Состояний всего пять:</p>
<ul>
<li><code>$pristine</code> – пользователь не работал с формой;</li>
<li><code>$dirty</code> – пользователь вводил какую-то информацию;
</li>
<li><code>$valid</code> – принимает значение true, если валидация прошла без ошибок для всех полей;
</li>
<li><code>$invalid</code> – true если хотя бы одно из полей не валидно;
</li>
<li><code>$error</code> – хеш, содержащий ссылки на все не валидные поля.
</li>
</ul>
<p>В стандартном варианте AngularJS поддерживает проверку большинства распространённых полей (<code>text</code>, <code>number</code>, <code>url</code>, <code>email</code>, <code>radio</code>, <code>checkbox</code>), а для установки правил проверки используются атрибуты – <code>required</code>, <code>pattern</code>, <code>minlength</code>, <code>maxlength</code>, <code>min</code>, <code>max</code>.</p>
<p>Рассмотрим в качестве примера поле с названием объекта.</p>
<pre class="brush: html; gutter: true; first-line: 1; highlight: []; html-script: false">&lt;input type=&quot;text&quot; ng-model=&quot;place.p_title&quot; name=&quot;pTitle&quot; required&gt;
&lt;span ng-show=&quot;placeForm.$dirty &amp;&amp; placeForm.pTitle.$error.required&quot;&gt;{{ &#039;FIELD_NOT_EMPTY&#039; | translate }}.&lt;/span&gt;</pre>
<p>Для поля мы установили атрибут <code>required</code>, т.е. поле является обязательным. А тег <code>span</code> будет отображаться, если пользователь работал с формой <code>placeForm.$dirty == true</code> и оставил название пустым <code>placeForm.pTitle.$error.required == true</code>. Без <code>placeForm.$dirty</code> сообщение об ошибке отображалось бы ещё до того, как пользователь начал ввод данных.</p>
<h2>Тестирование контроллеров</h2>
<p>В <a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html">прошлой части</a> мы рассматривали тестирование сервисов с помощью фреймворка <a href="http://pivotal.github.io/jasmine/">Jasmine</a>. Принцип тестирования контроллеров очень похож, но есть нюансы. Рассмотрим их на примере тестов для списка объектов (файл <code>test/unit/js/controllers/list.spec.js</code>).</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">describe(&#039;Places List controller&#039;, function() {
    var scope, rootScope, dialog, routeParams, controller, places;

    beforeEach(function() {
        module(&#039;personalmaps&#039;, function($provide) {
            $provide.value(&#039;lang&#039;, &#039;&#039;);
        });

        inject(function($rootScope, $controller, $routeParams, $dialog, lang) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
            dialog = $dialog;
            routeParams = $routeParams;
            controller = $controller;

            dialog = {
                messageBox: function() {}
            }
            spyOn(dialog, &#039;messageBox&#039;).andReturn({
                open: function() {
                    return {
                        then: function() {}
                    }
                }
            });

            places = jasmine.createSpyObj(&#039;Places&#039;, [&#039;getAll&#039;, &#039;get&#039;, &#039;add&#039;, &#039;update&#039;, &#039;delete&#039;, &#039;save&#039;]);
        });
    });

    it(&#039;should call getAll&#039;, function() {
        controller(&#039;PlacesListController&#039;, {$scope: scope, $rootScope: rootScope, &#039;Places&#039;: places, $routeParams: routeParams, $dialog: dialog});
        expect(places.getAll).toHaveBeenCalled();
    });

    it(&#039;should call getAll on places:updated event&#039;, function() {
        controller(&#039;PlacesListController&#039;, {$scope: scope, $rootScope: rootScope, &#039;Places&#039;: places, $routeParams: routeParams, $dialog: dialog});
        rootScope.$emit(&#039;places:updated&#039;);
        expect(places.getAll.calls.length).toEqual(2);
    });

    it(&#039;should open dialog on confirm call&#039;, function() {
        controller(&#039;PlacesListController&#039;, {$scope: scope, $rootScope: rootScope, &#039;Places&#039;: places, $routeParams: routeParams, $dialog: dialog});
        scope.confirm();
        expect(dialog.messageBox).toHaveBeenCalled();
    });
});</pre>
<p>Как вы помните, очень желательно тестировать каждый компонент приложения отдельно. Чтобы ошибки в других компонентах не влияли на выполнение тестов. Поэтому перед запуском каждого теста мы должны создать окружение, в котором будет выполняться тест.</p>
<p>Для этого мы используем функцию <code>beforeEach</code>.</p>
<p>В первую очередь создаём модуль приложения (строки 5-7). Затем с помощью <code>inject</code> (строки 9-29) подключаем зависимости. Здесь есть нюансы. Подключение стандартных компонентов AngularJS (вроде <code>$rootScope</code>) мы рассматривали в прошлой части. Но сейчас мы тестируем контроллер и для него нам нужно создать <code>scope</code>. В самом приложении это происходит автоматически, когда Angular обрабатывает директиву <code>ng-controller</code> или <code>ng-view</code>, но при тестировании это нужно сделать вручную:</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">scope = $rootScope.$new();</pre>
<p>Кроме того, в нашем контроллере используется <code>$dialog</code> (создаёт окно с просьбой подтвердить удаление). Наш контроллер вызывает методы этого компонента и нам нужно протестировать, что эти методы вызываются правильно. Но использовать настоящий <code>$dialog</code> не желательно, т.к. нам нужно зафиксировать только факт вызова метода, работа компонента нас сейчас не интересует. Поэтому мы создаём mock объект, который содержит функцию <code>messageBox</code></p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">dialog = {
    messageBox: function() {}
}</pre>
<p>и с помощью функции <code>spyOn</code> указываем <strong>Jasmine</strong>, что нужно отслеживать вызовы <code>messageBox</code></p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">spyOn(dialog, &#039;messageBox&#039;).andReturn({
	open: function() {
		return {
			then: function() {}
		}
	}
});</pre>
<p>Тут же указываем, какой объект должен вернуть вызов <code>messageBox</code>. Это тоже заглушка, содержащая функции <code>open</code> и <code>then</code>. Она необходима, потому что в контроллере (list.js, строки 25-31) мы эти функции вызываем.</p>
<p>Также нам нужно отследить, как контроллер вызывает методы сервиса <code>Places</code>. Реальный сервис мы не используем (для него у нас тесты написаны), а создаём mock объект.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">places = jasmine.createSpyObj(&#039;Places&#039;, [&#039;getAll&#039;, &#039;get&#039;, &#039;add&#039;, &#039;update&#039;, &#039;delete&#039;, &#039;save&#039;]);</pre>
<p>В первом параметре функции <code>createSpyObj</code> указываем имя сервиса, во втором – массив с именами методов.</p>
<p>Теперь можно написать тесты (строки 31-46).</p>
<p>В каждом тесте мы сначала создаём контроллер</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">controller(&#039;PlacesListController&#039;, {$scope: scope, $rootScope: rootScope, &#039;Places&#039;: places, $routeParams: routeParams, $dialog: dialog});</pre>
<p>и затем имитируем поведение пользователя. Например, вызываем метод <code>confirm</code> и проверяем, что в ответ контроллер вызвал метод <code>dialog.messageBox</code>.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">scope.confirm();
expect(dialog.messageBox).toHaveBeenCalled();</pre>
<p>Точно также проверяем реакцию на события.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">rootScope.$emit(&#039;places:updated&#039;);
expect(places.getAll.calls.length).toEqual(2);</pre>
<p>Метод <code>getAll</code> вызывался дважды, т.к. в первый раз он был вызван при создании контроллера, а во второй – при получении события <code>places:updated</code>.</p>
<p>Тесты для формы я описывать не буду, т.к. они аналогичны и вы можете посмотреть их на <a href="https://github.com/vladimir-s/personal-maps/blob/master/public_html/protected/tests/unit/js/controllers/form.spec.js">GitHub</a>.</p>
<p>В следующей части мы рассмотрим <strong>создание директивы</strong> для подключения Google Map.</p>
<p>Если есть вопросы или замечания, пишите.</p>
<h3>Содержание</h3>
<div id="serial-posts-wrapper">
<h3 class="serial-posts-heading"></h3>
<ul class="serial-posts">
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ispolzuem-yii-i-angularjs-dlya-razrabotki-web-prilozheniya-chast-1.html" title="Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.">Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ustanavlivaem-i-nastraivaem-yii-proektiruem-strukturu-bazy-dannyx-chast-2.html" title="Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.">Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-client-side-app-structure-part-3.html" title="Personal Maps: главная страница и структура клиентской части приложения. Часть 3.">Personal Maps: главная страница и структура клиентской части приложения. Часть 3.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html" title="Personal Maps: создаём сервис AngularJS. Часть 4.">Personal Maps: создаём сервис AngularJS. Часть 4.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html" title="Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</a></li>
<li class="serial-posts-list-item current-inactive">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html" title="Personal maps: создаём директиву для подключения Google Maps. Часть 7.">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html" title="Personal maps: REST интерфейс. Часть 8.">Personal maps: REST интерфейс. Часть 8.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html" title="Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html" title="Personal Maps: локализация и интернационализация. Часть 10">Personal Maps: локализация и интернационализация. Часть 10</a></li>
</ul>
</div><p>The post <a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html/feed</wfw:commentRss>
			<slash:comments>19</slash:comments>
		
		
			</item>
		<item>
		<title>Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</title>
		<link>https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html</link>
					<comments>https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html#comments</comments>
		
		<dc:creator><![CDATA[Владимир]]></dc:creator>
		<pubDate>Sun, 15 Sep 2013 16:30:31 +0000</pubDate>
				<category><![CDATA[Ajax]]></category>
		<category><![CDATA[AngularJS]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Web разработка]]></category>
		<guid isPermaLink="false">https://www.simplecoding.org/?p=1431</guid>

					<description><![CDATA[<p>В прошлый раз мы создали сервис AngularJS под названием Places, через который происходит передача данных между клиентской и серверной частями приложения. Наш сервис использует несколько встроенных компонентов Angular ($rootScope и $http) и не зависит от остальных компонентов приложения. С дугой стороны, остальные компоненты (контроллеры, директивы) используют методы сервиса. Любые изменения в названиях или количестве аргументов...  <a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html" title="Read Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.">Читать дальше &#187;</a></p>
<p>The post <a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>В <a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html">прошлый раз</a> мы создали сервис <strong>AngularJS</strong> под названием <code>Places</code>, через который происходит передача данных между клиентской и серверной частями приложения.</p>
<p>Наш сервис использует несколько встроенных компонентов Angular (<code>$rootScope</code> и <code>$http</code>) и не зависит от остальных компонентов приложения. С дугой стороны, остальные компоненты (контроллеры, директивы) используют методы сервиса. Любые изменения в названиях или количестве аргументов этих методов приведут к тому, что придется изменять все компоненты, которые их используют. Таким образом, полезно протестировать работу сервиса перед разработкой остальной части приложения. Этот подход можно использовать не только по отношению к сервисам – сначала разрабатываем и тестируем компоненты с наименьшим числом зависимостей, а затем собираем из них всё приложение.<br />
<span id="more-1431"></span><br />
<em>Примечание</em>. Исходный код размещён на GitHub, также доступна демоверсия приложения.</p>
<a href="https://github.com/vladimir-s/personal-maps" class="src-button">Source</a>
<a href="http://personal-maps.simplecoding.org" class="demo-button">Demo</a>
<p>Прежде чем переходить к написанию тестов, давайте определим <strong>общие требования к тестированию</strong>.</p>
<p>Написание тестов занимает время. И полностью автоматизировать этот процесс нельзя, но его можно сократить за счёт использования библиотек и автоматизации запуска тестов. Также очень полезно иметь возможность запуска тестов из консоли, особенно если вы используете какой-нибудь CI (Continuous Integration) сервер.</p>
<p>Для решения всех перечисленных задач мы используем:</p>
<ul>
<li>Фреймворк <a href="http://pivotal.github.io/jasmine/">Jasmine</a>. В принципе, его можно заменить на какой-нибудь другой, но в примерах в документации AngularJS используется именно он. Кроме того, Jasmine в любом случае является одним из самых популярных.</li>
<li>Утилиту для запуска тестов <a href="http://karma-runner.github.io/0.10/index.html">Karma test runner</a>. Она позволяет запускать тесты из консоли, следит за изменениями файлов и перезапускает тесты при их изменениях.</li>
<li><a href="http://phantomjs.org/">PhantomJS</a> – так называемый headless браузер, т.е. без графического интерфейса. Работает на движке webkit. Пока вы разрабатываете локально, вам не принципиально, какой браузер использовать, но на linux-сервере без иксов обычные браузеры просто не запустятся.</li>
</ul>
<h2>Установка и настройка окружения</h2>
<p>В Yii фреймворке тесты находятся в папке <code>protected/tests</code>. Нас это размещение вполне устраивает.</p>
<p>Т.е. структура папок будет такой:</p>
<pre class="brush: text; gutter: true; first-line: 1; highlight: []; html-script: false">protected/
	tests/
		libs/
			//дополнительные JS библиотеки
		unit/
			js/
				controllers/
				services/
				directives/
		karma.conf.js //файл конфигурации Karma</pre>
<h2>Установка Karma test runner</h2>
<p>Для работы <strong>Karma</strong> необходим <a href="http://nodejs.org/">Node.JS</a>. Скачайте инсталлятор для вашей операционной системы и просто следуйте инструкциям. Если вы устанавливаете Node из исходников, то вам потребуется добавить в переменную <code>PATH</code> путь к исполняемым файлам.</p>
<p>Karma устанавливается с помощью команды:</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">npm install -g karma</pre>
<p>npm скачает все необходимые файлы, и вы сможете выполнять из консоли команды вроде</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">karma start</pre>
<p>После этого, необходимо создать переменные окружения, в которых будет указано размещение браузеров, которые должна использовать Karma. Например, для Google Chrome и PhantomJS нужно создать следующие переменные.</p>
<pre class="brush: text; gutter: true; first-line: 1; highlight: []; html-script: false">CHROME_BIN=&quot;полный_путь\chrome.exe&quot;
PHANTOMJS_BIN=&quot;полный_путь\phantomjs.exe&quot;</pre>
<h2>Настраиваем Karma test runner</h2>
<p>Технически Karma запускает свой web сервер, который по-умолчанию использует порт 9876. Это даёт возможность разместить файлы тестов вне <code>DOCUMENT_ROOT</code> сервера, который используется для приложения.</p>
<p>Создадим конфигурационный файл <code>karma.conf.js</code> с помощью следующей команды</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">karma init karma.conf.js</pre>
<p>Эту команду необходимо выполнить из папки <code>protected/tests</code>. В результате будет создан файл с настройками по-умолчанию. Нам нужно их немного изменить для того, чтобы утилита знала, где находятся файлы проекта и файлы тестов.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">// base path, that will be used to resolve files and exclude
basePath = &#039;&#039;;

// list of files / patterns to load in the browser
files = [
  JASMINE,
  JASMINE_ADAPTER,
  &#039;http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js&#039;,
  &#039;http://maps.googleapis.com/maps/api/js?sensor=false&#039;,
  &#039;../../js/markdown.js&#039;,
  &#039;http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js&#039;,
  &#039;libs/angular-mocks.js&#039;,
  &#039;../../js/ui-bootstrap-tpls-0.4.0.min.js&#039;,
  &#039;../../js/angular-translate.min.js&#039;,
  &#039;unit/js/bootstrap.js&#039;,
  &#039;../../js/app.js&#039;,
  &#039;../../js/controllers/*.js&#039;,
  &#039;../../js/services/*.js&#039;,
  &#039;../../js/directives/*.js&#039;,
  &#039;unit/js/controllers/*.spec.js&#039;,
  &#039;unit/js/services/*.spec.js&#039;,
  &#039;unit/js/directives/*.spec.js&#039;
];

// list of files to exclude
exclude = [];

// test results reporter to use
// possible values: &#039;dots&#039;, &#039;progress&#039;, &#039;junit&#039;
reporters = [&#039;progress&#039;];

// web server port
port = 9876;

// cli runner port
runnerPort = 9100;

// enable / disable colors in the output (reporters and logs)
colors = true;

// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel = LOG_DEBUG;

// enable / disable watching file and executing tests whenever any file changes
autoWatch = true;

// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers = [&#039;Chrome&#039;];

// If browser does not capture in given timeout [ms], kill it
captureTimeout = 60000;

// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun = false;
</pre>
<p>Прежде всего, указываем размещение файлов проекта в массиве <code>files</code> (строки 5-23).<br />
<code>JASMINE</code> и <code>JASMINE_ADAPTER</code> подключают фреймворк Jasmine вместе с адаптером для Karma (необходимые файлы входят в дистрибутив Karma).<br />
Затем мы подключаем все сторонние библиотеки, которые используются приложением. Их порядок должен соответствовать последовательности их подключения на странице приложения.</p>
<p>Кроме того, нам понадобиться библиотека <code>angular-mocks.js</code> (строка 12, найти эту библиотеку можно <a href="http://docs.angularjs.org/misc/downloading">здесь</a>), которая содержит mock-объекты для встроенных сервисов AngularJS. С их помощью мы, например, сможем тестировать работу сервиса <code>$http</code>, не отправляя реальных запросов на сервер (об этом чуть ниже).</p>
<p>Также подключаем скрипт <code>tests/unit/js/bootstrap.js</code>. В нём мы создадим JS объекты, которые при работе приложения устанавливаются с помощью PHP кода. Например, язык приложения указывается в конфигурационном файле Yii <code>main.php</code>. При формировании страницы приложения, значение этого параметра присваивается JS переменной <code>lang</code>. Это позволяет указать языковые настройки только один раз, а не отдельно для серверной и клиентской части. Но в результате, переменная <code>lang</code> оказывается недоступной для тестов, т.к. Karma формирует страницу самостоятельно, без использования нашего PHP кода.</p>
<p>Наконец, мы подключаем JavaScript файлы приложения и тесты (строки 17-22). Обратите внимание, мы можем использовать <code>*</code> для того, чтобы подключить все файлы в папке.</p>
<p>Из остальных параметров мы установим для уровня логгирования значение <code>LOG_DEBUG</code> (строка 43) и укажем, какой браузер Karma должна использовать (строка 56). Вообще, <code>LOG_DEBUG</code> использовать не обязательно. Просто в этом режиме в консоль выводятся сообщения о подключении JS файлов, и если вы допустите ошибку при формировании массива <code>files</code>, то так будет проще её найти.</p>
<p>Параметр <code>singleRun = false</code> (строка 63) означает, что после запуска Karma будет отслеживать изменения и автоматически перезапускать тесты.</p>
<p><strong>Запуск Karma</strong> выполняется с помощью команды</p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">karma start</pre>
<p>или </p>
<pre class="brush: bash; gutter: true; first-line: 1; highlight: []; html-script: false">karma start karma.conf.js</pre>
<p>После её выполнения запустится браузер, а в консоли вы увидите результаты выполнения тестов.</p>
<img loading="lazy" decoding="async" src="https://www.simplecoding.org/wp-content/uploads/2013/09/7_tests_results.png" alt="karma tests results" width="641" height="108" class="alignnone size-full wp-image-1433" srcset="https://www.simplecoding.org/wp-content/uploads/2013/09/7_tests_results.png 641w, https://www.simplecoding.org/wp-content/uploads/2013/09/7_tests_results-450x75.png 450w" sizes="auto, (max-width: 641px) 100vw, 641px" />
<p><strong>Karma test runner</strong> мы настроили. Теперь напишем тесты.</p>
<p class="wp-caption"><script type="text/javascript"><!--
google_ad_client = "ca-pub-1195314499431105";
/* simplecoding content */
google_ad_slot = "2021931265";
google_ad_width = 336;
google_ad_height = 280;
//-->
</script>
<script type="text/javascript"
src="https://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</p>
<h2>Тестирование сервиса Places</h2>
<p>Создадим файл <code>tests/unit/js/services/places.spec.js</code></p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">describe(&#039;Places service&#039;, function() {
	...
});</pre>
<p>Функция <code>describe</code> описывает набор тестов. В первом параметре указывается название набора, а во втором – функция, содержащая тесты.</p>
<p>Тест состоит из набора спецификаций (создаются с помощью функции </p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">it</pre>
<p>), которые содержат «ожидания» (вызовы функции <code>expect</code>). Например, простой тест может выглядеть так:</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">describe(&#039;Places service&#039;, function() {
    it(&#039;returns particular place&#039;, function() {
        expect(true).toEqual(true);
    });
});</pre>
<p>Но в нашем случае ситуация сложнее. Нам необходимо тестировать код, который отправляет AJAX запросы. Если тест будет отправлять реальные запросы, то возникнет ряд проблем:</p>
<ul>
<li>Время выполнения теста увеличится, т.к. он будет ждать ответы сервера.</li>
<li>Результаты сервера могут зависеть от состояния базы данных. Можно, конечно, создать специальную базу с тестовыми данными, но процесс тестирования в любом случае станет сложнее.</li>
<li>Серверная часть может оказаться в нерабочем состоянии, например, потому что она просто не полностью написана и сама содержит ошибки.</li>
</ul>
<p>Поэтому нам важно запускать тесты автономно. И в этом нам поможет библиотека <code>angular-mocks.js</code>. Она содержит mock-объект <code>$httpBackend</code>, который эмулирует работу сервиса <code>$http</code>. Рассмотрим, как его подключить и использовать.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">describe(&#039;Places service&#039;, function() {
    var $httpBackend, injector;

    var response = [
        {
            &#039;id&#039;: 1,
            &#039;p_title&#039;: &#039;title 1&#039;,
            &#039;p_description&#039;: &#039;desc 1&#039;,
            &quot;p_lng&quot;: 50.4,
            &quot;p_lat&quot;: 30.76,
            &#039;p_user&#039;: 1
        },
		...
    ];

    var newPlace = {
        &#039;id&#039;: &#039;3&#039;,
        &#039;p_title&#039;: &#039;title 3&#039;,
        &#039;p_description&#039;: &#039;desc 3&#039;,
        &#039;p_lng&#039;: &#039;50.4&#039;,
        &#039;p_lat&#039;: &#039;30.76&#039;,
        &#039;p_user&#039;: 1
    };

    beforeEach(function() {
        module(&#039;personalmaps&#039;, function($provide) {
            $provide.value(&#039;lang&#039;, &#039;&#039;);
        });

        inject(function($injector, lang) {
            injector = $injector;
            $httpBackend = $injector.get(&#039;$httpBackend&#039;);
            $httpBackend.when(&#039;GET&#039;, &#039;api/places&#039;).respond(response);
            $httpBackend.when(&#039;POST&#039;, &#039;api/places&#039;).respond(newPlace);
            $httpBackend.when(&#039;PUT&#039;, &#039;api/places/2&#039;).respond(response[1]);
            $httpBackend.when(&#039;PUT&#039;, &#039;api/places/5&#039;).respond(response[1]);
            $httpBackend.when(&#039;DELETE&#039;, &#039;api/places/2&#039;).respond(&#039;&#039;);
            $httpBackend.when(&#039;GET&#039;, &#039;foo/bar.json?lang=en&#039;).respond(&#039;[]&#039;);
        });
    });

    afterEach(function() {
        $httpBackend.verifyNoOutstandingExpectation();
        $httpBackend.verifyNoOutstandingRequest();
    });

    it(&#039;calls api/places&#039;, function() {
        $httpBackend.expectGET(&#039;api/places&#039;);
        injector.get(&#039;Places&#039;);
        $httpBackend.flush();
    });

	...
});</pre>
<p>Перед запуском тестов мы создаём массивы с тестовыми данными, которые будет возвращать <code>$httpBackend</code> (строки 4-23). Формат этих данных должен совпадать с форматом ответа реального сервера.</p>
<p>Затем с помощью функции <code>beforeEach</code> настроим <code>$httpBackend</code>. Эта функция вызывается перед каждым вызовом <code>it</code>.</p>
<p>Тут есть несколько важных моментов. При создании приложения AngularJS выполняет довольно много работы, которую в случае использования тестов мы должны выполнить самостоятельно. Перед каждым тестом мы создаём модуль (строки 26-28) и вызываем функцию <code>inject</code>, которая определена в файле <code>angular-mocks.js</code>. Она создаёт объект <code>$injector</code>, с помощью которого можно использовать механизм внедрения зависимостей (<a href="http://docs.angularjs.org/guide/di">Dependency injection</a>).</p>
<p>При создании компонентов AngularJS мы указываем списки зависимостей. Например, при создании нашего сервиса мы так подключили <code>$http</code> и <code>$rootScope</code>. Но наш тест ничего не знает о том, что нам нужен <code>$httpBackend</code> и сам сервис <code>Places</code>. <code>$injector</code> как раз и позволяет подключить их, т.е. «внедрить» в наш тест.</p>
<p>Посмотрите, в строке 32 мы использовали метод <code>$injector.get</code> для того, чтобы получить <code>$httpBackend</code>, а в строке 49 мы с помощью <code>injector</code> подключили сервис <code>Places</code>.</p>
<p>После того, как <code>$httpBackend</code> подключён, его нужно настроить. Т.е. указать какие запросы он будет обрабатывать, и какие ответы отправлять.</p>
<p>Например, </p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$httpBackend.when(&#039;GET&#039;, &#039;api/places&#039;).respond(response);</pre>
<p>означает, что если где-нибудь в приложении будет выполнен вызов </p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$http({method: &#039;GET&#039;, url: &#039;api/places&#039;})</pre>
<p>то <code>$httpBackend</code> его перехватит и вернёт значение <code>response</code>.</p>
<p>Таким образом, мы определяем ответы на все ajax запросы, которые отправляет наш сервис. Реальные запросы при этом не отправляются, т.е. работоспособность серверной части приложения нас не интересует, и мы всегда будем уверены, что получили правильный ответ.</p>
<p>Теперь взгляните на код теста (строки 47-51). Он проверяет, что сразу после создания приложения сервис <code>Places</code> отправляет запрос к <code>api/places</code>, который должен вернуть полный список объектов.</p>
<p>Мы вызываем</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$httpBackend.expectGET(&#039;api/places&#039;);</pre>
<p>т.е. указываем, что ожидаем отправки AJAX-запроса. Затем подключаем сервис</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">injector.get(&#039;Places&#039;);</pre>
<p>в результате AngularJS выполнит функцию сервиса <code>Places</code> (файл <code>public_html/js/services/places.js</code>), которая отправит запрос. Т.к. мы хотим получить результат сразу, то вызываем</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">$httpBackend.flush();</pre>
<p>Напомню, AJAX запросы являются асинхронными, поэтому ответ может прийти в любой момент времени. При вызове <code>flush</code> все отправленные запросы будут завершены и вернут ответ. Таким образом, ответ на запрос к <code>api/places</code> будет получен во время выполнения теста и «ожидание» (<code>expectGET</code>) будет успешным.</p>
<p>После завершения теста Jasmine автоматически вызовет функцию <code>afterEach</code> (строки 42-45). В ней мы с помощью методов <code>verifyNoOutstandingExpectation</code> и <code>verifyNoOutstandingRequest</code> закрываем все «ожидания» и запросы. Если мы этого не сделаем, то выполнение одного из тестов может повлиять на работу остальных, а этого допускать нельзя.</p>
<p>Как видите, тестирование асинхронного JavaScript кода довольно не тривиальная задача, даже с использованием фреймворков. Но, по-сути, вам нужно научиться использовать несколько дополнительных объектов, и хотя бы в общих чертах разобраться в принципе подключения объектов в Angular (это желательно сделать не только ради тестов). После этого вы увидите, что все тесты используют очень похожий код. Например, рассмотрим следующий тест.</p>
<pre class="brush: javascript; gutter: true; first-line: 1; highlight: []; html-script: false">it(&#039;returns null for unknown places&#039;, function() {
	var places = injector.get(&#039;Places&#039;);
	$httpBackend.flush();
	expect(places.get(&#039;43534535&#039;)).toBe(null);
});</pre>
<p>Он проверяет, что метод <code>Places.get</code> возвращает <code>null</code> если объект с заданным <code>id</code> не найден. Мы подключаем сервис <code>Places</code>. Сервис выполняет запрос и в ответ получает массив <code>response</code>. Посмотрите, в этом массиве нет объекта с <code>id</code> равным <code>43534535</code>. Затем вызываем <code>$httpBackend.flush()</code>, т.е. обеспечиваем получение ответа до того, как будет выполнена следующая строка. И с помощью метода <code>places.get</code> ищем объект с <code>id == 43534535</code>. Т.к. такого объекта нет, то мы ожидаем, что метод вернёт <code>null</code>.</p>
<p>Остальные тесты работают аналогично, вы можете посмотреть их на <a href="https://github.com/vladimir-s/personal-maps/blob/master/public_html/protected/tests/unit/js/services/places.spec.js">GitHub</a>.</p>
<p>На этом, мы завершаем работу с сервисом и в следующий раз рассмотрим контроллер и представления.</p>
<p>Если есть вопросы или замечания, пишите.<br />
<strong>Успехов!</strong></p>
<h3>Содержание</h3>
<div id="serial-posts-wrapper">
<h3 class="serial-posts-heading"></h3>
<ul class="serial-posts">
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ispolzuem-yii-i-angularjs-dlya-razrabotki-web-prilozheniya-chast-1.html" title="Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.">Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-ustanavlivaem-i-nastraivaem-yii-proektiruem-strukturu-bazy-dannyx-chast-2.html" title="Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.">Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-client-side-app-structure-part-3.html" title="Personal Maps: главная страница и структура клиентской части приложения. Часть 3.">Personal Maps: главная страница и структура клиентской части приложения. Часть 3.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-servis-angularjs-chast-4.html" title="Personal Maps: создаём сервис AngularJS. Часть 4.">Personal Maps: создаём сервис AngularJS. Часть 4.</a></li>
<li class="serial-posts-list-item current-inactive">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-kontrollery-i-predstavleniya-v-angularjs-chast-6.html" title="Personal Maps: контроллеры и представления в AngularJS. Часть 6.">Personal Maps: контроллеры и представления в AngularJS. Часть 6.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-sozdayom-direktivu-dlya-podklyucheniya-google-maps-chast-7.html" title="Personal maps: создаём директиву для подключения Google Maps. Часть 7.">Personal maps: создаём директиву для подключения Google Maps. Часть 7.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-rest-interfejs-chast-8.html" title="Personal maps: REST интерфейс. Часть 8.">Personal maps: REST интерфейс. Часть 8.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-avtorizaciya-i-autentifikaciya-s-ispolzovaniem-yii-rbac-chast-9.html" title="Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.">Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.</a></li>
<li class="serial-posts-list-item"><a href="https://www.simplecoding.org/personal-maps-lokalizaciya-i-internacionalizaciya-chast-10.html" title="Personal Maps: локализация и интернационализация. Часть 10">Personal Maps: локализация и интернационализация. Часть 10</a></li>
</ul>
</div><p>The post <a href="https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html">Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.</a> first appeared on <a href="https://www.simplecoding.org">SimpleCoding.org</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.simplecoding.org/personal-maps-testirovanie-angularjs-servisa-s-pomoshhyu-jasmine-i-karma-chast-5.html/feed</wfw:commentRss>
			<slash:comments>26</slash:comments>
		
		
			</item>
	</channel>
</rss>
