Единая точка входа (Front Controller) — шаблон проектирования

25 января 2020
Шаблон предлагает сделать для обработки входящих обращений единый начальный обработчик, который будет выполнять роль точки входа и распределительного центра. Это делается для того, чтобы запросы собирались централизованно в одном месте и уже от туда, в зависимости от типа обращения, перенаправлялись дальше каждый своему целевому обработчику.

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

Пример задачи: создать скрипт для обработки API-запросов.

Код реализации паттерна на языке PHP и веб-сервере Apache. Скачать исходники (ZIP, 2 Кб)



По задумке, в нашем распоряжении есть тестовый сайт test-site.ru, у которого в папке /api/ мы хотим создать API-сервис. Скрипт /api/index.php будет выполнять единую точку входа для запросов. В рамках демонстрации мы реализуем шесть видов запросов:
  1. Получение списка категорий
    GET /api/categories/
  2. Получение категории с ID = 5
    GET /api/categories/5/
  3. Удаление категории с ID = 5
    DELETE /api/categories/5/
  4. Получение списка товаров
    GET /api/products/
  5. Получение товара с ID = 1
    GET /api/products/1/
  6. Удаление товара с ID = 3
    DELETE /api/products/3/

.htaccess

Файл конфигурации сервера Apache, в котором выставлено перенаправление запросов из директории /api/ в файл /api/index.php
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . index.php [L]

api.class.php

Содержит класс API для непосредственного выполнения API-методов. В классе имеются следующие методы:
  • getCategories() — возвращает список категорий;
  • getCategory($id) — возвращает сведения о категории по заданному ID;
  • deleteCategory($id) — удаляет категорию по заданному ID;
  • getProducts() — возвращает список товаров;
  • getProduct($id) — возвращает сведения о товаре по заданному ID;
  • deleteProduct($id) — удаляет товар по заданному ID.
<?

/**
 * Класс API
 */
class API
{
    /**
     * Категории товаров. В ключе элемента находится ID категории
     * @var array
     */
    private static $categories = array(
        '1' => array(
            'id' => '1',
            'name' => 'Одежда'
        ),
        '2' => array(
            'id' => '2',
            'name' => 'Книги'
        ),
        '3' => array(
            'id' => '3',
            'name' => 'Электроника'
        ),
        '4' => array(
            'id' => '4',
            'name' => 'Продукты питания'
        ),
        '5' => array(
            'id' => '5',
            'name' => 'Косметика'
        ),
        '6' => array(
            'id' => '6',
            'name' => 'Разное'
        )
    );

    /**
     * Товары. В ключе элемента находится ID товара
     * @var array
     */
    private static $products = array(
        '1' => array(
            'id' => '1',
            'name' => 'Свитер вязанный'
        ),
        '2' => array(
            'id' => '2',
            'name' => 'Пиджак'
        ),
        '3' => array(
            'id' => '3',
            'name' => 'Джинсы'
        ),
        '4' => array(
            'id' => '4',
            'name' => 'Макасины'
        ),
        '5' => array(
            'id' => '5',
            'name' => 'Ботинки туристические'
        ),
        '6' => array(
            'id' => '6',
            'name' => 'Футболка'
        )
    );

    /**
     * Возвращает список категорий
     * @return array
     */
    public static function getCategories()
    {
        return self::$categories;
    }

    /**
     * Возвращает сведения о категории
     * @param int $id - идентификатор (ID) категории
     * @return array|bool - в случае успеха вернёт массив, а в случае ошибки вернёт 'false'
     */
    public static function getCategory($id)
    {
        if (isset(self::$categories[$id])) {
            return self::$categories[$id];
        } else {
            return false;
        }
    }

    /**
     * Удаляет категорию по заданному ID
     * @param int $id - идентификатор (ID) категории
     * @return bool - в случае успеха вернёт 'true', а в случае ошибки вернёт 'false'
     */
    public static function deleteCategory($id)
    {
        if (isset(self::$categories[$id])) {
            //Удаляем категорию

            return true;
        } else {
            return false;
        }
    }

    /**
     * Возвращает список товаров
     * @return array
     */
    public static function getProducts()
    {
        return self::$products;
    }

    /**
     * Возвращает сведения о товаре
     * @param int $id - идентификатор (ID) товара
     * @return array|bool - в случае успеха вернёт массив, а в случае ошибки вернёт 'false'
     */
    public static function getProduct($id)
    {
        if (isset(self::$products[$id])) {
            return self::$products[$id];
        } else {
            return false;
        }
    }

