Использование преимуществ встроенного PHP сервера

Original author: Vito Tardia
  • Translation
  • Tutorial
Одним из крутейший новшеств в php 5.4 является встроенный сервер, созданный специально для разработки и тестирования. Теперь вы можете писать и тестировать свой код не имея полноценного веб-сервера — просто запустите встроенный сервер, протестируйте свой код, и выключите его, когда закончите.
Сервер, так же, предоставляет возможность и для творческого использования. Например, вы можете распространять портативное web-приложение на CD или USB, или даже как десктопное приложение, созданное на PHP без использования GTK или других графических библиотек.

В мануале PHP подчеркивается, что встроенный сервер предназначен для разработки и рекомендуется не использовать его на боевом сервере. Отсутствуют какие-либо INI директивы отвечающие за сервер (за исключением раскрашивания вывода в консоли), и, кажется, что основная идея документации: «у нас теперь тоже есть Web сервер, отстаньте от нас».
Несмотря на это, я считаю, что встроенные сервер может быть ценным инструментом для разработки и тестирования. К примеру, на моей машине я использую предустановленный Apache с кастомной конфигурацией, подходящей для меня, но иногда я хочу попробовать какие-либо новые Web-приложения. Со встроенным web-сервером я могу протестировать приложение прямо из папки «downloads» или временной папки и переместить в обычную среду, только тогда, когда это будет необходимо.
Но для начала, это не так просто, ведь многие написанные приложения используют .htaccess и mod_rewrite. Но я уверен, что кто-то (может быть один из вас, почему бы и нет?) напишет адаптер для этого, и я хотел бы быть первым, кто протестируют его.
В этой статье я объясню некоторые основные примеры использования встроенного сервера и покажу вам как сделать его полезным для разработки и тестирования.

Используем встроенный сервер


Итак, для использования сервера нам необходим php 5.4 или выше. Для проверки версии PHP, выполните:
php -v

Так же вы можете определить доступен ли сервер в вашей сборке выполнив:
php -h

и найдите там описание параметров "-S" и "-t", которые используются только для сервера.
Для проверки сервера вы можете создать в текущей директории файл index.php, который будет содержать в себе вызов функции phpinfo() и затем запустить сервер:
[ec2-user@ip-10-229-67-156 ~]$ php -S 127.0.0.1:8080
PHP 5.4.0RC7 Development Server started at Fri Feb 26 18:49:29 2012
Listening on 127.0.0.1:8080
Document root is /home/ec2-user
Press Ctrl-C to quit.

И теперь вы можете увидеть содержимое отданной встроенным web-сервером:
image
В консоль же будет писаться каждый запрос клиента:
[Sun Feb 26 18:55:30 2012] 80.180.55.37:36318 [200]: /
[Sun Feb 26 18:56:23 2012] 80.180.55.37:36584 [200]: /

Возвращаясь назад, разберем параметр командной строки "-S", который используется для указания адреса, с которого сервер будет доступен. Возможные значения:
localhost — сервер будет доступен только с локальной машины,
0.0.0.0 — на любому интерфейсе машины,
Любой внешний или серый IP — только на указанном IP
Параметр "-t" устанавливает указанную директорию «directory root». Например:
[ec2-user@ip-10-229-67-156 ~]$ php -S <localhost or your public IP>:8090 -t /home/ec2-user/public

Кроме того,. вы может указать имя конкретного файла-роутера. Например:
[ec2-user@ip-10-229-67-156 ~]$ php -S >localhost or your public IP>:8080 -t /home/ec2-user/public public/index.php

Вывод этого роутера будет парситься и выполняться сервером. Простой пример:
<?php
$extensions = array("php", "jpg", "jpeg", "gif", "css");

$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$ext = pathinfo($path, PATHINFO_EXTENSION);
if (in_array($ext, $extensions)) {
    // let the server handle the request as-is
    return false;  


}

echo "<p>Welcome to PHP</p>";

