Verification: a143cc29221c9be0

Php array sort array of objects

Содержание

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


type

В JSON схеме доступны следующие семь примитивных типов:

  • string — строковое значение.
  • null — значение null.
  • number — Любое число. Эквивалент float в PHP.
  • integer — Целое число. float не допускается.
  • boolean — значение true/false.
  • array — Список значений. Эквивалент массива в JavaScript. В PHP соответствует массиву с числами в ключах или массиву без указанных ключей.
  • object — Пары ключ => значение. Эквивалент объекта в JavaScript. В PHP соответствует ассоциативному массиву (массиву с ключами).

Примитивный тип указывается в type элементе массива. Например:

array(
	'type' => 'string',
);

JSON схема позволяет указать сразу несколько типов:

array(
	'type' => [ 'boolean', 'string' ],
);
меню

Преобразование типов

REST API WordPress принимает данные в GET или POST запросе, поэтому данные нужно преобразовать. Например некоторые строковые значения нужно превратить в их реальные типы.

  • string — этот тип должен пройти проверку is_string().
  • null — значение должно быть реальным null. Это означает, что отправка null значения в URL-адресе или в виде закодированного в форме URL тела сообщения невозможна, необходимо использовать тело запроса JSON.
  • number — число или строка, которая пройдет проверку is_numeric(). Значение будет преобразовано во (float).
  • integer — Целые числа или строки без дробной части. Значение будет преобразовано во (int).
  • boolean — Логические true/false. Числа или строки 0, 1, '0', '1', 'false', 'true'. 0 это false. 1 это true.
  • array — Индексный массив соответствующий wp_is_numeric_array() или строка. Если передана строка, то значения разделенные запятыми станут значениями элементов массива. Если запятых в строке нет, то значение станет первым элементом массива. Например: 'red, yellow' превратится в array( 'red', 'yellow' ), а 'blue' станет array( 'blue' ).
  • object — Массив, объект stdClass, объект применяемый JsonSerializable или пустая строка. Значения будут преобразованы в PHP массив.

При использовании нескольких типов, типы будут обрабатываться в указанном порядке. Это может повлиять на результат очистки. Например для 'type' => [ 'boolean', 'string' ] отправленное значение '1' превратиться в логическое true. Однако, если поменять порядок, то значение будет строка '1'.

Спецификация JSON схемы позволяет определять схемы без поля type. Но в WordPress этот параметр должен быть указан, иначе вы получите заметку _doing_it_wrong().

меню

string

Указывает на то что в значении запроса должна быть строка. Дополнительными параметрами ниже можно определить какая именно строка должна передаваться в значении параметра.

minLength / maxLength

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

Например для следующей схемы ab, abc, abcd пройдут проверку, а a и abcde нет.

array(
	'type' => 'string',
	'minLength' => 2,
	'maxLength' => 4,
);

format

Если указать этот аргумент, то значения параметра переданного в REST будут проверятся на соответствие указанному формату.

Возможные варианты параметра:

  • date-time — дата в формате RFC3339. см. rest_parse_date()
  • uri — uri в соответствии с esc_url_raw().
  • email — См. is_email().
  • ip — v4 или v6 ip адрес. См. rest_is_ip_address()
  • uuid — uuid код любой версии. См. wp_is_uuid()
  • hex-color — 3 или 6 символов hex цвета в префиксом #. См. rest_parse_hex_color().

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

array(
	'type'   => 'string',
	'format' => 'date-time',
);

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

Например, следующая схема разрешит указать IP (127.0.0.1) или не указывать значение параметра:

array(
	'type'   => [ 'string', 'null' ],
	'format' => 'ip',
);

Код из ядра, который показывает как конкретно работает этот параметр:

