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

Первым делом, прикажем маршрутизатору при запросе файла sitemap.xml обращаться к контроллеру SitemapController:

'sitemap.xml'=>'sitemap/index',

Или лучше так:

array('sitemap/index', 'pattern'=>'sitemap.xml', 'urlSuffix'=>''),

Начнём написание нашего контроллера с худшего варианта:

class SitemapController extends Controller
{
    public function actionIndex()
    {
        $urls = array();
 
        // Записи блога
        $posts = Post::model()->findAll(array(
            'condition' => 't.public = 1 AND t.date <= NOW()';
        ));        
        foreach ($posts as $post){
            $urls[] = $this->createUrl('post/view', array('id'=>$post->id, 'alias'=>$post->alias));
        }
 
        // Страницы
        $pages = Page::model()->findAll(array(
            'condition' => 't.public = 1';
        ));
        foreach ($posts as $page){
            $urls[] = $this->createUrl('page/view', array('alias'=>$page->alias));
        }
 
        // Новости
        $news = News::model()->findAll(array(
            'condition' => 't.public = 1';
        ));
        foreach ($news as $new){
            $urls[] = $this->createUrl('news/view', array('id'=>$new->id));
        }
 
        // Работы портфолио
        $works = Work::model()->findAll(array(
            'condition' => 't.public = 1';
        ));
        foreach ($works as $work){
            $urls[] = $this->createUrl('work/view', array('id'=>$work->id));
        }
 
        // Товары
        $products = Product::model()->findAll(array(
            'condition' => 't.public = 1 AND t.count > 0';
        ));
        foreach ($products as $product){
            $urls[] = $this->createUrl('product/view', array('category'=>$product->category->alias, 'id'=>$product->id));
        }
 
        // ...
 
        $host = Yii::app()->request->hostInfo;
 
        echo '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
        echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
        foreach ($urls as $url){
            echo '<url>
                <loc>' . $host . $url '</loc>
                <changefreq>daily</changefreq>
                <priority>0.5</priority>
            </url>';
        }
        echo '</urlset>';   
        Yii::app()->end();            
    }
}

Что плохого в первом варианте кода?

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

Займёмся небольшим рефакторингом, а именно:

  • Перенесём все condition из findAll внутрь моделей;
  • Аналогично скроем генерирование адресов;
  • Добавим возможность вывода времени обновления записи;
  • Вынесем XML код в представление.

Для первого пункта во всех нужных нам моделях создадим именованную группу условийpublished(). Для второго же добавим геттер getUrl():

class Post extends CActiveRecord
{
    //...
 
    public function scopes()
    {
        return array(
            'published'=>array(
                'condition'=>'t.public = 1 AND t.date <= NOW()',
            ),
        );
    }
 
    private $_url;
 
    public function getUrl()
    {
        if ($this->_url === null)
            $this->_url = Yii::app()->createUrl('post/view', array('id'=>$this->id));
        return $this->_url;
    }
}

Теперь наш контроллер скинул пару десятков лишних строк:

class SitemapController extends Controller
{
    public function actionIndex()
    {
        $items = array();        
        $items = array_merge($items, Page::model()->published()->findAll());
        $items = array_merge($items, News::model()->published()->findAll());
        $items = array_merge($items, Post::model()->published()->findAll());
        $items = array_merge($items, Work::model()->published()->findAll());
        $items = array_merge($items, Product::model()->published()->findAll());
 
        $this->renderPartial('index', array(
            'host'=>Yii::app()->request->hostInfo,
            'items'=>$items,
        ));        
    }
}
<?php echo '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <?php foreach ($items as $item): ?>
    <url>
        <loc><?php echo $host; ?><?php echo $item->getUrl(); ?></loc>
        <lastmod><?php echo date(DATE_W3C, $item->update_time); ?></lastmod>
        <changefreq>daily</changefreq>
        <priority>0.5</priority>
    </url>
    <?php endforeach; ?>
</urlset>

 Здесь мы выводим дату последнего обновления в формате W3C Datetime, используя полеupdate_time модели формата TIMESTAMP:

echo date(DATE_W3C, $item->update_time);

Если же у вас в таблице время хранится в формате DATETIME, то сначала его необходимо преобразовать функцией strtotime():

echo date(DATE_W3C, strtotime($item->update_time));

