PHP 8 — Что нового? +26


AliExpress RU&CIS

PHP, начиная с 7 версии, кардинально изменился. Код стал куда быстрее и надёжнее, и писать его стало намного приятнее. Но вот, уже релиз 8 версии! Ноябрь 26, 2020 — примерно на год раньше, чем обещали сами разработчики. И всё же, не смотря на это, мажорная версия получилась особенно удачной. В этой статье я попытаюсь выложить основные приятные изменения, которые мы должны знать.


1. JIT


Как говорят сами разработчики, они выжали максимум производительности в 7 версии (тем самым сделав PHP наиболее шустрым среди динамических ЯПов). Для дальнейшего ускорения, без JIT-компилятора не обойтись. Справедливости ради, стоит сказать, что для веб-приложений использование JIT не сильно улучшает скорость обработки запросов (в некоторых случаях скорость будет даже меньше, чем без него). А вот, где нужно выполнять много математических операций — там прирост скорости очень даже значительный. Например, теперь можно делать такие безумные вещи, как ИИ на PHP.
Включить JIT можно в настройках opcache в файле php.ini.
Подробнее 1 | Подробнее 2 | Подробнее 3


2. Аннотации/Атрибуты (Attributes)


Все мы помним, как раньше на Symfony код писался на языке комментариев. Очень радует, что такое теперь прекратится, и можно будет использовать подсказки любимой IDE, функция "Find usages", и даже рефакторинг!


Забавно, что символ # также можно было использовать для создания комментариев. Так что ничего не меняется в этом мире.

Было очень много споров о синтаксисе для атрибутов, но приняли Rust-like синтаксис:


#[ORM\Entity]
#[ORM\Table("user")]
class User
{
    #[ORM\Id, ORM\Column("integer"), ORM\GeneratedValue]
    private $id;

    #[ORM\Column("string", ORM\Column::UNIQUE)]
    #[Assert\Email(["message" => "The email '{{ value }}' is not a valid email."])]
    private $email;
}

Подробнее 1 | Атрибуты в Symfony


3. Именованые параметры (Named Arguments)


Если вы когда-либо видели код, где есть булевы параметры, то понимаете насколько он ужасен. Ещё хуже, когда этих параметров 8 штук, 6 из которых имеют значение по-умолчанию, а вам нужно изменить значение последнего параметра.


К примеру, код для использования библиотеки phpamqplib:


$channel->queue_declare($queue, false, true, false, false);
// ...
$channel->basic_consume($queue, '', false, false, false, false, [$this, 'consume']);

С использованием именованых параметров, код становится намного легче читать:


$channel->queue_declare($queue, durable: true, auto_delete: false);
// ...
$channel->basic_consume($queue, callback: [$this, 'consume']);

Ещё несколько примеров:


htmlspecialchars($string, default, default, false);
// vs
htmlspecialchars($string, double_encode: false);

Внимание! Можно также использовать ассоциативные массивы для именованых параметров (и наоборот).


$params = ['start_index' => 0, 'num' => 100, 'value' => 50];
$arr = array_fill(...$params);

function test(...$args) { var_dump($args); }

test(1, 2, 3, a: 'a', b: 'b');
// [1, 2, 3, "a" => "a", "b" => "b"]

Подробнее


4. Оператор безопасного null (Nullsafe operator)


Null — сам по себе не очень хорошая штука (даже очень плохая). Когда функция возвращает null, то в каждом месте, где идёт её вызов, программист обязан проверить на null. И это приводит к ужасным последствиям.


$session = Session::find(123);

if ($session !== null) {
    $user = $session->user;

    if ($user !== null) {
        $address = $user->getAddress();

        if ($address !== null) {
            $country = $address->country;
        }
    }
}

По хорошему, должен быть метод Session::findOrFail, который будет кидать исключение в случае отсутствия результата. Но когда эти методы диктует фреймворк, то мы не можем ничего сделать. Единственное, это проверять каждый раз на null либо, где это уместно, использовать ?->.


$country = $session?->user?->getAddress()?->country;

Этот код более чистый, чем предыдущий. Но он не идеален. Для идеально чистого кода, нужно использовать шаблон Null Object, либо выбрасывать exception. Тогда нам не нужно будет держать в голове возможность null на каждом шагу.


Более правильный вариант:


$country = $session->user->getAddress()->country;

Интересным моментом в использовании nullsafe есть то, что при вызове метода с помощью ?->, параметры будут обработаны только если объект не null:


function expensive_function() {
    var_dump('will not be executed');
}

$foo = null;
$foo?->bar(expensive_function()); // won't be called

5. Оператор выбора match (Match expression v2)


Для начала покажу код до и после:


$v = 1;
switch ($v) {
    case 0:
        $result = 'Foo';
        break;
    case 1:
        $result = 'Bar';
        break;
    case 2:
        $result = 'Baz';
        break;
}

echo $result; // Bar

VS


$v = 1;
echo match ($v) {
    0 => 'Foo',
    1 => 'Bar',
    2 => 'Baz',
};  // Bar

