Файловый менеджер - Редактировать - /home/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>