Пример рефакторинга «извлечение метода» в коде контроллера избранных сайтов


Исходный код

switch($action){
    default:
    case 'main':
    {
        if($request_do == 'adtb'){
            $sql->Query("SELECT * FROM utb WHERE `siteid` = $request_site AND `uid` =  ".$_SESSION['id'].";");
            if($sql->size_of_result == 0){
                $sql->Query("INSERT INTO utb VALUES(null,".$_SESSION['id'].",$request_site,'$request_url');");
            }
        }
        if($request_do == 'del' && isset($request_id)){
            $sql->Query("SELECT * FROM utb WHERE `id` = $request_id AND  `uid` =  ".$_SESSION['id'].";");
            if($sql->size_of_result != 0){
                $sql->Query("DELETE FROM utb WHERE `id` = $request_id;");
            }
        }
        if($request_do == 'save' && is_array($request_tbdata)){
            foreach($request_tbdataas$k=>$v){
                $sql->Query("UPDATE utb SET `url` = '$v' WHERE `id` = $k;");
            }
        }
        $sql->Query("SELECT `id`, `domen`, `sitename` FROM sites;");
        $sdata = $sql->GetAssoc();
        $sql->Query("SELECT *,(SELECT `domen` FROM sites WHERE `id` = utb.id) as `site` FROM utb WHERE `uid` =  ".$_SESSION['id'].";");
        $udata = $sql->GetAssoc();
        $sql->Query("SELECT `id`, `domen` FROM sites WHERE `id` NOT IN(SELECT `siteid` FROM utb WHERE `uid` = ".$_SESSION['id']." );");
        $data = $sql->GetAssoc();
        $smarty->assign('sdata',$sdata);
        $smarty->assign('data',$data);
        $smarty->assign('udata',$udata);
        $smarty->assign('id',$_SESSION['id']);
        $smarty->display('links.tpl');
        break;
    }
}

Что не так в исходном коде

Архитектура контроллера

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

В вебе стандартным является подход, когда добавление, удаление, изменение и отображение — это четыре отдельные операции, которые максимально изолированы друг от друга. Технически это выглядит как четыре отдельных веб-запроса, причём изменяющие запросы работают через HTTP-методы, отличные от GET, а после выполнения соответствующей операции с данными делают редирект на нужную страницу, уже через метод GET.

Раз действия разделены логически, то логично будет их раскидать по отдельным методам, объединённые в классы соответственно контроллеру. Такой подход тоже широко применяется в различных фреймворках.

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

Запросы к БД при отображении данных

Для меня выглядит сомнительной необходимость во втором и третьем запросах. Они оба, причём довольно запутанных способом, извлекают всю ту же информацию, что и первый запрос, только поделенную на два сегмента.

Получается, что эти два запроса предоставляют не саму информацию, а способ разделения уже имеющейся информации на сегменты.

Для меня более логичным выглядит вариант с получением нужных строк по отдельности из каждой таблицы, а потом комбинирования этой информации во всех нужных сочетаниях.

Названия

Некоторые названия предельно лаконичны: таблицв utb, переменные $data$sdata$udata$k$v. Нужно много всего знать про остальной код, чтобы видеть межжду ними разницу. Чтобы помнить разницу между datasdataudata, нужно довольно сильно напрягаться.

Для меня важно, чтобы названия переменных точно называли то, что в них хранится. Например, в переменной $request_site хранится не сам сайт, а его идентификатор, а в переменной $request_url — не просто URL, а URL сайта, который нужно добавить. Поэтому более подходящие названия: $site_id и $site_url. В переменной $request_id, опять же, находится не просто айди, а айди сайта, то есть снова $site_id.

Экранирование входных данных для избежания SQL-инъекций

Оно отсутствует.

Оптимальность алгоритмов добавления и удаления записей

Нет необходимости проверять наличие записи перед добавлением — стоит воспользоваться средством игнорировать ошибку при добавлении дублирующейся записи. Такая возможность есть и в MySQL, и в PostgreSQL.

Нет необходимости проверять существование записи перед удалением: если нужной записи нет — просто ничего не будет удалено.

Вариант рефакторинга исходного кода

class MainController extends WebController
{
    public function add() {
        $site_id = $sql->EscapeInteger($this->params['site_id']);
        $site_url = $sql->EscapeString($this->params['site_url']);
        $sql->Query("INSERT IGNORE INTO utb VALUES(null,".$_SESSION['id'].",$request_site_id,'$request_url');");
        $this->redirectToAction('show');
    }

    public function delete() {
        $site_id = $sql->EscapeInteger($this->params['site_id']);
        $sql->Query("DELETE FROM utb WHERE `id` = $site_id;");
        $this->redirectToAction('show');
    }

    public function update() {
        $urls = $this->params['urls'];
        if (is_array($urls)) {
            foreach($urlsas$url_id => $url){
                $url_id = $sql->EscapeInteger($url_id);
                $url = $sql->EscapeString($url);
                $sql->Query("UPDATE utb SET `url` = '$url' WHERE `id` = $url_id;");
            }
        }
        $this->redirectToAction('show');
    }

    public function show() {
        $sql->Query("SELECT `id`, `domen`, `sitename` FROM sites;");
        $sites = $sql->GetAssoc();

        $sql->Query("SELECT `siteid` FROM utb WHERE `uid` = {$_SESSION['user_id']}");
        $bookmark_site_ids = $sql->GetColumn();

        list ($user_sites, $other_sites) = $this->splitSites($sites, $bookmark_ids);

        $smarty->assign('all_sites', $sites);
        $smarty->assign('user_sites', $user_sites);
        $smarty->assign('other_sites', $other_sites);
        $smarty->assign('user_id', $_SESSION['user_id']);

        $smarty->display('links.tpl');
    }

    private function splitSites($all_sites, $user_bookmark_site_ids) {
        $bookmark_index = array_fill_keys($user_bookmark_site_ids, true);
        $user_sites = array_filter($all_sites, function ($site) use ($bookmark_index) { returnarray_key_exists($site['id'], $bookmark_index); });
        $other_sites = array_filter($all_sites, function ($site) use ($bookmark_index) { return !array_key_exists($site['id'], $bookmark_index); });
        return [$user_sites, $other_sites];
    }
}

Теория