--- /dev/null
+<?php
+
+namespace Civi\Test;
+
+/**
+ * The "Invasive" helper makes it a bit easier to write unit-tests which
+ * touch upon private or protected members.
+ *
+ * @package Civi\Test
+ */
+class Invasive {
+
+ /**
+ * Call a private/protected method.
+ *
+ * This is only intended for unit-testing.
+ *
+ * @param array $callable
+ * Ex: [$myObject, 'myPrivateMethod']
+ * Ex: ['MyClass', 'myPrivateStaticMethod']
+ * @param array $args
+ * Ordered list of arguments.
+ * @return mixed
+ */
+ public static function call($callable, $args = []) {
+ list ($class, $object, $member) = self::parseRef($callable);
+ $reflection = new \ReflectionMethod($class, $member);
+ $reflection->setAccessible(TRUE);
+ return $reflection->invokeArgs($object, $args);
+ }
+
+ /**
+ * Get the content of a private/protected method.
+ *
+ * This is only intended for unit-testing.
+ *
+ * @param array $ref
+ * A reference to class+property.
+ * Ex: [$myObject, 'myPrivateField']
+ * Ex: ['MyClass', 'myPrivateStaticField']
+ * @return mixed
+ */
+ public static function get($ref) {
+ list ($class, $object, $member) = self::parseRef($ref);
+ $reflection = new \ReflectionProperty($class, $member);
+ $reflection->setAccessible(TRUE);
+ return $reflection->getValue($object);
+ }
+
+ /**
+ * Get the content of a private/protected method.
+ *
+ * This is only intended for unit-testing.
+ *
+ * @param array $ref
+ * A reference to class+property.
+ * Ex: [$myObject, 'myPrivateField']
+ * Ex: ['MyClass', 'myPrivateStaticField']
+ * @param mixed $value
+ * @return mixed
+ */
+ public static function set($ref, $value) {
+ list ($class, $object, $member) = self::parseRef($ref);
+ $reflection = new \ReflectionProperty($class, $member);
+ $reflection->setAccessible(TRUE);
+ $reflection->setValue($object, $value);
+ }
+
+ /**
+ * @param array $callable
+ * Ex: [$myObject, 'myPrivateMember']
+ * Ex: ['MyClass', 'myPrivateStaticMember']
+ * @return array
+ * Ordered array of [string $class, object? $object, string $memberName].
+ */
+ private static function parseRef($callable) {
+ if (is_string($callable)) {
+ list ($class, $member) = explode('::', $callable);
+ return [$class, NULL, $member];
+ }
+ elseif (is_string($callable[0])) {
+ return [$callable[0], NULL, $callable[1]];
+ }
+ elseif (is_object($callable[0])) {
+ return [get_class($callable[0]), $callable[0], $callable[1]];
+ }
+ else {
+ throw new \RuntimeException("Cannot parse reference to private member");
+ }
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Test;
+
+/**
+ * Class InvasiveExample
+ *
+ * This is a dummy/placeholder for use with InvasiveTest.
+ *
+ * @package Civi\Test
+ */
+class InvasiveExample {
+
+ private $privateField = 10;
+
+ private static $protectedStaticField = 20;
+
+ /**
+ * @return int
+ */
+ private function getPrivateField(): int {
+ return $this->privateField;
+ }
+
+ /**
+ * @param int $privateField
+ */
+ private function setPrivateField(int $privateField) {
+ $this->privateField = $privateField;
+ }
+
+ private function twiddlePrivateField(&$output) {
+ $output = $this->privateField * 100;
+ return $this->privateField * 10000;
+ }
+
+ /**
+ * @return int
+ */
+ protected static function getProtectedStaticField(): int {
+ return self::$protectedStaticField;
+ }
+
+ /**
+ * @param int $protectedStaticField
+ */
+ protected static function setProtectedStaticField(int $protectedStaticField) {
+ self::$protectedStaticField = $protectedStaticField;
+ }
+
+}
--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Test;
+
+class InvasiveTest extends \CiviUnitTestCase {
+
+ public function testPrivate() {
+ $tgt = new InvasiveExample();
+ $this->assertEquals(10, Invasive::get([$tgt, 'privateField']));
+ $this->assertEquals(10, Invasive::call([$tgt, 'getPrivateField']));
+ Invasive::call([$tgt, 'setPrivateField'], [11]);
+ $this->assertEquals(11, Invasive::call([$tgt, 'getPrivateField']));
+
+ $theRef = NULL;
+ $this->assertEquals(110000, Invasive::call([$tgt, 'twiddlePrivateField'], [&$theRef]));
+ $this->assertEquals(1100, $theRef);
+ }
+
+ public function testProtectedStatic() {
+ $tgt = InvasiveExample::class;
+ $this->assertEquals(20, Invasive::get([$tgt, 'protectedStaticField']));
+ $this->assertEquals(20, Invasive::call([$tgt, 'getProtectedStaticField']));
+ Invasive::call([$tgt, 'setProtectedStaticField'], [21]);
+ $this->assertEquals(21, Invasive::call([$tgt, 'getProtectedStaticField']));
+ }
+
+}