// This behavior matches rest_validate_value_from_schema().
if ( isset( $args['format'] )
	&& ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
) {
	switch ( $args['format'] ) {
		case 'hex-color':
			return (string) sanitize_hex_color( $value );

		case 'date-time':
			return sanitize_text_field( $value );

		case 'email':
			// sanitize_email() validates, which would be unexpected.
			return sanitize_text_field( $value );

		case 'uri':
			return esc_url_raw( $value );

		case 'ip':
			return sanitize_text_field( $value );

		case 'uuid':
			return sanitize_text_field( $value );
	}
}

Обратите внимание, что format обрабатывается не обязательно только тогда когда type = string. format будет применяться если:

  • type = string.
  • type отличается от стандартного примитивного типа.
  • type не указан (но в WP это запрещено).
меню

pattern

Используется для проверки соответствия значения строкового параметра указанному регулярному выражению.

Например, для следующей схемы, #123 подходит, а #abc нет:

array(
	'type'    => 'string',
	'pattern' => '#[0-9]+',
);

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

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

  • Разрешены отдельные символы Юникод соответствующие JSON спецификации [RFC4627].
  • Простая группа и диапазон символов: [abc] и [a-z].
  • Простая группа и диапазон исключающих символов: [^abc], [^a-z].
  • Просты квантификаторы: * (ноль или более), + (один или более), ? (один или ничего), и их «ленивые» версии: +?, *?, ??.
  • Квантификаторы диапазона: {x} (x раз), {x,y} (от x до y раз), {x,} (x и более раз), и их «ленивые» версии.
  • Анкоры начала и конца строки: ^, $.
  • Просты группы (...) и чередование |.

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

меню

null

Значение должно быть реальным null. Это означает, что отправка null значения в URL-адресе или в виде закодированного в форме URL тела сообщения невозможна, необходимо использовать тело запроса JSON.

boolean

Логические true/false. Можно запросе можно указать числа или строки:

  • true — 1, '1', 'true'.
  • false — 0, '0', 'false'.

Подробнее о том, как обрабатывается переданное значение смотрите в коде функции: rest_sanitize_boolean().

number / integer

Числа имеют тип number (любое число, может быть дробным) или integer (только целые числа):

if ( 'integer' === $args['type'] ) {
	return (int) $value;
}

if ( 'number' === $args['type'] ) {
	return (float) $value;
}

Для чисел также есть дополнительные параметры проверки.

minimum / maximum

Ограничивает диапазон допустимых чисел (включая сами числа). Например, 2 подойдет под схему ниже, а 0 и 4 - нет:

array(
	'type' => 'integer',
	'minimum' => 1,
	'maximum' => 3,
);

exclusiveMinimum / exclusiveMaximum

Это дополнительные параметры для minimum / maximum, которые отключают «включительную» проверку. Т.е. значение НЕ может равняться определенному минимуму или максимуму а должно быть больше или меньше, но не равно.

Например, в следующем случае приемлемым значением будет только 2:

array(
	'type'             => 'integer',
	'minimum'          => 1,
	'exclusiveMinimum' => true,
	'maximum'          => 3,
	'exclusiveMaximum' => true,
);

multipleOf

Утверждает кратность числа, т.е. полученное значение должно нацело делиться на указанное в этом параметре число.

Например, следующая схема будет принимать только четные целые числа:

array(
	'type'       => 'integer',
	'multipleOf' => 2,
);

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

array(
	'type'       => 'number',
	'minimum'    => 0,
	'maximum'    => 100,
	'multipleOf' => 0.1,
);

array

Указывает что должен быть передан массив. Смотрите: rest_sanitize_array().

items

Устанавливает формат каждого элемента в массиве. Для этого нужно использовать параметр items, в котором нужно указать JSON схему каждого элемента массива.

Например, следующая схема требует массив IP-адресов:

array(
	'type'  => 'array',
	'items' => array(
		'type'   => 'string',
		'format' => 'ip',
	),
);

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

[ "127.0.0.1", "255.255.255.255" ]

А эти нет:

[ "127.0.0.1", 5 ]

