515 lines
14 KiB
PHP
515 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* @package FOF
|
|
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 2, or later
|
|
*/
|
|
|
|
namespace FOF30\Model\DataModel;
|
|
|
|
defined('_JEXEC') || die;
|
|
|
|
use DirectoryIterator;
|
|
use FOF30\Model\DataModel;
|
|
use InvalidArgumentException;
|
|
use JDatabaseQuery;
|
|
|
|
class RelationManager
|
|
{
|
|
/** @var array The known relation types */
|
|
protected static $relationTypes = [];
|
|
/** @var DataModel The data model we are attached to */
|
|
protected $parentModel = null;
|
|
/** @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 = '\\FOF30\\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 = [];
|
|
|
|
if (!empty($this->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))
|
|
{
|
|
foreach ($this->relations as $name => $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 $name => $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 \FOF30\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, \FOF30\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);
|
|
}
|
|
}
|