Описан процесс создания простого модуля выводящего статистику посетителей на основе их IP адресов. Целью является ознакомление с процессом создания модулей, так же в результате получится модуль, который можно использовать для анализа посещаемости вашего сайта. Я разработал данный способ сбора информации год назад и использовал его на нескольких сайтах, в т.ч. на WordPress. Сейчас перенес на Yii2 и сделал в виде отдельного модуля.

Данный модуль будет хранить информацию в базе данных MySql и иметь такие возможности:
  • выводить отдельно статистику по посетителям сайта (людям) и поисковым ботам;
  • выводить статистику по-умолчанию за 3 последних дня;
  • выводить данные на определенную дату;
  • выводить данные за определенный период;
  • выводить данные по конкретному IP адресу;
  • содержать черный список, в который можно добавлять определенные IP, которые не нужно учитывать в статистике (например IP админа), так же можно указывать комментарий к такому адресу;
  • возможность удалять определенные IP из черного списка;
  • возможность удалять старые данные из БД.

Какую информацию можно будет получить используя данный модуль:
  • IP адрес посетителя;
  • время посещения;
  • URL страницы которую просматривали.
Информация выводится в виде таблиц.
По посетителям (людям):



По поисковым ботам:



Выбор нужных данных осуществляется с помощью форм:



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

Приступим к созданию модуля статистики.
Необходимо создать 2 таблицы в базе данных MySql в одной из которых будет храниться информация по людям посетившим сайт, в другой по поисковым ботам.
Для автоматического создания таблиц используем механизм миграций. Создание пустого файла миграции – выполнить в консоли находясь в корне сайта:
yii migrate/create ksl_stat_ip
Создастся файл в папке console\migrations куда вставить:
<?php

use yii\db\Migration;

class m170302_182527_ksl_stat_ip extends Migration
{
    public function safeUp()
    {
        $tableOptions = null;
        //Опции для mysql
        if ($this->db->driverName === 'mysql') {
            $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
        }

        //Создание таблицы IP пользователей
        $this->createTable('{{%ksl_ip_count}}', [
        'id' => $this->primaryKey(),
        'ip' => $this->string(15)->notNull(),
        'str_url' => $this->string(50),
        'date_ip' => $this->integer(),
        'black_list_ip' => $this->boolean()->defaultValue(0)->notNull(),    
        'comment' => $this->string(50),
        ], $tableOptions);

        //Создание таблицы IP ботов
        $this->createTable('{{%ksl_ip_bots}}', [
        'id_bot' => $this->primaryKey(),
        'bot_ip' => $this->string(15)->notNull(),
        'str_url' => $this->string(50),
        'bot_name' => $this->string(30),
        'date' => $this->integer(),
        ], $tableOptions);
    }

    public function safeDown()
    {
        $this->dropTable('{{%ksl_ip_count}}');
        $this->dropTable('{{%ksl_ip_bots}}');
    }
}
при этом название класса исправить на то, которое получилось при создании файла миграции (название файла).



Далее создаем папки и файлы модуля. Для этого первым делом используем GII генератор модулей:


Каждый модуль объявляется с помощью класса, который наследуется от yii\base\Module. Этот класс должен быть помещен в корне модуля и поддерживать автозагрузку (то есть размещаться в отдельном файле с соответствующим названием, с указанием пространства имен).
После генерации модуля, появится такой файл в папке statistics - StatModule.php с содержимым:
<?php

namespace common\modules\statistics;

class StatModule extends \yii\base\Module
{
    public $controllerNamespace = 'common\modules\statistics\controllers';

    public function init()
    {
        parent::init();
    }
}
Так же автоматически создастся файл контроллера - statistics\controllers\DefaultController.php. Переименуем его в StatController.php и заполним:
<?php

namespace common\modules\statistics\controllers;

use Yii;
use yii\web\Controller;
use common\modules\statistics\models\Count;
use common\modules\statistics\models\Bot;

class StatController extends Controller
{

