diff --git a/src/ORM/DataList.php b/src/ORM/DataList.php index 9110caabb..44ed17976 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..e2a95f511 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->augmentTable($foreignClass, $foreignTable), $joinExpression, $foreignTableAliased ); @@ -944,7 +947,7 @@ class DataQuery if ($ancestorTable !== $foreignTable) { $ancestorTableAliased = $foreignPrefix . $ancestorTable; $this->query->addLeftJoin( - $ancestorTable, + $this->augmentTable($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->augmentTable($foreignClass, $foreignBaseTable), "{$foreignIDColumn} = {$localColumn}", $foreignPrefix . $foreignBaseTable ); @@ -1005,7 +1008,7 @@ class DataQuery if ($ancestorTable !== $foreignBaseTable) { $ancestorTableAliased = $foreignPrefix . $ancestorTable; $this->query->addLeftJoin( - $ancestorTable, + $this->augmentTable($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); + $relationTableAugmented = $this->augmentTable($relationClassOrTable, $relationTable); + } else { + // table is provided + $relationTable = $relationClassOrTable; + $relationTableAugmented = $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, + $relationTableAugmented, "\"{$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->augmentTable($componentBaseClass, $componentBaseTable), "\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}", $componentAliasedTable ); @@ -1076,7 +1085,7 @@ class DataQuery if ($ancestorTable !== $componentBaseTable) { $ancestorTableAliased = $componentPrefix . $ancestorTable; $this->query->addLeftJoin( - $ancestorTable, + $this->augmentTable($ancestor, $ancestorTable), "{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"", $ancestorTableAliased ); @@ -1084,6 +1093,22 @@ class DataQuery } } + /** + * Use this extension point to alter the table name + * useful for versioning for example + * + * @param $class + * @param $table + * @return mixed + */ + protected function augmentTable($class, $table) + { + $augmented = $table; + $this->invokeWithExtensions('augmentJoinTable', $class, $table, $augmented); + + return $augmented; + } + /** * Removes the result of query from this query. * @@ -1142,16 +1167,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 +1313,22 @@ 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 + if (!$query->isJoinedTo($tablePrefix)) { + return false; + } + + return true; + } } diff --git a/tests/php/ORM/DataListTest.php b/tests/php/ORM/DataListTest.php index a5dbbe3ab..aa11a64fc 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 @@ -1835,4 +1835,37 @@ class DataListTest extends SapphireTest $list->column("Title") ); } + + 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); + } }