Делаем Sitemap для проекта на Yii

Карта и компас

Поразмышлять о вариантах создания карты сайта для проекта на фрэймворке 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
<?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
<?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
<?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
<?php
 /**
 * @author ElisDN <mail@elisdn.ru>
 * @link https://elisdn.ru
 */
 
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() в его модель и добавить имя класса модели в этот список.

Комментарии

 

lordius – google.com

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

Ответить

 

Дмитрий Елисеев

А как он сможет сам очищать кэш?

Ответить

 

lordius – google.com

Хм, я имел ввыду кеш браузера, мб я пропусти что в коде или не знаю, но вы же кеш в браузер делаете, или еще мб в бд или где то еще?
З.Ы. мб = может быть.

Ответить

 

Дмитрий Елисеев

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

Ответить

 

TranceSmile

Тут не имелось ввиду кеш браузера.

Ответить

 

TranceSmile

Отличный пост.
У Вас небольшая опечатка

const NEWER = never;
Ответить

 

Дмитрий Елисеев

Спасибо. Исправил.

Ответить

 

Myres – 3dlip.ru

Дмитрий спасибо за пост. Это то что я долго искал и не мог сделать сам.

Ответить

 

standalone

Ошибка: error on line 3 at column 1: Extra content at the end of the document

Ответить

 

Дмитрий Елисеев

В каком фрагменте кода?

Ответить

 

standalone

Не в фрагменте кода, а сразу при формировании xml при переходе на sitemap.xml

Ответить

 

Дмитрий Елисеев

Это с использованием DSitemap или без? Что в исходном коде страницы? Все ли теги закрыты?

Ответить

 

Дмитрий Елисеев

Проверил код листинга DSitemap. Полностью совпадает с рабочим кодом. Проверьте просто без добавления записей:

$dom = new DOMDocument('1.0', 'utf-8');
$urlset = $dom->createElement('urlset');
$urlset->setAttribute('xmlns','http://www.sitemaps.org/schemas/sitemap/0.9');
$dom->appendChild($urlset);
return $dom->saveXML();

будут ли ошибки?

Ответить

 

standalone

Сделайте после добавления комментария на сайте $this->refresh(), а то при перезагрузке данные сохраняются в форме

Ответить

 

Дмитрий Елисеев

Это просто сохранение в Cookie, чтобы повторно имя не набирать.

Ответить

 

Alex

Отличная статья! Мало того что это то, что я искал (не хотел подключать "толстые" расширения), так еще и подробно описан процесс эволюции кода, обьясняется необходимость этого. Т.е. отличная иллюстрация, даже УРОК того как НЕ делать "в лоб" (обычно, у меня так получается )))) а делать как следует.
Спасибо!

Ответить

 

Alex

А вот скажите, вот по Вашему мнению - нужна ли карта сайта на фронтенде (HTML)? Либо это уже пережиток прошлого? (я не говорю про случаи запутанных клубков меню\подменю - на таких сайтах просто надо меню логичнее построить). Вопрос даже такой, прагматичный: какова сегодня средняя статистика заходов на ссылки "Карта сайта" (заходы из поисковика, думаю не стоит считать)?

Ответить

 

Дмитрий Елисеев

Если на сайте большое запутанное меню или если отсутствует поиск, то можно сделать и HTML-карту. А так она не особо нужна.

Ответить

 

Alex

Вопрос:
До секции "Указание частоты обновлений и приоритета" у меня всё работало нормально.
Но когда с помощью копи-паста вставил контроллер и вьюху из секции "Указание частоты обновлений и приоритета" (оставил только модель "Post" в массиве), повалились ошибки:

Notice: Undefined index: models in \www\protected\views\sitemap\index.php on line 4
Warning: Invalid argument supplied for foreach() in \protected\views\sitemap\index.php on line 4


Дамп $items содержит правильный ассоциативный массив с ключами models,changefreq и тд.
В дампе $item вообще остутсвуют эти ключи. Поэтому после " foreach ($items as $item):" дамп показывает что в переменной $item['models']=null.

Почему-то после "foreach ($items as $item):" массив развалился ))))

Ответить

 

Дмитрий Елисеев

Исправил array(...) на array(array(...)) в этом примере. Попробуйте ещё раз.

Ответить

 

Alex

Спасибо, заработало!

Ответить

 

Alex

Аха-хаааа, как я жестко подставился..... ))) ЧАС сижу валидирую sitemap.xml. Во-первых в коде

<?php echo '<?xml version="1.0" encoding="UTF-8"?>' ?>

надо добавить после закрывающей скобки .PHP_EOL; - иначе Гугл-валидатор вам на дверь покажет ничего не обьясняя,
во-вторых (это самое забавное для меня было):

$items = array_merge($items, array(array(
    'models' => CActiveRecord::model($class)->published()->findAll(),
    'changefreq' => $options[1],
    'priority' => $options[0],
)));

