[Hexlet] Веб-разработка на PHP - Часть 2

Веб-разработка на PHP Model-View-Controller (MVC)

Архитектура веб-приложений, в первую очередь, определяется самой природой веба, тем, как работает HTTP. Последовательность запрос-обработка-ответ — базис, на который нанизывается все остальное. Фреймворки идут дальше и разделяют приложение на дополнительные слои уже внутри самого процесса обработки запроса. Такое разделение напрашивается само собой, без него код быстро превращается в мешанину из запросов к базе данных, формирований html и логики обработки данных.

Из обработки запроса естественным образом выделяется слой шаблонов, на основе которых генерируется HTML. Этот слой принято называть View. Кроме него, как минимум, выделяют еще два слоя: Model и Controller. Остальное добавляется по мере роста сложности приложения. Аббревиатура MVC (Model-View-Controller) — тема нашего урока.

Существует две разных версии MVC. Та, которая была придумана изначально (в сообществе SmallTalk программистов), создавалась под толстые клиенты, а они представляют собой событийные системы, как современные фронтенд приложения. На сервере используется другая вариация MVC, которая называется MVC v2. Буквы в ней те же, но означают местами другое, и самое главное, что совсем по-другому строится взаимодействие.

Архитектурный шаблон MVC задает основную структуру приложения и позволяет коду достаточно долго развиваться, оставаясь удобным в поддержке. MVC, с некоторыми модификациями, реализуется всеми веб-фреймворками. И если с View все понятно, то с Model и Controller нужно разбираться отдельно. Под контроллерами понимаются обработчики запросов. Они принимают объект запроса и возвращают объект ответа. В случае Slim, контроллеры представлены анонимными функциями, но это не обязательно: в больших фреймворках контроллер — это класс, а обработчики — его методы. Эти методы обычно именуют действиями (actions). В принципе, на этом наше разделение можно было бы и закончить. При таком подходе вся логика сосредоточена в самих контроллерах, что вполне допустимо в самых примитивных случаях.

Во всех остальных ситуациях выделяют еще один слой, который называют Model. Сразу хочу оговориться, что понятие Model не включает в себя персистентность (постоянное хранение, базы данных). Среди разработчиков распространено заблуждение, что модель — это база данных и данные внутри нее. Но это не так.

Слой Model отвечает за бизнес-логику приложения и данные, связанные с ней. Чисто технически, этот слой может быть представлен большим количеством разных способов, которые еще сильно зависят от конкретного языка программирования и используемых библиотек. Самый распространенный вариант — это ORM, но так бывает не всегда. Более того, довольно часто, даже при наличии отдельного слоя Model, часть логики все же проникает в контроллеры.

Зачем понадобилось выделять слой Model? Достаточно давно мне попалась на глаза интересная статья, которая называлась Rails is not your application (Rails - популярный веб фреймворк, который стал прообразом для большинства современных фреймворков на разных языках программирования). Идея статьи заключается в том, что предметная область, которую мы реализуем внутри нашего сайта, никак не связана ни с сайтом ни тем более с фреймворком, который используется внутри. Посудите сами, могут ли поменяться правила бухгалтерии в зависимости от выбранного фреймворка и вообще, как связана бухгалтерия и фреймворк? Очевидно, никак и бизнес-правила этой области не зависят от существования программирования. Посредством программирования мы можем их выразить в коде, но этот код снова не будет связан с используемым фреймворком. В идеале, код который описывает предметную область и позволяет с ней работать, можно взять и перенести в другой фреймворк без модификаций. Как видно, на логическом уровне есть граница между кодом, моделирующим предметную область, и кодом, обслуживающим веб запросы. Но эту границу иногда провести трудно. Например, к чему относится отправка письма при регистрации, а авторизация, а восстановление пароля? Если закапываться дальше, то на горизонте возникают понятия Application Logic и Business Logic, а затем и Service Layer. Если вам интересно, то прочитайте про них самостоятельно.

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

Взаимоотношения между слоями в MVC не менее важны, чем наличие самих слоев. Model, как мы уже выяснили, живет своей жизнью и не знает (и не может знать) ничего про существование Controller или View. Последние, в свою очередь, используют модель для запуска бизнес логики или для формирования HTTP ответа. Controller инициирует различные процессы и запуск бизнес-логики. Кроме того, Controller отвечает за формирование ответа и запускает рендеринг шаблонов. Шаблоны не знают про существование слоя Controller, но используют данные, предоставленные им для формирование HTML (или JSON, или чего-то еще).

