Verification: a143cc29221c9be0

Php add one item to array

Php add one item to array

Содержание

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



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

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


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

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



Изначально идея позаимствована в языка-брата 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';

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


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


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

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



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 не происходит неявного преобразования.

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



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


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 будет сгенерирован.


Вступление

Одна из наиболее распространенных особенностей процедурных языков программирования - это концепция массива. Массивы кажутся простыми вещами, но есть много вопросов, на которые необходимо ответить при их добавлении в язык, например:

  • фиксированного или переменного размера?
  • является ли размер частью типа?
  • как выглядят многомерные массивы?
  • имеет ли смысл пустой массив?

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

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

В этой статье мы попытаемся устранить путаницу. Мы это сделаем, собрав некоторые части вместе, чтобы объяснить, как работает встроенная функция append и почему она работает именно так.

Массивы

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

Массивы нечасто встречаются в программах Go, потому что размер массива является частью его типа, что ограничивает его выразительные возможности.

Выражение

var buffer [256]byte

объявит переменную buffer, которая будет содержать массив из 256 байт. Тип переменной buffer включает размер [256]byte. Массив из 512 байт имел бы отличный тип [512]byte.

Данные, связанные с массивом — это просто массив элементов. Схематично наш буфер в памяти выглядит так

buffer: byte byte byte ... 256 раз ... byte byte byte

То есть переменная содержит 256 байт данных и ничего больше. Мы можем получить доступ к его элементам с помощью знакомого синтаксиса индексации: buffer[0], buffer[1] и т.д. до buffer[255]. (Диапазон индекса от 0 до 255 охватывает 256 элементов.) Попытка индексировать буфер со значением вне этого диапазона приведет к сбою программы.

Существует встроенная функция len, которая возвращает количество элементов массива или среза, а также, для нескольких других типов данных. Для массивов очевидно, что возвращает len. В нашем примере len(buffer) возвращает фиксированное значение 256.

У массивов есть свое место — например, они являются хорошим представлением матрицы преобразования, но их наиболее распространенная цель в Go - хранить память для среза.

Срезы: заголовок среза

Срезы — это то место, где происходит действие, но, чтобы правильно их использовать, нужно точно понимать, что они из себя представляют и что они делают.

Срез — это структура данных, описывающая непрерывный раздел массива, хранящийся отдельно от самой переменной среза. Срез — это не массив. Срез описывает часть массива.

Учитывая нашу переменную буферного массива из предыдущего раздела, мы могли бы создать срез, который описывает элементы от 100 до 150 (точнее, от 100 до 149 включительно), срезав массив:

var slice []byte = buffer[100:150]

В этом фрагменте мы использовали полное объявление переменной для наглядности. Переменная slice имеет тип []byte, произносится как «срез байт», и инициализируется из массива, называемого буфером, путем разделения элементов с 100 (включительно) до 150 (исключая). Более идиоматический синтаксис отбрасывает тип, который задается инициализирующим выражением:

var slice = buffer[100:150]

Внутри функции мы могли бы использовать короткую форму объявления

slice := buffer[100:150]

Что именно представляет собой эта переменная среза? Мы имеем не совсем полную картину, но пока представьте себе срез как небольшую структуру данных с двумя элементами: длиной и указателем на элемент массива. Вы можете думать об этом как о чем то созданном за кулисами:

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

slice := sliceHeader{
    Length:        50,
    ZerothElement: &buffer[100],
}

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

До сих пор мы использовали операцию среза в массиве, но мы также можем срезать срез, например:

slice2 := slice[5:10]

Как и раньше, эта операция создает новый срез, в данном случае с элементами с 5 по 9 (включительно) исходного среза, что означает элементы со 105 по 109 исходного массива. Базовая структура sliceHeader для переменной slice2 выглядит так:

slice2 := sliceHeader{
    Length:        5,
    ZerothElement: &buffer[105],
}

Обратите внимание, что этот заголовок по-прежнему указывает на тот же базовый массив, хранящийся в переменной buffer.

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

slice = slice[5:10]

структура sliceHeader для переменной slice выглядит так же, как и для переменной slice2. Вы увидите, что повторное использование срезов часто используется, например, для уменьшения среза. Этот оператор удаляет первый и последний элементы нашего среза:

slice = slice[1:len(slice)-1]

Упражнение: запишите, как будет выглядеть структура sliceHeader после этого присвоения.

