primo commit
This commit is contained in:
4162
libraries/fof40/Model/DataModel.php
Normal file
4162
libraries/fof40/Model/DataModel.php
Normal file
File diff suppressed because it is too large
Load Diff
76
libraries/fof40/Model/DataModel/Behaviour/Access.php
Normal file
76
libraries/fof40/Model/DataModel/Behaviour/Access.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
libraries/fof40/Model/DataModel/Behaviour/Assets.php
Normal file
198
libraries/fof40/Model/DataModel/Behaviour/Assets.php
Normal 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;
|
||||
}
|
||||
}
|
||||
100
libraries/fof40/Model/DataModel/Behaviour/ContentHistory.php
Normal file
100
libraries/fof40/Model/DataModel/Behaviour/ContentHistory.php
Normal 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();
|
||||
}
|
||||
}
|
||||
72
libraries/fof40/Model/DataModel/Behaviour/Created.php
Normal file
72
libraries/fof40/Model/DataModel/Behaviour/Created.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
libraries/fof40/Model/DataModel/Behaviour/EmptyNonZero.php
Normal file
36
libraries/fof40/Model/DataModel/Behaviour/EmptyNonZero.php
Normal 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);
|
||||
}
|
||||
}
|
||||
75
libraries/fof40/Model/DataModel/Behaviour/Enabled.php
Normal file
75
libraries/fof40/Model/DataModel/Behaviour/Enabled.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
libraries/fof40/Model/DataModel/Behaviour/Filters.php
Normal file
133
libraries/fof40/Model/DataModel/Behaviour/Filters.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
libraries/fof40/Model/DataModel/Behaviour/Language.php
Normal file
171
libraries/fof40/Model/DataModel/Behaviour/Language.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
71
libraries/fof40/Model/DataModel/Behaviour/Modified.php
Normal file
71
libraries/fof40/Model/DataModel/Behaviour/Modified.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
82
libraries/fof40/Model/DataModel/Behaviour/Own.php
Normal file
82
libraries/fof40/Model/DataModel/Behaviour/Own.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
libraries/fof40/Model/DataModel/Behaviour/Tags.php
Normal file
170
libraries/fof40/Model/DataModel/Behaviour/Tags.php
Normal 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();
|
||||
}
|
||||
}
|
||||
336
libraries/fof40/Model/DataModel/Collection.php
Normal file
336
libraries/fof40/Model/DataModel/Collection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
15
libraries/fof40/Model/DataModel/Exception/BaseException.php
Normal file
15
libraries/fof40/Model/DataModel/Exception/BaseException.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
27
libraries/fof40/Model/DataModel/Exception/NoAssetKey.php
Normal file
27
libraries/fof40/Model/DataModel/Exception/NoAssetKey.php
Normal 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 );
|
||||
}
|
||||
|
||||
}
|
||||
24
libraries/fof40/Model/DataModel/Exception/NoContentType.php
Normal file
24
libraries/fof40/Model/DataModel/Exception/NoContentType.php
Normal 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 );
|
||||
}
|
||||
|
||||
}
|
||||
24
libraries/fof40/Model/DataModel/Exception/NoItemsFound.php
Normal file
24
libraries/fof40/Model/DataModel/Exception/NoItemsFound.php
Normal 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 );
|
||||
}
|
||||
|
||||
}
|
||||
15
libraries/fof40/Model/DataModel/Exception/NoTableColumns.php
Normal file
15
libraries/fof40/Model/DataModel/Exception/NoTableColumns.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
424
libraries/fof40/Model/DataModel/Filter/AbstractFilter.php
Normal file
424
libraries/fof40/Model/DataModel/Filter/AbstractFilter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
25
libraries/fof40/Model/DataModel/Filter/Boolean.php
Normal file
25
libraries/fof40/Model/DataModel/Filter/Boolean.php
Normal 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 === '');
|
||||
}
|
||||
}
|
||||
209
libraries/fof40/Model/DataModel/Filter/Date.php
Normal file
209
libraries/fof40/Model/DataModel/Filter/Date.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
260
libraries/fof40/Model/DataModel/Filter/Number.php
Normal file
260
libraries/fof40/Model/DataModel/Filter/Number.php
Normal 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);
|
||||
}
|
||||
}
|
||||
38
libraries/fof40/Model/DataModel/Filter/Relation.php
Normal file
38
libraries/fof40/Model/DataModel/Filter/Relation.php
Normal 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 . ')';
|
||||
}
|
||||
}
|
||||
150
libraries/fof40/Model/DataModel/Filter/Text.php
Normal file
150
libraries/fof40/Model/DataModel/Filter/Text.php
Normal 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 '';
|
||||
}
|
||||
}
|
||||
282
libraries/fof40/Model/DataModel/Relation.php
Normal file
282
libraries/fof40/Model/DataModel/Relation.php
Normal 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);
|
||||
}
|
||||
72
libraries/fof40/Model/DataModel/Relation/BelongsTo.php
Normal file
72
libraries/fof40/Model/DataModel/Relation/BelongsTo.php
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
409
libraries/fof40/Model/DataModel/Relation/BelongsToMany.php
Normal file
409
libraries/fof40/Model/DataModel/Relation/BelongsToMany.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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
|
||||
{
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
@ -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 {}
|
||||
171
libraries/fof40/Model/DataModel/Relation/HasMany.php
Normal file
171
libraries/fof40/Model/DataModel/Relation/HasMany.php
Normal 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;
|
||||
}
|
||||
}
|
||||
46
libraries/fof40/Model/DataModel/Relation/HasOne.php
Normal file
46
libraries/fof40/Model/DataModel/Relation/HasOne.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
511
libraries/fof40/Model/DataModel/RelationManager.php
Normal file
511
libraries/fof40/Model/DataModel/RelationManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
30
libraries/fof40/Model/Exception/CannotGetName.php
Normal file
30
libraries/fof40/Model/Exception/CannotGetName.php
Normal 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 );
|
||||
}
|
||||
|
||||
}
|
||||
77
libraries/fof40/Model/Mixin/Assertions.php
Normal file
77
libraries/fof40/Model/Mixin/Assertions.php
Normal 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);
|
||||
}
|
||||
}
|
||||
141
libraries/fof40/Model/Mixin/DateManipulation.php
Normal file
141
libraries/fof40/Model/Mixin/DateManipulation.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
61
libraries/fof40/Model/Mixin/Generators.php
Normal file
61
libraries/fof40/Model/Mixin/Generators.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
libraries/fof40/Model/Mixin/ImplodedArrays.php
Normal file
59
libraries/fof40/Model/Mixin/ImplodedArrays.php
Normal 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);
|
||||
}
|
||||
}
|
||||
62
libraries/fof40/Model/Mixin/JsonData.php
Normal file
62
libraries/fof40/Model/Mixin/JsonData.php
Normal 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);
|
||||
}
|
||||
}
|
||||
565
libraries/fof40/Model/Model.php
Normal file
565
libraries/fof40/Model/Model.php
Normal 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;
|
||||
}
|
||||
}
|
||||
2277
libraries/fof40/Model/TreeModel.php
Normal file
2277
libraries/fof40/Model/TreeModel.php
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user