Skip to content

Commit 5b746d5

Browse files
committed
feat(interfaces): added support for short form of php(implements("\\...")) macro
1 parent 6ce42d5 commit 5b746d5

File tree

6 files changed

+236
-14
lines changed

6 files changed

+236
-14
lines changed

crates/macros/src/class.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,13 @@ impl ToTokens for ClassEntryAttribute {
121121
ClassEntryAttribute::Name(name) => {
122122
// For a string literal, generate a function that looks up the class at runtime
123123
// Uses try_find_no_autoload to avoid triggering autoloading during MINIT
124+
// Strip leading backslash since PHP's class table stores names without it
125+
// Convert to lowercase since PHP's class table uses lowercase keys
126+
let lookup_name = name.strip_prefix('\\').unwrap_or(name).to_lowercase();
124127
quote! {
125128
(
126-
|| ::ext_php_rs::zend::ClassEntry::try_find_no_autoload(#name)
127-
.expect(concat!("Failed to find class entry for ", #name)),
129+
|| ::ext_php_rs::zend::ClassEntry::try_find_no_autoload(#lookup_name)
130+
.expect(concat!("Failed to find class entry for ", #lookup_name)),
128131
#name
129132
)
130133
}

src/zend/class.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,32 @@ impl ClassEntry {
3939
/// triggering autoloading.
4040
///
4141
/// This is useful during module initialization when autoloading might not be
42-
/// available or could cause issues.
42+
/// available or could cause issues. It uses the compiler globals class table
43+
/// which is available during MINIT.
4344
///
4445
/// Returns a reference to the class if found, or [`None`] if the class
4546
/// could not be found or the class table has not been initialized.
4647
#[must_use]
4748
pub fn try_find_no_autoload(name: &str) -> Option<&'static Self> {
48-
// ZEND_FETCH_CLASS_NO_AUTOLOAD = 0x80
49-
const ZEND_FETCH_CLASS_NO_AUTOLOAD: u32 = 0x80;
49+
use crate::types::ZendHashTable;
50+
use crate::zend::CompilerGlobals;
5051

51-
ExecutorGlobals::get().class_table()?;
52-
let mut name = ZendStr::new(name, false);
52+
let cg = CompilerGlobals::get();
53+
let class_table = cg.class_table()?;
5354

54-
unsafe {
55-
crate::ffi::zend_lookup_class_ex(
56-
&raw mut *name,
57-
ptr::null_mut(),
58-
ZEND_FETCH_CLASS_NO_AUTOLOAD,
55+
// zend_hash_str_find_ptr_lc handles lowercase conversion internally
56+
let ce_ptr = unsafe {
57+
crate::ffi::zend_hash_str_find_ptr_lc(
58+
ptr::from_ref::<ZendHashTable>(class_table).cast(),
59+
name.as_ptr().cast(),
60+
name.len(),
5961
)
60-
.as_ref()
62+
};
63+
64+
if ce_ptr.is_null() {
65+
None
66+
} else {
67+
unsafe { (ce_ptr as *const Self).as_ref() }
6168
}
6269
}
6370

src/zend/globals.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ impl ExecutorGlobals {
226226
}
227227
}
228228

229+
/// Stores global variables used in the PHP compiler.
229230
pub type CompilerGlobals = _zend_compiler_globals;
230231

231232
impl CompilerGlobals {
@@ -284,6 +285,15 @@ impl CompilerGlobals {
284285

285286
GlobalWriteGuard { globals, guard }
286287
}
288+
289+
/// Attempts to retrieve the global class hash table from compiler globals.
290+
///
291+
/// This is useful during module initialization (MINIT) when the executor
292+
/// globals class table may not be available yet.
293+
#[must_use]
294+
pub fn class_table(&self) -> Option<&ZendHashTable> {
295+
unsafe { self.class_table.as_ref() }
296+
}
287297
}
288298

289299
/// Stores the SAPI module used in the PHP executor.

src/zend/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub use ex::ExecuteData;
3838
pub use exception_observer::{ExceptionInfo, ExceptionObserver};
3939
pub use function::Function;
4040
pub use function::FunctionEntry;
41+
pub use globals::CompilerGlobals;
4142
pub use globals::ExecutorGlobals;
4243
pub use globals::FileGlobals;
4344
pub use globals::ProcessGlobals;

tests/src/integration/interface/interface.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,62 @@ function greetWithInterface(ExtPhpRs\Interface\ParentInterface $obj): string {
166166

167167
$result = greetWithInterface($greeter);
168168
assert($result === 'Hello from World!', 'parentMethod should work via interface type hint');
169+
170+
// ============================================================================
171+
// Test Feature 5: Short form implements syntax
172+
// Using #[php(implements("\\InterfaceName"))] instead of verbose form
173+
// ============================================================================
174+
175+
// Test ArrayAccess implementation using short form
176+
assert(class_exists('ExtPhpRs\Interface\ShortFormArrayAccess'), 'ShortFormArrayAccess should exist');
177+
178+
$arr = new ExtPhpRs\Interface\ShortFormArrayAccess();
179+
assert($arr instanceof ArrayAccess, 'ShortFormArrayAccess should implement ArrayAccess');
180+
181+
// Test ArrayAccess methods
182+
assert(isset($arr[0]), 'offset 0 should exist');
183+
assert(isset($arr[4]), 'offset 4 should exist');
184+
assert(!isset($arr[5]), 'offset 5 should not exist');
185+
assert($arr[0] === 10, 'offset 0 should be 10');
186+
assert($arr[2] === 30, 'offset 2 should be 30');
187+
188+
$arr[1] = 99;
189+
assert($arr[1] === 99, 'offset 1 should be 99 after set');
190+
191+
// Test Countable implementation using short form
192+
assert(class_exists('ExtPhpRs\Interface\CountableTest'), 'CountableTest should exist');
193+
194+
$countable = new ExtPhpRs\Interface\CountableTest();
195+
assert($countable instanceof Countable, 'CountableTest should implement Countable');
196+
197+
// Test count() function works
198+
assert(count($countable) === 0, 'Empty CountableTest should have count 0');
199+
200+
$countable->add('one');
201+
$countable->add('two');
202+
$countable->add('three');
203+
assert(count($countable) === 3, 'CountableTest with 3 items should have count 3');
204+
205+
// Test mixed implements syntax (explicit form + short form)
206+
assert(class_exists('ExtPhpRs\Interface\MixedImplementsTest'), 'MixedImplementsTest should exist');
207+
208+
$mixed = new ExtPhpRs\Interface\MixedImplementsTest();
209+
assert($mixed instanceof Iterator, 'MixedImplementsTest should implement Iterator (explicit form)');
210+
assert($mixed instanceof Countable, 'MixedImplementsTest should implement Countable (short form)');
211+
assert($mixed instanceof Traversable, 'MixedImplementsTest should implement Traversable');
212+
213+
// Test count() works
214+
assert(count($mixed) === 3, 'MixedImplementsTest should have count 3');
215+
216+
// Test iteration works
217+
$collected = [];
218+
foreach ($mixed as $key => $value) {
219+
$collected[$key] = $value;
220+
}
221+
assert($collected === [0 => 10, 1 => 20, 2 => 30], 'MixedImplementsTest should iterate correctly');
222+
223+
// Test reflection shows both interfaces
224+
$mixedReflection = new ReflectionClass(ExtPhpRs\Interface\MixedImplementsTest::class);
225+
$interfaces = $mixedReflection->getInterfaceNames();
226+
assert(in_array('Iterator', $interfaces), 'Reflection should show Iterator interface');
227+
assert(in_array('Countable', $interfaces), 'Reflection should show Countable interface');

tests/src/integration/interface/mod.rs

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use ext_php_rs::prelude::*;
2-
use ext_php_rs::types::ZendClassObject;
2+
use ext_php_rs::types::{ZendClassObject, Zval};
33
use ext_php_rs::zend::ce;
44

55
#[php_interface]
@@ -211,6 +211,144 @@ impl VecIterator {
211211
// You can use `#[php_impl_interface]` to implement interfaces defined in other crates.
212212
// See the `php_interface` and `php_impl_interface` macros for more details.
213213

214+
// ============================================================================
215+
// Test Feature 5: Short form implements syntax
216+
// Using #[php(implements("\\InterfaceName"))] instead of the verbose
217+
// #[php(implements(ce = ce::interface, stub = "\\InterfaceName"))]
218+
// ============================================================================
219+
220+
/// Test class implementing `ArrayAccess` using short form syntax.
221+
/// This tests runtime class entry lookup via `ClassEntry::try_find_no_autoload()`.
222+
#[php_class]
223+
#[php(name = "ExtPhpRs\\Interface\\ShortFormArrayAccess")]
224+
#[php(implements("\\ArrayAccess"))]
225+
pub struct ShortFormArrayAccess {
226+
data: Vec<i64>,
227+
}
228+
229+
#[php_impl]
230+
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
231+
impl ShortFormArrayAccess {
232+
pub fn __construct() -> Self {
233+
Self {
234+
data: vec![10, 20, 30, 40, 50],
235+
}
236+
}
237+
238+
/// `ArrayAccess::offsetExists` - offset must be `mixed` to match PHP interface.
239+
pub fn offset_exists(&self, offset: &Zval) -> bool {
240+
if let Some(idx) = offset.long() {
241+
idx >= 0 && (idx as usize) < self.data.len()
242+
} else {
243+
false
244+
}
245+
}
246+
247+
/// `ArrayAccess::offsetGet` - offset must be `mixed` to match PHP interface.
248+
pub fn offset_get(&self, offset: &Zval) -> Option<i64> {
249+
let idx = offset.long()?;
250+
if idx >= 0 {
251+
self.data.get(idx as usize).copied()
252+
} else {
253+
None
254+
}
255+
}
256+
257+
/// `ArrayAccess::offsetSet` - offset and value must be `mixed` to match PHP interface.
258+
pub fn offset_set(&mut self, offset: &Zval, value: &Zval) {
259+
if let (Some(idx), Some(val)) = (offset.long(), value.long())
260+
&& idx >= 0
261+
&& (idx as usize) < self.data.len()
262+
{
263+
self.data[idx as usize] = val;
264+
}
265+
}
266+
267+
/// `ArrayAccess::offsetUnset` - offset must be `mixed` to match PHP interface.
268+
pub fn offset_unset(&mut self, offset: &Zval) {
269+
if let Some(idx) = offset.long()
270+
&& idx >= 0
271+
&& (idx as usize) < self.data.len()
272+
{
273+
self.data.remove(idx as usize);
274+
}
275+
}
276+
}
277+
278+
/// Test class implementing `Countable` using short form syntax.
279+
/// `Countable` is defined in SPL which is always available.
280+
#[php_class]
281+
#[php(name = "ExtPhpRs\\Interface\\CountableTest")]
282+
#[php(implements("\\Countable"))]
283+
pub struct CountableTest {
284+
items: Vec<String>,
285+
}
286+
287+
#[php_impl]
288+
#[allow(clippy::cast_possible_wrap)]
289+
impl CountableTest {
290+
pub fn __construct() -> Self {
291+
Self { items: Vec::new() }
292+
}
293+
294+
pub fn add(&mut self, item: String) {
295+
self.items.push(item);
296+
}
297+
298+
/// Returns the count for `count()`.
299+
pub fn count(&self) -> i64 {
300+
self.items.len() as i64
301+
}
302+
}
303+
304+
/// Test class implementing multiple interfaces using mixed syntax
305+
/// (both short form and explicit form).
306+
#[php_class]
307+
#[php(name = "ExtPhpRs\\Interface\\MixedImplementsTest")]
308+
#[php(implements(ce = ce::iterator, stub = "\\Iterator"))]
309+
#[php(implements("\\Countable"))]
310+
pub struct MixedImplementsTest {
311+
items: Vec<i64>,
312+
index: usize,
313+
}
314+
315+
#[php_impl]
316+
impl MixedImplementsTest {
317+
pub fn __construct() -> Self {
318+
Self {
319+
items: vec![10, 20, 30],
320+
index: 0,
321+
}
322+
}
323+
324+
// Iterator methods
325+
pub fn current(&self) -> Option<i64> {
326+
self.items.get(self.index).copied()
327+
}
328+
329+
pub fn key(&self) -> usize {
330+
self.index
331+
}
332+
333+
pub fn next(&mut self) {
334+
self.index += 1;
335+
}
336+
337+
pub fn rewind(&mut self) {
338+
self.index = 0;
339+
}
340+
341+
pub fn valid(&self) -> bool {
342+
self.index < self.items.len()
343+
}
344+
345+
#[allow(clippy::cast_possible_wrap)]
346+
// Countable method
347+
pub fn count(&self) -> i64 {
348+
self.items.len() as i64
349+
}
350+
}
351+
214352
// Test Feature 2: Interface inheritance via trait bounds
215353
// Define a parent interface
216354
#[php_interface]
@@ -270,6 +408,10 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
270408
.class::<VecIterator>()
271409
// Greeter with #[php_impl_interface]
272410
.class::<Greeter>()
411+
// Short form implements syntax tests
412+
.class::<ShortFormArrayAccess>()
413+
.class::<CountableTest>()
414+
.class::<MixedImplementsTest>()
273415
}
274416

275417
#[cfg(test)]

0 commit comments

Comments
 (0)