Функции в PHP 5.6 — что нового?

в 13:16, , рубрики: php, php 5.6, Веб-разработка, метки: ,

Функции в PHP 5.6 — что нового?

Слева направо: Rasmus, Build 5.4, Version 5.5, Relise 5.6

Сегодня хочу поделиться своим видением того, как будет выглядеть работа с функциями уже в ближайшем мажорном релизе PHP — 5.6. Для этого я изучил рабочие предложения и нашёл там немало вкусняшек:

  • Новый синтаксис для функций с переменным числом аргументов и радостный отход в историю мороки с func_get_args():
    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:
    deprecated function myOldFunction() { }
    

  • Вызов методов и доступ к свойствам создаваемого объекта:
    new foo()->xyz;
    new baz()->bar(); 
    

Большинство из приведенных предложений пока находятся на стадии обсуждения. Но среди них уже есть утверждённые и даже реализованные.

Также искушённого читателя ждёт эксклюзив: изучая чужие умные мысли, я и сам решился написать собственный RFC. Сейчас вы не увидите его в списке предложений, так как на данный момент он находится на самом начальном этапе — на рассылке internals@lists.php.net.

А начну обзор с RFC, который уже реализован и гарантированно попадает в релиз 5.6.

Синтаксис для функций с переменным числом аргументов

Реализовано в PHP 5.6, Принято 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();

Проблемы старого подхода и как их решить.

Во-первых, глядя на синтаксис функции public function query($query) невозможно понять, что это, собственно говоря, функция с переменным числом аргументов. Кажется, что это функция выполняется только с обычным запросом и не поддерживает дополнительных аргументов.

Во-вторых, так как 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. Используя новый синтаксис, мы решаем обе вышеперечисленные проблемы.

Возможности нового синтаксиса

  • function fn($arg, ...$args): собирает все переменные аргументы в массив $args
  • function fn($arg, &...$args): собирает по ссылке
  • function fn($arg, array ...$args): гарантирует, что все переменные аргументы будут массивами (возможно указать любой другой тип)

Преимущества

  • Наглядность: сразу ясно, что это функция с переменным числом аргументов, без необходимости читать документацию.
  • Нет надобности использовать array_slice() для получения переменных аргументов из func_get_args()
  • Можно передавать переменные аргументы по ссылке
  • Можно указать тип переменных аргументов
  • Контроль типа переменных аргументов через интерфейс или наследование

Переопределение списка аргументов — эксклюзив!

Обсуждается на 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 (по количеству и типу аргументов). Я уверен, что возможность изменять набор аргументов метода при наследовании — это очень полезно. Проблем с обратной совместимостью возникнуть не должно.

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

На обсуждении

Так как в PHP нет поддержки именованных параметров, довольно часто функции содержат много необязательных параметров:

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 с нулевым указателем на ссылку. Это не выглядит большой проблемой с точки зрения безопасности, но в любом случае не особо приятно.

Импорт функций из пространства имён

Принято 16 голосами против 4, Предложено включить в 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() более не ищется в глобальном пространстве имён.

Зачем нужен оператор use function, почему не просто use?

В 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, будут вызваны разные функции.

Исключения вместо фатальных ошибок

На обсуждении, Предложено для 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

Потенциальные проблемы

Совместимость с E_RECOVERABLE_ERROR

Сейчас можно игнорировать восстанавливаемые фатальные ошибки, используя кастомный обработчик ошибок. Заменив их на исключения, мы потеряем такую возможность, ломая, таким образом обратную совместимость.

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

Catch-all блоки в уже существующем коде

Так как EngineException расширяет Exception, он будет отлавливаться блоками catch с типом Exception. Это может привести к незапланированному отлову исключений. Решением этой проблемы могло бы быть введение BaseException, который был бы родителем Exception. От BaseException можно было бы наследовать только те исключения, которые нежелательно отлавливать. Такой подход используется в Python (BaseException) и Java (Throwable).

Добавление модификатора deprecated

На обсуждении

Данный 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

Вызов методов и доступ к свойствам создаваемого объекта

На обсуждении

Цель этого RFC — предоставить поддержку вызова методов и доступа к свойствам созданного объекта одной строкой. Мы можем использовать один из двух нижеприведенных синтаксисов.

Синтаксис 1 (без скобок)

  • new foo->bar()
  • new $foo()->bar
  • new $bar->y()->x

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()

Синтаксис 2 (со скобками)

  • (new foo())->bar()
  • (new $foo())->bar
  • (new $bar->y)->x
  • (new foo)[0]

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js