Если скрипт вернет FALSE, тогда запрашиваемый URI будет обрабатываться сервером, который будет выдавать запрошенный ресурс, либо вернет 404 ошибку. Если скрипт возвращает что-либо ещё, вывод скрипта передастся клиенту.
Хотя данный подход даёт нам больше контроля, есть несколько вещей, которые вы должны знать. Во-первых, PHP сервер отдаёт только минимальный набор HTTP заголовков:
Connection: closed
Content-Type: text/html
Host: aws-dev-01.vtardia.com
X-Powered-By: PHP/5.4.0RC7
D
Сравним это с заголовками, возвращаемыми сервером Apache:
Accept-Ranges: bytes
Connection: Keep-Alive
Content-Length: 631
Content-Type: text/html
Date: Sat, 04 Feb 2012 18:24:42 GMT
Etag: "bbb99-277-4ace8c5470a40"
Keep-Alive: timeout=15, max=100
Last-Modified: Wed, 14 Sep 2011 15:54:09 GMT
Server: Apache/2.2.21 (Unix) DAV/2

Если ваше приложение использует заголовки, то оно должно учитывать разницу в development-среде и в production.
Во-вторых, встроенный сервер имеет другое SAPI (Server API). Таким образом выполняя маршрутизацию в index,php вы можете определить на тестовом или боевом сервер происходит обращение к скрипту. php_sapi_name() вернет «cli-server» на встроенном сервере:
<?php
if (php_sapi_name() == "cli-server") {
    // running under built-in server so
    // route static assets and return false
    $extensions = array("php", "jpg", "jpeg", "gif", "css");
    $path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
    $ext = pathinfo($path, PATHINFO_EXTENSION);
    if (in_array($ext, $extensions)) {
        return false;  
    }
}

Существует одна специальная INI директива — «cli_server.color». Данная директива возвращает раскрашенный вывод в консоли. Создайте пустой файл с именем cli-server.ini и вставьте эту строку:
cli_server.color = on

Вы можете создать уникальную конфигурацию окружения для вашего сервера, указав в вашем INI файле необходимые директивы. Не объявленные директивы примут значения по-умолчанию. Сейчас мы объявили только одну директиву — cli_server.color.
Запустить сервер с параметром "-c" с указанием INI файла:
[ec2-user@ip-10-229-67-156 ~]$ php -S localhost -c cli-server.ini

Если ваш терминал поддерживает цвета, то вы сможете увидеть «цветной» вывод в консоли. 200 статус будет выделен зеленым, 404 — оранжевым, а ошибки сценария будут выделены красным цветом.

Создаём персональный сервер


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

Папка «library» содержит код приложения, «public» — корневая директория, содержит index.php и несколько статичных файлов. Особое внимание в этом руководстве будет уделено папке «server», и поэтому наше приложение будет состоять из простого «Hello Word!» и нескольких картинок и css.
Наша цель — получить возможность запускать сервер из директории приложения одной командой, а наш сервер будет заботиться о роутинге, HTTP заголовках и ошибках.
[ec2-user@ip-10-229-67-156 myapp]$  ./start.sh

Давайте рассмотрим сценарий запуска:
#! /bin/bash

INIFILE="$(pwd)/server/server.ini"
DOCROOT="$(pwd)/public"
ROUTER="$(pwd)/server/router.php"
HOST=0.0.0.0
PORT=8080

PHP=$(which php)
if [ $? != 0 ] ; then
    echo "Unable to find PHP"
    exit 1
fi

$PHP -S $HOST:$PORT -c $INIFILE -t $DOCROOT $ROUTER

Я предполагаю, что скрипт запускается из директории приложения, поэтому INIFILE, DOCROOT, ROUTER определяются используя pwd. Путь до php определяется используя команду which. Если php не был найден в пользовательском $PATH, то скрипт завершит работу ошибкой.
Данный способ работает достаточно хорошо, но давайте предоставим пользователю возможность изменить любой из заданных параметров из командной строки, например:
if [ ! -z $INIFILE ]; then
    INIFILE="$(pwd)/server/server.ini"
fi

Продолжим, папка «errors» содержит файлы для сообщений об HTTP ошибках. Вот пример о 403 ошибке: хотя я и использовал только HTML, скрипт будет подключен, использую include, поэтому вы можете использовать любой php код:
<!doctype html>  
<html lang="en">
 <head>
  <meta charset="utf-8">
  <title>403</title>  
 </head>
 <body>
  <h1>403: Forbidden</h1>
  <p>Sorry, the requested resource is not accessible.</p>
 </body>  
