597 lines
14 KiB
PHP
597 lines
14 KiB
PHP
<?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;
|
||
}
|
||
}
|
||
}
|
||
}
|