Организация работы с глобальными ресурсами в PHP приложениях

Практически в каждом веб-приложении используются глобальные ресурсы, к которым должен быть доступ из любой части кода приложения. К таким ресурсами относятся соединение с базой данных, конфигурация, процедуры обработки ошибок и тд. Если вы php программист, то вам, возможно, не раз доводилась слышать, что глобальные переменные это зло. Что же в таком случае можно использовать вместо глобальных переменных?

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

Глобальные переменные

Пожалуй самым простейшим для понимания и реализации решением является использование глобальных переменных. Глобальная переменная может быть объявлена с помощью ключевого слова global. Это делает ее доступной из любого участка кода. Для доступа к глобальной переменной в любой области видимости необходимо объявить переменную глобальной (с помощью global). Также можно использовать суперглобальный массив $GLOBALS для доступа к переменной без ее предварительного объявления.

Пример использования глобальной переменной:

global $database;

$database = new Database();
 
function doSomething()
{
    // Это необходимо, чтобы PHP понимал, что вы хотите использовать 
    // глобальную переменную, а не локальную
    global $database; 

    $data = $database->readStuff(); 
}

Пример работы с глобальной переменной с помощью $GLOBALS:

global $database;

$database = new Database();
 
function doSomething()
{
    $data = $_GLOBALS['database']->readStuff(); 
    //Не нужно предварительно объявлять глобальную переменную
}

Достоинства глобальных переменных

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

Недостатки глобальных переменных

Недостатки глобальных переменных полностью перекрывают их достоинства:

  • Использование глобальных переменных снижает читабельность, делает код сложным для понимания. Очень сложно определить где, как и с какой целью была проинициализирована глобальная переменная и как правильно ее использовать.
  • Код с глобальными переменными сложно сопровождать. При изменении глобальной переменной вы вынуждены просматривать весь код, чтобы внести изменения везде, где используется эта переменная.
  • Злоупотребление глобальными переменными может привести к ошибкам, которые очень сложно отлаживать. Без какого-либо механизма контроля за использованием переменной очень просто записать в переменную невалидные данные, которые могут привести к ошибкам в других частях кода (например, если в одной части кода переменная заполняется массивом, а в другой части кода в этой переменной ожидается объект).
  • Можно забыть объявить переменную глобальной и работать с локальной переменной, не замечая этого, до тех пор пока приложение не сломается. Такие ошибки сложно отлаживать.
  • Если вы совмещаете свой код с чужим (например при использовании стороних библиотек или при написании расширений для другого ПО) и обе системы используют глобальные переменные, существует вероятность того, что названия переменных могут совпасть. Это становится причиной возникновения ошибок в обеих системах, которые сложно отлавливать.
  • Все части кода, использующие одну глобальную переменную, сильно связаны между собой. Разделить сильно связанный код очень сложно. Это затрудняет его повторное использование.
  • Написание юнит-тестов становится более сложным, поскольку тесту не известно, какие глобальные переменные нужны и как проинициализировать все глобальные переменные валидными значениями.

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

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

Статические классы (Вспомогательные классы)

Пример статического класса:

class SmtpConfig
{
    public static $host = 'localhost';
    public static $port = 465;
    public static $user = 'me@example.com';
    public static $password = 'j4a!9Sd@aKP2f';
    public static $tls = true;
}
 
echo SmtpConfig::$user; 
// Это и другие значения свойств класса допступны везде пока файл 
// содержит объявление класса, которое может быть подключено или автозагружено

Достоинства статических классов

  • Статические вспомогательные классы позволяют группировать вместе несколько связанных данных.
  • Статические классы легки для понимания и использования. При этом они не настолько склонны к конфликту имен, как глобальные переменные (к тому же сейчас, когда в PHP появилась поддержка пространств имен, проблема конфликта имен классов практически исчезла).
  • Позволяет без особых усилий найти в коде место, где были первоначально проинициализированы ресурсы (при необходимости большинство IDE могут автоматически находить описание класса, в то время как с глобальными переменными не всегда возможно сказать где они были первоначально объявлены). Хотя все еще значения могут быть проинициализированы или изменены в любой части вашего кода.

Недостатки статических классов

  • Писать юнит-тесты для статических классов, имеющих свои зависимости, сложнее, чем для экземпляров классов с внедрением зависимостей (смотрите далее).
  • Основным недостатком статических классов является то, что они усиливают связанность кода - любой объект, использующий статические методы или свойства класса, тесно связан с кодом, описывающим и инициализирующим эти методы и свойства (и, которые в свою очередь, могут иметь свои собственные зависимости).
  • В статических классах не используются конструкторы. Поэтому каждый статический метод сам должен выполнять проверку своих зависимостей, а вызывающий код должен инициализировать все параметры метода, если не используются значения по умолчанию.