    public function actionIndex()
    {

    $count_model = new Count(); //модель Count
    $bot_model = new Bot(); //модель Bot

    $condition = [];
    $days_ago = null;
    $stat_ip = false;

    //Получение данных из формы для модели Count
    if ($count_model->load(Yii::$app->request->post())){

        //Сброс фильтров    
        if($count_model->reset){
            $condition = [];
        }
        //Вывод по дате
        if($count_model->date_ip){
            $timeUnix = strtotime($count_model->date_ip);
            time_max = $timeUnix + 86400;
            $condition = ["between", "date_ip", $timeUnix , $time_max];
        }
        //За период
        if($count_model->start_time){

        $timeStartUnix = strtotime($count_model->start_time);
        //Если не передана дата конца - ставим текущую
        if(!$count_model->stop_time) {
            $timeStopUnix = time();
        } else {
            $timeStopUnix = strtotime($count_model->stop_time);
        }
        $timeStopUnix += 86400; //целый день (до конца суток)
        $condition = ["between", "date_ip", $timeStartUnix , $timeStopUnix];    
        }
        //По IP
        if($count_model->ip){
            $condition = ["ip" => $count_model->ip];
            $days_ago = 86400 * 30; //за 30 дней
            $stat_ip = true;
        }
        //Добавить в черный список
        if($count_model->add_black_list){
            $count_model->set_black_list($count_model->ip, $count_model->comment);
            $condition = [];
           $days_ago = null;
        }
        //Удалить из черного списка
        if($count_model->del_black_list){
            $count_model->remove_black_list($count_model->ip);
            $condition = [];
            $days_ago = null;
        }
        //Удалить старые данные
        if($count_model->del_old){
            $count_model->remove_old();
            Yii::$app->end();  //PJAX
        }
        $count_model = new Count(); //новый объект модели для очистки формы
    }

    //Статистика по поисковым ботам
    if ($bot_model->load(Yii::$app->request->post())){
        if($bot_model->get_bot_stat){
            $bot_model->by_bot();
            Yii::$app->end();  //PJAX
        }
    }

    //Получение списка статистики
    $count_ip = $count_model->getCount($condition, $days_ago);
    /*
     * Устанавливаем значение полей по-умолчанию для вывода в полях формы
     */
    $count_model->date_ip = time(); //сегодня
    $count_model->start_time = date('Y-m-01'); //первое число текущего месяца
    $count_model->stop_time = time(); //сегодня

    return $this->render('index',[
        'count_model'=> $count_model,
        'bot_model'=> $bot_model,
        'count_ip'=> $count_ip, //статистика
        'stat_ip' => $stat_ip, //true если фильтр по определенному IP
    ]);
    }
}
Здесь у нас единственное действие, которое будет получать данные из форм методом Post и передавать их в соответствующие модели. Далее данные, после обработки моделями передаются в вид
index.php. Данный файл вида (представления) так же сгенирируется автоматически (statistics\views\default\index.php).
Модули могут иметь так же и свой шаблон для файлов видов (файл main.php в папке layouts), но я решил использовать стандартный шаблон для видов frontend, который подключится сам. Если захотите создать отдельный шаблон, то укажите в в config\main.php в массиве "statistics":
  'layout' =>'main', 
После этого можете создавать отдельный шаблон модуля, файл common\modules\statistics\views\layouts\main.php
Заполним файл вида index.php:
<?php 
    use yii\widgets\ActiveForm;
    use yii\helpers\Html;
    use yii\jui\DatePicker;
    use yii\widgets\Pjax;
    
    common\modules\statistics\assets\StatAsset::register($this); //стили
?>  
    
<h3 class="stat_center">Статистика посещений по IP</h3>
<div id="stat_ip">

<?php echo $this->render('default',[
    'count_ip'=> $count_ip,
    'stat_ip' => $stat_ip,
]); ?>

<?php $form = ActiveForm::begin(); ?>
    <?=$form->field($count_model, 'reset')->hiddenInput(['value' => true])->label(false)?>
    <div class="button-reset">
    <?=Html::submitButton('Сбросить фильтры'); ?>
</div>
<?php ActiveForm::end(); ?>    
<hr>    

