По вашему запросу ничего не найдено :(
Убедитесь, что запрос написан правильно, или посмотрите другие
наши статьи:
Что бы рассказать обо всех особенностях Siebel CRM одной статьи точно не хватит, нужно написать как минимум книгу. Поэтому в данной статье мы постараемся осветить самые главные моменты, расскажем об основных составляющих, архитектуре и функциональных возможностях одной из самых востребованных на рынке управления взаимоотношениями с клиентами системе – Siebel CRM.
Компания Siebel Systems была одной из первых, вышедших на рынок CRM систем, в 2000-х годах ее доля относительно остальных вендоров подобных решений составляла 45%. Ее заказчиками в разное время становились такие компании как Cisco Systems и Compaq. В 2005 Siebel Systems была поглощена компанией Oracle и теперь полное название системы управления взаимоотношениями с клиентами выходит под брендом Oracle Siebel CRM.
Стоит сразу оговориться, что Siebel CRM ориентирована на большие компании, с численностью 10 000 и более сотрудников, а значит должна быть легко масштабируема и абсолютно совместима с различными платформами, обеспечивая одновременный доступ тысяч пользователей к корпоративным данным. Поэтому современные версии Siebel CRM имеют стандартную вэб-ориентированную, модульную архитектуру.
Основные составляющие архитектуры Siebel CRM
Реляционная база данных
Хранит в себе клиентскую и административную информацию, а также репозитории (различные версии конфигураций Siebel CRM). В качестве такой базы данных может использоваться MS SQL, Oracle Enterprise Server, IBM D2B и др.
Общий каталог
Хранит нереляционную и бинарную информацию, например: вложения, документы и временные файлы.
Сервера Siebel
В системе может быть один или несколько Siebel серверов, вместе они образуют Enterprise сервер. Enterprise сервер – это некое логическое объединение, которое обеспечивает доступ ко всей базе данных и файловой системе и которое управляется одним Siebel Gateway Name Server’ом.
Siebel Gateway Name Server
Обеспечивает управление всеми Siebel серверами, входящими в Enterprise и, соответственно, хранит его конфигурацию.
Вэб-сервер
Принимает http запросы от web-браузеров пользователей. Подойдут Oracle Apache или IIS
Siebel Web Server Extension
Устанавливается на вэб-сервер, осуществляет взаимодействие web-браузеров с объектами Siebel серверов, обеспечивает аутентификацию пользователей и балансировку нагрузки.
Вэб-браузер
Обеспечивает графическое отображение объектов Siebel CRM и доступ к интерфейсу пользователя. Полный доступ обеспечивает только Internet Explorer, в ограниченном режиме поддерживаются Mozilla Firefox и Safari.
Ниже представлено схематичное представление архитектуры Web Siebel CRM
Таким образом, Siebel CRM представляет из себя некое приложение, доступ к которому осуществляется через Интернет по специальному URL.
Функциональные возможности Siebel CRM:
Взаимосвязь между сущностями
Возможность настроить сложную иерархическую связь между сущностями системы: Компания, Контакт, Лицевой счет, Устройство, Услуга, Платеж, Справочник адресов, Справочник телефонов, Сервисное обращение, Взаимодействие.
Возможности интеграции
Интеграция может производиться онлайн как через вэб-сервисы, так и с помощью представлений, на уровне данных. Например, для интеграции с телефонией используется программируемый интерфейс Siebel Communication Layer.
Возможности идентификации клиента
При поступлении входящего звонка, механизмы Siebel CRM запускают процесс поиска звонящего в базе телефонных номеров, а перед пользователем всплывает соответствующая карточка клиента со всей доступной информацией.
Взаимодействие с почтой
Сервис Siebel Email Response позволяет получать и отправлять письма через любой почтовый сервер. Данный компонент также дает возможность фиксировать получение письма, открытие и переход по ссылке внутри письма.
Автоматическое создание кампаний обзвона
Возможность формирования кампаний исходящего обзвона с выборкой номеров целевой аудитории для дальнейшей обработки в call-центре.
Перечисленный выше функционал – далеко не полный список возможностей Siebel CRM, однако, пожалуй основной для большинства отделов современных компаний.
С помощью PHP можно относительно легко создать веб-приложение, и это является одной из причин, по которой он так популярен. Однако, несмотря на простоту его использования, PHP стал довольно навороченным языком с большим количеством различных нюансов и тонкостей, которые могут усложнить процесс отладки для разработчиков так, что они волосы начнут на себе рвать. В этой статье я выделил десять самых распространенных ошибок, которых PHP-разработчикам следует избегать.
Ошибка №1. Оставлять висячие ссылки на массивы после циклов foreach
Не знаете точно, как использовать циклы foreach в PHP? Если вы хотите работать с каждым элементом итерируемого массива в отдельности, использование ссылок в циклах foreach может оказаться полезным.
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
$value = $value * 2;
}
// $arr is now array(2, 4, 6, 8) { // $arr – это теперь array(2, 4, 6, 8) }
Проблема в том, что, если вы не будете достаточно осторожны, это может привести к некоторым нежелательным побочным эффектам и последствиям. В частности, если мы посмотрим на пример выше, то увидим, что после того, как код выполнится, $value останется в области видимости и будет хранить ссылку на последний элемент массива. Таким образом, если вы будете использовать $value и дальше, то можете случайно изменить последний элемент массива.
Главное запомните, что у foreach нет области видимости. А значит, $value в приведенном выше примере является ссылкой на верхнюю часть сценария. На каждой итерации foreach задает ссылку, которая указывает на следующий элемент массива $array. Так что, после того, как цикл закончит свою работу, $value так и будет указывать на последний элемент $array и останется в области видимости.
Ниже приведен пример ошибок, к которым это может привести. Такие ошибки сбивают с толку и их довольно трудно обнаружить.
$array = [1, 2, 3];
echo implode(',', $array), "\n";
foreach ($array as &$value) {} // by reference { // по ссылке }
echo implode(',', $array), "\n";
foreach ($array as $value) {} // by value (i.e., copy) { // по значению (то есть путем копирования) }
echo implode(',', $array), "\n";
Приведенный выше код выдаст следующий результат:
1,2,3
1,2,3
1,2,2
Нет, это не опечатка. Последнее значение в последней строке и правда равно 2, а не 3.
Почему же?
После того, как мы прошли первый цикл foreach, $array остается таким же, но, как мы уже говорили выше, $value остается в качестве висячей ссылки на последний элемент $array (так как цикл foreach обращался к $value по ссылке).
В результате, когда мы проходим по второму циклу foreach, начинают происходить всякие «странные вещи». В частности, так как теперь мы осуществляем доступ к $value по значению (то есть путем копирования), foreach на каждом шаге цикла копирует каждый последующий элемент массива $array в $value. Таким образом, на каждом шаге второго цикла происходит следующее:
Проход 1: $array[0] (т.е. «1») копируется в $value (которое является ссылкой на $array[2]), поэтому теперь $array[2] равен 1. Так что, теперь $array содержит [1, 2, 1].
Проход 2: $array[1] (т.е. «2») копируется в $value (которое является ссылкой на $array[2]), поэтому теперь $array[2] равен 2. Так что, теперь $array содержит [1, 2, 2].
Проход 3: $array[2] (которое теперь равно «2») копируется в $value (которое является ссылкой на $array[2]), так что $array[2] так и остается равным 2. Поэтому теперь $array содержит [1, 2, 2].
Для того, чтобы быть иметь возможность использовать преимущества ссылок в циклах foreach и при этом быть уверенным в том, что подобных проблем возникать не будет, вызовите функцию unset() для переменной сразу после цикла foreach, чтобы удалить ссылку. Например,
$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
$value = $value * 2;
}
unset($value); // $value no longer references $arr[3] { // $value уже не является ссылкой на $arr[3] }
Ошибка №2. Непонимание того, как ведет себя функция isset()
Несмотря на свое название, функция
isset()
возвращает значение false не только для несуществующих элементов, но и для нулевых значений (null).
Такое поведение функции может привести к куда более серьезным проблемам, чем кажется на первый взгляд. Именно оно и является довольно-таки распространенным источником проблем.
Давайте посмотрим на следующий фрагмент кода:
$data = fetchRecordFromStorage($storage, $identifier);
if (!isset($data['keyShouldBeSet']) {
// do something here if 'keyShouldBeSet' is not set
{ // если 'keyShouldBeSet' не существует, выполняются действия }
}
Тот, кто писал этот код, судя по всему, хотел проверить, существует ли keyShouldBeSet в $data. Но, как мы уже говорили ранее, функция isset($data['keyShouldBeSet']) вернет false, даже если $data['keyShouldBeSet'] существует, но имеет значение null. Так что, логика, приведенная выше, неверна.
А вот другой пример:
if ($_POST['active']) {
$postData = extractSomething($_POST);
}
// ...
if (!isset($postData)) {
echo 'post not active';
}
Код выше предполагает, что, если $_POST['active'] возвращает true, то postData обязательно существует, а, значит, isset($postData) вернет true. И наоборот, если isset($postData) возвращает false, то и $_POST['active'] возвращает false.
Но это не так.
Как мы уже говорили, isset($postData) может также вернуть false, если $postData имеет значение null. Таким образом, функция isset($postData) может вернуть false, даже если $_POST['active'] вернул true. И опять получается, что логика, приведенная выше, неверна.
К слову, если цель приведенного выше кода на самом деле заключалась в том, чтобы еще раз проверить, возвращает ли $_POST['active'] значение true, прибегнуть к помощи функции isset() было не самым хорошим решением. Вместо этого было бы гораздо лучше просто перепроверить $_POST['active'], то есть:
if ($_POST['active']) {
$postData = extractSomething($_POST);
}
// ...
if ($_POST['active']) {
echo 'post not active';
}
Правда, говоря о случаях, когда нам нужно проверить, существует ли переменная на самом деле (то есть отличить переменную, которая не существует, от переменной, значение которой равно null), гораздо более надежным решением будет использование метода array_key_exists().
Например, вы могли бы переписать первый пример следующим образом:
$data = fetchRecordFromStorage($storage, $identifier);
if (! array_key_exists('keyShouldBeSet', $data)) {
// do this if 'keyShouldBeSet' isn't set
{ // если ‘keyShouldBeSet’ не существует, то выполняются действия, указанные здесь }
}
Более того, вы можете проверить, существует ли переменная в текущей области видимости. Для этого вам нужно объединить методы array_key_exists() и get_defined_vars():
if (array_key_exists('varShouldBeSet', get_defined_vars())) {
// variable $varShouldBeSet exists in current scope
{ // переменная $varShouldBeSet существует в текущей области видимости }
}
Ошибка №3. Путаница с возвратом по ссылке и по значению
Давайте посмотрим на следующий фрагмент кода:
class Config
{
private $values = [];
public function getValues() {
return $this->values;
}
}
$config = new Config();
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
Если вы запустите этот код, то получите следующий результат:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
Что же не так?
Проблема в том, что в коде, который мы привели выше, произошла путаница с возвратом массива по ссылке и по значению. Если вы явно не укажите PHP, чтобы он вернул массив по ссылке (то есть с помощью &), то он по умолчанию вернет массив «по значению». Это значит, что будет возвращена копия массива, и, как следствие, вызываемая функция и вызывающая сторона будут обращаться к разным экземплярам массива.
Так что, вызываемая выше функция getValues() возвращает копию массива $values, а не ссылку на него. Помня об этом, вернемся к двум ключевым строкам из примера выше:
// getValues() returns a COPY of the $values array, so this adds a 'test' element
// to a COPY of the $values array, but not to the $values array itself.
{ // функция getValues() возвращает КОПИЮ массива $values, а значит, что элемент test добавляется в КОПИЮ массива $values, в не в сам массив. }
$config->getValues()['test'] = 'test';
// getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't
// contain a 'test' element (which is why we get the "undefined index" message).
{ // функция getValues() снова возвращает ДРУГУЮ КОПИЮ массива $values, и в ЭТОЙ копии нет элемента test (собственно говоря, почему мы и получаем сообщение о неопределенном индексе) }
echo $config->getValues()['test'];
Одно из возможных решений этой проблемы заключается в следующем: вы можете сохранить первую копию массива $values, которую возвращает функция getValues(), и дальше работать с этой копией. Например,
$vals = $config->getValues();
$vals['test'] = 'test';
echo $vals['test'];
Этот код сработает отлично, то есть он выведет элемент test, не выдавая сообщения о неопределенном индексе. Однако такой подход может оказаться непригодным; это зависит от ваших целей. В частности, с помощью этого кода вы не сможете изменить исходный массив $values. Так что, если вы хотите, чтобы изменения (например, добавление элемента test) касались также исходного массива, вам нужно изменить функцию getValues() так, чтобы она возвращала ссылку на сам массив $values. Вы можете это сделать, добавив & перед именем функции, показывая таким образом, что она должна вернуть ссылку:
class Config
{
private $values = [];
// return a REFERENCE to the actual $values array
{ // возвращает ССЫЛКУ на фактический массив $values }
public function &getValues() {
return $this->values;
}
}
$config = new Config();
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
Как мы и хотели, результатом будет test.
Но давайте запутаем ситуацию еще больше. Рассмотрим следующий код:
class Config
{
private $values;
// using ArrayObject rather than array
{ // используем ArrayObject вместо массива }
public function __construct() {
$this->values = new ArrayObject();
}
public function getValues() {
return $this->values;
}
}
$config = new Config();
$config->getValues()['test'] = 'test';
echo $config->getValues()['test'];
Если вы считаете, что здесь мы получим ту же ошибку с неопределенным индексом (что и в предыдущем примере), то вы ошибаетесь. На самом деле, этот код будет отлично работать. Это связано с тем, что PHP всегда передает объекты по ссылке (в отличие от массивов). (ArrayObject – это объект SPL, который полностью имитирует использование массивов, но при этом работает как объект.)
Как вы могли заметить, в PHP не всегда можно сразу понять, работаете вы с копией или с ссылкой. Поэтому важно знать, как ведут себя те или иные объекты по умолчанию (т.е. то, что переменные и массивы передаются по значению, а объекты – по ссылке). Кроме того, стоит хорошо изучить документацию API для функции, которую вы вызываете, чтобы понимать, что она возвращает (значение, копию массива, ссылку на массив или ссылку на объект).
При этом стоит отметить, что возвращение ссылок на массив или ArrayObject, как правило, считается не самой лучшей практикой, так как вызывающая сторона может изменить частные данные экземпляра. Это противоречит понятию инкапсуляции. Вместо этого лучше использовать старомодные геттеры и сеттеры. Например,
class Config
{
private $values = [];
public function setValue($key, $value) {
$this->values[$key] = $value;
}
public function getValue($key) {
return $this->values[$key];
}
}
$config = new Config();
$config->setValue('testKey', 'testValue');
echo $config->getValue('testKey'); // echos 'testValue'
При таком подходе вызывающая сторона может задать или получить любое значение массива, не предоставляя общий доступ к самому массиву $values, который скрыт для других.
Ошибка №4. Выполнение запросов внутри цикла
Нередко можно столкнуться с чем-то подобным, что приводит к тому, что ваш PHP-код не работает:
$models = [];
foreach ($inputValues as $inputValue) {
$models[] = $valueRepository->findByValue($inputValue);
}
По сути, здесь все верно, но если вы проследите за логикой кода, то можете обнаружить, что безвредный на первый взгляд вызов $valueRepository->findByValue() в итоге приводит к появлению некоторого запроса:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
В результате, с каждой итерацией будет выполняться запрос к базе данных. Поэтому, если вы передадите в цикл массив из 1000 элементов, то он создаст 1000 отдельных запросов к базе данных! А если этот сценарий будет вызываться в нескольких потоках, то в теории это может привести к полной остановке системы.
Именно поэтому очень важно уметь распознавать, когда ваш код создает запросы, и по возможности собирать эти значения, чтобы выполнить один запрос, который вернет все результаты.
Существуют довольно распространенные ситуации, когда запросы выполняются неэффективно (т.е. в цикле). Например, когда отправляется форма со списком значений (например, идентификаторов). После чего код будет перебирать массив в цикле и выполнять отдельный SQL-запрос для каждого идентификатора, чтобы получить полные данные записи для каждого из них. В большинстве случаев это будет выглядеть следующим образом:
$data = [];
foreach ($ids as $id) {
$result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id);
$data[] = $result->fetch_row();
}
Но то же самое можно выполнить гораздо более эффективным способом. Вам понадобиться всего один SQL-запрос:
$data = [];
if (count($ids)) {
$result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids));
while ($row = $result->fetch_row()) {
$data[] = $row;
}
}
Таким образом, очень важно уметь распознавать, выполняет ваш код запросы напрямую или нет. По возможности соберите значения, а потом, чтобы получить все результаты, выполните единый запрос. И все же даже здесь следует быть осторожным. И здесь мы плавно переходим к следующей распространенной ошибке…
Ошибка №5. Ложное и нерациональное использование памяти
Несмотря на то, что извлечение множества записей за раз куда как более эффективно, чем выполнение запросов для каждой строки, которую вы хотите извлечь, если вы используете расширение PHP mysql и библиотеку libmysqlclient, то такой подход в теории может привести к ситуации под названием «недостаточно памяти».
Чтобы понять, о чем я говорю, давайте посмотрим на пример с ограниченными ресурсами (512 Мб оперативной памяти), MySQL и php-cli.
Давайте загрузим таблицу базы данных:
// connect to mysql { // подключаемся к MySQL }
$connection = new mysqli('localhost', 'username', 'password', 'database');
// create table of 400 columns { // создаем таблицу из 400 столбцов }
$query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT';
for ($col = 0; $col < 400; $col++) {
$query .= ", `col$col` CHAR(10) NOT NULL";
}
$query .= ');';
$connection->query($query);
// write 2 million rows { // добавляем 2 миллиона строк }
for ($row = 0; $row < 2000000; $row++) {
$query = "INSERT INTO `test` VALUES ($row";
for ($col = 0; $col < 400; $col++) {
$query .= ', ' . mt_rand(1000000000, 9999999999);
}
$query .= ')';
$connection->query($query);
}
Окей, а теперь давайте проверим, как используются ресурсы:
// connect to mysql { // подключаемся к MySQL }
$connection = new mysqli('localhost', 'username', 'password', 'database');
echo "Before: " . memory_get_peak_usage() . "\n";
$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1');
echo "Limit 1: " . memory_get_peak_usage() . "\n";
$res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000');
echo "Limit 10000: " . memory_get_peak_usage() . "\n";
Вот так выглядит результат:
Before: 224704
Limit 1: 224704
Limit 10000: 224704
Отлично. Выглядит так, как будто с точки зрения ресурсов запрос благополучно управляется внутри.
Но, чтобы убедиться наверняка, давайте увеличим значение LIMIT до 100 000. Опа, и вот что мы получим:
PHP Warning: mysqli::query(): (HY000/2013):
Lost connection to MySQL server during query in /root/test.php on line 11
Что же произошло?
Здесь проблема заключается в том, как работает модуль PHP mysql. По сути это просто прокси для libmysqlclient, который выполняет всю грязную работу. Когда часть данных уже выбрана, они попадаю непосредственно в память. А так как эта память никак не управляется менеджером PHP, функция memory_get_peak_usage() не сообщит нам о том, что количество использованных ресурсов возросло, так как мы увеличиваем LIMIT в нашем запросе. Это приводит к проблемам, о которых мы говорили выше. Получается, что нас обманным путем пытаются уверить, что с нашим управлением памятью все в порядке. Но по сути наше управление памятью далеко не идеально, и мы можем столкнуться с проблемами (такими как мы описывали выше).
Во всяком случае, вы можете избежать ситуаций с ложным использованием памяти, описанных выше. Для этого вам нужно использовать модуль mysqlnd. Хотя сам по себе он вам никак не поможет оптимизировать использование вашей памяти. Этот модуль скомпилирован как собственное расширение PHP, и он использует диспетчер памяти PHP.
Таким образом, если мы запустим тестовый код, приведенный выше, и вместо mysql воспользуемся mysqlnd, то получим гораздо более реалистичную картину использования нашей памяти:
Before: 232048
Limit 1: 324952
Limit 10000: 32572912
И кстати, она будет куда хуже. Если обратиться к документации PHP, то там можно найти, что mysql использует в два раза больше ресурсов для хранения данных, чем mysqlnd. Именно поэтому исходный сценарий, в котором мы использовали mysql, на самом деле использовал даже больше памяти, чем продемонстрировано здесь (примерно в два раза больше).
Дабы избежать такого рода проблем, попробуйте ограничить размер ваших запросов или использовать цикл с меньшим количеством итераций. Например,
$totalNumberToFetch = 10000;
$portionSize = 100;
for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) {
$limitFrom = $portionSize * $i;
$res = $connection->query(
"SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize");
}
Если мы рассуждаем об этой ошибке и ошибке №4, то должны понимать, что необходимо найти золотую середину между слишком детализированными и повторяющимися запросами и огромными штучными запросами. Как и почти во всем в этой жизни, необходим баланс; любая крайность может навредить корректной работе PHP.
Ошибка №6. Игнорирование проблем с Unicode/UTF-8
В некотором смысле это скорее проблема самого PHP, нежели что-то, с чем вы столкнетесь при отладке кода. Однако эту проблему так толком и не решили. Ядро PHP 6 должно было поддерживать Unicode, но, так как разработка PHP 6 была приостановлена в 2010 году, с этим пришлось повременить.
Но это вовсе не значит, что разработчики могут игнорировать правильную обработку UTF-8 и думать, что все строки обязательно будут «старым добрым ASCII». Код, который не может правильно обработать не-ASCII строки, печально известен внедрением опасных гейзенбагов. Даже простые вызовы strlen($_POST['name']) могут вызвать проблемы, если некто с фамилией Schr?dinger захочет зарегистрироваться в вашей системе.
Вашему внимаю предоставляю небольшой чек-лист, который поможет вам избежать таких проблем в вашем коде:
Если вы мало что знаете о Unicode и UTF-8, то изучите, по крайней мере, основы.
Вместо старых строковых функций используйте функции mb_* (убедитесь, что в вашей сборке PHP есть расширение multibyte).
Убедитесь, что ваша база данных и таблицы настроены таким образом, что могут использовать Unicode (многие сборки MySQL все еще используют по умолчанию latin1).
Помните, что json_encode() преобразует не-ASCII символы (например, Schr?dinger преобразуется в Schr\u00f6dinger), а serialize() - нет.
Убедитесь, что ваши файлы с PHP-кодом имеют кодировку UTF-8. Это необходимо, чтобы избежать конфликтов при объединении строк с жестко запрограммированными или сконфигурированными строковыми постоянными.
Ошибка №7. Предполагать, что $_POST всегда будет содержать ваши данные POST-запроса
Несмотря на свое название, массив $_POST не всегда будет содержать ваши данные POST-запроса. Он и вовсе может оказаться пустым. Чтобы разобраться в этом, давайте рассмотрим пример. Допустим, что мы делаем запрос к серверу, вызвав jQuery.ajax():
// js
$.ajax({
url: 'http://my.site/some/path',
method: 'post',
data: JSON.stringify({a: 'a', b: 'b'}),
contentType: 'application/json'
});
(К слову, обратите внимание, что здесь есть contentType: 'application/json'. Мы отправляем данные в формате JSON, что довольно популярно для API. Это стандартная практика, например, при добавлении службы $http в AngularJS.)
Мы просто распечатываем массив $_POST на серверной стороне:
// php
var_dump($_POST);
Сюрприз! А вот и результат:
array(0) { }
Но почему? Что случилось со строкой JSON {a: 'a', b: 'b'}?
Ответ таков: PHP анализирует полезную нагрузку POST-запроса автоматически только в том случае, если она имеет тип содержимого application/x-www-form-urlencoded или multipart/form-data. Причины такого поведения исторически сложившиеся – много лег назад, когда $_POST был только реализован в PHP, это были единственные типы содержимого. Таким образом, PHP не загружает полезную нагрузку POST-запроса автоматически ни для каких других типов содержимого (даже для тех, которые считаются довольно популярными на сегодняшний день, например, application/json).
Так как $_POST - это суперглобальная переменная, то переопределив ее один раз (желательно в самом начале нашего сценария), мы сможем пользоваться этим измененным значением (т.е. в том числе и полезной нагрузкой POST-запроса) во всем нашем коде. Это очень важно, так как фреймворки PHP и почти все пользовательские сценарии используют $_POST для того, чтобы извлекать и преобразовывать данные запроса.
Поэтому, если мы, например, обрабатываем полезную нагрузку POST-запроса с типом содержимого application/json, то мы должны вручную проанализировать содержимое запроса (т.е. декодировать данные JSON) и переопределить переменную $_POST:
// php
$_POST = json_decode(file_get_contents('php://input'), true);
После чего, когда мы распечатаем массив, мы увидим, что он должным образом содержит полезную нагрузку POST-запроса:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
Ошибка №8. Думать о том, что PHP поддерживает символьный тип данных
Посмотрите на следующий фрагмент кода и попробуйте догадаться, что он выведет:
for ($c = 'a'; $c <= 'z'; $c++) {
echo $c . "\n";
}
Если вы ответили, что он выведет буквы от «a» до «z», то, вы будете удивлены – это не так.
Да, он выведет буквы от «а» до «z», но потом он также выведет сочетания букв от «аа» до «yz». Давайте разберемся, почему так происходит.
В PHP нет такого типа данных как char; есть только тип string. Принимая это в расчет, мы должны понимать, что, увеличив строку z, PHP выдаст aa:
php> $c = 'z'; echo ++$c . "\n";
aa
Впрочем, запутаю вас еще больше: с лексикографической точки зрения aa меньше, чем z:
php> var_export((boolean)('aa' < 'z')) . "\n";
true
Именно поэтому пример кода, который мы привели в начале, сначала напечатает буквы от a до z, а потом также напечатает сочетания букв от aa до yz. Он остановится, когда достигнет значения za – первого значения, которое «больше», чем z:
php> var_export((boolean)('za' < 'z')) . "\n";
false
А раз так, то вот один из способов, как можно правильно перебрать в PHP значения от «а» до «z»:
for ($i = ord('a'); $i <= ord('z'); $i++) {
echo chr($i) . "\n";
}
Или вот так:
$letters = range('a', 'z');
for ($i = 0; $i < count($letters); $i++) {
echo $letters[$i] . "\n";
}
Ошибка №9. Игнорирование стандартов написания кода
И хотя игнорирование стандартов написания кода не приводит сразу к необходимости отлаживать PHP-код, это все же, пожалуй, одна из самых важных тем, которую стоит здесь обсудить.
Если вы будете игнорировать эти стандарты, то можете столкнуться с целым рядом проблем в своем проекте. В самом лучшем случае это приведет к тому, что ваш код будет непоследовательным (так как каждый разработчик «занимается своим делом»). А в худшем случае вы получите PHP-код, который не будет работать или будет слишком сложен для перемещения по нему (в некоторых случаях это может быть практически невозможно), что, соответственно, приводит к тому, что его будет крайне сложно отлаживать, совершенствовать и сопровождать. А это значит, что продуктивность вашей команды снизится, а все из-за того, что вы будете тратить свои силы впустую.
К счастью для PHP-разработчиков, существуют Рекомендации по стандартам PHP (PSR - PHP Standards Recommendation). В ней есть пять стандартов:
PSR-0: Стандарт автозагрузки
PSR-1: Базовый стандарт оформления кода
PSR-2: Рекомендации по оформлению кода
PSR-3: Интерфейс протоколирования
PSR-4: Улучшенная автозагрузка
Изначально PSR был создан на базе материалов, полученных от лиц, сопровождающих наиболее узнаваемые на рынке платформы. Свой вклад внесли Zend, Drupal, Symfony, Joomla и прочие, и теперь они придерживаются этих стандартов. И даже PEAR, который много лет назад делал попытку стать стандартом, теперь является частью PSR.
В некоторым смысле практически неважно, какой стандарт вы используете, если вы принимаете его «условия игры» и придерживаетесь его. Однако, лучше всего придерживаться именно PSR, если у вас нет каких-то веских причин этого не делать. Все больше и больше команд и проектов соответствуют требованиям PSR. На данный момент большая часть разработчиков PHP абсолютно точно признала его Стандартом, поэтому, используя его, вы можете быть уверены, что новым разработчикам, которые будут присоединяться к вашей команде, этот стандарт будет знаком, и они будут чувствовать себя комфортно.
Ошибка №10. Неправильное использование метода empty()
Некоторые разработчики PHP очень любят использовать метод empty() для логических проверок практически всего. Однако бывают моменты, когда это может привести к путанице.
Для начала давайте вернемся к массивам и экземплярам ArrayObject (которые имитируют массивы). Учитывая их сходство, можно предположить, что массивы и экземпляры ArrayObject будут вести себя одинаково. Но это далеко не так. Например, вот что происходит в PHP 5.0:
// PHP 5.0 or later: { // PHP 5.0 и более поздние версии: }
$array = [];
var_dump(empty($array)); // outputs bool(true) { // выводит bool(true) }
$array = new ArrayObject();
var_dump(empty($array)); // outputs bool(false) { // выводит bool(false) }
// why don't these both produce the same output?
{ // почему мы получаем разный результат? }
А знаете, что еще хуже? Для более ранних версий PHP (до 5.0) результаты были бы другими:
// Prior to PHP 5.0: { // версии, предшествующие PHP 5.0: }
$array = [];
var_dump(empty($array)); // outputs bool(false) { // выводит bool(false) }
$array = new ArrayObject();
var_dump(empty($array)); // outputs bool(false) { // выводит bool(false) }
Как ни печально, но такой подход довольно популярен. Например, как предполагает документация, Zend\Db\TableGateway из Zend Framework 2 именно таким образом возвращает данные при вызове current() для результата TableGateway::select(). Разработчик может легко допустить ошибку, используя такие данные.
Для того, чтобы избежать таких проблем, лучше всего для проверки пустых массивов использовать функцию count():
// Note that this work in ALL versions of PHP (both pre and post 5.0):
{ // Обратите внимание, что это работает во ВСЕХ версиях PHP (и тех, что были до 5.0, и тех, что были после 5.0) }
$array = [];
var_dump(count($array)); // outputs int(0) { // выводит int(0) }
$array = new ArrayObject();
var_dump(count($array)); // outputs int(0) { // выводит int(0) }
И к слову, так как PHP преобразует 0 в false, функцию count() можно использовать для проверки пустых массивов в условной конструкции if (). Также стоит отметить, что эта функция имеет постоянную сложность (то есть O(1)), что еще больше доказывает тот факт, что это правильный выбор.
Есть еще один пример, когда метод empty() может быть опасной. Речь идет о ее совместном использовании с функцией класса Magic – функцией __get(). Давайте определим два класса и свойство test в каждом из них.
Для начала давайте определим класс Regular, у которого свойство test - это обычное свойство:
class Regular
{
public $test = 'value';
}
А теперь давайте определим класс Magic, который для доступа к свойству test использует оператор __get():
class Magic
{
private $values = ['test' => 'value'];
public function __get($key)
{
if (isset($this->values[$key])) {
return $this->values[$key];
}
}
}
Окей, а теперь давайте посмотрим, что будет происходить, когда мы попытаемся получить доступ к свойству test каждого из этих классов:
$regular = new Regular();
var_dump($regular->test); // outputs string(4) "value" { // выводит string(4) “value” }
$magic = new Magic();
var_dump($magic->test); // outputs string(4) "value" { // выводит string(4) “value” }
Пока все хорошо.
А теперь давайте посмотрим, что будет происходить, когда вы вызовем метод empty() для каждого из эти классов:
var_dump(empty($regular->test)); // outputs bool(false) { // выводит bool(false) }
var_dump(empty($magic->test)); // outputs bool(true) { // выводит bool(true) }
Ах ты! Так что, если мы прибегаем к помощи метода empty(), то мы можем прийти в неверным выводам, полагая, что свойство test ($magic) пусто, хотя на самом деле у него есть значение - 'value'.
Увы, но если класс для получения значения свойства использует функцию __get(), то нет какого-то беспроигрышного способа проверить, является ли свойство пустым или нет. Если вы находитесь вне области видимости класса, то вы сможете лишь проверить, вернется ли нулевое значение (null), и это не обязательно значит, что соответствующее свойство не существует, ему просто могли присвоить значение null.
Для сравнения, если мы попытаемся обратиться к несуществующему свойству экземпляра класса Regular, то вы получим вот такое уведомление:
Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10
Call Stack:
0.0012 234704 1. {main}() /path/to/test.php:0
Так что, главное, что нужно запомнить, что метод empty() стоит использовать аккуратно, так как в противном случае вы можете получить довольно запутанные (или даже неправильные) результаты.
Заключение
Простота использования PHP может усыпить бдительность разработчиков, внушив им ложное чувство комфорта. Однако из-за некоторых нюансов и особенностей языка разработчики могут попасть в ловушку под названием «утомительная отладка». В результате, PHP-код может просто не заработать или могут возникнуть проблемы, о которых мы говорили в этой статье.
PHP неплохо эволюционировал за свою 20-летнюю историю. Так что, вам определенно стоит ознакомиться с его тонкостями. Вы сможете создавать более масштабируемое, более надежное программное обеспечение, которое будет удобно в сопровождении.
С помощью этого руководства вы научитесь писать модульные тесты для функций Python. Но для чего вообще нужно уделять такое внимание модульным тестам?
Что ж, когда вы работаете над большими проектами, вам часто приходится обновлять какие-то модули и выполнять перепроектирование кода по мере необходимости. Однако такие изменения могут негативно влиять на другие модули, в которых используется этот обновленный модуль. В некоторых случаях это может нарушить существующую функциональность программы.
Как разработчик, вы обязаны протестировать свой программный код с целью убедиться, что все модули в приложении работают должным образом. Модульные тесты позволяют проверять, корректно ли работают маленькие блоки программного кода, и исправлять несоответствия, которые возникают в результате обновлений и перепроектирования.
Это руководство поможет вам научиться обращаться с модульными тестами в Python. Здесь вы узнаете, как использовать встроенный в Python модуль
unittest
для настройки и запуска модульных тестов, а также для написания тестовых примеров для проверки функций Python. Также вы узнаете, как тестировать функции, которые вызывают исключения.
Давайте начнем!
Тестирование в Python – первый этап
Мы начнем с того, что определим функцию Python и напишем модульный тест, чтобы проверить, работает ли она должным образом. Для того, чтобы перейти к настройке модульных тестов, рассмотрим простую функцию
is_prime()
, которая принимает на вход число и проверяет его на простоту.
import math
def is_prime(num):
'''Check if num is prime or not.'''
for i in range(2,int(math.sqrt(num))+1):
if num%i==0:
return False
return True
Давайте запустим Python REPL, вызовем функцию
is_prime()
, передав ей аргументы, и проверим результаты.
>>> from prime_number import is_prime
>>> is_prime(3)
True
>>> is_prime(5)
True
>>> is_prime(12)
False
>>> is_prime(8)
False
>>> assert is_prime(7) == True
Для того, чтобы убедиться, что
is_prime(),
возвращает ожидаемое логическое значение, вы также можете использовать конструкцию
assert
(см. выше). Если значение, которое вернула функция, отличается от ожидаемого логического значения, то возникнет ошибка
AssertionError
.
Такое ручное тестирование будет не самым эффективным вариантом, если вы захотите проверить свою функцию на гораздо большем количестве аргументов. У вас есть возможность настроить автоматическое тестирование, которое будет запускать функцию и проверять выходные данные на соответствие тестовым примерам, которые были определены в тестовом наборе.
Как работать с модулем Python
unittest
В Python втроен модуль
unittest
, с помощью которого можно настраивать автоматические тесты для функций и классов в вашем приложении. Общая процедура для настройки модульных тестов в Python выглядит следующим образом:
# .py
import unittest
from import
# all entries within <> are placeholders
class TestClass(unittest.TestCase):
def test_(self):
# check function_to_test
def test_(self):
# check function_to_test
:
:
:
def test_(self):
# check function_to_test
Фрагмент кода
.py
, который приведен выше, выполняет следующее:
Импортирует встроенный в Python модуль
unittest
.
Импортирует функцию Python
, которую необходимо протестировать, из модуля
, в котором она определена.
Создает тестовый класс (
TestClass
), который наследуется от класса
unittest.TestCase
.
Все тесты, которые должны быть запущены, должны быть определены в качестве методов внутри тестового класса.
?
Примечание
: для того, чтобы
unittest
определял эти методы как тесты и запускал их, названия этих методов должны начинаться с
test_
.
Класс
TestCase
из модуля
unittest
предоставляет полезные методы с утверждениями для проверки того факта, что тестируемая функция возвращает ожидаемые значения.
Ниже перечислены самые распространенные методы с утверждениями, и некоторые из них мы будем использовать в этом руководстве.
Метод
Описание
assertEqual(expected_value,actual_value)
Утверждает, что
expected_value == actual_value
assertTrue(result)
Утверждает, что
bool(result)
это
True
assertFalse(result)
Утверждает, что
bool(result)
это
False
assertRaises(exception, function, *args, **kwargs)
Утверждает, что
function(*args, **kwargs)
вызывает
exception
Для того, чтобы запустить эти тесты, мы должны запустить
unittest
в качестве основного модуля с помощью следующей команды:
$ python -m unittest .py
Для запуска
unittest
в качестве основного модуля мы можем добавить условие
if __name__=='__main__'
.
if __name__=='__main__':
unittest.main()
Добавив if, у нас появится возможность запускать тесты, просто запуская модуль Python, который содержит эти тесты.
$ python .py
Как определять тестовые примеры для функций Python
В этом разделе мы напишем модульные тесты для функции
is_prime()
с помощью уже изученного синтаксиса.
Для того, чтобы протестировать функцию
is_prime()
, которая возвращает логическое значение, мы можем воспользоваться методами
assertTrue()
и
assertFalse()
. Мы определяем четыре метода тестирования в классе
TestPrime
, который наследуется от
unittest.TestCase
.
import unittest
# import the is_prime function
from prime_number import is_prime
class TestPrime(unittest.TestCase):
def test_two(self):
self.assertTrue(is_prime(2))
def test_five(self):
self.assertTrue(is_prime(5))
def test_nine(self):
self.assertFalse(is_prime(9))
def test_eleven(self):
self.assertTrue(is_prime(11))
if __name__=='__main__':
unittest.main()
$ python test_prime.py
Ниже вывод в виде точки ( . ) говорит нам об успешно выполненном тестировании.
Output
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
В приведенном выше коде есть четыре метода тестирования. Каждый из этих методов проверяет какие-то определенные входные данные. Вместо этого вы можете определить один метод тестирования, который проверял бы результат для всех четырех входных данных.
import unittest
from prime_number import is_prime
class TestPrime(unittest.TestCase):
def test_prime_not_prime(self):
self.assertTrue(is_prime(2))
self.assertTrue(is_prime(5))
self.assertFalse(is_prime(9))
self.assertTrue(is_prime(11))
После того, как мы запустили модуль
test_prime,
мы видим, что успешно было выполнено только одно тестирование. Если какой-либо из методов с утверждением выдает
AssertionError
, то это значит, что тест завершился неудачно.
$ python test_prime.py
Output
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Как писать модульные тесты для проверки на исключения
В предыдущем разделе мы протестировали функцию
is_prime()
, передавая в качестве входных данных как простые, так и непростые числа. Если более конкретно, то все входные данные были положительными числами.
Мы еще не установили тот факт, что аргументы в вызове функции
is_prime()
должны быть целыми положительными числами. Вы можете использовать подсказки при вводе кода для того, чтобы обеспечить соблюдение типов или выкидывать исключения для недопустимых входных данных.
При тестировании функции
is_prime()
мы не учли следующее:
Если аргумент будет числом с плавающей точкой, то функция
is_prime()
также будет работать и возвращать значение
True
или
False
, что является ошибочным.
Если аргумент будет совершенно другого типа (например, строкой «five» вместо числа 5), то функция выдаст ошибку
TypeError
.
Если аргумент будет отрицательным числом, то функция
math.sqrt()
выдаст ошибку
ValueError
, поскольку квадраты всех действительных чисел (положительных, отрицательных или нуля) всегда неотрицательны.
Давайте проверим то, что мы рассмотрели выше, запустив несколько циклов Python REPL.
>>> from prime_number import is_prime
>>> is_prime('five')
Traceback (most recent call last):
File "", line 1, in
File "/home/bala/unit-test-1/prime_number.py", line 5, in is_prime
for i in range(2,int(math.sqrt(num))+1):
TypeError: must be real number, not str
>>> is_prime(-10)
Traceback (most recent call last):
File "", line 1, in
File "/home/bala/unit-test-1/prime_number.py", line 5, in is_prime
for i in range(2,int(math.sqrt(num))+1):
ValueError: math domain error
>>> is_prime(2.5)
True
Как вызвать исключения для недопустимых входных данных
Для того, чтобы устранить все недочеты, которые мы обсудили выше, мы проверим значение
num
, которое используется в вызове функции, и выбросим исключение, если это будет нужно.
Проверяем, является ли
num
целым числом. Если является, то переходим к следующей проверке. В противном случае выбрасываем исключение
TypeError
.
Проверяем, является ли
num
отрицательным целым числом. Если является, то выбрасываем исключение
ValueError
.
Переопределяем функцию, чтобы она также проверяла значение и выбрасывала по мере необходимости исключения. И получаем:
import math
def is_prime(num):
'''Check if num is prime or not.'''
# raise TypeError for invalid input type
if type(num) != int:
raise TypeError('num is of invalid type')
# raise ValueError for invalid input value
if num < 0:
raise ValueError('Check the value of num; is num a non-negative integer?')
# for valid input, proceed to check if num is prime
for i in range(2,int(math.sqrt(num))+1):
if num%i==0:
return False
return True
Теперь, когда мы изменили функцию так, чтобы мы могли выбрасывать исключения
ValueError
и
TypeError
для недопустимых входных данных, следующий шаг - проверка того, выпадает ли исключение.
Как пользовать методом
assertRaises()
для проверки наличия исключений
Давайте добавим в определение
TestPrime
методы для проверки того, выпадают ли исключения.
Мы определим методы
test_typeerror_1()
и
test_typeerror_2()
для проверки исключения
TypeError
, и метод
test_valueerror()
для проверки исключения
ValueError
.
?
Для вызова метода
assertRaises()
мы можем использовать следующий общий синтаксис:
def test_exception(self):
self.assertRaises(exception-name,function-name,args)
Мы также можем применить и другой синтаксис, используя менеджер контекста (в этом примере мы будем использовать именно его):
def test_exception(self):
with self.assertRaises(exception-name):
function-name(args)
Добавив эти методы тестирования, получим:
import unittest
from prime_number import is_prime
class TestPrime(unittest.TestCase):
def test_prime_not_prime(self):
self.assertTrue(is_prime(2))
self.assertTrue(is_prime(5))
self.assertFalse(is_prime(9))
self.assertTrue(is_prime(11))
def test_typeerror_1(self):
with self.assertRaises(TypeError):
is_prime(6.5)
def test_typeerror_2(self):
with self.assertRaises(TypeError):
is_prime('five')
def test_valueerror(self):
with self.assertRaises(ValueError):
is_prime(-4)
if __name__=='__main__':
unittest.main()
Давайте запустим модуль
test_prime
и посмотрим на результат:
$ python test_prime.py
Output
....
----------------------------------------------------------------------
Ran 4 tests in 0.002s
OK
Во всех примерах, которые мы успели написать до этого момента, все тесты выполнялись успешно. Давайте изменим один из методов, допустим,
test_typeerror_2()
:
def test_typeerror_2(self):
with self.assertRaises(TypeError):
is_prime(5)
Мы вызываем функцию
is_prime()
и передаем ей в качестве аргумента число 5. В данном случае 5 является допустимым вводом, для которого функция возвращает
True
. Поэтому функция не вызывает исключение
TypeError
. А когда мы снова запустим тесты, то увидим, что один тест завершился неудачно.
$ python test_prime.py
Output
..F.
======================================================================
FAIL: test_typeerror_2 (__main__.TestPrime)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_prime.py", line 17, in test_typeerror_2
is_prime(5)
AssertionError: TypeError not raised
----------------------------------------------------------------------
Ran 4 tests in 0.003s
FAILED (failures=1)
Заключение
Благодарю, что дочитали до конца!
?
Надеюсь, что это руководство помогло вам понять азы модульного тестирования в Python.
Вы научились настраивать тестирования, которые позволяют проверять, работает ли функция должным образом или выкидывает исключение – и все это с помощью встроенного в Python модуля
unittest
.