В каких случаях рекомендуется использовать статические классы

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

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

Синглтон

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

Пример синглтона:

class Singleton
{
    protected static $instance = null;
 
    protected function __construct()
    {
    }
 
    protected function __clone()
    {
    }
 
    public static function getInstance()
    {
        if (!isset(static::$instance)) self::$instance = new Singleton();
        return static::$instance;
    }
}
 
$singleton = Singleton::getInstance();

Достоинства синглтона

  • Гарантирует существование только одной версии объекта (что позволяет расшаривать ресурсы).
  • Может быть использован где угодно в коде. Если он еще не был создан, то это произойдет при первом его использовании.
  • Частично поддерживает наследование и полиморфизм.

Недостатки синглтона

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

В каких случаях рекомендуется использовать синглтон

Синглтон нужно использовать когда один ресурс должен быть разделен между различными объектами. При этом нужно убедиться в том что текущие условия не требуют нескольких экземпляров ресурса, а также любые вероятные или возможные условия не будут требовать нескольких экземпляров. Шаблон проектирования Singlton нашел широкое применение совместно с шаблоном Registry (смотрите ниже), а также для взаимодействия с операционной системой или хостом, на котором выполняется приложение, поскольку приложение может взаимодействовать только с одной операционной системой или хостом.

Шаблон проектирования Реестр (Registry)

Шаблон проектирования Registry позволяет определить объект (обычно это синглтон), который хранит ссылки на различные другие ресурсы (обычно в виде пар ключ/значение), необходимые вашему приложению (например, подключение к базе данных или конфигурация приложения). Хотя сам реестр, как правило, является синглтоном (поскольку приложению нужен только один реестр), ресурсы, хранящиеся в нем, не обязательно должны быть синглтонами - реестр может хранить несколько экземпляров одного и того же класса. Ресурсы содержащиеся в нем, даже могут быть не объектами, а примитивными типами данных или массивами

Ресурсы могут храниться в хэш-таблице (массиве), или, если вам заранее известно, какие элементы должны быть в реестре, вы можете строго задать их тип (это будет способствовать автозаполнению кода в IDE). Также допустима комбинация этих двух вариантов!

Пример реестра (слабо типизированный вариант):

class Registry
{
    protected static $instance;
    protected $resources = array();
 
    protected function __construct()
    {
    }
 
    protected function __clone()
    {
    }
 
    public static function getInstance()
    {
        if (!isset(self::$instance)) {
            self::$instance = new Registry();
        }
        return self::$instance;
    }
 
    public function setResource($key, $value, $force_refresh = false)
    {
        if (!$force_refresh && isset($this->resources[$key])) {
            throw new RuntimeException('Resource ' . $key . ' has already been set. If you really '
                                       . 'need to replace the existing resource, set the $force_refresh '
                                       . 'flag to true.');
        }
        else {
            $this->resources[$key] = $value;
        }
    }
 
    public function getResource($key)
    {
        if (isset($this->resources[$key])) {
            return $this->resources[$key];
        }
        throw new RuntimeException ('Resource ' . $key . ' not found in the registry');
    }
}
 
// Добавление ресурса в реестр
$db = new Database();
Registry::getInstance()->setResource('Database', $db);
 
// Извлечение ресурса из реестра
$db = Registry::getInstance()->getResource('Database');

Пример реестра (строго типизированный вариант):

class Registry
{
    protected static $instance;
    protected $main_db;
    protected $sync_db;
    protected $config;
 
    protected function __construct()
    {
    }
 
    protected function __clone()
    {
    }
 
    public static function getInstance()
    {
        if (!isset(self::$instance)) {
            self::$instance = new Registry();
        }
        return self::$instance;
    }
 
    public function setMainDatabase(Database $value, $force_refresh = false)
    {
        if (!$force_refresh && isset($this->main_db)) {
            throw new RuntimeException('Main database has already been set. If you really '
                                       . 'need to replace the existing database, set the '
                                       . '$force_refresh flag to true.');
        }
        else {
            $this->main_db = $value;
        }
    }
 
    public function getMainDatabase()
    {
        if (isset($this->main_db)) {
            return $this->main_db;
        }
        throw new RuntimeException ('Main database resource not found in the registry');
    }
 
    public function setSyncDatabase(Database $value, $force_refresh = false)
    {
        if (!$force_refresh && isset($this->sync_db)) {
            throw new RuntimeException('Synchronisation database has already been set. If you really '
                                       . 'need to replace the existing database, set the $force_refresh '
                                       . 'flag to true.');
        }
        else {
            $this->sync_db = $value;
        }
    }
 