<?php $form = ActiveForm::begin(); ?>
    <h3>Сформировать за указанную дату</h3>
    <?=$form->field($count_model, 'date_ip')->widget(DatePicker ::classname(), [
        'dateFormat' => 'dd.MM.yyyy',
        'language' => 'ru',    
        'clientOptions' => [ 
            'yearRange' => '2015:2025',
            'changeMonth' => 'true',
            'changeYear' => 'true',
            'firstDay' => '1',
        ]  
    ])->label(false) ?> 
    <?=Html::submitButton('Отфильтровать'); ?>
<?php ActiveForm::end(); ?>    
<hr>    
        
<?php $form = ActiveForm::begin(); ?>
    <h3>Сформировать за выбранный период </h3>
    <?=$form->field($count_model, 'start_time')->widget(DatePicker ::classname(), [
        'dateFormat' => 'dd.MM.yyyy',
        'language' => 'ru',  
        'clientOptions' => [ 
            'yearRange' => '2015:2025',
            'changeMonth' => 'true',
            'changeYear' => 'true',
            'firstDay' => '1',  
        ]  
    ])->label('Начало') ?> 
    <?=$form->field($count_model, 'stop_time')->widget(DatePicker ::classname(), [
        'dateFormat' => 'dd.MM.yyyy',
        'language' => 'ru',  
        'clientOptions' => [ 
            'yearRange' => '2015:2025',
            'changeMonth' => 'true',
            'changeYear' => 'true',
            'firstDay' => '1',  
        ]  
    ])->label('Конец  ')  ?> 
    <?=Html::submitButton('Отфильтровать'); ?>
<?php ActiveForm::end(); ?>
<hr>    
      
<?php $form = ActiveForm::begin(); ?>
    <h3>Сформировать по определенному IP</h3>
    <?=$form->field($count_model, 'ip', [
        'inputOptions' => [
        'size'=> 20,
    ]])->textInput(['value'=>'127.0.0.1'])->label('IP') ?> 
    <?=Html::submitButton('Отфильтровать'); ?>
<?php ActiveForm::end(); ?>    
<hr>

<h3>Черный список IP</h3>
<p>Под черным списком понимаются IP, по которым не нужна статистика, например IP администратора сайта.
Поисковые боты отфильтровываются специальной функцией и попасть в общую статистику не должны.
<br>По данным IP статистика не будет сохраняться с момента добавления в черный список.</p>
    
<table>
    <tr class='tr_small'>
    <?php 
    $black_list = $count_model->count_black_list();  

    echo "<h4>Сейчас в черном списке:</h4>";
    foreach($black_list as $key=>$value){
        echo "<td>". $value['ip'];
        if(!empty($value['comment'])) echo " - ". $value['comment'];
        echo "</td>";
    } 
    if(count($black_list)==0) echo "<td>Черный список пуст.</td>";
    ?> 
    </tr>    
</table> 
<br>
    
<?php $form = ActiveForm::begin(); ?>    
    <?=$form->field($count_model, 'ip', [
        'inputOptions' => [
        'size'=> 20,
    ]])->textInput(['value'=>'127.0.0.1'])->label('IP') ?> 

    <?=$form->field($count_model, 'comment', [
        'inputOptions' => [
        'size'=> 20,
    ]])->label('Комментарий') ?> 
  
    <?=$form->field($count_model, 'add_black_list')->hiddenInput(['value' => true])->label(false)?>
    <?=Html::submitButton('Добавить в черный список'); ?>
<?php ActiveForm::end(); ?>    
<br>

<?php $form = ActiveForm::begin(); ?>
    <?=$form->field($count_model, 'ip', [
        'inputOptions' => [
        'size'=> 20,
    ]])->textInput(['value'=>'127.0.0.1'])->label('IP') ?> 
    <?=$form->field($count_model, 'del_black_list')->hiddenInput(['value' => true])->label(false)?>
    <?=Html::submitButton('Удалить из черного списка'); ?>
<?php ActiveForm::end(); ?>
<hr>
    
<h3>Статистика по поисковым роботам за последний месяц</h3> 
<?php Pjax::begin(['enablePushState' => false]); ?>
     <?php $form = ActiveForm::begin([
        'options' => [
        'data-pjax' => true,
    ]]); ?>
    <?=$form->field($bot_model, 'get_bot_stat')->hiddenInput(['value' => true])->label(false)?>
    <?=Html::submitButton('Сформировать'); ?>    
    <?php ActiveForm::end(); ?>    