    /**
     * Удаляет товар по заданному ID
     * @param int $id - идентификатор (ID) товара
     * @return bool - в случае успеха вернёт 'true', а в случае ошибки вернёт 'false'
     */
    public static function deleteProduct($id)
    {
        if (isset(self::$products[$id])) {
            //Удаляем товар
            
            return true;
        } else {
            return false;
        }
    }
}

?>

index.php

Содержит скрипт, обрабатывающий все входящие запросы к API. Обратите внимание, что для подготовки ответа на каждый вид запроса используется свой отдельный метод из класса API.
<?
header('Content-Type: application/json');

//Подключаем класс для работы с API
require_once(__DIR__.'/api.class.php');

$return = array(
    'status' => '',
    'status_desc' => ''
);

$requestType = $_SERVER['REQUEST_METHOD'];

$requestPath = parse_url($_SERVER['REQUEST_URI']);
$requestPath = explode('/', trim($requestPath['path'], '/'));

//Метод API
$method = '';
if (isset($requestPath[1]) && $requestPath[1] != '') {
    $method = trim($requestPath[1]);
}

//ID элемента
$elementId = '';
if (isset($requestPath[2]) && $requestPath[2] > 0) {
    $elementId = (int)$requestPath[2];
}

if ($method !='') {
    //КАТЕГОРИИ
    if ($method == 'categories') {
        if ($requestType == 'GET') {
            //Получение списка категорий
            if ($elementId === '') {
                $return['categories'] = API::getCategories();
                $return['status'] = 'success';
            } 
            //Получение конкретной категории
            else {
                $return['category'] = API::getCategory($elementId);
                if (is_array($return['category']) == true) {
                    $return['status'] = 'success';
                } else {
                    $return['status'] = 'error';
                    $return['status_desc'] = 'Не удалось найти категорию с ID = '.$elementId;
                }
            }
        } if ($requestType == 'DELETE') {
            //Удаление категории
            if ($elementId > 0) {
                if (API::deleteCategory($elementId) == true) {
                    $return['status'] = 'success';
                    $return['status_desc'] = 'Категория с ID = '.$elementId.' удалена';
                } else {
                    $return['status'] = 'error';
                    $return['status_desc'] = 'Не удалось удалить категорию с ID = '.$elementId;
                }
            }
        }
    }    
    //ТОВАРЫ
    else if ($method == 'products') {
        if ($requestType == 'GET') {
            //Получение списка товаров
            if ($elementId === '') {
                $return['products'] = API::getProducts();
                $return['status'] = 'success';
            } 
            //Получение конкретного товара
            else {
                $return['product'] = API::getProduct($elementId);
                if (is_array($return['product']) == true) {
                    $return['status'] = 'success';
                } else {
                    $return['status'] = 'error';
                    $return['status_desc'] = 'Не удалось найти товар с ID = '.$elementId;
                }
            }
        } if ($requestType == 'DELETE') {
            //Удаление товара
            if ($elementId > 0) {
                if (API::deleteProduct($elementId) == true) {
                    $return['status'] = 'success';
                    $return['status_desc'] = 'Товар с ID = '.$elementId.' удалён';
                } else {
                    $return['status'] = 'error';
                    $return['status_desc'] = 'Не удалось удалить товар с ID = '.$elementId;
                }
            }
        }
    } 
    //Ошибка. Указан недоступный метод
    else {
        $return['status'] = 'error';
        $return['status_desc'] = 'Указан недоступный метод';
    }
} else {
    $return['status'] = 'error';
    $return['status_desc'] = 'Не удалось определить метод API';
}

if ($return['status'] == '') {
    $return['status'] = 'error';
    $return['status_desc'] = 'По вашему запросу не удалось подобать нужный метод';
}

echo json_encode($return);

?>

Результат выполнения запроса на получение списка категорий /api/categories/
{
    "status": "success",
    "status_desc": "",
    "categories": {
        "1": {
            "id": "1",
            "name": "Одежда"
        },
        "2": {
            "id": "2",
            "name": "Книги"
        },
        "3": {
            "id": "3",
            "name": "Электроника"
        },
        "4": {
            "id": "4",
            "name": "Продукты питания"
        },
        "5": {
            "id": "5",
            "name": "Косметика"
        },
        "6": {
            "id": "6",
            "name": "Разное"
        }
    }
}

Результат выполнения запроса на получение сведений о товаре с ID = 1 /api/products/1/
{
    "status": "success",
    "status_desc": "",
    "product": {
        "id": "1",
        "name": "Свитер вязанный"
    }
}

Результат выполнения запроса на удаление товара с ID = 4 /api/products/4/
{
    "status": "success",
    "status_desc": "Товар с ID = 4 удалён"
}