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

в 9:40, , рубрики: legacy, php, refactoring, Блог компании Mail.Ru Group, никто не читает теги, Программирование, Проектирование и рефакторинг, Разработка веб-сайтов
Модернизация старого PHP-приложения - 1

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

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

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

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

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

Ваш файл .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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Из-за отсутствия экранирования параметров в запросе есть опасность SQL-инъекций. Чтобы их предотвратить, используйте PDO!
  • Из-за отображения не экранированныхпользовательских данных есть опасность XSS-инъекций. Чтобы их предотвратить, используйте htmlspecialchars.
  • Загрузка файлов… это отдельная тема. Если разработчик реализовал собственную загрузку, самодельное решение, то высока вероятность, что у него возникла одна или несколько проблем с безопасностью.
  • Из-за отсутствия проверки источника запроса есть опасность CSRF-атак. Рекомендую использовать пакет Anti-CSRF, который можно легко интегрировать в имеющееся приложение.
  • Плохое шифрование паролей. Я видел много проектов, до сих пор использующих 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, или codeception.

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

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

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

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

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

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

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

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

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

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

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

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

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

Автор: Макс

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js