Как видим, это очень приятный оператор для выбора значений, который удобно заменяет switch.
Но есть очень важное отчилие switch от match: первый сравнивает нестрого ==, а во втором производится строгое === сравнение.


Наглядный пример различия:


switch ('foo') {
    case 0:
      $result = "Oh no!\n";
      break;
    case 'foo':
      $result = "This is what I expected\n";
      break;
}
echo $result; 
// Oh no!

VS


echo match ('foo') {
    0 => "Oh no!\n",
    'foo' => "This is what I expected\n",
}; 
// This is what I expected

В PHP8 этот пример со switch работает по другому, далее рассмотрим это.

Также, сравниваемыми значениями оператора match могут быть выражения. При этом, будут выполнены только те, пока не будет найден первый совпадающий вариант:


$result = match ($x) {
    foo() => ...,
    $this->bar() => ..., // bar() isn't called if foo() matched with $x
    $this->baz => ...,
    // etc.
};

6. Адекватное приведение строки в число (Saner string to number comparisons)


Проблема


$validValues = ["foo", "bar", "baz"];
$value = 0;
var_dump(in_array($value, $validValues));
// bool(true) ???

Это происходит потому, что при нестрогом == сравнении строки с числом, строка приводится к числу, то-есть, например (int)"foobar" даёт 0.


В PHP8, напротив, сравнивает строку и число как числа только если строка представляет собой число. Иначе, число будет конвертировано в строку, и будет производиться строковое сравнение.


Comparison Before After
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42foo" true false

Стоит отметить, что теперь выражение 0 == "" даёт false. Если у вас из базы пришло значение пустой строки и обрабатывалось как число 0, то теперь это не будет работать. Нужно вручную приводить типы.

Эти изменения относятся ко всем операциям, которые производят нестрогое сравнение:


  • Операторы <=>, ==, !=, >, >=, <, <=.
  • Функции in_array(), array_search(), array_keys() с параметром strict: false (то-есть по-умолчанию).
  • Сотрировочные функции sort(), rsort(), asort(), arsort(), array_multisort() с флагом sort_flags: SORT_REGULAR (то-есть по-умолчанию).

Также, есть специальные значения которые при нестрогом сравнении дают true:


Expression Before After
INF == "INF" false true
-INF == "-INF" false true
NAN == "NAN" false false
INF == "1e1000" true true
-INF == "-1e1000" true true

7. Constructor Property Promotion


Изначально идея позаимствована в языка-брата Hack. Она состоит в том, чтобы упростить инициализацию полей класса в конструкторе.


Вместо прописания полей класа, параметров конструктора, инициализации полей с помощью параметров, можно просто прописать поля параметрами конструктора:


class Point {
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0,
    ) {}
}

Это эквивалентно:


class Point {
    public float $x;
    public float $y;
    public float $z;

    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

С этим всё просто, так как это синтаксический сахар. Но интересный момент возникает при использовании вариативные параметры (их нельзя объявлять таким образом). Для них нужно по-старинке вручную прописать поля и установить их в конструкторе:


class Test extends FooBar {
    private array $integers;

    public function __construct(
        private int $promotedProp, 
        Bar $bar,
        int ...$integers,
    ) {
        parent::__construct($bar);
        $this->integers = $integers;
    }
}

8. Новые функции для работы со строками (str_contains, str_starts_with, str_ends_with)


Функция str_contains проверяет, содержит ли строка $haystack строку $needle:


str_contains("abc", "a"); // true
str_contains("abc", "d"); // false
str_contains("abc", "B"); // false 

// $needle is an empty string
str_contains("abc", "");  // true
str_contains("", "");     // true

Функция str_starts_with проверяет, начинается ли строка $haystack строкой $needle:


$str = "beginningMiddleEnd";
var_dump(str_starts_with($str, "beg")); // true
var_dump(str_starts_with($str, "Beg")); // false

Функция str_ends_with проверяет, кончается ли строка $haystack строкой $needle:


$str = "beginningMiddleEnd";
var_dump(str_ends_with($str, "End")); // true
var_dump(str_ends_with($str, "end")); // false

Вариантов mb_str_ends_with, mb_str_starts_with, mb_str_contains нету, так как эти функции уже хорошо работают с мутльтибайтовыми символами.


9. Использование ::class на объектах (Allow ::class on objects)


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


$object = new stdClass;
$className = get_class($object); // "stdClass"

Теперь же, можно использовать такую же нотацию, как и ClassName::class:


$object = new stdClass;
var_dump($object::class); // "stdClass"

10. Возвращаемый тип static (Static return type)


Тип static был добавлен для более явного указания, что используется позднее статическое связывание (Late Static Binding) при возвращении результата:


class Foo {
    public static function createFromWhatever(...$whatever): static {
        return new static(...$whatever);
    }
}

Также, для возвращения $this, стоит указывать static вместо self:


abstract class Bar {
    public function doWhatever(): static {
        // Do whatever.
        return $this;
    }
}

11. Weak Map


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


Интерфейс класса выглядит следующим образом:


WeakMap implements Countable , ArrayAccess , Iterator {
    public __construct ( )
    public count ( ) : int
    public current ( ) : mixed
    public key ( ) : object
    public next ( ) : void
    public offsetExists ( object $object ) : bool
    public offsetGet ( object $object ) : mixed
    public offsetSet ( object $object , mixed $value ) : void
    public offsetUnset ( object $object ) : void
    public rewind ( ) : void
    public valid ( ) : bool
}

Особенностью есть то, что объекты, используемые как ключи, подвержены сборке мусора. Поэтому, WeakMaps особенно пригодные для долгоживущих процессов.


class FooBar {
    private WeakMap $cache;