<?php Pjax::end(); ?>
    <hr>
 
<h3>Очистка базы данных <span class="font_min">(старше 90 дней)</span></h3> 
<?php Pjax::begin(['enablePushState' => false]); ?>
     <?php $form = ActiveForm::begin([
        'options' => [
        'data-pjax' => true,
    ]]); ?>
    <?=$form->field($count_model, 'del_old')->hiddenInput(['value' => true])->label(false)?>
    <?=Html::submitButton('Удалить старые данные'); ?>    
    <?php ActiveForm::end(); ?>    
<?php Pjax::end(); ?>
</div>
В начале файла, строкой
common\modules\statistics\assets\StatAsset::register($this);
мы регистрируем файл подключающий ресурсы, в нашем случае файл со стилями.
В представлении много форм, которые передают данные в наш контроллер. Там, где требуется ввод даты - подключен виджет DatePicker, для удобного выбора даты на всплывающем календаре.
Вывод таблицы статистики по ботам и работа с черным списком IP сделаны с использованием PJAX (без перезагрузки страницы).

Вывод таблицы статистики я вынес в отдельный файл, подключаем его кодом:
<?php echo $this->render('default',[
    'count_ip'=> $count_ip,
    'stat_ip' => $stat_ip,
]); ?>

Файл default.php создаем в папке с видами modules\statistics\views\stat
<?php
    $transition = 1; //счетчик переходов на страницы
    $count = 0; //счетчик посетителей
    $count_day = 0;
    $num_ip = ''; 
    //получаем первую дату из массива объектов
    if (isset($count_ip[0])){
        $date = date("d.m.Y",$count_ip[0]->date_ip);
    } else $date = null;    
    ?>
     
    <table class='get_table'>
    <thead>
    <tr>
    <th>Переходы на страницы сайта</th>
    <th>IP</th>
    <th>URL просматриваемой страницы</th>
    <th>Время посещения</th>
    </tr>
    </thead>
    <tbody> 

    <?php foreach ($count_ip as $key=>$value){
    //кол-во посетителей по дням (вывод последнего дня после цикла)
    if($date && $date != date("d.m.Y",$value->date_ip)) {
        echo $date . ' - '. $count_day . '<br>';
        $date = date("d.m.Y",$value->date_ip);
        $count_day = 0;
    }
    if ($stat_ip) $count_day++; //для фильтра по определенному IP

    //Если сменился IP, то включаем счетчики
    if ($num_ip != $value->ip){
        $num_ip = $value->ip;
        $transition = 1;
        $count++;
        if (!$stat_ip) $count_day++; //для фильтра по определенному IP
        } else {
        $transition++;
        } 

        echo "<tr ";
        if ($transition == 1) {
           echo "class='tr_first'><td colspan='4'>Новый посетитель.</td></tr>"; 
        } else {
       echo ">"; 
        }
        echo "<td>$transition</td>
        <td><a href='http://speed-tester.info/ip_location.php?ip=".$value->ip."'>".$value->ip."</a></td>   
        <td><a href='".$value->str_url."'>".$value->str_url."</a></td>                     
        <td>".date("d.m.Y H:i:s",$value->date_ip)."</td></tr>";   
  
    }
    //вывод кол-ва посетителей за последнее число 
    if($date) echo $date . ' - '. $count_day . '<br>';
    ?>
    <p>Всего посетителей за период - <?=$count?></p>
    </tbody>
    </table>
IP выводим в виде ссылки на сервис speed-tester.info, который позволяет узнать информацию по передаваемому в GET параметре IP, а именно - его местонахождение.
Как писалось выше, подключение ресурсов (файла стилей) я вынес в файл Asset. Создадим его. Для этого создаем в корневой папке модуля (statistics) папку assets, а в ней файл StatAsset.php:
<?php
namespace common\modules\statistics\assets;

use yii\web\AssetBundle;

