- PVSM.RU - https://www.pvsm.ru -

Слева направо: Rasmus [1], Build 5.4 [2], Version 5.5 [3], Relise 5.6 [4]
Сегодня хочу поделиться своим видением того, как будет выглядеть работа с функциями уже в ближайшем мажорном релизе PHP — 5.6. Для этого я изучил рабочие предложения [5] и нашёл там немало вкусняшек:
function fn($reqParam, $optParam = null, ...$params) { }
function create_query($where, $order_by, $join_type = '', $execute = false, $report_errors = false) { }
create_query('deleted=0', 'name', default, default, /*report_errors*/ true);
use function foobarbaz;
baz();
<?php
function call_method($obj) {
$obj->method();
}
call_method(null); // oops!
try {
call_method(null); // oops!
} catch (EngineException $e) {
echo "Cool Exception: {$e->getMessage()}n";
}
deprecated function myOldFunction() { }
new foo()->xyz;
new baz()->bar();
Большинство из приведенных предложений пока находятся на стадии обсуждения. Но среди них уже есть утверждённые и даже реализованные.
Также искушённого читателя ждёт эксклюзив: изучая чужие умные мысли, я и сам решился написать собственный RFC. Сейчас вы не увидите его в списке предложений, так как на данный момент он находится на самом начальном этапе — на рассылке [6] internals@lists.php.net [7].
А начну обзор с RFC, который уже реализован и гарантированно попадает в релиз 5.6.
Реализовано [8] в PHP 5.6, Принято [9] 36 голосами против 1
И сразу в бой: рассмотрим код, который показывает как переменный аргумент ...$params будет заполняться в зависимости от количества переданных аргументов:
function fn($reqParam, $optParam = null, ...$params) {
var_dump($reqParam, $optParam, $params);
}
fn(1); // 1, null, []
fn(1, 2); // 1, 2, []
fn(1, 2, 3); // 1, 2, [3]
fn(1, 2, 3, 4); // 1, 2, [3, 4]
fn(1, 2, 3, 4, 5); // 1, 2, [3, 4, 5]
$params будет пустым массивом, если число переданных аргументов меньше, чем число объявленных. Все последующие аргументы будут добавлены в массив $params (с сохранением порядка). Индексы в массиве $params заполняются от 0 и по возрастанию.
На данный момент функции с переменным числом аргументов реализуются при помощи функции func_get_args(). Следующий пример показывает, как сейчас реализуется функция с переменным числом аргументов для подготовки и выполнения запроса MySQL:
class MySQL implements DB {
protected $pdo;
public function query($query) {
$stmt = $this->pdo->prepare($query);
$stmt->execute(array_slice(func_get_args(), 1));
return $stmt;
}
// ...
}
$userData = $db->query('SELECT * FROM users WHERE id = ?', $userID)->fetch();
Во-вторых, так как func_get_args() возвращает все аргументы, переданные в функцию, то вам сначала необходимо удалить параметр $query используя array_slice(func_get_args(), 1).
Данное RFC предлагает решить эти проблемы добавлением специального синтаксиса для функций с переменным числом аргументов:
class MySQL implements DB {
public function query($query, ...$params) {
$stmt = $this->pdo->prepare($query);
$stmt->execute($params);
return $stmt;
}
// ...
}
$userData = $db->query('SELECT * FROM users WHERE id = ?', $userID)->fetch();
Синтаксис ...$params сигнализирует о том, что это функция с переменным числом аргументов, и что все аргументы после $query должны быть занесены в массив $params. Используя новый синтаксис, мы решаем обе вышеперечисленные проблемы.
Обсуждается на internals@lists.php.net
И так, на данные момент моё предложение уже существенно отличается от первоначального и сводится к следующему: предоставить возможность изменять список аргументов метода (их количество и/или тип) при наследовании классов:
class Figure
{
public function calcPerimeter(array $angles)
{
return array_sum($angles);
}
}
class Square extends Figure
{
public function calcPerimeter($angle)
{
return 4 * $angle;
}
}
class Rectangle extends Figure
{
public function calcPerimeter($height, $width)
{
return 2 * ($height + $width);
}
}
Теперь можно использовать обе реализации расчёта периметра:
$square = new Square();
var_dump($square->calcPerimeter(array(1, 1, 1, 1))); // 4
var_dump($square->calcPerimeter(1)); // 4
Большая наглядность и естественность второго вызова очевидна. Конечно, что-то подобное можно реализовать заморочившись с func_get_args(). Но предложенный подход даёт полную ясность того, что конкретно реализовывает метод, делает код более чистым, избавляя от каскада if-else (по количеству и типу аргументов). Я уверен, что возможность изменять набор аргументов метода при наследовании — это очень полезно. Проблем с обратной совместимостью возникнуть не должно.
На обсуждении [10]
Так как в PHP нет поддержки именованных параметров [11], довольно часто функции содержат много необязательных параметров:
function create_query($where, $order_by, $join_type='', $execute = false, $report_errors = true) {...}
Если мы всегда используем дефолтные значение, то проблем не возникает. Но что, если нам надо поменять только параметр $report_errors, а остальные оставить без изменений? Для этого нам придётся найти определение функции и скопировать все остальные дефолтные значения в вызов. А это уже довольно скучно, потенциально глючно и может поломаться, если часть дефолтных значений изменится.
Предложение заключается в том, чтобы разрешить опускать указание значений для дефолтных аргументов в вызове, сохраняя, таким образом, их значение по умолчанию:
create_query("deleted=0", "name", default, default, /*report_errors*/ true);
В коде выше, $join_type и $execute будут иметь дефолтное значение. Естественно, таким образом мы можем опускать только необязательные параметры. В случае обязательных параметров будет генерироваться такая же ошибка, как и сейчас, когда в вызов функции передано недостаточно аргументов.
Проблема возникнет с поддержкой внутренних функций, использующих ручной вызов ZEND_NUM_ARGS(). Автор предложения просмотрел все функции в стандартном дистрибутиве, однако расширения PECL, которые не используют using zend_parse_parameters или делают это странным образом с zval-type без их корректной инициализации или проверки выходных результатов, могут требовать фиксов. Другими словами, они могут поломаться, если передать default с нулевым указателем на ссылку. Это не выглядит большой проблемой с точки зрения безопасности, но в любом случае не особо приятно.
Принято [9] 16 голосами против 4, Предложено [12] включить в PHP 5.6
PHP предоставляет возможность импортировать пространства имён и типы (классы, интерфейсы, трейты) через оператор use. Тем не менее, для функций такой возможности нет. В результате, работа с функциями в пространстве имён выглядит довольно громоздко.
Функцию можно вызвать без указания полного имени, только если вызов находится в том же пространстве имён, что и сама функция:
namespace foobar {
function baz() {
return 'foo.bar.baz';
}
}
namespace foobar {
function qux() {
return baz();
}
}
namespace {
var_dump(foobarqux());
}
Можно избежать указания полного имени, если импортировать пространство имён, в котором определена функция. Однако его алиас всё равно должен быть указан при вызове функции:
namespace foobar {
function baz() {
return 'foo.bar.baz';
}
}
namespace {
use foobar as b;
var_dump(bbaz());
}
Невозможно импортировать функцию напрямую. Пока что PHP не поддерживает это.
Использование одного и того же оператора use и для пространства имён классов, и для функций, скорее всего, приведёт к конфликтам и оверхеду.
Вместо изобретения нового оператора, можно просто использовать комбинацию use и function:
namespace foobar {
function baz() {
return 'foo.bar.baz';
}
function qux() {
return baz();
}
}
namespace {
use function foobarbaz, foobarqux;
var_dump(baz());
var_dump(qux());
}
Причём, такой подход можно было бы использовать не только для функций, но и для констант:
namespace foobar {
const baz = 42;
}
namespace {
use const foobarbaz;
var_dump(baz);
}
Так же как и для классов, должна быть возможность делать алиасы для импортируемых функций и констант:
namespace {
use function foobar as foo_bar;
use const fooBAZ as FOO_BAZ;
var_dump(foo_bar());
var_dump(FOO_BAZ);
}
Действительно, вы можете импортировать пространство имён. Это не критично для классов и, подавно, для функций. Но есть один случай, когда импорт функций может ощутимо улучшить читаемость кода. Это будет очень полезно при использовании небольших библиотечек, которые представляют собой просто несколько функций. Можно было бы помещать их в пространство имён, например, по имени автора: igorwcompose(). Это бы помогло избежать конфликтов. И пользователь такой функции, которому нет дела до того как зовут её автора, мог бы просто писать compose(). Вместо этого пользователь вынужден изобретать новый бессмысленный алиас просто для того, чтобы использовать функцию.
По умолчанию PHP будет искать функции в локальном пространстве имён, а затем будет возвращаться в глобальное. Для функций, которые были импортированы оператором use, возврата в глобальное пространство имён быть недолжно.
namespace foobar {
function strlen($str) {
return 4;
}
}
namespace {
use function foobarstrlen;
use function foobarnon_existent;
var_dump(strlen('x'));
var_dump(non_existent());
}
Вызов strlen() теперь однозначен. non_existent() более не ищется в глобальном пространстве имён.
В PHP функции и классы хранятся в отдельных пространствах имён. Функция foobar и класс foobar могут сосуществовать, потому что из контекста можно понять, что мы используем (класс или функцию):
namespace foo {
function bar() {}
class bar {}
}
namespace {
foobar(); // function call
new foobar(); // class instantiation
foobar::baz(); // static method call on class
}
Если оператор use будет поддерживать импорт функций, то это повлечёт проблемы с обратной совместимостью.
Пример:
namespace {
function bar() {}
}
namespace foo {
function bar() {}
}
namespace {
use foobar;
bar();
}
Поведение изменилось после того, как поменялся use. В зависимости от версии PHP, будут вызваны разные функции.
На обсуждении [13], Предложено для PHP 5.6
Это RFC предлагает разрешить использование исключений вместо фатальных ошибок.
Для наглядности предложения, рассмотрим следующий кусок кода:
<?php
function call_method($obj) {
$obj->method();
}
call_method(null); // oops!
Сейчас этот код приведёт к фатальной ошибке:
Fatal error: Call to a member function method() on a non-object in /path/file.php on line 4
Данный RFC заменяет фатальную ошибку на EngineException. Если исключение не будет обработано, мы всё равно получим фатальную ошибку:
Fatal error: Uncaught exception 'EngineException' with message 'Call to a member function method() on a non-object' in /path/file.php:4
Stack trace:
#0 /path/file.php(7): call_method(NULL)
#1 {main}
thrown in /path/file.php on line 4
Конечно, его не проблема и обработать:
try {
call_method(null); // oops!
} catch (EngineException $e) {
echo "Exception: {$e->getMessage()}n";
}
// Exception: Call to a member function method() on a non-object
Сейчас можно игнорировать восстанавливаемые фатальные ошибки, используя кастомный обработчик ошибок. Заменив их на исключения, мы потеряем такую возможность, ломая, таким образом обратную совместимость.
Я никогда не видел применения этой возможности на практике (не считая грязных хаков). В большинстве случаев, кастомный обработчик событий выкидывает ErrorException, то есть эмилирует предложенное поведение, лишь с другим типом исключения.
Так как EngineException расширяет Exception, он будет отлавливаться блоками catch с типом Exception. Это может привести к незапланированному отлову исключений. Решением этой проблемы могло бы быть введение BaseException, который был бы родителем Exception. От BaseException можно было бы наследовать только те исключения, которые нежелательно отлавливать. Такой подход используется в Python (BaseException) и Java (Throwable).
На обсуждении [14]
Данный RFC предлагает добавить модификатор deprecated для методов и функций, которые присваивают функциям флаг ZEND_ACC_DEPRECATED, таким образом, при вызове выкидывая исключение E_DEPRECATED.
Зачем это нужно?
Помечать используемые функции и методы как устаревшие — это обычная практика для больших PHP фреймворков при релизе новых версий. Позже эти функции убираются окончательно. Нативные функции требуют только флага ZEND_ACC_DEPRECATED для того, чтобы Zend при их вызове автоматически сгенерировал ошибку E_DEPRECATED. Тем не менее, пользовательские функции могут быть отмечены как устаревшие только добавлением тега @deprecated (в комментарии документации) или генерацией ошибки E_USER_DEPRECATED. Но почему обычный разработчик должен генерировать ошибку вручную, в то время как нативная функция требует только флага?
Если помечать функцию устаревшей через простое добавление модификатора deprecated, это даст большую читабельность, быстроту и наглядность для разработчиков.
Ошибка E_USER_DEPRECATED может быть скорее использована для пометки как устаревшей целой библиотеки или способа вызова функций.
Так же, метод ReflectionFunction::isDeprecated() на данный момент бесполезен для пользовательских функций. И этот RFC решает указанную проблему.
deprecated function myFunction() {
// ...
}
myFunction();
Deprecated: Function myFunction() is deprecated in ... on line 5
class MyClass {
public deprecated static function myMethod() {
// ...
}
}
MyClass::myMethod();
Deprecated: Function MyClass::myMethod() is deprecated in ... on line 7
На обсуждении [15]
Цель этого RFC — предоставить поддержку вызова методов и доступа к свойствам созданного объекта одной строкой. Мы можем использовать один из двух нижеприведенных синтаксисов.
class foo {
public $x = 'testing';
public function bar() {
return "foo";
}
public function baz() {
return new self;
}
static function xyz() {
}
}
var_dump(new foo()->bar()); // string(3) "foo"
var_dump(new foo()->baz()->x); // string(7) "testing"
var_dump(new foo()->baz()->baz()->bar()); // string(3) "foo"
var_dump(new foo()->xyz()); // NULL
new foo()->www(); // Fatal error: Call to undefined method foo::www()
class foo {
public $x = 1;
}
class bar {
public $y = 'foo';
}
$x = 'bar';
$bar = new bar;
var_dump((new bar)->y); // foo
var_dump((new $x)->y); // foo
var_dump((new $bar->y)->x); // 1
Ну, вот и всё. Список изменений получился довольно внушительный, а ведь он затрагивается только работу с функциями! Лично мне планируемые нововведения очень нравятся, хочется их попробовать уже сейчас :)
Буду рад обсудить все эти новшества и, конечно же, пуститься в горячую дискуссию вокруг моего RFC. А, возможно, кто-нибудь ещё и свой предложит :)
До встречи в комментариях!
Автор: uaoleg
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/47882
Ссылки в тексте:
[1] Rasmus: http://ru.wikipedia.org/wiki/%D0%9B%D0%B5%D1%80%D0%B4%D0%BE%D1%80%D1%84,_%D0%A0%D0%B0%D1%81%D0%BC%D1%83%D1%81
[2] Build 5.4: http://php.net/releases/5_4_0.php
[3] Version 5.5: http://php.net/releases/5_5_0.php
[4] Relise 5.6: http://www.youtube.com/watch?v=OdPXskJBFl4
[5] рабочие предложения: https://wiki.php.net/rfc
[6] рассылке: http://php.net/mailing-lists.php
[7] internals@lists.php.net: mailto:internals@lists.php.net
[8] Реализовано: https://github.com/php/php-src/commit/0d7a6388663b76ebed6585ac92dfca5ef65fa7af
[9] Принято: https://wiki.php.net/rfc/variadics#vote
[10] На обсуждении: https://wiki.php.net/rfc/skipparams
[11] именованных параметров: http://en.wikipedia.org/wiki/Named_parameter
[12] Предложено: https://wiki.php.net/rfc/use_function#proposed_php_version_s
[13] На обсуждении: https://wiki.php.net/rfc/engine_exceptions
[14] На обсуждении: https://wiki.php.net/rfc/deprecated-modifier
[15] На обсуждении: https://wiki.php.net/rfc/instance-method-call
[16] Источник: http://habrahabr.ru/post/198980/
Нажмите здесь для печати.