Схема items может быть любой, в том числе и схемой вложенного массива:

array(
	'type'  => 'array',
	'items' => array(
		'type'  => 'array',
		'items' => array(
			'type'   => 'string',
			'format' => 'hex-color',
		),
	),
);

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

[
  [ "#ff6d69", "#fecc50" ],
  [ "#0be7fb" ]
]

А эти не пройдут:

[
  [ "#ff6d69", "#fecc50" ],
  "george"
]

меню

minItems / maxItems

Используется для ограничения минимального и максимального количества элементов массива (включительно).

Например, следующая схема пропустит данные [ 'a' ] и [ 'a', 'b' ], но не пропустит
[] и [ 'a', 'b', 'c' ]:

array(
	'type'     => 'array',
	'minItems' => 1,
	'maxItems' => 2,
	'items'    => array(
		'type' => 'string',
	),
);

uniqueItems

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

Например, следующая схема посчитает правильными данные [ 'a', 'b' ], и не правильными [ 'a', 'a' ]:

array(
	'type'        => 'array',
	'uniqueItems' => true,
	'items'       => array(
		'type' => 'string',
	),
);
Важно знать об уникальности значений.
  • Значения разных типов рассматриваются как уникальные. Например, '1', 1 и 1.0 — это разные значения.

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

    [
      [ "a", "b" ],
      [ "b", "a" ]
    ]
  • При сравнении объектов, порядок определения свойств НЕ имеет значения. Например у такого массив объекты будут считаться одинаковыми:

    [
      {
    	"a": 1,
    	"b": 2
      },
      {
    	"b": 2,
    	"a": 1
      }
    ]
  • Уникальность проверяется рекурсивно для значений массивов в обеих функциях: rest_validate_value_from_schema() и rest_sanitize_value_from_schema(). Нужно это для того чтобы не было моментов, когда items могут быть уникальными до очистки и одинаковыми после.

    Возьмем для примера такую схему:

    array(
    	'type' => 'array',
    	'uniqueItems' => true,
    	'items' => array(
    		'type' => 'string',
    		'format' => 'uri',
    	),
    );

    Такой запрос прошёл бы проверку потому что строки разные:

    [ "https://site.com/hello world", "https://site.com/hello%20world" ]

    Однако после обработки функцией esc_url_raw() строки станут одинаковые.

    В этом случае rest_sanitize_value_from_schema() вернула бы ошибку. Поэтому вам всегда следует проверить и очищать параметры.

меню

object

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

См. rest_sanitize_object().

properties

Обязательные свойства объекта. Для каждого свойства указывается своя схема.

Например, следующая схема требует объект, где свойство name является строкой, а color шестнадцатеричным цветом.

array(
	'type'       => 'object',
	'properties' => array(
		'name'  => array(
			'type' => 'string',
		),
		'color' => array(
			'type'   => 'string',
			'format' => 'hex-color',
		),
	),
);

Такие данные пройдут проверку:

{
  "name": "Primary",
  "color": "#ff6d69"
}

А такие не пройдет:

{
  "name": "Primary",
  "color": "orange"
}
Обязательные свойства

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

Есть два способа сделать свойство обязательным.

Способ 1: добавить поле required для каждого свойства.

Это синтаксис 3 версии схемы JSON:

array(
	'type'       => 'object',
	'properties' => array(
		'name'  => array(
			'type'     => 'string',
			'required' => true,
		),
		'color' => array(
			'type'     => 'string',
			'format'   => 'hex-color',
			'required' => true,
		),
	),
);

Способ 2: добавить поле required в общий массив где перечистить обязательные свойства.

Это синтаксис 4 версии схемы JSON:

register_post_meta( 'post', 'fixed_in', array(
	'type'         => 'object',
	'show_in_rest' => array(
		'single' => true,
		'schema' => array(
			'required'   => array( 'revision', 'version' ),
			'type'       => 'object',
			'properties' => array(
				'revision' => array(
					'type' => 'integer',
				),
				'version'  => array(
					'type' => 'string',
				),
			),
		),
	),
) );