class StatAsset extends AssetBundle
{
    public $sourcePath = '@moduleStat/web/';
    public $css = ['css/style_ip.css'];
    public $js = [];
}
В переменной $sourcePath указываем путь к файлам ресурсов. @moduleStat - это алиас пути, который создадим позже. Ресурсы для модуля будем размещать в привычном месте - папке web в корне самого модуля. В данном файле подключаем файл style_ip.css, который размещаем в папке css. Добавим в него:
#stat_ip{
    max-width: 1920px;
    border: 3px solid blue;
    margin-right: 10px;
    padding: 10px;
}
.stat_center{
    text-align: center;
    color: blue;
} 
#stat_ip .get_table{
    margin: 0 auto;
    margin-bottom: 5px;
}
#stat_ip .get_table, #stat_ip .get_table th{
    border-collapse: collapse;  
    border: 3px solid green; 
    width: 90%;
}  
#stat_ip .get_table th, #stat_ip .get_table tr, #stat_ip table td {   
    border: 1px dashed green; 
    padding: 3px;  
    width: 20%;
    text-align: center;
}
#stat_ip .get_table th {
    border: 3px solid green; 
}

#stat_ip .font_min{
    font-size: 0.7em;
}
#stat_ip h3{
    padding-top: 20px;
}
.red {
    color: red;
}
.tr_small{
    display: inline-block;
    
}
#stat_ip .get_table .tr_first  {
    line-height: 40px;
    font-weight: bold;
    border-top: 2px solid green; 
    border-bottom: 1px solid green; 
}


#stat_ip .form-group{
    display: inline-block;
    margin-bottom: 0px !important;
}
#stat_ip button[type='submit']{
    margin-left: 5px;
}

#stat_ip .button-reset{
    text-align: center;
}

Осталось создать основной функционал - модели модуля. Для этого так же воспользуемся Gii генератором



Корректируем появившиеся файлы.
Файл Count.php:
<?php
namespace common\modules\statistics\models;

use Yii;
use yii\db\ActiveRecord;

class Count extends ActiveRecord
{
    const STAT_DEFAUL = 2; //2 дня + сегодняшний
    
    public $start_time;
    public $stop_time;
    public $add_black_list;
    public $del_black_list;
    public $del_old;
    public $reset;

    public static function tableName()
    {
        return '{{%ksl_ip_count}}';
    }

    public function rules()
     {
         return [
             [['ip'], 'required'],
             [['str_url'], 'url'],
             [['date_ip', 'start_time', 'stop_time', 'add_black_list', 'del_black_list', 'del_old', 'reset'], 'safe'],
             [['black_list_ip'], 'boolean'],
             [['comment'], 'string'],
         ];
     }

    //проверка наличия IP в черном списке (которые не надо выводить и сохранять в БД)
    //если есть хоть одна строка, то вернет true
    public function inspection_black_list($ip){
        $check = $this->
            find()->
            where(['ip' => $ip])->
            andWhere(['black_list_ip' => 1])->
            one();
        if ($check) return true;   
    }   

    public function setCount($ip, $str_url, $black_list_ip = 0){
        $this->ip = $ip;
        $this->str_url = $str_url;
        $this->date_ip = time();
        $this->black_list_ip = $black_list_ip;
        $this->save();    
    }
    
    public function getCount($condition = null, $days_ago = null){

        $sec_todey = time() - strtotime('today'); //сколько секунд прошло с начала дня
        //за сколько дней показывать по-умолчанию (позавчера/вчера/сегодня)
        if (!$days_ago) $days_ago = time() - (86400 * self::STAT_DEFAUL) - $sec_todey;

        if(in_array( 'date_ip',$condition)) {
            $count_ip = $this->find()
            ->where(['not',['black_list_ip' => 1]])
            ->andWhere($condition)
            ->orderBy('date_ip desc')
            ->all();
        } elseif($condition){
            $count_ip = $this->find()
            ->where(['not',['black_list_ip' => 1]])
            ->andWhere(['>','date_ip', $days_ago])
            ->andWhere($condition)
            ->orderBy('date_ip desc')
            ->all();
            } else {
       $count_ip = $this->find()
        ->where(['not',['black_list_ip' => 1]])
        ->andWhere(['>','date_ip', $days_ago])
        ->orderBy('date_ip desc')
        ->all();
        }
        return $count_ip;
    }
    
