diff --git a/src/ORM/DataList.php b/src/ORM/DataList.php
index d5c9df54a..ee76453a5 100644
--- a/src/ORM/DataList.php
+++ b/src/ORM/DataList.php
@@ -501,6 +501,16 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
* Unlike getRelationName, this is immutable and will fallback to the quoted field
* name if not a relation.
*
+ * Example use (simple WHERE condition on data sitting in a related table):
+ *
+ *
+ * $columnName = null;
+ * $list = Page::get()
+ * ->applyRelation('TaxonomyTerms.ID', $columnName)
+ * ->where([$columnName => 'my value']);
+ *
+ *
+ *
* @param string $field Name of field or relation to apply
* @param string &$columnName Quoted column name
* @param bool $linearOnly Set to true to restrict to linear relations only. Set this
diff --git a/src/ORM/DataQuery.php b/src/ORM/DataQuery.php
index 6ca70c13e..64ead5a87 100644
--- a/src/ORM/DataQuery.php
+++ b/src/ORM/DataQuery.php
@@ -4,6 +4,7 @@ namespace SilverStripe\ORM;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
+use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\Connect\Query;
use SilverStripe\ORM\Queries\SQLConditionGroup;
@@ -21,6 +22,8 @@ use InvalidArgumentException;
class DataQuery
{
+ use Extensible;
+
/**
* @var string
*/
@@ -930,7 +933,7 @@ class DataQuery
$joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}";
}
$this->query->addLeftJoin(
- $foreignTable,
+ $this->getJoinTableName($foreignClass, $foreignTable),
$joinExpression,
$foreignTableAliased
);
@@ -944,7 +947,7 @@ class DataQuery
if ($ancestorTable !== $foreignTable) {
$ancestorTableAliased = $foreignPrefix . $ancestorTable;
$this->query->addLeftJoin(
- $ancestorTable,
+ $this->getJoinTableName($ancestor, $ancestorTable),
"\"{$foreignTableAliased}\".\"ID\" = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased
);
@@ -990,7 +993,7 @@ class DataQuery
$foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID', $foreignPrefix);
$localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID", $localPrefix);
$this->query->addLeftJoin(
- $foreignBaseTable,
+ $this->getJoinTableName($foreignClass, $foreignBaseTable),
"{$foreignIDColumn} = {$localColumn}",
$foreignPrefix . $foreignBaseTable
);
@@ -1005,7 +1008,7 @@ class DataQuery
if ($ancestorTable !== $foreignBaseTable) {
$ancestorTableAliased = $foreignPrefix . $ancestorTable;
$this->query->addLeftJoin(
- $ancestorTable,
+ $this->getJoinTableName($ancestor, $ancestorTable),
"{$foreignIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased
);
@@ -1039,7 +1042,13 @@ class DataQuery
$schema = DataObject::getSchema();
if (class_exists($relationClassOrTable)) {
- $relationClassOrTable = $schema->tableName($relationClassOrTable);
+ // class is provided
+ $relationTable = $schema->tableName($relationClassOrTable);
+ $relationTableUpdated = $this->getJoinTableName($relationClassOrTable, $relationTable);
+ } else {
+ // table is provided
+ $relationTable = $relationClassOrTable;
+ $relationTableUpdated = $relationClassOrTable;
}
// Check if already joined to component alias (skip join table for the check)
@@ -1051,10 +1060,10 @@ class DataQuery
}
// Join parent class to join table
- $relationAliasedTable = $componentPrefix . $relationClassOrTable;
+ $relationAliasedTable = $componentPrefix . $relationTable;
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix);
$this->query->addLeftJoin(
- $relationClassOrTable,
+ $relationTableUpdated,
"\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}",
$relationAliasedTable
);
@@ -1062,7 +1071,7 @@ class DataQuery
// Join on base table of component class
$componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix);
$this->query->addLeftJoin(
- $componentBaseTable,
+ $this->getJoinTableName($componentBaseClass, $componentBaseTable),
"\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}",
$componentAliasedTable
);
@@ -1076,7 +1085,7 @@ class DataQuery
if ($ancestorTable !== $componentBaseTable) {
$ancestorTableAliased = $componentPrefix . $ancestorTable;
$this->query->addLeftJoin(
- $ancestorTable,
+ $this->getJoinTableName($ancestor, $ancestorTable),
"{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased
);
@@ -1142,16 +1151,47 @@ class DataQuery
/**
* Query the given field column from the database and return as an array.
+ * querying DB columns of related tables is supported but you need to make sure that the related table
+ * is already available in join
+ *
+ * @see DataList::applyRelation()
+ *
+ * example use:
+ *
+ *
+ * column("MyTable"."Title")
+ *
+ * or
+ *
+ * $columnName = null;
+ * Category::get()
+ * ->applyRelation('Products.Title', $columnName)
+ * ->column($columnName);
+ *
*
* @param string $field See {@link expressionForField()}.
* @return array List of column values for the specified column
+ * @throws InvalidArgumentException
*/
public function column($field = 'ID')
{
$fieldExpression = $this->expressionForField($field);
- $query = $this->getFinalisedQuery(array($field));
+ $query = $this->getFinalisedQuery([$field]);
$originalSelect = $query->getSelect();
- $query->setSelect(array());
+ $query->setSelect([]);
+
+ // field wasn't recognised as a valid field from the table class hierarchy
+ // check if the field is in format "".""
+ // if that's the case we may want to query related table
+ if (!$fieldExpression) {
+ if (!$this->validateColumnField($field, $query)) {
+ throw new InvalidArgumentException('Invalid column name ' . $field);
+ }
+
+ $fieldExpression = $field;
+ $field = null;
+ }
+
$query->selectField($fieldExpression, $field);
$this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
@@ -1257,4 +1297,34 @@ class DataQuery
$this->dataQueryManipulators[] = $manipulator;
return $this;
}
+
+ private function validateColumnField($field, SQLSelect $query)
+ {
+ // standard column - nothing to process here
+ if (strpos($field, '.') === false) {
+ return false;
+ }
+
+ $fieldData = explode('.', $field);
+ $tablePrefix = str_replace('"', '', $fieldData[0]);
+
+ // check if related table is available
+ return $query->isJoinedTo($tablePrefix);
+ }
+
+ /**
+ * Use this extension point to alter the table name
+ * useful for versioning for example
+ *
+ * @param $class
+ * @param $table
+ * @return mixed
+ */
+ private function getJoinTableName($class, $table)
+ {
+ $updated = $table;
+ $this->invokeWithExtensions('updateJoinTableName', $class, $table, $updated);
+
+ return $updated;
+ }
}
diff --git a/tests/php/ORM/DataListTest.php b/tests/php/ORM/DataListTest.php
index e967a32a3..f11b7e4eb 100755
--- a/tests/php/ORM/DataListTest.php
+++ b/tests/php/ORM/DataListTest.php
@@ -4,24 +4,24 @@ namespace SilverStripe\ORM\Tests;
use InvalidArgumentException;
use SilverStripe\Core\Convert;
-use SilverStripe\Core\Injector\InjectorNotFoundException;
+use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataQuery;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Filterable;
use SilverStripe\ORM\Filters\ExactMatchFilter;
-use SilverStripe\Dev\SapphireTest;
-use SilverStripe\ORM\Tests\DataObjectTest\Fixture;
use SilverStripe\ORM\Tests\DataObjectTest\Bracket;
use SilverStripe\ORM\Tests\DataObjectTest\EquipmentCompany;
use SilverStripe\ORM\Tests\DataObjectTest\Fan;
+use SilverStripe\ORM\Tests\DataObjectTest\Fixture;
use SilverStripe\ORM\Tests\DataObjectTest\Player;
use SilverStripe\ORM\Tests\DataObjectTest\Sortable;
+use SilverStripe\ORM\Tests\DataObjectTest\Staff;
use SilverStripe\ORM\Tests\DataObjectTest\SubTeam;
use SilverStripe\ORM\Tests\DataObjectTest\Team;
use SilverStripe\ORM\Tests\DataObjectTest\TeamComment;
use SilverStripe\ORM\Tests\DataObjectTest\ValidatedObject;
-use SilverStripe\ORM\Tests\DataObjectTest\Staff;
+use SilverStripe\ORM\Tests\ManyManyListTest\Category;
/**
* @skipUpgrade
@@ -1842,4 +1842,37 @@ class DataListTest extends SapphireTest
$this->assertSQLContains(DB::get_conn()->random() . ' AS "_SortColumn', $list->dataQuery()->sql());
}
+
+ public function testColumnFailureInvalidColumn()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ Category::get()->column('ObviouslyInvalidColumn');
+ }
+
+ public function testColumnFailureInvalidTable()
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $columnName = null;
+ Category::get()
+ ->applyRelation('Products.ID', $columnName)
+ ->column('"ObviouslyInvalidTable"."ID"');
+ }
+
+ public function testColumnFromRelatedTable()
+ {
+ $columnName = null;
+ $productTitles = Category::get()
+ ->applyRelation('Products.Title', $columnName)
+ ->column($columnName);
+
+ $productTitles = array_diff($productTitles, [null]);
+ sort($productTitles);
+
+ $this->assertEquals([
+ 'Product A',
+ 'Product B',
+ ], $productTitles);
+ }
}