Skip to content

Commit facc273

Browse files
committed
Add README, CODE_OF_CONDUCT, Travis build
1 parent 5a6db76 commit facc273

6 files changed

+332
-1
lines changed

.travis.coverage.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
set -x
2+
if [ "TRAVIS_JOB_NAME" = '7.4' ] ; then
3+
wget https://scrutinizer-ci.com/ocular.phar
4+
php ocular.phar code-coverage:upload --format=php-clover ./build/clover.xml
5+
fi

.travis.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
services:
2+
- docker
3+
matrix:
4+
include:
5+
- language: php
6+
name: "7.4"
7+
after_script:
8+
- sh .travis.coverage.sh
9+
env:
10+
- COVERAGE_FLAGS="--coverage-text --coverage-clover=build/clover.xml"
11+
12+
before_script:
13+
- docker build --build-arg PHP_VERSION=${TRAVIS_JOB_NAME} -t immutable-object .
14+
script:
15+
- docker run immutable-object /usr/src/immutable-object/vendor/bin/phpunit --verbose ${COVERAGE_FLAGS} --colors

CODE_OF_CONDUCT.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Contributor Covenant Code of Conduct
2+
3+
## Our Pledge
4+
5+
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6+
7+
## Our Standards
8+
9+
Examples of behavior that contributes to creating a positive environment include:
10+
11+
* Using welcoming and inclusive language
12+
* Being respectful of differing viewpoints and experiences
13+
* Gracefully accepting constructive criticism
14+
* Focusing on what is best for the community
15+
* Showing empathy towards other community members
16+
17+
Examples of unacceptable behavior by participants include:
18+
19+
* The use of sexualized language or imagery and unwelcome sexual attention or advances
20+
* Trolling, insulting/derogatory comments, and personal or political attacks
21+
* Public or private harassment
22+
* Publishing others' private information, such as a physical or electronic address, without explicit permission
23+
* Other conduct which could reasonably be considered inappropriate in a professional setting
24+
25+
## Our Responsibilities
26+
27+
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28+
29+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30+
31+
## Scope
32+
33+
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34+
35+
## Enforcement
36+
37+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38+
39+
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40+
41+
## Attribution
42+
43+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44+
45+
[homepage]: http://contributor-covenant.org
46+
[version]: http://contributor-covenant.org/version/1/4/

Dockerfile

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
ARG PHP_VERSION
2+
FROM php:$PHP_VERSION
3+
RUN apt-get update \
4+
&& apt-get install -y libffi-dev git unzip \
5+
&& docker-php-source extract \
6+
&& docker-php-ext-install ffi \
7+
&& docker-php-source delete
8+
WORKDIR /usr/src/immutable-object
9+
RUN curl -sS https://getcomposer.org/installer | php && mv ./composer.phar /usr/local/bin/composer
10+
COPY . /usr/src/immutable-object
11+
RUN composer install

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2019 Lisachenko Alexander <[email protected]>
1+
Copyright (c) 2020 Lisachenko Alexander <[email protected]>
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy
44
of this software and associated documentation files (the "Software"), to deal

README.md

