ext_php_rs/class.rs
1//! Types and traits used for registering classes with PHP.
2
3use std::{
4 marker::PhantomData,
5 sync::atomic::{AtomicPtr, Ordering},
6};
7
8use once_cell::sync::OnceCell;
9
10use crate::{
11 builders::{ClassBuilder, FunctionBuilder},
12 convert::IntoZvalDyn,
13 describe::DocComments,
14 exception::PhpException,
15 flags::{ClassFlags, MethodFlags, PropertyFlags},
16 internal::property::PropertyDescriptor,
17 zend::{ClassEntry, ExecuteData, ZendObjectHandlers},
18};
19
20/// A type alias for a tuple containing a function pointer to a class entry
21/// and a string representing the class name used in stubs.
22pub type ClassEntryInfo = (fn() -> &'static ClassEntry, &'static str);
23
24/// Implemented on Rust types which are exported to PHP. Allows users to get and
25/// set PHP properties on the object.
26pub trait RegisteredClass: Sized + 'static {
27 /// PHP class name of the registered class.
28 const CLASS_NAME: &'static str;
29
30 /// Function to be called when building the class. Allows user to modify the
31 /// class at runtime (add runtime constants etc).
32 const BUILDER_MODIFIER: Option<fn(ClassBuilder) -> ClassBuilder>;
33
34 /// Parent class entry. Optional.
35 const EXTENDS: Option<ClassEntryInfo>;
36
37 /// Interfaces implemented by the class.
38 const IMPLEMENTS: &'static [ClassEntryInfo];
39
40 /// PHP flags applied to the class.
41 const FLAGS: ClassFlags = ClassFlags::empty();
42
43 /// Doc comments for the class.
44 const DOC_COMMENTS: DocComments = &[];
45
46 /// Returns a reference to the class metadata, which stores the class entry,
47 /// handlers, and property descriptors.
48 ///
49 /// This must be statically allocated, and is usually done through the
50 /// [`macro@php_class`] macro.
51 ///
52 /// [`macro@php_class`]: crate::php_class
53 fn get_metadata() -> &'static ClassMetadata<Self>;
54
55 /// Returns method properties (from `#[php(getter)]`/`#[php(setter)]`).
56 /// Resolved via autoref specialization in the macro-generated code.
57 #[must_use]
58 fn method_properties() -> &'static [PropertyDescriptor<Self>] {
59 &[]
60 }
61
62 /// Returns the method builders required to build the class.
63 fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)>;
64
65 /// Returns the class constructor (if any).
66 fn constructor() -> Option<ConstructorMeta<Self>>;
67
68 /// Returns the constants provided by the class.
69 fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)];
70
71 /// Returns the static properties provided by the class.
72 ///
73 /// Static properties are declared at the class level and managed by PHP,
74 /// not by Rust handlers. Each tuple contains (name, flags, default, docs).
75 /// The default value is optional - `None` means null default.
76 #[must_use]
77 fn static_properties() -> &'static [(
78 &'static str,
79 PropertyFlags,
80 Option<&'static (dyn IntoZvalDyn + Sync)>,
81 DocComments,
82 )] {
83 &[]
84 }
85
86 /// Returns interfaces from `#[php_impl_interface]` trait implementations.
87 ///
88 /// This method is generated by the `#[php_class]` macro and uses the
89 /// `inventory` crate to collect interface registrations across crate
90 /// boundaries at link time.
91 ///
92 /// The default implementation returns an empty vector. The macro overrides
93 /// this to iterate over `InterfaceRegistration` entries matching this type.
94 #[must_use]
95 fn interface_implementations() -> Vec<ClassEntryInfo> {
96 Vec::new()
97 }
98
99 /// Returns methods from `#[php_impl_interface]` trait implementations.
100 ///
101 /// This method is generated by the `#[php_class]` macro and uses the
102 /// `inventory` crate to collect method registrations across crate
103 /// boundaries at link time.
104 ///
105 /// The default implementation returns an empty vector. The macro overrides
106 /// this to iterate over `MethodRegistration` entries matching this type.
107 #[must_use]
108 fn interface_method_implementations() -> Vec<(FunctionBuilder<'static>, MethodFlags)> {
109 Vec::new()
110 }
111
112 /// Returns a default instance of the class for immediate initialization.
113 ///
114 /// This is used when PHP creates an object without calling the constructor,
115 /// such as when throwing exceptions via `zend_throw_exception_ex`. For types
116 /// that derive `Default`, this will return `Some(Self::default())`, allowing
117 /// the object to be properly initialized even without a constructor call.
118 #[must_use]
119 fn default_init() -> Option<Self> {
120 None
121 }
122
123 /// Returns a clone of this object for PHP's `clone` operator.
124 ///
125 /// When PHP executes `clone $obj`, this method is called to produce a copy
126 /// of the Rust data. For types that derive `Clone`, the `#[php_class]` macro
127 /// generates an implementation returning `Some(self.clone())`. Types that
128 /// don't implement `Clone` use the default (returns `None`), which causes
129 /// PHP to throw an error when attempting to clone.
130 #[must_use]
131 fn clone_obj(&self) -> Option<Self> {
132 None
133 }
134}
135
136/// Stores metadata about a classes Rust constructor, including the function
137/// pointer and the arguments of the function.
138pub struct ConstructorMeta<T> {
139 /// Constructor function.
140 pub constructor: fn(&mut ExecuteData) -> ConstructorResult<T>,
141 /// Function called to build the constructor function. Usually adds
142 /// arguments.
143 pub build_fn: fn(FunctionBuilder) -> FunctionBuilder,
144 /// Add constructor modification
145 pub flags: Option<MethodFlags>,
146}
147
148/// Result returned from a constructor of a class.
149pub enum ConstructorResult<T> {
150 /// Successfully constructed the class, contains the new class object.
151 Ok(T),
152 /// An exception occurred while constructing the class.
153 Exception(PhpException),
154 /// Invalid arguments were given to the constructor.
155 ArgError,
156}
157
158impl<T, E> From<std::result::Result<T, E>> for ConstructorResult<T>
159where
160 E: Into<PhpException>,
161{
162 fn from(result: std::result::Result<T, E>) -> Self {
163 match result {
164 Ok(x) => Self::Ok(x),
165 Err(e) => Self::Exception(e.into()),
166 }
167 }
168}
169
170impl<T> From<T> for ConstructorResult<T> {
171 fn from(result: T) -> Self {
172 Self::Ok(result)
173 }
174}
175
176/// Stores the class entry, handlers, and property descriptors for a Rust type
177/// which has been exported to PHP. Usually allocated statically.
178pub struct ClassMetadata<T: 'static> {
179 handlers: OnceCell<ZendObjectHandlers>,
180 field_properties: &'static [PropertyDescriptor<T>],
181 method_properties: OnceCell<&'static [PropertyDescriptor<T>]>,
182 method_mangled_names: OnceCell<Box<[Box<str>]>>,
183 ce: AtomicPtr<ClassEntry>,
184
185 // `AtomicPtr` is used here because it is `Send + Sync`.
186 // fn() -> T could have been used but that is incompatible with const fns at
187 // the moment.
188 phantom: PhantomData<AtomicPtr<T>>,
189}
190
191impl<T: 'static> ClassMetadata<T> {
192 /// Creates a new class metadata instance with the given field properties.
193 #[must_use]
194 pub const fn new(field_properties: &'static [PropertyDescriptor<T>]) -> Self {
195 Self {
196 handlers: OnceCell::new(),
197 field_properties,
198 method_properties: OnceCell::new(),
199 method_mangled_names: OnceCell::new(),
200 ce: AtomicPtr::new(std::ptr::null_mut()),
201 phantom: PhantomData,
202 }
203 }
204}
205
206impl<T: 'static> Default for ClassMetadata<T> {
207 fn default() -> Self {
208 Self::new(&[])
209 }
210}
211
212impl<T: RegisteredClass> ClassMetadata<T> {
213 /// Returns an immutable reference to the object handlers contained inside
214 /// the class metadata.
215 pub fn handlers(&self) -> &ZendObjectHandlers {
216 self.handlers.get_or_init(ZendObjectHandlers::new::<T>)
217 }
218
219 /// Checks if the class entry has been stored, returning a boolean.
220 pub fn has_ce(&self) -> bool {
221 !self.ce.load(Ordering::SeqCst).is_null()
222 }
223
224 /// Retrieves a reference to the stored class entry.
225 ///
226 /// # Panics
227 ///
228 /// Panics if there is no class entry stored inside the class metadata.
229 pub fn ce(&self) -> &'static ClassEntry {
230 // SAFETY: There are only two values that can be stored in the atomic
231 // ptr: null or a static reference to a class entry. On the null case,
232 // `as_ref()` will return `None` and the function will panic.
233 unsafe { self.ce.load(Ordering::SeqCst).as_ref() }
234 .expect("Attempted to retrieve class entry before it has been stored.")
235 }
236
237 /// Stores a reference to a class entry inside the class metadata.
238 ///
239 /// # Panics
240 ///
241 /// Panics if the class entry has already been set in the class metadata.
242 /// This function should only be called once.
243 pub fn set_ce(&self, ce: &'static mut ClassEntry) {
244 self.ce
245 .compare_exchange(
246 std::ptr::null_mut(),
247 ce,
248 Ordering::SeqCst,
249 Ordering::Relaxed,
250 )
251 .expect("Class entry has already been set");
252 }
253
254 /// Finds a property descriptor by name.
255 ///
256 /// Checks field properties first, then method properties. Linear scan is
257 /// used since typical PHP classes have few properties and cache locality
258 /// beats hashing at small N.
259 #[must_use]
260 #[inline]
261 pub fn find_property(&self, name: &str) -> Option<&PropertyDescriptor<T>> {
262 self.field_properties
263 .iter()
264 .find(|p| p.name == name)
265 .or_else(|| self.method_properties().iter().find(|p| p.name == name))
266 }
267
268 /// Returns the field properties (from `#[php(prop)]` struct fields).
269 #[must_use]
270 #[inline]
271 pub fn field_properties(&self) -> &'static [PropertyDescriptor<T>] {
272 self.field_properties
273 }
274
275 /// Returns the method properties (from `#[php(getter)]`/`#[php(setter)]`).
276 /// Lazily initialized on first access from `RegisteredClass::method_properties()`.
277 #[must_use]
278 #[inline]
279 pub fn method_properties(&self) -> &'static [PropertyDescriptor<T>] {
280 self.method_properties.get_or_init(T::method_properties)
281 }
282
283 /// Iterates over all properties (field + method).
284 pub fn all_properties(&self) -> impl Iterator<Item = &PropertyDescriptor<T>> {
285 self.field_properties
286 .iter()
287 .chain(self.method_properties().iter())
288 }
289
290 /// Returns pre-computed PHP-convention mangled names for method properties.
291 ///
292 /// Lazily initialized on first access. One allocation per class for the
293 /// entire process lifetime. Field properties already carry compile-time
294 /// mangled names in their [`PropertyDescriptor::mangled_name`] field.
295 #[must_use]
296 #[inline]
297 pub fn method_mangled_names(&self) -> &[Box<str>] {
298 self.method_mangled_names.get_or_init(|| {
299 self.method_properties()
300 .iter()
301 .map(|desc| {
302 if desc.flags.contains(PropertyFlags::Private) {
303 format!("\0{}\0{}", T::CLASS_NAME, desc.name).into_boxed_str()
304 } else if desc.flags.contains(PropertyFlags::Protected) {
305 format!("\0*\0{}", desc.name).into_boxed_str()
306 } else {
307 desc.name.into()
308 }
309 })
310 .collect()
311 })
312 }
313}