Вы часто будете слышать, как опытные программисты Go говорят о «заголовке среза», потому что это действительно то, что хранится в переменной среза. Например, когда вы вызываете функцию, которая принимает срез в качестве аргумента, такую как bytes.IndexRune, именно этот заголовок передается в функцию. В этом вызове

slashPos := bytes.IndexRune(slice, '/')

аргумент среза, который передается в функцию IndexRune, на самом деле является «заголовком среза».

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

Передача заголовков в функции

Важно понимать, что даже если срез содержит указатель, он сам является значением. “Под капотом” - это структурное значение, содержащее указатель и длину. Это не указатель на структуру.

Это важно.

Когда мы вызывали IndexRune в предыдущем примере, ему была передана копия заголовка среза. Такое поведение имеет важные следствия.

Рассмотрите эту функцию:

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}

Она делает именно то, что выходит из ее названия, перебирает индексы среза (используя цикл for range), увеличивая его элементы.

Попробуйте запустить:

func main() {
    slice := buffer[10:20]
    for i := 0; i 

Результат

before [0 1 2 3 4 5 6 7 8 9]
after [1 2 3 4 5 6 7 8 9 10]

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

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

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[0 : len(slice)-1]
    return slice
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    newSlice := SubtractOneFromLength(slice)
    fmt.Println("After:  len(slice) =", len(slice))
    fmt.Println("After:  len(newSlice) =", len(newSlice))
}

Результат

Before: len(slice) = 50
After:  len(slice) = 50
After:  len(newSlice) = 49

Здесь мы видим, что содержимое аргумента среза может быть изменено функцией, но его заголовок — нет. Длина, хранящаяся в переменной среза, не изменяется при вызове функции, так как функции передается копия заголовка среза, а не оригинал. Таким образом, если мы хотим написать функцию, изменяющую заголовок, мы должны вернуть его как параметр результата, как мы это сделали здесь. Переменная среза не изменяется, но возвращаемое значение имеет новую длину, которая затем сохраняется в newSlice

Указатели на срезы: приёмники

Другой способ заставить функцию изменить заголовок среза — передать указатель на него. Вот вариант нашего предыдущего примера, который делает это:

func PtrSubtractOneFromLength(slicePtr *[]byte) {
    slice := *slicePtr
    *slicePtr = slice[0 : len(slice)-1]
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    PtrSubtractOneFromLength(&slice)
    fmt.Println("After:  len(slice) =", len(slice))
}

Результат

Before: len(slice) = 50
After:  len(slice) = 49

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

Допустим, мы хотели иметь метод для среза, который обрезает его до последней косой черты. Мы могли бы написать это так:

type path []byte

func (p *path) TruncateAtFinalSlash() {
    i := bytes.LastIndex(*p, []byte("/"))
    if i >= 0 {
        *p = (*p)[0:i]
    }
}

func main() {
    pathName := path("/usr/bin/tso") // Приведение string в path.
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName)
}

Результат

/usr/bin

Если вы запустите этот пример, вы увидите, что он работает правильно, обновляя срез в вызывающей программе.

Упражнение: измените тип приемника на значение, а не указатель, и запустите его снова. Объясните, что происходит.

С другой стороны, если бы мы хотели написать метод для пути, который меняет регистр ASCII букв в пути на верхний (ограниченно игнорируя неанглийские имена), метод мог бы быть значением, потому что приемник-значение по-прежнему будет указывать на тот же базовый массив.

type path []byte

