PHP и неизменяемость: объекты и обобщение. Часть 3

Это перевод оригинальной статьи Саймона Холивелла PHP and immutability: objects and generalisation — part three.

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

А какая проблема с объектами?

Объекты или экземпляры классов в PHP передаются по ссылке. Любые изменения в классе отобразятся во всех местах, куда он передан. Это отличается от скалярных объектов типа строк, которые передаются по значению.

$class = new stdClass();
function addItem($x, $item) {
    $x->$item = $item;
}
var_dump($class); // object(stdClass)#1 (0) {}
addItem($class, 'test');
var_dump($class);
/*
object(stdClass)#1 (1) {
  ["test"]=> string(4) "test"
}
*/

Здесь вы можете видеть функцию addItem() , которая добавляет свойство к экземпляру stdClass — а это вызывает побочный эффект. Оигинальный класс $class также обновлен, так как он ссылается на то же самое значение, так что если мы посмотрим дамп переменной, то сможем увидеть, что её значение изменилось.

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

$string = 'begin';
function addItem($x, $item) {
    $x .= $item;
}
var_dump($string); // string(5) "begin"
addItem($string, 'end');
var_dump($string); // string(5) "begin"

Здесь оригинальное значение остаётся нетронутым, потому что, в отличие от объекта, на неё нет ссылки из функции addItem().

Эти побочные эффекты создают сложности в деле превращения объекта в неизменяемую структуру. Кто-нибудь с доступом к ссылке может просто изменить объект пост фактум, таким образом разрушив неизменяемость.

Что насчёт ресурсов?

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

$f = fopen('/tmp/test.txt', 'r'); // содержит"123456789"
$out = fread($f, 3);
var_dump($out); // string(3) "123"

$out2 = fread($f, 3);
var_dump($out2); // string(3) "456"

Это происходит, потому что fread() перемещает указатель по мере чтения. Даже если мы перемотаем обратно указатель с помощью rewind(), это не гарантия, что мы получим обратно то же значение.

Дополнительная проблема с ресурсами заключается в том, что они по своей природе не являются «конечными» вещами, так что даже если вы бы предотвратили изменения в своей программе, то всё равно столкнулись бы с изменениями — например, если кто-то обновил бы файл на диске.

$f = fopen('/dev/urandom', 'r');
$out = bin2hex(fread($f, 3));
var_dump($out); // string(6) "82e42b"

rewind($f); // сброс указателя к началу
$out2 = bin2hex(fread($f, 3));
var_dump($out2); // string(6) "e20c78"


Между двумя вызовами fread() данные в ресурсе изменились из-за внешнего вмешательства. Новое случайное значение в действительности записано в /dev/urandom , означая, что выводимое значение также изменилось, даже несмотря на то, что мы перевели указатель и использовали то же смещение/индекс 3.

Заметьте, что использование bin2hex() преобразовывает бинарные байты, производимые /dev/urandom, в шестнадцатиричный вид, чтобы сделать более читаемыми. Этот процесс преобразования также увеличивает длину значения, так что �@D��N� становитсяdd4044f5f84ed6 в шестнадцатеричной нотации. Вот почему смещение может быть 3, но строка, которую получаем, вимеет длину  6 символов.

Однако, если ваш источник данных не является бинарным, то и использовать  bin2hex()  в вашем коде нет необходимости.

Что мы можем сделать, чтобы это исправить?

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

$f = fopen('/dev/urandom', 'r');
$randomStr = bin2hex(fread($f, 7));
var_dump($randomStr); // string(14) "d102c7ca28b6f1"

var_dump(substr($randomStr, 0, 3)); // string(3) "d10"
var_dump(substr($randomStr, 0, 3)); // string(3) "d10"

Как вы уже могли ясно увидеть, в этом примере между двумя выводами значение не изменяется, потому что мы обращаемся к скалярной строке вместо того, чтобы обращаться непосредственно к ресурсу. Вы можете также с лёгкостью «скормить» $randomStrопределениям Immutable, которые будут позже описаны. 
В случае объектов, однако, есть кое-что, что мы можем сделать, чтоб защитить неизменяемое от собственной природы, подразумевающей передачу по ссылке. Для простых объектов вы можете просто клонировать значение входящего объекта при записи его в неизменяемую структуру данных. Это создаст новую копию объекта со своей собственной ссылкой и, таким образом, устранит зависимость от предыдущей ссылки — два объекта больше не связаны по ссылке. Любое изменение в одном из них не будет воспроизведено в другом.

declare(strict_types=1);