Теперь следующий запрос не пройдет проверку:

{
	"title": "Check required properties",
	"content": "We should check that required properties are provided",
	"meta": {
		"fixed_in": {
			"revision": 47089
		}
	}
}

Если мета-поле fixed_in вообще не указать, то никакой ошибки не возникнет. Объект, определяющий список обязательных свойств, не определяет сам объект обязательным. Просто если объект указан, то обязательные свойства также должны быть указаны.

Синтаксис 4 версии не поддерживается для схем эндпоинта верхнего уровня в WP_REST_Controller::get_item_schema().

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

array(
	'$schema'    => 'http://json-schema.org/draft-04/schema#',
	'title'      => 'my-endpoint',
	'type'       => 'object',
	'required'   => array( 'title', 'content' ),
	'properties' => array(
		'title'   => array(
			'type' => 'string',
		),
		'content' => array(
			'type' => 'string',
		),
	),
);

меню

additionalProperties

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

По умолчанию схема JSON позволяет указывать свойства, которые не указаны в схеме.

Таким образом, чтобы следующее не прошло проверку, нужно указать параметр additionalProperties = false, т.е. неописанные в схеме (дополнительные) свойства запрещены.

array(
	'type'                 => 'object',
	'additionalProperties' => false,
	'properties'           => array(
		'name'  => array(
			'type' => 'string',
		),
		'color' => array(
			'type'   => 'string',
			'format' => 'hex-color',
		),
	),
);

Ключевое слово additionalProperties само может быть использовано как схема свойств объекта. Так неизвестные свойства должны будут пройти указанную проверку.

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

array(
	'type'                 => 'object',
	'properties'           => array(),
	'additionalProperties' => array(
		'type'       => 'object',
		'properties' => array(
			'name'  => array(
				'type'     => 'string',
				'required' => true,
			),
			'color' => array(
				'type'     => 'string',
				'format'   => 'hex-color',
				'required' => true,
			),
		),
	),
);

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

{
  "primary": {
	"name": "Primary",
	"color": "#ff6d69"
  },
  "secondary": {
	"name": "Secondary",
	"color": "#fecc50"
  }
}

А вот эти не пройдут:

{
  "primary": {
	"name": "Primary",
	"color": "#ff6d69"
  },
  "secondary": "#fecc50"
}

меню

patternProperties

Ключевое слово patternProperties аналогично ключевому слову additionalProperties, но позволяет утверждать, что свойство соответствует шаблону регулярных выражений. Ключевое слово - это объект, где каждое свойство является шаблоном регулярных выражений, а его значение - JSON схемой, используемой для проверки свойств, соответствующих этому шаблону.

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

array(
  'type'                 => 'object',
  'patternProperties'    => array(
	'^\\w+$' => array(
	  'type'   => 'string',
	  'format' => 'hex-color',
	),
  ),
  'additionalProperties' => false,
);

В результате это пройдет проверку:

{
  "primary": "#ff6d69",
  "secondary": "#fecc50"
}

А это не пройдет:

{
  "primary": "blue",
  "$secondary": "#fecc50"
}

При проверке схемы patternProperties, если свойство не соответствует ни одному из шаблонов, это свойство будет разрешено и его содержимое не будет никак проверяться. Если такое поведение не подходит, то вам нужно запретить неизвестные (дополнительные) свойства с помощью параметра additionalProperties.

меню

minProperties / maxProperties

Используется для ограничения минимального и максимального количества свойств объекта (включительно). Это аналоги свойств minItems и maxItems у массива.

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

array(
  'type'                 => 'object',
  'additionalProperties' => array(
	'type'   => 'string',
	'format' => 'hex-color',
  ),
  'minProperties'        => 1,
  'maxProperties'        => 2,
);