Веб-разработка на PHP Cookies

Cookies - это механизм хранения данных браузером удаленной машины для отслеживания или идентификации возвращающихся посетителей. По сути единственный надежный способ понять что перед нами тот же самый пользователь (хотя если угнать чужие куки, то можно подделать запрос от другого пользователя). Куки использует не только сам сайт, но и всемозможные виджеты, которые встраиваются на разные странички. Именно благодаря кукам, гугл узнает вас и преследует рекламой того что вы недавно искали, на сайтах, которые вроде бы не должны знать о ваших предпочтениях.

PHP прозрачно поддерживает работу с куками. Куки приходящие в HTTP запросе становятся доступны через суперглобальный массив $_COOKIES . Этот массив используется только для чтения, писать в него бесполезно. Установка кук осуществляется функцией setcookie() . Так как куки отправляются вместе с заголовками, то вызов этой функции должен происходит до любой отправки данных в браузер.

В PSR7 определены только два метода работающих с куками и оба этих метода извлекают куки.

  • getCookieParams() - возвращает все куки
  • getCookieParam($name, $default = null) - возвращает указанную куку

Для установки кук придется воспользоваться более низкоуровневым методом withHeader($name, $value) , добавляющим любые заголовки в ответ:

<?php

$app->post('/example', function ($request, $response) {
    // Set-Cookie: <cookie-name>=<cookie-value>
    return $response->withHeader('Set-Cookie', "foo=bar")
});

Типичный вариант использования кук - корзина в интернет-магазине. Во время добавления товара, код магазина формирует куку, в которую начинает заносить данные о заказе.

<?php

$app->post('/cart-items', function ($request, $response) {
    // Информация о добавляемом товаре
    $item = $request->getParsedBodyParam('item');

    // Данные корзины
    $cart = json_decode($request->getCookieParam('cart', json_encode([])));

    // Добавление нового товара
    $cart[] = $item;

    // Кодирование корзины
    $encodedCart = json_encode($cart);

    // Установка новой корзины в куку
    return $response->withHeader('Set-Cookie', "cart={$encodedCart}")
        ->withRedirect('/');
});

Обратите внимание на необходимость кодирования данных корзины. Кука с точки зрения HTTP содержит значение в виде строки, а значит для хранения составных структур нужно проводить ручное кодирование в строку и декодирование при извлечении. Иногда простого кодирования недостаточно, особенно если данные имеют повышенную важность и их желательно защитить. В таких случаях дополнительно применяют шифрование на стороне сервера.

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

Дополнительные материалы

  1. Официальная документация

Веб-разработка на PHP Сессия

Сессия, в отличие от кук, как понятие, не существует ни в браузере ни в HTTP. Это абстракция, созданная для удобной работы с индивидуальными пользователями. Сессии реализуются на уровне конкретных фреймворков и только в PHP сессии встроены в язык. Общий принцип работы сессии сводится к трем операциям:

  • Старт сессии. Так мы говорим системе что хотим начать следить за пользователем. Во многих фреймворках эта операция выполняется неявно, при попытке чтения или записи в сессию.
  • Запись данных в сессию.
  • Чтение данных из сессии.
Set-Cookie: _hexlet_session=CM5DvfXch6M3uPJHyfLDpv52wBe4iu3og domain=.hexlet.io; path=/; expires=Sun, 12 Aug 2018 12:56:51 -0000; secure; HttpOnly

Старт сессии на техническом уровне означает установку специальной куки в браузер. Обычно эта кука содержит идентификатор сессии, который уникален для каждого пользователя. Данные же сессии, могут хранится где угодно, это зависит от конкретной реализации. В этом одно из ключевых отличий работы с пользователями напрямую через куки или через сессию. Сессия более высокоуровневая абстракция. Например в PHP, по-умолчанию, данные сессии хранятся в файлах. Из этого следует сразу два вывода. Во-первых сессия ограничена только физическим пространством дисков, во-вторых данные хранятся на сервере что безопаснее. Если этого недостаточно, например серверов больше чем один, то буквально парой строк кода в конфигурации, можно изменить тип хранилища с файлов на базу данных.

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