Но и это не предел. Если у всех моделей есть модификатор published(), то можно уменьшить число строк сборщика массива моделей до трёх:

class SitemapController extends Controller
{
    public function actionIndex()
    {
        $items = array();        
        foreach (array('Post', 'News', 'Page', 'Work', 'Product') as $class)
            $items = array_merge($items, CActiveRecord::model($class)->published()->findAll());
 
        $this->renderPartial('index', array(
            'host'=>Yii::app()->request->hostInfo,
            'items'=>$items,
        ));        
    }
}
<?php echo '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <?php foreach ($items as $item): ?>
    <url>
        <loc><?php echo $host; ?><?php echo $item->getUrl(); ?></loc>
        <lastmod><?php echo date(DATE_W3C, $item->update_time); ?></lastmod>
        <changefreq>daily</changefreq>
        <priority>0.5</priority>
    </url>
    <?php endforeach; ?>
</urlset>

Вот теперь можно похвастаться перед друзьями истинно «тонким» контроллером.

Указание частоты обновлений и приоритета

Порядочным поисковым роботам нужно помогать. Мы добавили поддержку параметра lastmod. Теперь добавим поля changefreq и priority. Для этого немного модифицируем последний пример:

class SitemapController extends Controller
{
    const ALWAYS = 'always';
    const HOURLY = 'hourly';
    const DAILY = 'daily';
    const WEEKLY = 'weekly';
    const MONTHLY = 'monthly';
    const YEARLY = 'yearly';
    const NEVER = 'never';
 
    public function actionIndex()
    {
        $classes = array(
            'Post' => array(self::DAILY, 0.8), 
            'News' => array(self::DAILY, 0.5), 
            'Page' => array(self::WEEKLY, 0.2), 
            'Work' => array(self::WEEKLY, 0.5), 
            'Product' => array(self::DAILY, 0.5),
        );
 
        $items = array();
        foreach ($classes as $class=>$options){
            $items = array_merge($items, array(array(
                'models' => CActiveRecord::model($class)->published()->findAll(),
                'changefreq' => $options[0],
                'priority' => $options[1],
            )));
        }
 
        $this->renderPartial('index', array(
            'items'=>$items,
            'host'=>Yii::app()->request->hostInfo,
        ));        
    }
}
<?php echo '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <?php foreach ($items as $item): ?>
        <?php foreach ($item['models'] as $model): ?>
        <url>
            <loc><?php echo $host; ?><?php echo $model->getUrl(); ?></loc>
            <lastmod><?php echo date(DATE_W3C, $model->update_time); ?></lastmod>
            <changefreq><?php echo $item['changefreq']; ?></changefreq>
            <priority><?php echo $item['priority']; ?></priority>
        </url>
        <?php endforeach; ?>
    <?php endforeach; ?>
</urlset>

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

Вынесение логики из контроллера

Код контроллера вполне можно оставить в таком состоянии. Но если кому-то не нравится нахождение всего функционала в контроллере и генерирование XML вручную, то можно пойти дальше.

Вынесем все константы и всю логику генерации карты сайта в отдельный класс:

<?php
class DSitemap
{
    const ALWAYS = 'always';
    const HOURLY = 'hourly';
    const DAILY = 'daily';
    const WEEKLY = 'weekly';
    const MONTHLY = 'monthly';
    const YEARLY = 'yearly';
    const NEVER = 'never';
 
    protected $items = array();
 
    /**
     * @param $url
     * @param string $changeFreq
     * @param float $priority
     * @param int $lastmod
     */
    public function addUrl($url, $changeFreq=self::DAILY, $priority=0.5, $lastMod=0)
    {
        $host = Yii::app()->request->hostInfo;
        $item = array(
            'loc' => $host . $url,
            'changefreq' => $changeFreq,
            'priority' => $priority
        );
        if ($lastMod)
            $item['lastmod'] = $this->dateToW3C($lastMod);
 
        $this->items[] = $item;
    }
 
    /**
     * @param CActiveRecord[] $models
     * @param string $changeFreq
     * @param float $priority
     */
    public function addModels($models, $changeFreq=self::DAILY, $priority=0.5)
    {
        $host = Yii::app()->request->hostInfo;
        foreach ($models as $model)
        {
            $item = array(
                'loc' => $host . $model->getUrl(),
                'changefreq' => $changeFreq,
                'priority' => $priority
            );
 
            if ($model->hasAttribute('update_time'))
                $item['lastmod'] = $this->dateToW3C($model->update_time);
 
            $this->items[] = $item;
        }
    }
 
