Skip to content

Commit 0c2be48

Browse files
authored
Merge pull request #65 from ingenerator/2.x-feat-rip-stdclass
bug: Fix `ObjectPropertyRipper` to handle `stdClass` objects
2 parents 5635024 + b73a201 commit 0c2be48

File tree

3 files changed

+75
-16
lines changed

3 files changed

+75
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
### Unreleased
22

3+
### v2.4.0 (2025-06-05)
4+
5+
* Fix `ObjectPropertyRipper` to handle `stdClass` objects
6+
37
### v2.3.1 (2025-03-12)
48

59
* Support option to customize EOL character in CSVWriter

src/Object/ObjectPropertyRipper.php

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ public static function ripAll(object $object): array
4949
// We also shouldn't cache, as individual objects may have variable field names (e.g. with public vars)
5050
// that are not present on other instances of the same class
5151

52-
$props = (\Closure::bind(
52+
$props = (self::bindScopedClosure(
5353
fn() => \get_object_vars($object),
54-
NULL,
55-
$object
54+
$object,
5655
))();
5756

5857
// Safety check - the method above is efficient but can't return private props from parent classes
@@ -92,21 +91,35 @@ public static function ripOne($object, $property)
9291
*/
9392
protected static function getRipper($class)
9493
{
95-
if ( ! isset(static::$rippers[$class])) {
96-
static::$rippers[$class] = \Closure::bind(
97-
function ($object, $properties) {
98-
$values = [];
99-
foreach ($properties as $property) {
100-
$values[$property] = $object->$property;
101-
}
94+
static::$rippers[$class] ??= self::bindScopedClosure(
95+
function ($object, $properties) {
96+
$values = [];
97+
foreach ($properties as $property) {
98+
$values[$property] = $object->$property;
99+
}
102100

103-
return $values;
104-
},
105-
NULL,
106-
$class
107-
);
108-
}
101+
return $values;
102+
},
103+
$class,
104+
);
109105

110106
return static::$rippers[$class];
111107
}
108+
109+
/**
110+
* @param object|class-string<object> $scope
111+
*/
112+
private static function bindScopedClosure(callable $callback, object|string $scope): \Closure
113+
{
114+
$scope_class = \is_object($scope) ? $scope::class : $scope;
115+
if ($scope_class === \stdClass::class) {
116+
// Cannot bind to the scope of an internal class (e.g. stdClass), and there is no need to do so since
117+
// all stdClass properties are public.
118+
// Note that this is the *not* the case for a user-defined class that extends stdClass, hence checking
119+
// for the exact class name rather than `instanceof`.
120+
$scope = null;
121+
}
122+
123+
return \Closure::bind($callback, newThis: null, newScope: $scope);
124+
}
112125
}

test/unit/Object/ObjectPropertyRipperTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use Ingenerator\PHPUtils\Object\ObjectPropertyRipper;
1111
use PHPUnit\Framework\TestCase;
12+
use stdClass;
1213

1314
class ObjectPropertyRipperTest extends TestCase
1415
{
@@ -44,6 +45,47 @@ public function test_it_rips_all_properties()
4445
);
4546
}
4647

48+
public function test_it_can_rip_from_stdclass()
49+
{
50+
$c = new stdClass();
51+
$c->data = 'something';
52+
$c->other = 1;
53+
54+
$this->assertSame(
55+
[
56+
'data' => 'something',
57+
'other' => 1,
58+
],
59+
ObjectPropertyRipper::ripAll($c),
60+
);
61+
62+
$this->assertSame('something', ObjectPropertyRipper::ripOne($c, 'data'));
63+
$this->assertSame(['other' => 1], ObjectPropertyRipper::rip($c, ['other']));
64+
}
65+
66+
public function test_it_can_rip_from_class_that_inherits_from_stdclass()
67+
{
68+
// This is an edge case and should be fairly unlikely IRL, but it is valid.
69+
70+
$c = new class extends stdClass {
71+
private string $hidden = 'whatever';
72+
};
73+
$c->data = 'something';
74+
$c->other = 1;
75+
76+
$this->assertSame(
77+
[
78+
'hidden' => 'whatever',
79+
'data' => 'something',
80+
'other' => 1,
81+
],
82+
ObjectPropertyRipper::ripAll($c),
83+
);
84+
85+
$this->assertSame('whatever', ObjectPropertyRipper::ripOne($c, 'hidden'));
86+
$this->assertSame(['hidden' => 'whatever', 'other' => 1], ObjectPropertyRipper::rip($c, ['hidden', 'other']));
87+
}
88+
4789
public function test_it_throws_if_ripping_all_from_an_object_with_private_parent_properties()
4890
{
4991
$this->expectException(\DomainException::class);

0 commit comments

Comments
 (0)