Понимание addPackage, loadClass и getService

Перевод заметки Understanding addPackage, loadClass and getService
ВНИМАНИЕ! Актуально для MODx 2.*
Там на самом донышке пару строк про MODx 3 из официальной документации касательно данной темы

Повышайте свой уровень разработки на MODx. Присоединяйтесь к исследованию Боба (Bob Ray) о том, когда и зачем использовать каждый из трех методов загрузки классов MODX.

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

Эти три метода используются в процессе загрузки классов в MODX.
Почему бы просто не использовать include или require?
Конечно, вы можете использовать традиционные include или require для загрузки ваших классов, но если есть какие-либо ошибки и E_NOTICE включен, вы можете получить неприятный и потенциально запутанный сюрприз. Методы MODX работают гораздо лучше. Если они сталкиваются с ошибкой, они записывают ее в журнал ошибок MODX (часто с некоторой полезной информацией), а не в стандартный вывод. Методы MODX также обеспечивают дополнительный контроль над процессом загрузки и используют кеш.

Вы, несомненно, знаете, что если PHP подключает один и тот же класс дважды и класс не обернут в конструкцию if (! class_exists('classname')) {}, он выдаст ошибку, сообщающую вам, что класс уже определен. Это часто происходит с плагинами, которые могут вызываться более одного раза во время одного и того же запроса. Использование loadClass() позволит избежать этой проблемы. Его можно вызывать несколько раз подряд в одном и том же классе без ошибок. MODX понимает, что класс уже загружен, и не пытается его подключить.

addPackage()


public function addPackage($pkg = '', $path = '', $prefix = null)

addPackage() загружает класс и устанавливает путь к каталогам компонента.
Со следующим кодом addPackage() выполняется успешно, а loadClass() не справляется:

$loginModelPath = MODX_CORE_PATH . 'components/login/model/';

$success = $modx->addPackage('Login',$loginModelPath , '');
echo "\naddPackage " . ($success? 'OK' : 'Failed');
// OK

$success = $modx->loadClass('login.Login');
echo "\nloadClass " . ($success  ? 'OK' : 'Failed');
// Failed

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

Когда я их изучал, в документации подразумевалось, что addPackage() позволит вам получить доступ к другим классам в компоненте, но этот код отлично работает и без addPackage():

$loginModelPath = MODX_CORE_PATH . 'components/login/model/';
$success = $modx->loadClass('recaptcha.reCaptcha', $loginModelPath, true, true);
$reCaptcha = new reCaptcha($modx);

В примере выше класс reCaptcha находится в подкаталоге login/model/.
Итак, пока loadClass() имеет правильный базовый путь (почти всегда заканчивающийся на model/), он будет загружать любые классы в компоненте, если вы указываете полное имя (точки представляют разделитель каталогов — recaptcha.reCaptcha). В этом случае addPackage() снова можно опустить, поскольку класс reCaptcha не является объектом, хранящимся в базе данных.

Последний аргумент addPackage() $prefix позволяет указать префикс таблицы. Это необходимо, если ваш класс является объектом хранящимся в базе данных и его таблица имеет префикс, отличный от префикса классов MODX.
Настоятельно рекомендуется использовать другой префикс таблицы для ваших собственных классов.

loadClass()


public function loadClass($fqn, $path= '', $ignorePkg= false, $transient= false)

Первый аргумент loadClass() — $fqn — означает «полное имя». При вызове loadClass (как в примере с reCaptcha выше) часть после последней точки в этом аргументе должна быть фактическим именем самого класса, поэтому оно написано именно так, а не в нижнем регистре.
При преобразовании в нижний регистр оно также должно быть именем самого файла класса (к нему автоматически добавляется .class.php). Часть до последней точки включительно является местоположением каталога, в ней разделитель каталогов заменен точками.
Другими словами, если вы переведете весь аргумент $fqn в нижний регистр, добавляете путь к $, добавляете .class.php в конец и преобразуете все точки в косые черты, у вас должен быть полный путь к вашему файлу класса (включая и название самого файла).

Если ваши аргументы для loadClass() выглядят так:
// MODX_CORE_PATH = usr/public_html/modx/core/
$fqn = 'login.Login';
$path = MODX_CORE_PATH . 'components/login/model/';

MODX попытается подключить следующий файл:
usr/public_html/modx/core/components/login/model/login/login.class.php