    public function getSomethingWithCaching(object $obj) {
        return $this->cache[$obj] ??= $this->decorated->getSomething($obj);
    }

    // ...
}

Подробнее можно почитать в документации.


12. Исключена возможность использовать левоассоциативный оператор (Deprecate left-associative ternary operator)


Рассмотрим код:


return $a == 1 ? 'one'
     : $a == 2 ? 'two'
     : $a == 3 ? 'three'
     : $a == 4 ? 'four'
              : 'other';

Вот как он всегда работал:


$a Result
1 'four'
2 'four'
3 'four'
4 'four'

В 7.4 код как прежде, отрабатывал, но выдавался Deprecated Warning.
Теперь же, в 8 версии, код упадёт с Fatal error.


13. Изменение приоритета оператора конкатенации (Change the precedence of the concatenation operator)


Раньше, приоритет оператора конкатенации . был на равне с + и -, поэтому они исполнялись поочерёдно слева направо, что приводило к ошибкам. Теперь же, его приоритет ниже:


Expression Before Currently
echo "sum: " . $a + $b; echo ("sum: " . $a) + $b; echo "sum :" . ($a + $b);

14. Возможность оставить запятую в конце списка параметров (Allow trailing comma in parameter list)


Это относится к методам:


public function whatever(
    string $s,
    float $f, // Allowed
) {
    // ...
}

Обычным функциям:


function whatever(
    string $s,
    float $f, // Allowed
) {
    // ...
}

Анонимным функциям:


$f = function(
    string $s,
    float $f, // Allowed
) {
    // ...
};

А также стрелочным функциям:


$f = fn(
    string $s,
    float $f, // Allowed
) => $s . $f;

15. Новый интерфейс Stringable


Объекты, которые реализуют метод __toString, неявно реализуют этот интерфейс. Сделано это в большей мере для гарантии типобезопасности. С приходом union-типов, можно писать string|Stringable, что буквально означает "строка" или "объект, который можно преобразовать в строку". В таком случае, объект будет преобразован в строку только когда уже не будет куда оттягивать.


interface Stringable
{
    public function __toString(): string;
}

Рассмотрим такой код:


class A{
    public function __toString(): string 
    {
        return 'hello';
    }
}

function acceptString(string $whatever) {
    var_dump($whatever);
}

acceptString(123.45); // string(6) "123.45"
acceptString(new A()); // string(5) "hello"

Здесь функция acceptString принимает строку, но что если нам нужно конкретно объект, что может быть преобразован в строку, а не что-либо иное. Вот тут нам поможет интерфейс Stringable:


function acceptString(Stringable $whatever) {
    var_dump($whatever);
    var_dump((string)$whatever);
}

// acceptString(123.45); 
/*
TypeError
*/

acceptString(new A()); 
/*
object(A)#1 (0) {
}
string(5) "hello"
*/

16. Теперь throw — это выражение


Примеры использования:


// This was previously not possible since arrow functions only accept a single expression while throw was a statement.
$callable = fn() => throw new Exception();

// $value is non-nullable.
$value = $nullableValue ?? throw new InvalidArgumentException();

// $value is truthy.
$value = $falsableValue ?: throw new InvalidArgumentException();

// $value is only set if the array is not empty.
$value = !empty($array)
    ? reset($array)
    : throw new InvalidArgumentException();

Подробнее можно почитать здесь.


17. Стабильная сортировка


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


Сюда входят sort, rsort, usort, asort, arsort, uasort, ksort, krsort, uksort, array_multisort, а также соответствующие методы в ArrayObject.


18. Возможньсть опустить переменную исключения (non-capturing catches)


Раньше, даже если переменная исключения не использовалась в блоке catch, её всё равно нужно быто объявлять (и IDE подсвечивала ошибку, что переменная нигде не используется):


try {
    changeImportantData();
} catch (PermissionException $ex) {
    echo "You don't have permission to do this";
}

Теперь же, можно опустить переменную, если никакая дополнительная информация не нужна:


try {
    changeImportantData();
} catch (PermissionException) { // The intention is clear: exception details are irrelevant
    echo "You don't have permission to do this";
}

19. Обеспечение правильной сигнатуры магических методов (Ensure correct signatures of magic methods):


Когда были добавлены type-hints в php, оставалась возможность непавильно написать сигнатуру для магических методов.
К примеру:


class Test {
    public function __isset(string $propertyName): float {
        return 123.45;
    }
}

$t = new Test();

var_dump(isset($t)); // true

Теперь же, всё жёстко контролируется, и допустить ошибку сложнее.


Foo::__call(string $name, array $arguments): mixed;

Foo::__callStatic(string $name, array $arguments): mixed;

Foo::__clone(): void;

Foo::__debugInfo(): ?array;

Foo::__get(string $name): mixed;

Foo::__invoke(mixed $arguments): mixed;

Foo::__isset(string $name): bool;

Foo::__serialize(): array;

Foo::__set(string $name, mixed $value): void;

Foo::__set_state(array $properties): object;

Foo::__sleep(): array;

Foo::__unserialize(array $data): void;

Foo::__unset(string $name): void;

Foo::__wakeup(): void;

20. Включить расширение json по-умолчанию (Always available JSON extension)


Так как функции для работы с json постоянно используются, и нужны чуть ли не в каждом приложении, то было принято решение включить ext-json в PHP по-умолчанию.


21. Более строгие проверки типов при для арифметических и побитовых операторов (Stricter type checks for arithmetic/bitwise operators)


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


var_dump([] % [42]);

Что должен вывести этот код? Здесь непредсказуемое поведение (будет 0). Всё потому, что большинство арифметических операторов не должны применятся на массивах.


Теперь, при использовании операторов +, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, -- будет вызывать исключение TypeError для операндов array, resource и object.


22. Валидация абстрактных методов в трейтах (Validation for abstract trait methods)


До восьмой версии, можно было писать что-то вроде:


trait T {
    abstract public function test(int $x);
}

class C {
    use T;