    //выборка номеров IP которые в черном списке
    public function count_black_list(){
        $black_list = (new \yii\db\Query())
            ->select('ip')
            ->from('{{%ksl_ip_count}}')
            ->where(['black_list_ip' => 1])
            ->distinct() //уникальные значения
            ->all();
        //По полученному массиву IP получаем значение ячейки "comment"
        foreach ($black_list as $key=>$arr){
            $rez = self::find()->where(['ip' => $arr['ip']])->one();    
            $black_list[$key]['comment'] = $rez->comment;
        }
        return $black_list;   
    }
    
    //Добавить в черн список
    public function set_black_list($ip, $comment){
        $verify_black_list = self::find()->where(['ip' => $ip])->all();
        //Если такой IP уже есть
        if($verify_black_list){
        foreach ($verify_black_list as $str){
            $str->black_list_ip = 1;
            $str->comment = $comment;
            $str->save();
        }    
        } else {
            $this->ip = $ip;
            $this->black_list_ip = 1;
            $this->comment = $comment;
           $this->save();
           }
    }
    //Удаление из черного списка
    public function remove_black_list($ip){
        $verify_black_list = self::find()->where(['ip' => $ip])->all();
        foreach ($verify_black_list as $str){
            $str->black_list_ip = 0;
            $str->comment = null;
            $str->save();
       }
    }
    
    //Удаление данных старше 90 дней
    public function remove_old(){
        $today = time();
        $old_time = $today - (86400*90);
        $old = self::find()->where(['<','date_ip', $old_time])->all();
        foreach($old as $str){
            $str->delete();
       }
        echo '<p class="red">Удалено '. count($old) . ' строк.</p>';
    }    
}
Файл Bot.php:
<?php
namespace common\modules\statistics\models;

use Yii;
use yii\db\ActiveRecord;

class Bot extends ActiveRecord
{
    const AGE_BOT = 30; //сколько дней хранить статистику по ботам
    public $get_bot_stat;

    public static function tableName()
    {
        return '{{%ksl_ip_bots}}';
    }

     public function rules()
     {
         return [
             [['bot_ip'], 'required'],
             [['str_url'], 'url'],
             [['date'], 'integer'],
             [['bot_name'], 'string'],    
             [['get_bot_stat'], 'safe'],
         ];
     }

    //Проверяем, является ли посетитель роботом поисковой системы.
    static function isBot(&$botname = ''){
        $bots = array(
        'rambler','googlebot','aport','yahoo','msnbot','turtle','mail.ru','omsktele',
        'yetibot','picsearch','sape.bot','sape_context','gigabot','snapbot','alexa.com',
        'megadownload.net','askpeter.info','igde.ru','ask.com','qwartabot','yanga.co.uk',
        'scoutjet','similarpages','oozbot','shrinktheweb.com','aboutusbot','followsite.com',
        'dataparksearch','google-sitemaps','appEngine-google','feedfetcher-google',
        'liveinternet.ru','xml-sitemaps.com','agama','metadatalabs.com','h1.hrn.ru',
        'googlealert.com','seo-rus.com','yaDirectBot','yandeG','yandex',
        'yandexSomething','Copyscape.com','AdsBot-Google','domaintools.com',
        'Nigma.ru','bing.com','dotnetdotcom'
        );
        foreach($bots as $bot)
        if(stripos($_SERVER['HTTP_USER_AGENT'], $bot) !== false){
          $botname = $bot;
          return $botname;
        }
      return false;
    }

    //Выводит таблицу статистики по поисковым ботам    
    protected function get_table_bot($data_bot){
        echo "   
            <table class='get_table'>
                <thead>
                 <tr>
                    <th align='center'>№</th>
                    <th align='center'>Чей бот</th>
                    <th>IP</th>
                    <th>URL</th>
                    <th>Время посещения</th>
                </tr>
                </thead>
                <tbody>"; 
        foreach ($data_bot as $key =>$value){
            $key = $key+1;
            echo "   
                <tr>
                    <td>$key</td>
                    <td>".$value['bot_name']."</td>
                    <td>".$value['bot_ip']."</td>
                    <td>".$value['str_url']."</td>
                    <td>".date("d.m.Y H:i:s",$value['date'])."</td>    
                </tr>";           
        }
            echo " 
                </tbody>
            </table>";
    }
    
