- PVSM.RU - https://www.pvsm.ru -

Модернизация старого PHP-приложения

Модернизация старого PHP-приложения - 1

Недавно мне выдалась случайная возможность поработать с несколькими старыми PHP-приложениями. Я заметил несколько распространённых антипаттернов, которые пришлось исправлять. Эта статья не о том, как переписывать старое PHP-приложение на <вставьте сюда название чудесного фреймворка>, а о том, как сделать его более удобным в сопровождении и менее хлопотным в работе.

Антипаттерн №1: credential’ы в коде

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

Для исправления этого я предпочитаю способ, подходящий для любого приложения: устанавливаю пакет phpdotenv [1], который позволяет создавать файл окружения и обращаться к переменным с помощью суперпеременных окружения.

Создадим два файла: .env.example, который будет версионирован и служить шаблоном для файла .env, который будет содержать учётные данные. Файл .env не версионируется, так что добавьте его в .gitignore. Это хорошо объяснено в официальной документации [2].

Ваш файл .env.example будет перечислять учётные данные:

DB_HOST=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=

А сами данные будут в файле .env:

DB_HOST=localhost
DB_DATABASE=mydb
DB_USERNAME=root
DB_PASSWORD=root

В обычном файле загрузите .env:

$dotenv = DotenvDotenv::createImmutable(__DIR__);
$dotenv->load();

Затем можете обратиться к учётным данным с помощью, скажем, $_ENV['DB_HOST'].

Не рекомендуется использовать пакет в эксплуатации «как есть», для этого лучше:

  • Внедрить переменные окружения в runtime вашего контейнера, если у вас развёртывание на основе Docker, либо в серверную HTTP-конфигурацию, если это возможно.
  • Закэшировать переменные окружения, чтобы избежать накладных расходов на чтение .env при каждом запросе. Вот как это делает Laravel [3].

Файлы с учётными данными можно удалить из истории Git [4].

Антипаттерн №2: не используют Composer

Раньше было очень популярно иметь папку lib с большими библиотеками вроде PHPMailer. Этого нужно всячески избегать, когда речь заходит о версионировании, так что этими зависимостями следует управлять с помощью Composer [5]. Тогда вам будет очень легко увидеть, какая версия пакета используется, и обновить её при необходимости.

Так что поставьте Composer и используйте его для управления.

Антипаттерн №3: отсутствие локального окружения

В большинстве приложений, с которыми я работал, было только одно окружение: production.

Модернизация старого PHP-приложения - 2

Но избавившись от антипаттерна №1, вы сможете легко настроить локальное окружение. Возможно, часть конфигурации у вас была жёстко прописана в коде, например, пути загрузки, но теперь вы можете перенести это в .env.

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

Можете воспользоваться сервисом наподобие PHPDocker [6], или применить небольшой файл docker-compose.yml.

Антипаттерн №4: не используют папку Public

Оказалось, что большинство этих старых проектов доступно из их корневых папок. То есть любой файл, лежащий в корне, будет доступен для публичного чтения. Это особенно плохо, когда злоумышленники (например, скрипт-кидди) попытаются напрямую обратиться к включённым файлам, ведь вы могли не определить выход, если скрипт обратится напрямую ко всем вашим включённым файлам.

Очевидно, что эта ситуация несовместима с использованием .env или Composer, потому что открывать папку vendor — плохая идея [7]. Да, есть некоторые хитрости [8], позволяющие это сделать; но если это возможно, перенесите все открытые для клиентов PHP-файлы в папку Public и поменяйте конфигурацию сервера, чтобы эта папка стала корневой для вашего приложения.

Обычно я делаю так:

  • Создаю папку docker для файлов, относящихся к Docker (Nginx-конфигурация, PHP Dockerfile и т.д.).
  • Создаю папку app, в которой храню бизнес-логику (сервисы, классы и т.д.).
  • Создаю папку public, в которой храню открытые для клиентов PHP-скрипты и ресурсы (JS/CSS). Это корневая папка приложения с точки зрения клиентов.
  • Создаю в корне файлы .env и .env.example.

Антипаттерн №5: вопиющие проблемы с безопасностью

