{"id":16674662,"url":"https://github.com/lisachenko/immutable-object","last_synced_at":"2025-03-17T00:32:02.128Z","repository":{"id":49123217,"uuid":"184609194","full_name":"lisachenko/immutable-object","owner":"lisachenko","description":":lock: Immutable object library for PHP","archived":false,"fork":false,"pushed_at":"2021-06-28T04:20:39.000Z","size":30,"stargazers_count":97,"open_issues_count":3,"forks_count":3,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-10-13T12:43:48.331Z","etag":null,"topics":["ffi","immutable","object","php","z-engine"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lisachenko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-05-02T15:49:57.000Z","updated_at":"2024-07-07T18:36:51.000Z","dependencies_parsed_at":"2022-09-05T03:00:26.532Z","dependency_job_id":null,"html_url":"https://github.com/lisachenko/immutable-object","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lisachenko%2Fimmutable-object","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lisachenko%2Fimmutable-object/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lisachenko%2Fimmutable-object/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lisachenko%2Fimmutable-object/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lisachenko","download_url":"https://codeload.github.com/lisachenko/immutable-object/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221669313,"owners_count":16860852,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ffi","immutable","object","php","z-engine"],"created_at":"2024-10-12T12:43:50.934Z","updated_at":"2024-10-27T11:35:11.901Z","avatar_url":"https://github.com/lisachenko.png","language":"PHP","readme":"Immutable objects in PHP\n-----------------\nThis library provides native immutable objects for PHP\u003e=7.4.2\n\n[![Build Status](https://img.shields.io/travis/com/lisachenko/immutable-object/master)](https://travis-ci.org/lisachenko/immutable-object)\n[![GitHub release](https://img.shields.io/github/release/lisachenko/immutable-object.svg)](https://github.com/lisachenko/immutable-object/releases/latest)\n[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%207.4-8892BF.svg)](https://php.net/)\n[![License](https://img.shields.io/packagist/l/lisachenko/immutable-object.svg)](https://packagist.org/packages/lisachenko/immutable-object)\n\nRationale\n------------\nHow many times have you thought it would be nice to have immutable objects in PHP? How many errors could be avoided if\nthe objects could warn about attempts to change them outside the constructor? Unfortunately, \n[Immutability RFC](https://wiki.php.net/rfc/immutability) has never been implemented.\n\nWhat to do? \nOf course, there is [psalm-immutable](https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-immutable) \nannotation which can help us find errors when running static analysis. But during the development of the code itself, \nwe will not see any errors when trying to change a property in such an object.\n\nHowever, with the advent of [FFI](https://www.php.net/manual/en/book.ffi.php) and the \n[Z-Engine](https://github.com/lisachenko/z-engine) library, it became possible to use PHP to expand the capabilities\nof the PHP itself.\n\nPre-requisites and initialization\n--------------\nAs this library depends on `FFI`, it requires PHP\u003e=7.4 and `FFI` extension to be enabled.\n\nTo install this library, simply add it via `composer`:\n```bash\ncomposer require lisachenko/immutable-object\n```\nTo enable immutability, you should activate `FFI` bindings for PHP first by initializing the `Z-Engine` library with\nshort call to the `Core::init()`. And you also need to activate immutability handler for development mode (or do not\ncall it for production mode to follow Design-by-Contract logic and optimize performance and stability of application)\n\n```php\nuse Immutable\\ImmutableHandler;\nuse ZEngine\\Core;\n\ninclude __DIR__.'/vendor/autoload.php';\n\nCore::init();\nImmutableHandler::install();\n```\n\nProbably, `Z-Engine` will provide an automatic self-registration later, but for now it's ok to perform initialization\nmanually.\n\nApplying immutability\n--------\nIn order to make your object immutable, you just need to implement the `ImmutableInterface` interface marker in your\nclass and this library automatically convert this class to immutable. Please note, that this interface should be added\nto every class (it isn't guaranteed that it will work child with parent classes that was declared as immutable)\n\nNow you can test it with following example:\n```php\n\u003c?php\ndeclare(strict_types=1);\n\nuse Immutable\\ImmutableInterface;\nuse Immutable\\ImmutableHandler;\nuse ZEngine\\Core;\n\ninclude __DIR__.'/vendor/autoload.php';\n\nCore::init();\nImmutableHandler::install();\n\nfinal class MyImmutableObject implements ImmutableInterface\n{\n    public $value;\n\n    public function __construct($value)\n    {\n        $this-\u003evalue = $value;\n    }\n}\n\n$object = new MyImmutableObject(100);\necho $object-\u003evalue;  // OK, 100\n$object-\u003evalue = 200; // FAIL: LogicException: Immutable object could be modified only in constructor or static methods\n```\n\nLow-level details (for geeks)\n--------------\nEvery PHP class is represented by `zend_class_entry` structure in the engine:\n\n```text\nstruct _zend_class_entry {\n    char type;\n    zend_string *name;\n    /* class_entry or string depending on ZEND_ACC_LINKED */\n    union {\n        zend_class_entry *parent;\n        zend_string *parent_name;\n    };\n    int refcount;\n    uint32_t ce_flags;\n\n    int default_properties_count;\n    int default_static_members_count;\n    zval *default_properties_table;\n    zval *default_static_members_table;\n    zval ** static_members_table;\n    HashTable function_table;\n    HashTable properties_info;\n    HashTable constants_table;\n\n    struct _zend_property_info **properties_info_table;\n\n    zend_function *constructor;\n    zend_function *destructor;\n    zend_function *clone;\n    zend_function *__get;\n    zend_function *__set;\n    zend_function *__unset;\n    zend_function *__isset;\n    zend_function *__call;\n    zend_function *__callstatic;\n    zend_function *__tostring;\n    zend_function *__debugInfo;\n    zend_function *serialize_func;\n    zend_function *unserialize_func;\n\n    /* allocated only if class implements Iterator or IteratorAggregate interface */\n    zend_class_iterator_funcs *iterator_funcs_ptr;\n\n    /* handlers */\n    union {\n        zend_object* (*create_object)(zend_class_entry *class_type);\n        int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */\n    };\n    zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);\n    zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method);\n\n    /* serializer callbacks */\n    int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);\n    int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data);\n\n    uint32_t num_interfaces;\n    uint32_t num_traits;\n\n    /* class_entry or string(s) depending on ZEND_ACC_LINKED */\n    union {\n        zend_class_entry **interfaces;\n        zend_class_name *interface_names;\n    };\n\n    zend_class_name *trait_names;\n    zend_trait_alias **trait_aliases;\n    zend_trait_precedence **trait_precedences;\n\n    union {\n        struct {\n            zend_string *filename;\n            uint32_t line_start;\n            uint32_t line_end;\n            zend_string *doc_comment;\n        } user;\n        struct {\n            const struct _zend_function_entry *builtin_functions;\n            struct _zend_module_entry *module;\n        } internal;\n    } info;\n};\n```\nYou can notice that this structure is pretty big and contains a lot of interesting information. But we are interested in\nthe `interface_gets_implemented` callback which is called when some class trying to implement concrete interface. Do you\nremember about `Throwable` class that throws an error when you are trying to add this interface to your class? This is\nbecause `Throwable` class has such handler installed that prevents implementation of this interface in user-land.\n\nWe are going to use this hook for our `ImmutableInterface` interface to adjust original class behaviour. `Z-Engine` \nprovides a method called `ReflectionClass-\u003esetInterfaceGetsImplementedHandler()` that is used for installing custom \n`interface_gets_implemented` callback.\n\nBut how we will make existing class and objects immutable? Ok, let's have a look at one more structure, called\n`zend_object`. This structure represents an object in PHP.\n\n```text\nstruct _zend_object {\n    zend_refcounted_h gc;\n    uint32_t          handle;\n    zend_class_entry *ce;\n    const zend_object_handlers *handlers;\n    HashTable        *properties;\n    zval              properties_table[1];\n};\n``` \nYou can see that there is handle of object (almost not used), there is a link to class entry (`zend_class_entry *ce`),\nproperties table and strange `const zend_object_handlers *handlers` field. This `handlers` field points to the list of\nobject handlers hooks that can be used for object casting, operator overloading and much more:\n\n```text\nstruct _zend_object_handlers {\n    /* offset of real object header (usually zero) */\n    int                                      offset;\n    /* object handlers */\n    zend_object_free_obj_t                  free_obj;             /* required */\n    zend_object_dtor_obj_t                  dtor_obj;             /* required */\n    zend_object_clone_obj_t                 clone_obj;            /* optional */\n    zend_object_read_property_t             read_property;        /* required */\n    zend_object_write_property_t            write_property;       /* required */\n    zend_object_read_dimension_t            read_dimension;       /* required */\n    zend_object_write_dimension_t           write_dimension;      /* required */\n    zend_object_get_property_ptr_ptr_t      get_property_ptr_ptr; /* required */\n    zend_object_get_t                       get;                  /* optional */\n    zend_object_set_t                       set;                  /* optional */\n    zend_object_has_property_t              has_property;         /* required */\n    zend_object_unset_property_t            unset_property;       /* required */\n    zend_object_has_dimension_t             has_dimension;        /* required */\n    zend_object_unset_dimension_t           unset_dimension;      /* required */\n    zend_object_get_properties_t            get_properties;       /* required */\n    zend_object_get_method_t                get_method;           /* required */\n    zend_object_call_method_t               call_method;          /* optional */\n    zend_object_get_constructor_t           get_constructor;      /* required */\n    zend_object_get_class_name_t            get_class_name;       /* required */\n    zend_object_compare_t                   compare_objects;      /* optional */\n    zend_object_cast_t                      cast_object;          /* optional */\n    zend_object_count_elements_t            count_elements;       /* optional */\n    zend_object_get_debug_info_t            get_debug_info;       /* optional */\n    zend_object_get_closure_t               get_closure;          /* optional */\n    zend_object_get_gc_t                    get_gc;               /* required */\n    zend_object_do_operation_t              do_operation;         /* optional */\n    zend_object_compare_zvals_t             compare;              /* optional */\n    zend_object_get_properties_for_t        get_properties_for;   /* optional */\n};\n```\nBut there is one important fact. This field is declared as `const`, this means that it cannot be changed in runtime, we\nneed to initialize it only once during object creation. We can not hook into default object creation process without \nwriting a C extension, but we have an access to the `zend_class_entry-\u003ecreate_object` callback. We can replace it with\nour own implementation that could allocate our custom object handlers list for this class and save a pointer to it in\nmemory, providing API to modify object handlers in runtime, as they will point to one single place.\n\nWe will override low-level `write_property` handler to prevent changes of properties for every instance of class. But \nwe should preserve original logic in order to allow initialization in class constructor, otherwise properties will be \nimmutable from the beginning. Also we should throw an exception for attempts to unset property in `unset_property` hook.\nAnd we don't want to allow getting a reference to properties to prevent indirect modification like `$obj-\u003efield++` or\n`$byRef = \u0026$obj-\u003efield; $byRef++;`.\n\nThis is how immutable objects in PHP are implemented. Hope that this information will give you some food for thoughts )\n\nCode of Conduct\n--------------\n\nThis project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).\nBy participating, you are expected to uphold this code.\nPlease report any unacceptable behavior.\n\nLicense\n-------\nThis library is distributed under [MIT-license](LICENSE) and it uses `Z-Engine` library distributed under \n**RPL-1.5** with additional [premium license](ZENGINE_LICENSE_PREMIUM).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flisachenko%2Fimmutable-object","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flisachenko%2Fimmutable-object","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flisachenko%2Fimmutable-object/lists"}