</html>


Теперь посмотрим на router.php. Задача данного файла в получении и управлении всеми запросами и передавать их серверу, только если данный файл существует. Все страницы ошибку отображаются путём подключения шаблона.
<?php
// Set timezone
date_default_timezone_set("UTC");

// Directory that contains error pages
define("ERRORS", dirname(__FILE__) . "/errors");

// Default index file
define("DIRECTORY_INDEX", "index.php");

// Optional array of authorized client IPs for a bit of security
$config["hostsAllowed"] = array();

function logAccess($status = 200) {
    file_put_contents("php://stdout", sprintf("[%s] %s:%s [%s]: %s\n",
        date("D M j H:i:s Y"), $_SERVER["REMOTE_ADDR"],
        $_SERVER["REMOTE_PORT"], $status, $_SERVER["REQUEST_URI"]));
}

// Parse allowed host list
if (!empty($config['hostsAllowed'])) {
    if (!in_array($_SERVER['REMOTE_ADDR'], $config['hostsAllowed'])) {
        logAccess(403);
        http_response_code(403);
        include ERRORS . '/403.php';
        exit;
    }
}

// if requesting a directory then serve the default index
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$ext = pathinfo($path, PATHINFO_EXTENSION);
if (empty($ext)) {
    $path = rtrim($path, "/") . "/" . DIRECTORY_INDEX;
}

// If the file exists then return false and let the server handle it
if (file_exists($_SERVER["DOCUMENT_ROOT"] . $path)) {
    return false;
}

// default behavior
logAccess(404);
http_response_code(404);
include ERRORS . "/404.php";

В первых строках я определяю некоторые глобальные параметры, такие как DIRECTORY_INDEX, директория с шаблонами ошибок. Параметр date_default_timezone_set() должен совпадать с настройками ОС, иначе будут несоответствия между записями в логе и на сервере. Так же я добавил список разрешенных IP адресов, для повышения безопасности.
Функция logAccess() необходима, потому что когда скрипт роутинга принимает запрос лог сервера по-умолчанию игнорируется. Функция принимает только код статуса, а формат вывода полностью соответствует формату сервера.
Наша первая задача — проверка безопасности. Если IP клиента не находится в массиве разрешенных IP, выводим сообщение об ошибке и завершаем работу скрипта. Нам необходимо отдавать код статуса отличный от 200 и функция header() не будет работать в здесь, поэтому мы используем новую функцию — http_response_code.
Если IP клиента находится в массиве разрешенных IP, то следующий наш шаг — получение запрашиваемого пути и расширения файла. Если расширение пустое, считаем, что пользователь запрашивает папку и строим получаем путь, используя определенный сначала DIRECTORY_INDEX.
В завершении, если запрашиваемый файл существует, возвращаем FALSE, и позволяем серверу обратиться к файлу. Если же нет, то отображается сообщение о 404 ошибке.

Резюме


Это всё. Как видите, php сервер просто в использовании. Наш персональный сервер очень прост. Код можно оптимизировать и включать в более сложные и функциональные классы. Happy coding!

p.s. С радостью приму критику и замечания к переводу в личку.
Share post

