Files
2024-12-17 17:34:10 +01:00

597 lines
14 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 2, or later
*/
namespace FOF30\Model;
defined('_JEXEC') || die;
use FOF30\Container\Container;
use FOF30\Input\Input;
use FOF30\Model\Exception\CannotGetName;
use Joomla\CMS\Filter\InputFilter;
use RuntimeException;
use stdClass;
/**
* Class Model
*
* A generic MVC model implementation
*
* @property-read Input $input The input object (magic __get returns the Input from the Container)
*/
class Model
{
/**
* Should I save the model's state in the session?
*
* @var boolean
*/
protected $_savestate = true;
/**
* Should we ignore request data when trying to get state data not already set in the Model?
*
* @var bool
*/
protected $_ignoreRequest = false;
/**
* The model (base) name
*
* @var string
*/
protected $name;
/**
* A state object
*
* @var string
*/
protected $state;
/**
* Are the state variables already set?
*
* @var boolean
*/
protected $_state_set = false;
/**
* The container attached to the model
*
* @var Container
*/
protected $container;
/**
* The state key hash returned by getHash(). This is typically something like "com_foobar.example." (note the dot
* at the end). Always use getHash to get it and setHash to set it.
*
* @var null|string
*/
private $stateHash = null;
/**
* Public class constructor
*
* You can use the $config array to pass some configuration values to the object:
*
* state stdClass|array. The state variables of the Model.
* use_populate Boolean. When true the model will set its state from populateState() instead of the request.
* ignore_request Boolean. When true getState will not automatically load state data from the request.
*
* @param Container $container The configuration variables to this model
* @param array $config Configuration values for this model
*/
public function __construct(Container $container, array $config = [])
{
$this->container = $container;
// Set the model's name from $config
if (isset($config['name']))
{
$this->name = $config['name'];
}
// If $config['name'] is not set, auto-detect the model's name
$this->name = $this->getName();
// Do we have a configured state hash? Since 3.1.2.
if (isset($config['hash']) && !empty($config['hash']))
{
$this->setHash($config['hash']);
}
elseif (isset($config['hash_view']) && !empty($config['hash_view']))
{
$this->getHash($config['hash_view']);
}
// Set the model state
if (array_key_exists('state', $config))
{
if (is_object($config['state']))
{
$this->state = $config['state'];
}
elseif (is_array($config['state']))
{
$this->state = (object) $config['state'];
}
// Protect vs malformed state
else
{
$this->state = new stdClass();
}
}
else
{
$this->state = new stdClass();
}
// Set the internal state marker
if (!empty($config['use_populate']))
{
$this->_state_set = true;
}
// Set the internal state marker
if (!empty($config['ignore_request']))
{
$this->_ignoreRequest = true;
}
}
/**
* Method to get the model name
*
* The model name. By default parsed using the classname or it can be set
* by passing a $config['name'] in the class constructor
*
* @return string The name of the model
*
* @throws RuntimeException If it's impossible to get the name
*/
public function getName()
{
if (empty($this->name))
{
$r = null;
if (!preg_match('/(.*)\\\\Model\\\\(.*)/i', get_class($this), $r))
{
throw new CannotGetName;
}
$this->name = $r[2];
}
return $this->name;
}
/**
* Get a filtered state variable
*
* @param string $key The state variable's name
* @param mixed $default The default value to return if it's not already set
* @param string $filter_type The filter type to use
*
* @return mixed The state variable's contents
*/
public function getState($key = null, $default = null, $filter_type = 'raw')
{
if (empty($key))
{
return $this->internal_getState();
}
// Get the savestate status
$value = $this->internal_getState($key);
// Value is not found in the internal state
if (is_null($value))
{
// Can I fetch it from the request?
if (!$this->_ignoreRequest)
{
$value = $this->container->platform->getUserStateFromRequest($this->getHash() . $key, $key, $this->input, $value, 'none', $this->_savestate);
// Did I get any useful value from the request?
if (is_null($value))
{
return $default;
}
}
// Nope! Let's return the default value
else
{
return $default;
}
}
if (strtoupper($filter_type) == 'RAW')
{
return $value;
}
else
{
$filter = new InputFilter();
return $filter->clean($value, $filter_type);
}
}
/**
* Method to set model state variables
*
* @param string $property The name of the property.
* @param mixed $value The value of the property to set or null.
*
* @return mixed The previous value of the property or null if not set.
*/
public function setState($property, $value = null)
{
if (is_null($this->state))
{
$this->state = new stdClass();
}
return $this->state->$property = $value;
}
/**
* Returns a unique hash for each view, used to prefix the state variables to allow us to retrieve them from the
* state later on. If it's not already set (with setHash) it will be set in the form com_something.myModel. If you
* pass a non-empty $viewName then if it's not already set it will be instead set in the form of
* com_something.viewName.myModel which is useful when you are reusing models in multiple views and want to avoid
* state bleedover among views.
*
* Also see the hash and hash_view parameters in the constructor's options.
*
* @return string
*/
public function getHash($viewName = null)
{
if (is_null($this->stateHash))
{
$this->stateHash = ucfirst($this->container->componentName) . '.';
if (!empty($viewName))
{
$this->stateHash .= $viewName . '.';
}
$this->stateHash .= $this->getName() . '.';
}
return $this->stateHash;
}
/**
* Sets the unique hash to prefix the state variables. The hash is cleaned according to the 'CMD' input filtering,
* must end in a dot (if not a dot is added automatically) and cannot be empty.
*
* @param string $hash
*
* @return void
*
* @see self::getHash()
*/
public function setHash($hash)
{
// Clean the hash, it has to conform to 'CMD' filtering
$tempInput = new Input(['hash' => $hash]);
$hash = $tempInput->getCmd('hash', null);
if (empty($hash))
{
return;
}
if (substr($hash, -1) == '_')
{
$hash = substr($hash, 0, -1);
}
if (substr($hash, -1) != '.')
{
$hash .= '.';
}
$this->stateHash = $hash;
}
/**
* Clears the model state, but doesn't touch the internal lists of records,
* record tables or record id variables. To clear these values, please use
* reset().
*
* @return static
*/
public function clearState()
{
$this->state = new stdClass();
return $this;
}
/**
* Clones the model object and returns the clone
*
* @return static
*/
public function getClone()
{
$clone = clone($this);
return $clone;
}
/**
* Returns a reference to the model's container
*
* @return Container
*/
public function getContainer()
{
return $this->container;
}
/**
* Magic getter; allows to use the name of model state keys as properties. Also handles magic properties:
* $this->input mapped to $this->container->input
*
* @param string $name The state variable key
*
* @return static
*/
public function __get($name)
{
// Handle $this->input
if ($name == 'input')
{
return $this->container->input;
}
return $this->getState($name);
}
/**
* Magic setter; allows to use the name of model state keys as properties
*
* @param string $name The state variable key
* @param mixed $value The state variable value
*
* @return static
*/
public function __set($name, $value)
{
return $this->setState($name, $value);
}
/**
* Magic caller; allows to use the name of model state keys as methods to
* set their values.
*
* @param string $name The state variable key
* @param mixed $arguments The state variable contents
*
* @return static
*/
public function __call($name, $arguments)
{
$arg1 = array_shift($arguments);
$this->setState($name, $arg1);
return $this;
}
/**
* Sets the model state auto-save status. By default the model is set up to
* save its state to the session.
*
* @param boolean $newState True to save the state, false to not save it.
*
* @return static
*/
public function savestate($newState)
{
$this->_savestate = $newState ? true : false;
return $this;
}
/**
* Public setter for the _savestate variable. Set it to true to save the state
* of the Model in the session.
*
* @return static
*/
public function populateSavestate()
{
if (is_null($this->_savestate))
{
$savestate = $this->input->getInt('savestate', -999);
if ($savestate == -999)
{
$savestate = true;
}
$this->savestate($savestate);
}
}
/**
* Gets the ignore request flag. When false, getState() will try to populate state variables not already set from
* same-named state variables in the request.
*
* @return boolean
*/
public function getIgnoreRequest()
{
return $this->_ignoreRequest;
}
/**
* Sets the ignore request flag. When false, getState() will try to populate state variables not already set from
* same-named state variables in the request.
*
* @param boolean $ignoreRequest
*
* @return static
*/
public function setIgnoreRequest($ignoreRequest)
{
$this->_ignoreRequest = $ignoreRequest;
return $this;
}
/**
* Returns a temporary instance of the model. Please note that this returns a _clone_ of the model object, not the
* original object. The new object is set up to not save its stats, ignore the request when getting state variables
* and comes with an empty state.
*
* @return static
*/
public function tmpInstance()
{
return $this->getClone()->savestate(false)->setIgnoreRequest(true)->clearState();
}
/**
* Method to auto-populate the model state.
*
* This method should only be called once per instantiation and is designed
* to be called on the first call to the getState() method unless the model
* configuration flag to ignore the request is set.
*
* @return void
*
* @note Calling getState in this method will result in recursion.
*/
protected function populateState()
{
}
/**
* Triggers an object-specific event. The event runs both locally if a suitable method exists and through the
* object's behaviours dispatcher and Joomla! plugin system. Neither handler is expected to return anything (return
* values are ignored). If you want to mark an error and cancel the event you have to raise an exception.
*
* EXAMPLE
* Component: com_foobar, Object name: item, Event: onBeforeSomething, Arguments: array(123, 456)
* The event calls:
* 1. $this->onBeforeSomething(123, 456)
* 2. $his->behavioursDispatcher->trigger('onBeforeSomething', array(&$this, 123, 456))
* 3. Joomla! plugin event onComFoobarModelItemBeforeSomething($this, 123, 456)
*
* @param string $event The name of the event, typically named onPredicateVerb e.g. onBeforeKick
* @param array $arguments The arguments to pass to the event handlers
*
* @return void
*/
protected function triggerEvent($event, array $arguments = [])
{
// If there is an object method for this event, call it
if (method_exists($this, $event))
{
switch (count($arguments))
{
case 0:
$this->{$event}();
break;
case 1:
$this->{$event}($arguments[0]);
break;
case 2:
$this->{$event}($arguments[0], $arguments[1]);
break;
case 3:
$this->{$event}($arguments[0], $arguments[1], $arguments[2]);
break;
case 4:
$this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
break;
case 5:
$this->{$event}($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
break;
default:
call_user_func_array([$this, $event], $arguments);
break;
}
}
// All other event handlers live outside this object, therefore they need to be passed a reference to this
// objects as the first argument.
array_unshift($arguments, $this);
// Trigger the object's behaviours dispatcher, if such a thing exists
if (property_exists($this, 'behavioursDispatcher') && method_exists($this->behavioursDispatcher, 'trigger'))
{
$this->behavioursDispatcher->trigger($event, $arguments);
}
// Prepare to run the Joomla! plugins now.
// If we have an "on" prefix for the event (e.g. onFooBar) remove it and stash it for later.
$prefix = '';
if (substr($event, 0, 2) == 'on')
{
$prefix = 'on';
$event = substr($event, 2);
}
// Get the component/model prefix for the event
$prefix .= 'Com' . ucfirst($this->container->bareComponentName) . 'Model';
$prefix .= ucfirst($this->getName());
// The event name will be something like onComFoobarItemsBeforeSomething
$event = $prefix . $event;
// Call the Joomla! plugins
$this->container->platform->runPlugins($event, $arguments);
}
/**
* Method to get model state variables
*
* @param string $property Optional parameter name
* @param mixed $default Optional default value
*
* @return object The property where specified, the state object where omitted
*/
private function internal_getState($property = null, $default = null)
{
if (!$this->_state_set)
{
// Protected method to auto-populate the model state.
$this->populateState();
// Set the model state set flag to true.
$this->_state_set = true;
}
if (is_null($property))
{
return $this->state;
}
else
{
if (property_exists($this->state, $property))
{
return $this->state->$property;
}
else
{
return $default;
}
}
}
}