<?php

// Операция идемпотентна. Не важно была ли инициализирована сессия раньше, старт сессии выполняется всегда
session_start();

if (!isset($_SESSION['count'])) {
    $_SESSION['count'] = 0;
} else {
    $_SESSION['count']++;
}

print_r($_SESSION['count']);

Этот простой скрипт демонстрирует работу сессий в PHP. В отличии от всех остальных суперглобальных массивов для работы с HTTP, массив $_SESSION мутабельный. Все что добавится в него, автоматически попадает в сессию и сохраняется между запросами до тех пор, пока кука не будет удалена (или изменена). Даже из этого простого примера видно что сессия упрощает работу с пользователем. Кроме того, значением массива $_SESSION может быть любая составная структура, массив или объект. Механизм сессий автоматически беспокоится о сериализации и десереализации.

Внутри Slim нет никакого особенного механизма для работы с сессиями, так как они не являются частью стандарта PSR7. Работа с сессией происходит напрямую. Перепишем наш пример добавления товаров в корзину используя сессию.

<?php

session_start();

$app->post('/cart-items', function ($request, $response) {
    // Информация о добавляемом товаре
    $item = $request->getParsedBodyParam('item');

    // Добавление нового товара
    $_SESSION['cart'][] = $item;

    return $response->withRedirect('/');
});

По сравнению с версией на куках, ушла значительная часть кода. Кодирование и декодирование в json, извлечение куки и перезапись куки.

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

  • Обновление куки с установкой даты в прошлое
  • Обнуление массива $_SESSION - session_unset()
  • Обнуление хранилища сессий - session_destroy()

Только в этом случае сессия уничтожится полностью. Первый шаг можно не делать, но второй и третий желательно.

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

Дополнительные материалы

  1. Официальная документация

Веб-разработка на PHP Деплой

После того как сайт написан, встает вопрос о том как выложить его в интернет. Стандартный путь включает три пункта:

  1. Покупка домена
  2. Покупка хостинга и его настройка
  3. Деплой

