ext_php_rs/types/object.rs
1//! Represents an object in PHP. Allows for overriding the internal object used
2//! by classes, allowing users to store Rust data inside a PHP object.
3
4use std::{convert::TryInto, fmt::Debug, os::raw::c_char, ptr};
5
6use crate::{
7 boxed::{ZBox, ZBoxable},
8 class::RegisteredClass,
9 convert::{FromZendObject, FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
10 error::{Error, Result},
11 ffi::{
12 ext_php_rs_zend_object_release, object_properties_init, zend_call_known_function,
13 zend_function, zend_hash_str_find_ptr_lc, zend_object, zend_objects_new, HashTable,
14 ZEND_ISEMPTY, ZEND_PROPERTY_EXISTS, ZEND_PROPERTY_ISSET,
15 },
16 flags::DataType,
17 rc::PhpRc,
18 types::{ZendClassObject, ZendStr, Zval},
19 zend::{ce, ClassEntry, ExecutorGlobals, ZendObjectHandlers},
20};
21
22/// A PHP object.
23///
24/// This type does not maintain any information about its type, for example,
25/// classes with have associated Rust structs cannot be accessed through this
26/// type. [`ZendClassObject`] is used for this purpose, and you can convert
27/// between the two.
28pub type ZendObject = zend_object;
29
30impl ZendObject {
31 /// Creates a new [`ZendObject`], returned inside an [`ZBox<ZendObject>`]
32 /// wrapper.
33 ///
34 /// # Parameters
35 ///
36 /// * `ce` - The type of class the new object should be an instance of.
37 ///
38 /// # Panics
39 ///
40 /// Panics when allocating memory for the new object fails.
41 #[must_use]
42 pub fn new(ce: &ClassEntry) -> ZBox<Self> {
43 // SAFETY: Using emalloc to allocate memory inside Zend arena. Casting `ce` to
44 // `*mut` is valid as the function will not mutate `ce`.
45 unsafe {
46 let ptr = match ce.__bindgen_anon_2.create_object {
47 None => {
48 let ptr = zend_objects_new(ptr::from_ref(ce).cast_mut());
49 assert!(!ptr.is_null(), "Failed to allocate memory for Zend object");
50
51 object_properties_init(ptr, ptr::from_ref(ce).cast_mut());
52 ptr
53 }
54 Some(v) => v(ptr::from_ref(ce).cast_mut()),
55 };
56
57 ZBox::from_raw(
58 ptr.as_mut()
59 .expect("Failed to allocate memory for Zend object"),
60 )
61 }
62 }
63
64 /// Creates a new `stdClass` instance, returned inside an
65 /// [`ZBox<ZendObject>`] wrapper.
66 ///
67 /// # Panics
68 ///
69 /// Panics if allocating memory for the object fails, or if the `stdClass`
70 /// class entry has not been registered with PHP yet.
71 ///
72 /// # Example
73 ///
74 /// ```no_run
75 /// use ext_php_rs::types::ZendObject;
76 ///
77 /// let mut obj = ZendObject::new_stdclass();
78 ///
79 /// obj.set_property("hello", "world");
80 /// ```
81 #[must_use]
82 pub fn new_stdclass() -> ZBox<Self> {
83 // SAFETY: This will be `NULL` until it is initialized. `as_ref()` checks for
84 // null, so we can panic if it's null.
85 Self::new(ce::stdclass())
86 }
87
88 /// Converts a class object into an owned [`ZendObject`]. This removes any
89 /// possibility of accessing the underlying attached Rust struct.
90 #[must_use]
91 pub fn from_class_object<T: RegisteredClass>(obj: ZBox<ZendClassObject<T>>) -> ZBox<Self> {
92 let this = obj.into_raw();
93 // SAFETY: Consumed box must produce a well-aligned non-null pointer.
94 unsafe { ZBox::from_raw(this.get_mut_zend_obj()) }
95 }
96
97 /// Returns the [`ClassEntry`] associated with this object.
98 ///
99 /// # Panics
100 ///
101 /// Panics if the class entry is invalid.
102 #[must_use]
103 pub fn get_class_entry(&self) -> &'static ClassEntry {
104 // SAFETY: it is OK to panic here since PHP would segfault anyway
105 // when encountering an object with no class entry.
106 unsafe { self.ce.as_ref() }.expect("Could not retrieve class entry.")
107 }
108
109 /// Attempts to retrieve the class name of the object.
110 ///
111 /// # Errors
112 ///
113 /// * `Error::InvalidScope` - If the object handlers or the class name
114 /// cannot be retrieved.
115 pub fn get_class_name(&self) -> Result<String> {
116 unsafe {
117 self.handlers()?
118 .get_class_name
119 .and_then(|f| f(self).as_ref())
120 .ok_or(Error::InvalidScope)
121 .and_then(TryInto::try_into)
122 }
123 }
124
125 /// Returns whether this object is an instance of the given [`ClassEntry`].
126 ///
127 /// This method checks the class and interface inheritance chain.
128 ///
129 /// # Panics
130 ///
131 /// Panics if the class entry is invalid.
132 #[must_use]
133 pub fn instance_of(&self, ce: &ClassEntry) -> bool {
134 self.get_class_entry().instance_of(ce)
135 }
136
137 /// Checks if the given object is an instance of a registered class with
138 /// Rust type `T`.
139 ///
140 /// This method doesn't check the class and interface inheritance chain.
141 #[must_use]
142 pub fn is_instance<T: RegisteredClass>(&self) -> bool {
143 (self.ce.cast_const()).eq(&ptr::from_ref(T::get_metadata().ce()))
144 }
145
146 /// Returns whether this object is an instance of \Traversable
147 ///
148 /// # Panics
149 ///
150 /// Panics if the class entry is invalid.
151 #[must_use]
152 pub fn is_traversable(&self) -> bool {
153 self.instance_of(ce::traversable())
154 }
155
156 /// Tries to call a method on the object.
157 ///
158 /// # Returns
159 ///
160 /// Returns the return value of the method, or an error if the method
161 /// could not be found or called.
162 ///
163 /// # Errors
164 ///
165 /// * `Error::Callable` - If the method could not be found.
166 /// * If a parameter could not be converted to a zval.
167 /// * If the parameter count is bigger than `u32::MAX`.
168 // TODO: Measure this
169 #[allow(clippy::inline_always)]
170 #[inline(always)]
171 pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
172 let mut retval = Zval::new();
173 let len = params.len();
174 let params = params
175 .into_iter()
176 .map(|val| val.as_zval(false))
177 .collect::<Result<Vec<_>>>()?;
178 let packed = params.into_boxed_slice();
179
180 unsafe {
181 let res = zend_hash_str_find_ptr_lc(
182 &raw const (*self.ce).function_table,
183 name.as_ptr().cast::<c_char>(),
184 name.len(),
185 )
186 .cast::<zend_function>();
187
188 if res.is_null() {
189 return Err(Error::Callable);
190 }
191
192 zend_call_known_function(
193 res,
194 ptr::from_ref(self).cast_mut(),
195 self.ce,
196 &raw mut retval,
197 len.try_into()?,
198 packed.as_ptr().cast_mut(),
199 std::ptr::null_mut(),
200 );
201 };
202
203 Ok(retval)
204 }
205
206 /// Attempts to read a property from the Object. Returns a result containing
207 /// the value of the property if it exists and can be read, and an
208 /// [`Error`] otherwise.
209 ///
210 /// # Parameters
211 ///
212 /// * `name` - The name of the property.
213 /// * `query` - The type of query to use when attempting to get a property.
214 ///
215 /// # Errors
216 ///
217 /// * `Error::InvalidScope` - If the object handlers or the properties
218 /// cannot be retrieved.
219 pub fn get_property<'a, T>(&'a self, name: &str) -> Result<T>
220 where
221 T: FromZval<'a>,
222 {
223 if !self.has_property(name, PropertyQuery::Exists)? {
224 return Err(Error::InvalidProperty);
225 }
226
227 let mut name = ZendStr::new(name, false);
228 let mut rv = Zval::new();
229
230 let zv = unsafe {
231 self.handlers()?.read_property.ok_or(Error::InvalidScope)?(
232 self.mut_ptr(),
233 &raw mut *name,
234 1,
235 ptr::null_mut(),
236 &raw mut rv,
237 )
238 .as_ref()
239 }
240 .ok_or(Error::InvalidScope)?;
241
242 T::from_zval(zv).ok_or_else(|| Error::ZvalConversion(zv.get_type()))
243 }
244
245 /// Attempts to set a property on the object.
246 ///
247 /// # Parameters
248 ///
249 /// * `name` - The name of the property.
250 /// * `value` - The value to set the property to.
251 ///
252 /// # Errors
253 ///
254 /// * `Error::InvalidScope` - If the object handlers or the properties
255 /// cannot be retrieved.
256 pub fn set_property(&mut self, name: &str, value: impl IntoZval) -> Result<()> {
257 let mut name = ZendStr::new(name, false);
258 let mut value = value.into_zval(false)?;
259
260 unsafe {
261 self.handlers()?.write_property.ok_or(Error::InvalidScope)?(
262 self,
263 &raw mut *name,
264 &raw mut value,
265 ptr::null_mut(),
266 )
267 .as_ref()
268 }
269 .ok_or(Error::InvalidScope)?;
270 Ok(())
271 }
272
273 /// Checks if a property exists on an object. Takes a property name and
274 /// query parameter, which defines what classifies if a property exists
275 /// or not. See [`PropertyQuery`] for more information.
276 ///
277 /// # Parameters
278 ///
279 /// * `name` - The name of the property.
280 /// * `query` - The 'query' to classify if a property exists.
281 ///
282 /// # Errors
283 ///
284 /// * `Error::InvalidScope` - If the object handlers or the properties
285 /// cannot be retrieved.
286 pub fn has_property(&self, name: &str, query: PropertyQuery) -> Result<bool> {
287 let mut name = ZendStr::new(name, false);
288
289 Ok(unsafe {
290 self.handlers()?.has_property.ok_or(Error::InvalidScope)?(
291 self.mut_ptr(),
292 &raw mut *name,
293 query as _,
294 std::ptr::null_mut(),
295 )
296 } > 0)
297 }
298
299 /// Attempts to retrieve the properties of the object. Returned inside a
300 /// Zend Hashtable.
301 ///
302 /// # Errors
303 ///
304 /// * `Error::InvalidScope` - If the object handlers or the properties
305 /// cannot be retrieved.
306 pub fn get_properties(&self) -> Result<&HashTable> {
307 unsafe {
308 self.handlers()?
309 .get_properties
310 .and_then(|props| props(self.mut_ptr()).as_ref())
311 .ok_or(Error::InvalidScope)
312 }
313 }
314
315 /// Extracts some type from a Zend object.
316 ///
317 /// This is a wrapper function around `FromZendObject::extract()`.
318 ///
319 /// # Errors
320 ///
321 /// Returns an error if the conversion fails.
322 pub fn extract<'a, T>(&'a self) -> Result<T>
323 where
324 T: FromZendObject<'a>,
325 {
326 T::from_zend_object(self)
327 }
328
329 /// Returns an unique identifier for the object.
330 ///
331 /// The id is guaranteed to be unique for the lifetime of the object.
332 /// Once the object is destroyed, it may be reused for other objects.
333 /// This is equivalent to calling the [`spl_object_id`] PHP function.
334 ///
335 /// [`spl_object_id`]: https://www.php.net/manual/function.spl-object-id
336 #[inline]
337 #[must_use]
338 pub fn get_id(&self) -> u32 {
339 self.handle
340 }
341
342 /// Computes an unique hash for the object.
343 ///
344 /// The hash is guaranteed to be unique for the lifetime of the object.
345 /// Once the object is destroyed, it may be reused for other objects.
346 /// This is equivalent to calling the [`spl_object_hash`] PHP function.
347 ///
348 /// [`spl_object_hash`]: https://www.php.net/manual/function.spl-object-hash.php
349 #[must_use]
350 pub fn hash(&self) -> String {
351 format!("{:016x}0000000000000000", self.handle)
352 }
353
354 /// Attempts to retrieve a reference to the object handlers.
355 #[inline]
356 unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> {
357 self.handlers.as_ref().ok_or(Error::InvalidScope)
358 }
359
360 /// Returns a mutable pointer to `self`, regardless of the type of
361 /// reference. Only to be used in situations where a C function requires
362 /// a mutable pointer but does not modify the underlying data.
363 #[inline]
364 fn mut_ptr(&self) -> *mut Self {
365 ptr::from_ref(self).cast_mut()
366 }
367}
368
369unsafe impl ZBoxable for ZendObject {
370 fn free(&mut self) {
371 unsafe { ext_php_rs_zend_object_release(self) }
372 }
373}
374
375impl Debug for ZendObject {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 let mut dbg = f.debug_struct(
378 self.get_class_name()
379 .unwrap_or_else(|_| "ZendObject".to_string())
380 .as_str(),
381 );
382
383 if let Ok(props) = self.get_properties() {
384 for (key, val) in props {
385 dbg.field(key.to_string().as_str(), val);
386 }
387 }
388
389 dbg.finish()
390 }
391}
392
393impl<'a> FromZval<'a> for &'a ZendObject {
394 const TYPE: DataType = DataType::Object(None);
395
396 fn from_zval(zval: &'a Zval) -> Option<Self> {
397 zval.object()
398 }
399}
400
401impl<'a> FromZvalMut<'a> for &'a mut ZendObject {
402 const TYPE: DataType = DataType::Object(None);
403
404 fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
405 zval.object_mut()
406 }
407}
408
409impl IntoZval for ZBox<ZendObject> {
410 const TYPE: DataType = DataType::Object(None);
411 const NULLABLE: bool = false;
412
413 #[inline]
414 fn set_zval(mut self, zv: &mut Zval, _: bool) -> Result<()> {
415 // We must decrement the refcounter on the object before inserting into the
416 // zval, as the reference counter will be incremented on add.
417 // NOTE(david): again is this needed, we increment in `set_object`.
418 self.dec_count();
419 zv.set_object(self.into_raw());
420 Ok(())
421 }
422}
423
424impl IntoZval for &mut ZendObject {
425 const TYPE: DataType = DataType::Object(None);
426 const NULLABLE: bool = false;
427
428 #[inline]
429 fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
430 zv.set_object(self);
431 Ok(())
432 }
433}
434
435impl FromZendObject<'_> for String {
436 fn from_zend_object(obj: &ZendObject) -> Result<Self> {
437 let mut ret = Zval::new();
438 unsafe {
439 zend_call_known_function(
440 (*obj.ce).__tostring,
441 ptr::from_ref(obj).cast_mut(),
442 obj.ce,
443 &raw mut ret,
444 0,
445 ptr::null_mut(),
446 ptr::null_mut(),
447 );
448 }
449
450 if let Some(err) = ExecutorGlobals::take_exception() {
451 // TODO: become an error
452 let class_name = obj.get_class_name();
453 panic!(
454 "Uncaught exception during call to {}::__toString(): {:?}",
455 class_name.expect("unable to determine class name"),
456 err
457 );
458 } else if let Some(output) = ret.extract() {
459 Ok(output)
460 } else {
461 // TODO: become an error
462 let class_name = obj.get_class_name();
463 panic!(
464 "{}::__toString() must return a string",
465 class_name.expect("unable to determine class name"),
466 );
467 }
468 }
469}
470
471impl<T: RegisteredClass> From<ZBox<ZendClassObject<T>>> for ZBox<ZendObject> {
472 #[inline]
473 fn from(obj: ZBox<ZendClassObject<T>>) -> Self {
474 ZendObject::from_class_object(obj)
475 }
476}
477
478/// Different ways to query if a property exists.
479#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
480#[repr(u32)]
481pub enum PropertyQuery {
482 /// Property exists and is not NULL.
483 Isset = ZEND_PROPERTY_ISSET,
484 /// Property is not empty.
485 NotEmpty = ZEND_ISEMPTY,
486 /// Property exists.
487 Exists = ZEND_PROPERTY_EXISTS,
488}