    public function getSyncDatabase()
    {
        if (isset($this->sync_db)) {
            return $this->sync_db;
        }
        throw new RuntimeException ('Synchronisation database resource not found in the registry');
    }
 
    public function setConfig(Config $value, $force_refresh = false)
    {
        if (!$force_refresh && isset($this->config)) {
            throw new RuntimeException('Configuration object has already been set. If you really '
                                       . 'need to replace the existing configuration, set the '
                                       . '$force_refresh flag to true.');
        }
        else {
            $this->config = $value;
        }
    }
 
    public function getConfig()
    {
        if (isset($this->config)) {
            return $this->config;
        }
        throw new RuntimeException ('Configuration resource not found in the registry');
    }
}
 
// Добавление ресурса в реестр
$db = new Database();
Registry::getInstance()->setMainDatabase($db);
 
// Извлечение ресурса из реестра
$db = Registry::getInstance()->getMainDatabase();

В этих примерах разработчику разрешено заменять существующие ресурсы, но только если он дает понять, что это было сделано намеренно (путем установки флага $force_refresh )

Достоинства реестра

  • Отпадает необходимость передавать каждый ресурс в индивидуальном параметре. Вместо этого можно использовать глобальный реестр
  • Реестр позволяет хранить несколько глобальных ресурсов и централизовано управлять ими. При этом для ресурсов можно применять наследование и полиморфизм без ограничений
  • Благодаря строгой типизации ваша IDE поможет избежать опечатки
  • По сравнению с внедрением зависимости реестр использовать проще

Недостатки реестра

Реестр скрывает зависимости и сильно связан с объектами, зависящими от него (или его содержимого). Но связь эта не настолько сильная, как в случае с глобальными переменными (поскольку ресурсы могут быть заменены другими подклассами).

В каких случаях рекомендуется использовать реестр

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

Внедрение зависимости

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

Пример внедрения зависимости:

class Person
{
    protected $database;
    public $title;
    public $first_name;
    public $last_name;
     
    public function __construct(Database $db, $last_name)
    {
        $this->database = $db;
        if (strlen($last_name) == 0) {
            throw new Exception('Last name required');
        }
        $this->last_name = $last_name;
    }
     
    public function setTitle($title)
    {
        $this->title = $title;
    }
 
    public function setFirstName($first_name)
    {
        $this->first_name = $first_name;
    }
     
    // Здесь методы, использующие подключение к базе
}
 
$db = new Database('localhost', 'user', 'password');
// Объект базы данных передается в объект Person 
// вместе с некоторыми другими данными
$person = new Person($db, 'Smith');
$person->setTitle('Mr'); // Опциональные зависимости могут быть 
                         // установлены с помощью отдельных методов

Достоинства внедрения зависимости

  • Внедрение зависимости уменьшает связанность кода, позволяет каждому объекту существовать и выполнять операции не зависимо от установок окружения
  • Это упрощает повторное использование кода, поскольку вы можете использовать тот же самый объект в другом приложении или в том же приложении, но в других условиях
  • Это также упрощает написание юнит тестов, поскольку тест может ввести реальные или фиктивные зависимости для тестирования объекта
  • Вызывающему коду известно какие существуют зависимости. Поэтому он может предоставить все необходимые зависимости и не беспокоиться о том что некоторые наличие скрытых и неудовлетворенных зависимостей могут нарушить функционирование приложения
  • Наследование и полиморфизм могут быть использованы для достижения большей эффективности, если указать родительский класс (или интерфейс) в качестве зависимости. Вызывающий код может передать любой подкласс и объект может ничего не знать и не беспокоиться о том, как реализован этот подкласс (это позволяет легко расширять приложение). Например, если у класса есть конструктор, требующий передачи объекта базы данных, вызывающий код может передать mysql базу или SQLite базу или любой другой подкласс базы данных (возможно даже тот, которой еще не существует)
  • При передачи зависимостей в конструктор любые проблемы могут быть выявлены на раннем этапе - класс может проверить наличие валидных данных, от которых он зависит, перед тем как создавать экземпляр. Это упрощает отладку приложения

Недостатки внедрения зависимости

  • Вызывающий код должен проделывать больше работы для инициализации объекта, особенно если зависимости, которые вы вводите, имеют свои собственные зависимости (Если это выходит из под контроля, можно подумать о необходимости использования контейнера зависимостей)
  • Если у объекта много зависимостей, вы получите в конструкторе этого объекта длинный список параметров, который делает код трудным для чтения и понимания

В каких случаях рекомендуется использовать внедрение зависимости

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

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

Заключение

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

Перевод статьи Handling Global Data in PHP Web Applications