final class Immutable {
    private $data;
    private $mutable = true;
    public function __construct(stdClass $value) {
        if (false === $this->mutable) {
            throw new \BadMethodCallException('Конструктор вызван дважды.');
        }
        $this->data = clone $value;
        $this->mutable = false;
    }
    public function get() {
        return $this->data;
    }
}

$test = new stdClass();
$test->data = 'test';
echo $test->data; // test

$imm = new Immutable($test);
echo $imm->get()->data; // test

$test->data = 'simon';
echo $test->data; // simon
echo $imm->get()->data; // test

Путём клонирования объекта мы создали сущность-дубликат и сослались на неё изнутри Immutable. Это значит, что когда$test позже обновлён, он не затронуло значения в$imm так как оно не содержит той же ссылки, что и в $test.
Вот и всё, мы это сделали.

Большая вложенность, однако

Да, верно, не так быстро! Предыдущий пример легко можно сломать одним небольшим изменением; создайте объект для хранения внутри $test.

$value = new stdClass();
$value->data = 'значение';

$test = new stdClass();
$test->data = $value;
var_dump($test->data);
/*
object(stdClass)#1 (1) {
  ["data"]=> string(5) "значение"
}
*/

$imm = new Immutable($test);
var_dump($imm->get()->data);
/*
object(stdClass)#1 (1) {
  ["data"]=> string(5) "значение"
}
*/

//измените значение во вложенном объекте, чтобы посмотреть, изменится ли и неизменяемое
$value->data = 'измененное значение!';
var_dump($imm->get()->data);
/*
object(stdClass)#1 (1) {
  ["data"]=> string(14) "измененное значение!"
}
*/

Как вы могли бы ожидать, мы клонировали $test , и хотя он записан внутри Immutable , это не означает, что его значения также клонируются. К сожалению, $value всё еще напрямую адресуется по ссылке, так что любые последующие изменения отражаются во всех ссылающихся на него местах, включая те, что находятся внутри нашегоImmutable.

То же самое будет верно и для любого неизменяемого, содержащего массив. Вы можете сделать объектом любой из элементов массива и позже поменять его подобно $value в этом примере для объекта.

Короче говоря, это неизменяемое фактически изменяемо.

Неизменяемое большой вложенности при помощи __clone()

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

declare(strict_types=1);

final class Immutable {
    private $data;
    private $mutable = true;
    public function __construct(MySimpleClass $value) {
        if (false === $this->mutable) {
            throw new \BadMethodCallException('Конструктор вызван дважды.');
        }
        $this->data = clone $value;
        $this->mutable = false;
    }
    public function get() {
        return $this->data;
    }
}

class MySimpleClass {
    private $data;
    public function __construct(stdClass $value) {
        $this->data = $value;
    }
    public function get() {
        return $this->data;
    }
    public function __clone() {
        $this->data = clone $this->data;
    }
}

$stdClass = new stdClass();
$stdClass->value = 'Привет';
var_dump($stdClass);
/*
object(stdClass)#1 (1) {
  ["value"]=> string(5) "Привет"
}
*/
$toBeStored = new MySimpleClass($stdClass);
var_dump($toBeStored->get());
/*
object(stdClass)#1 (1) {
  ["value"]=> string(5) "Привет"
}
*/
$imm = new Immutable($toBeStored);
var_dump($imm->get()->get());
/*
object(stdClass)#5 (1) {
  ["value"]=> string(5) "Привет"
}
*/

Как вы можете видеть, MySimpleClass спроектирован очень наивно, чтобы было проще понять пример. Вы также заметите, что ID объекта перепрыгивает на 5, когда применяется последний var_dump()  — это потому что сработал __clone() в MySimpleClass.

Если мы снова пройдёмся по имплементации и попробуем изменить $stdClass то, возможно, станет понятнее.

$stdClass = new stdClass();
$stdClass->value = 'Привет';
var_dump($stdClass);
/*
object(stdClass)#1 (1) {
  ["value"]=> string(5) "Привет"
}
*/

$toBeStored = new MySimpleClass($stdClass);
// мы всё ещё можем изменить этот объект, так как клонирование ещё не произошло
$stdClass->data = 'мир';
var_dump($toBeStored->get());
/*
object(stdClass)#1 (2) {
  ["value"]=> string(5) "Привет"
  ["data"]=> string(5) "мир"
}
*/

// клонирование будет запущено конструктором в Immutable вот здесь
$imm = new Immutable($toBeStored);
// Обратите внимание, что следующая строка возвращает другой объект (#5 вместо #1)
// из-за операции клонирования в конструкторе Immutable 
var_dump($imm->get()->get()); 
/*
object(stdClass)#5 (2) {
  ["value"]=> string(5) "Привет"
  ["data"]=> string(5) "мир"
}
*/

