mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #6939 from open-sausages/pulls/4.0/query-join-alias
API Ensure that all DataQuery joins are aliased based on relationship name
This commit is contained in:
commit
77444ca28d
@ -543,11 +543,13 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
|||||||
$relations = explode('.', $field);
|
$relations = explode('.', $field);
|
||||||
$fieldName = array_pop($relations);
|
$fieldName = array_pop($relations);
|
||||||
|
|
||||||
// Apply
|
// Apply relation
|
||||||
$relationModelName = $query->applyRelation($relations, $linearOnly);
|
$relationModelName = $query->applyRelation($relations, $linearOnly);
|
||||||
|
$relationPrefix = $query->applyRelationPrefix($relations);
|
||||||
|
|
||||||
// Find the db field the relation belongs to
|
// Find the db field the relation belongs to
|
||||||
$columnName = DataObject::getSchema()->sqlColumnForField($relationModelName, $fieldName);
|
$columnName = DataObject::getSchema()
|
||||||
|
->sqlColumnForField($relationModelName, $fieldName, $relationPrefix);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -88,15 +88,16 @@ class DataObjectSchema
|
|||||||
*
|
*
|
||||||
* @param string $class Class name (not a table).
|
* @param string $class Class name (not a table).
|
||||||
* @param string $field Name of field that belongs to this class (or a parent class)
|
* @param string $field Name of field that belongs to this class (or a parent class)
|
||||||
|
* @param string $tablePrefix Optional prefix for table (alias)
|
||||||
* @return string The SQL identifier string for the corresponding column for this field
|
* @return string The SQL identifier string for the corresponding column for this field
|
||||||
*/
|
*/
|
||||||
public function sqlColumnForField($class, $field)
|
public function sqlColumnForField($class, $field, $tablePrefix = null)
|
||||||
{
|
{
|
||||||
$table = $this->tableForField($class, $field);
|
$table = $this->tableForField($class, $field);
|
||||||
if (!$table) {
|
if (!$table) {
|
||||||
throw new InvalidArgumentException("\"{$field}\" is not a field on class \"{$class}\"");
|
throw new InvalidArgumentException("\"{$field}\" is not a field on class \"{$class}\"");
|
||||||
}
|
}
|
||||||
return "\"{$table}\".\"{$field}\"";
|
return "\"{$tablePrefix}{$table}\".\"{$field}\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -756,11 +756,38 @@ class DataQuery
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix of all joined table aliases. E.g. ->filter('Banner.Image.Title)'
|
||||||
|
* Will join the Banner, and then Image relations
|
||||||
|
* `$relationPrefx` will be `banner_image_`
|
||||||
|
* Each table in the Image chain will be suffixed to this prefix. E.g.
|
||||||
|
* `banner_image_File` and `banner_image_Image`
|
||||||
|
*
|
||||||
|
* This will be null if no relation is joined.
|
||||||
|
* E.g. `->filter('Title')`
|
||||||
|
*
|
||||||
|
* @param string|array $relation Relation in '.' delimited string, or array of parts
|
||||||
|
* @return string Table prefix
|
||||||
|
*/
|
||||||
|
public static function applyRelationPrefix($relation)
|
||||||
|
{
|
||||||
|
if (!$relation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (is_string($relation)) {
|
||||||
|
$relation = explode(".", $relation);
|
||||||
|
}
|
||||||
|
return strtolower(implode('_', $relation)) . '_';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traverse the relationship fields, and add the table
|
* Traverse the relationship fields, and add the table
|
||||||
* mappings to the query object state. This has to be called
|
* mappings to the query object state. This has to be called
|
||||||
* in any overloaded {@link SearchFilter->apply()} methods manually.
|
* in any overloaded {@link SearchFilter->apply()} methods manually.
|
||||||
*
|
*
|
||||||
|
* Note, that in order to filter against the joined relation user code must
|
||||||
|
* use {@see tablePrefix()} to get the table alias used for this relation.
|
||||||
|
*
|
||||||
* @param string|array $relation The array/dot-syntax relation to follow
|
* @param string|array $relation The array/dot-syntax relation to follow
|
||||||
* @param bool $linearOnly Set to true to restrict to linear relations only. Set this
|
* @param bool $linearOnly Set to true to restrict to linear relations only. Set this
|
||||||
* if this relation will be used for sorting, and should not include duplicate rows.
|
* if this relation will be used for sorting, and should not include duplicate rows.
|
||||||
@ -780,20 +807,43 @@ class DataQuery
|
|||||||
$modelClass = $this->dataClass;
|
$modelClass = $this->dataClass;
|
||||||
|
|
||||||
$schema = DataObject::getSchema();
|
$schema = DataObject::getSchema();
|
||||||
|
$currentRelation = [];
|
||||||
foreach ($relation as $rel) {
|
foreach ($relation as $rel) {
|
||||||
|
// Get prefix for join for this table (and parent to join on)
|
||||||
|
$parentPrefix = $this->applyRelationPrefix($currentRelation);
|
||||||
|
$currentRelation[] = $rel;
|
||||||
|
$tablePrefix = $this->applyRelationPrefix($currentRelation);
|
||||||
|
|
||||||
|
// Check has_one
|
||||||
if ($component = $schema->hasOneComponent($modelClass, $rel)) {
|
if ($component = $schema->hasOneComponent($modelClass, $rel)) {
|
||||||
// Join via has_one
|
// Join via has_one
|
||||||
$this->joinHasOneRelation($modelClass, $rel, $component);
|
$this->joinHasOneRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix);
|
||||||
$modelClass = $component;
|
$modelClass = $component;
|
||||||
} elseif ($component = $schema->hasManyComponent($modelClass, $rel)) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check has_many
|
||||||
|
if ($component = $schema->hasManyComponent($modelClass, $rel)) {
|
||||||
// Fail on non-linear relations
|
// Fail on non-linear relations
|
||||||
if ($linearOnly) {
|
if ($linearOnly) {
|
||||||
throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass");
|
throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass");
|
||||||
}
|
}
|
||||||
// Join via has_many
|
// Join via has_many
|
||||||
$this->joinHasManyRelation($modelClass, $rel, $component);
|
$this->joinHasManyRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix, 'has_many');
|
||||||
$modelClass = $component;
|
$modelClass = $component;
|
||||||
} elseif ($component = $schema->manyManyComponent($modelClass, $rel)) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check belongs_to (like has_many but linear safe)
|
||||||
|
if ($component = $schema->belongsToComponent($modelClass, $rel)) {
|
||||||
|
// Piggy back off has_many logic
|
||||||
|
$this->joinHasManyRelation($modelClass, $rel, $component, $parentPrefix, $tablePrefix, 'belongs_to');
|
||||||
|
$modelClass = $component;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check many_many
|
||||||
|
if ($component = $schema->manyManyComponent($modelClass, $rel)) {
|
||||||
// Fail on non-linear relations
|
// Fail on non-linear relations
|
||||||
if ($linearOnly) {
|
if ($linearOnly) {
|
||||||
throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass");
|
throw new InvalidArgumentException("$rel is not a linear relation on model $modelClass");
|
||||||
@ -804,31 +854,113 @@ class DataQuery
|
|||||||
$component['childClass'],
|
$component['childClass'],
|
||||||
$component['parentField'],
|
$component['parentField'],
|
||||||
$component['childField'],
|
$component['childField'],
|
||||||
$component['join']
|
$component['join'],
|
||||||
|
$parentPrefix,
|
||||||
|
$tablePrefix
|
||||||
);
|
);
|
||||||
$modelClass = $component['childClass'];
|
$modelClass = $component['childClass'];
|
||||||
} else {
|
continue;
|
||||||
throw new InvalidArgumentException("$rel is not a relation on model $modelClass");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// no relation
|
||||||
|
throw new InvalidArgumentException("$rel is not a relation on model $modelClass");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $modelClass;
|
return $modelClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join the given has_many relation to this query.
|
||||||
|
* Also works with belongs_to
|
||||||
|
*
|
||||||
|
* Doesn't work with polymorphic relationships
|
||||||
|
*
|
||||||
|
* @param string $localClass Name of class that has the has_many to the joined class
|
||||||
|
* @param string $localField Name of the has_many relationship to join
|
||||||
|
* @param string $foreignClass Class to join
|
||||||
|
* @param string $localPrefix Table prefix for parent class
|
||||||
|
* @param string $foreignPrefix Table prefix to use
|
||||||
|
* @param string $type 'has_many' or 'belongs_to'
|
||||||
|
*/
|
||||||
|
protected function joinHasManyRelation(
|
||||||
|
$localClass,
|
||||||
|
$localField,
|
||||||
|
$foreignClass,
|
||||||
|
$localPrefix = null,
|
||||||
|
$foreignPrefix = null,
|
||||||
|
$type = 'has_many'
|
||||||
|
) {
|
||||||
|
if (!$foreignClass || $foreignClass === DataObject::class) {
|
||||||
|
throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}");
|
||||||
|
}
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
|
// Skip if already joined
|
||||||
|
// Note: don't just check base class, since we need to join on the table with the actual relation key
|
||||||
|
$foreignTable = $schema->tableName($foreignClass);
|
||||||
|
$foreignTableAliased = $foreignPrefix . $foreignTable;
|
||||||
|
if ($this->query->isJoinedTo($foreignTableAliased)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join table with associated has_one
|
||||||
|
/** @var DataObject $model */
|
||||||
|
$foreignKey = $schema->getRemoteJoinField($localClass, $localField, $type, $polymorphic);
|
||||||
|
$localIDColumn = $schema->sqlColumnForField($localClass, 'ID', $localPrefix);
|
||||||
|
if ($polymorphic) {
|
||||||
|
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}ID", $foreignPrefix);
|
||||||
|
$foreignKeyClassColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}Class", $foreignPrefix);
|
||||||
|
$localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName', $localPrefix);
|
||||||
|
$joinExpression =
|
||||||
|
"{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}";
|
||||||
|
} else {
|
||||||
|
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey, $foreignPrefix);
|
||||||
|
$joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}";
|
||||||
|
}
|
||||||
|
$this->query->addLeftJoin(
|
||||||
|
$foreignTable,
|
||||||
|
$joinExpression,
|
||||||
|
$foreignTableAliased
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add join clause to the component's ancestry classes so that the search filter could search on
|
||||||
|
// its ancestor fields.
|
||||||
|
$ancestry = ClassInfo::ancestry($foreignClass, true);
|
||||||
|
$ancestry = array_reverse($ancestry);
|
||||||
|
foreach ($ancestry as $ancestor) {
|
||||||
|
$ancestorTable = $schema->tableName($ancestor);
|
||||||
|
if ($ancestorTable !== $foreignTable) {
|
||||||
|
$ancestorTableAliased = $foreignPrefix.$ancestorTable;
|
||||||
|
$this->query->addLeftJoin(
|
||||||
|
$ancestorTable,
|
||||||
|
"\"{$foreignTableAliased}\".\"ID\" = \"{$ancestorTableAliased}\".\"ID\"",
|
||||||
|
$ancestorTableAliased
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join the given class to this query with the given key
|
* Join the given class to this query with the given key
|
||||||
*
|
*
|
||||||
* @param string $localClass Name of class that has the has_one to the joined class
|
* @param string $localClass Name of class that has the has_one to the joined class
|
||||||
* @param string $localField Name of the has_one relationship to joi
|
* @param string $localField Name of the has_one relationship to joi
|
||||||
* @param string $foreignClass Class to join
|
* @param string $foreignClass Class to join
|
||||||
|
* @param string $localPrefix Table prefix to use for local class
|
||||||
|
* @param string $foreignPrefix Table prefix to use for joined table
|
||||||
*/
|
*/
|
||||||
protected function joinHasOneRelation($localClass, $localField, $foreignClass)
|
protected function joinHasOneRelation(
|
||||||
{
|
$localClass,
|
||||||
|
$localField,
|
||||||
|
$foreignClass,
|
||||||
|
$localPrefix = null,
|
||||||
|
$foreignPrefix = null
|
||||||
|
) {
|
||||||
if (!$foreignClass) {
|
if (!$foreignClass) {
|
||||||
throw new InvalidArgumentException("Could not find a has_one relationship {$localField} on {$localClass}");
|
throw new InvalidArgumentException("Could not find a has_one relationship {$localField} on {$localClass}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($foreignClass === 'SilverStripe\ORM\DataObject') {
|
if ($foreignClass === DataObject::class) {
|
||||||
throw new InvalidArgumentException(
|
throw new InvalidArgumentException(
|
||||||
"Could not join polymorphic has_one relationship {$localField} on {$localClass}"
|
"Could not join polymorphic has_one relationship {$localField} on {$localClass}"
|
||||||
);
|
);
|
||||||
@ -838,14 +970,18 @@ class DataQuery
|
|||||||
// Skip if already joined
|
// Skip if already joined
|
||||||
$foreignBaseClass = $schema->baseDataClass($foreignClass);
|
$foreignBaseClass = $schema->baseDataClass($foreignClass);
|
||||||
$foreignBaseTable = $schema->tableName($foreignBaseClass);
|
$foreignBaseTable = $schema->tableName($foreignBaseClass);
|
||||||
if ($this->query->isJoinedTo($foreignBaseTable)) {
|
if ($this->query->isJoinedTo($foreignPrefix.$foreignBaseTable)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join base table
|
// Join base table
|
||||||
$foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID');
|
$foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID', $foreignPrefix);
|
||||||
$localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID");
|
$localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID", $localPrefix);
|
||||||
$this->query->addLeftJoin($foreignBaseTable, "{$foreignIDColumn} = {$localColumn}");
|
$this->query->addLeftJoin(
|
||||||
|
$foreignBaseTable,
|
||||||
|
"{$foreignIDColumn} = {$localColumn}",
|
||||||
|
$foreignPrefix.$foreignBaseTable
|
||||||
|
);
|
||||||
|
|
||||||
// Add join clause to the component's ancestry classes so that the search filter could search on
|
// Add join clause to the component's ancestry classes so that the search filter could search on
|
||||||
// its ancestor fields.
|
// its ancestor fields.
|
||||||
@ -855,63 +991,17 @@ class DataQuery
|
|||||||
foreach ($ancestry as $ancestor) {
|
foreach ($ancestry as $ancestor) {
|
||||||
$ancestorTable = $schema->tableName($ancestor);
|
$ancestorTable = $schema->tableName($ancestor);
|
||||||
if ($ancestorTable !== $foreignBaseTable) {
|
if ($ancestorTable !== $foreignBaseTable) {
|
||||||
$this->query->addLeftJoin($ancestorTable, "{$foreignIDColumn} = \"{$ancestorTable}\".\"ID\"");
|
$ancestorTableAliased = $foreignPrefix.$ancestorTable;
|
||||||
|
$this->query->addLeftJoin(
|
||||||
|
$ancestorTable,
|
||||||
|
"{$foreignIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
|
||||||
|
$ancestorTableAliased
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Join the given has_many relation to this query.
|
|
||||||
*
|
|
||||||
* Doesn't work with polymorphic relationships
|
|
||||||
*
|
|
||||||
* @param string $localClass Name of class that has the has_many to the joined class
|
|
||||||
* @param string $localField Name of the has_many relationship to join
|
|
||||||
* @param string $foreignClass Class to join
|
|
||||||
*/
|
|
||||||
protected function joinHasManyRelation($localClass, $localField, $foreignClass)
|
|
||||||
{
|
|
||||||
if (!$foreignClass || $foreignClass === 'SilverStripe\ORM\DataObject') {
|
|
||||||
throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}");
|
|
||||||
}
|
|
||||||
$schema = DataObject::getSchema();
|
|
||||||
|
|
||||||
// Skip if already joined
|
|
||||||
$foreignTable = $schema->tableName($foreignClass);
|
|
||||||
if ($this->query->isJoinedTo($foreignTable)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join table with associated has_one
|
|
||||||
/** @var DataObject $model */
|
|
||||||
$foreignKey = $schema->getRemoteJoinField($localClass, $localField, 'has_many', $polymorphic);
|
|
||||||
$localIDColumn = $schema->sqlColumnForField($localClass, 'ID');
|
|
||||||
if ($polymorphic) {
|
|
||||||
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}ID");
|
|
||||||
$foreignKeyClassColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}Class");
|
|
||||||
$localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName');
|
|
||||||
$this->query->addLeftJoin(
|
|
||||||
$foreignTable,
|
|
||||||
"{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey);
|
|
||||||
$this->query->addLeftJoin($foreignTable, "{$foreignKeyIDColumn} = {$localIDColumn}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add join clause to the component's ancestry classes so that the search filter could search on
|
|
||||||
// its ancestor fields.
|
|
||||||
$ancestry = ClassInfo::ancestry($foreignClass, true);
|
|
||||||
$ancestry = array_reverse($ancestry);
|
|
||||||
foreach ($ancestry as $ancestor) {
|
|
||||||
$ancestorTable = $schema->tableName($ancestor);
|
|
||||||
if ($ancestorTable !== $foreignTable) {
|
|
||||||
$this->query->addInnerJoin($ancestorTable, "\"{$foreignTable}\".\"ID\" = \"{$ancestorTable}\".\"ID\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join table via many_many relationship
|
* Join table via many_many relationship
|
||||||
*
|
*
|
||||||
@ -921,6 +1011,8 @@ class DataQuery
|
|||||||
* @param string $parentField
|
* @param string $parentField
|
||||||
* @param string $componentField
|
* @param string $componentField
|
||||||
* @param string $relationClassOrTable Name of relation table
|
* @param string $relationClassOrTable Name of relation table
|
||||||
|
* @param string $parentPrefix Table prefix for parent class
|
||||||
|
* @param string $componentPrefix Table prefix to use for both joined and mapping table
|
||||||
*/
|
*/
|
||||||
protected function joinManyManyRelationship(
|
protected function joinManyManyRelationship(
|
||||||
$relationClass,
|
$relationClass,
|
||||||
@ -928,7 +1020,9 @@ class DataQuery
|
|||||||
$componentClass,
|
$componentClass,
|
||||||
$parentField,
|
$parentField,
|
||||||
$componentField,
|
$componentField,
|
||||||
$relationClassOrTable
|
$relationClassOrTable,
|
||||||
|
$parentPrefix = null,
|
||||||
|
$componentPrefix = null
|
||||||
) {
|
) {
|
||||||
$schema = DataObject::getSchema();
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
@ -936,23 +1030,30 @@ class DataQuery
|
|||||||
$relationClassOrTable = $schema->tableName($relationClassOrTable);
|
$relationClassOrTable = $schema->tableName($relationClassOrTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join on parent table
|
// Check if already joined to component alias (skip join table for the check)
|
||||||
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID');
|
$componentBaseClass = $schema->baseDataClass($componentClass);
|
||||||
|
$componentBaseTable = $schema->tableName($componentBaseClass);
|
||||||
|
$componentAliasedTable = $componentPrefix . $componentBaseTable;
|
||||||
|
if ($this->query->isJoinedTo($componentAliasedTable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join parent class to join table
|
||||||
|
$relationAliasedTable = $componentPrefix.$relationClassOrTable;
|
||||||
|
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix);
|
||||||
$this->query->addLeftJoin(
|
$this->query->addLeftJoin(
|
||||||
$relationClassOrTable,
|
$relationClassOrTable,
|
||||||
"\"$relationClassOrTable\".\"$parentField\" = {$parentIDColumn}"
|
"\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}",
|
||||||
|
$relationAliasedTable
|
||||||
);
|
);
|
||||||
|
|
||||||
// Join on base table of component class
|
// Join on base table of component class
|
||||||
$componentBaseClass = $schema->baseDataClass($componentClass);
|
$componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix);
|
||||||
$componentBaseTable = $schema->tableName($componentBaseClass);
|
|
||||||
$componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID');
|
|
||||||
if (!$this->query->isJoinedTo($componentBaseTable)) {
|
|
||||||
$this->query->addLeftJoin(
|
$this->query->addLeftJoin(
|
||||||
$componentBaseTable,
|
$componentBaseTable,
|
||||||
"\"$relationClassOrTable\".\"$componentField\" = {$componentIDColumn}"
|
"\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}",
|
||||||
|
$componentAliasedTable
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Add join clause to the component's ancestry classes so that the search filter could search on
|
// Add join clause to the component's ancestry classes so that the search filter could search on
|
||||||
// its ancestor fields.
|
// its ancestor fields.
|
||||||
@ -960,8 +1061,13 @@ class DataQuery
|
|||||||
$ancestry = array_reverse($ancestry);
|
$ancestry = array_reverse($ancestry);
|
||||||
foreach ($ancestry as $ancestor) {
|
foreach ($ancestry as $ancestor) {
|
||||||
$ancestorTable = $schema->tableName($ancestor);
|
$ancestorTable = $schema->tableName($ancestor);
|
||||||
if ($ancestorTable != $componentBaseTable && !$this->query->isJoinedTo($ancestorTable)) {
|
if ($ancestorTable !== $componentBaseTable) {
|
||||||
$this->query->addLeftJoin($ancestorTable, "{$componentIDColumn} = \"{$ancestorTable}\".\"ID\"");
|
$ancestorTableAliased = $componentPrefix.$ancestorTable;
|
||||||
|
$this->query->addLeftJoin(
|
||||||
|
$ancestorTable,
|
||||||
|
"{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
|
||||||
|
$ancestorTableAliased
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1060,7 +1166,6 @@ class DataQuery
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* An arbitrary store of query parameters that can be used by decorators.
|
* An arbitrary store of query parameters that can be used by decorators.
|
||||||
* @todo This will probably be made obsolete if we have subclasses of DataList and/or DataQuery.
|
|
||||||
*/
|
*/
|
||||||
private $queryParams;
|
private $queryParams;
|
||||||
|
|
||||||
|
@ -91,9 +91,11 @@ class FulltextFilter extends SearchFilter
|
|||||||
*/
|
*/
|
||||||
protected function prepareColumns($columns)
|
protected function prepareColumns($columns)
|
||||||
{
|
{
|
||||||
|
$prefix = DataQuery::applyRelationPrefix($this->relation);
|
||||||
$table = DataObject::getSchema()->tableForField($this->model, current($columns));
|
$table = DataObject::getSchema()->tableForField($this->model, current($columns));
|
||||||
$columns = array_map(function ($column) use ($table) {
|
$fullTable = $prefix . $table;
|
||||||
return Convert::symbol2sql("$table.$column");
|
$columns = array_map(function ($col) use ($fullTable) {
|
||||||
|
return "\"{$fullTable}\".\"{$col}\"";
|
||||||
}, $columns);
|
}, $columns);
|
||||||
return implode(',', $columns);
|
return implode(',', $columns);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\Filters;
|
namespace SilverStripe\ORM\Filters;
|
||||||
|
|
||||||
|
use SilverStripe\Core\ClassInfo;
|
||||||
use SilverStripe\Core\Injector\Injectable;
|
use SilverStripe\Core\Injector\Injectable;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
use SilverStripe\ORM\DataQuery;
|
use SilverStripe\ORM\DataQuery;
|
||||||
@ -28,7 +29,11 @@ abstract class SearchFilter
|
|||||||
use Injectable;
|
use Injectable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Classname of the inspected {@link DataObject}
|
* Classname of the inspected {@link DataObject}.
|
||||||
|
* If pointing to a relation, this will be the classname of the leaf
|
||||||
|
* class in the relation
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $model;
|
protected $model;
|
||||||
|
|
||||||
@ -53,11 +58,13 @@ abstract class SearchFilter
|
|||||||
protected $modifiers;
|
protected $modifiers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Name of a has-one, has-many or many-many relation (not the classname).
|
* @var array Parts of a has-one, has-many or many-many relation (not the classname).
|
||||||
* Set in the constructor as part of the name in dot-notation, and used in
|
* Set in the constructor as part of the name in dot-notation, and used in
|
||||||
* {@link applyRelation()}.
|
* {@link applyRelation()}.
|
||||||
|
*
|
||||||
|
* Also used to build table prefix (see getRelationTablePrefix)
|
||||||
*/
|
*/
|
||||||
protected $relation;
|
protected $relation = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of data about an aggregate column being used
|
* An array of data about an aggregate column being used
|
||||||
@ -137,11 +144,11 @@ abstract class SearchFilter
|
|||||||
* Set the root model class to be selected by this
|
* Set the root model class to be selected by this
|
||||||
* search query.
|
* search query.
|
||||||
*
|
*
|
||||||
* @param string $className
|
* @param string|DataObject $className
|
||||||
*/
|
*/
|
||||||
public function setModel($className)
|
public function setModel($className)
|
||||||
{
|
{
|
||||||
$this->model = $className;
|
$this->model = ClassInfo::class_name($className);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,6 +268,7 @@ abstract class SearchFilter
|
|||||||
"Model supplied to " . static::class . " should be an instance of DataObject."
|
"Model supplied to " . static::class . " should be an instance of DataObject."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
$tablePrefix = DataQuery::applyRelationPrefix($this->relation);
|
||||||
$schema = DataObject::getSchema();
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
if ($this->aggregate) {
|
if ($this->aggregate) {
|
||||||
@ -280,24 +288,25 @@ abstract class SearchFilter
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'%s("%s".%s)',
|
'%s("%s%s".%s)',
|
||||||
$function,
|
$function,
|
||||||
|
$tablePrefix,
|
||||||
$table,
|
$table,
|
||||||
$column ? "\"$column\"" : '"ID"'
|
$column ? "\"$column\"" : '"ID"'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Find table this field belongs to
|
// Check if this column is a table on the current model
|
||||||
$table = $schema->tableForField($this->model, $this->name);
|
$table = $schema->tableForField($this->model, $this->name);
|
||||||
if (!$table) {
|
if ($table) {
|
||||||
// fallback to the provided name in the event of a joined column
|
return $schema->sqlColumnForField($this->model, $this->name, $tablePrefix);
|
||||||
// name (as the candidate class doesn't check joined records)
|
|
||||||
$parts = explode('.', $this->fullName);
|
|
||||||
return '"' . implode('"."', $parts) . '"';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf('"%s"."%s"', $table, $this->name);
|
// fallback to the provided name in the event of a joined column
|
||||||
|
// name (as the candidate class doesn't check joined records)
|
||||||
|
$parts = explode('.', $this->fullName);
|
||||||
|
return '"' . implode('"."', $parts) . '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -136,12 +136,12 @@ class GridFieldSortableHeaderTest extends SapphireTest
|
|||||||
$config = new GridFieldConfig_RecordEditor();
|
$config = new GridFieldConfig_RecordEditor();
|
||||||
$gridField = new GridField('testfield', 'testfield', $list, $config);
|
$gridField = new GridField('testfield', 'testfield', $list, $config);
|
||||||
$state = $gridField->State->GridFieldSortableHeader;
|
$state = $gridField->State->GridFieldSortableHeader;
|
||||||
$compontent = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class);
|
$component = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class);
|
||||||
|
|
||||||
// Test that inherited dataobjects will work correctly
|
// Test that inherited dataobjects will work correctly
|
||||||
$state->SortColumn = 'Cheerleader.Hat.Colour';
|
$state->SortColumn = 'Cheerleader.Hat.Colour';
|
||||||
$state->SortDirection = 'asc';
|
$state->SortDirection = 'asc';
|
||||||
$relationListA = $compontent->getManipulatedData($gridField, $list);
|
$relationListA = $component->getManipulatedData($gridField, $list);
|
||||||
$relationListAsql = Convert::nl2os($relationListA->sql(), ' ');
|
$relationListAsql = Convert::nl2os($relationListA->sql(), ' ');
|
||||||
|
|
||||||
// Assert that all tables are joined properly
|
// Assert that all tables are joined properly
|
||||||
@ -153,12 +153,16 @@ class GridFieldSortableHeaderTest extends SapphireTest
|
|||||||
);
|
);
|
||||||
$this->assertContains(
|
$this->assertContains(
|
||||||
'LEFT JOIN "GridFieldSortableHeaderTest_Cheerleader" '
|
'LEFT JOIN "GridFieldSortableHeaderTest_Cheerleader" '
|
||||||
. 'ON "GridFieldSortableHeaderTest_Cheerleader"."ID" = "GridFieldSortableHeaderTest_Team"."CheerleaderID"',
|
. 'AS "cheerleader_GridFieldSortableHeaderTest_Cheerleader" '
|
||||||
|
. 'ON "cheerleader_GridFieldSortableHeaderTest_Cheerleader"."ID" = '
|
||||||
|
. '"GridFieldSortableHeaderTest_Team"."CheerleaderID"',
|
||||||
$relationListAsql
|
$relationListAsql
|
||||||
);
|
);
|
||||||
$this->assertContains(
|
$this->assertContains(
|
||||||
'LEFT JOIN "GridFieldSortableHeaderTest_CheerleaderHat" '
|
'LEFT JOIN "GridFieldSortableHeaderTest_CheerleaderHat" '
|
||||||
. 'ON "GridFieldSortableHeaderTest_CheerleaderHat"."ID" = "GridFieldSortableHeaderTest_Cheerleader"."HatID"',
|
. 'AS "cheerleader_hat_GridFieldSortableHeaderTest_CheerleaderHat" '
|
||||||
|
. 'ON "cheerleader_hat_GridFieldSortableHeaderTest_CheerleaderHat"."ID" = '
|
||||||
|
. '"cheerleader_GridFieldSortableHeaderTest_Cheerleader"."HatID"',
|
||||||
$relationListAsql
|
$relationListAsql
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -168,7 +172,7 @@ class GridFieldSortableHeaderTest extends SapphireTest
|
|||||||
$relationListA->column('City')
|
$relationListA->column('City')
|
||||||
);
|
);
|
||||||
$state->SortDirection = 'desc';
|
$state->SortDirection = 'desc';
|
||||||
$relationListAdesc = $compontent->getManipulatedData($gridField, $list);
|
$relationListAdesc = $component->getManipulatedData($gridField, $list);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
array('Melbourne', 'Wellington', 'Auckland', 'Cologne'),
|
array('Melbourne', 'Wellington', 'Auckland', 'Cologne'),
|
||||||
$relationListAdesc->column('City')
|
$relationListAdesc->column('City')
|
||||||
@ -177,7 +181,7 @@ class GridFieldSortableHeaderTest extends SapphireTest
|
|||||||
// Test subclasses of tables
|
// Test subclasses of tables
|
||||||
$state->SortColumn = 'CheerleadersMom.Hat.Colour';
|
$state->SortColumn = 'CheerleadersMom.Hat.Colour';
|
||||||
$state->SortDirection = 'asc';
|
$state->SortDirection = 'asc';
|
||||||
$relationListB = $compontent->getManipulatedData($gridField, $list);
|
$relationListB = $component->getManipulatedData($gridField, $list);
|
||||||
$relationListBsql = $relationListB->sql();
|
$relationListBsql = $relationListB->sql();
|
||||||
|
|
||||||
// Assert that subclasses are included in the query
|
// Assert that subclasses are included in the query
|
||||||
@ -188,20 +192,27 @@ class GridFieldSortableHeaderTest extends SapphireTest
|
|||||||
$relationListBsql
|
$relationListBsql
|
||||||
);
|
);
|
||||||
// Joined tables are joined basetable first
|
// Joined tables are joined basetable first
|
||||||
|
// Note: CheerLeader is base of Mom table, hence the alias
|
||||||
$this->assertContains(
|
$this->assertContains(
|
||||||
'LEFT JOIN "GridFieldSortableHeaderTest_Cheerleader" '
|
'LEFT JOIN "GridFieldSortableHeaderTest_Cheerleader" '
|
||||||
. 'ON "GridFieldSortableHeaderTest_Cheerleader"."ID" = "GridFieldSortableHeaderTest_Team"."CheerleadersMomID"',
|
. 'AS "cheerleadersmom_GridFieldSortableHeaderTest_Cheerleader" '
|
||||||
|
. 'ON "cheerleadersmom_GridFieldSortableHeaderTest_Cheerleader"."ID" = '
|
||||||
|
. '"GridFieldSortableHeaderTest_Team"."CheerleadersMomID"',
|
||||||
$relationListBsql
|
$relationListBsql
|
||||||
);
|
);
|
||||||
// Then the basetable of the joined record is joined to the specific subtable
|
// Then the basetable of the joined record is joined to the specific subtable
|
||||||
$this->assertContains(
|
$this->assertContains(
|
||||||
'LEFT JOIN "GridFieldSortableHeaderTest_Mom" '
|
'LEFT JOIN "GridFieldSortableHeaderTest_Mom" '
|
||||||
. 'ON "GridFieldSortableHeaderTest_Cheerleader"."ID" = "GridFieldSortableHeaderTest_Mom"."ID"',
|
. 'AS "cheerleadersmom_GridFieldSortableHeaderTest_Mom" '
|
||||||
|
. 'ON "cheerleadersmom_GridFieldSortableHeaderTest_Cheerleader"."ID" = '
|
||||||
|
. '"cheerleadersmom_GridFieldSortableHeaderTest_Mom"."ID"',
|
||||||
$relationListBsql
|
$relationListBsql
|
||||||
);
|
);
|
||||||
$this->assertContains(
|
$this->assertContains(
|
||||||
'LEFT JOIN "GridFieldSortableHeaderTest_CheerleaderHat" '
|
'LEFT JOIN "GridFieldSortableHeaderTest_CheerleaderHat" '
|
||||||
. 'ON "GridFieldSortableHeaderTest_CheerleaderHat"."ID" = "GridFieldSortableHeaderTest_Cheerleader"."HatID"',
|
. 'AS "cheerleadersmom_hat_GridFieldSortableHeaderTest_CheerleaderHat" '
|
||||||
|
. 'ON "cheerleadersmom_hat_GridFieldSortableHeaderTest_CheerleaderHat"."ID" = '
|
||||||
|
. '"cheerleadersmom_GridFieldSortableHeaderTest_Cheerleader"."HatID"',
|
||||||
$relationListBsql
|
$relationListBsql
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -212,7 +223,7 @@ class GridFieldSortableHeaderTest extends SapphireTest
|
|||||||
$relationListB->column('City')
|
$relationListB->column('City')
|
||||||
);
|
);
|
||||||
$state->SortDirection = 'desc';
|
$state->SortDirection = 'desc';
|
||||||
$relationListBdesc = $compontent->getManipulatedData($gridField, $list);
|
$relationListBdesc = $component->getManipulatedData($gridField, $list);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
array('Melbourne', 'Wellington', 'Auckland', 'Cologne'),
|
array('Melbourne', 'Wellington', 'Auckland', 'Cologne'),
|
||||||
$relationListBdesc->column('City')
|
$relationListBdesc->column('City')
|
||||||
|
@ -5,10 +5,12 @@ namespace SilverStripe\ORM\Tests;
|
|||||||
use SilverStripe\Core\Convert;
|
use SilverStripe\Core\Convert;
|
||||||
use SilverStripe\Core\Injector\InjectorNotFoundException;
|
use SilverStripe\Core\Injector\InjectorNotFoundException;
|
||||||
use SilverStripe\ORM\DataList;
|
use SilverStripe\ORM\DataList;
|
||||||
|
use SilverStripe\ORM\DataQuery;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
use SilverStripe\ORM\Filterable;
|
use SilverStripe\ORM\Filterable;
|
||||||
use SilverStripe\ORM\Filters\ExactMatchFilter;
|
use SilverStripe\ORM\Filters\ExactMatchFilter;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\ORM\Tests\DataObjectTest\Bracket;
|
||||||
use SilverStripe\ORM\Tests\DataObjectTest\EquipmentCompany;
|
use SilverStripe\ORM\Tests\DataObjectTest\EquipmentCompany;
|
||||||
use SilverStripe\ORM\Tests\DataObjectTest\Fan;
|
use SilverStripe\ORM\Tests\DataObjectTest\Fan;
|
||||||
use SilverStripe\ORM\Tests\DataObjectTest\Player;
|
use SilverStripe\ORM\Tests\DataObjectTest\Player;
|
||||||
@ -851,6 +853,69 @@ class DataListTest extends SapphireTest
|
|||||||
$this->assertEquals(2, $gtList->count());
|
$this->assertEquals(2, $gtList->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a filter correctly aliases relationships that share common classes
|
||||||
|
*/
|
||||||
|
public function testFilterSharedRelationalClasses()
|
||||||
|
{
|
||||||
|
/** @var Bracket $final1 */
|
||||||
|
$final1 = $this->objFromFixture(Bracket::class, 'final');
|
||||||
|
$prefinal1 = $this->objFromFixture(Bracket::class, 'prefinal1');
|
||||||
|
$prefinal2 = $this->objFromFixture(Bracket::class, 'prefinal2');
|
||||||
|
$semifinal1 = $this->objFromFixture(Bracket::class, 'semifinal1');
|
||||||
|
$team2 = $this->objFromFixture(Team::class, 'team2');
|
||||||
|
|
||||||
|
// grand child can be found from parent
|
||||||
|
$found = Bracket::get()->filter('Next.Next.Title', $final1->Title);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[['Title' => $semifinal1->Title]],
|
||||||
|
$found
|
||||||
|
);
|
||||||
|
|
||||||
|
// grand child can be found from child
|
||||||
|
$found = Bracket::get()->filter('Next.Title', $prefinal1->Title);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[['Title' => $semifinal1->Title]],
|
||||||
|
$found
|
||||||
|
);
|
||||||
|
|
||||||
|
// child can be found from parent
|
||||||
|
$found = Bracket::get()->filter('Next.Title', $final1->Title);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => $prefinal1->Title],
|
||||||
|
['Title' => $prefinal2->Title]
|
||||||
|
],
|
||||||
|
$found
|
||||||
|
);
|
||||||
|
|
||||||
|
// Complex filter, get brackets where the following bracket was won by team 1
|
||||||
|
// Note: Includes results from multiple levels
|
||||||
|
$found = Bracket::get()->filter('Next.Winner.Title', $team2->Title);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => $prefinal1->Title],
|
||||||
|
['Title' => $prefinal2->Title],
|
||||||
|
['Title' => $semifinal1->Title]
|
||||||
|
],
|
||||||
|
$found
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterOnImplicitJoinWithSharedInheritance()
|
||||||
|
{
|
||||||
|
$list = DataObjectTest\RelationChildFirst::get()->filter(array(
|
||||||
|
'ManyNext.ID' => array(
|
||||||
|
$this->idFromFixture(DataObjectTest\RelationChildSecond::class, 'test1'),
|
||||||
|
$this->idFromFixture(DataObjectTest\RelationChildSecond::class, 'test2'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
$this->assertEquals(2, $list->count());
|
||||||
|
$ids = $list->column('ID');
|
||||||
|
$this->assertContains($this->idFromFixture(DataObjectTest\RelationChildFirst::class, 'test1'), $ids);
|
||||||
|
$this->assertContains($this->idFromFixture(DataObjectTest\RelationChildFirst::class, 'test2'), $ids);
|
||||||
|
}
|
||||||
|
|
||||||
public function testFilterAny()
|
public function testFilterAny()
|
||||||
{
|
{
|
||||||
$list = TeamComment::get();
|
$list = TeamComment::get();
|
||||||
@ -1229,13 +1294,13 @@ class DataListTest extends SapphireTest
|
|||||||
$filter = new ExactMatchFilter(
|
$filter = new ExactMatchFilter(
|
||||||
'Comments.Count()'
|
'Comments.Count()'
|
||||||
);
|
);
|
||||||
$filter->setModel(new DataObjectTest\Team());
|
$filter->apply(new DataQuery(DataObjectTest\Team::class));
|
||||||
$this->assertEquals('COUNT("DataObjectTest_Team"."ID")', $filter->getDBName());
|
$this->assertEquals('COUNT("comments_DataObjectTest_TeamComment"."ID")', $filter->getDBName());
|
||||||
|
|
||||||
foreach (['Comments.Max(ID)', 'Comments.Max( ID )', 'Comments.Max( ID)'] as $name) {
|
foreach (['Comments.Max(ID)', 'Comments.Max( ID )', 'Comments.Max( ID)'] as $name) {
|
||||||
$filter = new ExactMatchFilter($name);
|
$filter = new ExactMatchFilter($name);
|
||||||
$filter->setModel(new DataObjectTest\Team());
|
$filter->apply(new DataQuery(DataObjectTest\Team::class));
|
||||||
$this->assertEquals('MAX("DataObjectTest_Team"."ID")', $filter->getDBName());
|
$this->assertEquals('MAX("comments_DataObjectTest_TeamComment"."ID")', $filter->getDBName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ use SilverStripe\ORM\ManyManyList;
|
|||||||
*/
|
*/
|
||||||
class Class4 extends DataObject implements TestOnly
|
class Class4 extends DataObject implements TestOnly
|
||||||
{
|
{
|
||||||
|
private static $table_name = 'DataObjectDuplicateTest_Class4';
|
||||||
|
|
||||||
private static $db = [
|
private static $db = [
|
||||||
'Title' => 'Varchar',
|
'Title' => 'Varchar',
|
||||||
];
|
];
|
||||||
|
@ -51,6 +51,10 @@ class DataObjectTest extends SapphireTest
|
|||||||
DataObjectTest\Ploy::class,
|
DataObjectTest\Ploy::class,
|
||||||
DataObjectTest\Bogey::class,
|
DataObjectTest\Bogey::class,
|
||||||
DataObjectTest\Sortable::class,
|
DataObjectTest\Sortable::class,
|
||||||
|
DataObjectTest\Bracket::class,
|
||||||
|
DataObjectTest\RelationParent::class,
|
||||||
|
DataObjectTest\RelationChildFirst::class,
|
||||||
|
DataObjectTest\RelationChildSecond::class,
|
||||||
);
|
);
|
||||||
|
|
||||||
protected static function getExtraDataObjects()
|
protected static function getExtraDataObjects()
|
||||||
|
@ -145,3 +145,37 @@ SilverStripe\ORM\Tests\DataObjectTest\Company:
|
|||||||
company1:
|
company1:
|
||||||
Name: 'Team co.'
|
Name: 'Team co.'
|
||||||
Owner: =>SilverStripe\ORM\Tests\DataObjectTest\Player.player2
|
Owner: =>SilverStripe\ORM\Tests\DataObjectTest\Player.player2
|
||||||
|
SilverStripe\ORM\Tests\DataObjectTest\Bracket:
|
||||||
|
final:
|
||||||
|
Title: 'Final'
|
||||||
|
Winner: =>SilverStripe\ORM\Tests\DataObjectTest\Team.team2
|
||||||
|
prefinal1:
|
||||||
|
Title: 'Prelim final 1'
|
||||||
|
Next: =>SilverStripe\ORM\Tests\DataObjectTest\Bracket.final
|
||||||
|
Winner: =>SilverStripe\ORM\Tests\DataObjectTest\Team.team2
|
||||||
|
prefinal2:
|
||||||
|
Title: 'Prelim final 2'
|
||||||
|
Next: =>SilverStripe\ORM\Tests\DataObjectTest\Bracket.final
|
||||||
|
Winner: =>SilverStripe\ORM\Tests\DataObjectTest\Team.team1
|
||||||
|
semifinal1:
|
||||||
|
Title: 'Semi final 1'
|
||||||
|
Next: =>SilverStripe\ORM\Tests\DataObjectTest\Bracket.prefinal1
|
||||||
|
Winner: =>SilverStripe\ORM\Tests\DataObjectTest\Team.team2
|
||||||
|
SilverStripe\ORM\Tests\DataObjectTest\RelationChildSecond:
|
||||||
|
test1:
|
||||||
|
Title: 'Test 1'
|
||||||
|
test2:
|
||||||
|
Title: 'Test 2'
|
||||||
|
test3:
|
||||||
|
Title: 'Test 3'
|
||||||
|
SilverStripe\ORM\Tests\DataObjectTest\RelationChildFirst:
|
||||||
|
test1:
|
||||||
|
Title: 'Test1'
|
||||||
|
ManyNext:
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\RelationChildSecond.test1
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\RelationChildSecond.test2
|
||||||
|
test2:
|
||||||
|
Title: 'Test2'
|
||||||
|
ManyNext:
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\RelationChildSecond.test1
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\RelationChildSecond.test3
|
||||||
|
32
tests/php/ORM/DataObjectTest/Bracket.php
Normal file
32
tests/php/ORM/DataObjectTest/Bracket.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DataObjectTest;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $Title
|
||||||
|
* @method Bracket Parent()
|
||||||
|
* @method Team First()
|
||||||
|
* @method Team Second()
|
||||||
|
* @method Team Winner()
|
||||||
|
*/
|
||||||
|
class Bracket extends DataObject
|
||||||
|
{
|
||||||
|
private static $table_name = 'DataObjectTest_Bracket';
|
||||||
|
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar(100)',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'Next' => Bracket::class,
|
||||||
|
'First' => Team::class,
|
||||||
|
'Second' => Team::class,
|
||||||
|
'Winner' => Team::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $has_many = [
|
||||||
|
'Previous' => Bracket::class,
|
||||||
|
];
|
||||||
|
}
|
@ -7,8 +7,8 @@ class CEO extends Staff
|
|||||||
private static $table_name = 'DataObjectTest_CEO';
|
private static $table_name = 'DataObjectTest_CEO';
|
||||||
|
|
||||||
private static $belongs_to = array(
|
private static $belongs_to = array(
|
||||||
'Company' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Company.CEO',
|
'Company' => Company::class.'.CEO',
|
||||||
'PreviousCompany' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Company.PreviousCEO',
|
'PreviousCompany' => Company::class.'.PreviousCEO',
|
||||||
'CompanyOwned' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Company.Owner'
|
'CompanyOwned' => Company::class.'.Owner'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class Company extends DataObject implements TestOnly
|
|||||||
];
|
];
|
||||||
|
|
||||||
private static $has_many = array(
|
private static $has_many = array(
|
||||||
'CurrentStaff' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Staff.CurrentCompany',
|
'CurrentStaff' => Staff::class.'.CurrentCompany',
|
||||||
'PreviousStaff' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Staff.PreviousCompany'
|
'PreviousStaff' => Staff::class.'.PreviousCompany'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,13 @@ class Player extends Member implements TestOnly
|
|||||||
);
|
);
|
||||||
|
|
||||||
private static $has_many = array(
|
private static $has_many = array(
|
||||||
'Fans' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Fan.Favourite', // Polymorphic - Player fans
|
'Fans' => Fan::class.'.Favourite', // Polymorphic - Player fans
|
||||||
'CaptainTeams' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Team.Captain',
|
'CaptainTeams' => Team::class.'.Captain',
|
||||||
'FoundingTeams' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Team.Founder'
|
'FoundingTeams' => Team::class.'.Founder'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $belongs_to = array(
|
private static $belongs_to = array(
|
||||||
'CompanyOwned' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Company.Owner'
|
'CompanyOwned' => Company::class.'.Owner'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $searchable_fields = array(
|
private static $searchable_fields = array(
|
||||||
|
12
tests/php/ORM/DataObjectTest/RelationChildFirst.php
Normal file
12
tests/php/ORM/DataObjectTest/RelationChildFirst.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DataObjectTest;
|
||||||
|
|
||||||
|
class RelationChildFirst extends RelationParent
|
||||||
|
{
|
||||||
|
private static $table_name = 'DataObjectTest_RelationChildFirst';
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
'ManyNext' => RelationChildSecond::class,
|
||||||
|
];
|
||||||
|
}
|
12
tests/php/ORM/DataObjectTest/RelationChildSecond.php
Normal file
12
tests/php/ORM/DataObjectTest/RelationChildSecond.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DataObjectTest;
|
||||||
|
|
||||||
|
class RelationChildSecond extends RelationParent
|
||||||
|
{
|
||||||
|
private static $table_name = 'DataObjectTest_RelationChildSecond';
|
||||||
|
|
||||||
|
private static $belongs_many_many = [
|
||||||
|
'ManyPrev' => RelationChildFirst::class,
|
||||||
|
];
|
||||||
|
}
|
15
tests/php/ORM/DataObjectTest/RelationParent.php
Normal file
15
tests/php/ORM/DataObjectTest/RelationParent.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DataObjectTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class RelationParent extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $table_name = 'DataObjectTest_RelationParent';
|
||||||
|
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar(255)',
|
||||||
|
];
|
||||||
|
}
|
@ -39,8 +39,8 @@ class Team extends DataObject implements TestOnly
|
|||||||
private static $has_many = array(
|
private static $has_many = array(
|
||||||
'SubTeams' => SubTeam::class,
|
'SubTeams' => SubTeam::class,
|
||||||
'Comments' => TeamComment::class,
|
'Comments' => TeamComment::class,
|
||||||
'Fans' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Fan.Favourite', // Polymorphic - Team fans
|
'Fans' => Fan::class.'.Favourite', // Polymorphic - Team fans
|
||||||
'PlayerFans' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\Player.FavouriteTeam'
|
'PlayerFans' => Player::class.'.FavouriteTeam'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $many_many = array(
|
private static $many_many = array(
|
||||||
@ -54,8 +54,8 @@ class Team extends DataObject implements TestOnly
|
|||||||
);
|
);
|
||||||
|
|
||||||
private static $belongs_many_many = array(
|
private static $belongs_many_many = array(
|
||||||
'Sponsors' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\EquipmentCompany.SponsoredTeams',
|
'Sponsors' => EquipmentCompany::class.'.SponsoredTeams',
|
||||||
'EquipmentSuppliers' => 'SilverStripe\\ORM\\Tests\\DataObjectTest\\EquipmentCompany.EquipmentCustomers'
|
'EquipmentSuppliers' => EquipmentCompany::class.'.EquipmentCustomers'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $summary_fields = array(
|
private static $summary_fields = array(
|
||||||
|
@ -75,13 +75,13 @@ class DataQueryTest extends SapphireTest
|
|||||||
// Test applyRelation with two has_ones pointing to the same class
|
// Test applyRelation with two has_ones pointing to the same class
|
||||||
$dq = new DataQuery(DataQueryTest\ObjectB::class);
|
$dq = new DataQuery(DataQueryTest\ObjectB::class);
|
||||||
$dq->applyRelation('TestC');
|
$dq->applyRelation('TestC');
|
||||||
$this->assertTrue($dq->query()->isJoinedTo('DataQueryTest_C'));
|
$this->assertTrue($dq->query()->isJoinedTo('testc_DataQueryTest_C'));
|
||||||
$this->assertContains('"DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCID"', $dq->sql());
|
$this->assertContains('"testc_DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCID"', $dq->sql());
|
||||||
|
|
||||||
$dq = new DataQuery(DataQueryTest\ObjectB::class);
|
$dq = new DataQuery(DataQueryTest\ObjectB::class);
|
||||||
$dq->applyRelation('TestCTwo');
|
$dq->applyRelation('TestCTwo');
|
||||||
$this->assertTrue($dq->query()->isJoinedTo('DataQueryTest_C'));
|
$this->assertTrue($dq->query()->isJoinedTo('testctwo_DataQueryTest_C'));
|
||||||
$this->assertContains('"DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCTwoID"', $dq->sql());
|
$this->assertContains('"testctwo_DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCTwoID"', $dq->sql());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testApplyReplationDeepInheretence()
|
public function testApplyReplationDeepInheretence()
|
||||||
@ -91,7 +91,7 @@ class DataQueryTest extends SapphireTest
|
|||||||
//apply a relation to a relation from an ancestor class
|
//apply a relation to a relation from an ancestor class
|
||||||
$newDQ->applyRelation('TestA');
|
$newDQ->applyRelation('TestA');
|
||||||
$this->assertTrue($newDQ->query()->isJoinedTo('DataQueryTest_C'));
|
$this->assertTrue($newDQ->query()->isJoinedTo('DataQueryTest_C'));
|
||||||
$this->assertContains('"DataQueryTest_A"."ID" = "DataQueryTest_C"."TestAID"', $newDQ->sql($params));
|
$this->assertContains('"testa_DataQueryTest_A"."ID" = "DataQueryTest_C"."TestAID"', $newDQ->sql($params));
|
||||||
|
|
||||||
//test many_many relation
|
//test many_many relation
|
||||||
|
|
||||||
|
@ -346,17 +346,11 @@ class ManyManyListTest extends SapphireTest
|
|||||||
|
|
||||||
public function testFilteringOnPreviouslyJoinedTable()
|
public function testFilteringOnPreviouslyJoinedTable()
|
||||||
{
|
{
|
||||||
|
/** @var ManyManyListTest\Category $category */
|
||||||
/**
|
|
||||||
* @var ManyManyListTest\Category $category
|
|
||||||
*/
|
|
||||||
$category = $this->objFromFixture(ManyManyListTest\Category::class, 'categorya');
|
$category = $this->objFromFixture(ManyManyListTest\Category::class, 'categorya');
|
||||||
|
|
||||||
/**
|
/** @var ManyManyList $productsRelatedToProductB */
|
||||||
* @var ManyManyList $productsRelatedToProductB
|
$productsRelatedToProductB = $category->Products()->filter('RelatedProducts.Title', 'Product A');
|
||||||
*/
|
|
||||||
$productsRelatedToProductB = $category->Products()->filter('RelatedProducts.Title', 'Product B');
|
|
||||||
|
|
||||||
$this->assertEquals(1, $productsRelatedToProductB->count());
|
$this->assertEquals(1, $productsRelatedToProductB->count());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,11 @@ namespace SilverStripe\ORM\Tests\ManyManyListTest;
|
|||||||
|
|
||||||
use SilverStripe\Dev\TestOnly;
|
use SilverStripe\Dev\TestOnly;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\ManyManyList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method ManyManyList Products()
|
||||||
|
*/
|
||||||
class Category extends DataObject implements TestOnly
|
class Category extends DataObject implements TestOnly
|
||||||
{
|
{
|
||||||
private static $table_name = 'ManyManyListTest_Category';
|
private static $table_name = 'ManyManyListTest_Category';
|
||||||
|
Loading…
Reference in New Issue
Block a user