func (p path) ToUpper() {
    for i, b := range p {
        if 'a' 

Результат

/USR/BIN/TSO

Здесь метод ToUpper использует две переменные в конструкции for range для захвата индекса и элемента среза. Эта форма цикла позволяет избежать многократной записи p[i] в теле.

Упражнение: преобразуйте метод ToUpper для использования приемника-указателя и посмотрите, изменится ли его поведение.

Продвинутое упражнение: преобразование метода ToUpper для обработки букв Unicode, а не только ASCII.

Емкость

Посмотрите на следующую функцию, которая расширяет свой аргумент (срез целых чисел) на один элемент:

func Extend(slice []int, element int) []int {
    n := len(slice)
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

(Зачем нужно возвращать измененный срез?) Теперь запустите его:

func main() {
    var iBuffer [10]int
    slice := iBuffer[0:0]
    for i := 0; i 

Результат

[0]
[0 1]
[0 1 2]
[0 1 2 3]
[0 1 2 3 4]
[0 1 2 3 4 5]
[0 1 2 3 4 5 6]
[0 1 2 3 4 5 6 7]
[0 1 2 3 4 5 6 7 8]
[0 1 2 3 4 5 6 7 8 9]
panic: runtime error: slice bounds out of range [:11] with capacity 10

goroutine 1 [running]:
main.Extend(...)
	/tmp/sandbox751785990/prog.go:16
main.main()
	/tmp/sandbox751785990/prog.go:25 +0x105

Program exited: status 2.

Посмотрите, как срез растет, пока … не перестает.

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

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

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

После создания нашего примера среза

slice := iBuffer[0:0]

его заголовок выглядит как

slice := sliceHeader{
    Length:        0,
    Capacity:      10,
    ZerothElement: &iBuffer[0],
}

Поле Capacity равно длине базового массива за вычетом индекса в массиве первого элемента среза (в данном случае — нуля). Если вы хотите узнать, какова емкость среза, используйте встроенную функцию cap:

if cap(slice) == len(slice) {
    fmt.Println("slice is full!")
}

Make

Что, если мы хотим вырастить срез сверх его емкости? Вы не можете! По определению, емкость — это предел роста. Но вы можете достичь эквивалентного результата, выделив новый массив, скопировав данные и изменив срез для описания нового массива.

Начнем с аллокации. Мы могли бы использовать встроенную функцию new, чтобы выделить больший массив, а затем нарезать результат, но вместо этого проще использовать встроенную функцию make. Она выделяет новый массив и создает заголовок среза для его описания сразу. Функция make принимает три аргумента: тип среза, его начальную длину и его емкость, которая представляет собой длину массива, который make выделяет для хранения данных среза. Этот вызов создает фрагмент длиной 10 с местом для еще 5 (15-10), как вы можете видеть, запустив его:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

Результат

len: 10, cap: 15

Этот фрагмент удваивает емкость нашего int среза, но сохраняет его длину прежней:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
    newSlice[i] = slice[i]
}
slice = newSlice
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

Результат

len: 10, cap: 15
len: 10, cap: 30

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

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

gophers := make([]Gopher, 10)

длина и емкость среза равны 10.

Copy

Когда мы удвоили емкость нашего среза в предыдущем разделе, мы написали цикл для копирования старых данных в новый срез. Go имеет встроенную функцию копирования, чтобы упростить эту задачу. Ее аргументы представляют собой два среза, и она копирует данные из правого аргумента в левый аргумент. Вот наш пример, переписанный для использования копирования:

newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)

Функция копирования умная. Она копирует только то, что может, обращая внимание на длину обоих аргументов. Другими словами, количество копируемых элементов равно минимальной длине двух срезов. Это может немного сэкономить написание кода. Кроме того, copy возвращает целочисленное значение — количество скопированных элементов, хотя это не всегда стоит проверять.

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

// Insert вставляет значение в срез по указанному индексу
// который должен быть в диапазоне
// Срез должен иметь место для нового элемента
func Insert(slice []int, index, value int) []int {
    // Увеличиваем срез на один элемент
    slice = slice[0 : len(slice)+1]
    // Используем copy чтобы сдвинуть верхнюю чать среза и освободить место
    copy(slice[index+1:], slice[index:])
    // Записываем значение
    slice[index] = value
    // Возвращаем результат
    return slice
}

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

slice[i:]

означает то же самое что и

slice[i:len(slice)]

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

slice[:]

просто означает сам срез, что полезно при разрезании массива. Это выражение — самый короткий способ сказать «срез, описывающий все элементы массива»:

array[:]

Теперь, когда это не мешает, давайте запустим нашу функцию Insert.

slice := make([]int, 10, 20) // capacity > length: место для нового эелемента
for i := range slice {
    slice[i] = i
}
fmt.Println(slice)
slice = Insert(slice, 5, 99)
fmt.Println(slice)

Результат

[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 99 5 6 7 8 9]

Append: пример

Несколькими разделами назад мы написали функцию Extend, которая расширяет срез на один элемент. Однако она имела недостаток — потому что, если емкость среза была бы слишком мала, то функция вышла бы из строя. (В нашем примере Insert имеет ту же проблему.) Теперь у нас есть все необходимое, чтобы исправить это, поэтому давайте напишем надежную реализацию Extend для целочисленных срезов.

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // Срез полный, необходимо увеличение
        // Мы удвоим его размер и добавим 1, так, чтобы если размер был бы 0 - мы по прежнему могли бы увеличить его
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

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

slice := make([]int, 0, 5)
for i := 0; i 

Результат

len=1 cap=5 slice=[0]
address of 0th element: 0xc0000b2030
len=2 cap=5 slice=[0 1]
address of 0th element: 0xc0000b2030
len=3 cap=5 slice=[0 1 2]
address of 0th element: 0xc0000b2030
len=4 cap=5 slice=[0 1 2 3]
address of 0th element: 0xc0000b2030
len=5 cap=5 slice=[0 1 2 3 4]
address of 0th element: 0xc0000b2030
len=6 cap=11 slice=[0 1 2 3 4 5]
address of 0th element: 0xc000094060
len=7 cap=11 slice=[0 1 2 3 4 5 6]
address of 0th element: 0xc000094060
len=8 cap=11 slice=[0 1 2 3 4 5 6 7]
address of 0th element: 0xc000094060
len=9 cap=11 slice=[0 1 2 3 4 5 6 7 8]
address of 0th element: 0xc000094060
len=10 cap=11 slice=[0 1 2 3 4 5 6 7 8 9]
address of 0th element: 0xc000094060

Обратите внимание на перераспределение при заполнении исходного массива размером 5. И емкость, и адрес нулевого элемента меняются при выделении нового массива.

С помощью надежной функции Extend в качестве руководства мы можем написать еще более удобную функцию, которая позволяет нам расширять фрагмент на несколько элементов. Для этого мы используем способность Go превращать список аргументов функции в срез при вызове функции. То есть мы используем возможность вариативной функции Go.

Назовем функцию Append. Для первой версии мы можем просто многократно вызывать Extend, чтобы был понятен механизм вариативной функции. Сигнатура Append такая:

func Append(slice []int, items ...int) []int

Это говорит о том, что Append принимает один аргумент, срез, за которым следует ноль или более аргументов типа int. Эти аргументы представляют собой в точности часть int в том, что касается реализации Append, как вы можете видеть:

// Append добавляет элементы в срез.
// Первая версия: просто итеративно вызываем Extend.
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}

Обратите внимание на цикл for range, перебирающий элементы аргумента items, который подразумевает type [] int. Также обратите внимание на использование пустого идентификатора _, чтобы отбросить индекс в цикле, который нам в данном случае не нужен.

Попробуйте запустить

slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)