    /**
     * @return string XML code
     */
    public function render()
    {
        $dom = new DOMDocument('1.0', 'utf-8');
        $urlset = $dom->createElement('urlset');
        $urlset->setAttribute('xmlns','http://www.sitemaps.org/schemas/sitemap/0.9');
        foreach($this->items as $item)
        {
            $url = $dom->createElement('url');
 
            foreach ($item as $key=>$value)
            {
                $elem = $dom->createElement($key);
                $elem->appendChild($dom->createTextNode($value));
                $url->appendChild($elem);
            }
 
            $urlset->appendChild($url);
        }
        $dom->appendChild($urlset);
 
        return $dom->saveXML();
    }
 
    protected function dateToW3C($date)
    {
        if (is_int($date))
            return date(DATE_W3C, $date);
        else
            return date(DATE_W3C, strtotime($date));
    }
}

Заметим, что в классе мы полностью автоматизировали работу с атрибутом update_timeмодели. Если это поле у модели существует, то оно автоматически переконвертируется в нужный формат и выведется в опции lastmod.

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

class SitemapController extends Controller
{
    public function actionIndex()
    {
        $sitemap = new DSitemap();
        $sitemap->addModels(Post::model()->published()->findAll(), DSitemap::DAILY, 0.8);
        $sitemap->addModels(News::model()->published()->findAll(), DSitemap::DAILY, 0.5);
        $sitemap->addModels(Page::model()->published()->findAll(), DSitemap::WEEKLY, 0.2);
        $sitemap->addModels(Work::model()->published()->findAll(), DSitemap::WEEKLY, 0.5);
        $sitemap->addModels(Product::model()->published()->findAll(), DSitemap::DAILY, 0.5);
 
        header("Content-type: text/xml");
        echo $sitemap->render();
        Yii::app()->end();
    }
}

Или расширим наш предыдущий вариант с массивом:

class SitemapController extends Controller
{
    public function actionIndex()
    {
        $classes = array(
            'Post' => array(DSitemap::DAILY, 0.8), 
            'News' => array(DSitemap::DAILY, 0.5), 
            'Page' => array(DSitemap::WEEKLY, 0.2), 
            'Work' => array(DSitemap::WEEKLY, 0.5), 
            'Product' => array(DSitemap::DAILY, 0.5),
        );    
 
        $sitemap = new DSitemap();
        foreach ($classes as $class=>$options)
            $sitemap->addModels(CActiveRecord::model($class)->published()->findAll(), $options[0], $options[1]);
 
        header("Content-type: text/xml");
        echo $sitemap->render();
        Yii::app()->end();      
    }
}

Здесь мы также создаём объект $sitemap и в цикле передаём ему наши модели.

Оптимизация производительности

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

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

Также в этом случае не стоит брать DomDocument, а лучше передавать итератор в упоминавшееся выше представление через renderPartial() и генерировать ссылки простой конкатенацией строк вместо createUrl.

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

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

class SitemapController extends Controller
{
    public function actionIndex()
    {
        if (!$xml = Yii::app()->cache->get('sitemap'))
        {    
            $classes = array(
                'Post' => array(DSitemap::DAILY, 0.8), 
                'News' => array(DSitemap::DAILY, 0.5), 
                'Page' => array(DSitemap::WEEKLY, 0.2), 
                'Work' => array(DSitemap::WEEKLY, 0.5), 
                'Product' => array(DSitemap::DAILY, 0.5),
            );    
 
            $sitemap = new DSitemap();
 
            $sitemap->addUrl('/contacts', DSitemap::WEEKLY);
 
            foreach ($classes as $class=>$options)
                $sitemap->addModels(CActiveRecord::model($class)->published()->findAll(), $options[0], $options[1]);
 
            $xml = $sitemap->render();
            Yii::app()->cache->set('sitemap', $xml, 3600*6);
        }
 
        header("Content-type: text/xml");
        echo $xml;
        Yii::app()->end();      
    }
}

Теперь для подключения нового модуля на сайте нужно добавить группу условий published и геттер getUrl() в его модель и добавить имя класса модели в этот список.

 

Взято отсюда

Комментарии

comments powered by Disqus