primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,76 @@
<?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);
}
}
}

View File

@ -0,0 +1,198 @@
<?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;
}
}

View File

@ -0,0 +1,100 @@
<?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();
}
}

View File

@ -0,0 +1,72 @@
<?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');
}
}
}
}

View File

@ -0,0 +1,36 @@
<?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);
}
}

View File

@ -0,0 +1,75 @@
<?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);
}
}
}

View File

@ -0,0 +1,133 @@
<?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);
}
}
}
}

View File

@ -0,0 +1,171 @@
<?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();
}
}
}

View File

@ -0,0 +1,71 @@
<?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');
}
}
}

View File

@ -0,0 +1,82 @@
<?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);
}
}
}

View File

@ -0,0 +1,79 @@
<?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));
}
}
}

View File

@ -0,0 +1,85 @@
<?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);
}
}
}
}

View File

@ -0,0 +1,170 @@
<?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();
}
}

View File

@ -0,0 +1,336 @@
<?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;
}
}

View File

@ -0,0 +1,15 @@
<?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
{
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,15 @@
<?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
{
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,24 @@
<?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 );
}
}

View File

@ -0,0 +1,24 @@
<?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 );
}
}

View File

@ -0,0 +1,15 @@
<?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
{
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,15 @@
<?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
{
}

View File

@ -0,0 +1,24 @@
<?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 );
}
}

View File

@ -0,0 +1,21 @@
<?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 );
}
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,24 @@
<?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 );
}
}

View File

@ -0,0 +1,24 @@
<?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);
}
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,24 @@
<?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 );
}
}

View File

@ -0,0 +1,424 @@
<?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;
}
}

View File

@ -0,0 +1,25 @@
<?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 === '');
}
}

View File

@ -0,0 +1,209 @@
<?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;
}
}

View File

@ -0,0 +1,27 @@
<?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 );
}
}

View File

@ -0,0 +1,24 @@
<?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 );
}
}

View File

@ -0,0 +1,260 @@
<?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);
}
}

View File

@ -0,0 +1,38 @@
<?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 . ')';
}
}

View File

@ -0,0 +1,150 @@
<?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 '';
}
}

View File

@ -0,0 +1,282 @@
<?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);
}

View File

@ -0,0 +1,72 @@
<?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");
}
}

View File

@ -0,0 +1,409 @@
<?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;
}
}

View File

@ -0,0 +1,12 @@
<?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 {}

View File

@ -0,0 +1,14 @@
<?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
{
}

View File

@ -0,0 +1,12 @@
<?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 {}

View File

@ -0,0 +1,12 @@
<?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 {}

View File

@ -0,0 +1,12 @@
<?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 {}

View File

@ -0,0 +1,12 @@
<?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 {}

View File

@ -0,0 +1,171 @@
<?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;
}
}

View File

@ -0,0 +1,46 @@
<?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);
}
}
}

View File

@ -0,0 +1,511 @@
<?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);
}
}

View File

@ -0,0 +1,30 @@
<?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 );
}
}

View File

@ -0,0 +1,77 @@
<?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);
}
}

View File

@ -0,0 +1,141 @@
<?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();
}
}
}

View File

@ -0,0 +1,61 @@
<?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;
}
}
}

View File

@ -0,0 +1,59 @@
<?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);
}
}

View File

@ -0,0 +1,62 @@
<?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);
}
}

View File

@ -0,0 +1,565 @@
<?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;
}
}

File diff suppressed because it is too large Load Diff