// следующая строка не затронет даннуе, обёрнутые в Immutable, так как $stdClass
// ссылается на исходный объект #1
$stdClass->combined = $stdClass->value . $stdClass->data;
var_dump($imm->get()->get());
/*
object(stdClass)#5 (2) {
  ["value"]=> string(5) "Привет"
  ["data"]=> string(5) "мир"
}
*/

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

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

Обобщённое неизменяемое большой вложенности

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

declare(strict_types=1);

final class Immutable {
    private $data;
    private $mutable = true;
    
    public function __construct(array $args) {
        if (false === $this->mutable) {
            throw new \BadMethodCallException('Конструктор вызван дважды.');
        }
        $this->data = $this->sanitiseInput($args);
        $this->mutable = false;
    }
    public function getData(): array {
        return $this->data;
    }
    public function sanitiseInput(array $args): array {
        return array_map(function($x) {
            if (is_scalar($x)) return $x;
            else if (is_object($x)) return $this->sanitiseObject($x);
            else if (is_array($x)) return $this->sanitiseInput($x);
            else throw new \InvalidArgumentException(gettype($x) . ' не может быть сохранён в качестве Immutable.');
        }, $args);
    }
    // Этот метод предотвращает присваивание недоверямых объектов указанием типа
    // в сочетании с declare(strict_types=1) в начале файла.
    // Заметьте, что он также клонирует данный объект.
    private static function sanitiseObject(Immutable $object): Immutable {
        return clone $object;
    }
    public function __clone() {
        $this->data = $this->sanitiseInput($this->data);
    }
    public function __unset(string $id): void {}
    public function __set(string $id, $val): void {}
}

Можно сделать такой класс для создания неизменяемых списков вещей:

$immA = new Immutable([1, 'unjani wena']);
var_dump($immA);
/*
object(Immutable)#1 (2) {
  ["data":"Immutable":private]=> array(2) {
    [0]=> int(1)
    [1]=> string(11) "unjani wena"
  }
  ["mutable":"Immutable":private]=> bool(false)
}
*/
$immB = new Immutable([2, $immA]);
var_dump($immB);
/*
object(Immutable)#2 (2) {
  ["data":"Immutable":private]=> array(2) {
    [0]=> int(2)
    [1]=> object(Immutable)#4 (2) {
      ["data":"Immutable":private]=> array(2) {
        [0]=> int(1)
        [1]=> string(11) "unjani wena"
      }
      ["mutable":"Immutable":private]=> bool(false)
    }
  }
  ["mutable":"Immutable":private]=> bool(false)
}
*/
$immC = new Immutable([2, new stdClass]);
// Error: Аргумент 1, переданный в Immutable::sanitiseObject(), должен быть экземпляром //Immutable,
// а у нас экземпляр stdClass

Главной новой концепцией здесь является рекурсивный метод sanitiseInput(), который рекурсивно обходит массив данных, клонируя все объекты, которые находит. Это осуществляется в sanitiseObject() , который, как вы заметите, использует указание типа, чтобы убедиться, что только экземпляры Immutable могут быть присвоены в качестве значений. Вот так мы гарантируем, что только известные неизменяемые объекты присваиваются внутриImmutable.

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

  • расширить базовый или абстрактный класс, исплементируя их все,
  • использовать интерфейс, который будут имплементировать они все или
  • простой набор проверокinstanceOf.

Что-то из этого должно сработать.

/**
 * @param Immutable|MyOtherImmutable|SomeOtherImmutable $object
 */
protected function sanitiseObject($object) {
    if (array_filter(
        ['Immutable', 'MyOtherImmutable', 'SomeOtherImmutable'],
        function($x) use ($object) { return $object instanceOf $x; }
    )) {
        return clone $object;
    }
    throw new \InvalidArgumentException(gettype($x) . ' не может быть сохранён в Immutable.');
}

Какой способ вы ни выберете или предпочтёте, дело ваше, конечно.

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

Использование генератора для облегчения обобщения

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

Структура данных

Обратив взгляд в сторону самой структуры, мы добавим несколько методов, которые повысят надёжность доступа к данным в обобщённом классе. В этом отношении очень полезно знать, существует ли значение, так что мы добавим метод has($key) . Он также будет использоваться функцией getOrElse($key, $default) для того, чтобы в случае отсутствия ключа бралось бы значение по умолчанию.

declare(strict_types=1);