Результат

[0 1 2 3 4]
[0 1 2 3 4 5 6 7 8]

Еще одна новая техника в этом примере заключается в том, что мы инициализируем срез, записывая составной литерал, который состоит из типа среза, за которым следуют его элементы в фигурных скобках:

slice := []int{0, 1, 2, 3, 4}

Функция Append интересна еще по одной причине. Мы можем не только добавлять элементы, но и целый второй срез, “разбивая” срез на аргументы, используя нотацию “…” в месте вызова:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // The '...' is essential!
fmt.Println(slice1)

Результат

[0 1 2 3 4]
[0 1 2 3 4 55 66 77]

Конечно, мы можем сделать Append более эффективным, выделяя память не более одного раза, опираясь на реализацию Extend:

// Append добавляет элементы в срез.
// Эффективная версия.
func Append(slice []int, elements ...int) []int {
    n := len(slice)
    total := len(slice) + len(elements)
    if total > cap(slice) {
        // Выделяем память. Увеличиваем в 1.5 раза новый размер так, что мы можем еще увеличивать
        newSize := total*3/2 + 1
        newSlice := make([]int, total, newSize)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[:total]
    copy(slice[n:], elements)
    return slice
}

Здесь обратите внимание на то, как мы используем копию дважды: один раз для перемещения данных среза во вновь выделенную память, а затем для копирования добавляемых элементов в конец старых данных.

Попробуй запустить; поведение такое же, как и раньше:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // The '...' is essential!
fmt.Println(slice1)

Результат

[0 1 2 3 4]
[0 1 2 3 4 55 66 77]

Append: встроенная функция

Итак, мы подошли к мотивации для разработки встроенной функции append. Она делает то же самое, что и наш пример Append, с эквивалентной эффективностью, но работает для любого типа среза.

A weakness of Go is that any generic-type operations must be provided by the run-time (примечание переводчика: предположительно, Роб Пайк хотел указать на одно из слабых мест в Go - отсутствие дженериков и связанные с этим расходы на память). Когда-нибудь это может измениться, но сейчас, чтобы упростить работу со срезами, Go предоставляет встроенную универсальную функцию добавления. Он работает так же, как наша версия среза int, но для любого типа среза.

Помните, что поскольку заголовок среза всегда обновляется вызовом append, вам необходимо сохранить возвращенный срез после вызова. Фактически, компилятор не позволит вам вызвать append без сохранения результата.

Вот несколько однострочников, смешанных с операторами вывода. Попробуйте их, отредактируйте и исследуйте:

// Создаем пару начальных срезов.
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2:", slice2)

// Добавляем элемент в срез.
slice = append(slice, 4)
fmt.Println("Add one item:", slice)

// Добавляем один срез к другому.
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)