PHP-приложения, особенно старые, которые не используют фреймворки, часто страдают от вопиющих проблем с безопасностью:

  • Из-за отсутствия экранирования параметров в запросе есть опасность SQL-инъекций. Чтобы их предотвратить, используйте PDO!
  • Из-за отображения не экранированныхпользовательских данных есть опасность XSS-инъекций. Чтобы их предотвратить, используйте htmlspecialchars.
  • Загрузка файлов… это отдельная тема [9]. Если разработчик реализовал собственную загрузку, самодельное решение, то высока вероятность, что у него возникла одна или несколько проблем с безопасностью.
  • Из-за отсутствия проверки источника запроса есть опасность CSRF-атак [10]. Рекомендую использовать пакет Anti-CSRF [11], который можно легко интегрировать в имеющееся приложение.
  • Плохое шифрование паролей. Я видел много проектов, до сих пор использующих SHA-1 и даже MD5 для хэширования паролей. В PHP начиная с 5.5 из коробки хорошая поддержка BCrypt, стыдно этим не пользоваться. Чтобы комфортно переносить пароли, я предпочитаю обновлять хэши в базе данных по мере входов пользователей. Главное убедиться, что что колонка password достаточно длинная и вмещает BCrypt-пароли, вполне подходит VARCHAR(255). Вот псевдокод, чтобы было понятнее:
    <?php
    // Пароль в старом хэше: не начинается с $
    // Пароль верный: преобразуем его и журналируем пользователя
    if (strpos($oldPasswordHash, '$') !== 0 &&
        hash_equals($oldPasswordHash, sha1($clearPasswordInput))) {
        $newPasswordHash = password_hash($clearPasswordInput, PASSWORD_DEFAULT);
    
        // Обновляем колонку password
    
        // Пользователь вошёл: возвращаем сообщение об успешности
    }
    
    // Пароль уже преобразован
    if (password_verify($clearPasswordInput, $currentPasswordHash)) {
        // Пользователь вошёл: возвращаем сообщение об успешности
    }
    
    // Пользователь не вошёл: возвращаем сообщение о неуспешности
    

Антипаттерн №6: отсутствие тестов

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

Модернизация старого PHP-приложения - 3

Это высокоуровневые тесты, которые помогут вам убедиться, что последующий рефакторинг приложения не сломал его. Тесты могут быть простыми, например, запускаем браузер и входим в приложение, затем ожидаем получения HTTP-кода об успешности операции и/или соответствующего сообщения на финальной странице. Для тестов можно использовать PHPUnit, или Cypress [12], или codeception [13].

Антипаттерн №7: плохая обработка ошибок

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

Вам нужно иметь возможность вылавливать и журналировать как можно больше ошибок, чтобы исправлять их. По этому теме есть хорошие статьи [14].

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

Антипаттерн №8: глобальные переменные

Думал, я их больше не увижу, пока не начал работать со старыми проектами. Глобальные переменные делают непредсказуемым чтение и понимание поведения кода. Короче, это зло [15].

Лучше вместо них использовать внедрение зависимостей [16], потому что это позволяет вам контролировать, какие экземпляры используются, и где. Например, хорошо показал себя пакет Pimple [17].

Что ещё улучшить?

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

Во-первых, если приложение работает на древней версии PHP (ниже 7), постарайтесь обновить её. Чаще всего это не доставляет больших проблем, и больше всего времени уйдёт, скорее всего, на избавление от вызовов mysql_ calls, если они есть. Чтобы наспех это исправить, можете воспользоваться подобной библиотекой [18], но лучше переписать все запросы на PDO, чтобы все параметры экранировались одновременно.

Если в приложении не используется паттерн MVC, то есть бизнес-логика и шаблоны разделены, то самое время добавить библиотеку шаблонов (я знаю, что PHP шаблонный язык, но современные библиотеки гораздо удобнее), например, Smarty, Twig или Blade.

Наконец, в долгосрочной перспективе лучше будет переписать приложение на современном PHP-фреймворке вроде Laravel или Symfony. У вас будут все инструменты, необходимые для безопасной и продуманной PHP-разработки. Если приложение большое, то рекомендую использовать паттерн strangler [19], чтобы избежать big bang-переписывания [20], которое может (вероятно, так и будет) плохо закончиться. Поэтому вы можете мигрировать в новую систему те части кода, над которыми вы сейчас работаете, сохраняя старые работающие части в неприкосновенности, пока до них не дойдёт очередь.

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

Автор: Макс

Источник [21]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/356180

Ссылки в тексте:

[1] phpdotenv: https://github.com/vlucas/phpdotenv

[2] официальной документации: https://github.com/vlucas/phpdotenv#usage

[3] это делает Laravel: https://github.com/vlucas/phpdotenv/issues/207#issuecomment-260116783

[4] удалить из истории Git: https://docs.github.com/en/github/authenticating-to-github/removing-sensitive-data-from-a-repository

[5] Composer: https://getcomposer.org/

[6] PHPDocker: https://phpdocker.io/generator

[7] плохая идея: https://www.reddit.com/r/PHP/comments/8hnpot/a_vendor_directory_should_always_be_outside_the/

[8] хитрости: https://stackoverflow.com/a/50766284/11989865

[9] отдельная тема: https://www.acunetix.com/websitesecurity/upload-forms-threat/

[10] CSRF-атак: https://portswigger.net/web-security/csrf

[11] Anti-CSRF: https://github.com/paragonie/anti-csrf

[12] Cypress: https://www.cypress.io/

[13] codeception: https://codeception.com/

[14] хорошие статьи: https://netgen.io/blog/modern-error-handling-in-php

[15] зло: https://softwareengineering.stackexchange.com/a/148109

[16] внедрение зависимостей: https://softwareengineering.stackexchange.com/a/297935

[17] Pimple: https://github.com/silexphp/Pimple

[18] подобной библиотекой: https://github.com/dshafik/php7-mysql-shim

[19] strangler: https://martinfowler.com/bliki/StranglerFigApplication.html

[20] big bang-переписывания: https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/

[21] Источник: https://habr.com/ru/post/515778/?utm_source=habrahabr&utm_medium=rss&utm_campaign=515778