Предполагается, что класс называется Login.
Если вы загружаете класс с именем MyClass, и он находится непосредственно в вашем каталоге /model/, вызов будет выглядеть примерно так:
$path = MODX_CORE_PATH . 'components/mycomponent/model/';
$modx->loadClass('MyClass', $path, true, true);

Если бы класс находился в каталоге /model/utilities/myclass/, он выглядел бы так:
$path = MODX_CORE_PATH . 'components/mycomponent/model/';
$modx->loadClass('utilities.myclass.MyClass', $path, true, true);

В приведенном выше примере myclass — это название каталога, содержащего файл класса (myclass.class.php), а MyClass — фактическое имя класса. Имя файла будет myclass.class.php.
Файлы классов в MODX всегда должны называться classname.class.php.

Как насчет последних двух аргументов ($ignorePkg, $transient)?
Первый из них, $ignorePkg, сообщает loadClass(), следует ли игнорировать пакет. По умолчанию он равен false, и его значение не является критическим. Если оно ложно, MODX будет искать пакет, относящийся к классу, но вызов будет успешным независимо от того, найдет он его или нет. Если вы знаете, что метод addPackage() не вызывался, вы можете немного ускорить процесс, установив для него значение true, чтобы MODX знал, что поиск пакетов нужно пропустить.

Второй аргумент является критическим. Он сообщает loadClass(), является ли класс временным или нет. Если для него установлено значение false, это сообщает MODX, что пакет не является временным, а это означает, что он хранится в базе данных. Если ваш класс не представляет объект базы данных, MODX не найдет его. Вызов завершится ошибкой, и файл класса не будет загружен.

Если ваш класс не представляет объект базы данных, последний аргумент всегда должен иметь значение true. Если это объект базы данных, и ваш код будет выполнять какое-либо взаимодействие с базой данных, для него следует установить значение false.

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

getService()


public function &getService($name, $class= '', $path= '', $params= array ())

getService() загружает файл класса (вызывая loadClass()), но также добавляет класс к объекту $modx, вы cможете вызывать его методы с помощью $modx->className->methodName().
getService() возвращает null в случае ошибки, и пока вы указываете полный путь, вам не нужно запускать ни addPackage(), ни loadClass() перед его вызовом:

$loginModelPath = MODX_CORE_PATH . 'components/login/model/';
$success = $modx->getService('login', 'login.Login', $loginModelPath);
echo "\ngetService" . ($success != null ? 'OK' : 'Failed');
// OK

if ($modx->login instanceof Login) {
echo "\n Login OK";
}
// Login OK

Для примера, класс doodles имеет метод getChunk(), который переопределяет одноименный метод MODX. Поэтому, если вы добавили класс doodles с помощью getService(), например, вы можете использовать $modx->doodles->getChunk() для вызова этого метода.

Эта техника будет работать внутри другого класса или везде, где доступен объект $modx, хотя класс сервиса (service) доступен только во время запроса, когда он был загружен как сервис. Это очень удобно для CMP, таких как Doodles.

Если сервис уже был загружен, getService() просто возвращает ссылку на него. Однако, поскольку он уже присоединен к объекту $modx, вам на самом деле не нужна ссылка, поэтому возвращаемое значение getService() почти никогда не используется в коде. Если сервис не существует, MODX создаст его, и его можно будет использовать на протяжении всего цикла запроса.

Определенные сервисные классы, такие как modLexicon и modError, почти всегда доступны в MODX, но никогда не помешает убедиться в этом, вызвав для них getService(). В коде, работающем вне MODX, это критично. Код, работающий вне MODX, обычно должен загружать службу ошибок. В противном случае программа рухнет, если возникнут какие-либо ошибки MODX.

$modx->getService('error', 'modError');
$modx->getService('lexicon', 'modLexicon');
$modx->getService('fileHandler', 'modFileHandler');

Первый аргумент getService(), $name — это псевдоним(alias), который можно использовать для вызова сервиса. Второй аргумент, $class, должен быть фактическим именем класса. Итак, если вы хотите использовать лексикон MODX после того, как сервис был создан с помощью getService() (либо вами, либо MODX), вы делаете это с псевдонимом:
$message = $modx->lexicon('file_not_found');

Если сервис является встроенным сервисом MODX (lexicon, mail, error и т. д.), вам не нужно указывать путь, поскольку core/model/ в MODX является значением по умолчанию для аргумента $path и там же почти все они находятся, хотя некоторые, вроде modRegistry и modPHPMailer, находятся в каталоге модели MODX, но немного глубже:
$modx->getService('registry', 'registry.modRegistry');
$modx->getService('mail', 'mail.modPHPMailer');