// Делаем копию среза чисел.
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)

// Копируем срез в конец себя.
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)

Результат

Start slice:  [1 2 3]
Start slice2: [55 66 77]
Add one item: [1 2 3 4]
Add one slice: [1 2 3 4 55 66 77]
Copy a slice: [1 2 3 4 55 66 77]
Before append to self: [1 2 3 4 55 66 77]
After append to self: [1 2 3 4 55 66 77 1 2 3 4 55 66 77]

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

Есть еще много примеров добавления, копирования и других способов использования срезов на созданной сообществом вики-странице Slice Tricks.

Nil

Кстати, с нашими новооткрытыми знаниями мы можем увидеть, что представляет собой нулевой срез (nil slice). Естественно, это нулевое значение заголовка среза:

sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}

или просто

sliceHeader{}

Ключевой деталью является то, что указатель элемента тоже равен нулю. Срез, созданный посредством

array[0:0]

имеет нулевую длину (и, возможно, даже нулевую емкость), но его указатель не равен нулю, поэтому это не нулевой срез.

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

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

Strings

Теперь краткий раздел о строках в Go в контексте срезов.

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

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

Для начала мы можем проиндексировать их для доступа к отдельным байтам:

slash := "/usr/ken"[0] // возвращает значение байта '/'.

Мы можем срезать строку, чтобы получить подстроку:

usr := "/usr/ken"[0:4] // возваращает строку "/usr"

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

Мы также можем взять обычный срез байт и создать из него строку с помощью простого преобразования:

str := string(slice)

и идем в обратном направлении:

slice := []byte(usr)

Массив, лежащий в основе строки, скрыт от просмотра; нет другого способа получить доступ к его содержимому, кроме как через строку. Это означает, что когда мы выполняем любое из этих преобразований, должна быть сделана копия массива. Go, конечно, позаботится об этом, вам не нужно об этом беспокоиться. После любого из этих преобразований изменения в массиве, лежащем в основе байтового среза, не влияют на соответствующую строку.

Важным следствием подобной конструкции срезов строк является то, что создание подстроки очень эффективно. All that needs to happen is the creation of a two-word string header. Поскольку строка предназначена только для чтения, исходная строка и строка, полученная в результате операции среза, могут безопасно совместно использовать один и тот же массив.

Историческое примечание: самая ранняя реализация строк всегда подразумевала выделение памяти, но, когда в язык добавлялись срезы, они предоставили модель для эффективной обработки строк. В результате некоторые тесты показали значительное ускорение.

Конечно, строки — это гораздо больше, и отдельный пост в блоге освещает их более подробно.

Заключение

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

Как только вы поймете, как они работают, срезы станут не только простыми в использовании, но и мощными и выразительными, особенно с помощью встроенных функций copy и append.

Прочитать больше

В сети есть много чего найти о срезах в Go. Как упоминалось ранее, “Slice Tricks” Wiki page имеет много примеров. В статье блога Go Slices подробно описаны детали структуры памяти с четкими диаграммами. Статья Расса Кокса Go Data Structures включает обсуждение срезов вместе с некоторыми другими внутренними структурами данных Go.

Доступно гораздо больше материала, но лучший способ узнать о срезах — это использовать их.

