Статьи

Spooky Scary PHP

Разбейте леденец и яблочный сидр; это снова время года! Остальной мир, возможно, не будет праздновать Хэллоуин так дико, как в Америке, но я подумал, что было бы интересно поделиться некоторыми страшными вещами PHP, чтобы отметить этот праздник. Это забавная статья, в которой рассказывается о каком-то пугающем (но логичном) поведении, обнаруженном в самом PHP, и о жутких (и, возможно, довольно нелогичных) способах, которыми некоторые извращают PHP для выполнения своих ставок. Думайте об этом как о моем удовольствии, немного офигенного «умопомрачительного леденца» — потому что почему все трюки или угощения должны иметь все вкусности?

Массив с привидениями

Однажды в магазине разработки, расположенном неподалеку, Артур поздно ночью встал, взламывая какой-то код. Мало ли он знал, что массив, который он собирался использовать, часто посещался! Он почувствовал, как по спине пробежал холодок, когда он печатал свои заявления, нажатие клавиши было нажатием клавиши, но глупо отмахнулся от своего тонкого предчувствия.

<?php $spell = array("double", "toil", "trouble", "cauldron", "bubble"); foreach ($spell as &$word) { $word = ucfirst($word); } foreach ($spell as $word) { echo $word . "n"; } 

Хорошо, так что массив на самом деле не дает покоя, но результат определенно был неожиданным:

  двойной
 маяться
 Беда
 котелок
 котелок 

Причина такого пугающего поведения заключается в том, что PHP сохраняет ссылку вне первого цикла foreach . $word прежнему была ссылкой, указывающей на последний элемент массива, когда начался второй цикл. Первая итерация второго цикла присвоила «double» $word , что перезаписало последний элемент. Вторая итерация присвоила «труду» $word , снова перезаписав последний элемент. К тому времени, когда цикл прочитал значение последнего элемента, он уже был растоптан несколько раз.

Для более подробного объяснения этого поведения, я рекомендую прочитать сообщение в блоге Йоханнеса Шлютера на тему « Ссылки и foreach» . Вы также можете запустить эту слегка измененную версию и проверить ее вывод для лучшего понимания того, что делает PHP:

 <?php $spell = array("double", "toil", "trouble", "cauldron", "bubble"); foreach ($spell as &$word) { $word = ucfirst($word); } var_dump($spell); foreach ($spell as $word) { echo join(" ", $spell) . "n"; } 

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

 <?php foreach ($spell as $key => $word) { $spell[$key] = ucfirst($word); } 

Соединение с фантомной базой данных

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

И вот Сьюзен писала задачу параллельной обработки, которая напоминала следующий код:

 #! /usr/bin/env php <?php $pids = array(); foreach (range(0, 4) as $i) { $pid = pcntl_fork(); if ($pid > 0) { echo "Fork child $pid.n"; // record PIDs in reverse lookup array $pids[$pid] = true; } else if ($pid == 0) { echo "Child " . posix_getpid() . " working...n"; sleep(5); exit; } } // wait for children to finish while (count($pids)) { $pid = pcntl_wait($status); echo "Child $pid finished.n"; unset($pids[$pid]); } echo "Tasks complete.n"; не #! /usr/bin/env php <?php $pids = array(); foreach (range(0, 4) as $i) { $pid = pcntl_fork(); if ($pid > 0) { echo "Fork child $pid.n"; // record PIDs in reverse lookup array $pids[$pid] = true; } else if ($pid == 0) { echo "Child " . posix_getpid() . " working...n"; sleep(5); exit; } } // wait for children to finish while (count($pids)) { $pid = pcntl_wait($status); echo "Child $pid finished.n"; unset($pids[$pid]); } echo "Tasks complete.n"; 

Ее код разветвлял дочерние процессы для выполнения некоторой длительной работы параллельно, в то время как родительский процесс продолжал наблюдать за дочерними процессами, сообщая о завершении всех из них.

  Вилка детская 1634. 
 Вилка детская 1635 г. 
 Вилка детская 1636. 
 Ребенок 1634 года работает ... 
 Вилка детская 1637. 
 Ребенок 1635 года работает ... 
 Ребенок 1636 года работает ... 
 Вилка детская 1638 года. 
 Ребенок 1637 года работает ... 
 Ребенок 1638 года работает ... 
 Ребенок 1637 закончил. 
 Ребенок 1636 закончил. 
 Ребенок 1638 года закончил. 
 Ребенок 1635 закончил. 
 Ребенок 1634 закончил. 
 Задачи выполнены. 

Вместо того чтобы выводить сообщения о состоянии на стандартный вывод, руководитель Сьюзен попросил ее записать время начала обработки и завершения всех детей. Сьюзен расширила свой код, используя механизм подключения к базе данных Singleton-ized PDO, который уже был частью кодовой базы компании.

 #! /usr/bin/env php <?php $db = Db::connection(); $db->query("UPDATE timings SET tstamp=NOW() WHERE name='start time'"); $pids = array(); foreach (range(0, 4) as $i) { ... } while (count($pids)) { ... } $db->query("UPDATE timings SET tstamp=NOW() WHERE name='stop time'"); class Db { protected static $db; public static function connection() { if (!isset(self::$db)) { self::$db = new PDO("mysql:host=localhost;dbname=test", "dbuser", "dbpass"); self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } return self::$db; } } 