final class ImmutableData {
    private $data = [];
    private function __construct() {}
    public static function create(array $args): ImmutableData {
        $immutable = new self;
        $immutable->data = static::sanitiseInput($args);
        return $immutable;
    }
    public function has($key) {
        return array_key_exists($key, $this->data);
    }
    public function get($key) {
        return $this->data[$key];
    }
    public function getOrElse($key, $default) {
        if($this->has($key)) {
            return $this->get($key);
        }
        return $default;
    }
    public function getAsArray(): array {
        return $this->data;
    }
    protected static function sanitiseInput(array $arr): array {
        return array_map(function($x) {
            if (is_scalar($x)) return $x;
            else if (is_object($x)) return static::sanitiseObject($x);
            else if (is_array($x)) return static::sanitiseInput($x);
            else throw new \InvalidArgumentException(gettype($x) . ' не может быть сохранён как Immutable.');
        }, $arr);
    }
    protected static function sanitiseObject(ImmutableData $object): ImmutableData {
        return clone $object;
    }

    // возвращает текстовое предстваление класса, которое может быть распарсено    
    public function __toString(): string {
        return var_export($this->getAsArray(), true);
    }
    // вызывается, когда класс парсится после var_export
    public function __set_state(array $args): ImmutableData {
        return static:create($args);
    }
    public function __unset($a): void {}
    public function __set($a, $b): void {}
    private function __clone() {
        $this->data = static::sanitiseInput($this->data);
    }
}

Это полностью неизменяемая структура, которую за нас заполнит наш генератор.

В отличие от последнего Immutable , этот использует статические методы и предотвращает доступ к конструктору класса делая его приватным. Это позволяет обойтись без танцев с бубнами вокруг $mutable true/false, которые нам понадобились прежде. я предпочитаю «танцы», но это прекрасный пример другого метода, с помощью которого мы добьёмся того же результата.

Вы заметите, что есть несколько других методов, которые мы пока не обсудили. Есть get($key) , который даёт нам возможность легко получить доступ к значению по ключу, и getAsArray() берёт на себя заботу вернуть весь массив $this->data . Наконец, есть метод toString() , который выдаёт представление сохранённых данных в виде строки, которая может быть распарсена PHP.

Генератор в деталях

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

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

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

Все данные надо будет хранить по ключу, чтобы к ним можно было иметь доступ.

class Immutable {
    private $data = [];
    private function __construct() {}
    public static function create(): self {
        return new self;
    }
    public static function with(ImmutableData $old): self {
        $new = static::create();
        $new->data = $old->getAsArray();
        return $new;
    }
    public function set(string $key, $value): self {
        return $this->setData($key, $value);
    }
    public function unset($key): self {
        unset($this->data[$key]);
        return $this;
    }
    public function setIntKey(int $key, $value): self {
        return $this->setData($key, $value);
    }
    private function setData($key, $value): self {
        $this->data[$key] = $value;
        return $this;
    }
    public function arr(array $arr): self {
        foreach($arr as $key => $value) {
            if (is_string($key)) {
                $this->set($key, $value);
            } else if (is_int($key)) {
                $this->setIntKey($key, $value);
            }
        }
        return $this;
    }
    public function unsetArr(array $arr): self {
        foreach($arr as $key) {
            $this->unset($key);
        }
        return $this;
    }
    public function build(): ImmutableData {
        return ImmutableData::create($this->data);
    }
    public function getAsArray(): array {
        return $this->data;
    }
}

Опять же, этот класс использует приватный конструктор и статический метод, чтобы предотвратить вызовы конструктора. Однако, если б захотели, вы могли бы здесь легко применить установку в true/false свойства $mutable .

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

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

$immX = Immutable::create()
    ->set('test', 'a string goes here')
    ->set('another', 100)
    ->arr([1,2,3,4,5,6])
    ->arr(['a' => 1, 'b' => 2])
    ->build();
echo (string) $immX;

В примере для вывода простого и распарсиваемого представления текста используется метод __toString() .

array (
  'test' => 'a string goes here',
  'another' => 100,
  0 => 1,
  1 => 2,
  2 => 3,
  3 => 4,
  4 => 5,
  5 => 6,
  'a' => 1,
  'b' => 2,
)

Вы также можете разместить доверяемый объект в неизменяемом — в этом случае мы можем просто использовать неизменяемое, которе использовали раньше — $immX.

$immY = Immutable::create()
    ->set('anObject', $immX)
    ->build();
echo (string) $immY;

