1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie;
13
14 /**
15 * Together with the {@link Prototype} class the {@link Object} class provides means to
16 * define getters and setters, as well as define getters, setters, and method at runtime.
17 *
18 * The class also provides a method to create instances in the same fashion PDO creates instances
19 * with the `FETCH_CLASS` mode, that is the properties of the instance are set *before* its
20 * constructor is invoked.
21 *
22 * @property-read Prototype $prototype The prototype associated with the class.
23 */
24 class Object implements ToArrayRecursive
25 {
26 use ToArrayRecursiveTrait;
27 use PrototypeTrait;
28
29 /**
30 * Creates a new instance of the class using the supplied properties.
31 *
32 * The instance is created in the same fashion [PDO](http://www.php.net/manual/en/book.pdo.php)
33 * creates instances when fetching objects using the `FETCH_CLASS` mode, that is the properties
34 * of the instance are set *before* its constructor is invoked.
35 *
36 * Note: Because the method uses the [`unserialize`](http://www.php.net/manual/en/function.unserialize.php)
37 * function to create the instance, the `__wakeup()` magic method will be called if it is
38 * defined by the class, and it will be called *before* the constructor.
39 *
40 * Note: The {@link __wakeup()} method of the {@link Object} class removes `null` properties
41 * for which a getter is defined.
42 *
43 * @param array $properties Properties to be set before the constructor is invoked.
44 * @param array $construct_args Arguments passed to the constructor.
45 * @param string|null $class_name The name of the instance class. If empty the name of the
46 * called class is used.
47 *
48 * @return mixed The new instance.
49 */
50 static public function from($properties=null, array $construct_args=[], $class_name=null)
51 {
52 if (!$class_name)
53 {
54 $class_name = get_called_class();
55 }
56
57 $properties_count = 0;
58 $serialized = '';
59
60 if ($properties)
61 {
62 $class_reflection = new \ReflectionClass($class_name);
63 $class_properties = $class_reflection->getProperties();
64 $defaults = $class_reflection->getDefaultProperties();
65
66 $done = [];
67
68 foreach ($class_properties as $property)
69 {
70 if ($property->isStatic())
71 {
72 continue;
73 }
74
75 $properties_count++;
76
77 $identifier = $property->name;
78 $done[] = $identifier;
79 $value = null;
80
81 if (array_key_exists($identifier, $properties))
82 {
83 $value = $properties[$identifier];
84 }
85 else if (isset($defaults[$identifier]))
86 {
87 $value = $defaults[$identifier];
88 }
89
90 if ($property->isProtected())
91 {
92 $identifier = "\x00*\x00" . $identifier;
93 }
94 else if ($property->isPrivate())
95 {
96 $identifier = "\x00" . $property->class . "\x00" . $identifier;
97 }
98
99 $serialized .= serialize($identifier) . serialize($value);
100 }
101
102 $extra = array_diff(array_keys($properties), $done);
103
104 foreach ($extra as $name)
105 {
106 $properties_count++;
107
108 $serialized .= serialize($name) . serialize($properties[$name]);
109 }
110 }
111
112 $serialized = 'O:' . strlen($class_name) . ':"' . $class_name . '":' . $properties_count . ':{' . $serialized . '}';
113
114 $instance = unserialize($serialized);
115
116 #
117 # for some reason is_callable() sometimes returns true event if the `__construct` method is not defined.
118 #
119
120 if (method_exists($instance, '__construct') && is_callable([ $instance, '__construct' ]))
121 {
122 call_user_func_array([ $instance, '__construct' ], $construct_args);
123 }
124
125 return $instance;
126 }
127
128 /**
129 * Returns the private properties defined by the reference, this includes the private
130 * properties defined by the whole class inheritance.
131 *
132 * @param string|object $reference Class name or instance.
133 *
134 * @return array
135 */
136 static public function resolve_private_properties($reference)
137 {
138 if (is_object($reference))
139 {
140 $reference = get_class($reference);
141 }
142
143 if (isset(self::$resolve_private_properties_cache[$reference]))
144 {
145 return self::$resolve_private_properties_cache[$reference];
146 }
147
148 $private_properties = [];
149 $class_reflection = new \ReflectionClass($reference);
150
151 while ($class_reflection)
152 {
153 $private_properties = array_merge($private_properties, $class_reflection->getProperties(\ReflectionProperty::IS_PRIVATE));
154
155 $class_reflection = $class_reflection->getParentClass();
156 }
157
158 return self::$resolve_private_properties_cache[$reference] = $private_properties;
159 }
160
161 static private $resolve_private_properties_cache = [];
162
163 /**
164 * Returns the façade properties implemented by the specified reference.
165 *
166 * A façade property is a combination of a private property with the corresponding volatile
167 * getter and setter.
168 *
169 * @param string|object $reference Class name of instance.
170 *
171 * @return array[string]\ReflectionProperty
172 */
173 static public function resolve_facade_properties($reference)
174 {
175 if (is_object($reference))
176 {
177 $reference = get_class($reference);
178 }
179
180 if (isset(self::$resolve_facade_properties_cache[$reference]))
181 {
182 return self::$resolve_facade_properties_cache[$reference];
183 }
184
185 $facade_properties = [];
186
187 foreach (self::resolve_private_properties($reference) as $property)
188 {
189 $name = $property->name;
190
191 if (!method_exists($reference, "get_{$name}") || !method_exists($reference, "set_{$name}"))
192 {
193 continue;
194 }
195
196 $facade_properties[$name] = $property;
197 }
198
199 return self::$resolve_facade_properties_cache[$reference] = $facade_properties;
200 }
201
202 static private $resolve_facade_properties_cache = [];
203
204 /**
205 * Converts the object into an array.
206 *
207 * Only public properties and façade properties are included.
208 *
209 * @return array
210 */
211 public function to_array()
212 {
213 $array = Object\get_public_object_vars($this);
214
215 foreach (array_keys(self::resolve_facade_properties($this)) as $name)
216 {
217 $array[$name] = $this->$name;
218 }
219
220 return $array;
221 }
222
223 /**
224 * Converts the object into a JSON string.
225 *
226 * @return string
227 */
228 public function to_json()
229 {
230 return json_encode($this->to_array_recursive());
231 }
232 }
233
234 namespace ICanBoogie\Object;
235
236 function get_public_object_vars($object)
237 {
238 return get_object_vars($object);
239 }