Эти данные пройдут проверку:

{
  "primary": "#52accc",
  "secondary": "#096484"
}

А вот эти нет:

{
  "primary": "#52accc",
  "secondary": "#096484",
  "tertiary": "#07526c"
}
меню

enum

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

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

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

array(
	'description' => __( 'Order sort attribute ascending or descending.' ),
	'type'        => 'string',
	'default'     => 'asc',
	'enum'        => array(
		'asc',
		'desc',
	),
);

Код из ядра, показывает как именно делается проверка:

if ( ! in_array( $value, $args['enum'], true ) ) {
	return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
}
меню

oneOf / anyOf

Совпадение с одной или любой из описанных схем. Смотрите: rest_find_any_matching_schema(), rest_find_one_matching_schema().

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

  • anyOf разрешает, чтобы значение соответствовало одной из указанных схем или нескольким схемам.
  • oneOf требует, чтобы значение подходило только под одну схему (не под две и более).

Например, эта схема позволяет отправлять массив "операций" в конечную точку. Каждая операция может быть либо "crop", либо "rotation".

array(
	'type'  => 'array',
	'items' => array(
		'oneOf' => array(
			array(
				'title'      => 'Crop',
				'type'       => 'object',
				'properties' => array(
					'operation' => array(
						'type' => 'string',
						'enum' => array(
							'crop',
						),
					),
					'x'         => array(
						'type' => 'integer',
					),
					'y'         => array(
						'type' => 'integer',
					),
				),
			),
			array(
				'title'      => 'Rotation',
				'type'       => 'object',
				'properties' => array(
					'operation' => array(
						'type' => 'string',
						'enum' => array(
							'rotate',
						),
					),
					'degrees'   => array(
						'type'    => 'integer',
						'minimum' => 0,
						'maximum' => 360,
					),
				),
			),
		),
	),
);

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

operations[0] is not a valid Rotation. Reason: operations[0][degrees] must be between 0 (inclusive) and 360 (inclusive)

Чтобы генерировать понятные сообщения об ошибках, рекомендуется присвоить каждой схеме в массиве oneOf или anyOf свойство title.

меню

Цикл each (jQuery.each). Примеры использования

Синтаксис функции each:

// array или object - массив или объект, элементы или свойства которого необходимо перебрать
// callback - функция, которая будет выполнена для каждого элемента массива или свойства объекта
$.each(array или object,callback);

Работу с функцией each разберём на примерах.

Пример №1. В нём выполним переберор всех элементов массива (array).

// массив, состоящий из 3 строк
var arr = ['Автомобиль','Грузовик','Автобус'];

// переберём массив arr
$.each(arr,function(index,value){

  // действия, которые будут выполняться для каждого элемента массива
  // index - это текущий индекс элемента массива (число)
  // value - это значение текущего элемента массива
  
  //выведем индекс и значение массива в консоль
  console.log('Индекс: ' + index + '; Значение: ' + value);

});

/*
Результат (в консоли):
Индекс: 0; Значение: Автомобиль
Индекс: 1; Значение: Грузовик
Индекс: 2; Значение: Автобус
*/

В вышеприведённом коде функция each используется для перебора массива. Функция имеет 2 обязательных параметра. Первый параметр - это сущность (массив или объект), элементы (свойства) которой необходимо перебрать. В данном случае - это массив arr. Второй параметр - это функция обратного вызова, которая будет выполнена для каждого элемента (в данном случае) массива. Она имеет 2 параметра, которые доступны внутри неё посредством соответствующих переменных. Первый параметр - это порядковый номер элемента (отсчёт выполняется с 0). Второй параметр - это значение текущего элемента массива.

Пример №2. В этом примере осуществим перебор всех свойств объекта.

// объект smartphone, имеющий 5 свойств
var smartphone = {
  "name": "LG G5 se",
  "year": "2016",
  "screen-size": "5.3",
  "screen-resolution": "2560 x 1440",
  "os" : "Android 6.0 (Marshmallow)"
};