Опять же, вывод может быть распарсен движком PHP, так что вы заметите там слегка странный вызов магического метода __set_state()  — вы вполне можете это проигнорировать и сконцентрироваться на самих данных. Этот магический метод реализован в классе ImmutableData , который мы определили ранее, он предназначен просто для наполнения класса набором данных/состояния, в то время как вывод var_export() парсится PHP.

array (
  'anObject' => 
  ImmutableData::__set_state(array(
     'data' => 
    array (
      'test' => 'a string goes here',
      'another' => 100,
      0 => 1,
      1 => 2,
      2 => 3,
      3 => 4,
      4 => 5,
      5 => 6,
      'a' => 1,
      'b' => 2,
    ),
  )),
)

Итак, в чём тут соль, если мы не можем извлечь свои данные? Ну, помните те методы get()has() иgetOrElse()? Они могут использоваться, чтобы легко и относительно просто получить доступ к сохранённым данным по ключу. Эти методы понятны по названиям, так что перейдём сразу к нескольким примерам для иллюстрации того, как их применять с $immY.

echo $immY->get('test'); // здесь у нас строка
var_dump($immY->has('test')); // bool(true)
var_dump($immY->has('non-existent')); // bool(false)
echo $immY->getOrElse('test', 'some default text'); // здесь у нас строка
echo $immY->getOrElse('non-existent', 'некий текст по умолчанию'); 
// некий текст по умолчанию

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

Модификация копий неизменяемой структуры с применением генератора

Ключ к тому, чтобы сделать неизменяемые сущности полезными в том, чтобы позволить потребителям легко и быстро создавать модифицированные копии инкапсюлированных данных. Это вписано в генератор, который мы определили ранее, и может быть лучше всего описано на нескольких примерах. Заметьте, что статический метод with() sспособен принять объект ImmutableData в качестве своегопервого параметра, и модификация — это именно то, для чего это нужно. Затем вы можете применить set() , чтобы добавить или изменить значения.

$immZ = Immutable::with($immY)
    ->set('a story', 'This is where someone should write a story')
    ->setIntKey(300, 'My int indexed value')
    ->arr(['arr: int indexed', 'arr' => 'arr: assoc key becomes immutable key'])
    ->build();
echo (string) $immZ;

в результате, мы должны увидеть, что наши новые свойства добавлены к сохранённому массиву из $immY.

array (
  'x' => 
  ImmutableData::__set_state(array(
     'data' => 
    array (
      'test' => 'a string goes here',
      'another' => 100,
      0 => 1,
      1 => 2,
      2 => 3,
      3 => 4,
      4 => 5,
      5 => 6,
      'a' => 1,
      'b' => 2,
    ),
  )),
  'a story' => 'This is where someone should write a story',
  300 => 'My int indexed value',
  0 => 'arr: int indexed',
  'arr' => 'arr: assoc key becomes immutable key',
)

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

$throwAway = Immutable::with($immZ)
    ->set('a story', 'My story begins by the slow moving waters of the meandering river.')
    ->build();
echo (string) $throwAway;

Результатом будет структура данных типа такой:

array (
  'x' => 
  ImmutableData::__set_state(array(
     'data' => 
    array (
      'test' => 'a string goes here',
      'another' => 100,
      0 => 1,
      1 => 2,
      2 => 3,
      3 => 4,
      4 => 5,
      5 => 6,
      'a' => 1,
      'b' => 2,
    ),
  )),
  'a story' => 'My story begins by the slow moving waters of the meandering river.',
  300 => 'My int indexed value',
  0 => 'arr: int indexed',
  'arr' => 'arr: assoc key becomes immutable key',
)

Это также легко применяется для удаления элементов из списка данных. Мы можем или удалить их по одному с помощью unset($key) , или же вы можете удалить сразу много, передав список в unsetArr().

$immAA = Immutable::with($immZ)
    ->unset('x')
    ->unsetArr(['a story', 300])
    ->build();
echo (string) $immAA;

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

array (
  0 => 'arr: int indexed',
  'arr' => 'arr: assoc key becomes immutable key',
)

Перед вызовом build()  вы можете делать unset()unsetArrsetsetIntKey иarr столько, сколько хотите, всё в одной цепи вызовов.

Заключение

Теперь у вас есть обобщённая неизменяемая структура данных, в которой вы можете хранить всё, что пожелаете. Если у вас есть недоверяемый объект, его вам нужно сохранить как строку, используя serialize() илиvar_export(). То же самое касается ресурсов типа обработчиков файлов, где вам перед сохранением нужно извлечь текст как строку.

За исключением этих двух «подводных камней», вы относительно свободны в использовании неизменяемых сущностей.

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

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