lisachenko / immutable-object
不可变对象库
Requires
- lisachenko/z-engine: ^0.7.1
Requires (Dev)
- phpunit/phpunit: ^7.5
This package is auto-updated.
Last update: 2024-08-28 10:54:38 UTC
README
此库为PHP>=7.4.2提供原生的不可变对象。
理由
你有多少次想过PHP中如果能使用不可变对象会多么方便?如果对象可以在构造函数外部尝试更改时发出警告,可以避免多少错误?不幸的是,不可变RFC从未被实现。
那该怎么办?当然,有psalm-immutable注释可以帮助我们在运行静态分析时找到错误。但在代码本身开发过程中,我们尝试更改此类对象的属性时将不会看到任何错误。
然而,随着FFI和Z-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中不可变对象是如何实现的。希望这些信息能给您一些启示)
行为准则
本项目遵循贡献者公约 行为准则。通过参与,您应遵守此准则。请报告任何不可接受的行为。