lisachenko/immutable-object

不可变对象库

0.5.0 2020-02-07 12:31 UTC

README

此库为PHP>=7.4.2提供原生的不可变对象。

Build Status GitHub release Minimum PHP Version License

理由

你有多少次想过PHP中如果能使用不可变对象会多么方便?如果对象可以在构造函数外部尝试更改时发出警告,可以避免多少错误?不幸的是,不可变RFC从未被实现。

那该怎么办?当然,有psalm-immutable注释可以帮助我们在运行静态分析时找到错误。但在代码本身开发过程中,我们尝试更改此类对象的属性时将不会看到任何错误。

然而,随着FFIZ-Engine库的出现,现在可以使用PHP扩展PHP自身的能力。

先决条件和初始化

由于此库依赖于FFI,它需要PHP>=7.4以及启用了FFI扩展。

要安装此库,只需通过composer添加即可

composer require lisachenko/immutable-object

为了启用不可变性,您首先需要通过用短调用初始化Z-Engine库来激活PHP的FFI绑定。您还需要为开发模式激活不可变性处理程序(或在生产模式下不调用它以遵循设计-by-Contract逻辑并优化应用程序的性能和稳定性)

use Immutable\ImmutableHandler;
use ZEngine\Core;

include __DIR__.'/vendor/autoload.php';

Core::init();
ImmutableHandler::install();

也许Z-Engine将来会提供自动的自注册,但到目前为止手动初始化就足够了。

应用不可变性

为了使您的对象不可变,您只需在您的类中实现ImmutableInterface接口标记,然后此库会自动将该类转换为不可变。请注意,此接口应添加到每个类(不能保证它将在声明为不可变的父类中正常工作)

现在您可以使用以下示例进行测试

<?php
declare(strict_types=1);

use Immutable\ImmutableInterface;
use Immutable\ImmutableHandler;
use ZEngine\Core;

include __DIR__.'/vendor/autoload.php';

Core::init();
ImmutableHandler::install();

final class MyImmutableObject implements ImmutableInterface
{
    public $value;

    public function __construct($value)
    {
        $this->value = $value;
    }
}

$object = new MyImmutableObject(100);
echo $object->value;  // OK, 100
$object->value = 200; // FAIL: LogicException: Immutable object could be modified only in constructor or static methods

底层细节(针对技术爱好者)

在引擎中,每个PHP类都由zend_class_entry结构表示

struct _zend_class_entry {
    char type;
    zend_string *name;
    /* class_entry or string depending on ZEND_ACC_LINKED */
    union {
        zend_class_entry *parent;
        zend_string *parent_name;
    };
    int refcount;
    uint32_t ce_flags;

    int default_properties_count;
    int default_static_members_count;
    zval *default_properties_table;
    zval *default_static_members_table;
    zval ** static_members_table;
    HashTable function_table;
    HashTable properties_info;
    HashTable constants_table;

    struct _zend_property_info **properties_info_table;

    zend_function *constructor;
    zend_function *destructor;
    zend_function *clone;
    zend_function *__get;
    zend_function *__set;
    zend_function *__unset;
    zend_function *__isset;
    zend_function *__call;
    zend_function *__callstatic;
    zend_function *__tostring;
    zend_function *__debugInfo;
    zend_function *serialize_func;
    zend_function *unserialize_func;

    /* allocated only if class implements Iterator or IteratorAggregate interface */
    zend_class_iterator_funcs *iterator_funcs_ptr;

    /* handlers */
    union {
        zend_object* (*create_object)(zend_class_entry *class_type);
        int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */
    };
    zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);
    zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method);

    /* serializer callbacks */
    int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);
    int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data);

    uint32_t num_interfaces;
    uint32_t num_traits;

    /* class_entry or string(s) depending on ZEND_ACC_LINKED */
    union {
        zend_class_entry **interfaces;
        zend_class_name *interface_names;
    };

    zend_class_name *trait_names;
    zend_trait_alias **trait_aliases;
    zend_trait_precedence **trait_precedences;

    union {
        struct {
            zend_string *filename;
            uint32_t line_start;
            uint32_t line_end;
            zend_string *doc_comment;
        } user;
        struct {
            const struct _zend_function_entry *builtin_functions;
            struct _zend_module_entry *module;
        } internal;
    } info;
};