Последний аргумент getService()($params) может содержать ассоциативный массив параметров, которые будут доступны в массиве $scriptProperties внутри класса сервиса.

Подытожим


Метод loadClass() является хорошей альтернативой include и require. Он загрузит специфичную для базы данных информацию для объекта, что важно, если ваш класс представляет объект базы данных, и вы используете базу данных, отличную от MySQL. Все три метода пропустят загрузку класса, если он уже загружен.

Кроме того,loadClass() позволит вам указать файл как «переходный или временный», и в этом случае он пропустит загрузку информации, относящейся к базе данных, и некоторые другие шаги в процессе. Установка аргумента $transient в значение true имеет решающее значение, если ваш класс не представляет объект базы данных, иначе вызов завершится ошибкой.

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

И addPackage(), и loadClass() возвращают false в случае ошибки, но я обнаружил, что в некоторых случаях (например, когда вы не включаете путь) они могут возвращать не false, но вы все равно не можете создать экземпляр класса.
getService() вернет null в случае ошибки.

После того, как вы зарегистрировали пакет с помощью addPackage(), его объекты, связанные с БД, будут автоматически вызывать loadClass() (если класс еще не загружен), когда вы пытаетесь получить к ним доступ с помощью любого метода объекта xPDO, такого как newObject(), getObject () или getCollection(). С этими классами вы всегда должны использовать $modx->newObject() для их создания, а не просто new. В этом случае нет необходимости вызывать loadClass.

Метод getService() прикрепит класс к объекту $modx. Как только это будет сделано, вы можете вызывать методы класса с помощью $modx->serviceAlias->methodName() в любое время в течение текущего цикла запроса. Для встроенных сервисов MODX вы можете использовать сокращенную версию: $modx->lexicon().

Памятка


  • Если вы просто хотите создать экземпляр некоторого класса, который не представляет объект БД, используйте loadClass(). Обязательно укажите путь и установите для четвертого аргумента значение true, иначе вызов завершится ошибкой.
  • Если вы хотите создать экземпляр некоторого класса, представляющего объект БД, и для него были сгенерированы файлы классов и карт, используйте addPackage(). loadClass() будет вызываться автоматически при любом вызове объекта xPDO, который обращается к базе данных, но если вы будете использовать свой класс до того, как это произойдет, вам нужно сначала вызвать loadClass() самостоятельно.
  • Если вы хотите, чтобы класс был доступен как $modx->className, или если класс уже зарегистрирован как служба MODX (например, modMail, modLexicon, modError), используйте getService(). В последнем случае путь не нужен, но если пакет находится в подкаталоге каталога core/, его нужно будет указать: $modx->getService('registry,Registry.modRegistry').


В MODx 3


имеем следующую картину:
— метод getService() помечен как deprecated

Вместо него будет контейнер зависимостей и автозагрузка.
MODX3 использует контейнер зависимостей (Dependency Injection Container) основанный на Pimple 3, который содержит основные(core) и пользовательские сервисы.

Контейнер доступен в modX:$services, и его можно будет использовать одним из следующих способов:
$modx->services // (in snippets, plugins, etc)
$this->modx->services // (in controllers, processors, etc)
$this->xpdo->services // (in model classes)

Доступные сервисы


На данный момент в ядре автоматически регистрируются следующие сервисы:

config: возвращает массив свойств конфигурации.

Загрузка сервисов


Чтобы загрузить сервис из контейнера, вызовите метод get (и/или метод has(), чтобы сначала проверить, существует ли он) с идентификатором службы.
try {
    $config = $modx->services->get('config');
}
catch (ContainerExceptionInterface $e) {
    // handle the thing not being available
}
или
$service = null;
try {
    if ($modx->services->has('custom_service')) {
        $service = $modx->services->get('custom_service');
    }
}
catch (ContainerExceptionInterface $e) {
    // handle the thing not being available
}
## Существующий сервис (т. е. has(), возвращающая true) не является гарантией того, что он не вызовет исключения, когда для нее вызывается get(). *Не очень понятный момент из документации, как же тогда проверять правильно ли был загружен сервис?*

Внедрение сервисов


Рекомендуемый способ внедрить или перезаписать сервисы в расширении — пространства имен файла bootstrap.php, который, в процессе инициализации, запускается как можно раньше.

