Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
380 changes: 380 additions & 0 deletions libraries/src/Object/CMSDynamicObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,380 @@
<?php

namespace Joomla\CMS\Object;

// phpcs:disable PSR1.Files.SideEffects
\defined('JPATH_PLATFORM') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
* Joomla Platform Object Class with Dynamic Property Allocation compatible with PHP 8.2+
*
* This class allows for simple but smart objects with get and set methods.
*
* It extends from stdClass to avoid problems in PHP 8.2 where creation of dynamic object properties
* is deprecated, and PHP 9.0 where creation of dynamic object properties is removed and causes a
* Fatal error exception. It is still compatible with PHP 8.0 and 8.1.
*
* This class also provides an internal error handler which is, however, deprecated.
*
* This is a drop-in replacement for the deprecated \Joomla\CMS\Object\CMSObject class with a few
* changes:
* - Using the underscore (`_`) prefix for "private" properties is not allowed. Use property
* visibility instead. Use _underscore_private to override.
* - getProperties(true) will return dynamic properties with underscore prefixes as they are no
* longer considered "private" (see above).
* - You cannot modify concrete non-public properties. Use _access_private to
* override.
* - Using setError throws a RuntimeException. Set _use_exceptions to false if you want to go back
* to the error messages stack behavior of CMSObject.
*
* All aforementioned flags can be set using $this->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
);
}
}
Loading