    // Allowed, but shouldn't be due to invalid type.
    public function test(string $x) {}
}

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


trait MyTrait {
    abstract private function neededByTheTrait(): string;

    public function doSomething() {
        return strlen($this->neededByTheTrait());
    }
}

class TraitUser {
    use MyTrait;

    // This is allowed:
    private function neededByTheTrait(): string { }

    // This is forbidden (incorrect return type)
    private function neededByTheTrait(): stdClass { }

    // This is forbidden (non-static changed to static)
    private static function neededByTheTrait(): string { }
}

Случаи, когда реализация приходит из родительского класса, или трейт применён в родительском классе, также проверяются.


23. Объединения типов (Union Types 2.0)


Рассмотрим код:


class Number {
    /**
     * @var int|float $number
     */
    private $number;

    /**
     * @param int|float $number
     */
    public function setNumber($number) {
        $this->number = $number;
    }

    /**
     * @return int|float
     */
    public function getNumber() {
        return $this->number;
    }
}

Здесь тип переменной $number контролируется только соглашениями программистов. На самом деле, туда может попасть что-угодно, и выйти отсюда может также что-угодно, так как проверки на тип не обеспечиваются ядром языка.


Теперь же, можно прописать тип int|float (или любой другой) явно, чтобы обеспечить корректность работы модуля:


class Number {
    private int|float $number;

    public function setNumber(int|float $number): void {
        $this->number = $number;
    }

    public function getNumber(): int|float {
        return $this->number;
    }
}

А также, код становится немного чище, так как мы можем избавится от излишних комментариев.


Типы-объединения имеют синтаксис T1|T2|... и могут быть использованы во всех местах, где можно прописать type-hints с некоторыми оговорками:


  • Тип void не может быть частью объединения.
  • Чтобы обозначить отсутствие результата, можно объявить "Nullable union type", который имеет следующий синтаксис: T1|T2|null.
  • Тип null не может быть использован вне объединения. Вместо него стоит использовать void.
  • Существует также псевдотип false, который по историческим причинам уже используется некоторыми функциями в php. С другой стороны, не существует тип true, так как он нигде не использовался ранее.

Типы полей класса инвариантны, и не могут быть изменены при наследовании.
А вот с методами всё немного интересней:


  1. Параметры методов можно расширить, но нельзя сузить.
  2. Возвращаемые типы можно сузить, но нельзя расширить.

Вот как это выглядит в коде:


class Test {
    public function param1(int $param) {}
    public function param2(int|float $param) {}

    public function return1(): int|float {}
    public function return2(): int {}
}

class Test2 extends Test {
    public function param1(int|float $param) {} // Allowed: Adding extra param type
    public function param2(int $param) {} // FORBIDDEN: Removing param type

    public function return1(): int {} // Allowed: Removing return type
    public function return2(): int|float {} // FORBIDDEN: Adding extra return type
}

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


class A {}
class B extends A {}

class Test {
    public function param1(B|string $param) {}
    public function param2(A|string $param) {}

    public function return1(): A|string {}
    public function return2(): B|string {}
}

class Test2 extends Test {
    public function param1(A|string $param) {} // Allowed: Widening union member B -> A
    public function param2(B|string $param) {} // FORBIDDEN: Restricting union member A -> B

    public function return1(): B|string {} // Allowed: Restricting union member A -> B
    public function return2(): A|string {} // FORBIDDEN: Widening union member B -> A
}

Интереснее становится когда strict_types установлен в 0, то-есть по-умолчанию. Например, функция принимает int|string, а мы передали ей bool. Что в результате должно быть в переменной? Пустая строка, или ноль? Есть набор правил, по которым будет производиться приведение типов.


Так, если переданный тип не является частью объединения, то действуют следующие приоритеты:


  1. int;
  2. float;
  3. string;
  4. bool;

Так вот, будет перебираться этот список с типами, и для каждого проверяться: Если тип существует в объединении, и значение может быть приведёно к нему в соответствии с семантикой PHP, то так и будет сделано. Иначе пробуем следующий тип.


Как исключение, если string должен быть приведён к int|float, то сравнение идёт в первую очередь в соответствии с семантикой "числовых строк". К примеру, "123" станет int(123), в то время как "123.0" станет float(123.0).


К типам null и false не происходит неявного преобразования.

Таблица неявного приведения типов:


Original type 1st try 2nd try 3rd try
bool int float string
int float string bool
float int string bool
string int/float bool
object string

Типы полей и ссылки


class Test {
    public int|string $x;
    public float|string $y;
}
$test = new Test;
$r = "foobar";
$test->x =& $r;
$test->y =& $r;

// Reference set: { $r, $test->x, $test->y }
// Types: { mixed, int|string, float|string }

$r = 42; // TypeError

Здесь проблема в том, что тип устанавливаемого значения не совместим с объявленными в полях класса. Для Test::$x — это могло быть int(42), а для Test::$yfloat(42.0). Так как эти значения не эквивалентны, то невозможно обеспечить единую ссылку, и TypeError будет сгенерирован.


24. Тип mixed (Mixed Type v2)


Был добавлен новый тип mixed.
Он эквивалентен типу array|bool|callable|int|float|object|resource|string|null.
Когда параметр объявлен без типа, то его тип — это mixed.
Но стоит отметить, что если функция не объявляет возвращаемого значения, то это не mixed, а mixed|void. Таким образом, если функция гарантировано должна что-то возвращать, но тип результата не известен заранее, то стоит написать его mixed.


При наследовании действуют следующие правила:


class A
{
    public function bar(): mixed {}
}

class B extends A
{
    // return type was narrowed from mixed to int, this is allowed
    public function bar(): int {}
}

class C
{
    public function bar(): int {}
}

class D extends C
{
    // return type cannot be widened from int to mixed
    // Fatal error thrown
    public function bar(): mixed {}
}

Подробнее можно почитать здесь


Где смотреть новые фичи


Более информации про новые функции в PHP можно посмотреть на rfc watch.


IMHO хорошие идеи для PHP


Неизменные (постоянные, надёжные) переменные

Иногда программисты сохраняют результат в одну переменную и при последующей обработке перезаписывают её.


$result = getThis();
$result = processThat($result, $whatever);
$result = formatForDisplay($result);
// ...

Я вижу как минимум 5 недостатков в таком подходе:


  1. Скорее всего переменная названа каким-то общим значением как $data или $result, что сильно ухудшает дальнейшее понимание кода.
  2. Никогда точно не знаешь что в переменной. Чтобы это узнать, нужно либо лезть в последнюю вызваную функцию и смотреть что она возвращает, либо использовать дебаггер, либо вывести результат на экран и посмотреть что же там. Это занимает много времени. Код должен быть очевидным. Почему читатели кода должны так много времени тратить на то, чтобы просто узнать что же храниться в переменной? Давайте не будем их заставлять это делать.
  3. Невозможность использовать уже перезаписанное значение где-то дальше в коде. И если уже возникла такая необходимость, то нужно переименовывать переменную начиная с какого-то места. А это чревато новыми ошибками.
  4. Неустойчивость к изменениям — если производиться копипаст большой части кода, в котором хотя-бы одно название переменной совпадает с внешним контекстом (например где-то во вложенном if), тогда ночь отладки обеспеченна.
  5. Каждый раз нужно писать знак $ для обозначения переменной. Да, это спорно, но ведь без долларов проще читать код. Возьмите какого-либо джависта, что он скажет про ваш код? — Уххх как много долларов!

С текущей реализацией PHP можно писать программы практически не используя переназначение переменной. Тем самым код стал бы куда лучше понятным, так как для каждой переменной нужно дать имя в соответствии с тем, что там храниться. Было бы хорошо на уровне языка дать возможность ограничить перезаписывание переменной.


Вот пример проблемного кода. Метод довльно длинный, а переменная $user используется во многих местах. Со временем значение может быть случайно перезаписано:


final class RegisterUser 
{
    public function __construct(private EntityManager $entityManager, ...) { } 

