<?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>hudson@work &#187; sympal</title>
	<atom:link href="http://hudson.su/tag/sympal/feed/" rel="self" type="application/rss+xml" />
	<link>http://hudson.su</link>
	<description>статьи о web-разработке, менеджменте IT проектов и контроле качества</description>
	<lastBuildDate>Fri, 20 Jan 2012 13:15:39 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	
		<item>
		<title>The symfony 2009 Advent Calendar: день 2 &#8211; продвинутая маршрутизация (часть 1)</title>
		<link>http://hudson.su/2010/01/14/the-symfony-2009-advent-calendar-day2-advanced-routing-p1/</link>
		<comments>http://hudson.su/2010/01/14/the-symfony-2009-advent-calendar-day2-advanced-routing-p1/#comments</comments>
		<pubDate>Thu, 14 Jan 2010 01:23:29 +0000</pubDate>
		<dc:creator>hudson</dc:creator>
				<category><![CDATA[Профессиональное]]></category>
		<category><![CDATA[doctrine]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[symfony]]></category>
		<category><![CDATA[symfony advent calendar'09]]></category>
		<category><![CDATA[sympal]]></category>

		<guid isPermaLink="false">http://hudson.su/?p=721</guid>
		<description><![CDATA[Symfony 2009 advent calendar &#8211; это 24 урока продвинутого уровня о symfony. Все уроки представлены на 5 языках: английском, французском, испанском, итальянском, японском. Русского нет, это досадное упущение постараюсь исправить. Перевод статьи Symfony 2009 Advent Calendar: Advanced Routing (part 1). by Ryan Weaver Маршрутизатор (или же каркас маршрутизации), если смотреть изнутри, представляет из себя карту, [...]]]></description>
			<content:encoded><![CDATA[<blockquote><p><strong>Symfony 2009 advent calendar</strong> &#8211; это 24 урока <strong>продвинутого</strong> уровня о symfony. Все уроки представлены на 5 языках: английском, французском, испанском, итальянском, японском. Русского нет, это досадное упущение постараюсь исправить.</p></blockquote>
<p>Перевод статьи <a href="http://www.symfony-project.org/advent_calendar/2/en" target="_blank">Symfony 2009 Advent Calendar: Advanced Routing (part 1)</a>.</p>
<p><span id="more-721"></span></p>
<p><em>by Ryan Weaver</em></p>
<p>Маршрутизатор (или же каркас маршрутизации), если смотреть изнутри, представляет из себя карту, которая связывает каждый URL с некоторой локацией внутри проекта на symfony и наоборот. Он может с легкостью создать красивые URL, оставаясь независимым от логики приложения. Учитывая прогресс, которого удалось достигнуть в предыдущих версиях symfony, маршрутизатор делает следующий шаг вперед.</p>
<p>Эта глава иллюстрирует процесс создания простого веб-приложения, в котором каждый клиент использует отдельный субдомен (например client1.mydomain.com и client2.mydomain.com). Это будет совсем несложно, когда мы расширим возможности маршрутизатора ))</p>
<blockquote><p>Для примера из этой главы мы будем использовать Doctrine в качестве ORM</p></blockquote>
<h2>Настройка проекта: CMS для многих клиентов</h2>
<p>Для начала представим некую вымышленную компанию, Sympal Builder, которая желает создать CMS, с помощью которой ее клиенты смогут создавать вебсайты на поддоменах сайта sympalbuilder.com. Т.е. клиент XXX может просмотреть свой сайт по адресу xxx.sympalbuilder.com и использовать админку по адресу xxx.sympalbuilder.com/backend.php.</p>
<blockquote><p>Наименование Sympal мы позаимствовали у одноименного проекта Jonathan Wage &#8211; <a href="http://www.sympalphp.org/" target="_blank">Sympal</a>, CMF, созданная на symfony.</p></blockquote>
<p>Проект имеет два основных требования:</p>
<ul>
<li>Пользователь должен иметь возможность создавать страницы и указывать для них title, content и URL.</li>
<li>Приложение должно быть создано внутри одного проекта symfony, который контролирует фронтэнд и бэкэнд всех клентских сайтов, определяя клиента и загружая корректные данные для поддомена.</li>
</ul>
<blockquote><p>Для создания нашего приложения, вебсервер должен быть настроен направлять все запросы на *.sympalbuilder.com на тот же document root &#8211; web директорию нашего проекта на symfony.</p></blockquote>
<h3>Схема и данные</h3>
<p>База данных для проекта будет состоять из объектов Client и Page. Каждый Client представлен на своем поддомене и может содержать много объектов Page.</p>
<pre># config/doctrine/schema.yml
Client:
  columns:
    name:       string(255)
    subdomain:  string(50)
  indexes:
    subdomain_index:
      fields:   [subdomain]
      type:     unique

Page:
  columns:
    title:      string(255)
    slug:       string(255)
    content:    clob
    client_id:  integer
  relations:
    Client:
      alias:        Client
      foreignAlias: Pages
      onDelete:     CASCADE
  indexes:
    slug_index:
      fields:   [slug, client_id]
      type:     unique
</pre>
<blockquote><p>Хотя индексы для таблиц не являются необходимыми, создать их все же рекомендуется, поскольку приложение будет часто запрашивать данные из них.</p></blockquote>
<p>Для того чтобы наш проект заработал, необходимо разместить следующие тестовые данные в файле <code>data/fixtures/fixtures.yml</code>:</p>
<pre># data/fixtures/fixtures.yml
Client:
  client_pete:
    name:      Pete's Pet Shop
    subdomain: pete
  client_pub:
    name:      City Pub and Grill
    subdomain: citypub

Page:
  page_pete_location_hours:
    title:     Location and Hours | Pete's Pet Shop
    content:   We're open Mon - Sat, 8 am - 7pm
    slug:      location
    Client:    client_pete
  page_pub_menu:
    title:     City Pub And Grill | Menu
    content:   Our menu consists of fish, Steak, salads, and more.
    slug:      menu
    Client:    client_pub</pre>
<p>Это тестовые данные для двух вебсайтов, каждый из них имеет по одной странице. Полный URL каждой страницы определяется колонкой <strong>subdomain</strong> в таблице Client и колонкой <strong>slug </strong>в таблице Page.</p>
<pre>http://pete.sympalbuilder.com/location

http://citypub.sympalbuilder.com/menu
</pre>
<h3>Маршрутизация</h3>
<p>Каждая страница вебсайта Sympal Builder напрямую соответствует объекту Page, который определяет ее title и content. Для того чтобы связать каждый URL с его страницей, создадим маршрут типа <code>sfDoctrineRoute</code>, который использует поле slug:</p>
<pre># apps/frontend/config/routing.yml
page_show:
  url:        /:slug
  class:      sfDoctrineRoute
  options:
    model:    Page
    type:     object
  params:
    module:   page
    action:   show
</pre>
<p>Этот маршрут связывает страницу http://pete.sympalbuilder.com/location с соответствующим объектом Page. К несчастью, этот маршрут также соответствует URL&#8217;у http://pete.sympalbuilder.com/menu, т.е. меню ресторана (это другая страница, которая относится к клиенту citypub) также будет отбражаться и на сайте Пита!  На этом шаге маршрут не знает о важности клиентских поддоменов.</p>
<p>Для того чтобы проект заработал, маршрут должен стать более умным. Он должен находить корректную страницу основываясь на полях slug и client_id одновременно, что позволит определять соответствующий хост (например pete.sympalbuilder.com) по колонке subdomain в модели Client. Для того чтобы достичь этого, мы воспользуемся каркасом маршрутизации и создадим свой собственный класс маршрута.</p>
<p>Но, для начала, нам необходимо немного узнать о том, как работает система маршрутизации.</p>
<h3>Как работает система маршрутизации</h3>
<p>Не смотря на то, что маршруты в основном определены в YAML-файле, они трансформируются в объект в момент вызова при помощи специального класса, который называется cache config handler. В итоге, PHP код представляет каждый маршрут в приложении. Поскольку специфика данного процесса выходит за рамки данной статьи, давайте сразу перейдем к его окончанию, т.е. к компилированной версии маршрута page_show. Компилированный файл находится в файле cache/yourappname/envname/config/config_routing.yml.php для конкретного приложения (app) и окружения (env). Ниже приведена сокращенная версия того как выглядит в итоге маршрут page_show:</p>
<pre>new sfDoctrineRoute('/:slug', array (
  'module' =&gt; 'page',
  'action' =&gt; 'show',
), array (
  'slug' =&gt; '[^/\.]+',
), array (
  'model' =&gt; 'Page',
  'type' =&gt; 'object',
));</pre>
<blockquote><p>Наименование класса каждого маршрута определяется ключом class внутри файла routing.yml. Если этот ключ не указан, маршрут по умолчанию будет являться классом sfRoute. Другой стандартный класс маршрута &#8211; это sfRequestRoute, который позволяет разработчику создавать RESTful маршруты. Полный список классов маршрутов и доступных для них опций вы можете найти в с <a href="http://www.symfony-project.org/reference/1_3/en/10-Routing" target="_blank">правочнике symfony</a>.</p></blockquote>
<h3>Сверка входящего запроса с конкретным маршрутом</h3>
<p>Одно из основных занятий маршрутизатора &#8211; сверять каждый входящий URL с корректным объектом маршрута. Класс sfPatternRouting это ядро движка маршрутизации и занимается только этой задачей. Не смотря на его важность, разработчики редко взаимодействуют напрямую с sfPatternRouting.</p>
<p>Для определения корректного маршрута, sfPatternRouting циклически просматривает каждый sfRoute и &#8220;спрашивает&#8221;, подходит ли ему входящий URL. Изнутри это выглядит следующим образом: sfPatternRouting вызывает метод sfRoute::matchesUrl() для каждого объекта маршрута. Этот метод просто возвращает false, если маршрут не соответствует входящему URL.</p>
<p>А вот если маршрут <strong>соответствует </strong>входящему URL, sfRoute::matchesUrl() делает несколько больше, нежели просто возвращает true. Вместо этого, маршрут возвращает массив параметров, которые объединяются с объектом запроса. Например, URL http://pete.sympalbuilder.com/location соответствует маршруту page_show, чей метод matchesUrl() вернет следующий массив:</p>
<pre>array('slug' =&gt; 'location')</pre>
<p>Эти данные будут включены в объект запроса, именно поэтому можно получить доступ к переменным маршрута (например, slug) из экшена или из других мест где это требуется.</p>
<pre>$this-&gt;slug = $request-&gt;getParameter('slug');</pre>
<p>Как вы могли предположить, переопределение метода sfRoute::matchesUrl() это отличный способ настроить маршрут и сделать почти все что угодно.</p>
<h3>Создание собственного класса маршрута</h3>
<p>Для того чтобы расширить возможности маршрута page_show для того чтобы он осуществлял проверку, основываясь на поддомене из объекта Client, мы создадим свой собственный класс маршрута. Создадим файл acClientObjectRoute.class.php и разместим его в директории lib/routing нашего проекта (вам нужно будет создать эту директорию):</p>
<pre>// lib/routing/acClientObjectRoute.class.php
class acClientObjectRoute extends sfDoctrineRoute
{
  public function matchesUrl($url, $context = array())
  {
    if (false === $parameters = parent::matchesUrl($url, $context))
    {
      return false;
    }

    return $parameters;
  }
}</pre>
<p>Следующим шагом проинструктируем маршрут page_show чтобы он использовал наш класс маршрута. В routing.yml нужно обновить ключ class в маршруте:</p>
<pre># apps/fo/config/routing.yml
page_show:
  url:        /:slug
  class:      acClientObjectRoute
  options:
    model:    Page
    type:     object
  params:
    module:   page
    action:   show
</pre>
<p>Пока что acClientObjectRoute не добавляет функциональности, но тем не менее все части на своих местах. Метод matchesUrl имеет две специфичные функции.</p>
<h3>Добавляем логику в наш маршрут</h3>
<p>Для того чтобы добавить в маршрут необходимую функциональность, заменим содержимое файла acClientObjectRoute.class.php на следующее:</p>
<pre>class acClientObjectRoute extends sfDoctrineRoute
{
  protected $baseHost = '.sympalbuilder.com';

  public function matchesUrl($url, $context = array())
  {
    if (false === $parameters = parent::matchesUrl($url, $context))
    {
      return false;
    }

    // return false if the baseHost isn't found
    if (strpos($context['host'], $this-&gt;baseHost) === false)
    {
      return false;
    }

    $subdomain = str_replace($this-&gt;baseHost, '', $context['host']);

    $client = Doctrine_Core::getTable('Client')
      -&gt;findOneBySubdomain($subdomain)
    ;

    if (!$client)
    {
      return false;
    }

    return array_merge(array('client_id' =&gt; $client-&gt;id), $parameters);
  }
}</pre>
<p>Вызов parent::matchesUrl() необходим чтобы запустить обычный процесс проверки соответствия маршрутов. В нашем примере, так как URL /location соответствует маршруту page_show, метод parent::matchesUrl() должен вернуть соответствующий параметр slug:</p>
<pre>array('slug' =&gt; 'location')</pre>
<p>Другими словами, всю тяжелую работу по сравнению маршрутов сделали за нас ))) это позволяет нам сфокусироваться на проверке соответствия, основываясь на поддомене клиента.</p>
<pre>public function matchesUrl($url, $context = array())
{
  // ...

  $subdomain = str_replace($this-&gt;baseHost, '', $context['host']);

  $client = Doctrine_Core::getTable('Client')
    -&gt;findOneBySubdomain($subdomain)
  ;

  if (!$client)
  {
    return false;
  }

  return array_merge(array('client_id' =&gt; $client-&gt;id), $parameters);
}</pre>
<p>Путем выполнения простой строковой замены мы можем изолировать кусочек, соответствующий поддомену и запросить базу данных, есть ли клиент с таким поддоменом. Если ни один клиент не соответствует этому критерию, тогда мы с чистой совестью вернем false, подразумевая этим, что запрос не соответствует маршруту. Если же мы нашли клиента с текущим поддоменом, мы объединяем дополнительный параметр client_id с возвращаемым массивом.</p>
<blockquote><p>Массив $context, передаваемый в matchesUrl заполняется кучей полезной информации о текущем запросе, включая host, is_secure, request_uri, HTTP метод и так далее&#8230;</p></blockquote>
<p>И чего же мы в конце концов добились? Класс acClientObjectRoute теперь делает следующее:</p>
<ul>
<li>Входящий $url будет соответствовать только если host содержит поддомен, соответствующий одному их клиентских объектов.</li>
<li>Если маршрут соответствует, метод возвращает дополнительный параметр &#8211; client_id соответствующего клиента, объединяемый с параметрами запроса.</li>
</ul>
<h3>Используем новый маршрут</h3>
<p>Теперь, когда acClientObjectRoute возвращает корректный параметр client_id, мы можем получить его значение через объект запроса. Например, экшен page/show может использовать client_id для определения корректного объекта Page:</p>
<pre>public function executeShow(sfWebRequest $request)
{
  $this-&gt;page = Doctrine_Core::getTable('Page')-&gt;findOneBySlugAndClientId(
    $request-&gt;getParameter('slug'),
    $request-&gt;getParameter('client_id')
  );

  $this-&gt;forward404Unless($this-&gt;page);
}</pre>
<blockquote><p>Метод findOneBySlugAndClientId() это один из магических поисковых методов(<a href="http://www.doctrine-project.org/upgrade/1_2#Expanded%20Magic%20Finders%20to%20Multiple%20Fields">magic finder</a>) &#8211; нововведение Doctrine 1.2, которое позволяет искать объекты, основываясь на нескольких полях.</p></blockquote>
<p>Маршрутизатор может предложить даже еще более элегантное решение. Для начала добавим следующий метод в класс acClientObjectRoute:</p>
<pre>protected function getRealVariables()
{
  return array_merge(array('client_id'), parent::getRealVariables());
}</pre>
<p>Теперь, экшен может полностью полагаться на маршрут, который вернет правильный объект. Теперь экшен page/show может уместиться в одну строку:</p>
<pre>public function executeShow(sfWebRequest $request)
{
  $this-&gt;page = $this-&gt;getRoute()-&gt;getObject();
}</pre>
<p>Без лишних телодвижений, этот код будет запрашивать объект Page, основываясь на обеих колонках: slug и client_id. Кроме того, как и все другие объекты маршрутов, экшен будет автоматически перенаправлен на страницу 404, если соответствующий объект не найден.</p>
<p>Как же это работает? Объекты маршрутов, например sfDoctrineRoute, который расширяет наш класс acClientObjectRoute, автоматически запрашивает соответствующий объект, основываясь на переменных в ключе <strong>url </strong>маршрута. Например, маршрут page_show, который содержит переменную :slug в его url, будет запрашивать объект Page, основываясь на колонке slug.</p>
<p>В этом приложении, маршрут page_show должен также запрашивать объекты Page, основываясь на колонке client_id. Для того чтобы добиться этого, мы переопределили метод sfObjectRoute::getRealVariables(), который вызывается внутри для определения какие колонки нужно использовать для запроса объекта. Т.о., добавив в этот массив client_id, мы дали возможность acClientObjectRoute сформировать запрос используя и slug и client_id.</p>
<blockquote><p>Объекты маршрутов автоматически игнорируют любые переменные, которые не соответствуют реальным столбцам в базе данных. Например, если URL содержит переменную :page, но соответствующая таблица такой колонки не имеет, эта переменная будет проигнорирована.</p></blockquote>
<p>Итак, теперь наш маршрут умеет все что нам необходимо, при этом мы затратили очень мало усилий чтобы достичь этого. В следующей части мы используем наш маршрут еще раз, чтобы создать админку для пользователей.</p>
<h3>Генерируем корректный URL</h3>
<p>У нас осталась одна маленькая проблема, связанная с генерацией нашего маршрута. Допустим нам нужно создать ссылку на страницу при помощи следующего кода:</p>
<pre>&lt;?php echo link_to('Locations', 'page_show', $page) ?&gt;

Генерируется url: /location?client_id=1</pre>
<p>Как вы видите, client_id автоматически добавляется к url. Это происходит потому что маршрут пытается использовать все его переменные для генерации URL. Так как маршрут теперь осведомлен о двух параметрах &#8211; slug и client_id, он использует их оба для создания маршрута.</p>
<p>Для того чтобы поправить этот баг, добавим следующий метод к классу acClientObjectRoute :</p>
<pre>protected function doConvertObjectToArray($object)
{
  $parameters = parent::doConvertObjectToArray($object);

  unset($parameters['client_id']);

  return $parameters;
}</pre>
<p>Когда объект маршрута создается, он пытается получить всю необходимую информацию, вызывая doConvertObjectToArray(). По умолчанию, client_id возвращается в массиве $parameters. Когда мы его удаляем (unset), мы предотвращаем включение его в сгенерированный URL. Помните, мы позволили себе подобную роскошь, поскольку информация о клиенте содержится в его поддомене.</p>
<blockquote><p>Вы можете переопределить doConvertObjectToArray процесс целиком и обрабатывать его самостоятельно, путем добавления метода toParams в класс модели. Этот метод должен возвращать массив параметров, которые вы хотите использовать в процессе генерации маршрута.</p></blockquote>
<p>Продолжение следует! ;&#8211;)</p>
]]></content:encoded>
			<wfw:commentRss>http://hudson.su/2010/01/14/the-symfony-2009-advent-calendar-day2-advanced-routing-p1/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