您可以注意到这个结构相当大,包含了很多有趣的信息。但我们感兴趣的是当某个类尝试实现具体接口时被调用的interface_gets_implemented回调。你还记得当你尝试将此接口添加到你的类中时,会抛出Throwable类的错误吗?这是因为Throwable类安装了这样的处理程序,防止在用户空间中实现此接口。

我们将使用此钩子为我们的ImmutableInterface接口进行调整原始类行为。《Z-Engine》提供了一个名为ReflectionClass->setInterfaceGetsImplementedHandler()的方法,用于安装自定义的interface_gets_implemented回调。

但我们如何使现有的类和对象不可变呢?好吧,让我们看看另一个结构,叫做zend_object。这个结构代表了PHP中的一个对象。

struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

您可以看到对象(几乎未使用)的句柄,有一个指向类入口的链接(zend_class_entry *ce),属性表以及奇特的const zend_object_handlers *handlers字段。这个handlers字段指向可以用于对象类型转换、操作符重载等操作的列表

struct _zend_object_handlers {
    /* offset of real object header (usually zero) */
    int                                      offset;
    /* object handlers */
    zend_object_free_obj_t                  free_obj;             /* required */
    zend_object_dtor_obj_t                  dtor_obj;             /* required */
    zend_object_clone_obj_t                 clone_obj;            /* optional */
    zend_object_read_property_t             read_property;        /* required */
    zend_object_write_property_t            write_property;       /* required */
    zend_object_read_dimension_t            read_dimension;       /* required */
    zend_object_write_dimension_t           write_dimension;      /* required */
    zend_object_get_property_ptr_ptr_t      get_property_ptr_ptr; /* required */
    zend_object_get_t                       get;                  /* optional */
    zend_object_set_t                       set;                  /* optional */
    zend_object_has_property_t              has_property;         /* required */
    zend_object_unset_property_t            unset_property;       /* required */
    zend_object_has_dimension_t             has_dimension;        /* required */
    zend_object_unset_dimension_t           unset_dimension;      /* required */
    zend_object_get_properties_t            get_properties;       /* required */
    zend_object_get_method_t                get_method;           /* required */
    zend_object_call_method_t               call_method;          /* optional */
    zend_object_get_constructor_t           get_constructor;      /* required */
    zend_object_get_class_name_t            get_class_name;       /* required */
    zend_object_compare_t                   compare_objects;      /* optional */
    zend_object_cast_t                      cast_object;          /* optional */
    zend_object_count_elements_t            count_elements;       /* optional */
    zend_object_get_debug_info_t            get_debug_info;       /* optional */
    zend_object_get_closure_t               get_closure;          /* optional */
    zend_object_get_gc_t                    get_gc;               /* required */
    zend_object_do_operation_t              do_operation;         /* optional */
    zend_object_compare_zvals_t             compare;              /* optional */
    zend_object_get_properties_for_t        get_properties_for;   /* optional */
};

但有一个重要的事实。这个字段被声明为const,这意味着它在运行时不能被更改,我们只需要在对象创建期间初始化它一次。不编写C扩展,我们不能钩入默认的对象创建过程,但我们可以访问zend_class_entry->create_object回调。我们可以用自己的实现替换它,为这个类分配自定义对象处理程序列表,并在内存中保存其指针,提供修改对象处理程序的API,因为它们将指向一个单一的位置。

我们将覆盖低级的write_property处理程序以防止更改类每个实例的属性。但我们应保留原始逻辑,以便在类构造函数中进行初始化,否则属性将从开始时就是不可变的。我们还应在unset_property钩子中抛出异常以防止尝试取消属性。我们不想允许获取属性的引用,以防止间接修改,如$obj->field++$byRef = &$obj->field; $byRef++;

这就是PHP中不可变对象是如何实现的。希望这些信息能给您一些启示)

行为准则

本项目遵循贡献者公约 行为准则。通过参与,您应遵守此准则。请报告任何不可接受的行为。

许可证

此库根据MIT许可证分发,并使用在RPL-1.5下分发的Z-Engine库,以及额外的高级许可证