    public function __invoke(string $email, string $password): int {
        $user = new User();

        $user->email = $email;
        $user->password = $password;

        $this->entityManager->persist($user);
        // a lot of code 

        // someone copied a stump of code into here thereby owerwriting $user variable
        $user = $this->employeeRepository->findBySomething(...);

        // even more code

        return $user->id; // now wrong id is returned
    }
}

$registerUser = new RegisterUser(...);
$userId = $registerUser('admin@example.com', 'password');

Думаю проблема ясна. Кто-либо может скопировать часть кода, перезаписав критически важную переменную $user. А потом узнать это будет очень трудно. Кому-то придется много дебажить, чтобы найти проблемное место.


Вот как я вижу "fail fast" реализацию:


public function __invoke(string email, string password): int 
{
    user = new User();

    user->email = email;
    user->password = password;

    $this->entityManager->persist(user);
    // a lot of code

    // someone copied a stump of code into here
    // will throw an Error
    user = $this->employeeRepository->findBySomething(...);

    // even more code

    return user->id;
}

Теперь код более устойчив к изменениям. Случайная перезапись переменной исключена. Более того, мы теперь не нуждаемся в постоянных долларах для обозначения переменной. Согласитесь же, код стал намного лучше. И мы сразу видим что user — это "invariable" (неизменяемая переменная).


На счет иммутабельности, возможность записи в user->email может показаться странной, если переменная user неизменяемая. ИМХО, это даёт нам выбор, а значит — больше гибкости. Дело в том, что мы сами можем сделать наши классы иммутабельными, и контролировать это. То как это будет работать, полностью зависит от того как реализован класс, а не как мы объявили переменную. В любом случае, потом при необходимости можно будет добавить в язык фичу вычисляемых констант const user = findById(id), которые будут гарантировать иммутабельность даже объектов.

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


$a = ['zero','one','two', 'three'];

foreach ($a as &$v) { }

foreach ($a as $v) {
  echo "$v ";
}

Как вы (не) могли предположить, результатом будет zero one two two.
Добавив нормальную видимось для "immutable переменных", эта проблема решается сама собой. Как часто мы в теле цикла перезаписываем итерируемое значение? Лично я — никогда. Так почему же не использовать константную ссылку вместо ссылочной переменной?


function checkOutConstantLoopReferences(array arr) {
    result=[];

    foreach (arr as &immutableValueByReference) {
        result[] = immutableValueByReference['some']['very']['nested']['data']['structure'];
    }

    return result;
}

Перегрузка операторов

Ссылку на RFC можно найти вот тут.
Из 66 разработчиков, 28 проголосовали против, и поэтому, не смотря на то, что идея уже была реализована, предложение было отклонено. Думаю стоит поднять дискуссию на эту тему.


Почему я считаю это полезным?
Представим сложную систему для бронирования мест на самолёт. Мы реализуем доменную модель. Для предоставления денег, мы создали свой класс Money:


final class Money
{
    private string $currency;
    private int $valueInCoins;

    public function add(self $another): self
    {
       // ...
    }

    public function subtract(self $another): self
    {
       // ...
    }

    public function multiply(float $factor): self
    {
        // ...
    }

    public function divide(float $factor): self
    {
        // ...
    }

    public function equalsTo(self $other): bool
    {
        // ...
    }
}

Вот пример использования этого класса:


$money->add(Money::usd(100));

Внимание вопрос!
Метод add() добавляет деньги к текущему объекту, или возвращает новый объект с суммарной стоимостью? Клиентский код этого не может знать. Правда если назва метода не addGivenValueToThisAndReturnNew, но зачем так страшно называть метод? Название должно быть кратким и очевидным. Но к сожалению, в данном случае такого не существует. Никакое название не будет лучше передавать намерение, чем оператор +.


Вот этот код уже не вызывает сомнений:


$money + Money::usd(100);

Из этого кода сразу понятно что будет возвращено новое значение. Перегрузка операторов не приносит неясности. Наоборот, это даёт возможность более проясняет код.


Разрешить вычисления в константных выражениях для полей класса

Ссылку на RFC можно найти вот тут.
Как по мне, этого критически не хватает.
Вот пример кода, где это очень полезно:


use Spatie\ModelStates\State;

abstract class OrderStatus extends State
{
    public static string $name = static::getName();

    abstract protected function getName(): string;
}

При первом обращении к $name, будет вызван метод getName финального класса. Это дает нам возможность настраивать какие значения будут попадать в поля в зависимости от каких-либо условий. А в данном примере это использовано с шаблоном "Template Method", и финальные классы обязаны предоставить нам значение для поля.


Сейчас многие фреймворки имеют значения по-умолчанию для большинства конфигураций в своих классах. Проблема с таким подходом заключается в том, что мы можем предоставить примитивны. Никаких вызовов функций не разрешено. А что если мы хотим вызвать хелпер config для предоставления конфигурации, которая задаётся в поле класса? Тогда у нас возникнут трудности с этим. Нужно переопределять конструктор, где уже задавать значение поля. Хорошо, когда конструктор не имеет много параметров. А что, если там много параметров (к примеру 7)? Тогда для простого создания поля, нужно 20 дополнительных бесполезных строк кода. И выглядит это ещё как уродливо!


Просто сравните то как это сейчас:


    protected string $whatever;

    public function __construct(
        array $query = [],
        array $request = [],
        array $attributes = [],
        array $cookies = [],
        array $files = [],
        array $server = [],
        $content = null
    ) {
        parent::__construct(
            $query,
            $request,
            $attributes,
            $cookies,
            $files,
            $server,
            $content
        );

        $this->whatever = $this->whateverConfig();
    }

И то, как это могло быть:


    protected string $whatever = $this->whateverConfig();




Комментарии (31):

  1. gro
    /#22350224

    За список спасибо. Но комментарии местами оставляют лёгкое недоумние.

  2. Sabubu
    /#22350670 / +3