Сьюзен ожидала увидеть строки в таблице таймингов обновленными; строка «время начала» должна была содержать метку времени, когда весь процесс был запущен, и «время остановки» должно быть, когда все закончилось. К сожалению, исполнение бросило исключение, и база данных не отражала ее ожидания.

  Неустранимая ошибка PHP: необработанное исключение «PDOException» с сообщением «SQLSTATE [HY000]: общая ошибка: сервер MySQL 2006 исчез» в /home/susanbrown/test.php:21 
 Трассировки стека: 
 # 0 /home/susanbrown/test.php(21): PDO-> запрос ('ОБНОВЛЕНИЕ таймеров S ...') 
 # 1 {main} 
  + ------------ + --------------------- + 
 |  имя |  tstamp | 
 + ------------ + --------------------- + 
 |  время начала |  2012-10-13 01:11:37 | 
 |  остановить время |  0000-00-00 00:00:00 | 
 + ------------ + --------------------- + 

Как и массив Артура, база данных Сьюзен стала часто посещаться? Что ж, посмотрим, сможете ли вы собрать головоломку, если я дам вам следующие подсказки:

  1. Когда процесс разветвляется, родительский процесс копируется как дочерний. Эти повторяющиеся процессы затем продолжают выполняться с этого момента рядом с сайтом.
  2. Статические члены совместно используются всеми экземплярами класса.

Соединение PDO было упаковано как Singleton, поэтому любые ссылки на него в приложении указывают на один и тот же ресурс в памяти. Сначала DB::connection() вернул ссылку на объект, родительский элемент разветвился, дочерние элементы продолжили обработку, пока родительский ожидал, дочерние процессы завершились, и PHP очистил использованные ресурсы, а затем родительский объект попытался снова использовать объект базы данных. Соединение с MySQL было закрыто в дочернем процессе, поэтому последний вызов не удался.

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

Я рекомендую избегать Singletons — они на самом деле не более, чем причудливые глобальные переменные OOP, которые могут затруднить отладку. И хотя в нашем примере соединение все равно будет закрыто дочерним процессом, по крайней мере DB::connection() вернет новое соединение, если вы вызываете его перед вторым запросом, если Singletons не использовались.

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

 #! /usr/bin/env php <?php $pids = array(); foreach (range(0, 4) as $i) { ... } $db = Db::connection(); $db->query("UPDATE timings SET tstamp=NOW() WHERE name='start time'"); while (count($pids)) { ... } $db->query("UPDATE timings SET tstamp=NOW() WHERE name='stop time'"); 

API, достойный доктора Франкенштейна

« Франкенштейн» Мэри Шелли — это история ученого, который создает жизнь, но настолько отталкивает ее уродство, что отказывается от нее. После некоторой безвозмездной смерти и разрушения доктор Франкенштейн преследует свое творение буквально до краев земли, стремясь к ее уничтожению. Многие из нас вдохнули жизнь в код, настолько отвратительный, что позже мы захотели бы просто убежать от него — код, такой безобразный, такой тупой, такой запутанный, что заставляет нас хотеть рвотного эффекта, но он хочет только любви и понимания.

Несколько лет назад я размышлял над идеей, ориентированной на интерфейсы баз данных и как они могли бы выглядеть, если бы они более близко придерживались философии Unix «все — файл»: запрос будет записан в «файл», набор результатов будет прочитан из «файла». Одно ведет к другому, и после некоторого собственного кодирования смерти и разрушения я написал следующий класс, который не имеет никакого отношения к моей первоначальной идее:

 <?php class DBQuery implements Iterator { protected $db; protected $query; protected $result; protected $index; protected $numRows; public function __construct($host, $dbname, $username, $password) { $this->db = new PDO("mysql:dbname=$dbname;host=$host", $username, $password); } public function __get($query) { $this->query = $query; $this->result = $this->db->query($query); return $this->numRows = $this->result->rowCount(); } public function __call($query, $values) { $this->query = $query; $this->result = $this->db->prepare($this->query); $this->result->execute($values); return $this->numRows = $this->result->rowCount(); } public function clear() { $this->index = 0; $this->numRows = 0; $this->query = ""; $this->result->closeCursor(); } public function rewind() { $this->index = 0; } public function current() { return $this->result->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_ABS, $this->index); } public function key() { return $this->index; } public function next() { $this->index++; } public function valid() { return ($this->index < $this->numRows); } public function __toString() { return $this->query; } } 

Результат был гениальным, но отталкивающим: экземпляр, который выглядел как объект (без реальных методов API), или массив, или строка…

 <?php $dbq = new DBQuery("localhost", "test", "dbuser", "dbpassword"); // query the database if the user is authorized // (instance behaves like an object) $username = "administrator"; $password = sha1("password"); if (!$dbq->{"SELECT * FROM admin_user WHERE username=? " . "AND password=?"}($username, $password)) { die("Unauthorized."); } // query the database and display some records // (instance is iterable like an array) $dbq->{"SELECT id, first_name, last_name FROM employee"}; foreach ($dbq as $result) { print_r($result); } // casting the object string yields the query echo "Query: $dbq"; 

Вскоре после этого я написал об этом в блоге и назвал это злом. Друзья и коллеги, которые видели в значительной степени, отреагировали тем же: «Блестящий! А теперь убей это … убей это огнем.

Но с тех пор я признаю, что смягчил это. Единственное правило, которое он действительно изгибает, — это ожидание программистом мягко названных методов, таких как query() и result() . Вместо этого он использует саму строку запроса в качестве метода запроса, объект — это интерфейс и набор результатов. Конечно, это не хуже, чем чрезмерно обобщенный интерфейс ORM с методами select() и where() объединенными в цепочку, чтобы выглядеть как SQL-запрос, но с большим количеством -> . Может, мой класс на самом деле не такой злой? Может быть, он просто хочет быть любимым? Я конечно не хочу умирать в Арктике!

В заключение

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

Изображение через Fotolia