+254
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
Immutable objects in PHP
2+
-----------------
3+
This library provides native immutable objects for PHP>=7.4.2
4+
5+
[![Build Status](https://img.shields.io/travis/com/lisachenko/immutable-object/master)](https://travis-ci.org/lisachenko/immutable-object)
6+
[![GitHub release](https://img.shields.io/github/release/lisachenko/immutable-object.svg)](https://github.com/lisachenko/immutable-object/releases/latest)
7+
[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%207.4-8892BF.svg)](https://php.net/)
8+
[![License](https://img.shields.io/packagist/l/lisachenko/immutable-object.svg)](https://packagist.org/packages/lisachenko/immutable-object)
9+
10+
Rationale
11+
------------
12+
How many times have you thought it would be nice to have immutable objects in PHP? How many errors could be avoided if
13+
the objects could warn about attempts to change them outside the constructor? Unfortunately,
14+
[Immutability RFC](https://wiki.php.net/rfc/immutability) has never been implemented.
15+
16+
What to do?
17+
Of course, there is [psalm-immutable](https://psalm.dev/docs/annotating_code/supported_annotations/#psalm-immutable)
18+
annotation which can help us find errors when running static analysis. But during the development of the code itself,
19+
we will not see any errors when trying to change a property in such an object.
20+
21+
However, with the advent of [FFI](https://www.php.net/manual/en/book.ffi.php) and the
22+
[Z-Engine](https://travis-ci.org/lisachenko/z-engine) library, it became possible to use PHP to expand the capabilities
23+
of the PHP itself.
24+
25+
Pre-requisites and initialization
26+
--------------
27+
As this library depends on `FFI`, it requires PHP>=7.4 and `FFI` extension to be enabled.
28+
29+
To install this library, simply add it via `composer`:
30+
```bash
31+
composer require lisachenko/immutable-object
32+
```
33+
To enable immutability, you should activate `FFI` bindings for PHP first by initializing the `Z-Engine` library with
34+
short call to the `Core::init()`. And you also need to activate immutability handler for development mode (or do not
35+
call it for production mode to follow Design-by-Contract logic and optimize performance and stability of application)
36+
37+
```php
38+
use Immutable\ImmutableHandler;
39+
use ZEngine\Core;
40+
41+
include __DIR__.'/vendor/autoload.php';
42+
43+
Core::init();
44+
ImmutableHandler::install();
45+
```
46+
47+
Probably, `Z-Engine` will provide an automatic self-registration later, but for now it's ok to perform initialization
48+
manually.
49+
50+
Applying immutability
51+
--------
52+
In order to make your object immutable, you just need to implement the `ImmutableInterface` interface marker in your
53+
class and this library automatically convert this class to immutable. Please note, that this interface should be added
54+
to every class (it isn't guaranteed that it will work child with parent classes that was declared as immutable)
55+
56+
Now you can test it with following example:
57+
```php
58+
<?php
59+
declare(strict_types=1);
60+
61+
use Immutable\ImmutableInterface;
62+
use Immutable\ImmutableHandler;
63+
use ZEngine\Core;
64+
65+
include __DIR__.'/vendor/autoload.php';
66+
67+
Core::init();
68+
ImmutableHandler::install();
69+
70+
final class MyImmutableObject implements ImmutableInterface
71+
{
72+
public $value;
73+
74+
public function __construct($value)
75+
{
76+
$this->value = $value;
77+
}
78+
}
79+
80+
$object = new MyImmutableObject(100);
81+
echo $object->value; // OK, 100
82+
$object->value = 200; // FAIL: LogicException: Immutable object could be modified only in constructor or static methods
83+
```
84+
85+
Low-level details (for geeks)
86+
--------------
87+
Every PHP class is represented by `zend_class_entry` structure in the engine:
88+
89+
```text
90+
struct _zend_class_entry {
91+
char type;
92+
zend_string *name;
93+
/* class_entry or string depending on ZEND_ACC_LINKED */
94+
union {
95+
zend_class_entry *parent;
96+
zend_string *parent_name;
97+
};
98+
int refcount;
99+
uint32_t ce_flags;
100+
101+
int default_properties_count;
102+
int default_static_members_count;
103+
zval *default_properties_table;
104+
zval *default_static_members_table;
105+
zval ** static_members_table;
106+
HashTable function_table;
107+
HashTable properties_info;
108+
HashTable constants_table;
109+
110+
struct _zend_property_info **properties_info_table;
111+
112+
zend_function *constructor;
113+
zend_function *destructor;
114+
zend_function *clone;
115+
zend_function *__get;
116+
zend_function *__set;
117+
zend_function *__unset;
118+
zend_function *__isset;
119+
zend_function *__call;
120+
zend_function *__callstatic;
121+
zend_function *__tostring;
122+
zend_function *__debugInfo;
123+
zend_function *serialize_func;
124+
zend_function *unserialize_func;
125+
126+
/* allocated only if class implements Iterator or IteratorAggregate interface */
127+
zend_class_iterator_funcs *iterator_funcs_ptr;
128+
129+
/* handlers */
130+
union {
131+
zend_object* (*create_object)(zend_class_entry *class_type);
132+
int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */
133+
};
134+
zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);
135+
zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method);
136+
137+
/* serializer callbacks */
138+
int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);
139+
int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data);
140+
141+
uint32_t num_interfaces;
142+
uint32_t num_traits;
143+
144+
/* class_entry or string(s) depending on ZEND_ACC_LINKED */
145+
union {
146+
zend_class_entry **interfaces;
147+
zend_class_name *interface_names;
148+
};
149+
150+
zend_class_name *trait_names;
151+
zend_trait_alias **trait_aliases;
152+
zend_trait_precedence **trait_precedences;
153+
154+
union {
155+
struct {
156+
zend_string *filename;
157+
uint32_t line_start;
158+
uint32_t line_end;
159+
zend_string *doc_comment;
160+
} user;
161+
struct {
162+
const struct _zend_function_entry *builtin_functions;
163+
struct _zend_module_entry *module;
164+
} internal;
165+
} info;
166+
};
167+
```
168+
You can notice that this structure is pretty big and contains a lot of interesting information. But we are interested in
169+
the `interface_gets_implemented` callback which is called when some class trying to implement concrete interface. Do you
170+
remember about `Throwable` class that throws an error when you are trying to add this interface to your class? This is
171+
because `Throwable` class has such handler installed that prevents implementation of this interface in user-land.
172+
173+
We are going to use this hook for our `ImmutableInterface` interface to adjust original class behaviour. `Z-Engine`
174+
provides a method called `ReflectionClass->setInterfaceGetsImplementedHandler()` that is used for installing custom
175+
`interface_gets_implemented` callback.
176+
177+
But how we will make existing class and objects immutable? Ok, let's have a look at one more structure, called
178+
`zend_object`. This structure represents an object in PHP.
179+
180+
```text
181+
struct _zend_object {
182+
zend_refcounted_h gc;
183+
uint32_t handle;
184+
zend_class_entry *ce;
185+
const zend_object_handlers *handlers;
186+
HashTable *properties;
187+
zval properties_table[1];
188+
};
189+
```
190+
You can see that there is handle of object (almost not used), there is a link to class entry (`zend_class_entry *ce`),
191+
properties table and strange `const zend_object_handlers *handlers` field. This `handlers` field points to the list of
192+
object handlers hooks that can be used for object casting, operator overloading and much more:
193+
194+
```text
195+
struct _zend_object_handlers {
196+
/* offset of real object header (usually zero) */
197+
int offset;
198+
/* object handlers */
199+
zend_object_free_obj_t free_obj; /* required */
200+
zend_object_dtor_obj_t dtor_obj; /* required */
201+
zend_object_clone_obj_t clone_obj; /* optional */
202+
zend_object_read_property_t read_property; /* required */
203+
zend_object_write_property_t write_property; /* required */
204+
zend_object_read_dimension_t read_dimension; /* required */
205+
zend_object_write_dimension_t write_dimension; /* required */
206+
zend_object_get_property_ptr_ptr_t get_property_ptr_ptr; /* required */
207+
zend_object_get_t get; /* optional */
208+
zend_object_set_t set; /* optional */
209+
zend_object_has_property_t has_property; /* required */
210+
zend_object_unset_property_t unset_property; /* required */
211+
zend_object_has_dimension_t has_dimension; /* required */
212+
zend_object_unset_dimension_t unset_dimension; /* required */
213+
zend_object_get_properties_t get_properties; /* required */
214+
zend_object_get_method_t get_method; /* required */
215+
zend_object_call_method_t call_method; /* optional */
216+
zend_object_get_constructor_t get_constructor; /* required */
217+
zend_object_get_class_name_t get_class_name; /* required */
218+
zend_object_compare_t compare_objects; /* optional */
219+
zend_object_cast_t cast_object; /* optional */
220+
zend_object_count_elements_t count_elements; /* optional */
221+
zend_object_get_debug_info_t get_debug_info; /* optional */
222+
zend_object_get_closure_t get_closure; /* optional */
223+
zend_object_get_gc_t get_gc; /* required */
224+
zend_object_do_operation_t do_operation; /* optional */
225+
zend_object_compare_zvals_t compare; /* optional */
226+
zend_object_get_properties_for_t get_properties_for; /* optional */
227+
};
228+
```
229+
But there is one important fact. This field is declared as `const`, this means that it cannot be changed in runtime, we
230+
need to initialize it only once during object creation. We can not hook into default object creation process without
231+
writing a C extension, but we have an access to the `zend_class_entry->create_object` callback. We can replace it with
232+
our own implementation that could allocate our custom object handlers list for this class and save a pointer to it in
233+
memory, providing API to modify object handlers in runtime, as they will point to one single place.
234+
235+
We will override low-level `write_property` handler to prevent changes of properties for every instance of class. But
236+
we should preserve original logic in order to allow initialization in class constructor, otherwise properties will be
237+
immutable from the beginning. Also we should throw an exception for attempts to unset property in `unset_property` hook.
238+
And we don't want to allow getting a reference to properties to prevent indirect modification like `$obj->field++` or
239+
`$byRef = &$obj->field; $byRef++;`.
240+
241+
This is how immutable objects in PHP are implemented. Hope that this information will give you some food for thoughts )
242+
243+
Code of Conduct
244+
--------------
245+
246+
This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).
247+
By participating, you are expected to uphold this code.
248+
Please report any unacceptable behavior.
249+
250+
License
251+
-------
252+
This library is distributed under [MIT-licens](LICENSE) and it uses `Z-Engine` library distributed under
253+
**RPL-1.5** license.
254+
Please be informed about [Reciprocal Public License 1.5 (RPL-1.5)](https://opensource.org/licenses/RPL-1.5) rules.

0 commit comments

Comments
 (0)