| Current Path : /home/w/u/e/wuectly/www/03cbe/ |
| Current File : /home/w/u/e/wuectly/www/03cbe/Model.tar |
DataModel.php 0000604 00000331376 15245561014 0007123 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Controller\Exception\LockedRecord;
use FOF40\Date\Date;
use FOF40\Event\Dispatcher;
use FOF40\Event\Observer;
use FOF40\Model\DataModel\Collection as DataCollection;
use FOF40\Model\DataModel\Exception\BaseException;
use FOF40\Model\DataModel\Exception\CannotLockNotLoadedRecord;
use FOF40\Model\DataModel\Exception\InvalidSearchMethod;
use FOF40\Model\DataModel\Exception\NoAssetKey;
use FOF40\Model\DataModel\Exception\NoContentType;
use FOF40\Model\DataModel\Exception\NoItemsFound;
use FOF40\Model\DataModel\Exception\NoTableColumns;
use FOF40\Model\DataModel\Exception\RecordNotLoaded;
use FOF40\Model\DataModel\Exception\SpecialColumnMissing;
use FOF40\Model\DataModel\Relation\Exception\RelationNotFound;
use FOF40\Model\DataModel\RelationManager;
use FOF40\Utils\ArrayHelper;
use Joomla\CMS\Access\Rules;
use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Table\Asset;
use Joomla\CMS\Table\ContentHistory;
use Joomla\CMS\Table\ContentType;
use Joomla\CMS\Table\CoreContent;
use Joomla\CMS\Table\TableInterface;
use Joomla\CMS\UCM\UCMContent;
/**
* Data-aware model, implementing a convenient ORM
*
* Type hinting -- start
*
*
* @method $this hasOne() hasOne(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
* @method $this belongsTo() belongsTo(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
* @method $this hasMany() hasMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null)
* @method $this belongsToMany() belongsToMany(string $name, string $foreignModelClass = null, string $localKey = null, string $foreignKey = null, string $pivotTable = null, string $pivotLocalKey = null, string $pivotForeignKey = null)
*
* @method $this filter_order() filter_order(string $orderingField)
* @method $this filter_order_Dir() filter_order_Dir(string $direction)
* @method $this limit() limit(int $limit)
* @method $this limitstart() limitstart(int $limitStart)
* @method $this enabled() enabled(int $enabled)
* @method DataModel getNew() getNew(string $relationName)
*
* @property int $enabled Publish status of this record
* @property int $ordering Sort ordering of this record
* @property int $created_by ID of the user who created this record
* @property string $created_on Date/time stamp of record creation
* @property int $modified_by ID of the user who modified this record
* @property string $modified_on Date/time stamp of record modification
* @property int $locked_by ID of the user who locked this record
* @property string $locked_on Date/time stamp of record locking
*
* Type hinting -- end
*/
class DataModel extends Model implements TableInterface
{
/** @var array A list of tables in the database */
protected static $tableCache = [];
/** @var array A list of table fields, keyed per table */
protected static $tableFieldCache = [];
/** @var array A list of permutations of the prefix with upper/lowercase letters */
protected static $prefixCasePermutations = [];
/** @var array Table field name aliases, defined as aliasFieldName => actualFieldName */
protected $aliasFields = [];
/** @var boolean Should I run automatic checks on the table data? */
protected $autoChecks = true;
/** @var boolean Should I auto-fill the fields of the model object when constructing it? */
protected $autoFill = false;
/** @var Dispatcher An event dispatcher for model behaviours */
protected $behavioursDispatcher;
/** @var \JDatabaseDriver The database driver for this model */
protected $dbo;
/** @var array Which fields should be exempt from automatic checks when autoChecks is enabled */
protected $fieldsSkipChecks = [];
/** @var array Which fields should be auto-filled from the model state (by extent, the request)? */
protected $fillable = [];
/** @var array Which fields should never be auto-filled from the model state (by extent, the request)? */
protected $guarded = [];
/** @var string The identity field's name */
protected $idFieldName = '';
/** @var array A hash array with the table fields we know about and their information. Each key is the field name, the value is the field information */
protected $knownFields = [];
/** @var array The data of the current record */
protected $recordData = [];
/** @var boolean What will delete() do? True: trash (enabled set to -2); false: hard delete (remove from database) */
protected $softDelete = false;
/** @var string The name of the database table we connect to */
protected $tableName = '';
/** @var array A collection of custom, additional where clauses to apply during buildQuery */
protected $whereClauses = [];
/** @var RelationManager The relation manager of this model */
protected $relationManager;
/** @var array A list of all eager loaded relations and their attached callbacks */
protected $eagerRelations = [];
/** @var array A list of the relation filter definitions for this model */
protected $relationFilters = [];
/** @var array A list of the relations which will be auto-touched by save() and touch() methods */
protected $touches = [];
/** @var bool Should rows be tracked as ACL assets? */
protected $trackAssets = false;
/** @var bool Does the resource support joomla tags? */
protected $has_tags = false;
/** @var Rules The rules associated with this record. */
protected $rules;
/** @var string The UCM content type (typically: com_something.viewname, e.g. com_foobar.items) */
protected $contentType;
/** @var array Shared parameters for behaviors */
protected $behaviorParams = [];
/**
* The asset key for items in this table. It's usually something in the
* com_example.viewname format. They asset name will be this key appended
* with the item's ID, e.g. com_example.viewname.123
*
* @var string
*/
protected $assetKey = '';
/**
* Public constructor. Overrides the parent constructor, adding support for database-aware models.
*
* You can use the $config array to pass some configuration values to the object:
*
* tableName String The name of the database table to use. Default: #__appName_viewNamePlural (Ruby
* on Rails convention) idFieldName String The table key field name. Default:
* appName_viewNameSingular_id (Ruby on Rails convention) knownFields Array The known fields in the
* table. Default: read from the table itself autoChecks Boolean Should I turn on automatic data
* validation checks? fieldsSkipChecks Array List of fields which should not participate in automatic data
* validation checks. aliasFields Array Associative array of "magic" field aliases.
* behavioursDispatcher EventDispatcher The model behaviours event dispatcher. behaviourObservers Array The
* model behaviour observers to attach to the behavioursDispatcher. behaviours Array A list of
* behaviour names to instantiate and attach to the behavioursDispatcher. fillable_fields Array Which
* fields should be auto-filled from the model state (by extent, the request)? guarded_fields Array Which
* fields should never be auto-filled from the model state (by extent, the request)? relations Array
* (hashed) The relations to autoload on model creation. contentType String The UCM content type, e.g.
* "com_foobar.items"
*
* Setting either fillable_fields or guarded_fields turns on automatic filling of fields in the constructor. If
* both
* are set only guarded_fields is taken into account. Fields are not filled automatically outside the constructor.
*
* @param Container $container The configuration variables to this model
* @param array $config Configuration values for this model
*
* @throws \FOF40\Model\DataModel\Exception\NoTableColumns
* @see Model::__construct()
*
*/
public function __construct(Container $container, array $config = [])
{
// First call the parent constructor.
parent::__construct($container, $config);
// Should I use a different database object?
$this->dbo = $container->db;
// Do I have a table name?
if (isset($config['tableName']))
{
$this->tableName = $config['tableName'];
}
elseif (empty($this->tableName))
{
// The table name is by default: #__appName_viewNamePlural (Ruby on Rails convention)
$viewPlural = $container->inflector->pluralize($this->getName());
$this->tableName = '#__' . strtolower($this->container->bareComponentName) . '_' . strtolower($viewPlural);
}
// Do I have a table key name?
if (isset($config['idFieldName']))
{
$this->idFieldName = $config['idFieldName'];
}
elseif (empty($this->idFieldName))
{
// The default ID field is: appName_viewNameSingular_id (Ruby on Rails convention)
$viewSingular = $container->inflector->singularize($this->getName());
$this->idFieldName = strtolower($this->container->bareComponentName) . '_' . strtolower($viewSingular) . '_id';
}
// Do I have a list of known fields?
if (isset($config['knownFields']) && !empty($config['knownFields']))
{
if (!is_array($config['knownFields']))
{
$config['knownFields'] = explode(',', $config['knownFields']);
}
$this->knownFields = $config['knownFields'];
}
else
{
// By default the known fields are fetched from the table itself (slow!)
$this->knownFields = $this->getTableFields();
}
if (empty($this->knownFields))
{
throw new NoTableColumns(sprintf('Model %s could not fetch column list for the table %s', $this->getName(), $this->tableName));
}
// Should I turn on autoChecks?
if (isset($config['autoChecks']))
{
if (!is_bool($config['autoChecks']))
{
$config['autoChecks'] = strtolower($config['autoChecks']);
$config['autoChecks'] = in_array($config['autoChecks'], ['yes', 'true', 'on', 1]);
}
$this->autoChecks = $config['autoChecks'];
}
// Should I exempt fields from autoChecks?
if (isset($config['fieldsSkipChecks']))
{
if (!is_array($config['fieldsSkipChecks']))
{
$config['fieldsSkipChecks'] = explode(',', $config['fieldsSkipChecks']);
$config['fieldsSkipChecks'] = array_map(function ($x) {
return trim($x);
}, $config['fieldsSkipChecks']);
}
$this->fieldsSkipChecks = $config['fieldsSkipChecks'];
}
// Do I have alias fields?
if (isset($config['aliasFields']))
{
$this->aliasFields = $config['aliasFields'];
}
// Do I have a behaviours dispatcher?
if (isset($config['behavioursDispatcher']) && ($config['behavioursDispatcher'] instanceof Dispatcher))
{
$this->behavioursDispatcher = $config['behavioursDispatcher'];
}
// Otherwise create the model behaviours dispatcher
else
{
$this->behavioursDispatcher = new Dispatcher($this->container);
}
// Do I have an array of behaviour observers
if (isset($config['behaviourObservers']) && is_array($config['behaviourObservers']))
{
foreach ($config['behaviourObservers'] as $observer)
{
$this->behavioursDispatcher->attach($observer);
}
}
// Do I have a list of behaviours?
if (isset($config['behaviours']) && is_array($config['behaviours']))
{
foreach ($config['behaviours'] as $behaviour)
{
$this->addBehaviour($behaviour);
}
}
// Add extra behaviours
foreach (['Created', 'Modified'] as $behaviour)
{
$this->addBehaviour($behaviour);
}
// Do I have a list of fillable fields?
if (isset($config['fillable_fields']) && !empty($config['fillable_fields']))
{
if (!is_array($config['fillable_fields']))
{
$config['fillable_fields'] = explode(',', $config['fillable_fields']);
$config['fillable_fields'] = array_map(function ($x) {
return trim($x);
}, $config['fillable_fields']);
}
$this->fillable = [];
$this->autoFill = true;
foreach ($config['fillable_fields'] as $field)
{
if (array_key_exists($field, $this->knownFields))
{
$this->fillable[] = $field;
}
elseif (isset($this->aliasFields[$field]))
{
$this->fillable[] = $this->aliasFields[$field];
}
}
}
// Do I have a list of guarded fields?
if (isset($config['guarded_fields']) && !empty($config['guarded_fields']))
{
if (!is_array($config['guarded_fields']))
{
$config['guarded_fields'] = explode(',', $config['guarded_fields']);
$config['guarded_fields'] = array_map(function ($x) {
return trim($x);
}, $config['guarded_fields']);
}
$this->guarded = [];
$this->autoFill = true;
foreach ($config['guarded_fields'] as $field)
{
if (array_key_exists($field, $this->knownFields))
{
$this->guarded[] = $field;
}
elseif (isset($this->aliasFields[$field]))
{
$this->guarded[] = $this->aliasFields[$field];
}
}
}
// If we are tracking assets, make sure an access field exists and initially set the default.
$asset_id_field = $this->getFieldAlias('asset_id');
$access_field = $this->getFieldAlias('access');
if (array_key_exists($asset_id_field, $this->knownFields))
{
$this->trackAssets = true;
}
/**
* if ($this->trackAssets && array_key_exists($access_field, $this->knownFields) && !($this->getState($access_field, null)))
* {
* $this->$access_field = (int) $this->container->platform->getConfig()->get('access');
* }
**/
$assetKey = $this->container->componentName . '.' . strtolower($container->inflector->singularize($this->getName()));
$this->setAssetKey($assetKey);
// Set the UCM content type if applicable
if (isset($config['contentType']))
{
$this->contentType = $config['contentType'];
}
// Do I have to auto-fill the fields?
if ($this->autoFill)
{
$fields = !empty($this->guarded) ? array_keys($this->knownFields) : $this->fillable;
foreach ($fields as $field)
{
if (in_array($field, $this->guarded))
{
// Do not set guarded fields
continue;
}
$stateValue = $this->getState($field);
if (!is_null($stateValue))
{
$this->setFieldValue($field, $stateValue);
}
}
}
// Create a relation manager
$this->relationManager = new RelationManager($this);
// Do I have a list of relations?
if (isset($config['relations']) && is_array($config['relations']))
{
foreach ($config['relations'] as $relConfig)
{
if (!is_array($relConfig))
{
continue;
}
$defaultRelConfig = [
'type' => 'hasOne',
'foreignModelClass' => null,
'localKey' => null,
'foreignKey' => null,
'pivotTable' => null,
'pivotLocalKey' => null,
'pivotForeignKey' => null,
];
$relConfig = array_merge($defaultRelConfig, $relConfig);
$this->relationManager->addRelation($relConfig['itemName'], $relConfig['type'], $relConfig['foreignModelClass'],
$relConfig['localKey'], $relConfig['foreignKey'], $relConfig['pivotTable'],
$relConfig['pivotLocalKey'], $relConfig['pivotForeignKey']);
}
}
// Initialise the data model
foreach ($this->knownFields as $fieldName => $information)
{
// Initialize only the null or not yet set records
if (!isset($this->recordData[$fieldName]))
{
$this->recordData[$fieldName] = $information->Default;
}
}
// Trigger the onAfterConstruct event. This allows you to set up model state etc.
$this->triggerEvent('onAfterConstruct');
}
/**
* Magic caller. It works like the magic setter and returns ourselves for chaining. If no arguments are passed we'll
* only look for a scope filter.
*
* @param string $name
* @param mixed $arguments
*
* @return static
*/
public function __call($name, $arguments)
{
// If no arguments are provided try mapping to the scopeSomething() method
if (empty($arguments))
{
$methodName = 'scope' . ucfirst($name);
if (method_exists($this, $methodName))
{
$this->{$methodName}();
return $this;
}
}
// Implements getNew($relationName)
if (($name == 'getNew') && (is_array($arguments) || $arguments instanceof \Countable ? count($arguments) : 0))
{
return $this->relationManager->getNew($arguments[0]);
}
// Magically map relations to methods, e.g. $this->foobar will return the "foobar" relations' contents
if ($this->relationManager->isMagicMethod($name))
{
return call_user_func_array([$this->relationManager, $name], $arguments);
}
// Otherwise call the parent
return parent::__call($name, $arguments);
}
/**
* Magic checker on a property. It follows the same logic of the __get magic method, however, if nothing is found,
* it won't return the state of a variable (we are checking if a property is set)
*
* @param string $name The name of the field to check
*
* @return bool Is the field set?
*/
public function __isset($name)
{
$value = null;
$isState = false;
if (substr($name, 0, 3) == 'flt')
{
$isState = true;
$name = strtolower(substr($name, 3, 1)) . substr($name, 4);
}
// If $name is a field name, get its value
if (!$isState && array_key_exists($name, $this->recordData))
{
$value = $this->getFieldValue($name);
}
elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
{
$name = $this->aliasFields[$name];
$value = $this->getFieldValue($name);
}
elseif ($this->relationManager->isMagicProperty($name))
{
$value = $this->relationManager->$name;
}
// As the core function isset, the property must exists AND must be NOT null
return ($value !== null);
}
/**
* Magic getter. It will return the value of a field or, if no such field is found, the value of the relevant state
* variable.
*
* Tip: Trying to get fltSomething will always return the value of the state variable "something"
*
* Tip: You can define custom field getter methods as getFieldNameAttribute, where FieldName is your field's name,
* in CamelCase (even if the field name itself is in snake_case).
*
* @param string $name The name of the field / state variable to retrieve
*
* @return static|mixed
*/
public function __get($name)
{
// Handle $this->input
if ($name == 'input')
{
return $this->container->input;
}
$isState = false;
if (substr($name, 0, 3) == 'flt')
{
$isState = true;
$name = strtolower(substr($name, 3, 1)) . substr($name, 4);
}
// If $name is a field name, get its value
if (!$isState && array_key_exists($name, $this->recordData))
{
return $this->getFieldValue($name);
}
elseif (!$isState && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
{
$name = $this->aliasFields[$name];
return $this->getFieldValue($name);
}
elseif ($this->relationManager->isMagicProperty($name))
{
return $this->relationManager->$name;
}
// If $name is not a field name, get the value of a state variable
else
{
return $this->getState($name);
}
}
/**
* Magic setter. It will set the value of a field or the value of a dynamic scope filter, or the value of the
* relevant state variable.
*
* Tip: Trying to set fltSomething will always return the value of the state variable "something"
*
* Tip: Trying to set scopeSomething will always return the value of the dynamic scope filter "something"
*
* Tip: You can define custom field setter methods as setFieldNameAttribute, where FieldName is your field's name,
* in CamelCase (even if the field name itself is in snake_case).
*
* @param string $name The name of the field / scope / state variable to set
* @param mixed $value The value to set
*
* @return void
*/
public function __set($name, $value)
{
$isState = false;
$isScope = false;
if (substr($name, 0, 3) == 'flt')
{
$isState = true;
$name = strtolower(substr($name, 3, 1)) . substr($name, 4);
}
elseif (substr($name, 0, 5) == 'scope')
{
$isScope = true;
$name = strtolower(substr($name, 5, 1)) . substr($name, 5);
}
// If $name is a field name, set its value
if (!$isState && !$isScope && array_key_exists($name, $this->recordData))
{
$this->setFieldValue($name, $value);
}
elseif (!$isState && !$isScope && array_key_exists($name, $this->aliasFields) && array_key_exists($this->aliasFields[$name], $this->recordData))
{
$name = $this->aliasFields[$name];
$this->setFieldValue($name, $value);
}
// If $name is a dynamic scope filter, set its value
elseif ($isScope || method_exists($this, 'scope' . ucfirst($name)))
{
$method = 'scope' . ucfirst($name);
$this->{$method}($value);
}
// If $name is not a field name, set the value of a state variable
else
{
$this->setState($name, $value);
}
}
/**
* 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. The temporary object instance has its data reset as well.
*
* @return $this
*/
public function tmpInstance()
{
return parent::tmpInstance()->reset(true, true);
}
/**
* Adds a known field to the DataModel. This is only necessary if you are using a custom buildQuery with JOINs or
* field aliases. Please note that you need to make further modifications for bind() and save() to work in this
* case. Please refer to the documentation blocks of these methods for more information. It is generally considered
* a very BAD idea using JOINs instead of relations. It complicates your life and is bound to cause bugs that are
* very hard to track back.
*
* Basically, if you find yourself using this method you are probably doing something very wrong or very advanced.
* If you do not feel confident with debugging FOF code STOP WHATEVER YOU'RE DOING and rethink your Model. Why are
* you using a JOIN? If you want to filter the records by a field found in another table you can still use
* relations and whereHas with a callback.
*
* @param string $fieldName The name of the field
* @param mixed $default Default value, used by reset() (default: null)
* @param string $type Database type for the field. If unsure use 'integer', 'float' or 'text'.
* @param bool $replace Should we replace an existing known field definition?
*
* @return $this Self, for chaining
*/
public function addKnownField($fieldName, $default = null, $type = 'integer', $replace = false)
{
if (array_key_exists($fieldName, $this->knownFields) && !$replace)
{
return $this;
}
$info = (object) [
'Default' => $default,
'Type' => $type,
'Null' => 'YES',
];
$this->knownFields[$fieldName] = $info;
// Initialize only the null or not yet set records
if (!isset($this->recordData[$fieldName]))
{
$this->recordData[$fieldName] = $default;
}
return $this;
}
/**
* Get the columns from database table. For TableInterface compatibility.
*
* @return mixed An array of the field names, or false if an error occurs.
*/
public function getFields()
{
return $this->getTableFields();
}
/**
* Get the columns from a database table.
*
* @param string $tableName Table name. If null current table is used
*
* @return mixed An array of the field names, or false if an error occurs.
*/
public function getTableFields($tableName = null)
{
// Make sure we have a list of tables in this db
if (empty(static::$tableCache))
{
static::$tableCache = $this->getDbo()->getTableList();
}
if (!$tableName)
{
$tableName = $this->tableName;
}
// Try to load again column specifications if the table is not loaded OR if it's loaded and
// the previous call returned an error
if (!array_key_exists($tableName, static::$tableFieldCache) ||
(isset(static::$tableFieldCache[$tableName]) && !static::$tableFieldCache[$tableName])
)
{
// Lookup the fields for this table only once.
$name = $tableName;
$prefix = $this->getDbo()->getPrefix();
$checkName = substr($name, 0, 3) == '#__' ? $prefix . substr($name, 3) : $name;
// Iterate through all lower/uppercase permutations of the prefix if we have a prefix with at least one uppercase letter
if (!in_array($checkName, static::$tableCache) && preg_match('/[A-Z]/', $prefix) && (substr($name, 0, 3) == '#__'))
{
$prefixPermutations = $this->getPrefixCasePermutations();
$partialCheckName = substr($name, 3);
foreach ($prefixPermutations as $permutatedPrefix)
{
$checkName = $permutatedPrefix . $partialCheckName;
if (in_array($checkName, static::$tableCache))
{
break;
}
}
}
if (!in_array($checkName, static::$tableCache))
{
// The table doesn't exist. Return false.
static::$tableFieldCache[$tableName] = false;
}
else
{
$fields = $this->getDbo()->getTableColumns($name, false);
if (empty($fields))
{
$fields = false;
}
static::$tableFieldCache[$tableName] = $fields;
}
// PostgreSQL date type compatibility
if (($this->getDbo()->name == 'postgresql') && (static::$tableFieldCache[$tableName] != false))
{
foreach (static::$tableFieldCache[$tableName] as $field)
{
if (strtolower($field->type) != 'timestamp without time zone')
{
continue;
}
if (!stristr($field->Default, '\'::timestamp without time zone'))
{
continue;
}
[$date,] = explode('::', $field->Default, 2);
$field->Default = trim($date, "'");
}
}
}
return static::$tableFieldCache[$tableName];
}
/**
* Get the database connection associated with this data Model
*
* @return \JDatabaseDriver
*/
public function getDbo()
{
if (!is_object($this->dbo))
{
$this->dbo = $this->container->db;
}
return $this->dbo;
}
/**
* Returns the data currently bound to the model in an array format. Similar to toArray() but returns a copy instead
* of the internal table itself.
*
* @return array
*/
public function getData()
{
$ret = [];
foreach (array_keys($this->knownFields) as $field)
{
$ret[$field] = $this->getFieldValue($field);
}
return $ret;
}
/**
* Return the value of the identity column of the currently loaded record
*
* @return mixed
*/
public function getId()
{
return $this->{$this->idFieldName};
}
/**
* Returns the name of the table's id field (primary key) name
*
* @return string
*/
public function getIdFieldName()
{
return $this->idFieldName;
}
/**
* Alias of getIdFieldName. Used for TableInterface compatibility.
*
* @return string The name of the primary key for the table.
*
* @codeCoverageIgnore
*/
public function getKeyName()
{
return $this->getIdFieldName();
}
/**
* Returns the database table name this model talks to
*
* @return string
*/
public function getTableName()
{
return $this->tableName;
}
/**
* Returns the value of a field. If a field is not set it uses the $default value. Automatically uses magic
* getter variables if required.
*
* @param string $name The name of the field to retrieve
* @param mixed $default Default value, if the field is not set and doesn't have a getter method
*
* @return mixed The value of the field
*/
public function getFieldValue($name, $default = null)
{
if (array_key_exists($name, $this->aliasFields))
{
$name = $this->aliasFields[$name];
}
if (!array_key_exists($name, $this->knownFields))
{
return $default;
}
if (!isset($this->recordData[$name]))
{
$this->recordData[$name] = $default;
}
return $this->recordData[$name];
}
/**
* Sets the value of a field.
*
* @param string $name The name of the field to set
* @param mixed $value The value to set it to
*
* @return void
*/
public function setFieldValue($name, $value = null)
{
if (array_key_exists($name, $this->aliasFields))
{
$name = $this->aliasFields[$name];
}
if (array_key_exists($name, $this->knownFields))
{
$this->recordData[$name] = $value;
}
}
/**
* Applies the getSomethingAttribute methods to $this->recordData, converting the database representation of the
* data to the record representation. $this->recordData is directly modified.
*
* @return void
*/
public function databaseDataToRecordData()
{
foreach ($this->recordData as $name => $value)
{
$method = $this->container->inflector->camelize('get_' . $name . '_attribute');
if (method_exists($this, $method))
{
$this->recordData[$name] = $this->{$method}($value);
}
}
}
/**
* Applies the setSomethingAttribute methods to $this->recordData, converting the record representation to database
* representation. It does not modify $this->recordData, it returns a copy of the data array.
*
* If you are using custom knownFields to cater for table JOINs you need to override this method and _remove_ the
* fields which do not belong to the table you are saving to. It's generally a bad idea using JOINs instead of
* relations. You have been warned!
*
* @return array
*/
public function recordDataToDatabaseData()
{
$copy = array_merge($this->recordData);
foreach ($copy as $name => $value)
{
$method = $this->container->inflector->camelize('set_' . $name . '_attribute');
if (method_exists($this, $method))
{
$copy[$name] = $this->{$method}($value);
}
}
return $copy;
}
/**
* Does this model know about a field called $fieldName? Automatically uses aliases when necessary.
*
* @param string $fieldName Field name to check
*
* @return boolean True if the field exists
*/
public function hasField($fieldName)
{
$realFieldName = $this->getFieldAlias($fieldName);
return array_key_exists($realFieldName, $this->knownFields);
}
/**
* Is this field known to the model and marked as nullable in the database?
*
* Automatically uses aliases when necessary.
*
* @param string $fieldName Field name to check
*
* @return bool True if the field is nullable or doesn't exist
*/
public function isNullableField(string $fieldName): bool
{
if (!$this->hasField($fieldName))
{
return true;
}
$realFieldName = $this->getFieldAlias($fieldName);
return strtolower($this->knownFields[$realFieldName]->Null ?? 'YES') == 'yes';
}
/**
* Get the real name of a field name based on its alias. If the field is not aliased $alias is returned
*
* @param string $alias The field to get an alias for
*
* @return string The real name of the field
*/
public function getFieldAlias($alias)
{
if (array_key_exists($alias, $this->aliasFields))
{
return $this->aliasFields[$alias];
}
else
{
return $alias;
}
}
/**
* Returns an array mapping relation names to their local key field names.
*
* For example, given a relation "foobar" with local key name "example_item_id" it will return:
* ["foobar" => "example_item_id"]
*
* @return array Array of [relationName => fieldName] arrays
*
* @throws \FOF40\Model\DataModel\Relation\Exception\RelationNotFound
*/
public function getRelationFields()
{
$fields = [];
$relationNames = $this->relationManager->getRelationNames();
if (empty($relationNames))
{
return $fields;
}
foreach ($relationNames as $name)
{
$fields[$name] = $this->relationManager->getRelation($name)->getLocalKey();
}
return $fields;
}
/**
* Returns the qualified foreign model name, in the format "componentName.modelName", for the specified model
* field. First it checks the relations you have defined. If none is found it will try to parse the field name as
* following the componentName_modelName_id naming convention (FOF best practice and recommendation).
*
* This feature is used by the Blade compiler.
*
* @param string $fieldName The field name for which we'll get a foreign model name
*
* @return string
*/
public function getForeignModelNameFor($fieldName)
{
// First look for a local field mapped in a relationship
try
{
$relationMap = $this->getRelationFields();
$relationName = array_search($fieldName, $relationMap);
if ($relationName !== false)
{
$model = $this->relationManager->getRelation($relationName)->getForeignModel();
$component = $model->getContainer()->componentName;
$modelName = $model->getName();
return "$component.$modelName";
}
}
catch (RelationNotFound $e)
{
// Bummer. The relation cannot be found. I will fall back to parsing the field name.
}
// Do I have a field following the componentName_modelName_id format?
$parts = explode('_', $fieldName);
if ((substr($fieldName, -3) != '_id') || (count($parts) < 3))
{
throw new \RuntimeException("Cannot determine the foreign model for local field '$fieldName'; it does not follow the expected component_model_id convention.");
}
$fieldName = substr($fieldName, 0, -3);
[$component, $modelName] = explode('_', $fieldName, 2);
$modelName = $this->container->inflector->camelize($modelName);
return "$component.$modelName";
}
/**
* Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
* data, unless you provide a $data array.
*
* Special note if you are using a custom buildQuery with JOINs or field aliases:
* You will need to override the recordDataToDatabaseData method. Make sure that you _remove_ or rename any fields
* which do not exist in the table defined in $this->tableName. Otherwise Joomla! will not know how to insert /
* update the data on the table and will throw an Exception denoting a database error. It is generally a BAD idea
* using JOINs instead of relations. If unsure, use relations.
*
* @param null|array $data [Optional] Data to bind
* @param string $orderingFilter A WHERE clause used to apply table item reordering
* @param array $ignore A list of fields to ignore when binding $data
*
* @para boolean $resetRelations Should I automatically reset relations if relation-important fields are
* changed?
*
* @return DataModel Self, for chaining
*/
public function save($data = null, $orderingFilter = '', $ignore = null, $resetRelations = true)
{
// Stash the primary key
$oldPKValue = $this->getId();
// Call the onBeforeSave event
$this->triggerEvent('onBeforeSave', [&$data]);
// Get the relation to local field map and initialise the relationsAffected array
$relationImportantFields = $this->getRelationFields();
$dataBeforeBind = [];
// If we have relations we keep a copy of the data before bind.
if (count($relationImportantFields) > 0)
{
$dataBeforeBind = array_merge($this->recordData);
}
// Bind any (optional) data. If no data is provided, the current record data is used
if (!is_null($data))
{
$this->bind($data, $ignore);
}
$isNewRecord = empty($oldPKValue) ? true : $oldPKValue != $this->getId();
// Check the validity of the data
$this->check();
// Get the database object
$db = $this->getDbo();
// Insert or update the record. Note that the object we use for insertion / update is the a copy holding
// the transformed data.
$dataObject = $this->recordDataToDatabaseData();
$dataObject = (object) $dataObject;
if ($isNewRecord)
{
$this->triggerEvent('onBeforeCreate', [&$dataObject]);
// Insert the new record
$db->insertObject($this->tableName, $dataObject, $this->idFieldName);
// Update ourselves with the new ID field's value
$this->{$this->idFieldName} = $db->insertid();
// Rebase the relations with the newly created model
if ($resetRelations)
{
$this->relationManager->rebase($this);
}
$this->triggerEvent('onAfterCreate');
}
else
{
$this->triggerEvent('onBeforeUpdate', [&$dataObject]);
$db->updateObject($this->tableName, $dataObject, $this->idFieldName, true);
$this->triggerEvent('onAfterUpdate');
}
// If an ordering filter is set, attempt reorder the rows in the table based on the filter and value.
if ($orderingFilter)
{
$filterValue = $this->$orderingFilter;
$this->reorder($orderingFilter ? $db->qn($orderingFilter) . ' = ' . $db->q($filterValue) : '');
}
foreach ($this->touches as $relation)
{
$records = $this->getRelations()->getData($relation);
if (!empty($records))
{
if ($records instanceof DataModel)
{
$records = [$records];
}
/** @var DataModel $record */
foreach ($records as $record)
{
$record->touch();
}
}
}
// If we have relations we compare the data to the copy of the data before bind.
if (count($relationImportantFields) && $resetRelations)
{
// Since array_diff_assoc doesn't work recursively we have to do it the EXCRUCIATINGLY SLOW WAY. Sad panda :(
$keysRecord = (is_array($this->recordData) && !empty($this->recordData)) ? array_keys($this->recordData) : [];
$keysBefore = (is_array($dataBeforeBind) && !empty($dataBeforeBind)) ? array_keys($dataBeforeBind) : [];
$keysAll = array_merge($keysRecord, $keysBefore);
$keysAll = array_unique($keysAll);
$modifiedFields = [];
foreach ($keysAll as $key)
{
if (!isset($dataBeforeBind[$key]) || !isset($this->recordData[$key]))
{
$modifiedFields[] = $key;
}
elseif ($dataBeforeBind[$key] != $this->recordData[$key])
{
$modifiedFields[] = $key;
}
}
unset ($dataBeforeBind);
if (count($modifiedFields) > 0)
{
$relationsAffected = [];
unset($modifiedData);
foreach ($relationImportantFields as $relationName => $fieldName)
{
if (in_array($fieldName, $modifiedFields))
{
$relationsAffected[] = $relationName;
}
}
// Reset the relations which are affected by the save. This will force-reload the relations when you try to
// access them again.
$this->relationManager->resetRelationData($relationsAffected);
}
}
// Finally, call the onAfterSave event
$this->triggerEvent('onAfterSave');
return $this;
}
/**
* Alias of save. For TableInterface compatibility.
*
* @param boolean $updateNulls Blatantly ignored.
*
* @return boolean True on success.
*/
public function store($updateNulls = false)
{
try
{
$this->save();
}
catch (\Exception $e)
{
return false;
}
return true;
}
/**
* Save a record, creating it if it doesn't exist or updating it if it exists. By default it uses the currently set
* data, unless you provide a $data array. On top of that, it also saves all specified relations. If $relations is
* null it will save all relations known to this model.
*
* @param null|array $data [Optional] Data to bind
* @param string $orderingFilter A WHERE clause used to apply table item reordering
* @param array $ignore A list of fields to ignore when binding $data
* @param array $relations Which relations to save with the model's record. Leave null for all
* relations
*
* @return $this Self, for chaining
*/
public function push($data = null, $orderingFilter = '', $ignore = null, array $relations = null)
{
// Store the model's $touches definition
$touches = $this->touches;
$this->touches = is_array($relations) ? array_diff($this->touches, $relations) : [];
// Save this record
$this->save($data, $orderingFilter, $ignore, false);
// Push all relations specified (or all relations if $relations is null)
$relManager = $this->getRelations();
$allRelations = $relManager->getRelationNames();
foreach ($allRelations as $relationName)
{
if (!is_null($relations) && !in_array($relationName, $relations))
{
continue;
}
$relManager->save($relationName);
}
// Restore the model's $touches definition
$this->touches = $touches;
// Return self for chaining
return $this;
}
/**
* Method to bind an associative array or object to the DataModel instance. This method optionally takes an array of
* properties to ignore when binding.
*
* Special note if you are using a custom buildQuery with JOINs or field aliases:
* You will need to use addKnownField to let FOF know that the fields from your JOINs and the aliased fields should
* be bound to the record data. If you are using aliased fields you may also want to override the
* databaseDataToRecordData method. Generally, it is a BAD idea using JOINs instead of relations.
*
* @param mixed $data An associative array or object to bind to the DataModel instance.
* @param mixed $ignore An optional array or space separated list of properties to ignore while binding.
*
* @return static Self, for chaining
*
* @throws \InvalidArgumentException
* @throws \Exception
*/
public function bind($data, $ignore = [])
{
$this->triggerEvent('onBeforeBind', [&$data]);
// If the source value is not an array or object return false.
if (!is_object($data) && !is_array($data))
{
throw new \InvalidArgumentException(Text::sprintf('LIB_FOF40_MODEL_ERR_BIND', get_class($this), gettype($data)));
}
// If the ignore value is a string, explode it over spaces.
if (!is_array($ignore))
{
$ignore = explode(' ', $ignore);
}
// Bind the source value, excluding the ignored fields.
foreach (array_keys($this->recordData) as $k)
{
// Only process fields not in the ignore array.
if (!in_array($k, $ignore))
{
if (is_array($data) && isset($data[$k]))
{
$this->setFieldValue($k, $data[$k]);
}
elseif (is_object($data) && isset($data->$k))
{
$this->setFieldValue($k, $data->$k);
}
}
}
// Perform data transformation
$this->databaseDataToRecordData();
$this->triggerEvent('onAfterBind', [$data]);
return $this;
}
/**
* Check the data for validity. By default it only checks for fields declared as NOT NULL
*
* @return static Self, for chaining
*
* @throws \RuntimeException When the data bound to this record is invalid
*/
public function check()
{
if (!$this->autoChecks)
{
return $this;
}
// Run a custom event
$this->triggerEvent('onBeforeCheck');
// Create a slug if there is a title and an empty slug
$slugField = $this->getFieldAlias('slug');
$titleField = $this->getFieldAlias('title');
if ($this->hasField('title') && $this->hasField('slug') && !$this->$slugField)
{
$this->$slugField = ApplicationHelper::stringURLSafe($this->$titleField);
}
// Special handling of the ordering field
if ($this->hasField('ordering') && is_null($this->getFieldValue('ordering')))
{
$this->setFieldValue('ordering', 0);
}
foreach ($this->knownFields as $fieldName => $field)
{
// Never check the key if it's empty; an empty key is normal for new records
if ($fieldName == $this->idFieldName)
{
continue;
}
$value = $this->$fieldName;
if (isset($field->Null) && ($field->Null == 'NO') && empty($value) && !is_numeric($value) && !in_array($fieldName, $this->fieldsSkipChecks))
{
if (!is_null($field->Default))
{
$this->$fieldName = $field->Default;
continue;
}
$text = $this->container->componentName . '_' . $this->container->inflector->singularize($this->getName()) . '_ERR_'
. $fieldName . '_EMPTY';
throw new \RuntimeException(Text::_(strtoupper($text)), 500);
}
}
return $this;
}
/**
* Change the ordering of the records of the table
*
* @param string $where The WHERE clause of the SQL used to fetch the order
*
* @return static Self, for chaining
*
* @throws \UnexpectedValueException
*/
public function reorder($where = '')
{
// If there is no ordering field set an error and return false.
if (!$this->hasField('ordering'))
{
throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
}
$this->triggerEvent('onBeforeReorder', [&$where]);
$order_field = $this->getFieldAlias('ordering');
$k = $this->getIdFieldName();
$db = $this->getDbo();
// Get the primary keys and ordering values for the selection.
$query = $db->getQuery(true)
->select($db->qn($k) . ', ' . $db->qn($order_field))
->from($db->qn($this->getTableName()))
->where($db->qn($order_field) . ' >= ' . $db->q(0))
->order($db->qn($order_field) . 'ASC, ' . $db->qn($k) . 'ASC');
// Setup the extra where and ordering clause data.
if (!empty($where))
{
$query->where($where);
}
$rows = $db->setQuery($query)->loadObjectList();
// Compact the ordering values.
foreach ($rows as $i => $row)
{
// Make sure the ordering is a positive integer.
if ($row->$order_field < 0)
{
continue;
}
// Only update rows that are necessary.
if ($row->$order_field == $i + 1)
{
continue;
}
// Update the row ordering field.
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($db->qn($order_field) . ' = ' . $db->q($i + 1))
->where($db->qn($k) . ' = ' . $db->q($row->$k));
$db->setQuery($query)->execute();
}
$this->triggerEvent('onAfterReorder');
return $this;
}
/**
* Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
* Negative numbers move the row up in the sequence and positive numbers move it down.
*
* @param integer $delta The direction and magnitude to move the row in the ordering sequence.
* @param string $where WHERE clause to use for limiting the selection of rows to compact the
* ordering values.
*
* @return static Self, for chaining
*
* @throws \UnexpectedValueException If the table does not support reordering
* @throws \RuntimeException If the record is not loaded
*/
public function move($delta, $where = '')
{
if (!$this->hasField('ordering'))
{
throw new SpecialColumnMissing(sprintf('%s does not support ordering.', $this->tableName));
}
$this->triggerEvent('onBeforeMove', [&$delta, &$where]);
$ordering_field = $this->getFieldAlias('ordering');
// If the change is none, do nothing.
if (empty($delta))
{
$this->triggerEvent('onAfterMove');
return $this;
}
$k = $this->idFieldName;
$db = $this->getDbo();
$query = $db->getQuery(true);
// If the table is not loaded, return false
if (empty($this->$k))
{
throw new RecordNotLoaded(sprintf("Model %s does not have a loaded record", $this->getName()));
}
// Select the primary key and ordering values from the table.
$query->select([
$db->qn($this->idFieldName), $db->qn($ordering_field),
]
)->from($db->qn($this->tableName));
// If the movement delta is negative move the row up.
if ($delta < 0)
{
$query->where($db->qn($ordering_field) . ' < ' . $db->q((int) $this->$ordering_field));
$query->order($db->qn($ordering_field) . ' DESC');
}
// If the movement delta is positive move the row down.
elseif ($delta > 0)
{
$query->where($db->qn($ordering_field) . ' > ' . $db->q((int) $this->$ordering_field));
$query->order($db->qn($ordering_field) . ' ASC');
}
// Add the custom WHERE clause if set.
if (!empty($where))
{
$query->where($where);
}
// Select the first row with the criteria.
$row = $db->setQuery($query, 0, 1)->loadObject();
// If a row is found, move the item.
if (!empty($row))
{
// Update the ordering field for this instance to the row's ordering value.
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($db->qn($ordering_field) . ' = ' . $db->q((int) $row->$ordering_field))
->where($db->qn($k) . ' = ' . $db->q($this->$k));
$db->setQuery($query)->execute();
// Update the ordering field for the row to this instance's ordering value.
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($db->qn($ordering_field) . ' = ' . $db->q((int) $this->$ordering_field))
->where($db->qn($k) . ' = ' . $db->q($row->$k));
$db->setQuery($query)->execute();
// Update the instance value.
$this->$ordering_field = $row->$ordering_field;
}
$this->triggerEvent('onAfterMove');
return $this;
}
/**
* Process a large collection of records a few at a time.
*
* @param integer $chunkSize How many records to process at once
* @param callable $callback A callable to process each record
*
* @return $this Self, for chaining
*/
public function chunk($chunkSize, $callback)
{
$totalItems = $this->count();
if ($totalItems === 0)
{
return $this;
}
$start = 0;
while ($start < ($totalItems - 1))
{
$this->get(true, $start, $chunkSize)->transform($callback);
$start += $chunkSize;
}
return $this;
}
/**
* Get the number of all items
*
* @return integer
*/
public function count()
{
// Get a "count all" query
$db = $this->getDbo();
$query = $this->buildQuery(true);
$query->clear('select')->clear('order')->select('COUNT(*)');
// Run the "before build query" hook and behaviours
$this->triggerEvent('onBuildCountQuery', [&$query]);
return $db->setQuery($query)->loadResult();
}
/**
* Build the query to fetch data from the database
*
* @param boolean $overrideLimits Should I override limits
*
* @return \JDatabaseQuery The database query to use
*/
public function buildQuery($overrideLimits = false)
{
// Get a "select all" query
$db = $this->getDbo();
$query = $db->getQuery(true)
->select('*')
->from($this->getTableName());
// Run the "before build query" hook and behaviours
$this->triggerEvent('onBeforeBuildQuery', [&$query, $overrideLimits]);
// Apply custom WHERE clauses
if (count($this->whereClauses) > 0)
{
foreach ($this->whereClauses as $clause)
{
$query->where($clause);
}
}
$order = $this->getState('filter_order', null, 'cmd');
if (!array_key_exists($order, $this->knownFields))
{
$order = $this->getIdFieldName();
$this->setState('filter_order', $order);
}
$order = $db->qn($order);
$dir = strtoupper($this->getState('filter_order_Dir', '', 'cmd'));
if (!in_array($dir, ['ASC', 'DESC']))
{
$dir = 'ASC';
$this->setState('filter_order_Dir', $dir);
}
$query->order($order . ' ' . $dir);
// Run the "before after query" hook and behaviours
$this->triggerEvent('onAfterBuildQuery', [&$query, $overrideLimits]);
return $query;
}
/**
* Returns a DataCollection iterator based on your currently set Model state
*
* @param boolean $overrideLimits Should I ignore limits set in the Model?
* @param integer $limitstart How many items to skip from the start, only when $overrideLimits = true
* @param integer $limit How many items to return, only when $overrideLimits = true
*
* @return DataCollection The data collection
*/
public function get($overrideLimits = false, $limitstart = 0, $limit = 0)
{
if (!$overrideLimits)
{
$limitstart = $this->getState('limitstart', 0);
$limit = $this->getState('limit', 0);
}
$dataCollection = DataCollection::make($this->getItemsArray($limitstart, $limit, $overrideLimits));
$this->eagerLoad($dataCollection);
return $dataCollection;
}
/**
* Returns a raw array of DataModel instances based on your currently set Model state
*
* @param integer $limitstart How many items from the start to skip (0 = do not skip)
* @param integer $limit How many items to return (0 = all)
* @param bool $overrideLimits Set to true to override limitstart, limit and ordering
*
* @return array Array of DataModel objects
*/
public function &getItemsArray($limitstart = 0, $limit = 0, $overrideLimits = false)
{
$itemsTemp = $this->getRawDataArray($limitstart, $limit, $overrideLimits);
$items = [];
while (!empty($itemsTemp))
{
$data = array_shift($itemsTemp);
/** @var DataModel $item */
$item = clone $this;
$item->clearState()->reset();
$item->bind($data);
$items[$item->getId()] = $item;
$item->relationManager = clone $this->relationManager;
$item->relationManager->rebase($item);
}
$this->triggerEvent('onAfterGetItemsArray', [&$items]);
return $items;
}
/**
* Returns the raw data array, as fetched from the database, based on your currently set Model state
*
* @param integer $limitstart How many items from the start to skip (0 = do not skip)
* @param integer $limit How many items to return (0 = all)
* @param bool $overrideLimits Set to true to override limitstart, limit and ordering
*
* @return array Array of hashed arrays
*/
public function &getRawDataArray($limitstart = 0, $limit = 0, $overrideLimits = false)
{
$limitstart = max($limitstart, 0);
$limit = max($limit, 0);
$query = $this->buildQuery($overrideLimits);
$db = $this->getDbo();
$db->setQuery($query, $limitstart, $limit);
$rawData = $db->loadAssocList();
return $rawData;
}
/**
* Eager loads the provided relations and assigns their data to a data collection
*
* @param DataCollection $dataCollection The data collection on which the eager loaded relations will be
* applied
* @param array|null $relations The relations to eager load. Leave empty to use the already defined
* relations
*
* @return $this for chaining
*/
public function eagerLoad(DataCollection &$dataCollection, array $relations = null)
{
if (empty($relations))
{
$relations = $this->eagerRelations;
}
// Apply eager loaded relations
if ($dataCollection->count() && !empty($relations))
{
$relationManager = $this->getRelations();
foreach ($relations as $relation => $callback)
{
// Did they give us a relation name without a callback?
if (!is_callable($callback) && is_string($callback) && !empty($callback))
{
$relation = $callback;
$callback = null;
}
$relationData = $relationManager->getData($relation, $callback, $dataCollection);
$foreignKeyMap = $relationManager->getForeignKeyMap($relation);
/** @var DataModel $item */
foreach ($dataCollection as $item)
{
$item->getRelations()->setDataFromCollection($relation, $relationData, $foreignKeyMap);
}
}
}
return $this;
}
/**
* Archive the record, i.e. set enabled to 2
*
* @return $this For chaining
*/
public function archive()
{
if (!$this->getId())
{
throw new RecordNotLoaded("Can't archive a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
return $this;
}
$this->triggerEvent('onBeforeArchive');
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = 2;
$this->save();
$this->triggerEvent('onAfterArchive');
return $this;
}
/**
* Trashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
* is loaded before trying to trash it. Unlike a hard delete, trashing is a "soft delete", only setting the enabled
* field to -2.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function trash($id = null)
{
if (!empty($id))
{
$this->findOrFail($id);
}
$id = $this->getId();
if (!$id)
{
throw new RecordNotLoaded("Can't trash a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
throw new SpecialColumnMissing("DataModel::trash method needs an 'enabled' field");
}
$this->triggerEvent('onBeforeTrash', [&$id]);
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = -2;
$this->save();
$this->triggerEvent('onAfterTrash', [&$id]);
return $this;
}
/**
* Change the publish state of a record. By default it will set it to 1 (published) unless you specify a different
* value.
*
* @param int $state The publish state. Default: 1 (published).
*
* @return $this For chaining
*/
public function publish($state = 1)
{
if (!$this->getId())
{
throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
return $this;
}
$this->triggerEvent('onBeforePublish');
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = $state;
$this->save();
$this->triggerEvent('onAfterPublish');
return $this;
}
/**
* Unpublish the record, i.e. set enabled to 0
*
* @return $this For chaining
*/
public function unpublish()
{
if (!$this->getId())
{
throw new RecordNotLoaded("Can't unpublish a not loaded DataModel");
}
if (!$this->hasField('enabled'))
{
return $this;
}
$this->triggerEvent('onBeforeUnpublish');
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = 0;
$this->save();
$this->triggerEvent('onAfterUnpublish');
return $this;
}
/**
* Untrashes a record, either the currently loaded one or the one specified in $id. If an $id is specified that
* record is loaded before trying to untrash it. Please note that enabled is set to 0 (unpublished) when you untrash
* an item.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function restore($id = null)
{
if (!$this->hasField('enabled'))
{
return $this;
}
if (!empty($id))
{
$this->findOrFail($id);
}
$id = $this->getId();
if (!$id)
{
throw new RecordNotLoaded("Can't change the state of a not loaded DataModel");
}
$this->triggerEvent('onBeforeRestore', [&$id]);
$enabled = $this->getFieldAlias('enabled');
$this->$enabled = 0;
$this->save();
$this->triggerEvent('onAfterRestore', [&$id]);
return $this;
}
/**
* Creates a copy of the current record. After the copy is performed, the data model contains the data of the new
* record.
*
* @param array|DataModel An associative array or object to bind to the DataModel instance. Allows you to
* override values on the copied object.
*
* @return DataModel
*/
public function copy($data = null)
{
$this->triggerEvent('onBeforeCopy');
$this->{$this->idFieldName} = null;
if ($this->hasField('created_by'))
{
$this->setFieldValue('created_by');
}
if ($this->hasField('modified_by'))
{
$this->setFieldValue('modified_by');
}
if ($this->hasField('locked_by'))
{
$this->setFieldValue('locked_by');
}
if ($this->hasField('created_on'))
{
$this->setFieldValue('created_on');
}
if ($this->hasField('modified_on'))
{
$this->setFieldValue('modified_on');
}
if ($this->hasField('locked_on'))
{
$this->setFieldValue('locked_on');
}
$result = $this->save($data);
$this->triggerEvent('onAfterCopy', [&$result]);
return $result;
}
/**
* Check-in an item. This works similar to unlock() but performs additional checks. If the item is locked by another
* user you need to have adequate ACL privileges to unlock it, i.e. core.admin or core.manage component-wide
* privileges; core.edit.state privileges component-wide or per asset; or be the creator of the item and have
* core.edit.own privileges component-wide or per asset.
*
* @return $this
*
* @throws LockedRecord If you don't have the privilege to check in this item
*/
public function checkIn($userId = null)
{
// If there is no loaded record we can't do much, I'm afraid
if (!$this->getId())
{
throw new RecordNotLoaded("Can't checkin a not loaded DataModel");
}
// If the lock fields are missing we have nothing to do
if (!$this->hasField('locked_by') && !$this->hasField('locked_on'))
{
return $this;
}
// If there's no locked_by field we just unlock and return
if (!$this->hasField('locked_by'))
{
return $this->unlock();
}
// If the current user and the user who locked the record are the same, unlock it.
if (empty($userId))
{
$userId = $this->container->platform->getUser()->id;
}
$lockedBy = $this->getFieldValue('locked_by');
if (empty($lockedBy) || ($lockedBy == $userId))
{
return $this->unlock();
}
// Get the component privileges
$platform = $this->container->platform;
$component = $this->container->componentName;
$privileges = [
'editown' => $platform->authorise('core.edit.own', $component),
'editstate' => $platform->authorise('core.edit.state', $component),
'admin' => $platform->authorise('core.admin', $component),
'manage' => $platform->authorise('core.manage', $component),
];
// If we are trackign assets get the item's privileges
if ($this->isAssetsTracked())
{
$assetKey = $this->getAssetKey();
$assetPrivileges = [
'editown' => $platform->authorise('core.edit.own', $assetKey),
'editstate' => $platform->authorise('core.edit.state', $assetKey),
];
foreach ($assetPrivileges as $k => $v)
{
$privileges[$k] = $privileges[$k] || $v;
}
}
// If you are a Super User, component manager or allowed to edit the state of records we unlock it
if ($privileges['admin'] || $privileges['manage'] || $privileges['editstate'])
{
return $this->unlock();
}
// If you are the owner of the record and have core.edit.own privilege we will unlock it.
$owner = 0;
if ($this->hasField('created_by'))
{
$owner = $this->getFieldValue('created_by');
}
if ($privileges['editown'] && ($owner == $userId))
{
return $this->unlock();
}
// All else failed, you don't have the privilege to unlock this item.
throw new LockedRecord;
}
/**
* Reset the record data
*
* @param boolean $useDefaults Should I use the default values? Default: yes
* @param boolean $resetRelations Should I reset the relations too? Default: no
*
* @return static Self, for chaining
*/
public function reset($useDefaults = true, $resetRelations = false)
{
$this->recordData = [];
$this->whereClauses = [];
foreach ($this->knownFields as $fieldName => $information)
{
$this->recordData[$fieldName] = $useDefaults ? $information->Default : null;
}
if ($resetRelations)
{
$this->relationManager->resetRelationData();
$this->eagerRelations = [];
}
$this->relationFilters = [];
$this->triggerEvent('onAfterReset', [$useDefaults, $resetRelations]);
return $this;
}
/**
* Automatically performs a hard or soft delete, based on the value of $this->softDelete. A soft delete simply sets
* enabled to -2 whereas a hard delete removes the data from the database. If you want to force a specific behaviour
* directly call trash() for a soft delete or forceDelete() for a hard delete.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function delete($id = null)
{
if ($this->softDelete)
{
return $this->trash($id);
}
else
{
return $this->forceDelete($id);
}
}
/**
* Delete a record, either the currently loaded one or the one specified in $id. If an $id is specified that record
* is loaded before trying to delete it. In the end the data model is reset.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
*/
public function forceDelete($id = null)
{
if (!empty($id))
{
$this->findOrFail($id);
}
$id = $this->getId();
if (!$id)
{
throw new RecordNotLoaded("Can't delete a not loaded DataModel object");
}
$this->triggerEvent('onBeforeDelete', [&$id]);
$db = $this->getDbo();
$query = $db->getQuery(true)
->delete()
->from($this->tableName)
->where($db->qn($this->idFieldName) . ' = ' . $db->q($id));
$db->setQuery($query)->execute();
$this->triggerEvent('onAfterDelete', [&$id]);
$this->reset();
return $this;
}
/**
* Generic check for whether dependencies exist for this object in the db schema. This method is NOT used by
* default. If you want to use it you will have to override your delete(), trash() or forceDelete() method,
* or create an onBeforeDelete and/or onBeforeTrash event handler.
*
* @param integer $oid The primary key of the record to delete
* @param array $joins Any joins to foreign table, used to determine if dependent records exist
*
* @return void
*
* @throws \RuntimeException If you should not delete the record (the message tells you why)
*/
public function canDelete($oid = null, $joins = null)
{
$pkField = $this->getKeyName();
if ($oid)
{
$this->$pkField = (int) $oid;
}
if (!$this->$pkField)
{
throw new \InvalidArgumentException('Master table should be loaded or an ID should be passed');
}
if (is_array($joins))
{
$db = $this->getDbo();
$query = $db->getQuery(true)
->select($db->qn('master') . '.' . $db->qn($pkField))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('master'));
$tableNo = 0;
foreach ($joins as $table)
{
// Sanity check on passed array
$check = ['idfield', 'idalias', 'name', 'joinfield', 'label'];
$result = array_intersect($check, array_keys($table));
if (count($result) != count($check))
{
throw new \InvalidArgumentException('Join array missing some keys, please check the documentation');
}
$tableNo++;
$query->select(
[
'COUNT(DISTINCT ' . $db->qn('t' . $tableNo) .
'.' . $db->qn($table['idfield']) . ') AS ' . $db->qn($table['idalias']),
]
);
$query->join('LEFT', $db->qn($table['name']) .
' AS ' . $db->qn('t' . $tableNo) .
' ON ' . $db->qn('t' . $tableNo) . '.' . $db->qn($table['joinfield']) .
' = ' . $db->qn('master') . '.' . $db->qn($pkField)
);
}
$query->where($db->qn('master') . '.' . $db->qn($pkField) . ' = ' . $db->q($this->$pkField));
$query->group($db->qn('master') . '.' . $db->qn($pkField));
$this->getDbo()->setQuery((string) $query);
$obj = $this->getDbo()->loadObject();
$msg = [];
$i = 0;
foreach ($joins as $table)
{
$pkField = $table['idalias'];
if ($obj->$pkField > 0)
{
$msg[] = Text::_($table['label']);
}
$i++;
}
if (count($msg) > 0)
{
$option = $this->container->componentName;
$comName = $this->container->bareComponentName;
$tbl = $this->getTableName();
$tview = str_replace('#__' . $comName . '_', '', $tbl);
$prefix = $option . '_' . $tview . '_NODELETE_';
$message = '<ul>';
foreach ($msg as $key)
{
$message .= '<li>' . Text::_(strtoupper($prefix . $key)) . '</li>';
}
$message .= '</ul>';
throw new \RuntimeException($message);
}
}
}
/**
* Find and load a single record based on the provided key values. If the record is not found an exception is thrown
*
* @param array|mixed $keys An optional primary key value to load the row by, or an array of fields to match.
* If not set the "id" state variable or, if empty, the identity column's value is used
*
* @return static Self, for chaining
*
* @throws \RuntimeException When the row is not found
*/
public function findOrFail($keys = null)
{
$this->find($keys);
// We have to assign the value, since empty() is not triggering the __get magic method
// http://stackoverflow.com/questions/2045791/php-empty-on-get-accessor
$value = $this->getId();
if (empty($value))
{
throw new RecordNotLoaded;
}
return $this;
}
/**
* Method to load a row from the database by primary key. Used for TableInterface compatibility.
*
* @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. If
* not set the instance property value is used.
* @param boolean $reset True to reset the default values before loading the new row.
*
* @return boolean True if successful. False if row not found.
*
* @throws \RuntimeException
* @throws \UnexpectedValueException
* @link http://docs.joomla.org/JTable/load
* @since 3.2
*/
public function load($keys = null, $reset = true)
{
if ($reset)
{
$this->reset();
}
try
{
$this->findOrFail($keys);
}
catch (\Exception $e)
{
return false;
}
return true;
}
/**
* Find and load a single record based on the provided key values
*
* @param array|mixed $keys An optional primary key value to load the row by, or an array of fields to match.
* If not set the "id" state variable or, if empty, the identity column's value is used
*
* @return static Self, for chaining
*/
public function find($keys = null)
{
// Execute the onBeforeLoad event
$this->triggerEvent('onBeforeLoad', [&$keys]);
// If we are not given any keys, try to get the ID from the state or the table data
if (empty($keys))
{
$id = $this->getState('id', 0);
if (empty($id))
{
$id = $this->getId();
}
if (empty($id))
{
$this->triggerEvent('onAfterLoad', [false, &$keys]);
$this->reset();
return $this;
}
$keys = [$this->idFieldName => $id];
}
elseif (!is_array($keys))
{
if (empty($keys))
{
$this->triggerEvent('onAfterLoad', [false, &$keys]);
$this->reset();
return $this;
}
$keys = [$this->idFieldName => $keys];
}
// Reset the table
$this->reset();
// Get the query
$db = $this->getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->qn($this->tableName));
// Apply key filters
foreach ($keys as $filterKey => $filterValue)
{
if ($filterKey == 'id')
{
$filterKey = $this->getIdFieldName();
}
if (array_key_exists($filterKey, $this->recordData))
{
$query->where($db->qn($filterKey) . ' = ' . $db->q($filterValue));
}
}
// Get the row
$db->setQuery($query);
try
{
$row = $db->loadAssoc();
}
catch (\Exception $e)
{
$row = null;
}
if (empty($row))
{
$this->triggerEvent('onAfterLoad', [false, &$keys]);
return $this;
}
// Bind the data
$this->bind($row);
$this->relationManager->rebase($this);
// Execute the onAfterLoad event
$this->triggerEvent('onAfterLoad', [true, &$keys]);
return $this;
}
/**
* Create a new record with the provided data
*
* @param array $data The data to use in the new record
*
* @return static Self, for chaining
*/
public function create($data)
{
return $this->reset()->bind($data)->save();
}
/**
* Return the first item found or create a new one based on the provided $data
*
* @param array $data Data for the newly created item
*
* @return static
*/
public function firstOrCreate($data)
{
$item = $this->get(true, 0, 1)->first();
if (is_null($item))
{
$item = clone $this;
$item->create($data);
}
return $item;
}
/**
* Return the first item found or throw a \RuntimeException
*
* @return static
*
* @throws \RuntimeException
*/
public function firstOrFail()
{
$item = $this->get(true, 0, 1)->first();
if (is_null($item))
{
throw new NoItemsFound(get_class($this));
}
return $item;
}
/**
* Return the first item found or create a new, blank one
*
* @return static
*/
public function firstOrNew()
{
$item = $this->get(true, 0, 1)->first();
if (is_null($item))
{
$item = clone $this;
$item->reset();
}
return $item;
}
/**
* Adds a behaviour by its name. It will search the following classes, in this order:
* \component_namespace\Model\modelName\Behaviour\behaviourName
* \component_namespace\Model\Behaviour\behaviourName
* \FOF40\Model\DataModel\Behaviour\behaviourName
* where:
* component_namespace is the namespace of the component as defined in the container
* modelName is the model's name, first character uppercase, e.g. Baz
* behaviourName is the $behaviour parameter, first character uppercase, e.g. Something
*
* @param string $behaviour The behaviour's name
*
* @return $this Self, for chaining
*/
public function addBehaviour($behaviour)
{
$prefixes = [
$this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()),
$this->container->getNamespacePrefix() . 'Model\\Behaviour',
'\\FOF40\\Model\\DataModel\\Behaviour',
];
foreach ($prefixes as $prefix)
{
$className = $prefix . '\\' . ucfirst($behaviour);
if (class_exists($className, true) && !$this->behavioursDispatcher->hasObserverClass($className))
{
/** @var Observer $o */
$observer = new $className($this->behavioursDispatcher);
$this->behavioursDispatcher->attach($observer);
return $this;
}
}
return $this;
}
/**
* Removes a behaviour by its name. It will search the following classes, in this order:
* \component_namespace\Model\modelName\Behaviour\behaviourName
* \component_namespace\Model\DataModel\Behaviour\behaviourName
* \FOF40\Model\DataModel\Behaviour\behaviourName
* where:
* component_namespace is the namespace of the component as defined in the container
* modelName is the model's name, first character uppercase, e.g. Baz
* behaviourName is the $behaviour parameter, first character uppercase, e.g. Something
*
* @param string $behaviour The behaviour's name
*
* @return $this Self, for chaining
*/
public function removeBehaviour($behaviour)
{
$prefixes = [
$this->container->getNamespacePrefix() . 'Model\\Behaviour\\' . ucfirst($this->getName()),
$this->container->getNamespacePrefix() . 'Model\\Behaviour',
'\\FOF40\\Model\\DataModel\\Behaviour',
];
foreach ($prefixes as $prefix)
{
$className = ltrim($prefix . '\\' . ucfirst($behaviour), '\\');
$observer = $this->behavioursDispatcher->getObserverByClass($className);
if (is_null($observer))
{
continue;
}
$this->behavioursDispatcher->detach($observer);
return $this;
}
return $this;
}
/**
* Gives you access to the behaviours dispatcher, allowing to attach/detach behaviour observers
*
* @return Dispatcher
*/
public function &getBehavioursDispatcher()
{
return $this->behavioursDispatcher;
}
/**
* Set the field and direction of ordering for the query returned by buildQuery.
* Alias of $this->setState('filter_order', $fieldName) and $this->setState('filter_order_Dir', $direction)
*
* @param string $fieldName The field name to order by
* @param string $direction The direction to order by (ASC for ascending or DESC for descending)
*
* @return $this For chaining
*/
public function orderBy($fieldName, $direction = 'ASC')
{
$direction = strtoupper($direction);
if (!in_array($direction, ['ASC', 'DESC']))
{
$direction = 'ASC';
}
$this->setState('filter_order', $fieldName);
$this->setState('filter_order_Dir', $direction);
return $this;
}
/**
* Set the limitStart for the query, i.e. how many records to skip.
* Alias of $this->setState('limitstart', $limitStart);
*
* @param integer $limitStart Records to skip from the start
*
* @return $this For chaining
*/
public function skip($limitStart = null)
{
// Only positive integers are allowed
if (!is_int($limitStart) || $limitStart < 0 || !$limitStart)
{
$limitStart = 0;
}
$this->setState('limitstart', $limitStart);
return $this;
}
/**
* Set the limit for the query, i.e. how many records to return.
* Alias of $this->setState('limit', $limit);
*
* @param integer $limit Maximum number of records to return
*
* @return $this For chaining
*/
public function take($limit = null)
{
// Only positive integers are allowed
if (!is_int($limit) || $limit < 0 || !$limit)
{
$limit = 0;
}
$this->setState('limit', $limit);
return $this;
}
/**
* Return the record's data as an array
*
* @return array
*/
public function toArray()
{
return $this->recordData;
}
/**
* Returns the record's data as a JSON string
*
* @param boolean $prettyPrint Should I format the JSON for pretty printing
*
* @return string
*/
public function toJson($prettyPrint = false)
{
if (defined('JSON_PRETTY_PRINT'))
{
$options = $prettyPrint ? JSON_PRETTY_PRINT : 0;
}
else
{
$options = 0;
}
return json_encode($this->recordData, $options);
}
/**
* Touch a record, updating its modified_on and/or modified_by columns
*
* @param integer $userId Optional user ID of the user touching the record
*
* @return $this Self, for chaining
*/
public function touch($userId = null)
{
if (!$this->getId())
{
throw new RecordNotLoaded("Can't touch a not loaded DataModel");
}
if (!$this->hasField('modified_on') && !$this->hasField('modified_by'))
{
return $this;
}
$db = $this->getDbo();
$date = new Date();
// Update the created_on / modified_on
if ($this->hasField('modified_on'))
{
$modified_on = $this->getFieldAlias('modified_on');
$this->$modified_on = $date->toSql(false, $db);
}
// Update the created_by / modified_by values if necessary
if ($this->hasField('modified_by'))
{
if (empty($userId))
{
$userId = $this->container->platform->getUser()->id;
}
$modified_by = $this->getFieldAlias('modified_by');
$this->$modified_by = $userId;
}
$this->save();
return $this;
}
/**
* Lock a record by setting its locked_on and/or locked_by columns
*
* @param integer $userId
*
* @return $this Self, for chaining
*/
public function lock($userId = null)
{
if (!$this->getId())
{
throw new CannotLockNotLoadedRecord;
}
if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
{
return $this;
}
$this->triggerEvent('onBeforeLock');
$db = $this->getDbo();
if ($this->hasField('locked_on'))
{
$date = new Date();
$locked_on = $this->getFieldAlias('locked_on');
$this->$locked_on = $date->toSql(false, $db);
}
if ($this->hasField('locked_by'))
{
if (empty($userId))
{
$userId = $this->container->platform->getUser()->id;
}
$locked_by = $this->getFieldAlias('locked_by');
$this->$locked_by = $userId;
}
$this->save();
$this->triggerEvent('onAfterLock');
return $this;
}
/**
* Unlock a record by resetting its locked_on and/or locked_by columns
*
* @return $this Self, for chaining
*/
public function unlock()
{
if (!$this->getId())
{
throw new RecordNotLoaded("Can't unlock a not loaded DataModel");
}
if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
{
return $this;
}
$this->triggerEvent('onBeforeUnlock');
$db = $this->getDbo();
if ($this->hasField('locked_on'))
{
$locked_on = $this->getFieldAlias('locked_on');
$this->$locked_on = $this->isNullableField('locked_on') ? null : $db->getNullDate();
}
if ($this->hasField('locked_by'))
{
$locked_by = $this->getFieldAlias('locked_by');
$this->$locked_by = 0;
}
$this->save();
$this->triggerEvent('onAfterUnlock');
return $this;
}
/**
* Is this record locked by a different user than $userId?
*
* @param integer $userId
*
* @return bool True if the record is locked
*/
public function isLocked($userId = null)
{
if (!$this->hasField('locked_on') && !$this->hasField('locked_by'))
{
return false;
}
$nullDate = $this->isNullableField('locked_on') ? null : $this->getDbo()->getNullDate();
// Get the locked_by / locked_on
$locked_on = $nullDate;
$locked_by = 0;
if ($this->hasField('locked_on'))
{
$locked_on = $this->getFieldValue('locked_on', $nullDate);
if (empty($locked_on))
{
$locked_on = $nullDate;
}
}
if ($this->hasField('locked_by'))
{
$locked_by = $this->getFieldValue('locked_by', 0);
if (empty($locked_by))
{
$locked_by = 0;
}
}
$allowedUsers = [0];
if (!empty($userId))
{
$allowedUsers[] = $userId;
}
if (in_array($locked_by, $allowedUsers))
{
return false;
}
return !is_null($locked_on) && ($locked_on !== $nullDate);
}
/**
* Automatically uses the Filters behaviour to filter records in the model based on your criteria.
*
* @param string $fieldName The field name to filter on
* @param string $method The filtering method, e.g. <>, =, != and so on
* @param mixed $values The value you're filtering on. Some filters (e.g. interval or between) require an
* array of values
*
* @return $this For chaining
*/
public function where($fieldName, $method = '=', $values = null)
{
// Make sure the Filters behaviour is added to the model
if (!$this->behavioursDispatcher->hasObserverClass('FOF40\\Model\\DataModel\\Behaviour\\Filters'))
{
$this->addBehaviour('filters');
}
// If we are dealing with the primary key, let's set the field name to "id". This is a convention and it will
// be used inside the Filters behaviour
// -- Let's not do this. The Filters behaviour works just fine with the regular field name!
/**
* if ($fieldName == $this->getIdFieldName())
* {
* $fieldName = 'id';
* }
**/
$options = [
'method' => $method,
'value' => $values,
];
// Handle method aliases
switch ($method)
{
case '<>':
$options['method'] = 'search';
$options['operator'] = '!=';
break;
case 'lt':
$options['method'] = 'search';
$options['operator'] = '<';
break;
case 'le':
$options['method'] = 'search';
$options['operator'] = '<=';
break;
case 'gt':
$options['method'] = 'search';
$options['operator'] = '>';
break;
case 'ge':
$options['method'] = 'search';
$options['operator'] = '>=';
break;
case 'eq':
$options['method'] = 'search';
$options['operator'] = '=';
break;
case 'neq':
case 'ne':
$options['method'] = 'search';
$options['operator'] = '!=';
break;
case '<':
case '!<':
case '<=':
case '!<=':
case '>':
case '!>':
case '>=':
case '!>=':
case '!=':
case '=':
$options['method'] = 'search';
$options['operator'] = $method;
break;
case 'like':
case '~':
case '%':
$options['method'] = 'partial';
break;
case '==':
case '=[]':
case '=()':
case 'in':
$options['method'] = 'exact';
break;
case '()':
case '[]':
case '[)':
case '(]':
$options['method'] = 'between';
break;
case ')(':
case ')[':
case '](':
case '][':
$options['method'] = 'outside';
break;
case '*=':
case 'every':
$options['method'] = 'interval';
break;
case '?=':
$options['method'] = 'search';
break;
default:
throw new InvalidSearchMethod('Method ' . $method . ' is unsupported');
}
// Handle real methods
switch ($options['method'])
{
case 'between':
case 'outside':
if (is_array($values) && (count($values) > 1))
{
// Get the from and to values from the $values array
if (isset($values['from']) && isset($values['to']))
{
$options['from'] = $values['from'];
$options['to'] = $values['to'];
}
else
{
$options['from'] = array_shift($values);
$options['to'] = array_shift($values);
}
unset($options['value']);
}
else
{
// $values is not a from/to array. Treat as = (between) or != (outside)
if (is_array($values))
{
$values = array_shift($values);
}
$options['operator'] = ($options['method'] == 'between') ? '=' : '!=';
$options['value'] = $values;
$options['method'] = 'search';
}
break;
case 'interval':
if (is_array($values) && (count($values) > 1))
{
// Get the value and interval from the $values array
if (isset($values['value']) && isset($values['interval']))
{
$options['value'] = $values['value'];
$options['interval'] = $values['interval'];
}
else
{
$options['value'] = array_shift($values);
$options['interval'] = array_shift($values);
}
}
else
{
// $values is not a value/interval array. Treat as =
if (is_array($values))
{
$values = array_shift($values);
}
$options['value'] = $values;
$options['method'] = 'search';
$options['operator'] = '=';
}
break;
case 'search':
// We don't have to do anything if the operator is already set
if (isset($options['operator']))
{
break;
}
if (is_array($values) && (count($values) > 1))
{
// Get the operator and value from the $values array
if (isset($values['operator']) && isset($values['value']))
{
$options['operator'] = $values['operator'];
$options['value'] = $values['value'];
}
else
{
$options['operator'] = array_shift($values);
$options['value'] = array_shift($values);
}
}
break;
}
$this->setState($fieldName, $options);
return $this;
}
/**
* Add custom, pre-compiled WHERE clauses for use in buildQuery. The raw WHERE clause you specify is added as is to
* the query generated by buildQuery. You are responsible for quoting and escaping the field names and data found
* inside the WHERE clause.
*
* Using this method is a generally bad idea. You are better off overriding buildQuery and using state variables to
* customise the query build built instead of using this method to push raw SQL to the query builder. Mixing your
* business logic with raw SQL makes your application harder to maintain and refactor as dependencies to your
* database schema creep in areas of your code that should have nothing to do with it.
*
* @param string $rawWhereClause The raw WHERE clause to add
*
* @return $this For chaining
*/
public function whereRaw($rawWhereClause)
{
$this->whereClauses[] = $rawWhereClause;
return $this;
}
/**
* Instructs the model to eager load the specified relations. The $relations array can have the format:
*
* array('relation1', 'relation2')
* Eager load relation1 and relation2 without any callbacks
* array('relation1' => $callable1, 'relation2' => $callable2)
* Eager load relation1 with callback $callable1 etc
* array('relation1', 'relation2' => $callable2)
* Eager load relation1 without a callback, relation2 with callback $callable2
*
* The callback must have the signature function(\JDatabaseQuery $query) and doesn't return a value. It is
* supposed to modify the query directly.
*
* Please note that eager loaded relations produce their queries without going through the respective model. Instead
* they generate a SQL query directly, then map the loaded results into a DataCollection.
*
* @param array $relations The relations to eager load. See above for more information.
*
* @return $this For chaining
*/
public function with(array $relations)
{
if (empty($relations))
{
$this->eagerRelations = [];
return $this;
}
$knownRelations = $this->relationManager->getRelationNames();
foreach ($relations as $k => $v)
{
if (is_callable($v))
{
$relName = $k;
$callback = $v;
}
else
{
$relName = $v;
$callback = null;
}
if (in_array($relName, $knownRelations))
{
$this->eagerRelations[$relName] = $callback;
}
}
return $this;
}
/**
* Filter the model based on the fulfilment of relations. For example:
* $posts->has('comments', '>=', 10)->get();
* will return all posts with at least 10 comments.
*
* @param string $relation The relation to query
* @param string $operator The comparison operator. Same operators as the where() method.
* @param mixed $value The value(s) to compare against.
* @param bool $replace When true (default) any existing relation filters for the same relation will be
* replaced
*
* @return $this
*/
public function has($relation, $operator = '>=', $value = 1, $replace = true)
{
// Make sure the Filters behaviour is added to the model
if (!$this->behavioursDispatcher->hasObserverClass('FOF40\\Model\\DataModel\\Behaviour\\RelationFilters'))
{
$this->addBehaviour('relationFilters');
}
$filter = [
'relation' => $relation,
'method' => $operator,
'operator' => $operator,
'value' => $value,
];
// Handle method aliases
switch ($operator)
{
case '<>':
$filter['method'] = 'search';
$filter['operator'] = '!=';
break;
case 'lt':
$filter['method'] = 'search';
$filter['operator'] = '<';
break;
case 'le':
$filter['method'] = 'search';
$filter['operator'] = '<=';
break;
case 'gt':
$filter['method'] = 'search';
$filter['operator'] = '>';
break;
case 'ge':
$filter['method'] = 'search';
$filter['operator'] = '>=';
break;
case 'eq':
$filter['method'] = 'search';
$filter['operator'] = '=';
break;
case 'neq':
case 'ne':
$filter['method'] = 'search';
$filter['operator'] = '!=';
break;
case '<':
case '!<':
case '<=':
case '!<=':
case '>':
case '!>':
case '>=':
case '!>=':
case '!=':
case '=':
$filter['method'] = 'search';
$filter['operator'] = $operator;
break;
case 'like':
case '~':
case '%':
$filter['method'] = 'partial';
break;
case '==':
case '=[]':
case '=()':
case 'in':
$filter['method'] = 'exact';
break;
case '()':
case '[]':
case '[)':
case '(]':
$filter['method'] = 'between';
break;
case ')(':
case ')[':
case '](':
case '][':
$filter['method'] = 'outside';
break;
case '*=':
case 'every':
$filter['method'] = 'interval';
break;
case '?=':
$filter['method'] = 'search';
break;
case 'callback':
$filter['method'] = 'callback';
$filter['operator'] = 'callback';
break;
default:
throw new InvalidSearchMethod('Operator ' . $operator . ' is unsupported');
}
// Handle real methods
switch ($filter['method'])
{
case 'between':
case 'outside':
if (is_array($value) && (count($value) > 1))
{
// Get the from and to values from the $value array
if (isset($value['from']) && isset($value['to']))
{
$filter['from'] = $value['from'];
$filter['to'] = $value['to'];
}
else
{
$filter['from'] = array_shift($value);
$filter['to'] = array_shift($value);
}
unset($filter['value']);
}
else
{
// $value is not a from/to array. Treat as = (between) or != (outside)
if (is_array($value))
{
$value = array_shift($value);
}
$filter['operator'] = ($filter['method'] == 'between') ? '=' : '!=';
$filter['value'] = $value;
$filter['method'] = 'search';
}
break;
case 'interval':
if (is_array($value) && (count($value) > 1))
{
// Get the value and interval from the $value array
if (isset($value['value']) && isset($value['interval']))
{
$filter['value'] = $value['value'];
$filter['interval'] = $value['interval'];
}
else
{
$filter['value'] = array_shift($value);
$filter['interval'] = array_shift($value);
}
}
else
{
// $value is not a value/interval array. Treat as =
if (is_array($value))
{
$value = array_shift($value);
}
$filter['value'] = $value;
$filter['method'] = 'search';
$filter['operator'] = '=';
}
break;
case 'search':
// We don't have to do anything if the operator is already set
if (isset($filter['operator']))
{
break;
}
if ((is_array($value) || $value instanceof \Countable ? count($value) : 0) > 1)
{
// Get the operator and value from the $value array
if (isset($value['operator']) && isset($value['value']))
{
$filter['operator'] = $value['operator'];
$filter['value'] = $value['value'];
}
else
{
$filter['operator'] = array_shift($value);
$filter['value'] = array_shift($value);
}
}
break;
case 'callback':
if (!is_callable($filter['value']))
{
$filter['method'] = 'search';
$filter['operator'] = '=';
$filter['value'] = 1;
}
break;
}
if ($replace && !empty($this->relationFilters))
{
foreach ($this->relationFilters as $k => $v)
{
if ($v['relation'] == $relation)
{
unset ($this->relationFilters[$k]);
}
}
}
$this->relationFilters[] = $filter;
return $this;
}
/**
* Advanced model filtering on the fulfilment of relations. Unlike has() you can provide your own callback which
* modifies the COUNT subquery used to compare against the relation. The $callBack has the signature
* function(\JDatabaseQuery $query)
* and MUST return a string. The $query you are passed is the COUNT subquery of the relation, e.g.
* SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id
* You have to return a WHERE clause for the model's query, e.g.
* (SELECT COUNT(*) FROM #__comments AS reltbl WHERE reltbl.user_id = user_id) BETWEEN 1 AND 20
*
* @param string $relation The relation to query against
* @param callable $callBack The callback to use for filtering
* @param bool $replace When true (default) any existing relation filters for the same relation will be
* replaced
*
* @return $this
*/
public function whereHas($relation, $callBack, $replace = true)
{
$this->has($relation, 'callback', $callBack, $replace);
return $this;
}
/**
* Returns the relations manager of the model
*
* @return RelationManager
*/
public function &getRelations()
{
return $this->relationManager;
}
/**
* Gets the relation filter definitions, for use by the RelationFilters behaviour
*
* @return array
*/
public function getRelationFilters()
{
return $this->relationFilters;
}
/**
* Returns the list of relations which are touched by save() and touch()
*
* @return array
*/
public function &getTouches()
{
return $this->touches;
}
/**
* Method to get the rules for the record.
*
* @return Rules object
*/
public function getRules()
{
return $this->rules;
}
/**
* Method to set rules for the record.
*
* @param mixed $input A Rules object, JSON string, or array.
*
* @return void
*/
public function setRules($input)
{
$this->rules = $input instanceof Rules ? $input : new Rules($input);
}
/**
* Method to check if the record is treated as an ACL asset
*
* @return boolean [description]
*/
public function isAssetsTracked()
{
return $this->trackAssets;
}
/**
* Method to manually set this record as ACL asset or not.
* We have to do this since the automatic check is made in the constructor, but here we can't set any alias.
* So, even if you have an alias for `asset_id`, it wouldn't be recognized and assets won't be tracked.
*
* @param $state
*/
public function setAssetsTracked($state)
{
$state = (bool) $state;
$this->trackAssets = $state;
}
/**
* Gets the has tags switch state
*
* @return bool
*/
public function hasTags()
{
return $this->has_tags;
}
/**
* Sets the has tags switch state
*
* @param bool $newState
*/
public function setHasTags($newState = false)
{
$this->has_tags = $newState;
}
/**
* Method to compute the default name of the asset.
* The default name is in the form table_name.id
* where id is the value of the primary key of the table.
*
* @return string
* @throws NoAssetKey
*
*/
public function getAssetName()
{
$k = $this->getKeyName();
// If there is no assetKey defined, stop here, or we'll get a wrong name
if (!$this->assetKey || !$this->$k)
{
throw new NoAssetKey;
}
return $this->assetKey . '.' . (int) $this->$k;
}
/**
* Method to compute the default name of the asset.
* The default name is in the form table_name.id
* where id is the value of the primary key of the table.
*
* @return string
*/
public function getAssetKey()
{
return $this->assetKey;
}
/**
* This method sets the asset key for the items of this table. Obviously, it
* is only meant to be used when you have a table with an asset field.
*
* @param string $assetKey The name of the asset key to use
*
* @return void
*/
public function setAssetKey($assetKey)
{
$this->assetKey = $assetKey;
}
/**
* Method to return the title to use for the asset table. In
* tracking the assets a title is kept for each asset so that there is some
* context available in a unified access manager. Usually this would just
* return $this->title or $this->name or whatever is being used for the
* primary name of the row. If this method is not overridden, the asset name is used.
*
* @return string The string to use as the title in the asset table.
*
* @codeCoverageIgnore
*/
public function getAssetTitle()
{
return $this->getAssetName();
}
/**
* Method to get the parent asset under which to register this one.
* By default, all assets are registered to the ROOT node with ID,
* which will default to 1 if none exists.
* The extended class can define a table and id to lookup. If the
* asset does not exist it will be created.
*
* @param DataModel $model A model object for the asset parent.
* @param integer $id Id to look up
*
* @return integer
*/
public function getAssetParentId($model = null, $id = null)
{
// For simple cases, parent to the asset root.
$assets = new Asset($this->getDbo());
$rootId = $assets->getRootId();
if (!empty($rootId))
{
return $rootId;
}
return 1;
}
/**
* Method to load a row for editing from the version history table.
*
* @param integer $version_id Key to the version history table.
* @param string $alias The type_alias in #__content_types
*
* @return boolean True on success
*
* @throws RecordNotLoaded
* @throws BaseException
* @since 2.3
*
*/
public function loadhistory($version_id, $alias)
{
// Only attempt to check the row in if it exists.
if (empty($version_id))
{
throw new RecordNotLoaded;
}
// Get an instance of the row to checkout.
$historyTable = new ContentHistory(Factory::getDbo());
if (!$historyTable->load($version_id))
{
throw new BaseException($historyTable->getError());
}
$rowArray = ArrayHelper::fromObject(json_decode($historyTable->version_data));
$contentTypeTable = new ContentType(Factory::getDbo());
$typeId = $contentTypeTable->getTypeId($alias);
if ($historyTable->ucm_type_id != $typeId)
{
$key = $this->getKeyName();
if (isset($rowArray[$key]))
{
$this->{$this->idFieldName} = $rowArray[$key];
$this->unlock();
}
throw new BaseException(Text::_('JLIB_APPLICATION_ERROR_HISTORY_ID_MISMATCH'));
}
$this->setState('save_date', $historyTable->save_date);
$this->setState('version_note', $historyTable->version_note);
$this->bind($rowArray);
return true;
}
/**
* Applies view access level filtering for the specified user. Useful to
* filter a front-end items listing.
*
* @param integer $userID The user ID to use. Skip it to use the currently logged in user.
*
* @return DataModel Reference to self
*/
public function applyAccessFiltering($userID = null)
{
if (!$this->hasField('access'))
{
return $this;
}
$user = $this->container->platform->getUser($userID);
$accessField = $this->getFieldAlias('access');
$this->setState($accessField, $user->getAuthorisedViewLevels());
return $this;
}
/**
* Get the content type for ucm
*
* @return string The content type alias
*
* @throws NoContentType If you have not set the contentType configuration variable
*/
public function getContentType()
{
if (!empty($this->contentType))
{
return $this->contentType;
}
throw new NoContentType(get_class($this));
}
/**
* Check if a UCM content type exists for this resource, and
* create it if it does not
*
* @param string $alias The content type alias (optional)
*
* @return null
*/
public function checkContentType($alias = null)
{
$contentType = new ContentType($this->getDbo());
if (!$alias)
{
$alias = $this->getContentType();
}
$aliasParts = explode('.', $alias);
// Fetch the extension name
$component = $aliasParts[0];
$component = ComponentHelper::getComponent($component);
// Fetch the name using the menu item
$query = $this->getDbo()->getQuery(true);
$query->select('title')->from('#__menu')->where('component_id = ' . (int) $component->id);
$this->getDbo()->setQuery($query);
$component_name = Text::_($this->getDbo()->loadResult());
$name = $component_name . ' ' . ucfirst($aliasParts[1]);
// Create a new content type for our resource
if (!$contentType->load(['type_alias' => $alias]))
{
$contentType->type_title = $name;
$contentType->type_alias = $alias;
$contentType->table = json_encode(
[
'special' => [
'dbtable' => $this->getTableName(),
'key' => $this->getKeyName(),
'type' => $name,
'prefix' => $this->container->getNamespacePrefix() . '\\Model\\',
'class' => $this->getName(),
'config' => [],
],
'common' => [
'dbtable' => '#__ucm_content',
'key' => 'ucm_id',
'type' => 'CoreContent',
'prefix' => 'JTable',
'config' => [],
],
]
);
$contentType->field_mappings = json_encode(
[
'common' => [
0 => [
"core_content_item_id" => $this->getKeyName(),
"core_title" => $this->getUcmCoreAlias('title'),
"core_state" => $this->getUcmCoreAlias('enabled'),
"core_alias" => $this->getUcmCoreAlias('alias'),
"core_created_time" => $this->getUcmCoreAlias('created_on'),
"core_modified_time" => $this->getUcmCoreAlias('created_by'),
"core_body" => $this->getUcmCoreAlias('body'),
"core_hits" => $this->getUcmCoreAlias('hits'),
"core_publish_up" => $this->getUcmCoreAlias('publish_up'),
"core_publish_down" => $this->getUcmCoreAlias('publish_down'),
"core_access" => $this->getUcmCoreAlias('access'),
"core_params" => $this->getUcmCoreAlias('params'),
"core_featured" => $this->getUcmCoreAlias('featured'),
"core_metadata" => $this->getUcmCoreAlias('metadata'),
"core_language" => $this->getUcmCoreAlias('language'),
"core_images" => $this->getUcmCoreAlias('images'),
"core_urls" => $this->getUcmCoreAlias('urls'),
"core_version" => $this->getUcmCoreAlias('version'),
"core_ordering" => $this->getUcmCoreAlias('ordering'),
"core_metakey" => $this->getUcmCoreAlias('metakey'),
"core_metadesc" => $this->getUcmCoreAlias('metadesc'),
"core_catid" => $this->getUcmCoreAlias('cat_id'),
"core_xreference" => $this->getUcmCoreAlias('xreference'),
"asset_id" => $this->getUcmCoreAlias('asset_id'),
],
],
'special' => [
0 => [
],
],
]
);
$ignoreFields = [
$this->getUcmCoreAlias('modified_on', null),
$this->getUcmCoreAlias('modified_by', null),
$this->getUcmCoreAlias('locked_by', null),
$this->getUcmCoreAlias('locked_on', null),
$this->getUcmCoreAlias('hits', null),
$this->getUcmCoreAlias('version', null),
];
$contentType->content_history_options = json_encode(
[
"ignoreChanges" => array_filter($ignoreFields, 'strlen'),
]
);
$contentType->router = '';
$contentType->store();
}
}
/**
* Set a behavior param
*
* @param string $name The name of the param you want to set
* @param mixed $value The value to set
*
* @return $this Self, for chaining
*/
public function setBehaviorParam($name, $value)
{
$this->behaviorParams[$name] = $value;
return $this;
}
/**
* Get a behavior param
*
* @param string $name The name of the param you want to get
* @param mixed $default The default value returned if not set
*
* @return mixed
*/
public function getBehaviorParam($name, $default = null)
{
return $this->behaviorParams[$name] ?? $default;
}
/**
* Set or get the backlisted filters.
*
* Note: passing a null $list to get the filter blacklist is deprecated as of FOF 3.1. Pleas use getBlacklistFilters
* instead.
*
* @param mixed $list A filter or list of filters to backlist. If null return the list of backlisted filter
* @param boolean $reset Reset the blacklist if true
*
* @return null|array Return an array of value if $list is null
*/
public function blacklistFilters($list = null, $reset = false)
{
if (!isset($list))
{
return $this->getBehaviorParam('blacklistFilters', []);
}
if (is_string($list))
{
$list = (array) $list;
}
if (!$reset)
{
$list = array_unique(array_merge($this->getBehaviorParam('blacklistFilters', []), $list));
}
$this->setBehaviorParam('blacklistFilters', $list);
return null;
}
/**
* Get the blacklisted filters.
*
* @return array
*/
public function getBlacklistFilters()
{
return $this->getBehaviorParam('blacklistFilters', []);
}
/**
* This method is called by Joomla! itself when it needs to update the UCM content
*
* @return bool
*/
public function updateUcmContent()
{
// Process the tags
$data = $this->getData();
$alias = $this->getContentType();
$ucmContentTable = new CoreContent(Factory::getDbo());
$ucm = new UCMContent($this, $alias);
$ucmData = !empty($data) ? $ucm->mapData($data) : $ucm->ucmData;
$primaryId = $ucm->getPrimaryKey($ucmData['common']['core_type_id'], $ucmData['common']['core_content_item_id']);
$result = $ucmContentTable->load($primaryId);
$result = $result && $ucmContentTable->bind($ucmData['common']);
$result = $result && $ucmContentTable->check();
$result = $result && $ucmContentTable->store();
$ucmId = $ucmContentTable->core_content_id;
return $result;
}
/**
* Add a field to the list of fields to be ignored by the check() method
*
* @param string $fieldName The field to add (can be a field alias)
*
* @return void
*/
public function addSkipCheckField($fieldName)
{
if (!is_array($this->fieldsSkipChecks))
{
$this->fieldsSkipChecks = [];
}
if (!$this->hasField($fieldName))
{
return;
}
$fieldName = $this->getFieldAlias($fieldName);
if (!in_array($fieldName, $this->fieldsSkipChecks))
{
$this->fieldsSkipChecks[] = $fieldName;
}
}
/**
* Remove a field from the list of fields to be ignored by the check() method
*
* @param string $fieldName The field to remove (can be a field alias)
*
* @return void
*/
public function removeSkipCheckField($fieldName)
{
if (!is_array($this->fieldsSkipChecks))
{
$this->fieldsSkipChecks = [];
return;
}
if (!$this->hasField($fieldName))
{
return;
}
$fieldName = $this->getFieldAlias($fieldName);
if (in_array($fieldName, $this->fieldsSkipChecks))
{
$index = array_search($fieldName, $this->fieldsSkipChecks);
unset($this->fieldsSkipChecks[$index]);
}
}
/**
* Is a field present in the list of fields to be ignored by the check() method?
*
* @param string $fieldName The field to check (can be a field alias)
*
* @return bool True if the field is skipped from checks, false if not or if the field doesn't exist.
*/
public function hasSkipCheckField($fieldName)
{
if (!is_array($this->fieldsSkipChecks))
{
$this->fieldsSkipChecks = [];
return false;
}
if (!$this->hasField($fieldName))
{
return false;
}
$fieldName = $this->getFieldAlias($fieldName);
return in_array($fieldName, $this->fieldsSkipChecks);
}
/**
* Loads the asset table related to this table.
* This will help tests, too, since we can mock this function.
*
* @return bool|Asset False on failure, otherwise Asset
*/
protected function getAsset()
{
$name = $this->getAssetName();
// Do NOT touch Table here -- we are loading the core asset table which is a Joomla Table, not a FOF model
$asset = new Asset(Factory::getDbo());
if ($asset->loadByName($name) === 0)
{
return false;
}
return $asset;
}
/**
* Utility methods that fetches the column name for the field.
* If it does not exists, returns a "null" string
*
* @param string $alias The alias for the column
* @param string $null What to return if no column exists
*
* @return string The column name
*/
protected function getUcmCoreAlias($alias, $null = "null")
{
if (!$this->hasField($alias))
{
return $null;
}
return $this->getFieldAlias($alias);
}
/**
* Returns all lower and upper case permutations of the database prefix
*
* @return array
*/
protected function getPrefixCasePermutations()
{
if (empty(self::$prefixCasePermutations))
{
$prefix = $this->getDbo()->getPrefix();
$suffix = '';
if (substr($prefix, -1) == '_')
{
$suffix = '_';
$prefix = substr($prefix, 0, -1);
}
$letters = str_split($prefix, 1);
$permutations = [''];
foreach ($letters as $nextLetter)
{
$lower = strtolower($nextLetter);
$upper = strtoupper($nextLetter);
$ret = [];
foreach ($permutations as $perm)
{
$ret[] = $perm . $lower;
if ($lower !== $upper)
{
$ret[] = $perm . $upper;
}
$permutations = $ret;
}
}
$permutations = array_merge([
strtolower($prefix),
strtoupper($prefix),
], $permutations);
$permutations = array_map(function ($x) use ($suffix) {
return $x . $suffix;
}, $permutations);
self::$prefixCasePermutations = array_unique($permutations);
}
return self::$prefixCasePermutations;
}
}
Mixin/DateManipulation.php 0000604 00000006315 15245561014 0011603 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\Mixin;
defined('_JEXEC') || die;
use FOF40\Date\Date;
use FOF40\Model\DataModel;
/**
* Trait for date manipulations commonly used in models
*/
trait DateManipulation
{
/**
* Normalise a date into SQL format
*
* @param string $value The date to normalise
* @param string $default The default date to use if the normalised date is invalid or empty (use 'now' for
* current date/time)
*
* @return string
*/
protected function normaliseDate($value, $default = '2001-01-01')
{
/** @var DataModel $this */
$db = $this->container->platform->getDbo();
if (empty($value) || ($value == $db->getNullDate()))
{
$value = $default;
}
if (empty($value) || ($value == $db->getNullDate()))
{
return $value;
}
$regex = '/^\d{1,4}(\/|-)\d{1,2}(\/|-)\d{2,4}[[:space:]]{0,}(\d{1,2}:\d{1,2}(:\d{1,2}){0,1}){0,1}$/';
if (!preg_match($regex, $value))
{
$value = $default;
}
if (empty($value) || ($value == $db->getNullDate()))
{
return $value;
}
$date = new Date($value);
return $date->toSql();
}
/**
* Sort the published up/down times in case they are give out of order. If publish_up equals publish_down the
* foreverDate will be used for publish_down.
*
* @param string $publish_up Publish Up date
* @param string $publish_down Publish Down date
* @param string $foreverDate See above
*
* @return array (publish_up, publish_down)
*/
protected function sortPublishDates($publish_up, $publish_down, $foreverDate = '2038-01-18 00:00:00')
{
$jUp = new Date($publish_up);
$jDown = new Date($publish_down);
if ($jDown->toUnix() < $jUp->toUnix())
{
$temp = $publish_up;
$publish_up = $publish_down;
$publish_down = $temp;
}
elseif ($jDown->toUnix() == $jUp->toUnix())
{
$jDown = new Date($foreverDate);
$publish_down = $jDown->toSql();
}
return [$publish_up, $publish_down];
}
/**
* Publish or unpublish a DataModel item based on its publish_up / publish_down fields
*
* @param DataModel $row The DataModel to publish/unpublish
*
* @return void
*/
protected function publishByDate(DataModel $row)
{
static $uNow = null;
if (is_null($uNow))
{
$jNow = new Date();
$uNow = $jNow->toUnix();
}
/** @var \JDatabaseDriver $db */
$db = $this->container->platform->getDbo();
$triggered = false;
$publishDown = $row->getFieldValue('publish_down');
if (!empty($publishDown) && ($publishDown != $db->getNullDate()))
{
$publish_down = $this->normaliseDate($publishDown, '2038-01-18 00:00:00');
$publish_up = $this->normaliseDate($row->publish_up, '2001-01-01 00:00:00');
$jDown = new Date($publish_down);
$jUp = new Date($publish_up);
if (($uNow >= $jDown->toUnix()) && $row->enabled)
{
$row->enabled = 0;
$triggered = true;
}
elseif (($uNow >= $jUp->toUnix()) && !$row->enabled && ($uNow < $jDown->toUnix()))
{
$row->enabled = 1;
$triggered = true;
}
}
if ($triggered)
{
$row->save();
}
}
}
Mixin/ImplodedArrays.php 0000604 00000002200 15245561014 0011251 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\Mixin;
defined('_JEXEC') || die;
/**
* Trait for dealing with imploded arrays, stored as comma-separated values
*/
trait ImplodedArrays
{
/**
* Converts the loaded comma-separated list into an array
*
* @param string $value The comma-separated list
*
* @return array The exploded array
*/
protected function getAttributeForImplodedArray($value)
{
if (is_array($value))
{
return $value;
}
if (empty($value))
{
return [];
}
$value = explode(',', $value);
return array_map('trim', $value);
}
/**
* Converts an array of values into a comma separated list
*
* @param array|string $value The array of values (or the already imploded array as a string)
*
* @return string The imploded comma-separated list
*/
protected function setAttributeForImplodedArray($value)
{
if (!is_array($value))
{
return $value;
}
$value = array_map('trim', $value);
return implode(',', $value);
}
}
Mixin/JsonData.php 0000604 00000001764 15245561014 0010053 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\Mixin;
defined('_JEXEC') || die;
/**
* Trait for dealing with data stored as JSON-encoded strings
*/
trait JsonData
{
/**
* Converts the loaded JSON string into an array
*
* @param string $value The JSON string
*
* @return array The data
*/
protected function getAttributeForJson($value)
{
if (is_array($value))
{
return $value;
}
if (empty($value))
{
return [];
}
$value = json_decode($value, true);
if (empty($value))
{
return [];
}
return $value;
}
/**
* Converts and array into a JSON string
*
* @param array|string $value The data (or its JSON-encoded form)
*
* @return string The JSON string
*/
protected function setAttributeForJson($value)
{
if (!is_array($value))
{
return $value;
}
return json_encode($value);
}
}
Mixin/Generators.php 0000604 00000003667 15245561014 0010465 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\Mixin;
defined('_JEXEC') || die;
/**
* Trait for PHP 5.5 Generators
*/
trait Generators
{
/**
* Returns a PHP Generator of DataModel instances based on your currently set Model state. You can foreach() the
* returned generator to walk through each item of the data set.
*
* WARNING! This only works on PHP 5.5 and later.
*
* When the generator is done you might get a PHP warning. This is normal. Joomla! doesn't support multiple db
* cursors being open at once. What we do instead is clone the database object. Of course it cannot close the db
* connection when we dispose of it (since it's already in use by Joomla), hence the warning. Pay no attention.
*
* @param integer $limitstart How many items from the start to skip (0 = do not skip)
* @param integer $limit How many items to return (0 = all)
* @param bool $overrideLimits Set to true to override limitstart, limit and ordering
*
* @return \Generator A PHP generator of DataModel objects
* @since 3.3.2
* @throws \Exception
*/
public function &getGenerator($limitstart = 0, $limit = 0, $overrideLimits = false)
{
$limitstart = max($limitstart, 0);
$limit = max($limit, 0);
$query = $this->buildQuery($overrideLimits);
$db = clone $this->getDbo();
$db->setQuery($query, $limitstart, $limit);
$cursor = $db->execute();
$reflectDB = new \ReflectionObject($db);
$refFetchAssoc = $reflectDB->getMethod('fetchAssoc');
$refFetchAssoc->setAccessible(true);
while ($data = $refFetchAssoc->invoke($db, $cursor))
{
$item = clone $this;
$item->clearState()->reset(true);
$item->bind($data);
$item->relationManager = clone $this->relationManager;
$item->relationManager->rebase($item);
yield $item;
}
}
}
Mixin/Assertions.php 0000604 00000004172 15245561014 0010476 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\Mixin;
defined('_JEXEC') || die;
use Joomla\CMS\Language\Text;
use RuntimeException;
/**
* Trait for check() method assertions
*/
trait Assertions
{
/**
* Make sure $condition is true or throw a RuntimeException with the $message language string
*
* @param bool $condition The condition which must be true
* @param string $message The language key for the message to throw
*
* @throws RuntimeException
*/
protected function assert($condition, $message)
{
if (!$condition)
{
throw new RuntimeException(Text::_($message));
}
}
/**
* Assert that $value is not empty or throw a RuntimeException with the $message language string
*
* @param mixed $value The value to check
* @param string $message The language key for the message to throw
*
* @throws RuntimeException
*/
protected function assertNotEmpty($value, $message)
{
$this->assert(!empty($value), $message);
}
/**
* Assert that $value is set to one of $validValues or throw a RuntimeException with the $message language string
*
* @param mixed $value The value to check
* @param array $validValues An array of valid values for $value
* @param string $message The language key for the message to throw
*
* @throws RuntimeException
*/
protected function assertInArray($value, array $validValues, $message)
{
$this->assert(in_array($value, $validValues), $message);
}
/**
* Assert that $value is set to none of $validValues. Otherwise throw a RuntimeException with the $message language
* string.
*
* @param mixed $value The value to check
* @param array $validValues An array of invalid values for $value
* @param string $message The language key for the message to throw
*
* @throws \RuntimeException
*/
protected function assertNotInArray($value, array $validValues, $message)
{
$this->assert(!in_array($value, $validValues, true), $message);
}
}
Model.php 0000604 00000032651 15245561014 0006323 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Input\Input;
use FOF40\Model\Exception\CannotGetName;
use Joomla\CMS\Filter\InputFilter;
/**
* Class Model
*
* A generic MVC model implementation
*
* @property-read \FOF40\Input\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;
/**
* 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 $this for chaining
*/
public function getClone()
{
return clone($this);
}
/**
* Returns a reference to the model's container
*
* @return \FOF40\Container\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 mixed
*/
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 = (bool) $newState;
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 $this for chaining
*/
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 $this
*/
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))
{
$this->{$event}(...$arguments);
}
// 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;
}
if (property_exists($this->state, $property))
{
return $this->state->$property;
}
return $default;
}
}
TreeModel.php 0000604 00000161252 15245561014 0007143 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Model\DataModel\Exception\TreeIncompatibleTable;
use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtCurrent;
use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtOther;
use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtParent;
use FOF40\Model\DataModel\Exception\TreeInvalidLftRgtSibling;
use FOF40\Model\DataModel\Exception\TreeMethodOnlyAllowedInRoot;
use FOF40\Model\DataModel\Exception\TreeRootNotFound;
use FOF40\Model\DataModel\Exception\TreeUnexpectedPrimaryKey;
use FOF40\Model\DataModel\Exception\TreeUnsupportedMethod;
use Joomla\CMS\Application\ApplicationHelper;
/**
* A DataModel which implements nested trees
*
* @property int $lft Left value (for nested set implementation)
* @property int $rgt Right value (for nested set implementation)
* @property string $hash Slug hash (for faster searching)
*/
class TreeModel extends DataModel
{
/** @var int The level (depth) of this node in the tree */
protected $treeDepth;
/** @var TreeModel The root node in the tree */
protected $treeRoot;
/** @var TreeModel The parent node of ourselves */
protected $treeParent;
/** @var bool Should I perform a nested get (used to query ascendants/descendants) */
protected $treeNestedGet = false;
/**
* Public constructor. Overrides the parent constructor, making sure there are lft/rgt columns which make it
* compatible with nested sets.
*
* @param Container $container The configuration variables to this model
* @param array $config Configuration values for this model
*
* @throws \RuntimeException When lft/rgt columns are not found
* @see \FOF40\Model\DataModel::__construct()
*
*/
public function __construct(Container $container = null, array $config = [])
{
parent::__construct($container, $config);
if (!$this->hasField('lft') || !$this->hasField('rgt'))
{
throw new TreeIncompatibleTable($this->tableName);
}
}
/**
* Overrides the automated table checks to handle the 'hash' column for faster searching
*
* @return $this|DataModel
*/
public function check()
{
// Create a slug if there is a title and an empty slug
if ($this->hasField('title') && $this->hasField('slug') && !$this->slug)
{
$this->slug = ApplicationHelper::stringURLSafe($this->title);
}
// Create the SHA-1 hash of the slug for faster searching (make sure the hash column is CHAR(64) to take
// advantage of MySQL's optimised searching for fixed size CHAR columns)
if ($this->hasField('hash') && $this->hasField('slug'))
{
$this->hash = sha1($this->slug);
}
// Reset cached values
$this->resetTreeCache();
// Run the parent checks
parent::check();
return $this;
}
/**
* Delete a node, either the currently loaded one or the one specified in $id. If an $id is specified that node
* is loaded before trying to delete it. In the end the data model is reset. If the node has any children nodes
* they will be removed before the node itself is deleted.
*
* @param mixed $id Primary key (id field) value
*
* @return $this for chaining
* @throws \UnexpectedValueException
*
*/
public function forceDelete($id = null)
{
// Load the specified record (if necessary)
if (!empty($id))
{
$this->findOrFail($id);
}
$k = $this->getIdFieldName();
$pk = (!$id) ? $this->$k : $id;
// If no primary key is given, return false.
if (!$pk)
{
throw new TreeUnexpectedPrimaryKey;
}
// Execute the logic only if I have a primary key, otherwise I could have weird results
// Perform the checks on the current node *BEFORE* starting to delete the children
try
{
$this->triggerEvent('onBeforeDelete', [&$pk]);
}
catch (\Exception $e)
{
return false;
}
$result = true;
// Recursively delete all children nodes as long as we are not a leaf node
if (!$this->isLeaf())
{
// Get all sub-nodes
$table = $this->getClone();
$table->bind($this->getData());
$subNodes = $table->getDescendants();
// Delete all sub-nodes (goes through the model to trigger the observers)
if (!empty($subNodes))
{
/** @var TreeModel $item */
foreach ($subNodes as $item)
{
// We have to pass the id, so we are getting it again from the database.
// We have to do in this way, since a previous child could have changed our lft and rgt values
if (!$item->forceDelete($item->$k))
{
// A sub-node failed or prevents the delete, continue deleting other nodes,
// but preserve the current node (ie the parent)
$result = false;
}
};
// Load it again, since while deleting a children we could have updated ourselves, too
$this->find($pk);
}
}
if ($result)
{
$db = $this->getDbo();
// Delete the row by primary key.
$query = $db->getQuery(true);
$query->delete();
$query->from($this->getTableName());
$query->where($db->qn($this->getIdFieldName()) . ' = ' . $db->q($pk));
$db->setQuery($query)->execute();
$this->triggerEvent('onAfterDelete', [&$pk]);
}
return $this;
}
/**
* Not supported in nested sets
*
* @param string $where Ignored
*
* @return static Self, for chaining
*
* @throws \RuntimeException
*/
public function reorder($where = '')
{
throw new TreeUnsupportedMethod(__METHOD__);
}
/**
* Not supported in nested sets
*
* @param integer $delta Ignored
* @param string $where Ignored
*
* @return static Self, for chaining
*
* @throws \RuntimeException
*/
public function move($delta, $where = '')
{
throw new TreeUnsupportedMethod(__METHOD__);
}
/**
* Create a new record with the provided data. It is inserted as the last child of the current node's parent
*
* @param array $data The data to use in the new record
*
* @return static The new node
*/
public function create($data)
{
$newNode = $this->getClone();
$newNode->reset();
$newNode->bind($data);
if ($this->isRoot())
{
return $newNode->insertAsChildOf($this);
}
else
{
$parentNode = $this->getParent();
return $newNode->insertAsChildOf($parentNode);
}
}
/**
* Makes a copy of the record, inserting it as the last child of the current node's parent.
*
* @return static
*
* @codeCoverageIgnore
*/
public function copy($data = null)
{
$selfData = $this->toArray();
if (!is_array($data))
{
$data = [];
}
$data = array_merge($data, $selfData);
return $this->create($data);
}
/**
* Reset the record data and the tree cache
*
* @param boolean $useDefaults Should I use the default values? Default: yes
* @param boolean $resetRelations Should I reset the relations too? Default: no
*
* @return static Self, for chaining
*
* @codeCoverageIgnore
*/
public function reset($useDefaults = true, $resetRelations = false)
{
$this->resetTreeCache();
return parent::reset($useDefaults, $resetRelations);
}
/**
* Insert the current node as a tree root. It is a good idea to never use this method, instead providing a root node
* in your schema installation and then sticking to only one root.
*
* @return static
*
* @throws \RuntimeException
*/
public function insertAsRoot()
{
// You can't insert a node that is already saved i.e. the table has an id
if ($this->getId())
{
throw new TreeMethodOnlyAllowedInRoot(__METHOD__);
}
// First we need to find the right value of the last parent, a.k.a. the max(rgt) of the table
$db = $this->getDbo();
// Get the lft/rgt names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$query = $db->getQuery(true)
->select('MAX(' . $fldRgt . ')')
->from($db->qn($this->tableName));
$maxRgt = $db->setQuery($query, 0, 1)->loadResult();
if (empty($maxRgt))
{
$maxRgt = 0;
}
$this->lft = ++$maxRgt;
$this->rgt = ++$maxRgt;
return $this->save();
}
/**
* Insert the current node as the first (leftmost) child of a parent node.
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $parentNode The node which will become our parent
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertAsFirstChildOf(TreeModel &$parentNode)
{
if ($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's rgt
$myLeft = $parentNode->lft;
// Update my lft/rgt values
$this->lft = $myLeft + 1;
$this->rgt = $myLeft + 2;
// Update parent node's right (we added two elements in there, remember?)
$parentNode->rgt += 2;
// Wrap everything in a transaction
$db->transactionStart();
try
{
// Make a hole (2 queries)
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' > ' . $db->q($myLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+ 2')
->where($fldRgt . '>' . $db->q($myLeft));
$db->setQuery($query)->execute();
// Insert the new node
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
// Roll back the transaction on error
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Insert the current node as the last (rightmost) child of a parent node.
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $parentNode The node which will become our parent
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertAsLastChildOf(TreeModel &$parentNode)
{
if ($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's lft
$myRight = $parentNode->rgt;
// Update my lft/rgt values
$this->lft = $myRight;
$this->rgt = $myRight + 1;
// Update parent node's right (we added two elements in there, remember?)
$parentNode->rgt += 2;
// Wrap everything in a transaction
$db->transactionStart();
try
{
// Make a hole (2 queries)
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . '>=' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . '>' . $db->q($myRight));
$db->setQuery($query)->execute();
// Insert the new node
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
// Roll back the transaction on error
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Alias for insertAsLastchildOf
*
* @codeCoverageIgnore
*
* @param TreeModel $parentNode
*
* @return $this for chaining
*/
public function insertAsChildOf(TreeModel &$parentNode)
{
return $this->insertAsLastChildOf($parentNode);
}
/**
* Insert the current node to the left of (before) a sibling node
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $siblingNode We will be inserted before this node
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertLeftOf(TreeModel &$siblingNode)
{
if ($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's rgt
$myLeft = $siblingNode->lft;
// Update my lft/rgt values
$this->lft = $myLeft;
$this->rgt = $myLeft + 1;
// Update sibling's lft/rgt values
$siblingNode->lft += 2;
$siblingNode->rgt += 2;
$db->transactionStart();
try
{
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' >= ' . $db->q($myLeft))
)->execute();
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . ' > ' . $db->q($myLeft))
)->execute();
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Insert the current node to the right of (after) a sibling node
*
* WARNING: If it's an existing node it will be COPIED, not moved.
*
* @param TreeModel $siblingNode We will be inserted after this node
*
* @return $this for chaining
* @throws \Exception
* @throws \RuntimeException
*/
public function insertRightOf(TreeModel &$siblingNode)
{
if ($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
// Get a reference to the database
$db = $this->getDbo();
// Get the field names
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$fldLft = $db->qn($this->getFieldAlias('lft'));
// Nullify the PK, so a new record will be created
$this->{$this->idFieldName} = null;
// Get the value of the parent node's lft
$myRight = $siblingNode->rgt;
// Update my lft/rgt values
$this->lft = $myRight + 1;
$this->rgt = $myRight + 2;
$db->transactionStart();
try
{
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldRgt . ' = ' . $fldRgt . '+2')
->where($fldRgt . ' > ' . $db->q($myRight))
)->execute();
$db->setQuery(
$db->getQuery(true)
->update($db->qn($this->tableName))
->set($fldLft . ' = ' . $fldLft . '+2')
->where($fldLft . ' > ' . $db->q($myRight))
)->execute();
$this->save();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* Alias for insertRightOf
*
* @codeCoverageIgnore
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*/
public function insertAsSiblingOf(TreeModel &$siblingNode)
{
return $this->insertRightOf($siblingNode);
}
/**
* Move the current node (and its subtree) one position to the left in the tree, i.e. before its left-hand sibling
*
* @return $this
* @throws \RuntimeException
*
*/
public function moveLeft()
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
// If it is a root node we will not move the node (roots don't participate in tree ordering)
if ($this->isRoot())
{
return $this;
}
// Are we already the leftmost node?
$parentNode = $this->getParent();
if ($parentNode->lft === $this->lft - 1)
{
return $this;
}
// Get the sibling to the left
$db = $this->getDbo();
$leftSibling = $this->getClone()->reset()
->whereRaw($db->qn($this->getFieldAlias('rgt')) . ' = ' . $db->q($this->lft - 1))
->firstOrFail();
// Move the node
return $this->moveToLeftOf($leftSibling);
}
/**
* Move the current node (and its subtree) one position to the right in the tree, i.e. after its right-hand sibling
*
* @return $this
* @throws \RuntimeException
*
*/
public function moveRight()
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
// If it is a root node we will not move the node (roots don't participate in tree ordering)
if ($this->isRoot())
{
return $this;
}
// Are we already the rightmost node?
$parentNode = $this->getParent();
if ($parentNode->rgt === $this->rgt + 1)
{
return $this;
}
// Get the sibling to the right
$db = $this->getDbo();
$rightSibling = $this->getClone()->reset()
->whereRaw($db->qn($this->getFieldAlias('lft')) . ' = ' . $db->q($this->rgt + 1))
->firstOrFail();
// Move the node
return $this->moveToRightOf($rightSibling);
}
/**
* Moves the current node (and its subtree) to the left of another node. The other node can be in a different
* position in the tree or even under a different root.
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function moveToLeftOf(TreeModel $siblingNode)
{
// Sanity checks on current and sibling node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$sibLeft = $siblingNode->lft;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newSibLeft = ($sibLeft > $myRight) ? $sibLeft - $myWidth : $sibLeft;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newSibLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' >= ' . $db->q($newSibLeft));
$db->setQuery($query)->execute();
// Move node and sub-nodes
$moveRight = $newSibLeft - $myLeft;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Moves the current node (and its subtree) to the right of another node. The other node can be in a different
* position in the tree or even under a different root.
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function moveToRightOf(TreeModel $siblingNode)
{
// Sanity checks on current and sibling node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($siblingNode->lft >= $siblingNode->rgt)
{
throw new TreeInvalidLftRgtSibling;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$sibRight = $siblingNode->rgt;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newSibRight = ($sibRight > $myRight) ? $sibRight - $myWidth : $sibRight;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($newSibRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($newSibRight));
$db->setQuery($query)->execute();
// Move node and sub-nodes
$moveRight = ($sibRight > $myRight) ? $sibRight - $myRight : $sibRight - $myRight + $myWidth;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Alias for moveToRightOf
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makeNextSiblingOf(TreeModel $siblingNode)
{
return $this->moveToRightOf($siblingNode);
}
/**
* Alias for makeNextSiblingOf
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makeSiblingOf(TreeModel $siblingNode)
{
return $this->makeNextSiblingOf($siblingNode);
}
/**
* Alias for moveToLeftOf
*
* @param TreeModel $siblingNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makePreviousSiblingOf(TreeModel $siblingNode)
{
return $this->moveToLeftOf($siblingNode);
}
/**
* Moves a node and its subtree as a the first (leftmost) child of $parentNode
*
* @param TreeModel $parentNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function makeFirstChildOf(TreeModel $parentNode)
{
// Sanity checks on current and sibling node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$parentRight = $parentNode->rgt;
$parentLeft = $parentNode->lft;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newParentLeft = ($parentLeft > $myRight) ? $parentLeft - $myWidth : $parentLeft;
$newParentRight = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newParentLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($newParentLeft));
$db->setQuery($query)->execute();
// Move node and sub-nodes
$moveRight = $newParentLeft - $myLeft + 1;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Moves a node and its subtree as a the last (rightmost) child of $parentNode
*
* @param TreeModel $parentNode
*
* @return $this for chaining
*
* @throws \Exception
* @throws \RuntimeException
*/
public function makeLastChildOf(TreeModel $parentNode)
{
// Sanity checks on current and sibling node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($parentNode->lft >= $parentNode->rgt)
{
throw new TreeInvalidLftRgtParent;
}
$db = $this->getDbo();
$left = $db->qn($this->getFieldAlias('lft'));
$right = $db->qn($this->getFieldAlias('rgt'));
// Get node metrics
$myLeft = $this->lft;
$myRight = $this->rgt;
$myWidth = $myRight - $myLeft + 1;
// Get parent metrics
$parentRight = $parentNode->rgt;
// Start the transaction
$db->transactionStart();
try
{
// Temporary remove subtree being moved
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set("$left = " . $db->q(0) . " - $left")
->set("$right = " . $db->q(0) . " - $right")
->where($left . ' >= ' . $db->q($myLeft))
->where($right . ' <= ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Close hole left behind
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' - ' . $db->q($myWidth))
->where($left . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' - ' . $db->q($myWidth))
->where($right . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Make a hole for the new items
$newLeft = ($parentRight > $myRight) ? $parentRight - $myWidth : $parentRight;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $left . ' + ' . $db->q($myWidth))
->where($left . ' >= ' . $db->q($newLeft));
$db->setQuery($query)->execute();
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($right . ' = ' . $right . ' + ' . $db->q($myWidth))
->where($right . ' >= ' . $db->q($newLeft));
$db->setQuery($query)->execute();
// Move node and sub-nodes
$moveRight = ($parentRight > $myRight) ? $parentRight - $myRight - 1 : $parentRight - $myRight - 1 + $myWidth;
$query = $db->getQuery(true)
->update($db->qn($this->tableName))
->set($left . ' = ' . $db->q(0) . ' - ' . $left . ' + ' . $db->q($moveRight))
->set($right . ' = ' . $db->q(0) . ' - ' . $right . ' + ' . $db->q($moveRight))
->where($left . ' <= 0 - ' . $db->q($myLeft))
->where($right . ' >= 0 - ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
$db->transactionRollback();
throw $e;
}
// Let's load the record again to fetch the new values for lft and rgt
$this->findOrFail();
return $this;
}
/**
* Alias for makeLastChildOf
*
* @param TreeModel $parentNode
*
* @return $this for chaining
*
* @codeCoverageIgnore
*/
public function makeChildOf(TreeModel $parentNode)
{
return $this->makeLastChildOf($parentNode);
}
/**
* Makes the current node a root (and moving its entire subtree along the way). This is achieved by moving the node
* to the right of its root node
*
* @return $this for chaining
*/
public function makeRoot()
{
// Make sure we are not a root
if ($this->isRoot())
{
return $this;
}
// Get a reference to my root
$myRoot = $this->getRoot();
// Double check I am not a root
if ($this->equals($myRoot))
{
return $this;
}
// Move myself to the right of my root
$this->moveToRightOf($myRoot);
$this->treeDepth = 0;
return $this;
}
/**
* Gets the level (depth) of this node in the tree. The result is cached in $this->treeDepth for faster fetch.
*
* @return int|mixed
* @throws \RuntimeException
*
*/
public function getLevel()
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if (is_null($this->treeDepth))
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$query = $db->getQuery(true)
->select('(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
->group($db->qn('node') . '.' . $fldLft)
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$this->treeDepth = $db->setQuery($query, 0, 1)->loadResult();
}
return $this->treeDepth;
}
/**
* Returns the immediate parent of the current node
*
* @return static
* @throws \RuntimeException
*
*/
public function getParent()
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($this->isRoot())
{
return $this;
}
if (empty($this->treeParent) || !is_object($this->treeParent) || !($this->treeParent instanceof TreeModel))
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$query = $db->getQuery(true)
->select($db->qn('parent') . '.' . $fldLft)
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
->order($db->qn('parent') . '.' . $fldLft . ' DESC');
$targetLft = $db->setQuery($query, 1, 1)->loadResult();
$this->treeParent = $this->getClone()->reset()
->whereRaw($fldLft . ' = ' . $db->q($targetLft))
->firstOrFail();
}
return $this->treeParent;
}
/**
* Is this a top-level root node?
*
* @return bool
*/
public function isRoot()
{
// If lft=1 it is necessarily a root node
if ($this->lft == 1)
{
return true;
}
// Otherwise make sure its level is 0
return $this->getLevel() == 0;
}
/**
* Is this a leaf node (a node without children)?
*
* @return bool
* @throws \RuntimeException
*
*/
public function isLeaf()
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
return $this->rgt - 1 === $this->lft;
}
/**
* Is this a child node (not root)?
*
* @codeCoverageIgnore
*
* @return bool
*/
public function isChild()
{
return !$this->isRoot();
}
/**
* Returns true if we are a descendant of $otherNode
*
* @param TreeModel $otherNode
*
* @return bool
* @throws \RuntimeException
*
*/
public function isDescendantOf(TreeModel $otherNode)
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($otherNode->lft >= $otherNode->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return ($otherNode->lft < $this->lft) && ($otherNode->rgt > $this->rgt);
}
/**
* Returns true if $otherNode is ourselves or if we are a descendant of $otherNode
*
* @param TreeModel $otherNode
*
* @return bool
*/
public function isSelfOrDescendantOf(TreeModel $otherNode)
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($otherNode->lft >= $otherNode->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return ($otherNode->lft <= $this->lft) && ($otherNode->rgt >= $this->rgt);
}
/**
* Returns true if we are an ancestor of $otherNode
*
* @codeCoverageIgnore
*
* @param TreeModel $otherNode
*
* @return bool
*/
public function isAncestorOf(TreeModel $otherNode)
{
return $otherNode->isDescendantOf($this);
}
/**
* Returns true if $otherNode is ourselves or we are an ancestor of $otherNode
*
* @codeCoverageIgnore
*
* @param TreeModel $otherNode
*
* @return bool
*/
public function isSelfOrAncestorOf(TreeModel $otherNode)
{
return $otherNode->isSelfOrDescendantOf($this);
}
/**
* Is $node this very node?
*
* @param TreeModel $node
*
* @return bool
* @throws \RuntimeException
*
*/
public function equals(TreeModel &$node)
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($node->lft >= $node->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return (
($this->getId() == $node->getId())
&& ($this->lft === $node->lft)
&& ($this->rgt === $node->rgt)
);
}
/**
* Checks if our node is inside the subtree of $otherNode. This is a fast check as only lft and rgt values have to
* be compared.
*
* @param TreeModel $otherNode
*
* @return bool
* @throws \RuntimeException
*
*/
public function insideSubtree(TreeModel $otherNode)
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
if ($otherNode->lft >= $otherNode->rgt)
{
throw new TreeInvalidLftRgtOther;
}
return ($this->lft > $otherNode->lft) && ($this->rgt < $otherNode->rgt);
}
/**
* Returns true if both this node and $otherNode are root, leaf or child (same tree scope)
*
* @param TreeModel $otherNode
*
* @return bool
*/
public function inSameScope(TreeModel $otherNode)
{
if ($this->isLeaf())
{
return $otherNode->isLeaf();
}
elseif ($this->isRoot())
{
return $otherNode->isRoot();
}
elseif ($this->isChild())
{
return $otherNode->isChild();
}
else
{
return false;
}
}
/**
* get() will not return the selected node if it's part of the query results
*
* @param TreeModel $node The node to exclude from the results
*
* @return void
*/
public function withoutNode(TreeModel $node)
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$this->whereRaw('NOT(' . $db->qn('node') . '.' . $fldLft . ' = ' . $db->q($node->lft) . ')');
}
/**
* Returns the root node of the tree this node belongs to
*
* @return static
*
* @throws \RuntimeException
*/
public function getRoot()
{
// Empty node, let's try to get the first available root, ie lft=1
if (!$this->getId())
{
$this->load(['lft' => 1]);
}
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
// If this is a root node return itself (there is no such thing as the root of a root node)
if ($this->isRoot())
{
return $this;
}
if (empty($this->treeRoot) || !is_object($this->treeRoot) || !($this->treeRoot instanceof TreeModel))
{
$this->treeRoot = null;
// First try to get the record with the minimum ID
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$subQuery = $db->getQuery(true)
->select('MIN(' . $fldLft . ')')
->from($db->qn($this->tableName));
try
{
$root = $this->getClone()->reset()
->whereRaw($fldLft . ' = (' . $subQuery . ')')
->firstOrFail();
if ($this->isDescendantOf($root))
{
$this->treeRoot = $root;
}
}
catch (\RuntimeException $e)
{
// If there is no root found throw an exception. Basically: your table is FUBAR.
throw new TreeRootNotFound($this->tableName, $this->lft, 500, $e);
}
// If the above method didn't work, get all roots and select the one with the appropriate lft/rgt values
if (is_null($this->treeRoot))
{
// Find the node with depth = 0, lft < our lft and rgt > our right. That's our root node.
$query = $db->getQuery(true)
->select([
$db->qn('node') . '.' . $fldLft,
'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - 1) AS ' . $db->qn('depth'),
])
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' < ' . $db->q($this->lft))
->where($db->qn('node') . '.' . $fldRgt . ' > ' . $db->q($this->rgt))
->having($db->qn('depth') . ' = ' . $db->q(0))
->group($db->qn('node') . '.' . $fldLft);
// Get the lft value
$targetLeft = $db->setQuery($query)->loadResult();
if (empty($targetLeft))
{
// If there is no root found throw an exception. Basically: your table is FUBAR.
throw new TreeRootNotFound($this->tableName, $this->lft);
}
try
{
$this->treeRoot = $this->getClone()->reset()
->whereRaw($fldLft . ' = ' . $db->q($targetLeft))
->firstOrFail();
}
catch (\RuntimeException $e)
{
// If there is no root found throw an exception. Basically: your table is FUBAR.
throw new TreeRootNotFound($this->tableName, $this->lft, 500, $e);
}
}
}
return $this->treeRoot;
}
/**
* Get all ancestors to this node and the node itself. In other words it gets the full path to the node and the node
* itself.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestorsAndSelf()
{
$this->scopeAncestorsAndSelf();
return $this->get(true);
}
/**
* Get all ancestors to this node and the node itself, but not the root node. If you want to
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestorsAndSelfWithoutRoot()
{
$this->scopeAncestorsAndSelf();
$this->scopeWithoutRoot();
return $this->get(true);
}
/**
* Get all ancestors to this node but not the node itself. In other words it gets the path to the node, without the
* node itself.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestors()
{
$this->scopeAncestorsAndSelf();
$this->scopeWithoutSelf();
return $this->get(true);
}
/**
* Get all ancestors to this node but not the node itself and its root.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getAncestorsWithoutRoot()
{
$this->scopeAncestors();
$this->scopeWithoutRoot();
return $this->get(true);
}
/**
* Get all sibling nodes, including ourselves
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getSiblingsAndSelf()
{
$this->scopeSiblingsAndSelf();
return $this->get(true);
}
/**
* Get all sibling nodes, except ourselves
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getSiblings()
{
$this->scopeSiblings();
return $this->get(true);
}
/**
* Get all leaf nodes in the tree. You may want to use the scopes to narrow down the search in a specific subtree or
* path.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getLeaves()
{
$this->scopeLeaves();
return $this->get(true);
}
/**
* Get all descendant (children) nodes and ourselves.
*
* Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getDescendantsAndSelf()
{
$this->scopeDescendantsAndSelf();
return $this->get(true);
}
/**
* Get only our descendant (children) nodes, not ourselves.
*
* Note: all descendant nodes, even descendants of our immediate descendants, will be returned.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getDescendants()
{
$this->scopeDescendants();
return $this->get(true);
}
/**
* Get the immediate descendants (children). Unlike getDescendants it only goes one level deep into the tree
* structure. Descendants of descendant nodes will not be returned.
*
* @codeCoverageIgnore
*
* @return DataModel\Collection
*/
public function getImmediateDescendants()
{
$this->scopeImmediateDescendants();
return $this->get(true);
}
/**
* Returns a hashed array where each element's key is the value of the $key column (default: the ID column of the
* table) and its value is the value of the $column column (default: title). Each nesting level will have the value
* of the $column column prefixed by a number of $separator strings, as many as its nesting level (depth).
*
* This is useful for creating HTML select elements showing the hierarchy in a human readable format.
*
* @param string $column
* @param null $key
* @param string $separator
*
* @return array
*/
public function getNestedList($column = 'title', $key = null, $separator = ' ')
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
if (empty($key) || !$this->hasField($key))
{
$key = $this->getIdFieldName();
}
if (empty($column))
{
$column = 'title';
}
$fldKey = $db->qn($this->getFieldAlias($key));
$fldColumn = $db->qn($this->getFieldAlias($column));
$query = $db->getQuery(true)
->select([
$db->qn('node') . '.' . $fldKey,
$db->qn('node') . '.' . $fldColumn,
'(COUNT(' . $db->qn('parent') . '.' . $fldKey . ') - 1) AS ' . $db->qn('depth'),
])
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->group($db->qn('node') . '.' . $fldLft)
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$tempResults = $db->setQuery($query)->loadAssocList();
$ret = [];
if (!empty($tempResults))
{
foreach ($tempResults as $row)
{
$ret[$row[$key]] = str_repeat($separator, $row['depth']) . $row[$column];
}
}
return $ret;
}
/**
* Locate a node from a given path, e.g. "/some/other/leaf"
*
* Notes:
* - This will only work when you have a "slug" and a "hash" field in your table.
* - If the path starts with "/" we will use the root with lft=1. Otherwise the first component of the path is
* supposed to be the slug of the root node.
* - If the root node is not found you'll get null as the return value
* - You will also get null if any component of the path is not found
*
* @param string $path The path to locate
*
* @return TreeModel|null The found node or null if nothing is found
*/
public function findByPath($path)
{
// No path? No node.
if (empty($path))
{
return null;
}
// Extract the path parts
$pathParts = explode('/', $path);
$firstElement = array_shift($pathParts);
if (!empty($firstElement))
{
array_unshift($pathParts, $firstElement);
}
// Just a slash? Return the root
if (empty($pathParts[0]))
{
return $this->getRoot();
}
// Get the quoted field names
$db = $this->getDbo();
$fldLeft = $db->qn($this->getFieldAlias('lft'));
$fldRight = $db->qn($this->getFieldAlias('rgt'));
$fldHash = $db->qn($this->getFieldAlias('hash'));
// Get the quoted hashes of the slugs
$pathHashesQuoted = [];
foreach ($pathParts as $part)
{
$pathHashesQuoted[] = $db->q(sha1($part));
}
// Get all nodes with slugs matching our path
$query = $db->getQuery(true)
->select([
$db->qn('node') . '.*',
'(COUNT(' . $db->qn('parent') . '.' . $db->qn($this->getFieldAlias('lft')) . ') - 1) AS ' . $db->qn('depth'),
])->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLeft . ' >= ' . $db->qn('parent') . '.' . $fldLeft)
->where($db->qn('node') . '.' . $fldLeft . ' <= ' . $db->qn('parent') . '.' . $fldRight)
->where($db->qn('node') . '.' . $fldHash . ' IN (' . implode(',', $pathHashesQuoted) . ')')
->group($db->qn('node') . '.' . $fldLeft)
->order([
$db->qn('depth') . ' ASC',
$db->qn('node') . '.' . $fldLeft . ' ASC',
]);
$queryResults = $db->setQuery($query)->loadAssocList();
$pathComponents = [];
// Handle paths with (no root slug provided) and without (root slug provided) a leading slash
$currentLevel = (substr($path, 0, 1) == '/') ? 0 : -1;
$maxLevel = count($pathParts) + $currentLevel;
// Initialise the path results array
$i = $currentLevel;
foreach ($pathParts as $part)
{
$i++;
$pathComponents[$i] = [
'slug' => $part,
'id' => null,
'lft' => null,
'rgt' => null,
];
}
// Search for the best matching nodes
$colSlug = $this->getFieldAlias('slug');
$colLft = $this->getFieldAlias('lft');
$colRgt = $this->getFieldAlias('rgt');
$colId = $this->getIdFieldName();
foreach ($queryResults as $row)
{
if ($row['depth'] == $currentLevel + 1)
{
if ($row[$colSlug] != $pathComponents[$currentLevel + 1]['slug'])
{
continue;
}
if ($currentLevel > 0)
{
if ($row[$colLft] < $pathComponents[$currentLevel]['lft'])
{
continue;
}
if ($row[$colRgt] > $pathComponents[$currentLevel]['rgt'])
{
continue;
}
}
$currentLevel++;
$pathComponents[$currentLevel]['id'] = $row[$colId];
$pathComponents[$currentLevel]['lft'] = $row[$colLft];
$pathComponents[$currentLevel]['rgt'] = $row[$colRgt];
}
if ($currentLevel === $maxLevel)
{
break;
}
}
// Get the last found node
$lastNode = array_pop($pathComponents);
// If the node exists, return it...
if (!empty($lastNode['lft']))
{
return $this->getClone()->reset()->where($colLft, '=', $lastNode['lft'])->firstOrFail();
}
// ...otherwise return null
return null;
}
/**
* Overrides the DataModel's buildQuery to allow nested set searches using the provided scopes
*
* @param bool $overrideLimits
*
* @return \JDatabaseQuery
*/
public function buildQuery($overrideLimits = false)
{
$db = $this->getDbo();
$query = parent::buildQuery($overrideLimits);
// Wipe out select and from sections
$query->clear('select');
$query->clear('from');
$query
->select($db->qn('node') . '.*')
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'));
if ($this->treeNestedGet)
{
$query
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'));
}
return $query;
}
protected function onAfterDelete($oid)
{
$db = $this->getDbo();
$myLeft = $this->lft;
$myRight = $this->rgt;
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
// Move all siblings to the left
$width = $this->rgt - $this->lft + 1;
// Wrap everything in a transaction
$db->transactionStart();
try
{
// Shrink lft values
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldLft . ' = ' . $fldLft . ' - ' . $width)
->where($fldLft . ' > ' . $db->q($myLeft));
$db->setQuery($query)->execute();
// Shrink rgt values
$query = $db->getQuery(true)
->update($db->qn($this->getTableName()))
->set($fldRgt . ' = ' . $fldRgt . ' - ' . $width)
->where($fldRgt . ' > ' . $db->q($myRight));
$db->setQuery($query)->execute();
// Commit the transaction
$db->transactionCommit();
}
catch (\Exception $e)
{
// Roll back the transaction on error
$db->transactionRollback();
throw $e;
}
return $this;
}
/**
* get() will return all ancestor nodes and ourselves
*
* @return void
*/
protected function scopeAncestorsAndSelf()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' >= ' . $db->qn('node') . '.' . $fldLft);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' <= ' . $db->qn('node') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will return all ancestor nodes but not ourselves
*
* @return void
*/
protected function scopeAncestors()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' > ' . $db->qn('node') . '.' . $fldLft);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' < ' . $db->qn('node') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will return all sibling nodes and ourselves
*
* @return void
*/
protected function scopeSiblingsAndSelf()
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$parent = $this->getParent();
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->q($parent->lft));
$this->whereRaw($db->qn('node') . '.' . $fldRgt . ' < ' . $db->q($parent->rgt));
}
/**
* get() will return all sibling nodes but not ourselves
*
* @codeCoverageIgnore
*
* @return void
*/
protected function scopeSiblings()
{
$this->scopeSiblingsAndSelf();
$this->scopeWithoutSelf();
}
/**
* get() will return only leaf nodes
*
* @return void
*/
protected function scopeLeaves()
{
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' = ' . $db->qn('node') . '.' . $fldRgt . ' - ' . $db->q(1));
}
/**
* get() will return all descendants (even subtrees of subtrees!) and ourselves
*
* @return void
*/
protected function scopeDescendantsAndSelf()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft);
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will return all descendants (even subtrees of subtrees!) but not ourselves
*
* @return void
*/
protected function scopeDescendants()
{
$this->treeNestedGet = true;
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' > ' . $db->qn('parent') . '.' . $fldLft);
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' < ' . $db->qn('parent') . '.' . $fldRgt);
$this->whereRaw($db->qn('parent') . '.' . $fldLft . ' = ' . $db->q($this->lft));
}
/**
* get() will only return immediate descendants (first level children) of the current node
*
* @return void
* @throws \RuntimeException
*
*/
protected function scopeImmediateDescendants()
{
// Sanity checks on current node position
if ($this->lft >= $this->rgt)
{
throw new TreeInvalidLftRgtCurrent;
}
$db = $this->getDbo();
$fldLft = $db->qn($this->getFieldAlias('lft'));
$fldRgt = $db->qn($this->getFieldAlias('rgt'));
$subQuery = $db->getQuery(true)
->select([
$db->qn('node') . '.' . $fldLft,
'(COUNT(*) - 1) AS ' . $db->qn('depth'),
])
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->from($db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' = ' . $db->q($this->lft))
->group($db->qn('node') . '.' . $fldLft)
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$query = $db->getQuery(true)
->select([
$db->qn('node') . '.' . $fldLft,
'(COUNT(' . $db->qn('parent') . '.' . $fldLft . ') - (' .
$db->qn('sub_tree') . '.' . $db->qn('depth') . ' + 1)) AS ' . $db->qn('depth'),
])
->from($db->qn($this->tableName) . ' AS ' . $db->qn('node'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('parent'))
->join('CROSS', $db->qn($this->tableName) . ' AS ' . $db->qn('sub_parent'))
->join('CROSS', '(' . $subQuery . ') AS ' . $db->qn('sub_tree'))
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('parent') . '.' . $fldRgt)
->where($db->qn('node') . '.' . $fldLft . ' >= ' . $db->qn('sub_parent') . '.' . $fldLft)
->where($db->qn('node') . '.' . $fldLft . ' <= ' . $db->qn('sub_parent') . '.' . $fldRgt)
->where($db->qn('sub_parent') . '.' . $fldLft . ' = ' . $db->qn('sub_tree') . '.' . $fldLft)
->group($db->qn('node') . '.' . $fldLft)
->having([
$db->qn('depth') . ' > ' . $db->q(0),
$db->qn('depth') . ' <= ' . $db->q(1),
])
->order($db->qn('node') . '.' . $fldLft . ' ASC');
$leftValues = $db->setQuery($query)->loadColumn();
if (empty($leftValues))
{
$leftValues = [0];
}
array_walk($leftValues, function (&$item, $key) use (&$db) {
$item = $db->q($item);
});
$this->whereRaw($db->qn('node') . '.' . $fldLft . ' IN (' . implode(',', $leftValues) . ')');
}
/**
* get() will not return ourselves if it's part of the query results
*
* @codeCoverageIgnore
*
* @return void
*/
protected function scopeWithoutSelf()
{
$this->withoutNode($this);
}
/**
* get() will not return our root if it's part of the query results
*
* @codeCoverageIgnore
*
* @return void
*/
protected function scopeWithoutRoot()
{
$rootNode = $this->getRoot();
$this->withoutNode($rootNode);
}
/**
* Resets cached values used to speed up querying the tree
*
* @return static for chaining
*/
protected function resetTreeCache()
{
$this->treeDepth = null;
$this->treeRoot = null;
$this->treeParent = null;
$this->treeNestedGet = false;
return $this;
}
}
DataModel/Relation/BelongsTo.php 0000604 00000004617 15245561014 0012567 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation;
defined('_JEXEC') || die;
use FOF40\Model\DataModel;
/**
* BelongsTo (reverse 1-to-1 or 1-to-many) relation: this model is a child which belongs to the foreign table
*
* For example, parentModel is Articles and foreignModel is Users. Each article belongs to one user. One user can have
* one or more article.
*
* Example #2: parentModel is Phones and foreignModel is Users. Each phone belongs to one user. One user can have zero
* or one phones.
*/
class BelongsTo extends HasOne
{
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something"
* @param string $localKey The local table key for this relation, default: parentModel's ID field name
* @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name
* @param string $pivotTable IGNORED
* @param string $pivotLocalKey IGNORED
* @param string $pivotForeignKey IGNORED
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);
if (empty($localKey))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$this->localKey = $foreignModel->getIdFieldName();
}
if (empty($foreignKey))
{
if (!isset($foreignModel))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
}
$this->foreignKey = $foreignModel->getIdFieldName();
}
}
/**
* This is not supported by the belongsTo relation
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
public function getNew()
{
throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported by the belongsTo relation type");
}
}
DataModel/Relation/HasMany.php 0000604 00000011631 15245561014 0012225 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation;
defined('_JEXEC') || die;
use FOF40\Model\DataModel;
use FOF40\Model\DataModel\Relation;
/**
* HasMany (1-to-many) relation: this model is a parent which has zero or more children in the foreign table
*
* For example, parentModel is Users and foreignModel is Articles. Each user has zero or more articles.
*/
class HasMany extends Relation
{
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format
* "modelName@com_something"
* @param string $localKey The local table key for this relation, default: parentModel's ID field
* name
* @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name
* @param string $pivotTable IGNORED
* @param string $pivotLocalKey IGNORED
* @param string $pivotForeignKey IGNORED
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);
if (empty($this->localKey))
{
$this->localKey = $parentModel->getIdFieldName();
}
if (empty($this->foreignKey))
{
$this->foreignKey = $this->localKey;
}
}
/**
* Returns the count subquery for DataModel's has() and whereHas() methods.
*
* @param string $tableAlias The alias of the local table in the query. Leave blank to use the table's name.
*
* @return \JDatabaseQuery
*/
public function getCountSubquery($tableAlias = null)
{
// Get a model instance
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$db = $foreignModel->getDbo();
if (empty($tableAlias))
{
$tableAlias = $this->parentModel->getTableName();
}
return $db->getQuery(true)
->select('COUNT(*)')
->from($db->qn($foreignModel->getTableName(), 'reltbl'))
->where($db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey)) . ' = '
. $db->qn($tableAlias) . '.'
. $db->qn($this->parentModel->getFieldAlias($this->localKey)));
}
/**
* Returns a new item of the foreignModel type, pre-initialised to fulfil this relation
*
* @return DataModel
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
public function getNew()
{
// Get a model instance
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
// Prime the model
$foreignModel->setFieldValue($this->foreignKey, $this->parentModel->getFieldValue($this->localKey));
// Make sure we do have a data list
if (!($this->data instanceof DataModel\Collection))
{
$this->getData();
}
// Add the model to the data list
$this->data->add($foreignModel);
return $this->data->last();
}
/**
* Applies the relation filters to the foreign model when getData is called
*
* @param DataModel $foreignModel The foreign model you're operating on
* @param DataModel\Collection $dataCollection If it's an eager loaded relation, the collection of loaded
* parent records
*
* @return boolean Return false to force an empty data collection
*/
protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null)
{
// Decide how to proceed, based on eager or lazy loading
if (is_object($dataCollection))
{
// Eager loaded relation
if (!empty($dataCollection))
{
// Get a list of local keys from the collection
$values = [];
/** @var $item DataModel */
foreach ($dataCollection as $item)
{
$v = $item->getFieldValue($this->localKey, null);
if (!is_null($v))
{
$values[] = $v;
}
}
// Keep only unique values. This double step is required to re-index the array and avoid issues with
// Joomla Registry class. See issue #681
$values = array_values(array_unique($values));
// Apply the filter
if (!empty($values))
{
$foreignModel->where($this->foreignKey, 'in', $values);
}
else
{
return false;
}
}
else
{
return false;
}
}
else
{
// Lazy loaded relation; get the single local key
$localKey = $this->parentModel->getFieldValue($this->localKey, null);
if (is_null($localKey) || ($localKey === ''))
{
return false;
}
$foreignModel->where($this->foreignKey, '==', $localKey);
}
return true;
}
}
DataModel/Relation/Exception/ForeignModelNotFound.php 0000604 00000000454 15245561014 0016653 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation\Exception;
defined('_JEXEC') || die;
class ForeignModelNotFound extends \Exception {}
DataModel/Relation/Exception/SaveNotSupported.php 0000604 00000000450 15245561014 0016105 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation\Exception;
defined('_JEXEC') || die;
class SaveNotSupported extends \Exception {}
DataModel/Relation/Exception/RelationNotFound.php 0000604 00000000450 15245561014 0016052 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation\Exception;
defined('_JEXEC') || die;
class RelationNotFound extends \Exception {}
DataModel/Relation/Exception/RelationTypeNotFound.php 0000604 00000000454 15245561014 0016720 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation\Exception;
defined('_JEXEC') || die;
class RelationTypeNotFound extends \Exception {}
DataModel/Relation/Exception/PivotTableNotFound.php 0000604 00000000452 15245561014 0016350 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation\Exception;
defined('_JEXEC') || die;
class PivotTableNotFound extends \Exception {}
DataModel/Relation/Exception/NewNotSupported.php 0000604 00000000450 15245561014 0015740 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation\Exception;
defined('_JEXEC') || die;
class NewNotSupported extends \Exception
{
}
DataModel/Relation/HasOne.php 0000604 00000002551 15245561014 0012043 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation;
defined('_JEXEC') || die;
use FOF40\Model\DataModel;
use FOF40\Model\DataModel\Collection;
/**
* HasOne (straight 1-to-1) relation: this model is a parent which has exactly one child in the foreign table
*
* For example, parentModel is Users and foreignModel is Phones. Each uses has exactly one Phone.
*/
class HasOne extends HasMany
{
/**
* Get the relation data.
*
* If you want to apply additional filtering to the foreign model, use the $callback. It can be any function,
* static method, public method or closure with an interface of function(DataModel $foreignModel). You are not
* supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do:
* $foreignModel->setState('foo', 'bar')
*
* @param callable $callback The callback to run on the remote model.
* @param Collection $dataCollection
*
* @return Collection|DataModel
*/
public function getData($callback = null, Collection $dataCollection = null)
{
if (is_null($dataCollection))
{
return parent::getData($callback, $dataCollection)->first();
}
else
{
return parent::getData($callback, $dataCollection);
}
}
}
DataModel/Relation/BelongsToMany.php 0000604 00000027161 15245561014 0013413 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Relation;
defined('_JEXEC') || die;
use FOF40\Model\DataModel;
use FOF40\Model\DataModel\Relation;
/**
* BelongsToMany (many-to-many) relation: one or more records of this model are related to one or more records in the
* foreign model.
*
* For example, parentModel is Users and foreignModel is Groups. Each user can be assigned to many groups. Each group
* can be assigned to many users.
*/
class BelongsToMany extends Relation
{
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format
* "modelName@com_something"
* @param string $localKey The local table key for this relation, default: parentModel's ID field
* name
* @param string $foreignKey The foreign key for this relation, default: parentModel's ID field name
* @param string $pivotTable For many-to-many relations, the pivot (glue) table
* @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local
* key
* @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign
* key
*
* @throws DataModel\Relation\Exception\PivotTableNotFound
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
parent::__construct($parentModel, $foreignModelName, $localKey, $foreignKey, $pivotTable, $pivotLocalKey, $pivotForeignKey);
if (empty($localKey))
{
$this->localKey = $parentModel->getIdFieldName();
}
if (empty($pivotLocalKey))
{
$this->pivotLocalKey = $this->localKey;
}
if (empty($foreignKey))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$this->foreignKey = $foreignModel->getIdFieldName();
}
if (empty($pivotForeignKey))
{
$this->pivotForeignKey = $this->foreignKey;
}
if (empty($pivotTable))
{
// Get the local model's name (e.g. "users")
$localName = $parentModel->getName();
$localName = strtolower($localName);
// Get the foreign model's name (e.g. "groups")
if (!isset($foreignModel))
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
}
$foreignName = $foreignModel->getName();
$foreignName = strtolower($foreignName);
// Get the local model's app name
$parentModelBareComponent = $parentModel->getContainer()->bareComponentName;
$foreignModelBareComponent = $foreignModel->getContainer()->bareComponentName;
// There are two possibilities for the table name: #__component_local_foreign or #__component_foreign_local.
// There are also two possibilities for a component name (local or foreign model's)
$db = $parentModel->getDbo();
$prefix = $db->getPrefix();
$tableNames = [
'#__' . strtolower($parentModelBareComponent) . '_' . $localName . '_' . $foreignName,
'#__' . strtolower($parentModelBareComponent) . '_' . $foreignName . '_' . $localName,
'#__' . strtolower($foreignModelBareComponent) . '_' . $localName . '_' . $foreignName,
'#__' . strtolower($foreignModelBareComponent) . '_' . $foreignName . '_' . $localName,
];
$allTables = $db->getTableList();
$this->pivotTable = null;
foreach ($tableNames as $tableName)
{
$checkName = $prefix . substr($tableName, 3);
if (in_array($checkName, $allTables))
{
$this->pivotTable = $tableName;
}
}
if (empty($this->pivotTable))
{
throw new DataModel\Relation\Exception\PivotTableNotFound("Pivot table for many-to-many relation between '$localName and '$foreignName' not found'");
}
}
}
/**
* Populates the internal $this->data collection from the contents of the provided collection. This is used by
* DataModel to push the eager loaded data into each item's relation.
*
* @param DataModel\Collection $data The relation data to push into this relation
* @param mixed $keyMap Passes around the local to foreign key map
*
* @return void
*/
public function setDataFromCollection(DataModel\Collection &$data, $keyMap = null)
{
$this->data = new DataModel\Collection();
if (!is_array($keyMap))
{
return;
}
if (!empty($data))
{
// Get the local key value
$localKeyValue = $this->parentModel->getFieldValue($this->localKey);
// Make sure this local key exists in the (cached) pivot table
if (!isset($keyMap[$localKeyValue]))
{
return;
}
/** @var DataModel $item */
foreach ($data as $item)
{
// Only accept foreign items whose key is associated in the pivot table with our local key
if (in_array($item->getFieldValue($this->foreignKey), $keyMap[$localKeyValue]))
{
$this->data->add($item);
}
}
}
}
/**
* Returns the count subquery for DataModel's has() and whereHas() methods.
*
* @param string $tableAlias The alias of the local table in the query. Leave blank to use the table's name.
*
* @return \JDatabaseQuery
*/
public function getCountSubquery($tableAlias = null)
{
/** @var DataModel $foreignModel */
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$db = $foreignModel->getDbo();
if (empty($tableAlias))
{
$tableAlias = $this->parentModel->getTableName();
}
return $db->getQuery(true)
->select('COUNT(*)')
->from($db->qn($foreignModel->getTableName()) . ' AS ' . $db->qn('reltbl'))
->innerJoin(
$db->qn($this->pivotTable) . ' AS ' . $db->qn('pivotTable') . ' ON('
. $db->qn('pivotTable') . '.' . $db->qn($this->pivotForeignKey) . ' = '
. $db->qn('reltbl') . '.' . $db->qn($foreignModel->getFieldAlias($this->foreignKey))
. ')'
)
->where(
$db->qn('pivotTable') . '.' . $db->qn($this->pivotLocalKey) . ' ='
. $db->qn($tableAlias) . '.'
. $db->qn($this->parentModel->getFieldAlias($this->localKey))
);
}
/**
* Saves all related items. For many-to-many relations there are two things we have to do:
* 1. Save all related items; and
* 2. Overwrite the pivot table data with the new associations
*/
public function saveAll()
{
// Save all related items
parent::saveAll();
$this->saveRelations();
}
/**
* Overwrite the pivot table data with the new associations
*/
public function saveRelations()
{
// Get all the new keys
$newKeys = [];
if ($this->data instanceof DataModel\Collection)
{
foreach ($this->data as $item)
{
if ($item instanceof DataModel)
{
$newKeys[] = $item->getId();
}
elseif (!is_object($item))
{
$newKeys[] = $item;
}
}
}
$newKeys = array_unique($newKeys);
$db = $this->parentModel->getDbo();
$localKeyValue = $this->parentModel->getFieldValue($this->localKey);
// Kill all existing relations in the pivot table
$query = $db->getQuery(true)
->delete($db->qn($this->pivotTable))
->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKeyValue));
$db->setQuery($query);
$db->execute();
// Write the new relations to the database
$protoQuery = $db->getQuery(true)
->insert($db->qn($this->pivotTable))
->columns([$db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)]);
$i = 0;
$query = null;
foreach ($newKeys as $key)
{
$i++;
if (is_null($query))
{
$query = clone $protoQuery;
}
$query->values($db->q($localKeyValue) . ', ' . $db->q($key));
if (($i % 50) == 0)
{
$db->setQuery($query);
$db->execute();
$query = null;
}
}
if (!is_null($query))
{
$db->setQuery($query);
$db->execute();
}
}
/**
* This is not supported by the belongsTo relation
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
public function getNew()
{
throw new DataModel\Relation\Exception\NewNotSupported("getNew() is not supported for many-to-may relations. Please add/remove items from the relation data and use push() to effect changes.");
}
/**
* Applies the relation filters to the foreign model when getData is called
*
* @param DataModel $foreignModel The foreign model you're operating on
* @param DataModel\Collection $dataCollection If it's an eager loaded relation, the collection of loaded
* parent records
*
* @return boolean Return false to force an empty data collection
*/
protected function filterForeignModel(DataModel $foreignModel, DataModel\Collection $dataCollection = null)
{
$db = $this->parentModel->getDbo();
// Decide how to proceed, based on eager or lazy loading
if (is_object($dataCollection))
{
// Eager loaded relation
if (!empty($dataCollection))
{
// Get a list of local keys from the collection
$values = [];
/** @var $item DataModel */
foreach ($dataCollection as $item)
{
$v = $item->getFieldValue($this->localKey, null);
if (!is_null($v))
{
$values[] = $v;
}
}
// Keep only unique values
$values = array_unique($values);
$values = array_map(function ($x) use (&$db) {
return $db->q($x);
}, $values);
// Get the foreign keys from the glue table
$query = $db->getQuery(true)
->select([$db->qn($this->pivotLocalKey), $db->qn($this->pivotForeignKey)])
->from($db->qn($this->pivotTable))
->where($db->qn($this->pivotLocalKey) . ' IN(' . implode(',', $values) . ')');
$db->setQuery($query);
$foreignKeysUnmapped = $db->loadRowList();
$this->foreignKeyMap = [];
$foreignKeys = [];
foreach ($foreignKeysUnmapped as $unmapped)
{
$local = $unmapped[0];
$foreign = $unmapped[1];
if (!isset($this->foreignKeyMap[$local]))
{
$this->foreignKeyMap[$local] = [];
}
$this->foreignKeyMap[$local][] = $foreign;
$foreignKeys[] = $foreign;
}
// Keep only unique values. However, the array keys are all screwed up. See below.
$foreignKeys = array_unique($foreignKeys);
// This looks stupid, but it's required to reset the array keys. Without it where() below fails.
$foreignKeys = array_merge($foreignKeys);
// Apply the filter
if (!empty($foreignKeys))
{
$foreignModel->where($this->foreignKey, 'in', $foreignKeys);
}
else
{
return false;
}
}
else
{
return false;
}
}
else
{
// Lazy loaded relation; get the single local key
$localKey = $this->parentModel->getFieldValue($this->localKey, null);
if (is_null($localKey) || ($localKey === ''))
{
return false;
}
$query = $db->getQuery(true)
->select($db->qn($this->pivotForeignKey))
->from($db->qn($this->pivotTable))
->where($db->qn($this->pivotLocalKey) . ' = ' . $db->q($localKey));
$db->setQuery($query);
$foreignKeys = $db->loadColumn();
$this->foreignKeyMap[$localKey] = $foreignKeys;
// If there are no foreign keys (no foreign items assigned to our item) we return false which then causes
// the relation to return null, marking the lack of data.
if (empty($foreignKeys))
{
return false;
}
$foreignModel->where($this->foreignKey, 'in', $this->foreignKeyMap[$localKey]);
}
return true;
}
}
DataModel/Collection.php 0000604 00000015506 15245561014 0011210 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel;
defined('_JEXEC') || die;
use FOF40\Model\DataModel;
use FOF40\Utils\Collection as BaseCollection;
/**
* A collection of data models. You can enumerate it like an array, use it everywhere a collection is expected (e.g. a
* foreach loop) and even implements a countable interface. You can also batch-apply DataModel methods on it thanks to
* its magic __call() method, hence the type-hinting below.
*
* @method void setFieldValue(string $name, mixed $value = '')
* @method void archive()
* @method void save(mixed $data, string $orderingFilter = '', bool $ignore = null)
* @method void push(mixed $data, string $orderingFilter = '', bool $ignore = null, array $relations = null)
* @method void bind(mixed $data, array $ignore = [])
* @method void check()
* @method void reorder(string $where = '')
* @method void delete(mixed $id = null)
* @method void trash(mixed $id)
* @method void forceDelete(mixed $id = null)
* @method void lock(int $userId = null)
* @method void move(int $delta, string $where = '')
* @method void publish()
* @method void restore(mixed $id)
* @method void touch(int $userId = null)
* @method void unlock()
* @method void unpublish()
*/
class Collection extends BaseCollection
{
/**
* Find a model in the collection by key.
*
* @param mixed $key
* @param mixed $default
*
* @return DataModel
*/
public function find($key, $default = null)
{
if ($key instanceof DataModel)
{
$key = $key->getId();
}
return array_first($this->items, function ($itemKey, $model) use ($key) {
/** @var DataModel $model */
return $model->getId() == $key;
}, $default);
}
/**
* Remove an item in the collection by key
*
* @param mixed $key
*
* @return void
*/
public function removeById($key)
{
if ($key instanceof DataModel)
{
$key = $key->getId();
}
$index = array_search($key, $this->modelKeys());
if ($index !== false)
{
unset($this->items[$index]);
}
}
/**
* Add an item to the collection.
*
* @param mixed $item
*
* @return Collection
*/
public function add($item)
{
$this->items[] = $item;
return $this;
}
/**
* Determine if a key exists in the collection.
*
* @param mixed $key
*
* @return bool
*/
public function contains($key)
{
return !is_null($this->find($key));
}
/**
* Fetch a nested element of the collection.
*
* @param string $key
*
* @return Collection
*/
public function fetch(string $key): BaseCollection
{
return new static(array_fetch($this->toArray(), $key));
}
/**
* Get the max value of a given key.
*
* @param string $key
*
* @return mixed
*/
public function max($key)
{
return $this->reduce(function ($result, $item) use ($key) {
return (is_null($result) || $item->{$key} > $result) ? $item->{$key} : $result;
});
}
/**
* Get the min value of a given key.
*
* @param string $key
*
* @return mixed
*/
public function min($key)
{
return $this->reduce(function ($result, $item) use ($key) {
return (is_null($result) || $item->{$key} < $result) ? $item->{$key} : $result;
});
}
/**
* Get the array of primary keys
*
* @return array
*/
public function modelKeys()
{
return array_map(
function ($m) {
/** @var DataModel $m */
return $m->getId();
},
$this->items);
}
/**
* Merge the collection with the given items.
*
* @param BaseCollection|array $collection
*
* @return BaseCollection
*/
public function merge($collection): BaseCollection
{
$dictionary = $this->getDictionary($this);
foreach ($collection as $item)
{
$dictionary[$item->getId()] = $item;
}
return new static(array_values($dictionary));
}
/**
* Diff the collection with the given items.
*
* @param BaseCollection|array $collection
*
* @return BaseCollection
*/
public function diff($collection): BaseCollection
{
$diff = new static;
$dictionary = $this->getDictionary($collection);
foreach ($this->items as $item)
{
/** @var DataModel $item */
if (!isset($dictionary[$item->getId()]))
{
$diff->add($item);
}
}
return $diff;
}
/**
* Intersect the collection with the given items.
*
* @param BaseCollection|array $collection
*
* @return Collection
*/
public function intersect($collection): BaseCollection
{
$intersect = new static;
$dictionary = $this->getDictionary($collection);
foreach ($this->items as $item)
{
/** @var DataModel $item */
if (isset($dictionary[$item->getId()]))
{
$intersect->add($item);
}
}
return $intersect;
}
/**
* Return only unique items from the collection.
*
* @return BaseCollection
*/
public function unique(): BaseCollection
{
$dictionary = $this->getDictionary($this);
return new static(array_values($dictionary));
}
/**
* Get a base Support collection instance from this collection.
*
* @return BaseCollection
*/
public function toBase()
{
return new BaseCollection($this->items);
}
/**
* Magic method which allows you to run a DataModel method to all items in the collection.
*
* For example, you can do $collection->save('foobar' => 1) to update the 'foobar' column to 1 across all items in
* the collection.
*
* IMPORTANT: The return value of the method call is not returned back to you!
*
* @param string $name The method to call
* @param array $arguments The arguments to the method
*/
public function __call($name, $arguments)
{
if (count($this) === 0)
{
return;
}
$class = get_class($this->first());
if (method_exists($class, $name))
{
foreach ($this as $item)
{
switch (count($arguments))
{
case 0:
$item->$name();
break;
case 1:
$item->$name($arguments[0]);
break;
case 2:
$item->$name($arguments[0], $arguments[1]);
break;
case 3:
$item->$name($arguments[0], $arguments[1], $arguments[2]);
break;
case 4:
$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
break;
case 5:
$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
break;
case 6:
$item->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
break;
default:
call_user_func_array([$item, $name], $arguments);
break;
}
}
}
}
/**
* Get a dictionary keyed by primary keys.
*
* @param BaseCollection $collection
*
* @return array
*/
protected function getDictionary($collection)
{
$dictionary = [];
foreach ($collection as $value)
{
$dictionary[$value->getId()] = $value;
}
return $dictionary;
}
}
DataModel/Behaviour/RelationFilters.php 0000604 00000004360 15245561014 0014143 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
use Joomla\Registry\Registry;
class RelationFilters extends Observer
{
/**
* This event runs after we have built the query used to fetch a record list in a model. It is used to apply
* automatic query filters based on model relations.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
$relationFilters = $model->getRelationFilters();
foreach ($relationFilters as $filterState)
{
$relationName = $filterState['relation'];
$tableAlias = $model->getBehaviorParam('tableAlias', null);
$subQuery = $model->getRelations()->getCountSubquery($relationName, $tableAlias);
// Callback method needs different handling
if (isset($filterState['method']) && ($filterState['method'] == 'callback'))
{
call_user_func_array($filterState['value'], array(&$subQuery));
$filterState['method'] = 'search';
$filterState['operator'] = '>=';
$filterState['value'] = '1';
}
$options = new Registry($filterState);
$filter = new DataModel\Filter\Relation($model->getDbo(), $relationName, $subQuery);
$methods = $filter->getSearchMethods();
$method = $options->get('method', $filter->getDefaultSearchMethod());
if (!in_array($method, $methods))
{
$method = 'exact';
}
switch ($method)
{
case 'between':
case 'outside':
$sql = $filter->$method($options->get('from', null), $options->get('to'));
break;
case 'interval':
$sql = $filter->$method($options->get('value', null), $options->get('interval'));
break;
case 'search':
$sql = $filter->$method($options->get('value', null), $options->get('operator', '='));
break;
default:
$sql = $filter->$method($options->get('value', null));
break;
}
if ($sql)
{
$query->where($sql);
}
}
}
}
DataModel/Behaviour/Enabled.php 0000604 00000003342 15245561014 0012366 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to filter access to items based on the enabled status
*
* @since 2.1
*/
class Enabled extends Observer
{
/**
* This event runs before we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onBeforeBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
// Make sure the field actually exists
if (!$model->hasField('enabled'))
{
return;
}
$fieldName = $model->getFieldAlias('enabled');
$db = $model->getDbo();
$model->whereRaw($db->qn($fieldName) . ' = ' . $db->q(1));
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param mixed &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(DataModel &$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('enabled'))
{
return;
}
// Filter by enabled status
if (!$model->getFieldValue('enabled', 0))
{
$model->reset(true);
}
}
}
DataModel/Behaviour/EmptyNonZero.php 0000604 00000001636 15245561014 0013451 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to let the Filters behaviour know that zero value is a valid filter value
*
* @since 2.1
*/
class EmptyNonZero extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
$model->setBehaviorParam('filterZero', 1);
}
}
DataModel/Behaviour/Language.php 0000604 00000011131 15245561014 0012552 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
/**
* FOF model behavior class to filter front-end access to items
* based on the language.
*
* @since 2.1
*/
class Language extends Observer
{
/** @var \PlgSystemLanguageFilter */
protected $lang_filter_plugin;
/**
* This event runs before we have built the query used to fetch a record
* list in a model. It is used to blacklist the language filter
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The model which calls this event
*
* @return void
* @noinspection PhpUnusedParameterInspection
*/
public function onBeforeBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
if ($model->getContainer()->platform->isFrontend())
{
$model->blacklistFilters('language');
}
// Make sure the field actually exists AND we're not in CLI
if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
{
return;
}
/** @var SiteApplication $app */
$app = JoomlaFactory::getApplication();
$hasLanguageFilter = method_exists($app, 'getLanguageFilter');
if ($hasLanguageFilter)
{
$hasLanguageFilter = $app->getLanguageFilter();
}
if (!$hasLanguageFilter)
{
return;
}
// Ask Joomla for the plugin only if we don't already have it. Useful for tests
if(!$this->lang_filter_plugin)
{
$this->lang_filter_plugin = PluginHelper::getPlugin('system', 'languagefilter');
}
$lang_filter_params = new Registry($this->lang_filter_plugin->params);
$languages = array('*');
if ($lang_filter_params->get('remove_default_prefix'))
{
// Get default site language
$platform = $model->getContainer()->platform;
$lg = $platform->getLanguage();
$languages[] = $lg->getTag();
}
else
{
// We have to use JoomlaInput since the language fragment is not set in the $_REQUEST, thus we won't have it in our model
// TODO Double check the previous assumption
$languages[] = JoomlaFactory::getApplication()->input->getCmd('language', '*');
}
// Filter out double languages
$languages = array_unique($languages);
// And filter the query output by these languages
$db = $model->getDbo();
$languages = array_map(array($db, 'quote'), $languages);
$fieldName = $model->getFieldAlias('language');
$model->whereRaw($db->qn($fieldName) . ' IN(' . implode(', ', $languages) . ')');
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param mixed &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(DataModel &$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists AND we're not in CLI
if (!$model->hasField('language') || $model->getContainer()->platform->isCli())
{
return;
}
// Make sure it is a multilingual site and get a list of languages
/** @var SiteApplication $app */
$app = JoomlaFactory::getApplication();
$hasLanguageFilter = method_exists($app, 'getLanguageFilter');
if ($hasLanguageFilter)
{
$hasLanguageFilter = $app->getLanguageFilter();
}
if (!$hasLanguageFilter)
{
return;
}
// Ask Joomla for the plugin only if we don't already have it. Useful for tests
if(!$this->lang_filter_plugin)
{
$this->lang_filter_plugin = PluginHelper::getPlugin('system', 'languagefilter');
}
$lang_filter_params = new Registry($this->lang_filter_plugin->params);
$languages = array('*');
if ($lang_filter_params->get('remove_default_prefix'))
{
// Get default site language
$lg = $model->getContainer()->platform->getLanguage();
$languages[] = $lg->getTag();
}
else
{
$languages[] = JoomlaFactory::getApplication()->input->getCmd('language', '*');
}
// Filter out double languages
$languages = array_unique($languages);
// Filter by language
if (!in_array($model->getFieldValue('language'), $languages))
{
$model->reset();
}
}
}
DataModel/Behaviour/Created.php 0000604 00000004050 15245561014 0012400 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
/**
* FOF model behavior class to updated the created_by and created_on fields on newly created records.
*
* This behaviour is added to DataModel by default. If you want to remove it you need to do
* $this->behavioursDispatcher->removeBehaviour('Created');
*
* @since 3.0
*/
class Created extends Observer
{
/**
* Add the created_on and created_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
* so that we can fill them in through this behaviour.
*
* @param DataModel $model
*/
public function onBeforeCheck(DataModel &$model)
{
$model->addSkipCheckField('created_on');
$model->addSkipCheckField('created_by');
}
/**
* @param DataModel $model
* @param \stdClass $dataObject
*/
public function onBeforeCreate(DataModel &$model, &$dataObject)
{
// Handle the created_on field
if ($model->hasField('created_on'))
{
$nullDate = $model->isNullableField('created_on') ? null : $model->getDbo()->getNullDate();
$created_on = $model->getFieldValue('created_on');
if (empty($created_on) || ($created_on == $nullDate))
{
$model->setFieldValue('created_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));
$createdOnField = $model->getFieldAlias('created_on');
$dataObject->$createdOnField = $model->getFieldValue('created_on');
}
}
// Handle the created_by field
if ($model->hasField('created_by'))
{
$created_by = $model->getFieldValue('created_by');
if (empty($created_by))
{
$model->setFieldValue('created_by', $model->getContainer()->platform->getUser()->id);
$createdByField = $model->getFieldAlias('created_by');
$dataObject->$createdByField = $model->getFieldValue('created_by');
}
}
}
}
DataModel/Behaviour/Filters.php 0000604 00000006621 15245561014 0012447 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
use Joomla\Registry\Registry;
class Filters extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
$tableKey = $model->getIdFieldName();
$db = $model->getDbo();
$fields = $model->getTableFields();
$blacklist = $model->getBlacklistFilters();
$filterZero = $model->getBehaviorParam('filterZero', null);
$tableAlias = $model->getBehaviorParam('tableAlias', null);
foreach ($fields as $fieldname => $fieldmeta)
{
if (in_array($fieldname, $blacklist))
{
continue;
}
$fieldInfo = (object)array(
'name' => $fieldname,
'type' => $fieldmeta->Type,
'filterZero' => $filterZero,
'tableAlias' => $tableAlias,
);
$filterName = $fieldInfo->name;
$filterState = $model->getState($filterName, null);
// Special primary key handling: if ignore request is set we'll also look for an 'id' state variable if a
// state variable by the same name as the key doesn't exist. If ignore request is not set in the model we
// do not filter by 'id' since this interferes with going from an edit page to a browse page (the list is
// filtered by id without user controls to unset it).
if ($fieldInfo->name == $tableKey)
{
$filterState = $model->getState($filterName, null);
if (!$model->getIgnoreRequest())
{
continue;
}
if (empty($filterState))
{
$filterState = $model->getState('id', null);
}
}
$field = DataModel\Filter\AbstractFilter::getField($fieldInfo, array('dbo' => $db));
if (!is_object($field) || !($field instanceof DataModel\Filter\AbstractFilter))
{
continue;
}
if ((is_array($filterState) && (
array_key_exists('value', $filterState) ||
array_key_exists('from', $filterState) ||
array_key_exists('to', $filterState)
)) || is_object($filterState))
{
$options = new Registry($filterState);
}
else
{
$options = new Registry();
$options->set('value', $filterState);
}
$methods = $field->getSearchMethods();
$method = $options->get('method', $field->getDefaultSearchMethod());
if (!in_array($method, $methods))
{
$method = 'exact';
}
switch ($method)
{
case 'between':
case 'outside':
case 'range' :
$sql = $field->$method($options->get('from', null), $options->get('to', null), $options->get('include', false));
break;
case 'interval':
case 'modulo':
$sql = $field->$method($options->get('value', null), $options->get('interval'));
break;
case 'search':
$sql = $field->$method($options->get('value', null), $options->get('operator', '='));
break;
case 'exact':
case 'partial':
default:
$sql = $field->$method($options->get('value', null));
break;
}
if ($sql)
{
$query->where($sql);
}
}
}
}
DataModel/Behaviour/Own.php 0000604 00000004012 15245561014 0011572 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to filter access to items owned by the currently logged in user only
*
* @since 2.1
*/
class Own extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
// Make sure the field actually exists
if (!$model->hasField('created_by'))
{
return;
}
// Get the current user's id
$user_id = $model->getContainer()->platform->getUser()->id;
// And filter the query output by the user id
$db = $model->getContainer()->platform->getDbo();
$query->where($db->qn($model->getFieldAlias('created_by')) . ' = ' . $db->q($user_id));
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param mixed &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(DataModel &$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('created_by'))
{
return;
}
// Get the user
$user_id = $model->getContainer()->platform->getUser()->id;
$recordUser = $model->getFieldValue('created_by', null);
// Filter by authorised access levels
if ($recordUser != $user_id)
{
$model->reset(true);
}
}
}
DataModel/Behaviour/Modified.php 0000604 00000003623 15245561014 0012556 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to updated the modified_by and modified_on fields on newly created records.
*
* This behaviour is added to DataModel by default. If you want to remove it you need to do
* $this->behavioursDispatcher->removeBehaviour('Modified');
*
* @since 3.0
*/
class Modified extends Observer
{
/**
* Add the modified_on and modified_by fields in the fieldsSkipChecks list of the model. We expect them to be empty
* so that we can fill them in through this behaviour.
*
* @param DataModel $model
*/
public function onBeforeCheck(DataModel &$model)
{
$model->addSkipCheckField('modified_on');
$model->addSkipCheckField('modified_by');
}
/**
* @param DataModel $model
* @param \stdClass $dataObject
*/
public function onBeforeUpdate(DataModel &$model, &$dataObject)
{
// Make sure we're not modifying a locked record
$userId = $model->getContainer()->platform->getUser()->id;
$isLocked = $model->isLocked($userId);
if ($isLocked)
{
return;
}
// Handle the modified_on field
if ($model->hasField('modified_on'))
{
$model->setFieldValue('modified_on', $model->getContainer()->platform->getDate()->toSql(false, $model->getDbo()));
$modifiedOnField = $model->getFieldAlias('modified_on');
$dataObject->$modifiedOnField = $model->getFieldValue('modified_on');
}
// Handle the modified_by field
if ($model->hasField('modified_by'))
{
$model->setFieldValue('modified_by', $userId);
$modifiedByField = $model->getFieldAlias('modified_by');
$dataObject->$modifiedByField = $model->getFieldValue('modified_by');
}
}
}
DataModel/Behaviour/Assets.php 0000604 00000011362 15245561014 0012277 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use Joomla\CMS\Access\Rules;
use Joomla\CMS\Factory;
use Joomla\CMS\Table\Asset;
/**
* FOF model behavior class to add Joomla! ACL assets support
*
* @since 2.1
*/
class Assets extends Observer
{
public function onAfterSave(DataModel &$model)
{
if (!$model->hasField('asset_id') || !$model->isAssetsTracked())
{
return true;
}
$assetFieldAlias = $model->getFieldAlias('asset_id');
$currentAssetId = $model->getFieldValue('asset_id');
unset($model->$assetFieldAlias);
// Create the object used for inserting/updating data to the database
$fields = $model->getTableFields();
// Let's remove the asset_id field, since we unset the property above and we would get a PHP notice
if (isset($fields[$assetFieldAlias]))
{
unset($fields[$assetFieldAlias]);
}
// Asset Tracking
$parentId = $model->getAssetParentId();
$name = $model->getAssetName();
$title = $model->getAssetTitle();
$asset = new Asset(Factory::getDbo());
$asset->loadByName($name);
// Re-inject the asset id.
$this->$assetFieldAlias = $asset->id;
// Check for an error.
$error = $asset->getError();
// Since we are using \Joomla\CMS\Table\Table, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!empty($error))
{
throw new \Exception($error);
}
// @codeCoverageIgnoreEnd
// Specify how a new or moved node asset is inserted into the tree.
// Since we're unsetting the table field before, this statement is always true...
if (empty($model->$assetFieldAlias) || $asset->parent_id !== $parentId)
{
$asset->setLocation($parentId, 'last-child');
}
// Prepare the asset to be stored.
$asset->parent_id = $parentId;
$asset->name = $name;
$asset->title = $title;
if ($model->getRules() instanceof Rules)
{
$asset->rules = (string) $model->getRules();
}
// Since we are using \Joomla\CMS\Table\Table, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->check() || !$asset->store())
{
throw new \Exception($asset->getError());
}
// @codeCoverageIgnoreEnd
// Create an asset_id or heal one that is corrupted.
if (empty($model->$assetFieldAlias) || (($currentAssetId != $model->$assetFieldAlias) && !empty($model->$assetFieldAlias)))
{
// Update the asset_id field in this table.
$model->$assetFieldAlias = (int) $asset->id;
$k = $model->getKeyName();
$db = $model->getDbo();
$query = $db->getQuery(true)
->update($db->qn($model->getTableName()))
->set($db->qn($assetFieldAlias) . ' = ' . (int) $model->$assetFieldAlias)
->where($db->qn($k) . ' = ' . (int) $model->$k);
$db->setQuery($query)->execute();
}
return true;
}
public function onAfterBind(DataModel &$model, &$src)
{
if (!$model->isAssetsTracked())
{
return true;
}
$rawRules = [];
if (is_array($src) && array_key_exists('rules', $src) && is_array($src['rules']))
{
$rawRules = $src['rules'];
}
elseif (is_object($src) && isset($src->rules) && is_array($src->rules))
{
$rawRules = $src->rules;
}
if (empty($rawRules))
{
return true;
}
// Bind the rules.
if (isset($rawRules) && is_array($rawRules))
{
// We have to manually remove any empty value, since they will be converted to int,
// and "Inherited" values will become "Denied". Joomla is doing this manually, too.
$rules = [];
foreach ($rawRules as $action => $ids)
{
// Build the rules array.
$rules[$action] = [];
foreach ($ids as $id => $p)
{
if ($p !== '')
{
$rules[$action][$id] = $p == '1' || $p == 'true';
}
}
}
$model->setRules($rules);
}
return true;
}
public function onBeforeDelete(DataModel &$model, $oid)
{
if (!$model->isAssetsTracked())
{
return true;
}
$k = $model->getKeyName();
// If the table is not loaded, let's try to load it with the id
if (!$model->$k)
{
$model->load($oid);
}
// If I have an invalid assetName I have to stop
$name = $model->getAssetName();
// Do NOT touch \Joomla\CMS\Table\Table here -- we are loading the core asset table which is a \Joomla\CMS\Table\Table, not a FOF Table
$asset = new Asset(Factory::getDbo());
if ($asset->loadByName($name))
{
// Since we are using \Joomla\CMS\Table\Table, there is no way to mock it and test for failures :(
// @codeCoverageIgnoreStart
if (!$asset->delete())
{
throw new \Exception($asset->getError());
}
// @codeCoverageIgnoreEnd
}
return true;
}
}
DataModel/Behaviour/PageParametersToState.php 0000604 00000003324 15245561014 0015240 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\Registry\Registry;
/**
* FOF model behavior class to populate the state with the front-end page parameters
*
* @since 2.1
*/
class PageParametersToState extends Observer
{
public function onAfterConstruct(DataModel &$model)
{
// This only applies to the front-end
if (!$model->getContainer()->platform->isFrontend())
{
return;
}
// Get the page parameters
/** @var SiteApplication $app */
$app = JoomlaFactory::getApplication();
/** @var Registry $params */
$params = $app->getParams();
// Extract the page parameter keys
$asArray = [];
if (is_object($params) && method_exists($params, 'toArray'))
{
$asArray = $params->toArray();
}
if (empty($asArray))
{
// There are no keys; no point in going on.
return;
}
$keys = array_keys($asArray);
unset($asArray);
// Loop all page parameter keys
foreach ($keys as $key)
{
// This is the current model state
$currentState = $model->getState($key);
// This is the explicitly requested state in the input
$explicitInput = $model->input->get($key, null, 'raw');
// If the current state is empty and there's no explicit input we'll use the page parameters instead
if (!is_null($currentState))
{
return;
}
if (!is_null($explicitInput))
{
return;
}
$model->setState($key, $params->get($key));
}
}
}
DataModel/Behaviour/Tags.php 0000604 00000010100 15245561014 0011720 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observable;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use Joomla\CMS\Helper\TagsHelper;
/**
* FOF model behavior class to add Joomla! Tags support
*
* @since 2.1
*/
class Tags extends Observer
{
/** @var TagsHelper */
protected $tagsHelper;
public function __construct(Observable &$subject)
{
parent::__construct($subject);
$this->tagsHelper = new TagsHelper();
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
* @param \stdClass &$dataObject The data to bind to the form
*
* @return void
*/
public function onBeforeCreate(DataModel &$model, &$dataObject)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
unset($dataObject->$tagField);
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
* @param \stdClass &$dataObject The data to bind to the form
*
* @return void
*/
public function onBeforeUpdate(DataModel &$model, &$dataObject)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
unset($dataObject->$tagField);
}
/**
* The event which runs after binding data to the table
*
* @param DataModel &$model The model which calls this event
*
* @return void
*
* @throws \Exception Error message if failed to store tags
*/
public function onAfterSave(DataModel &$model)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
// Avoid to update on other method (e.g. publish, ...)
if (!in_array($model->getContainer()->input->getCmd('task'), ['apply', 'save', 'savenew']))
{
return;
}
$oldTags = $this->tagsHelper->getTagIds($model->getId(), $model->getContentType());
$newTags = $model->$tagField ? implode(',', $model->$tagField) : null;
// If no changes, we stop here
if ($oldTags == $newTags)
{
return;
}
// Check if the content type exists, and create it if it does not
$model->checkContentType();
$this->tagsHelper->typeAlias = $model->getContentType();
if (!$this->tagsHelper->postStoreProcess($model, $model->$tagField))
{
throw new \Exception('Error storing tags');
}
}
/**
* The event which runs after deleting a record
*
* @param DataModel &$model The model which calls this event
* @param integer $oid The PK value of the record which was deleted
*
* @return void
*
* @throws \Exception Error message if failed to detele tags
*/
public function onAfterDelete(DataModel &$model, $oid)
{
$this->tagsHelper->typeAlias = $model->getContentType();
if (!$this->tagsHelper->deleteTagData($model, $oid))
{
throw new \Exception('Error deleting Tags');
}
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
* @param mixed $data An associative array or object to bind to the DataModel instance.
*
* @return void
* @noinspection PhpUnusedParameterInspection
*/
public function onAfterBind(DataModel &$model, &$data)
{
$tagField = $model->getBehaviorParam('tagFieldName', 'tags');
if ($model->$tagField)
{
return;
}
$type = $model->getContentType();
$model->addKnownField($tagField);
$model->$tagField = $this->tagsHelper->getTagIds($model->getId(), $type);
}
/**
* This event runs after publishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterPublish(DataModel &$model)
{
$model->updateUcmContent();
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterUnpublish(DataModel &$model)
{
$model->updateUcmContent();
}
}
DataModel/Behaviour/ContentHistory.php 0000604 00000004244 15245561014 0014032 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use ContenthistoryHelper;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
/**
* FOF model behavior class to add Joomla! content history support
*
* @since 2.1
*/
class ContentHistory extends Observer
{
/** @var ContentHistoryHelper */
protected $historyHelper;
/**
* The event which runs after storing (saving) data to the database
*
* @param DataModel &$model The model which calls this event
*
* @return boolean True to allow saving without an error
*/
public function onAfterSave(DataModel &$model)
{
$model->checkContentType();
$componentParams = $model->getContainer()->params;
if ($componentParams->get('save_history', 0))
{
if (!$this->historyHelper)
{
$this->historyHelper = new ContentHistoryHelper($model->getContentType());
}
$this->historyHelper->store($model);
}
return true;
}
/**
* The event which runs before deleting a record
*
* @param DataModel &$model The model which calls this event
* @param integer $oid The PK value of the record to delete
*
* @return boolean True to allow the deletion
*/
public function onBeforeDelete(DataModel &$model, $oid)
{
$componentParams = $model->getContainer()->params;
if ($componentParams->get('save_history', 0))
{
if (!$this->historyHelper)
{
$this->historyHelper = new ContentHistoryHelper($model->getContentType());
}
$this->historyHelper->deleteHistory($model);
}
return true;
}
/**
* This event runs after publishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterPublish(DataModel &$model)
{
$model->updateUcmContent();
}
/**
* This event runs after unpublishing a record in a model
*
* @param DataModel &$model The model which calls this event
*
* @return void
*/
public function onAfterUnpublish(DataModel &$model)
{
$model->updateUcmContent();
}
}
DataModel/Behaviour/Access.php 0000604 00000003447 15245561014 0012243 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Behaviour;
defined('_JEXEC') || die;
use FOF40\Event\Observer;
use FOF40\Model\DataModel;
use JDatabaseQuery;
/**
* FOF model behavior class to filter access to items based on the viewing access levels.
*
* @since 2.1
*/
class Access extends Observer
{
/**
* This event runs after we have built the query used to fetch a record
* list in a model. It is used to apply automatic query filters.
*
* @param DataModel &$model The model which calls this event
* @param JDatabaseQuery &$query The query we are manipulating
*
* @return void
*/
public function onAfterBuildQuery(DataModel &$model, JDatabaseQuery &$query)
{
// Make sure the field actually exists
if (!$model->hasField('access'))
{
return;
}
$model->applyAccessFiltering(null);
}
/**
* The event runs after DataModel has retrieved a single item from the database. It is used to apply automatic
* filters.
*
* @param DataModel &$model The model which was called
* @param mixed &$keys The keys used to locate the record which was loaded
*
* @return void
*/
public function onAfterLoad(DataModel &$model, &$keys)
{
// Make sure we have a DataModel
if (!($model instanceof DataModel))
{
return;
}
// Make sure the field actually exists
if (!$model->hasField('access'))
{
return;
}
// Get the user
$user = $model->getContainer()->platform->getUser();
$recordAccessLevel = $model->getFieldValue('access', null);
// Filter by authorised access levels
if (!in_array($recordAccessLevel, $user->getAuthorisedViewLevels()))
{
$model->reset(true);
}
}
}
DataModel/RelationManager.php 0000604 00000032762 15245561014 0012170 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel;
defined('_JEXEC') || die;
use FOF40\Model\DataModel;
class RelationManager
{
/** @var array The known relation types */
protected static $relationTypes = [];
/** @var DataModel The data model we are attached to */
protected $parentModel;
/** @var Relation[] The relations known to us */
protected $relations = [];
/** @var array A list of the names of eager loaded relations */
protected $eager = [];
/**
* Creates a new relation manager for the defined parent model
*
* @param DataModel $parentModel The model we are attached to
*/
public function __construct(DataModel $parentModel)
{
// Set the parent model
$this->parentModel = $parentModel;
// Make sure the relation types are initialised
static::getRelationTypes();
// @todo Maybe set up a few relations automatically?
}
/**
* Populates the static map of relation type methods and relation handling classes
*
* @return array Key = method name, Value = relation handling class
*/
public static function getRelationTypes()
{
if (empty(static::$relationTypes))
{
$relationTypeDirectory = __DIR__ . '/Relation';
$fs = new \DirectoryIterator($relationTypeDirectory);
/** @var $file \DirectoryIterator */
foreach ($fs as $file)
{
if ($file->isDir())
{
continue;
}
if ($file->getExtension() != 'php')
{
continue;
}
$baseName = ucfirst($file->getBasename('.php'));
$methodName = strtolower($baseName[0]) . substr($baseName, 1);
$className = '\\FOF40\\Model\\DataModel\\Relation\\' . $baseName;
if (!class_exists($className, true))
{
continue;
}
static::$relationTypes[$methodName] = $className;
}
}
return static::$relationTypes;
}
/**
* Implements deep cloning of the relation object
*/
function __clone()
{
$relations = [];
/** @var Relation[] $relations */
foreach ($this->relations as $key => $relation)
{
$relations[$key] = clone($relation);
$relations[$key]->reset();
}
$this->relations = $relations;
}
/**
* Rebase a relation manager
*
* @param DataModel $parentModel
*/
public function rebase(DataModel $parentModel)
{
$this->parentModel = $parentModel;
if (count($this->relations) > 0)
{
foreach ($this->relations as $relation)
{
/** @var Relation $relation */
$relation->rebase($parentModel);
}
}
}
/**
* Populates the internal $this->data collection of a relation from the contents of the provided collection. This is
* used by DataModel to push the eager loaded data into each item's relation.
*
* @param string $name Relation name
* @param Collection $data The relation data to push into this relation
* @param mixed $keyMap Used by many-to-many relations to pass around the local to foreign key map
*
* @return void
*
* @throws Relation\Exception\RelationNotFound
*/
public function setDataFromCollection($name, Collection &$data, $keyMap = null)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
$this->relations[$name]->setDataFromCollection($data, $keyMap);
}
/**
* Adds a relation to the relation manager
*
* @param string $name The name of the relation as known to this relation manager, e.g. 'phone'
* @param string $type The relation type, e.g. 'hasOne'
* @param string $foreignModelName The name of the foreign key's model in the format "modelName@com_something"
* @param string $localKey The local table key for this relation
* @param string $foreignKey The foreign key for this relation
* @param string $pivotTable For many-to-many relations, the pivot (glue) table
* @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local key
* @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign key
*
* @return DataModel The parent model, for chaining
*
* @throws Relation\Exception\RelationTypeNotFound when $type is not known
* @throws Relation\Exception\ForeignModelNotFound when $foreignModelClass doesn't exist
*/
public function addRelation($name, $type, $foreignModelName = null, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
if (!isset(static::$relationTypes[$type]))
{
throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$type' not found");
}
// Guess the foreign model class if necessary
if (empty($foreignModelName))
{
$foreignModelName = ucfirst($name);
}
$className = static::$relationTypes[$type];
/** @var Relation $relation */
$relation = new $className($this->parentModel, $foreignModelName, $localKey, $foreignKey,
$pivotTable, $pivotLocalKey, $pivotForeignKey);
$this->relations[$name] = $relation;
return $this->parentModel;
}
/**
* Removes a known relation
*
* @param string $name The name of the relation to remove
*
* @return DataModel The parent model, for chaining
*/
public function removeRelation($name)
{
if (isset($this->relations[$name]))
{
unset ($this->relations[$name]);
}
return $this->parentModel;
}
/**
* Removes all known relations
*/
public function resetRelations()
{
$this->relations = [];
}
/**
* Resets the data of all relations in this manager. This doesn't remove relations, just their data so that they
* get loaded again.
*
* @param array $relationsToReset The names of the relations to reset. Pass an empty array (default) to reset
* all relations.
*/
public function resetRelationData(array $relationsToReset = [])
{
/** @var Relation $relation */
foreach ($this->relations as $name => $relation)
{
if (!empty($relationsToReset) && !in_array($name, $relationsToReset))
{
continue;
}
$relation->reset();
}
}
/**
* Returns a list of all known relations' names
*
* @return array
*/
public function getRelationNames()
{
return array_keys($this->relations);
}
/**
* Gets the related items of a relation
*
* @param string $name The name of the relation to return data for
*
* @return Relation
*
* @throws Relation\Exception\RelationNotFound
*/
public function &getRelation($name)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name];
}
/**
* Get a new related item which satisfies relation $name and adds it to this relation's data list.
*
* @param string $name The relation based on which a new item is returned
*
* @return DataModel
*
* @throws Relation\Exception\RelationNotFound
*/
public function getNew($name)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getNew();
}
/**
* Saves all related items belonging to the specified relation or, if $name is null, all known relations which
* support saving.
*
* @param null|string $name The relation to save, or null to save all known relations
*
* @return DataModel The parent model, for chaining
*
* @throws Relation\Exception\RelationNotFound
*/
public function save($name = null)
{
if (is_null($name))
{
foreach ($this->relations as $relation)
{
try
{
$relation->saveAll();
}
catch (DataModel\Relation\Exception\SaveNotSupported $e)
{
// We don't care if a relation doesn't support saving
}
}
}
else
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
$this->relations[$name]->saveAll();
}
return $this->parentModel;
}
/**
* Gets the related items of a relation
*
* @param string $name The name of the relation to return data for
* @param callable $callback A callback to customise the returned data
* @param \FOF40\Utils\Collection $dataCollection Used when fetching the data of an eager loaded relation
*
* @return Collection|DataModel
*
* @throws Relation\Exception\RelationNotFound
* @see Relation::getData()
*
*/
public function getData($name, $callback = null, \FOF40\Utils\Collection $dataCollection = null)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getData($callback, $dataCollection);
}
/**
* Gets the foreign key map of a many-to-many relation
*
* @param string $name The name of the relation to return data for
*
* @return array
*
* @throws Relation\Exception\RelationNotFound
*/
public function &getForeignKeyMap($name)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getForeignKeyMap();
}
/**
* Returns the count sub-query for a relation, used for relation filters (whereHas in the DataModel).
*
* @param string $name The relation to get the sub-query for
* @param string $tableAlias The alias to use for the local table
*
* @return \JDatabaseQuery
* @throws Relation\Exception\RelationNotFound
*/
public function getCountSubquery($name, $tableAlias = null)
{
if (!isset($this->relations[$name]))
{
throw new DataModel\Relation\Exception\RelationNotFound("Relation '$name' not found");
}
return $this->relations[$name]->getCountSubquery($tableAlias);
}
/**
* A magic method which allows us to define relations using shorthand notation, e.g. $manager->hasOne('phone')
* instead of $manager->addRelation('phone', 'hasOne')
*
* You can also use it to get data of a relation using shorthand notation, e.g. $manager->getPhone($callback)
* instead of $manager->getData('phone', $callback);
*
* @param string $name The magic method to call
* @param array $arguments The arguments to the magic method
*
* @return DataModel The parent model, for chaining
*
* @throws \InvalidArgumentException
* @throws DataModel\Relation\Exception\RelationTypeNotFound
*/
function __call($name, $arguments)
{
$numberOfArguments = count($arguments);
if (isset(static::$relationTypes[$name]))
{
if ($numberOfArguments == 1)
{
return $this->addRelation($arguments[0], $name);
}
elseif ($numberOfArguments == 2)
{
return $this->addRelation($arguments[0], $name, $arguments[1]);
}
elseif ($numberOfArguments == 3)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2]);
}
elseif ($numberOfArguments == 4)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3]);
}
elseif ($numberOfArguments == 5)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
}
elseif ($numberOfArguments == 6)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5]);
}
elseif ($numberOfArguments >= 7)
{
return $this->addRelation($arguments[0], $name, $arguments[1], $arguments[2], $arguments[3], $arguments[4], $arguments[5], $arguments[6]);
}
else
{
throw new \InvalidArgumentException("You can not create an unnamed '$name' relation");
}
}
elseif (substr($name, 0, 3) == 'get')
{
$relationName = substr($name, 3);
$relationName = strtolower($relationName[0]) . substr($relationName, 1);
if ($numberOfArguments == 0)
{
return $this->getData($relationName);
}
elseif ($numberOfArguments == 1)
{
return $this->getData($relationName, $arguments[0]);
}
elseif ($numberOfArguments == 2)
{
return $this->getData($relationName, $arguments[0], $arguments[1]);
}
else
{
throw new \InvalidArgumentException("Invalid number of arguments getting data for the '$relationName' relation");
}
}
// Throw an exception otherwise
throw new DataModel\Relation\Exception\RelationTypeNotFound("Relation type '$name' not known to relation manager");
}
/**
* Is $name a magic-callable method?
*
* @param string $name The name of a potential magic-callable method
*
* @return bool
*/
public function isMagicMethod($name)
{
if (isset(static::$relationTypes[$name]))
{
return true;
}
elseif (substr($name, 0, 3) == 'get')
{
$relationName = substr($name, 3);
$relationName = strtolower($relationName[0]) . substr($relationName, 1);
if (isset($this->relations[$relationName]))
{
return true;
}
}
return false;
}
/**
* Is $name a magic property? Corollary: returns true if a relation of this name is known to the relation manager.
*
* @param string $name The name of a potential magic property
*
* @return bool
*/
public function isMagicProperty($name)
{
return isset($this->relations[$name]);
}
/**
* Magic method to get the data of a relation using shorthand notation, e.g. $manager->phone instead of
* $manager->getData('phone')
*
* @param $name
*
* @return Collection
*/
function __get($name)
{
return $this->getData($name);
}
}
DataModel/Filter/Number.php 0000604 00000015721 15245561014 0011571 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter;
defined('_JEXEC') || die;
class Number extends AbstractFilter
{
/**
* The partial match is mapped to an exact match
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function partial($value)
{
return $this->exact($value);
}
/**
* Perform a between limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The highest value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function between($from, $to, $include = true)
{
$from = (float) $from;
$to = (float) $to;
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$from = $this->sanitiseValue($from);
$to = $this->sanitiseValue($to);
$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND ';
return $sql . ('(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))');
}
/**
* Perform an outside limits match. When $include is true
* the condition tested is:
* (VALUE <= $from) || (VALUE >= $to)
* When $include is false the condition tested is:
* (VALUE < $from) || (VALUE > $to)
*
* @param mixed $from The lowest value of the excluded range
* @param mixed $to The highest value of the excluded range
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function outside($from, $to, $include = false)
{
$from = (float) $from;
$to = (float) $to;
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$from = $this->sanitiseValue($from);
$to = $this->sanitiseValue($to);
$sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $from . ') OR ';
return $sql . ('(' . $this->getFieldName() . ' >' . $extra . ' ' . $to . '))');
}
/**
* Perform an interval match. It's similar to a 'between' match, but the
* from and to values are calculated based on $value and $interval:
* $value - $interval < VALUE < $value + $interval
*
* @param integer|float $value The center value of the search space
* @param integer|float $interval The width of the search space
* @param boolean $include Should I include the boundaries in the search?
*
* @return string The SQL where clause
*/
public function interval($value, $interval, $include = true)
{
if ($this->isEmpty($value))
{
return '';
}
// Convert them to float, just to be sure
$value = (float) $value;
$interval = (float) $interval;
$from = $value - $interval;
$to = $value + $interval;
$extra = '';
if ($include)
{
$extra = '=';
}
$from = $this->sanitiseValue($from);
$to = $this->sanitiseValue($to);
$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ') AND ';
return $sql . ('(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . '))');
}
/**
* Perform a range limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The highest value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function range($from, $to, $include = true)
{
if ($this->isEmpty($from) && $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = [];
if ($from)
{
$sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $from . ')';
}
if ($to)
{
$sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $to . ')';
}
return '(' . implode(' AND ', $sql) . ')';
}
/**
* Perform an interval match. It's similar to a 'between' match, but the
* from and to values are calculated based on $value and $interval:
* $value - $interval < VALUE < $value + $interval
*
* @param integer|float $value The starting value of the search space
* @param integer|float $interval The interval period of the search space
* @param boolean $include Should I include the boundaries in the search?
*
* @return string The SQL where clause
*/
public function modulo($value, $interval, $include = true)
{
if ($this->isEmpty($value) || $this->isEmpty($interval))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $value . ' AND ';
return $sql . ('(' . $this->getFieldName() . ' - ' . $value . ') % ' . $interval . ' = 0)');
}
/**
* Overrides the parent to handle floats in locales where the decimal separator is a comma instead of a dot
*
* @param mixed $value
* @param string $operator
*
* @return string
*/
public function search($value, $operator = '=')
{
$value = $this->sanitiseValue($value);
return parent::search($value, $operator);
}
/**
* Sanitises float values. Really ugly and desperate workaround. Read below.
*
* Some locales, such as el-GR, use a comma as the decimal separator. This means that $x = 1.23; echo (string) $x;
* will yield 1,23 (with a comma!) instead of 1.23 (with a dot!). This affects the way the SQL WHERE clauses are
* generated. All database servers expect a dot as the decimal separator. If they see a decimal with a comma as the
* separator they throw a SQL error.
*
* This method will try to replace commas with dots. I tried working around this with locale switching and the %F
* (capital F) format option in sprintf to no avail. I'm pretty sure I was doing something wrong, but I ran out of
* time trying to find an academically correct solution. The current implementation of sanitiseValue is a silly
* hack around the problem. If you have a proper –and better performing– solution please send in a PR and I'll put
* it to the test.
*
* @param mixed $value A string representing a number, integer, float or array of them.
*
* @return mixed The sanitised value, or null if the input wasn't numeric.
*/
public function sanitiseValue($value)
{
if (!is_numeric($value) && !is_string($value) && !is_array($value))
{
$value = null;
}
if (!is_array($value))
{
return str_replace(',', '.', (string) $value);
}
return array_map([$this, 'sanitiseValue'], $value);
}
}
DataModel/Filter/Date.php 0000604 00000011677 15245561014 0011224 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter;
defined('_JEXEC') || die;
class Date extends Text
{
/**
* Returns the default search method for this field.
*
* @return string
*/
public function getDefaultSearchMethod()
{
return 'exact';
}
/**
* Perform a between limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The highest value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function between($from, $to, $include = true)
{
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '((' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ') AND ';
return $sql . ('(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . '))');
}
/**
* Perform an outside limits match. When $include is true
* the condition tested is:
* (VALUE <= $from) || (VALUE >= $to)
* When $include is false the condition tested is:
* (VALUE < $from) || (VALUE > $to)
*
* @param mixed $from The lowest value of the excluded range
* @param mixed $to The highest value of the excluded range
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function outside($from, $to, $include = false)
{
if ($this->isEmpty($from) || $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '((' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($from) . ') AND ';
return $sql . ('(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($to) . '))');
}
/**
* Interval date search
*
* @param string $value The value to search
* @param string|array|object $interval The interval. Can be (+1 MONTH or array('value' => 1, 'unit' =>
* 'MONTH', 'sign' => '+'))
* @param boolean $include If the borders should be included
*
* @return string the sql string
*/
public function interval($value, $interval, $include = true)
{
if ($this->isEmpty($value) || $this->isEmpty($interval))
{
return '';
}
$interval = $this->getInterval($interval);
// Sanity check on $interval array
if (!isset($interval['sign']) || !isset($interval['value']) || !isset($interval['unit']))
{
return '';
}
$function = $interval['sign'] == '+' ? 'DATE_ADD' : 'DATE_SUB';
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $function;
return $sql . ('(' . $this->getFieldName() . ', INTERVAL ' . $interval['value'] . ' ' . $interval['unit'] . '))');
}
/**
* Perform a between limits match. When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The highest value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
public function range($from, $to, $include = true)
{
if ($this->isEmpty($from) && $this->isEmpty($to))
{
return '';
}
$extra = '';
if ($include)
{
$extra = '=';
}
$sql = [];
if ($from)
{
$sql[] = '(' . $this->getFieldName() . ' >' . $extra . ' ' . $this->db->q($from) . ')';
}
if ($to)
{
$sql[] = '(' . $this->getFieldName() . ' <' . $extra . ' ' . $this->db->q($to) . ')';
}
return '(' . implode(' AND ', $sql) . ')';
}
/**
* Parses an interval –which may be given as a string, array or object– into
* a standardised hash array that can then be used bu the interval() method.
*
* @param string|array|object $interval The interval expression to parse
*
* @return array The parsed, hash array form of the interval
*/
protected function getInterval($interval)
{
if (is_string($interval))
{
if (strlen($interval) > 2)
{
$interval = explode(" ", $interval);
$sign = ($interval[0] == '-') ? '-' : '+';
$value = (int) substr($interval[0], 1);
$interval = [
'unit' => $interval[1],
'value' => $value,
'sign' => $sign,
];
}
else
{
$interval = [
'unit' => 'MONTH',
'value' => 1,
'sign' => '+',
];
}
}
else
{
$interval = (array) $interval;
}
return $interval;
}
}
DataModel/Filter/AbstractFilter.php 0000604 00000023074 15245561014 0013252 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter;
defined('_JEXEC') || die;
use FOF40\Model\DataModel\Filter\Exception\InvalidFieldObject;
use FOF40\Model\DataModel\Filter\Exception\NoDatabaseObject;
abstract class AbstractFilter
{
/**
* The null value for this type
*
* @var mixed
*/
public $null_value;
protected $db;
/**
* The column name of the table field
*
* @var string
*/
protected $name = '';
/**
* The column type of the table field
*
* @var string
*/
protected $type = '';
/**
* Should I allow filtering against the number 0?
*
* @var bool
*/
protected $filterZero = true;
/**
* Prefix each table name with this table alias. For example, field bar normally creates a WHERE clause:
* `bar` = '1'
* If tableAlias is set to "foo" then the WHERE clause it generates becomes
* `foo`.`bar` = '1'
*
* @var null
*/
protected $tableAlias;
/**
* Constructor
*
* @param \JDatabaseDriver $db The database object
* @param object $field The field information as taken from the db
*/
public function __construct($db, $field)
{
$this->db = $db;
if (!is_object($field) || !isset($field->name) || !isset($field->type))
{
throw new InvalidFieldObject;
}
$this->name = $field->name;
$this->type = $field->type;
if (isset ($field->filterZero))
{
$this->filterZero = $field->filterZero;
}
if (isset ($field->tableAlias))
{
$this->tableAlias = $field->tableAlias;
}
}
/**
* Creates a field Object based on the field column type
*
* @param object $field The field information
* @param array $config The field configuration (like the db object to use)
*
* @return AbstractFilter The Filter object
*
* @throws \InvalidArgumentException
*/
public static function getField($field, $config = [])
{
if (!is_object($field) || !isset($field->name) || !isset($field->type))
{
throw new InvalidFieldObject;
}
$type = $field->type;
$classType = self::getFieldType($type);
$className = '\\FOF40\\Model\\DataModel\\Filter\\' . ucfirst($classType);
if (($classType !== false) && class_exists($className, true))
{
if (!isset($config['dbo']))
{
throw new NoDatabaseObject($className);
}
$db = $config['dbo'];
return new $className($db, $field);
}
return null;
}
/**
* Get the class name based on the field Type
*
* @param string $type The type of the field
*
* @return string the class name suffix
*/
public static function getFieldType($type)
{
// Remove parentheses, indicating field options / size (they don't matter in type detection)
if (!empty($type))
{
[$type,] = explode('(', $type);
}
$detectedType = null;
switch (trim($type))
{
case 'varchar':
case 'text':
case 'smalltext':
case 'longtext':
case 'char':
case 'mediumtext':
case 'character varying':
case 'nvarchar':
case 'nchar':
$detectedType = 'Text';
break;
case 'date':
case 'datetime':
case 'time':
case 'year':
case 'timestamp':
case 'timestamp without time zone':
case 'timestamp with time zone':
$detectedType = 'Date';
break;
case 'tinyint':
case 'smallint':
$detectedType = 'Boolean';
break;
}
// Sometimes we have character types followed by a space and some cruft. Let's handle them.
if (is_null($detectedType) && !empty($type))
{
[$type,] = explode(' ', $type);
switch (trim($type))
{
case 'varchar':
case 'text':
case 'smalltext':
case 'longtext':
case 'char':
case 'mediumtext':
case 'nvarchar':
case 'nchar':
$detectedType = 'Text';
break;
case 'date':
case 'datetime':
case 'time':
case 'year':
case 'timestamp':
$detectedType = 'Date';
break;
case 'tinyint':
case 'smallint':
$detectedType = 'Boolean';
break;
default:
$detectedType = 'Number';
break;
}
}
// If all else fails assume it's a Number and hope for the best
if (empty($detectedType))
{
$detectedType = 'Number';
}
return $detectedType;
}
/**
* Is it a null or otherwise empty value?
*
* @param mixed $value The value to test for emptiness
*
* @return boolean
*/
public function isEmpty($value)
{
return (($value === $this->null_value) || empty($value))
&& !($this->filterZero && ($value === "0"));
}
/**
* Returns the default search method for a field. This always returns 'exact'
* and you are supposed to override it in specialised classes. The possible
* values are exact, partial, between and outside, unless something
* different is returned by getSearchMethods().
*
* @return string
* @see self::getSearchMethods()
*
*/
public function getDefaultSearchMethod()
{
return 'exact';
}
/**
* Return the search methods available for this field class,
*
* @return array
*/
public function getSearchMethods()
{
$ignore = [
'isEmpty', 'getField', 'getFieldType', '__construct', 'getDefaultSearchMethod', 'getSearchMethods',
'getFieldName',
];
$class = new \ReflectionClass(__CLASS__);
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
$tmp = [];
foreach ($methods as $method)
{
$tmp[] = $method->name;
}
$methods = $tmp;
if ($methods = array_diff($methods, $ignore))
{
return $methods;
}
return [];
}
/**
* Perform an exact match (equality matching)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function exact($value)
{
if ($this->isEmpty($value))
{
return '';
}
if (is_array($value))
{
$db = $this->db;
$value = array_map([$db, 'quote'], $value);
return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))';
}
else
{
return $this->search($value);
}
}
/**
* Perform a partial match (usually: search in string)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
abstract public function partial($value);
/**
* Perform a between limits match (usually: search for a value between
* two numbers or a date between two preset dates). When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The highest value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
abstract public function between($from, $to, $include = true);
/**
* Perform an outside limits match (usually: search for a value outside an
* area or a date outside a preset period). When $include is true
* the condition tested is:
* (VALUE <= $from) || (VALUE >= $to)
* When $include is false the condition tested is:
* (VALUE < $from) || (VALUE > $to)
*
* @param mixed $from The lowest value of the excluded range
* @param mixed $to The highest value of the excluded range
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
abstract public function outside($from, $to, $include = false);
/**
* Perform an interval search (usually: a date interval check)
*
* @param string $from The value to search
* @param string|array|object $interval The interval
*
* @return string The SQL where clause for this search
*/
abstract public function interval($from, $interval);
/**
* Perform a between limits match (usually: search for a value between
* two numbers or a date between two preset dates). When $include is true
* the condition tested is:
* $from <= VALUE <= $to
* When $include is false the condition tested is:
* $from < VALUE < $to
*
* @param mixed $from The lowest value to compare to
* @param mixed $to The higherst value to compare to
* @param boolean $include Should we include the boundaries in the search?
*
* @return string The SQL where clause for this search
*/
abstract public function range($from, $to, $include = true);
/**
* Perform an modulo search
*
* @param integer|float $from The starting value of the search space
* @param integer|float $interval The interval period of the search space
* @param boolean $include Should I include the boundaries in the search?
*
* @return string The SQL where clause
*/
abstract public function modulo($from, $interval, $include = true);
/**
* Return the SQL where clause for a search
*
* @param mixed $value The value to search for
* @param string $operator The operator to use
*
* @return string The SQL where clause for this search
*/
public function search($value, $operator = '=')
{
if ($this->isEmpty($value))
{
return '';
}
$prefix = '';
if (substr($operator, 0, 1) == '!')
{
$prefix = 'NOT ';
$operator = substr($operator, 1);
}
return $prefix . '(' . $this->getFieldName() . ' ' . $operator . ' ' . $this->db->quote($value) . ')';
}
/**
* Get the field name
*
* @return string The field name
*/
public function getFieldName()
{
$name = $this->db->qn($this->name);
if (!empty($this->tableAlias))
{
$name = $this->db->qn($this->tableAlias) . '.' . $name;
}
return $name;
}
}
DataModel/Filter/Exception/InvalidFieldObject.php 0000604 00000001133 15245561014 0015750 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class InvalidFieldObject extends \InvalidArgumentException
{
public function __construct( $message = "", $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_FILTER_INVALIDFIELD');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Filter/Exception/NoDatabaseObject.php 0000604 00000001106 15245561014 0015417 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class NoDatabaseObject extends \InvalidArgumentException
{
public function __construct( $fieldType, $code = 500, Exception $previous = null )
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_FILTER_NODBOBJECT', $fieldType);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Filter/Text.php 0000604 00000006211 15245561014 0011257 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter;
defined('_JEXEC') || die;
class Text extends AbstractFilter
{
/**
* Constructor
*
* @param \JDatabaseDriver $db The database object
* @param object $field The field information as taken from the db
*/
public function __construct($db, $field)
{
parent::__construct($db, $field);
$this->null_value = '';
}
/**
* Returns the default search method for this field.
*
* @return string
*/
public function getDefaultSearchMethod()
{
return 'partial';
}
/**
* Perform a partial match (search in string)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function partial($value)
{
if ($this->isEmpty($value))
{
return '';
}
return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote('%' . $value . '%') . ')';
}
/**
* Perform an exact match (match string)
*
* @param mixed $value The value to compare to
*
* @return string The SQL where clause for this search
*/
public function exact($value)
{
if ($this->isEmpty($value))
{
return '';
}
if (is_array($value) || is_object($value))
{
$value = (array) $value;
$db = $this->db;
$value = array_map([$db, 'quote'], $value);
return '(' . $this->getFieldName() . ' IN (' . implode(',', $value) . '))';
}
return '(' . $this->getFieldName() . ' LIKE ' . $this->db->quote($value) . ')';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $to Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function between($from, $to, $include = true)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $to Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function outside($from, $to, $include = false)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $value Ignored
* @param mixed $interval Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function interval($value, $interval, $include = true)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $to Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function range($from, $to, $include = false)
{
return '';
}
/**
* Dummy method; this search makes no sense for text fields
*
* @param mixed $from Ignored
* @param mixed $interval Ignored
* @param boolean $include Ignored
*
* @return string Empty string
*/
public function modulo($from, $interval, $include = false)
{
return '';
}
}
DataModel/Filter/Boolean.php 0000604 00000000760 15245561014 0011715 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter;
defined('_JEXEC') || die;
class Boolean extends Number
{
/**
* Is it a null or otherwise empty value?
*
* @param mixed $value The value to test for emptiness
*
* @return bool
*/
public function isEmpty($value)
{
return is_null($value) || ($value === '');
}
}
DataModel/Filter/Relation.php 0000604 00000001350 15245561014 0012107 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Filter;
defined('_JEXEC') || die;
class Relation extends Number
{
/** @var \JDatabaseQuery The COUNT subquery to filter by */
protected $subQuery;
public function __construct($db, $relationName, $subQuery)
{
$field = (object)array(
'name' => $relationName,
'type' => 'relation',
);
parent::__construct($db, $field);
$this->subQuery = $subQuery;
}
public function callback($value)
{
return call_user_func($value, $this->subQuery);
}
public function getFieldName()
{
return '(' . $this->subQuery . ')';
}
}
DataModel/Relation.php 0000604 00000020125 15245561014 0010663 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Model\DataModel;
abstract class Relation
{
/** @var DataModel The data model we are attached to */
protected $parentModel;
/** @var string The class name of the foreign key's model */
protected $foreignModelClass;
/** @var string The application name of the foreign model */
protected $foreignModelComponent;
/** @var string The bade name of the foreign model */
protected $foreignModelName;
/** @var string The local table key for this relation */
protected $localKey;
/** @var string The foreign table key for this relation */
protected $foreignKey;
/** @var null For many-to-many relations, the pivot (glue) table */
protected $pivotTable;
/** @var null For many-to-many relations, the pivot table's column storing the local key */
protected $pivotLocalKey;
/** @var null For many-to-many relations, the pivot table's column storing the foreign key */
protected $pivotForeignKey;
/** @var Collection The data loaded by this relation */
protected $data;
/** @var array Maps each local table key to an array of foreign table keys, used in many-to-many relations */
protected $foreignKeyMap = [];
/** @var Container The component container for this relation */
protected $container;
/**
* Public constructor. Initialises the relation.
*
* @param DataModel $parentModel The data model we are attached to
* @param string $foreignModelName The name of the foreign key's model in the format
* "modelName@com_something"
* @param string $localKey The local table key for this relation
* @param string $foreignKey The foreign key for this relation
* @param string $pivotTable For many-to-many relations, the pivot (glue) table
* @param string $pivotLocalKey For many-to-many relations, the pivot table's column storing the local
* key
* @param string $pivotForeignKey For many-to-many relations, the pivot table's column storing the foreign
* key
*/
public function __construct(DataModel $parentModel, $foreignModelName, $localKey = null, $foreignKey = null, $pivotTable = null, $pivotLocalKey = null, $pivotForeignKey = null)
{
$this->parentModel = $parentModel;
$this->foreignModelClass = $foreignModelName;
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->pivotTable = $pivotTable;
$this->pivotLocalKey = $pivotLocalKey;
$this->pivotForeignKey = $pivotForeignKey;
$this->container = $parentModel->getContainer();
$class = $foreignModelName;
if (strpos($class, '@') === false)
{
$this->foreignModelComponent = null;
$this->foreignModelName = $class;
}
else
{
$foreignParts = explode('@', $class, 2);
$this->foreignModelComponent = $foreignParts[1];
$this->foreignModelName = $foreignParts[0];
}
}
/**
* Reset the relation data
*
* @return $this For chaining
*/
public function reset()
{
$this->data = null;
$this->foreignKeyMap = [];
return $this;
}
/**
* Rebase the relation to a different model
*
* @param DataModel $model
*
* @return $this For chaining
*/
public function rebase(DataModel $model)
{
$this->parentModel = $model;
return $this->reset();
}
/**
* Get the relation data.
*
* If you want to apply additional filtering to the foreign model, use the $callback. It can be any function,
* static method, public method or closure with an interface of function(DataModel $foreignModel). You are not
* supposed to return anything, just modify $foreignModel's state directly. For example, you may want to do:
* $foreignModel->setState('foo', 'bar')
*
* @param callable $callback The callback to run on the remote model.
* @param Collection $dataCollection
*
* @return Collection|DataModel
*/
public function getData($callback = null, Collection $dataCollection = null)
{
if (is_null($this->data))
{
// Initialise
$this->data = new Collection();
// Get a model instance
$foreignModel = $this->getForeignModel();
$foreignModel->setIgnoreRequest(true);
$filtered = $this->filterForeignModel($foreignModel, $dataCollection);
if (!$filtered)
{
return $this->data;
}
// Apply the callback, if applicable
if (!is_null($callback) && is_callable($callback))
{
call_user_func($callback, $foreignModel);
}
// Get the list of items from the foreign model and cache in $this->data
$this->data = $foreignModel->get(true);
}
return $this->data;
}
/**
* Populates the internal $this->data collection from the contents of the provided collection. This is used by
* DataModel to push the eager loaded data into each item's relation.
*
* @param Collection $data The relation data to push into this relation
* @param mixed $keyMap Used by many-to-many relations to pass around the local to foreign key map
*
* @return void
*/
public function setDataFromCollection(Collection &$data, $keyMap = null)
{
$this->data = new Collection();
if (!empty($data))
{
$localKeyValue = $this->parentModel->getFieldValue($this->localKey);
/** @var DataModel $item */
foreach ($data as $item)
{
if ($item->getFieldValue($this->foreignKey) == $localKeyValue)
{
$this->data->add($item);
}
}
}
}
/**
* Returns the count subquery for DataModel's has() and whereHas() methods.
*
* @return \JDatabaseQuery
*/
abstract public function getCountSubquery();
/**
* Returns a new item of the foreignModel type, pre-initialised to fulfil this relation
*
* @return DataModel
*
* @throws DataModel\Relation\Exception\NewNotSupported when it's not supported
*/
abstract public function getNew();
/**
* Saves all related items. You can use it to touch items as well: every item being saved causes the modified_by and
* modified_on fields to be changed automatically, thanks to the DataModel's magic.
*/
public function saveAll()
{
if ($this->data instanceof Collection)
{
foreach ($this->data as $item)
{
if ($item instanceof DataModel)
{
$item->save();
}
}
}
}
/**
* Returns the foreign key map of a many-to-many relation, used for eager loading many-to-many relations
*
* @return array
*/
public function &getForeignKeyMap()
{
return $this->foreignKeyMap;
}
/**
* Gets an object instance of the foreign model
*
* @param array $config Optional configuration information for the Model
*
* @return DataModel
*/
public function &getForeignModel(array $config = [])
{
// If the model comes from this component go through our Factory
if (is_null($this->foreignModelComponent))
{
/** @var DataModel $model */
$model = $this->container->factory->model($this->foreignModelName, $config)->tmpInstance();
return $model;
}
// The model comes from another component. Create a container and go through its factory.
$foreignContainer = Container::getInstance($this->foreignModelComponent, ['tempInstance' => true]);
/** @var DataModel $model */
$model = $foreignContainer->factory->model($this->foreignModelName, $config)->tmpInstance();
return $model;
}
/**
* Returns the name of the local key of the relation
*
* @return string
*/
public function getLocalKey()
{
return $this->localKey;
}
/**
* Applies the relation filters to the foreign model when getData is called
*
* @param DataModel $foreignModel The foreign model you're operating on
* @param Collection $dataCollection If it's an eager loaded relation, the collection of loaded parent records
*
* @return boolean Return false to force an empty data collection
*/
abstract protected function filterForeignModel(DataModel $foreignModel, Collection $dataCollection = null);
}
DataModel/Exception/NoContentType.php 0000604 00000001070 15245561014 0013613 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class NoContentType extends \UnexpectedValueException
{
public function __construct( $className, $code = 500, Exception $previous = null )
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_NOCONTENTTYPE', $className);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeIncompatibleTable.php 0000604 00000001111 15245561014 0015234 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeIncompatibleTable extends \UnexpectedValueException
{
public function __construct( $tableName, $code = 500, Exception $previous = null )
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_INCOMPATIBLETABLE', $tableName);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeUnexpectedPrimaryKey.php 0000604 00000001130 15245561014 0016000 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeUnexpectedPrimaryKey extends \UnexpectedValueException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_TREE_UNEXPECTEDPK');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgt.php 0000604 00000000724 15245561014 0014400 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
abstract class TreeInvalidLftRgt extends \RuntimeException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgtSibling.php 0000604 00000001131 15245561014 0015701 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeInvalidLftRgtSibling extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_SIBLING');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgtCurrent.php 0000604 00000001131 15245561014 0015734 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeInvalidLftRgtCurrent extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_CURRENT');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/NoTableColumns.php 0000604 00000000442 15245561014 0013731 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
class NoTableColumns extends BaseException
{
}
DataModel/Exception/CannotLockNotLoadedRecord.php 0000604 00000001125 15245561014 0016027 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class CannotLockNotLoadedRecord extends BaseException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_CANNOTLOCKNOTLOADEDRECORD');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/SpecialColumnMissing.php 0000604 00000000450 15245561014 0015133 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
class SpecialColumnMissing extends BaseException
{
}
DataModel/Exception/InvalidSearchMethod.php 0000604 00000000447 15245561014 0014726 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
class InvalidSearchMethod extends BaseException
{
}
DataModel/Exception/TreeInvalidLftRgtOther.php 0000604 00000001125 15245561014 0015376 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeInvalidLftRgtOther extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_OTHER');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/RecordNotLoaded.php 0000604 00000001076 15245561014 0014060 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class RecordNotLoaded extends BaseException
{
public function __construct( $message = "", $code = 404, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_COULDNOTLOAD');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeRootNotFound.php 0000604 00000001076 15245561014 0014270 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeRootNotFound extends \RuntimeException
{
public function __construct($tableName, $lft, $code = 500, Exception $previous = null)
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_ROOTNOTFOUND', $tableName, $lft);
parent::__construct($message, $code, $previous);
}
}
DataModel/Exception/TreeMethodOnlyAllowedInRoot.php 0000604 00000001077 15245561014 0016416 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeMethodOnlyAllowedInRoot extends \RuntimeException
{
public function __construct( $method = '', $code = 500, Exception $previous = null )
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_ONLYINROOT', $method);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeInvalidLftRgtParent.php 0000604 00000001127 15245561014 0015550 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeInvalidLftRgtParent extends TreeInvalidLftRgt
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_TREE_INVALIDLFTRGT_PARENT');
}
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/NoItemsFound.php 0000604 00000001052 15245561014 0013414 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class NoItemsFound extends BaseException
{
public function __construct( $className, $code = 404, Exception $previous = null )
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_NOITEMSFOUND', $className);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/TreeUnsupportedMethod.php 0000604 00000001076 15245561014 0015361 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class TreeUnsupportedMethod extends \LogicException
{
public function __construct( $method = '', $code = 500, Exception $previous = null )
{
$message = Text::sprintf('LIB_FOF40_MODEL_ERR_TREE_UNSUPPORTEDMETHOD', $method);
parent::__construct( $message, $code, $previous );
}
}
DataModel/Exception/BaseException.php 0000604 00000000445 15245561014 0013600 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
class BaseException extends \RuntimeException
{
}
DataModel/Exception/NoAssetKey.php 0000604 00000001103 15245561014 0013064 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\DataModel\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
class NoAssetKey extends \UnexpectedValueException
{
public function __construct( $message = '', $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_NOASSETKEY');
}
parent::__construct( $message, $code, $previous );
}
}
Exception/CannotGetName.php 0000604 00000001164 15245561014 0011677 0 ustar 00 <?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Model\Exception;
defined('_JEXEC') || die;
use Exception;
use Joomla\CMS\Language\Text;
/**
* Exception thrown when we can't get a Controller's name
*/
class CannotGetName extends \RuntimeException
{
public function __construct( $message = "", $code = 500, Exception $previous = null )
{
if (empty($message))
{
$message = Text::_('LIB_FOF40_MODEL_ERR_GET_NAME');
}
parent::__construct( $message, $code, $previous );
}
}
index.html 0000604 00000000352 15245602041 0006534 0 ustar 00 <!--~
~ @package akeebabackup
~ @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
~ @license GNU General Public License version 3, or later
-->
<html><head><title></title></head><body></body></html>