PHP Internals


  • [RFC] First-class callable syntax


    В качестве альтернативы довольно сложному [RFC] Partial Function Application Никита предлагает более простое решение проблемы получения ссылки на любую функцию или метод.
    // Сейчас вот так
    $fn = Closure::fromCallable('strlen');
    $fn = Closure::fromCallable([$this, 'method']);
    $fn = Closure::fromCallable([Foo::class, 'method']);
    
    // Предлагается вот такое
    $fn = strlen(...);
    $fn = $this->method(...);
    $fn = Foo::method(...);
    

    И соответственно, такой синтаксис можно будет применять везде, где ожидается callable. Например, вот так:

    array_map(Something::toString(...), [1, 2, 3]);
    array_map(strval(...), [1, 2, 3]);
    
    // вместо
    array_map([Something::class, 'toString'], [1, 2, 3])
    array_map('strval', [1, 2, 3]);
    

  • [RFC] Disable autovivification on false


    Сейчас PHP позволяет инициализировать массив из переменной со значением null или false. Предлагается для false все-таки бросать Fatal error:
    $a = true;
    $a[] = 'value'; // Fatal error: Uncaught Error: Cannot use a scalar value as an array
    
    $a = null;
    $a[] = 'value'; // Ok
    
    $a = false;
    $a[] = 'value'; // Сейчас это работает, но предлагается задепрекейтить
    
    3v4l.org/UucOC
  • [RFC] Allow static properties in enums


    В PHP 8.1 будут енумы. Подробный разбор был videoна стриме PHP-дайджеста и в тексте на php.watch.

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

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

    enum Environment {
        case DEV;
        case STAGE;
        case PROD;
    
        private static Environment $currentEnvironment;
    
        /**
         * Read the current environment from a file on disk, once.
         * This will affect various parts of the application.
         */
        public static function current(): Environment {
            if (!isset(self::$currentEnvironment)) {
                $info = json_decode(file_get_contents(__DIR__ . '/../../config.json'), true);
                self::$currentEnvironment = match($info['env']) {
                    'dev' => self::DEV,
                    'stage' => self::STAGE,
                    'prod' => self::PROD,
                };
            }
            return self::$currentEnvironment;
        }
        // Other methods can also access self::$currentEnvironment
    }
    printf("Current environment is %s\n", Environment::current()->name);
    

    Предложение спорное. Пишите в комментариях, что думаете по этому поводу.

    Кстати, в релизе PhpStorm 2021.2 уже будет поддержка enum, а пощупать можно будет на этой неделе в выпуске 2021.2 EAP.

  • [PR] Поддержка HTTP Early Hint support


    По умолчанию, PHP поддерживает отправку только одного набора заголовков. Но статус коды HTTP 1xx могут потребовать отправки нескольких наборов хедеров. В частности, для использования 103, нужно сначала отправить заголовки Link, и затем, когда весь ответ будет готов, отправить обычные 200 OK.

    Сейчас такое можно сделать, но немного криво: заголовки 103 отправить, как обычно, через header(), а следующую порцию заголовков — вручную прям через echo.

    Никита предлагает добавить функцию для того, чтоб можно было отправлять несколько наборов заголовков. В пул-реквесте обсуждение API с участием команды Symfony и одним из авторов спецификаций HTTP.

  • check[RFC] Add IntlDatePatternGenerator


    Предложение принято. В PHP 8.1 будет класс IntlDatePatternGenerator для быстрого создания дат в локализированном формате. Подробнее в PHP Internals News #85 с автором RFC.
  • [RFC] Final class constants


    На голосовании.
  • В Internals обсуждается идея задепрекейтить багтрекер bugs.php.net


    Вместо него предлагается использовать issues на GitHub. У идеи есть как плюсы, так и минусы. Но как первый шаг, все баги документации теперь будут Гитхабе. Так что если вы нашли ошибку в мануале PHP, то можно просто создать issue в репозитории php/doc-en или php/doc-ru. Вот пример.

Инструменты


  • Doctrine ORM 2.9 — Большое обновление популярной ORM. Под капотом поддержка атрибутов PHP 8, типизированные свойства, и другое.
  • Flarum 1.0.0 — Релиз популярного движка для форума на PHP.
  • moneyphp/money 4.0 — Пакет для правильной работы с денежными значениями.
  • phpast.com — Просмотр дерева абстрактного синтаксиса PHP. Полезно при отладке инструментов на базе nikic/PHP-Parser. Код на гитхабе: ryangjchandler/phpast.com.
  • JBZoo/CI-Report-Converter — Всеядный конвертер отчетов для CI. Основное призвание утилиты — совместить самый разный результат линтеров с самыми разными CI (TeamCity, GitHub Actions, etc). Прислал smetdenis.
  • veewee/xml — Все для удобной работы с XML в одном пакете.

Symfony


  • Как эффективно использовать сервисы с тегами в Symfony.
  • Что нового будет в Symfony 5.3.
  • В Symfony 6 минимальная версия PHP будет 8.0 — Это позволило удалить очень много проверок и кода, который нужен был для совместимости с более ранними версиями.

Laravel


Статьи


Аудио/Видео