надо было поменять индексы местами (10 раз смотрел на результат вывода и ниразу не заметил баг)

А для валидации карты я бы посоветовал добавить схему

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">

и перед отправкой на гугл проверить ее сначала на http://www.xmlvalidation.com (поставить галку там использовать схему). Ато как мне придется ждать пару часов пока Гугл разрешит повторный пересмотр и отправку файла карты )))

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

Ответить

 

Дмитрий Елисеев

Добавил PHP_EOL и исправил индексы, а валидатор теперь найдут в этом комментарии. Спасибо за замечания!

Ответить

 

Jakeroid – jakeroid.com

Очень полезная статья, спасибо. Прям то, что искал.

Ответить

 

данил – ves-vash-dom.ru

Здравствуйте. Скажите, а как можно было бы сделать добавление страниц, которые прошли через пагинатор, скажем такого вида http://www.ves-vash-dom.ru/blog/dizayn?page=2 при использовании вашего метода. Пока я их просто склеиваю с тем, что уже сгенерировалось. Может есть другой способ? Спасибо.

Ответить

 

Дмитрий Елисеев

Можно генерировать ссылку прямо через createUrl():

for ($i=1; $i<=$pages_count; $i++) {
    $sitemap->addUrl(Yii::app()->createUrl('blog/category', array_merge(
        array('alias'=>$model->alias),
        $i>1 ? array('page'=>$i) : array()
    )));
}

Или немного переделать метод getUrl():

public function getUrl($page=1) {
    return Yii::app()->createUrl('blog/category', array_merge(
        array('alias'=>$this->alias),
        $page>1 ? array('page'=>$page) : array())
    )
}

и для модели получать ссылку на нужную страницу:

for ($i=1; $i<=$page_count; $i++) {
    $sitemap->addUrl($model->getUrl($i));
}
Ответить

 

seydamet

Спасибо огромное за статью!

У вас очепятка в коде :) DSiteap вместо DSitemap который у нас имеется.

Ответить

 

Дмитрий Елисеев

Исправил. Спасибо!

Ответить

 

bemulima Долотов

Жестоко намудрили))) Меня вполне устраивает 2-3 вариант, спасибо за статью!

Ответить

 

bemulima Долотов

Это только у меня site.ru/sitemap.xml открывается в html формате или у вас нормально xml формате открывается, когда вводите в адресной строке адрес?

Ответить

 

bemulima Долотов

ай чёрт, забыл вот это header("Content-type: text/xml");

Ответить

 

Александр Шиллинг

Спасибо, сделал у себя на сайте, всё работает как часы.

Ответить

 

Виктор

Все четко работает, использую dsitemap
но как быть на сайте с двумя языками, пробовал делать два цикла меняя Yii::app lang но меняется не во всех ссылках гдето 90% русских к 10 английских, похоже надо рыть в гетурл (

Ответить

 

alex

а для какой версии? 1 или 2

Ответить

 

Дмитрий Елисеев

Для первой.

Ответить

 

Юрий

Спасибо, хорошее решение. Предлагаю сделать еще лучше. Файл sitemap имеет ограничение на количество адресов страниц - 50000. Старые успешные проекты запросто могут иметь на много больше. Если добавите (при достижении лимита) возможность создания нескольких файлов (sitemap1 sitemap2 и т.д.) и файла sitemapindex, то получится совсем универсально. Я общий массив резал по 50К, результаты рендерил с выводом в файл, файлы складывал в отдельную директорию (планировщиком); а по запросу генерировал sitemapindex. Может и Вы что-то подобное (а может и лучше) добавите.

Ответить

 

demonafi

А почему бы не выложить готовый модуль в архиве или github? чтобы можно было полностью посмотреть, а не собирать самостоятельно.

Ответить

 

Елена – dicapo.ru

А нет ли подобной статьи для Yii2?

Ответить

 

Дмитрий Елисеев

Поищите.

Ответить

 

Илья

А как быть с листингом категории? Нужно ли указывать ссылки на страницы типа ...?page=2 и т.д?

Ответить

 

Дмитрий Елисеев

Можно сделать, но для SEO обычно открывают только первую страницу категории.

Ответить

 

Илья

Уведомление на email приходит от www-data) пофиксите а то не комельфо)

Ответить

 

Max Гордиенко

Во круто! Взял ваш DSitemap() и вставил спокойно(чуть исправив) в Yii2, и всё работает как надо :) Спасибо большое.

Ответить

 

Sergey

А как сделать для yii2. Можно пример кода?

Ответить

 

krutik
Комментарий удалён
Ответить

 

вйвйвйв

такой гавнокод.... array все сразу понятно об уровне данного человека

Ответить

 

Дмитрий Елисеев

> array все сразу понятно...

Это статья 2013-го года.

Ответить

Оставить комментарий

Войти | Завести аккаунт | Войти через


(никто не увидит)





Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>