    Union-типы — ужасная ошибка. Вместо них должны быть Generic Types, а их лучше бы и не вводить. Это ненормально, когда функция принимает непонятно что и возвращает непонятно что. Неудобно работать с функцией, которая возвращает тип вроде string|A, так как перед любым действием надо проверять этот тип.


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


    Синтаксис атрибутов громоздкий и неудачный в сравнении с @attribute.


    Null-safe оператор выглядит коряво — целых три символа. throw в выражении тоже коряво, не лучше ли тут было использовать if вместо тернарного оператора?


    Куда-то PHP катится не туда. Что особенно печально, никто не отменяет старый синтаксис. И в итоге из языка, синтаксис которого можно изучить за пару дней, мы получаем язык с годами накопленными синтаксисами, где одну и ту же вещь можно сделать несколькими способами (match/switch), где есть куча оговорок и исключений. Тяжелое наследие Си тянется из года в годы.


    Пример с иммутабельными переменными выглядит неудачно. Его, наоборот тяжело читать, так как там теперь 3 переменных с длинными названиями вместо одной. А если нам надо сделать 5 преобразований, заводить 5 переменных? Плохой пример, скорее отталкивающий. Не надо слепо тащить фичи из других языков, если непонятно какая от них выгода. Даром не нужны эти иммутабельные переменные (тем более, что в том же JS в такой иммутабельной переменной может быть ссылка на мутабельный объект, что вообще идиотски выглядит. Если уж называете что-то константой — так следуйте этой идее до конца, иначе это не константа, а полуконстанта и должно обозначаться не как const а как halfconst. Впрочем, это Яваскрипт, что с него возьмешь).

    • m03r
      /#22351160

      Union-типы — ужасная ошибка. Вместо них должны быть Generic Types, а их лучше бы и не вводить. Это ненормально, когда функция принимает непонятно что и возвращает непонятно что.

      Union'ы — это как раз-таки способ превратить непонятно что в нечто гораздо более определённое и понятное. Между mixed, в котором может быть что угодно, и int|string — большая разница. Например, int|string точно можно конкатенировать с другой строкой. int|float — это вообще прекрасно — гарантирует корректную работу почти любой математики.


      Вместо них должны быть Generic Types

      Generic Types по факту можно сделать с помощью статического анализа, например, у Psalm'а очень неплохие возможности по этому поводу, Phan и phpstan, кажется, тоже умеют дженерики, и синтаксис у них, кстати, очень похожий. Может быть, в PHP 9 это просто сделают официальной частью языка, как с union и атрибутами, которые тоже выросли из аннотаций?


      Перегрузка операторов не нужна

      Тут я согласен с Вами и с сообществом, которое фичу не поддержало


      Null-safe

      Сахар он и есть сахар, равно как и объявление свойств в конструкторе, который выглядит для меня куда мене очевидным, чем ?->


      Пример с иммутабельными переменными выглядит неудачно.

      И здесь тоже согласен. Вот специфицировать тип переменной было бы полезно (по аналогии с типизацией свойств в 7.4)


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

    • rela589n
      /#22351908

      На счет Generic, то все бы хотели этого. Но, как говорит Никита Попов, внедри они Generics, код станет намного медленней работать, даже если он не использует этот функционал. Не знаю что там у них в исходном коде твориться, но это очень странно.


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

      Я добавил пример, посмотрите его пожалуйста.


      Пример с иммутабельными переменными выглядит неудачно

      Изменил пример. Надеюсь теперь более понятно что я хотел донести.


      Union-типы — ужасная ошибка

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


      С другой стороны, в будущем будет сделать что-то вроде SomeInterface1&SomeInterface2 (параметр должен реализовать перечисленные интерфейсы). Это хорошо, когда мы следуем принципу ISP. Тогда у нас появляется много интерфейсов, и мы могли бы явно указать в параметре что должен уметь делать объект.


      Синтаксис атрибутов громоздкий и неудачный в сравнении с @attribute.

      Да, мне тоже больше по душе джавовский синтаксис.

      • pronskiy
        /#22364910

        Не знаю что там у них в исходном коде твориться, но это очень странно.

        А что именно странно? Как еще проверки в рантайме можно сделать? Во многих других языках нет таких проверок, поэтому и не возникает проблема.

  3. Fragster
    /#22350968

    Вот пункт 12 меня прям разочаровал.
    Почему было не починить, чтобы код из примера работал нормально и логично (как в других языках, например, в js)?

    let $a = 2
    console.log($a == 1 ? 'one'
         : $a == 2 ? 'two'
         : $a == 3 ? 'three'
         : $a == 4 ? 'four'
                  : 'other') // two

    • iulanovy4
      /#22351794 / +2

      Потому что это вызовет самые разные баги в существующем коде, который полагается на старую логику. Сначала все переходят на 8-ю версию, легко находят проблемные места благодаря Fatal error и чинят. В 9-й уже можно будет делать правильную логику.

      • AnthonyMikh
        /#22368152

        Сначала все переходят на 8-ю версию, легко находят проблемные места благодаря Fatal error и чинят.

        Или не чинят, потому что не все пути исполнения покрыты тестами.

  4. SCINER
    /#22351152