Comments 24

    +7
    И всё это добро упаковать в phar
      +7
      Вот интересно уже вышла PHP 5.4.8 кто-то реально юзает этот встроенный сервер в разработке? Опоздали с этой фичей лет на 10. Уже даже на винде всё окружение поднимается в пару кликов (а в том же Open Server, можно еще и несколько веток софта держать для тестов).
        –21
        На убунте поднять апач и настроить хосты до сих пор задача непростая и рутинная. На винде проще, да.
        Ну вот и в случаях где хосты насраивать лениво, такой сервер штука вполне удобная. Но пока ещё слабая и глючная, да.
          +24
          Рутинная? Для себя давно решил это дело так:
          echo «address=/.dev/127.0.0.1» >> /etc/dnsmasq.conf
          в конфиге nginx'a:
          server_name "~^(?.+)\.dev$";
          root /usr/share/nginx/$domain;

          Всё. Любая дира в /usr/share/nginx/directory доступна по адресу directory.dev. Никаких телодвижений.
            –7
            Ну отлично, что у вас есть лайфаки, у меня лично их нет.
            Кроме того, иногда нужно на гостевой системе что-то запустить. Там тоже сервер становится удобной штукой.
            Также, я не представляю как без него тестировать веб проекты через тот же Travis-CI.
              +2
              Я иногда так же пользуюсь встроенным сервером и считаю его очень удобной вещью, особенно для дебага, я лишь говорю про то, что настройка веб-сервера под linux легка и удобна.
                +3
                Почему вы называете лайфхаком обычное использование инструмента по назначению?
              0
              На убунте поднять апач и настроить хосты до сих пор задача непростая и рутинная. На винде проще, да.
              Отсыпьте. Нужно всего поставить один пакет и сделать несколько вариаций sites-enabled/000-default, в самом простом случае. Для тестирования одного веб-приложения можно вообще конфиги не править, а держать все в /var/www

              # apt-get install apache2 libapache2-mod-php5
                +1
                apt-get install libapache2-mod-php5
                

                Было бы достаточно
                  +1
                  Ну тогда уж
                  apt-get install phpmyadmin
                  

                  Чтобы и mysql за одним встал
            +1
            Я пользуюсь встроенным в PHP веб сервером с момента выхода 5.4
            Для небольших проектов очень удобно. все распространенные фреймворки (Yii, Symfony, Zend) запускаются сразу же без дополнительных настроек.
              0
              Хотел однажды использовать его для прогона UI-тестов через Zombie.js и столкнулся с тем, что этот самый отладочный сервер немилосердно выплёвывает segfault при любом отклонении от стандартного HTTP. В подробности не углублялся, похоже что это вина zombie.js, однако сегфолт вебсервера от ошибки протокола клиента — некрасивый поступок
                0
                Да, сталкивался с подобной проблемой. У меня сегфолт вылетал в случае, если подключен APC, правда не помню какой версии.
                +1
                Спасибо за анонс.
                Только вот глаз режет — в скрипте start.sh вместо $(pwd) стоит использовать $(dirname $0), а ещё лучше $(dirname $(readlink -f $0)). В последнем случае мы всегда будем иметь дело с абсолютными путями, хотя и упрщённый способ тоже действует прекрасно. Т.о. мы не привязаны к выполнению скрипта именно из его каталога.
                  0
                  >Со встроенным web-сервером я могу протестировать приложение прямо из папки «downloads» или временной папки и переместить в обычную среду, только тогда, когда это будет необходимо.
                  К сожалению большинство современных WEB-приложений требуют специфичный .htaccess, а я активно использую специфичный nginx.conf
                  твоя статья позновательна и полезна, но не является серебряной пулей, как ты ее представил
                    +1
                    Пишете РНР 5.4, а до сих пор используете dirname(__FILE__). Есть же __DIR__
                      0
                      причем, __DIR__ еще с 5.3.0 появилась.
                      0
                      Встроенный веб-сервер удобно использовать, когда есть кучка мелких проектов, под которые нет необходимости настраивать отдельные vhost'ы. Или разные ветки одного проекта, которые удобнее держать в отдельных папках, а не переключать внутри одного докрута.

                      Я рельсовый вебрик с самого его появления так использую, и то, что такой сервер наконец появился в PHP — это прекрасно. Не прошло и 10 лет, действительно =)
                        0
                        Вообще очень радуют последние изменения в php. Надеюсь что так и будет продолжаться.
                        –1
                        Прочел статью, где преимущества? Может для «hello world» то да, для мало мальского проекта это бесполезная хрень.
                          –1
                          Преимущества перед чем? Перед поднятием связки ngninx+php-fpm или apache+mod-php локально? Плюс виртхосты для каждого проекта?
                            0
                            Пост называется «Использование преимуществ встроенного PHP сервера» вот и у меня возникает вопрос — перед чем?
                        0
                        Вначале чтения подумал, что спрятать бы это dev-веб-сервер за nginx-ом, и получим… что-то такое, более похожее на живое решение.

                        Но — заголовки (их отсутствие), привычка делать segfault (вместо самостоятельного выкручивания из проблемы и вывода юзеру сообщения о том, что же не так) — все это нифига не смотрится на чем-то отличном от 127.0.0.1/hello_world.php. Мне php-fpm ближе :)

                        Only users with full accounts can post comments. Log in, please.