diff --git a/libraries/src/Object/CMSDynamicObject.php b/libraries/src/Object/CMSDynamicObject.php new file mode 100644 index 0000000000000..d61dc0d0a70f9 --- /dev/null +++ b/libraries/src/Object/CMSDynamicObject.php @@ -0,0 +1,380 @@ +setCMSObjectBackwardsCompatibility(true). + * + * All flags and the error management are deprecated in Joomla 5.0 and will be removed in Joomla + * 7.0. + * + * @since __DEPLOY_VERSION__ + */ +class CMSDynamicObject extends \stdClass +{ + /** + * An array of error messages or Exception objects. + * + * @var array + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will always use exceptions + */ + // phpcs:disable PSR2.Classes.PropertyDeclaration + protected array $_errors = []; + // phpcs:enable PSR2.Classes.PropertyDeclaration + + /** + * Should I throw exceptions instead of setting the error messages internally? + * + * When enabled (default) setError will immediately throw a RuntimeException. The getErrors + * method always returns an empty array, and the getError method always returns boolean FALSE. + * + * When disabled, setError pushes the messages into an array. The getErrors method returns the + * contents of the errors array, and the getError method return the latest error message, or + * boolean FALSE when the errors array is empty. This is how CMSObject used to work. + * + * @var bool + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will always use exceptions + */ + // phpcs:disable PSR2.Classes.PropertyDeclaration + protected bool $_use_exceptions = true; + // phpcs:enable PSR2.Classes.PropertyDeclaration + + /** + * Should underscore prefixed properties be considered private? + * + * When disabled (default) only concrete properties' visibility is taken into account. You can + * only modify public concrete properties and all dynamic properties, regardless of their + * name. + * + * When enabled, only properties whose name is prefixed with an underscore are considered + * private. Everything else is considered public and becomes user-accessible, regardless of the + * visibility of a concrete property by that name. This is how CMSObject used to work. + * + * @var bool + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will only consider member visibility + */ + // phpcs:disable PSR2.Classes.PropertyDeclaration + protected bool $_underscore_private = false; + // phpcs:enable PSR2.Classes.PropertyDeclaration + + /** + * Should I allow getting and setting private properties? + * + * When disabled (default) you cannot get or set private properties. + * + * When enabled, you can get and set private properties, even if they are concrete properties + * with a protected or private visibility. + * + * @var bool + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will disallow direct access to non-public properties + */ + // phpcs:disable PSR2.Classes.PropertyDeclaration + protected bool $_access_private = false; + // phpcs:enable PSR2.Classes.PropertyDeclaration + + /** + * Class constructor, overridden in descendent classes. + * + * @param array|object|null $properties An associative array or another object to set the + * initial properties of the object. + * + * @since __DEPLOY_VERSION__ + */ + public function __construct($properties = null, bool $cmsObjectCompatibility = false) + { + $this->setCMSObjectBackwardsCompatibility($cmsObjectCompatibility); + + if ($properties !== null) { + $this->setProperties($properties); + } + } + + /** + * Sets a default value if not already assigned i.e. if it's not NULL + * + * @param string $property The name of the property. + * @param mixed|null $default The default value. + * + * @return mixed The previous value of this property + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Use has(), get() and set() instead. + */ + public function def($property, $default = null) + { + $value = $this->get($property, $default); + + return $this->set($property, $value); + } + + /** + * Returns a property of the object or the default value if the property is not set. + * + * @param string $property The name of the property. + * @param mixed $default The default value. + * + * @return mixed The value of the property. + * @throws \OutOfBoundsException If the property is not public + * + * @since __DEPLOY_VERSION__ + * + * @see self::getProperties() + */ + public function get($property, $default = null) + { + if (!isset($this->$property)) { + return $default; + } + + if ($this->_access_private) { + return $this->$property; + } + + if (!array_key_exists($property, $this->getConcretePublicProperties())) { + throw new \OutOfBoundsException( + 'Direct access to non-public properties is not allowed' + ); + } + + return $this->$property; + } + + /** + * Modifies a property of the object, creating it if it does not already exist. + * + * @param string $property The name of the property. + * @param mixed $value The value of the property to set. + * + * @return mixed Previous value of the property, NULL if it did not exist + * @throws \OutOfBoundsException If the property is not public + * + * @since __DEPLOY_VERSION__ + */ + public function set($property, $value = null) + { + if ( + !$this->_access_private + && isset($this->$property) + && !array_key_exists($property, $this->getConcretePublicProperties()) + ) { + throw new \OutOfBoundsException( + 'Direct access to non-public properties is not allowed' + ); + } + + $previous = $this->$property ?? null; + $this->$property = $value; + + return $previous; + } + + /** + * Set the object properties based on a named array/hash. + * + * When $this->_use_exceptions is false (default) the return value is always true. If you pass a + * parameter which is neither an array nor an object you will get a TypeError exception. + * + * When $this->_use_exceptions is true (CMSObject b/c mode) the return value is true, unless you + * pass a parameter which is neither an array nor an object in which case you get false. + * + * @param array|object $properties Either an associative array or another object. + * + * @return boolean True on success, false when $properties is neither an array nor an object. + * + * @since __DEPLOY_VERSION__ + * + * @see self::set() + */ + public function setProperties($properties) + { + if (!is_array($properties) && !is_object($properties)) { + if ($this->_use_exceptions) { + throw new \TypeError( + sprintf( + 'The parameter to %s must be an array or an object, %s given', + __METHOD__, + get_debug_type($properties) + ) + ); + } + + return false; + } + + foreach ((array)$properties as $k => $v) { + $this->set($k, $v); + } + + return true; + } + + /** + * Returns an associative array of object properties. + * + * @param boolean $public If true, returns only the dynamic and concrete public properties. + * + * @return array + * + * @since __DEPLOY_VERSION__ + * + * @see self::get() + */ + public function getProperties($public = true) + { + return $public + ? $this->getConcretePublicProperties() + : get_object_vars($this); + } + + /** + * Get the most recent error message. + * + * @param int|null $i Option error index. + * @param bool $toString Indicates if Exception objects should return their error + * message. + * + * @return string|false Error message or FALSE if there is none + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will always use exceptions + */ + public function getError($i = null, $toString = true) + { + if ($this->_use_exceptions) { + return false; + } + + // Find the error + if ($i === null) { + // Default, return the last message + $error = end($this->_errors); + } elseif (!\array_key_exists($i, $this->_errors)) { + // If $i has been specified but does not exist, return false + return false; + } else { + $error = $this->_errors[$i]; + } + + // Check if only the string is requested + if ($error instanceof \Exception && $toString) { + return $error->getMessage(); + } + + return $error; + } + + /** + * Return all errors, if any. + * + * @return array Array of error messages. + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will always use exceptions + */ + public function getErrors() + { + if ($this->_use_exceptions) { + return []; + } + + return $this->_errors; + } + + /** + * Add an error message. + * + * @param string $error Error message. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will always use exceptions + */ + public function setError($error) + { + if ($this->_use_exceptions) { + throw new \RuntimeException($error); + } + + $this->_errors[] = $error; + } + + /** + * Should I enable the backwards compatibility with CMSObject? + * + * When this is enabled properties with an underscore prefix are considered 'private'. Moreover, + * get() and set() allow you to access the values of these pseudo-'private' properties, be they + * concrete or dynamic. + * + * Furthermore, the legacy error handling is used instead of exceptions. + * + * @param bool $enableCompatibilty Enable backwards compatibility with CMSObject? + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 + */ + protected function setCMSObjectBackwardsCompatibility(bool $enableCompatibilty): void + { + $this->_underscore_private = $enableCompatibilty; + $this->_access_private = $enableCompatibilty; + $this->_use_exceptions = !$enableCompatibilty; + } + + /** + * Get the concrete properties which are considered "public" (user-accessible). + * + * The behavior of this method depends on the $this->_underscore_private flag. + * + * @return array + * + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 + */ + private function getConcretePublicProperties(): array + { + if ($this->_underscore_private) { + return array_filter( + get_object_vars($this), + fn($key) => !str_starts_with($key, '_'), + ARRAY_FILTER_USE_KEY + ); + } + + return array_filter( + get_mangled_object_vars($this), + fn($key) => !str_starts_with($key, "\0"), + ARRAY_FILTER_USE_KEY + ); + } +} diff --git a/libraries/src/Object/CMSObject.php b/libraries/src/Object/CMSObject.php index 89b3bf7b44ec5..c8b47383bf47e 100644 --- a/libraries/src/Object/CMSObject.php +++ b/libraries/src/Object/CMSObject.php @@ -16,215 +16,79 @@ /** * Joomla Platform Object Class * - * This class allows for simple but smart objects with get and set methods - * and an internal error handler. + * This class allows for simple but smart objects with get and set methods and an internal error + * handler. * * @since 1.7.0 - * @deprecated 4.0.0 Use \stdClass or \Joomla\Registry\Registry instead. + * @deprecated 4.0.0 Use \stdClass, \Joomla\CMS\Object\CMSDynamicObject or \Joomla\Registry\Registry instead. */ -class CMSObject +class CMSObject extends CMSDynamicObject { /** - * An array of error messages or Exception objects. + * Should I throw exceptions instead of setting the error messages internally? * - * @var array - * @since 1.7.0 - * @deprecated 3.1.4 JError has been deprecated - */ - protected $_errors = array(); - - /** - * Class constructor, overridden in descendant classes. - * - * @param mixed $properties Either and associative array or another - * object to set the initial properties of the object. - * - * @since 1.7.0 - */ - public function __construct($properties = null) - { - if ($properties !== null) { - $this->setProperties($properties); - } - } - - /** - * Magic method to convert the object to a string gracefully. - * - * @return string The classname. - * - * @since 1.7.0 - * @deprecated 3.1.4 Classes should provide their own __toString() implementation. - */ - public function __toString() - { - return \get_class($this); - } - - /** - * Sets a default value if not already assigned - * - * @param string $property The name of the property. - * @param mixed $default The default value. - * - * @return mixed - * - * @since 1.7.0 - */ - public function def($property, $default = null) - { - $value = $this->get($property, $default); - - return $this->set($property, $value); - } - - /** - * Returns a property of the object or the default value if the property is not set. - * - * @param string $property The name of the property. - * @param mixed $default The default value. - * - * @return mixed The value of the property. - * - * @since 1.7.0 - * - * @see CMSObject::getProperties() - */ - public function get($property, $default = null) - { - if (isset($this->$property)) { - return $this->$property; - } - - return $default; - } - - /** - * Returns an associative array of object properties. - * - * @param boolean $public If true, returns only the public properties. - * - * @return array + * Overrides the parent class' flag value to provide b/c with CMSObject. * - * @since 1.7.0 - * - * @see CMSObject::get() + * @var bool + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will always use exceptions + * @see CMSDynamicObject::$_use_exceptions */ - public function getProperties($public = true) - { - $vars = get_object_vars($this); - - if ($public) { - foreach ($vars as $key => $value) { - if ('_' == substr($key, 0, 1)) { - unset($vars[$key]); - } - } - } - - return $vars; - } + // phpcs:disable PSR2.Classes.PropertyDeclaration + protected bool $_use_exceptions = false; + // phpcs:enable PSR2.Classes.PropertyDeclaration /** - * Get the most recent error message. + * Should underscore prefixed properties be considered private? * - * @param integer $i Option error index. - * @param boolean $toString Indicates if Exception objects should return their error message. + * Overrides the parent class' flag value to provide b/c with CMSObject. * - * @return string Error message - * - * @since 1.7.0 - * @deprecated 3.1.4 JError has been deprecated + * @var bool + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will only consider member visibility + * @see CMSDynamicObject::$_underscore_private */ - public function getError($i = null, $toString = true) - { - // Find the error - if ($i === null) { - // Default, return the last message - $error = end($this->_errors); - } elseif (!\array_key_exists($i, $this->_errors)) { - // If $i has been specified but does not exist, return false - return false; - } else { - $error = $this->_errors[$i]; - } - - // Check if only the string is requested - if ($error instanceof \Exception && $toString) { - return $error->getMessage(); - } - - return $error; - } + // phpcs:disable PSR2.Classes.PropertyDeclaration + protected bool $_underscore_private = true; + // phpcs:enable PSR2.Classes.PropertyDeclaration /** - * Return all errors, if any. + * Should I allow getting and setting private properties? * - * @return array Array of error messages. + * Overrides the parent class' flag value to provide b/c with CMSObject. * - * @since 1.7.0 - * @deprecated 3.1.4 JError has been deprecated + * @var bool + * @since __DEPLOY_VERSION__ + * @deprecated 7.0 Joomla 7.0 and later will disallow direct access to non-public properties + * @see CMSDynamicObject::$_access_private */ - public function getErrors() - { - return $this->_errors; - } + // phpcs:disable PSR2.Classes.PropertyDeclaration + protected bool $_access_private = true; + // phpcs:enable PSR2.Classes.PropertyDeclaration /** - * Modifies a property of the object, creating it if it does not already exist. - * - * @param string $property The name of the property. - * @param mixed $value The value of the property to set. - * - * @return mixed Previous value of the property. - * - * @since 1.7.0 - */ - public function set($property, $value = null) - { - $previous = $this->$property ?? null; - $this->$property = $value; - - return $previous; - } - - /** - * Set the object properties based on a named array/hash. - * - * @param mixed $properties Either an associative array or another object. + * Class constructor, overridden in descendant classes. * - * @return boolean + * @param mixed $properties Either and associative array or another + * object to set the initial properties of the object. * * @since 1.7.0 - * - * @see CMSObject::set() */ - public function setProperties($properties) + public function __construct($properties = null) { - if (\is_array($properties) || \is_object($properties)) { - foreach ((array) $properties as $k => $v) { - // Use the set function which might be overridden. - $this->set($k, $v); - } - - return true; - } - - return false; + parent::__construct($properties, true); } /** - * Add an error message. - * - * @param string $error Error message. + * Magic method to convert the object to a string gracefully. * - * @return void + * @return string The classname. * * @since 1.7.0 - * @deprecated 3.1.4 JError has been deprecated + * @deprecated 3.1.4 Classes should provide their own __toString() implementation. */ - public function setError($error) + public function __toString() { - $this->_errors[] = $error; + return \get_class($this); } } diff --git a/tests/Unit/Libraries/Cms/Object/CMSDynamicObjectTest.php b/tests/Unit/Libraries/Cms/Object/CMSDynamicObjectTest.php new file mode 100644 index 0000000000000..f597fa602caa9 --- /dev/null +++ b/tests/Unit/Libraries/Cms/Object/CMSDynamicObjectTest.php @@ -0,0 +1,485 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Tests\Unit\Libraries\Cms\Object; + +use Exception; +use Joomla\CMS\Object\CMSDynamicObject; +use Joomla\Tests\Unit\UnitTestCase; + +/** + * Test class for \Joomla\CMS\Object\CMSDynbamicObjectTest + * + * @package Joomla.UnitTest + * @subpackage Object + * @since __DEPLOY_VERSION__ + */ +// phpcs:disable PSR1.Classes.ClassDeclaration +class CMSDynamicObjectTest extends UnitTestCase +{ + /** + * Tests the object constructor. + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::__construct + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testIsConstructable() + { + $object = new CMSDynamicObject(['property1' => 'value1', 'property2' => 5]); + + $this->assertEquals('value1', $object->get('property1')); + } + + /** + * Tests setting the default for a property of the object. + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::def + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testDef() + { + $object = new CMSDynamicObject(); + + $object->def("check"); + $this->assertEquals(null, $object->def("check")); + $object->def("check", "paint"); + $object->def("check", "forced"); + $this->assertEquals("paint", $object->def("check")); + $this->assertNotEquals("forced", $object->def("check")); + } + + /** + * Tests getting a property of the object. + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::get + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetViaGet() + { + $object = new CMSDynamicObject(); + + $object->goo = 'car'; + $object->set('baz', 'bat'); + + $this->assertEquals('car', $object->get('goo', 'fudge')); + $this->assertEquals('bat', $object->get('baz', 'invalid')); + $this->assertEquals('fudge', $object->get('foo', 'fudge')); + $this->assertNotEquals(null, $object->get('foo', 'fudge')); + $this->assertNull($object->get('boo')); + } + + /** + * Tests getting a property of the object. + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::set + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetViaProperty() + { + $object = new CMSDynamicObject(); + + $object->goo = 'car'; + $object->set('baz', 'bat'); + + $this->assertEquals('car', $object->goo); + $this->assertEquals('bat', $object->baz); + $this->assertEquals(null, $object->foo ?? null); + $this->assertEquals('fudge', $object->foo ?? 'fudge'); + $this->assertNull($object->boo ?? null); + } + + /** + * Tests how a dynamically assigned array property behaves + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function testArrayAccess() + { + $object = new CMSDynamicObject(); + + $object->foo = []; + + $this->assertIsArray($object->foo); + + $object->foo['bar'] = 'baz'; + + $this->assertArrayHasKey('bar', $object->foo); + $this->assertEquals('baz', $object->foo['bar']); + } + + /** + * Tests getting the properties of the object. + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::getProperties + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetProperties() + { + $object = new CMSDynamicObject([ + '_privateproperty1' => 'valuep1', + 'property1' => 'value1', + 'property2' => 5 + ]); + + $this->assertEquals( + [ + '_errors' => [], + '_privateproperty1' => 'valuep1', + 'property1' => 'value1', + 'property2' => 5, + '_use_exceptions' => true, + '_underscore_private' => false, + '_access_private' => false, + ], + $object->getProperties(false), + 'Should get all properties, including private ones' + ); + + $this->assertEquals( + [ + 'property1' => 'value1', + 'property2' => 5, + '_privateproperty1' => 'valuep1', + ], + $object->getProperties(), + 'Should get all public properties' + ); + } + + /** + * Tests setting a private property via the concrete set() method + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::set + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetPrivatePropertyViaSet() + { + $object = new CMSDynamicObject(); + + $this->expectException(\OutOfBoundsException::class); + $object->set('_use_exceptions', true); + } + + /** + * Tests setting a private property via def() + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::def + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetPrivatePropertyViaDef() + { + $object = new CMSDynamicObject(); + + $this->expectException(\OutOfBoundsException::class); + $object->def('_use_exceptions', true); + } + + /** + * Tests getting a private property via the concrete get() method + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::get + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetPrivatePropertyViaGet() + { + $object = new CMSDynamicObject(); + + $this->expectException(\OutOfBoundsException::class); + $x = $object->get('_use_exceptions'); + } + + /** + * Tests how PHP behaves when isset is called against dynamically created properties + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::get + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testIsset() + { + $object = new CMSDynamicObject(['foo' => 'bar', 'bar' => null]); + + $this->assertTrue(isset($object->foo)); + $this->assertFalse(isset($object->baz)); + + // PHP CAVEAT: a property with a NULL value returns FALSE when checking if it's set + $this->assertFalse(isset($object->bar)); + // However, you can check if it has a NULL value + $this->assertEquals(null, $object->bar); + } + + /** + * Tests how PHP's empty() language construct behaves with dynamic properties + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::__isset + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testEmpty() + { + $object = new CMSDynamicObject(['foo' => 'bar', 'bar' => null]); + + $this->assertFalse(empty($object->foo)); + $this->assertTrue(empty($object->bar)); + $this->assertTrue(empty($object->baz)); + } + + /** + * Tests getting a single error (CMSObject b/c mode). + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::getError + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetErrorLegacy() + { + $object = new CMSDynamicObject(null, true); + + $object->setError(1234); + $object->setError('Second Test Error'); + $object->setError('Third Test Error'); + + $this->assertEquals( + 1234, + $object->getError(0, false), + 'Should return the test error as number' + ); + + $this->assertEquals( + 'Second Test Error', + $object->getError(1), + 'Should return the second test error' + ); + $this->assertEquals( + 'Third Test Error', + $object->getError(), + 'Should return the third test error' + ); + + $this->assertFalse( + $object->getError(20), + 'Should return false, since the error does not exist' + ); + + $exception = new Exception('error'); + $object->setError($exception); + $this->assertThat( + $object->getError(3, true), + $this->equalTo('error') + ); + } + + /** + * Tests getting the array of errors (CMSObject b/c mode). + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::getErrors + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testGetErrorsLegacy() + { + $object = new CMSDynamicObject(null, true); + + $errors = [1234, 'Second Test Error', 'Third Test Error']; + + foreach ($errors as $error) { + $object->setError($error); + } + + $this->assertEquals( + $errors, + $object->getErrors(), + 'Should return every error set' + ); + } + + /** + * Tests setting a property. + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::set + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSet() + { + $object = new CMSDynamicObject(); + + $this->assertEquals(null, $object->set("foo", "imintheair")); + $this->assertEquals("imintheair", $object->set("foo", "nojibberjabber")); + $this->assertEquals("nojibberjabber", $object->foo); + } + + /** + * Tests setting a dynamic property prefixed by an underscore (modern mode). + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::set + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetUnderscore() + { + $object = new CMSDynamicObject(); + + $object->set('_jsonEncode', ['params']); + $this->assertIsArray($object->get('_jsonEncode')); + $this->assertEquals(['params'], $object->get('_jsonEncode')); + } + + /** + * Tests setting a dynamic property prefixed by an underscore (legacy mode). + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::set + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetUnderscoreLegacy() + { + $object = new CMSDynamicObject(null, true); + + $object->set('_jsonEncode', ['params']); + $this->assertIsArray($object->get('_jsonEncode')); + $this->assertEquals(['params'], $object->get('_jsonEncode')); + } + + /** + * Tests setting a concrete property prefixed by an underscore (legacy mode). + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::set + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetConcreteUnderscoreLegacy() + { + $object = new CMSDynamicObjectStub(null); + + $this->assertTrue($object->get('_jsonEncode') !== null); + + $object->set('_jsonEncode', ['params']); + + $this->assertIsArray($object->get('_jsonEncode')); + $this->assertEquals(['params'], $object->get('_jsonEncode')); + $this->assertFalse(empty($object->get('_jsonEncode'))); + $this->assertFalse($object->isJsonEncodeEmpty()); + } + + /** + * Tests setting multiple properties. + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::setProperties + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetProperties() + { + $object = new CMSDynamicObject(); + $a = ["foo" => "ghost", "knife" => "stewie"]; + $f = "foo"; + + $this->assertEquals(true, $object->setProperties($a)); + $this->assertEquals("ghost", $object->foo); + $this->assertEquals("stewie", $object->knife); + $this->expectException(\TypeError::class); + $object->setProperties($f); + } + + /** + * Tests setting an error (CMSObject b/c mode). + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::setError + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetErrorLegacy() + { + $object = new CMSDynamicObject(null, true); + $object->setError('A Test Error'); + $this->assertEquals( + ['A Test Error'], + $object->getErrors() + ); + } + + /** + * Tests setting an error (Exceptions mode). + * + * @group CMSDynamicObject + * @covers CMSDynamicObject::setError + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function testSetError() + { + $object = new CMSDynamicObject(); + $this->expectException(\RuntimeException::class); + $object->setError('A Test Error'); + } +} + +class CMSDynamicObjectStub extends CMSDynamicObject +{ + protected $_jsonEncode = []; + + public function __construct( + object|array|null $properties = null + ) { + parent::__construct($properties, true); + } + + public function isJsonEncodeEmpty() + { + return empty($this->_jsonEncode); + } +} diff --git a/tests/Unit/Libraries/Cms/Object/CMSObjectTest.php b/tests/Unit/Libraries/Cms/Object/CMSObjectTest.php index 8ee1f1a649a7f..2bccf292e8dfe 100644 --- a/tests/Unit/Libraries/Cms/Object/CMSObjectTest.php +++ b/tests/Unit/Libraries/Cms/Object/CMSObjectTest.php @@ -98,10 +98,13 @@ public function testGetProperties() $this->assertEquals( [ - '_errors' => [], - '_privateproperty1' => 'valuep1', - 'property1' => 'value1', - 'property2' => 5 + '_errors' => [], + '_privateproperty1' => 'valuep1', + 'property1' => 'value1', + 'property2' => 5, + '_use_exceptions' => false, + '_underscore_private' => true, + '_access_private' => true, ], $object->getProperties(false), 'Should get all properties, including private ones'