// переберём объект smartphone
$.each(smartphone, function( key, value ) {

  // действия, которые будут выполняться для каждого свойства объекта
  // key - текущее имя свойства массива
  // value - значение текущего свойства объекта
 
  // выведем имя свойства и его значение в консоль
  console.log( 'Свойство: ' +key + '; Значение: ' + value );

});

/*
Результат (в консоли):
Свойство: name; Значение: LG G5 se
Свойство: year; Значение: 2016
Свойство: screen-size; Значение: 5.3
Свойство: screen-resolution; Значение: 2560 x 1440
Свойство: os; Значение: Android 6.0 (Marshmallow)
*/

Функция each может использоваться для перебора JavaScript объектов. Отличие её использования заключается только в том, что параметры функции обратного вызова имеют другие значения. Первый параметр хранит название свойства объекта, а второй - значение этого свойства.

Пример №3. В нём осуществим перебор более сложной структуры (рассмотрим, как использовать вложенные each).

// объект, состоящий из 2 свойств. Каждое свойство этого объект имеет в качестве значения массив, элементами которого являются тоже объекты
var articles = {
  "Bootstrap": [
    {"id":"1", "title":"Введение"},
    {"id":"2", "title":"Как установить"},
    {"id":"3", "title":"Сетка"}
  ],
  "JavaScript": [
    {"id":"4", "title":"Основы"},
    {"id":"5", "title":"Выборка элементов"}
  ]  
};

$.each(articles,function(key,data) {
  console.log('Раздел: ' + key);
  $.each(data, function(index,value) {
    console.log('Статья: id = ' + value['id'] + '; Название = '+ value['title']);
  });
});

/*
Результат:
Раздел: Bootstrap
Статья: id = 1; Название = Введение
Статья: id = 2; Название = Как установить
Статья: id = 3; Название = Сетка
Раздел: JavaScript
Статья: id = 4; Название = Основы
Статья: id = 5; Название = Выборка элементов
*/

Как прервать each (выйти из цикла)

Прерывание (break) цикла each осуществляется с помощью оператора return, который должен возвращать значение false.

Например, прервём выполнение цикла each после того как найдём в массиве arr число 7:

// массив, состоящий из 5 чисел
var arr = [5, 4, 7, 17, 19];

// число, которое необходимо найти
var find = 7;

// переберём массив arr
$.each(arr, function (index, value) {
  // если необходимое число найдено, то..
  if (value === find) {
    // вывести его в консоль
    console.log('Ура! Число ' + find + ' найдено! Данное число имеет индекс: ' + index);
    // прервать выполнение цикла
    return false;
  } else {
  // иначе вывести в консоль текущее число
  console.log('Текущее число: ' + value);
  }
});

/* Результат (в консоли):
Текущее число: 5
Текущее число: 4
Ура! Число 7 найдено! Данное число имеет индекс: 2
*/

Как перейти к следующей итерации (each continue)

В each прерывание выполнения текущей итерации и переход к следующей осуществляется с помощью оператора return, который должен иметь значение отличное от false.

// массив, состоящий из чисел
var arr = [3, 5, 4, 9, 17, 19, 30, 35, 40];

// массив, который должен содержать все элементы массива arr, кроме чётных чисел
var newarr = [];

// переберём массив arr
$.each(arr, function (index, value) {

  // если элемент чётный, то пропустим его
  if (value % 2 === 0) {
    // прервём выполнение текущей итерации и перейдём к следующей
    return;
  }
  // добавить в массив newarr значение value
  newarr.push(value);

});

console.log('Исходный массив (arr): ' + arr.join());
console.log('Результирующий массив (newarr): ' + newarr.join());

/* Результат (в консоли):
Исходный массив (arr): 3,5,4,9,17,19,30,35,40
Результирующий массив (newarr): 3,5,9,17,19,35
*/