Например, вы можете вызвать:
$modx->services->add('my_service', function($c) use ($modx) {
    return new MyPackage\MyService($modx);
});

Вы можете использовать правильное внедрение зависимостей, передав необходимые сервисы. Гипотетический пример:
$modx->services->add('my_service', function($c) use ($modx) {
    return new MyPackage\MyService($c['sessions'], $c['parser']);
});

Если вам требуется возвращать новый экземпляр для каждого запроса сервиса, используйте метод factory():
$modx->services['my_service'] = $modx->services->factory(function($c) use ($modx) {
    return new MyPackage\MyService($modx);
});

Чтобы расширить ранее определенный сервис, используйте extend:
$modx->services->extend('existing_service', function($existing, $c) use ($modx) {
    $existing->...();
    return $existing;
});


Больше примеров можно найти в документации к Pimple 3
Евгений Webinmd
22 января 2022, 01:06
modx.pro
6
1 653
+14

Комментарии: 8

Александр Мельник
22 января 2022, 10:19
0
спасибо за информацию.
Меня удивил выбор контейнера зависимостей для modx3.
Могу ошибаться но github.com/silexphp/Pimple мне кажется не очень популярным решением.
Ведь есть же программы, которые куда более популярны и уже чуть ли не десятилетний опыт имеют, например php-di.org/
    Aleksandr Huz
    22 января 2022, 11:55
    0
    Могу ошибаться но github.com/silexphp/Pimple мне кажется не очень популярным решением
    Pimple имеет 58+ миллионов загрузок, против 8.8 php-di, да и звездочек у Pimple на гитхабе больше, поэтому интересно, с чего ты решил что php-di более популярнее?
      Александр Мельник
      22 января 2022, 11:58
      0
      я же сказал что могу ошибаться.
      Сужу исключительно из своего опыта. Никогда не слышал про pimple, но очень часто слышу про phpdi и сам им пользуюсь)
      Иван Бочкарев
      22 января 2022, 16:39
      0
      @Сергей Шлоков прийди к нам =)
      Максим
      11 февраля 2022, 09:04
      0
      Если вы просто хотите создать экземпляр некоторого класса… используйте loadClass()
      Почему-то всегда думал, что loadClass просто подключает класс, а getService как раз создаёт экземпляр.
        Сергей Шлоков
        13 февраля 2022, 09:48
        +1
        Если внимательно прочитать, то так и написано. Просто не совсем корректно.

        getService() загружает файл класса (вызывая loadClass()), но также добавляет класс к объекту $modx, вы cможете вызывать его методы с помощью $modx->className->methodName().
        Правильнее было бы сказать, что к объекту $modx добавляется не класс, а экземпляр указанного класса. Он же возвращается методом getService().

        Соответственно получить доступ к этому экземпляру можно не через $modx->className, а через $modx->aliasName. Ниже в статье именно так и будет указано. Получается небольшое разночтение. Т.е. псевдоним вы можете указать любой (первый параметр), а вот класс (второй параметр) должен быть точным.
        Станислав
        13 июня 2023, 13:27
        +1
        Подскажите, я правильно понимаю, что если я раньше использовал
        $client = $modx->getService('rest', 'rest.modRest');
        то теперь
        $client = $modx->services->get('rest');
        А то на прошлый вариант пишет deprecated и не могу найти доки как в 3 версии rest клиентом пользоваться
        В целом код был такой у меня раньше
        $client = $modx->getService('rest', 'rest.modRest');
        $client->setOption('timeout', 15);
        $client->setOption('header', true);
        $client->setOption('connectTimeout',10);
        $response = $client->get($url, $params);
        $data = $response->process();
        $arr = $modx->sanitize($data, $modx->sanitizePatterns);
        и по сути только меняется getService?
          Станислав
          13 июня 2023, 14:04
          +1
          Только сейчас увидел, что у англ документации написано
          Note: modRest is deprecated.
          It's strongly encouraged to use the PSR HTTP Services provided since MODX 3.0.0-beta1.
          и ссылка на https://docs.modx.com/3.x/en/extending-modx/services/http
          а в ру документации вообще нет раздела про HTTP клиента и этого замечания, что метод устарел.
          Вообщем, как я понимаю, что надо переписывать
          $client = $modx->getService('rest', 'rest.modRest');
          на
          $client = $modx->services->get(\Psr\Http\Client\ClientInterface::class);
          в modx3
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          8