    В 8й версии добавили слишком много неочевидностей.
    Их черезчур много, язык деградировал.
    Надо было наоборот всё делать строже, более читабельным и предсказуемым, а тут наоборот, неявностей ещё больше только добавили
    Теперь он точно обречён на забвение.

    • rela589n
      /#22352718 / +1

      Неочевидностей? Каких именно?

      • SCINER
        /#22355468

        Как минимум
        — определение свойств класса в конструкторе
        — union types
        — switch/match

        • Aco
          /#22355966

          IDE все неочевиности разрулит, как разурливает в js, golang и других

          • SCINER
            /#22356164

            1. в Go нет таких этих неочевидностей
            2. нет совершенных IDE, которые все разрулят и все подскажут абсолютно правильно

            • Aco
              /#22356336

              1. без ide или тулзов вы не узнаете какие интерфейсы реализовли и реализовали-ли вообще. не говорю что это плохо, но без явного указания имплементация интерфейсов неочивидна (вы явно это на структуре не видите)
              2. согласен, но в случае php8 изменения простые, легко подхватываемые ide

              • SCINER
                /#22356344

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

      • Ivan_Popov
        /#22358052

        + к уже написанному:

        • автоимплементация интерфейса Stringable
        • новые правила приведения чисел и строк
        • тернарные операторы
        • изменение приоритета оператора конкатенации

  5. rjhdby
    /#22353206 / +1

    Тоже, чтоль, написать статью о том, что в PHP8 появилось?.. А то все, кому не лень, уже написали, а я еще нет...

  6. maximw
    /#22354222

    Все здорово, кроме, сахара в конструкторе. Это заметно усложнит чтение кода. Если раньше достаточно было посмотреть начало класса (а обычно там пишут определение свойств), то теперь надо смотреть два места.

    • rjhdby
      /#22368096

      Как раз наоборот. Если раньше надо было смотреть и и в объявление и в конструктор, то теперь достаточно только конструктора.
      А если придерживаться практики "один параметр — одна строка", то вообще сказка просто.

      • maximw
        /#22369266

        А зачем надо смотреть в конструктор — там логика инициализации свойств объекта, это другое дело. Я говорю про просмотр самого набора свойств, а не про то как именно они инициализируются.

        Правило «один параметр — одна строка» звучит, конечно, хорошо, но на практике будет просто каша из того и другого. Потому что не всегда все свойства объекта передаются в конструктор, так что часть будет объявлено отдельно, часть в конструкторе.

        • rjhdby
          /#22369296

          Наговнокодить можно на любом языке с любым синтаксисом. Так-то никто не мешает использовать старый подход там, где он оправдан.

          • maximw
            /#22369312

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

            • rjhdby
              /#22369610

              а именно в объектах, все свойства которых передаются в конструктор

              Что является признаком хорошего дизайна.
              Сейчас вот протыркал 15 случайных классов, какие под руку попались и только в одном были какие-то дополнительные свойства, не выставляемые через конструктор. Но и тот оказался legacy помеченным под рефакторинг. (константы не в счёт)


              Да, иногда нужно внутреннее изменяемое состояние, но довольно редко.

  7. caballero
    /#22355686

    у них же выходной там. Кто будет сегодня релизить восмерку

  8. Wowa69
    /#22356084

    1) Из-за аттрибутов типа #[...] пришлось переписать много кода, где были закомментированы части конфигов на php с помощью #, например:

    $Actions=[
      ['Action1', 'Arg1', 'Arg2'],
     #['Action2', 'Arg1', 'Arg2'],
      ['Action3', 'Arg1', 'Arg2'],
    ];

    И такого кода было много.
    2) Очень не хватает многопоточности, хотя бы в cli

    • SerafimArts
      /#22361712

      1) Что мешало сделать: Ctrl+R + #[ -> //[?
      2) Хотите многопоточность — читайте мануал по ОС и используйте, благо доступ к сисколлам есть из коробки, начиная с 7.4

      • Wowa69
        /#22369178

        1) Когда это в одном файле на одном компьютере — это легко, а когда множество проектов, это не так быстро.
        2) Это всё будут костыли, которые не везде и не всегда будут работать.

  9. Arris
    /#22361456 / +1

    А чего принципиально нового в этой статье по сравнению с десятками подобных?

  10. amberovsky
    /#22363876

    ИМХО не стоило null в mixed включать. Столько проблем от него, безопаснее было бы его явно указывать mixed|null
    С другой стороны это сломает все текущие пхп-доки, где mixed подразумевает null. Надеюсь, в 9 версии что-нибудь придумают с этим.

  11. AleksandrB
    /#22368582 / -2

    Такое ощущение, что php возвращается обратно к говнокоду. Я не могу представить адекватные ситуации в которых понадобились бы смешанные типы. Уважаемые разработчики, вы что нибудь о единой ответственности слышали? Из-за ваших "?" в возвращаемых значениях теперь приходится каждый раз на is_null проверять, теперь вы сделали mixed, который практически эквивалентен «этот код писал индус, который может вернуть bool|sting|int или один из 10 000 объектов в вашем коде»
    Проходное обновление, на 8 версию точно не тянет. имхо.