    //Вывод статистики посещений поисковых ботов
    public function by_bot(){
        //Удаляем ботов которые в таблице больше месяца
        $old_bot = self::AGE_BOT * 86400;
        $array_del_bot = $this->find()->where(['<','date', time() - $old_bot])->all();
       foreach($array_del_bot as $bot){
            $bot->delete();
       }
        $data = $this->find()->orderBy('date desc')->all();
        $this->get_table_bot($data);    
    }
    
    //Сохранение данных бота в БД
    public function set_stat_bot($bname,$str_url,$ip){
        $this->bot_ip = $ip;
        $this->str_url = $str_url;
        $this->bot_name    = $bname;    
        $this->date = time();
        $this->save();
    }
}

Дальше созданный модуль нужно зарегистрировать в файле \frontend\config\main.php, для чего в массиве return:
'timeZone' => 'Europe/Kiev', //для правильного форматирования времени
'modules' => [
    'statistics' => [
            'class' => 'common\modules\statistics\StatModule',
    ],
],
'aliases' => [
    '@moduleStat' => '@common/modules/statistics',
],
Здесь указываем класс модуля, а так же устанавливаем временную зону для правильного преобразования часов из метки времени. Разумеется нужно указать свою временную зону. Так же создаем отдельный алиас для папки модуля, который используется, например, при подключении дополнительных ресурсов (файла стилей).
Касательно последнего рекомендую использовать ссылки на такие файлы ресурсов вместо их копирования (автоматического) в папку web\assets. Для этого добавить (если еще не используется) туда же в \frontend\config\main.php:
//не дублировать ресурсы в web/assets, а делать символические ссылки
'assetManager' => [
    'linkAssets' => true,
],
Так же, для удобства использования, а именно для открытия страницы модуля по такой простой ссылке:
http://site.com/statistics
в компонент urlManager добавим правило :
'urlManager' => [
    'showScriptName' => false, //отключаем r=routes   
    'enableStrictParsing' => true, //запретить стандартные URL если не соответствуют правилам 
    'enablePrettyUrl' => true, //отключаем index.php
    'rules' => array(
        'statistics/' => 'statistics/stat/index', //модуль статистики    
...
теперь не нужно указывать название контроллера и действия, а только название модуля, т.к. все равно класс контроллера у нас состоит из всего одного метода.

Осталось создать класс работающий с нужными методами моделей Count и Bot для подсчета статистики. Назовем файл CountKsl.php и разместим в корне модуля на одном уровне с его главным файлом StatModule.php.
<?php
/*
 * Сохраняет в БД IP посетителя
 */
namespace common\modules\statistics;

use Yii;
use common\modules\statistics\models\Count;
use common\modules\statistics\models\Bot;

    class CountKsl
{
    static function init(){
        $ip = Yii::$app->request->userIP; //получаем IP текущего посетителя

        $count_model = new Count(); //модель Count
        $bot_model = new Bot(); //модель Bot

        $str_url =  "http://" . $_SERVER['SERVER_NAME'] . $_SERVER["REQUEST_URI"]; //URL текущей страницы

        //Проверка на бота
        $bot_name = $bot_model->isBot();

        if($bot_name){
            $bot_model->set_stat_bot($bot_name,$str_url,$ip);
        } else {
            //Проверка в черном списке
            $black = $count_model->inspection_black_list($ip);
            if(!$black){
                $count_model->setCount($ip, $str_url, 0);
            }
        }
    }
}

Для подсчета статистики, в нужных действиях контроллера(ов), выводящих страницы, по которым нужна статистика, вставить:
//Статистика посещений
  $this->on(yii\web\Controller::EVENT_AFTER_ACTION, function ($event) {
    \common\modules\statistics\CountKsl::init();
  });
это могут быть действия генерирующие главную страницу, страницу рубрик, отдельного поста, статичные страницы (например "Контакты") и тд.
Тут, по событию EVENT_AFTER_ACTION, вызывается метод init() класса CountKsl, который определяет текущий IP посетителя и выполняет требуемые действия по его проверке, сохранению и др. Так же можно вместо указания события в действиях переопределить метод контроллера behaviors(). См. статью поведения в yii2.
Все, модуль готов. Вопросы и предложения пишите в комментариях.