Первый я пропущу (скоро мы его опишем в https://guides.hexlet.io), а вот про два других поговорим.

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

  1. Код проекта скачивается на сервер (обычно через клонирование git)
  2. Ставятся все необходимые зависимости
  3. Выполняется процесс сборки, например собирается фронтенд часть
  4. Выполняются миграции. Миграции - sql скрипты, которые изменяют структуру базы данных
  5. Запускается новая версия кода

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

Несмотря на разнообразие способов деплоя, есть одно важное правило общее для всех - деплоить можно только вперед! Деплой нельзя “откатывать” (в первую очередь это касается миграций, но про базы мы пока не говорим). Если после или во время деплоя что-то пошло не так, то правильно деплоить снова, но предыдущую версию.

Кроме того, деплои можно классифицировать по способу обновления и отката:

  • Последовательное обновление - сервера обновляются по очереди
  • Сине-Зеленый деплой - полное дублирование инфраструктуры с подменой

Отдельно стоит сказать про Канареечный релиз, при таком подходе переключение на использовании новой версии происходит постепенно, сначала для небольшого процента пользователей, а затем и для всех

Способ деплоя сильно зависит от используемого хостинга и даже способа настройки серверного окружения. Выделяют следующие типы хостингов:

  • Shared Hosting - самый дешевый способ размещать сайт в интернете. Такая услуга включает в себя доступ на сервер с уже настроенным программным обеспечением под конкретный стек, например Linux + PHP + MySQL. Этот способ подходит для самых простых сайтов и требует минимальной настройки.
  • VPS/VDS - наиболее сбалансированная услуга, в рамках которой предоставляет виртуальная машина. Плюс в том что такой вид хостинга позволяет задействовать больше серверных мощностей: цпу, память и диск. Предустановленного ПО нет, все нужно делать самостоятельно. По сравнению с Shared Hosting вы не ограничены в правах и можете настраивать сервер как вам угодно.
  • Dedicated Server - выделеный сервер (либо свой, либо арендованный). Такой хостинг требует больше всего участия, но зато вы получаете лучшее соотношение производительность/цена.
  • IaaS - инфраструктура как сервис. Вид хостинга при котором большая часть возможностей представляется как сервис. Как пример Amazon Web Service (AWS).
  • PaaS - платформа как сервис. Наиболее дорогой и самый автоматизированный способ из коробки по размещению сайтов. Выкладка сайта происходит буквально по команде git push . Кроме цены важно учитывать используемые технологии и подходы. PaaS обладает наибольшим числом ограничений по тому что и как можно делать, но в обмен вы получаете не просто автоматизированный хостинг, но и платформу которая автоматически “скейлится” (масштабируется) под нагрузку.

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

PaaS

Самый простой способ начать деплоить. Большинство PaaS хостеров имеют бесплатные планы, достаточные для выкладки учебных проектов. Из плюсов, не придется покупать адрес, домен третьего уровня предоставляется бесплатно. Самое популярное PaaS решение на текущий день - Heroku. У Хероку прекрасная документация, следуя которой можно быстро выложить свой первый сайт. Пошаговый гайд описывающий выкладку сайта на PHP доступен по ссылке https://devcenter.heroku.com/articles/getting-started-with-php. Хероку используется на Хекслете для JavaScript и PHP проектов.

$ heroku create
Creating sharp-rain-871... done, stack is cedar-14
http://sharp-rain-871.herokuapp.com/ | https://git.heroku.com/sharp-rain-871.git
Git remote heroku added

$ git push heroku master
remote: Building source:
remote:
remote: -----> PHP app detected
remote: -----> Bootstrapping...
remote: -----> Installing platform packages...
remote:        NOTICE: No runtime required in composer.json; requirements
remote:        from dependencies in composer.lock will be used for selection
remote:        - php (7.1.3)
remote:        - apache (2.4.20)
remote:        - nginx (1.8.1)
remote: -----> Installing dependencies...
remote:        Composer version 1.4.1 2017-03-10 09:29:45
remote:        Loading composer repositories with package information
remote:        Installing dependencies from lock file
remote:        Package operations: 12 installs, 0 updates, 0 removals
remote:          - Installing psr/log (1.0.2): Loading from cache
remote:          - Installing monolog/monolog (1.22.1): Loading from cache
...
remote:          - Installing symfony/twig-bridge (v3.2.7): Loading from cache
remote:        Generating optimized autoload files
remote: -----> Preparing runtime environment...
remote: -----> Checking for additional extensions to install...
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 14.8M
remote: -----> Launching...
remote:        Released v17
remote:        https://gsphpjon.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/gsphpjon.git
 + 264e577...4f2369c master -> master (forced update)

Самостоятельная работа

Выложите на Хероку тот код который вы делали на Slim в течении этого курса.

Все остальное

Если не брать в рассчет самый примитивный Shared Hosting, который не позволяет никак настраивать серверное окружение, все остальные виды хостингов имеют схожие задачи для выкладки.

Самая первая задача - настроить окружение. Если в Shared Hosting всегда есть набор предустановленных программ, то во всех остальных видах хостинга нет ничего кроме голой операционной системы. Установка необходимого ПО такой же автоматизируемый процесс как процесс деплоя и у него есть даже собственное название - Configuration Management. Рекомендую использовать Ansible, популярное решение для настройки (На Хекслете есть соответствующий курс).

- hosts: all

  tasks:

    - lineinfile:
        create: yes
        regexp: ~/.local
        path: ~/.bash_profile
        line: "export PATH=$PATH:~/.local/bin"

    - name: install packages
      apt: pkg=python3-pip state=latest update_cache=yes
      tags: pip
      become: yes

    - pip:
        name: pip
        state: latest
      become: yes

Ключевое понятие Ansible - Playbook. Это файл (или файлы) описывающие в yaml, что нужно сделать на указанной машине. В каждом плейбуке используются готовые модули поставляемые вместе с Ansible. Этих модулей сотни, с помощью них можно делать практически все, начиная от установки программ, до настройки сети и управления правами файловой системы. Ansible универасальный инструмент, с его помощью можно не только настраивать окружение, но и собственно деплоить. Причем для деплоя есть готовый модуль - deploy helper.

В более продвинутых случаях, там где используется, например, Docker, развертывание осуществляется системами оркестрации, среди которых выделяется Kubernetes.

Дополнительные материалы

  1. Среды разработки
  2. DevOps
  3. Непрерывное развертывание
  4. Terraform
  5. Ansible