2009-11-22 06:16:38 +01:00
< ? php
/**
* An object representing a query of data from the DataObject ' s supporting database .
* Acts as a wrapper over { @ link SQLQuery } and performs all of the query generation .
2012-04-15 10:34:10 +02:00
* Used extensively by { @ link DataList } .
*
* @ subpackage model
2012-06-20 23:59:16 +02:00
* @ package framework
2009-11-22 06:16:38 +01:00
*/
class DataQuery {
2012-04-15 10:34:10 +02:00
/**
* @ var String
*/
2009-11-22 06:16:38 +01:00
protected $dataClass ;
2012-04-15 10:34:10 +02:00
/**
* @ var SQLQuery
*/
2009-11-22 06:16:38 +01:00
protected $query ;
2012-04-15 10:34:10 +02:00
/**
* @ var array
*/
2009-11-22 06:16:38 +01:00
protected $collidingFields = array ();
2012-04-20 00:08:17 +02:00
private $queriedColumns = null ;
2009-11-22 06:16:38 +01:00
2012-04-15 10:34:10 +02:00
/**
* @ var Boolean
*/
2009-11-22 06:16:38 +01:00
private $queryFinalised = false ;
// TODO: replace subclass_access with this
protected $querySubclasses = true ;
// TODO: replace restrictclasses with this
protected $filterByClassName = true ;
/**
* Create a new DataQuery .
2012-04-15 10:34:10 +02:00
*
* @ param String The name of the DataObject class that you wish to query
2009-11-22 06:16:38 +01:00
*/
function __construct ( $dataClass ) {
$this -> dataClass = $dataClass ;
$this -> initialiseQuery ();
}
/**
* Clone this object
*/
function __clone () {
$this -> query = clone $this -> query ;
}
/**
* Return the { @ link DataObject } class that is being queried .
*/
function dataClass () {
2012-05-03 09:34:16 +02:00
return $this -> dataClass ;
2009-11-22 06:16:38 +01:00
}
/**
* Return the { @ link SQLQuery } object that represents the current query ; note that it will
* be a clone of the object .
*/
function query () {
return $this -> getFinalisedQuery ();
}
/**
* Remove a filter from the query
*/
function removeFilterOn ( $fieldExpression ) {
$matched = false ;
2012-05-03 09:34:16 +02:00
$where = $this -> query -> getWhere ();
foreach ( $where as $i => $clause ) {
if ( strpos ( $clause , $fieldExpression ) !== false ) {
unset ( $where [ $i ]);
2009-11-22 06:16:38 +01:00
$matched = true ;
}
}
2012-05-03 09:34:16 +02:00
// set the entire where clause back, but clear the original one first
if ( $matched ) {
$this -> query -> setWhere ( $where );
} else {
throw new InvalidArgumentException ( " Couldn't find $fieldExpression in the query filter. " );
}
2009-11-22 06:16:38 +01:00
return $this ;
}
/**
2012-04-20 00:08:17 +02:00
* Set up the simplest initial query
2009-11-22 06:16:38 +01:00
*/
function initialiseQuery () {
2012-04-20 00:08:17 +02:00
// Get the tables to join to.
// Don't get any subclass tables - let lazy loading do that.
$tableClasses = ClassInfo :: ancestry ( $this -> dataClass , true );
2009-11-22 06:16:38 +01:00
// Error checking
if ( ! $tableClasses ) {
2011-05-01 07:12:26 +02:00
if ( ! SS_ClassLoader :: instance () -> hasManifest ()) {
2009-11-22 06:16:38 +01:00
user_error ( " DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php. " , E_USER_ERROR );
} else {
user_error ( " DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->dataClass . Please ensure you run dev/build after creating a new DataObject. " , E_USER_ERROR );
}
}
$baseClass = array_shift ( $tableClasses );
// Build our intial query
$this -> query = new SQLQuery ( array ());
2012-05-03 09:34:16 +02:00
$this -> query -> setDistinct ( true );
2009-11-22 06:16:38 +01:00
if ( $sort = singleton ( $this -> dataClass ) -> stat ( 'default_sort' )) {
$this -> sort ( $sort );
}
2012-05-03 09:34:16 +02:00
$this -> query -> setFrom ( " \" $baseClass\ " " );
2009-11-22 06:16:38 +01:00
2012-06-11 12:40:47 +02:00
$obj = Injector :: inst () -> get ( $baseClass );
$obj -> extend ( 'augmentDataQueryCreation' , $this -> query , $this );
2009-11-22 06:16:38 +01:00
}
2012-04-20 00:08:17 +02:00
function setQueriedColumns ( $queriedColumns ) {
$this -> queriedColumns = $queriedColumns ;
}
2009-11-22 06:16:38 +01:00
/**
* Ensure that the query is ready to execute .
*/
2012-04-20 00:08:17 +02:00
function getFinalisedQuery ( $queriedColumns = null ) {
if ( ! $queriedColumns ) $queriedColumns = $this -> queriedColumns ;
if ( $queriedColumns ) {
$queriedColumns = array_merge ( $queriedColumns , array ( 'Created' , 'LastEdited' , 'ClassName' ));
}
2009-11-22 06:16:38 +01:00
$query = clone $this -> query ;
2011-10-29 06:27:21 +02:00
2012-04-20 00:08:17 +02:00
// Generate the list of tables to iterate over and the list of columns required by any existing where clauses.
// This second step is skipped if we're fetching the whole dataobject as any required columns will get selected
// regardless.
if ( $queriedColumns ) {
$tableClasses = ClassInfo :: dataClassesFor ( $this -> dataClass );
2012-05-03 09:34:16 +02:00
foreach ( $query -> getWhere () as $where ) {
2012-04-20 00:08:17 +02:00
// Check for just the column, in the form '"Column" = ?' and the form '"Table"."Column"' = ?
if ( preg_match ( '/^"([^"]+)"/' , $where , $matches ) ||
preg_match ( '/^"([^"]+)"\."[^"]+"/' , $where , $matches )) {
if ( ! in_array ( $matches [ 1 ], $queriedColumns )) $queriedColumns [] = $matches [ 1 ];
}
}
}
else $tableClasses = ClassInfo :: ancestry ( $this -> dataClass , true );
$tableNames = array_keys ( $tableClasses );
$baseClass = $tableNames [ 0 ];
// Iterate over the tables and check what we need to select from them. If any selects are made (or the table is
// required for a select)
foreach ( $tableClasses as $tableClass ) {
$joinTable = false ;
// If queriedColumns is set, then check if any of the fields are in this table.
if ( $queriedColumns ) {
$tableFields = DataObject :: database_fields ( $tableClass );
$selectColumns = array ();
// Look through columns specifically requested in query (or where clause)
foreach ( $queriedColumns as $queriedColumn ) {
if ( array_key_exists ( $queriedColumn , $tableFields )) {
$selectColumns [] = $queriedColumn ;
}
}
$this -> selectColumnsFromTable ( $query , $tableClass , $selectColumns );
if ( $selectColumns && $tableClass != $baseClass ) {
$joinTable = true ;
}
} else {
$this -> selectColumnsFromTable ( $query , $tableClass );
if ( $tableClass != $baseClass ) $joinTable = true ;
}
if ( $joinTable ) {
2012-05-03 09:34:16 +02:00
$query -> addLeftJoin ( $tableClass , " \" $tableClass\ " . \ " ID \" = \" $baseClass\ " . \ " ID \" " ) ;
2009-11-22 06:16:38 +01:00
}
}
// Resolve colliding fields
if ( $this -> collidingFields ) {
foreach ( $this -> collidingFields as $k => $collisions ) {
$caseClauses = array ();
foreach ( $collisions as $collision ) {
if ( preg_match ( '/^"([^"]+)"/' , $collision , $matches )) {
$collisionBase = $matches [ 1 ];
$collisionClasses = ClassInfo :: subclassesFor ( $collisionBase );
2012-06-02 15:38:12 +02:00
$collisionClasses = array_map ( array ( DB :: getConn (), 'prepStringForDB' ), $collisionClasses );
2012-06-05 01:07:19 +02:00
$caseClauses [] = " WHEN \" $baseClass\ " . \ " ClassName \" IN ( "
. implode ( " , " , $collisionClasses ) . " ) THEN $collision " ;
2009-11-22 06:16:38 +01:00
} else {
user_error ( " Bad collision item ' $collision ' " , E_USER_WARNING );
}
}
2012-05-01 06:42:14 +02:00
$query -> selectField ( " CASE " . implode ( " " , $caseClauses ) . " ELSE NULL END " , $k );
2009-11-22 06:16:38 +01:00
}
}
if ( $this -> filterByClassName ) {
// If querying the base class, don't bother filtering on class name
if ( $this -> dataClass != $baseClass ) {
// Get the ClassName values to filter to
$classNames = ClassInfo :: subclassesFor ( $this -> dataClass );
2012-03-19 03:27:52 +01:00
if ( ! $classNames ) user_error ( " DataList::create() Can't find data sub-classes for ' $callerClass ' " );
2012-06-02 15:38:12 +02:00
$classNames = array_map ( array ( DB :: getConn (), 'prepStringForDB' ), $classNames );
2012-06-05 01:07:19 +02:00
$query -> addWhere ( " \" $baseClass\ " . \ " ClassName \" IN ( " . implode ( " , " , $classNames ) . " ) " );
2009-11-22 06:16:38 +01:00
}
}
2012-05-01 06:42:14 +02:00
$query -> selectField ( " \" $baseClass\ " . \ " ID \" " , " ID " );
2012-07-17 03:28:20 +02:00
$query -> selectField ( " CASE WHEN \" $baseClass\ " . \ " ClassName \" IS NOT NULL THEN \" $baseClass\ " . \ " ClassName \" ELSE " . DB :: getConn () -> prepStringForDB ( $baseClass ) . " END " , " RecordClassName " );
2011-10-29 06:27:21 +02:00
2009-11-22 06:16:38 +01:00
// TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses of DataQuery
2012-06-11 12:40:47 +02:00
2012-08-02 14:06:45 +02:00
$obj = Injector :: inst () -> get ( $this -> dataClass );
2012-06-11 12:40:47 +02:00
$obj -> extend ( 'augmentSQL' , $query , $this );
2011-10-29 06:27:21 +02:00
$this -> ensureSelectContainsOrderbyColumns ( $query );
2009-11-22 06:16:38 +01:00
return $query ;
}
2011-10-29 06:27:21 +02:00
/**
* Ensure that if a query has an order by clause , those columns are present in the select .
*
* @ param SQLQuery $query
* @ return null
*/
2012-05-01 07:09:57 +02:00
protected function ensureSelectContainsOrderbyColumns ( $query , $originalSelect = array ()) {
2011-10-29 06:27:21 +02:00
$tableClasses = ClassInfo :: dataClassesFor ( $this -> dataClass );
$baseClass = array_shift ( $tableClasses );
2012-05-03 09:34:16 +02:00
if ( $orderby = $query -> getOrderBy ()) {
2012-04-15 10:34:10 +02:00
foreach ( $orderby as $k => $dir ) {
// don't touch functions in the ORDER BY or function calls
// selected as fields
2012-05-01 07:09:57 +02:00
if ( strpos ( $k , '(' ) !== false ) continue ;
2012-05-03 02:02:21 +02:00
$col = str_replace ( '"' , '' , trim ( $k ));
$parts = explode ( '.' , $col );
2012-05-01 07:09:57 +02:00
// Pull through SortColumn references from the originalSelect variables
2012-05-03 02:02:21 +02:00
if ( preg_match ( '/_SortColumn/' , $col )) {
if ( isset ( $originalSelect [ $col ])) $query -> selectField ( $originalSelect [ $col ], $col );
2012-04-15 10:34:10 +02:00
continue ;
2012-05-01 07:09:57 +02:00
}
2012-04-15 10:34:10 +02:00
2011-10-29 06:27:21 +02:00
if ( count ( $parts ) == 1 ) {
$databaseFields = DataObject :: database_fields ( $baseClass );
2012-04-15 10:34:10 +02:00
// database_fields() doesn't return ID, so we need to
// manually add it here
2011-10-29 06:27:21 +02:00
$databaseFields [ 'ID' ] = true ;
2012-04-15 10:34:10 +02:00
2011-10-29 06:27:21 +02:00
if ( isset ( $databaseFields [ $parts [ 0 ]])) {
$qualCol = " \" $baseClass\ " . \ " { $parts [ 0 ] } \" " ;
2012-04-15 10:34:10 +02:00
// remove original sort
unset ( $orderby [ $k ]);
// add new columns sort
$orderby [ $qualCol ] = $dir ;
2011-10-29 06:27:21 +02:00
} else {
$qualCol = " \" $parts[0] \" " ;
}
2012-04-15 10:34:10 +02:00
2012-05-03 09:34:16 +02:00
// To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect()
2012-05-01 06:42:14 +02:00
// format internally; then this check can be part of selectField()
2012-05-03 09:34:16 +02:00
$selects = $query -> getSelect ();
if ( ! isset ( $selects [ $col ]) && ! in_array ( $qualCol , $selects )) {
2012-05-01 06:42:14 +02:00
$query -> selectField ( $qualCol );
2011-10-29 06:27:21 +02:00
}
} else {
$qualCol = '"' . implode ( '"."' , $parts ) . '"' ;
2012-04-15 10:34:10 +02:00
2012-05-03 09:34:16 +02:00
// To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect()
2012-05-01 06:42:14 +02:00
// format internally; then this check can be part of selectField()
2012-05-03 09:34:16 +02:00
if ( ! in_array ( $qualCol , $query -> getSelect ())) {
2012-05-01 06:42:14 +02:00
$query -> selectField ( $qualCol );
2011-10-29 06:27:21 +02:00
}
}
}
2012-05-03 09:34:16 +02:00
$query -> setOrderBy ( $orderby );
2011-10-29 06:27:21 +02:00
}
}
2009-11-22 06:16:38 +01:00
/**
* Execute the query and return the result as { @ link Query } object .
*/
function execute () {
return $this -> getFinalisedQuery () -> execute ();
}
/**
* Return this query ' s SQL
*/
function sql () {
return $this -> getFinalisedQuery () -> sql ();
}
/**
* Return the number of records in this query .
* Note that this will issue a separate SELECT COUNT () query .
*/
function count () {
2012-05-19 02:55:49 +02:00
$baseClass = ClassInfo :: baseDataClass ( $this -> dataClass );
2009-11-22 06:16:38 +01:00
return $this -> getFinalisedQuery () -> count ( " DISTINCT \" $baseClass\ " . \ " ID \" " );
}
/**
* Return the maximum value of the given field in this DataList
2012-05-16 11:29:49 +02:00
*
* @ param String $field Unquoted database column name ( will be escaped automatically )
2009-11-22 06:16:38 +01:00
*/
2012-05-29 00:03:22 +02:00
function max ( $field ) {
return $this -> aggregate ( sprintf ( 'MAX("%s")' , Convert :: raw2sql ( $field )));
2009-11-22 06:16:38 +01:00
}
/**
* Return the minimum value of the given field in this DataList
2012-05-16 11:29:49 +02:00
*
* @ param String $field Unquoted database column name ( will be escaped automatically )
2009-11-22 06:16:38 +01:00
*/
2012-05-19 02:55:49 +02:00
function min ( $field ) {
2012-05-29 00:03:22 +02:00
return $this -> aggregate ( sprintf ( 'MIN("%s")' , Convert :: raw2sql ( $field )));
2009-11-22 06:16:38 +01:00
}
/**
* Return the average value of the given field in this DataList
2012-05-16 11:29:49 +02:00
*
* @ param String $field Unquoted database column name ( will be escaped automatically )
2009-11-22 06:16:38 +01:00
*/
2012-05-19 02:55:49 +02:00
function avg ( $field ) {
2012-05-29 00:03:22 +02:00
return $this -> aggregate ( sprintf ( 'AVG("%s")' , Convert :: raw2sql ( $field )));
2009-11-22 06:16:38 +01:00
}
/**
* Return the sum of the values of the given field in this DataList
2012-05-16 11:29:49 +02:00
*
* @ param String $field Unquoted database column name ( will be escaped automatically )
2009-11-22 06:16:38 +01:00
*/
2012-05-19 02:55:49 +02:00
function sum ( $field ) {
2012-05-29 00:03:22 +02:00
return $this -> aggregate ( sprintf ( 'SUM("%s")' , Convert :: raw2sql ( $field )));
}
/**
* Runs a raw aggregate expression . Please handle escaping yourself
*/
function aggregate ( $expression ) {
return $this -> getFinalisedQuery () -> aggregate ( $expression ) -> execute () -> value ();
2009-11-22 06:16:38 +01:00
}
/**
* Return the first row that would be returned by this full DataQuery
* Note that this will issue a separate SELECT ... LIMIT 1 query .
*/
function firstRow () {
return $this -> getFinalisedQuery () -> firstRow ();
}
/**
* Return the last row that would be returned by this full DataQuery
* Note that this will issue a separate SELECT ... LIMIT query .
*/
function lastRow () {
return $this -> getFinalisedQuery () -> lastRow ();
}
/**
* Update the SELECT clause of the query with the columns from the given table
*/
2012-04-20 00:08:17 +02:00
protected function selectColumnsFromTable ( SQLQuery & $query , $tableClass , $columns = null ) {
// Add SQL for multi-value fields
$databaseFields = DataObject :: database_fields ( $tableClass );
$compositeFields = DataObject :: composite_fields ( $tableClass , false );
if ( $databaseFields ) foreach ( $databaseFields as $k => $v ) {
if (( is_null ( $columns ) || in_array ( $k , $columns )) && ! isset ( $compositeFields [ $k ])) {
// Update $collidingFields if necessary
2012-05-01 06:42:14 +02:00
if ( $expressionForField = $query -> expressionForField ( $k )) {
if ( ! isset ( $this -> collidingFields [ $k ])) $this -> collidingFields [ $k ] = array ( $expressionForField );
2012-04-20 00:08:17 +02:00
$this -> collidingFields [ $k ][] = " \" $tableClass\ " . \ " $k\ " " ;
2009-11-22 06:16:38 +01:00
2012-04-20 00:08:17 +02:00
} else {
2012-05-01 06:42:14 +02:00
$query -> selectField ( " \" $tableClass\ " . \ " $k\ " " , $k );
2012-04-20 00:08:17 +02:00
}
}
}
if ( $compositeFields ) foreach ( $compositeFields as $k => $v ) {
if (( is_null ( $columns ) || in_array ( $k , $columns )) && $v ) {
$dbO = Object :: create_from_string ( $v , $k );
$dbO -> addToQuery ( $query );
}
}
2009-11-22 06:16:38 +01:00
}
/**
* Set the HAVING clause of this query
2012-05-16 11:29:49 +02:00
*
* @ param String $having Escaped SQL statement
2009-11-22 06:16:38 +01:00
*/
function having ( $having ) {
if ( $having ) {
$clone = $this ;
2012-05-03 09:34:16 +02:00
$clone -> query -> addHaving ( $having );
2009-11-22 06:16:38 +01:00
return $clone ;
} else {
return $this ;
}
}
/**
2012-05-16 11:29:49 +02:00
* Set the WHERE clause of this query .
* There are two different ways of doing this :
*
* < code >
* // the entire predicate as a single string
* $query -> where ( " Column = 'Value' " );
*
* // multiple predicates as an array
* $query -> where ( array ( " Column = 'Value' " , " Column != 'Value' " ));
* </ code >
*
* @ param string | array $where Predicate ( s ) to set , as escaped SQL statements .
2009-11-22 06:16:38 +01:00
*/
2011-04-05 13:01:57 +02:00
function where ( $filter ) {
2009-11-22 06:16:38 +01:00
if ( $filter ) {
$clone = $this ;
2012-05-03 09:34:16 +02:00
$clone -> query -> addWhere ( $filter );
2009-11-22 06:16:38 +01:00
return $clone ;
} else {
return $this ;
}
}
2011-12-09 14:09:07 +01:00
/**
2012-05-16 11:29:49 +02:00
* Set a WHERE with OR .
*
* @ example $dataQuery -> whereAny ( array ( " Monkey = 'Chimp' " , " Color = 'Brown' " ));
* @ see where ()
2011-12-09 14:09:07 +01:00
*
2012-05-16 11:29:49 +02:00
* @ param array $filter Escaped SQL statement .
2011-12-09 14:09:07 +01:00
* @ return DataQuery
*/
function whereAny ( $filter ) {
if ( $filter ) {
$clone = $this ;
2012-06-22 04:34:06 +02:00
$clone -> query -> addWhereAny ( $filter );
2011-12-09 14:09:07 +01:00
return $clone ;
} else {
return $this ;
}
}
2009-11-22 06:16:38 +01:00
/**
* Set the ORDER BY clause of this query
2012-04-15 10:34:10 +02:00
*
* @ see SQLQuery :: orderby ()
*
2012-05-16 11:29:49 +02:00
* @ param String $sort Column to sort on ( escaped SQL statement )
* @ param String $direction Direction ( " ASC " or " DESC " , escaped SQL statement )
* @ param Boolean $clear Clear existing values
2012-04-15 10:34:10 +02:00
* @ return DataQuery
2009-11-22 06:16:38 +01:00
*/
2012-04-15 10:34:10 +02:00
function sort ( $sort = null , $direction = null , $clear = true ) {
$clone = $this ;
2012-05-03 09:34:16 +02:00
if ( $clear ) {
$clone -> query -> setOrderBy ( $sort , $direction );
} else {
$clone -> query -> addOrderBy ( $sort , $direction );
}
2012-04-15 10:34:10 +02:00
return $clone ;
}
/**
* Reverse order by clause
*
* @ return DataQuery
*/
function reverseSort () {
$clone = $this ;
$clone -> query -> reverseOrderBy ();
return $clone ;
2009-11-22 06:16:38 +01:00
}
/**
2012-05-16 11:29:49 +02:00
* Set the limit of this query .
*
* @ param int $limit
* @ param int $offset
2009-11-22 06:16:38 +01:00
*/
2012-03-09 02:02:37 +01:00
function limit ( $limit , $offset = 0 ) {
2011-10-27 05:11:26 +02:00
$clone = $this ;
2012-05-03 09:34:16 +02:00
$clone -> query -> setLimit ( $limit , $offset );
2011-10-27 05:11:26 +02:00
return $clone ;
2009-11-22 06:16:38 +01:00
}
/**
* Add a join clause to this query
2012-03-09 21:34:05 +01:00
* @ deprecated 3.0 Use innerJoin () or leftJoin () instead .
2009-11-22 06:16:38 +01:00
*/
function join ( $join ) {
2011-10-29 01:02:11 +02:00
Deprecation :: notice ( '3.0' , 'Use innerJoin() or leftJoin() instead.' );
2009-11-22 06:16:38 +01:00
if ( $join ) {
$clone = $this ;
2012-05-03 09:34:16 +02:00
$clone -> query -> addFrom ( $join );
2009-11-22 06:16:38 +01:00
// TODO: This needs to be resolved for all databases
2012-05-03 09:34:16 +02:00
if ( DB :: getConn () instanceof MySQLDatabase ) {
$from = $clone -> query -> getFrom ();
$clone -> query -> setGroupBy ( reset ( $from ) . " . \" ID \" " );
}
2009-11-22 06:16:38 +01:00
return $clone ;
} else {
return $this ;
}
}
/**
2012-05-16 11:29:49 +02:00
* Add an INNER JOIN clause to this query .
*
* @ param String $table The unquoted table name to join to .
* @ param String $onClause The filter for the join ( escaped SQL statement )
* @ param String $alias An optional alias name ( unquoted )
2009-11-22 06:16:38 +01:00
*/
public function innerJoin ( $table , $onClause , $alias = null ) {
if ( $table ) {
$clone = $this ;
2012-05-03 09:34:16 +02:00
$clone -> query -> addInnerJoin ( $table , $onClause , $alias );
2009-11-22 06:16:38 +01:00
return $clone ;
} else {
return $this ;
}
}
/**
2012-05-16 11:29:49 +02:00
* Add a LEFT JOIN clause to this query .
*
* @ param String $table The unquoted table to join to .
* @ param String $onClause The filter for the join ( escaped SQL statement ) .
* @ param String $alias An optional alias name ( unquoted )
2009-11-22 06:16:38 +01:00
*/
public function leftJoin ( $table , $onClause , $alias = null ) {
if ( $table ) {
$clone = $this ;
2012-05-03 09:34:16 +02:00
$clone -> query -> addLeftJoin ( $table , $onClause , $alias );
2009-11-22 06:16:38 +01:00
return $clone ;
} else {
return $this ;
}
}
2011-03-21 09:37:55 +01:00
/**
* Traverse the relationship fields , and add the table
* mappings to the query object state . This has to be called
* in any overloaded { @ link SearchFilter -> apply ()} methods manually .
*
2012-05-16 11:29:49 +02:00
* @ param String | array $relation The array / dot - syntax relation to follow
2011-03-21 09:37:55 +01:00
* @ return The model class of the related item
*/
function applyRelation ( $relation ) {
// NO-OP
if ( ! $relation ) return $this -> dataClass ;
if ( is_string ( $relation )) $relation = explode ( " . " , $relation );
2012-06-25 01:34:02 +02:00
2011-03-21 09:37:55 +01:00
$modelClass = $this -> dataClass ;
foreach ( $relation as $rel ) {
$model = singleton ( $modelClass );
if ( $component = $model -> has_one ( $rel )) {
if ( ! $this -> query -> isJoinedTo ( $component )) {
$foreignKey = $model -> getReverseAssociation ( $component );
2012-05-03 09:34:16 +02:00
$this -> query -> addLeftJoin ( $component , " \" $component\ " . \ " ID \" = \" { $modelClass } \" . \" { $foreignKey } ID \" " );
2011-03-21 09:37:55 +01:00
/**
* add join clause to the component ' s ancestry classes so that the search filter could search on its
* ancester fields .
*/
$ancestry = ClassInfo :: ancestry ( $component , true );
if ( ! empty ( $ancestry )){
$ancestry = array_reverse ( $ancestry );
foreach ( $ancestry as $ancestor ){
if ( $ancestor != $component ){
2012-05-03 09:34:16 +02:00
$this -> query -> addInnerJoin ( $ancestor , " \" $component\ " . \ " ID \" = \" $ancestor\ " . \ " ID \" " );
2011-03-21 09:37:55 +01:00
}
}
}
}
$modelClass = $component ;
} elseif ( $component = $model -> has_many ( $rel )) {
if ( ! $this -> query -> isJoinedTo ( $component )) {
$ancestry = $model -> getClassAncestry ();
$foreignKey = $model -> getRemoteJoinField ( $rel );
2012-05-03 09:34:16 +02:00
$this -> query -> addLeftJoin ( $component , " \" $component\ " . \ " { $foreignKey } \" = \" { $ancestry [ 0 ] } \" . \" ID \" " );
2011-03-21 09:37:55 +01:00
/**
* add join clause to the component ' s ancestry classes so that the search filter could search on its
* ancestor fields .
*/
$ancestry = ClassInfo :: ancestry ( $component , true );
if ( ! empty ( $ancestry )){
$ancestry = array_reverse ( $ancestry );
foreach ( $ancestry as $ancestor ){
if ( $ancestor != $component ){
2012-05-03 09:34:16 +02:00
$this -> query -> addInnerJoin ( $ancestor , " \" $component\ " . \ " ID \" = \" $ancestor\ " . \ " ID \" " );
2011-03-21 09:37:55 +01:00
}
}
}
}
$modelClass = $component ;
} elseif ( $component = $model -> many_many ( $rel )) {
list ( $parentClass , $componentClass , $parentField , $componentField , $relationTable ) = $component ;
$parentBaseClass = ClassInfo :: baseDataClass ( $parentClass );
$componentBaseClass = ClassInfo :: baseDataClass ( $componentClass );
2012-05-03 09:34:16 +02:00
$this -> query -> addInnerJoin ( $relationTable , " \" $relationTable\ " . \ " $parentField\ " = \ " $parentBaseClass\ " . \ " ID \" " );
$this -> query -> addLeftJoin ( $componentBaseClass , " \" $relationTable\ " . \ " $componentField\ " = \ " $componentBaseClass\ " . \ " ID \" " );
2011-03-21 09:37:55 +01:00
if ( ClassInfo :: hasTable ( $componentClass )) {
2012-05-03 09:34:16 +02:00
$this -> query -> addLeftJoin ( $componentClass , " \" $relationTable\ " . \ " $componentField\ " = \ " $componentClass\ " . \ " ID \" " );
2011-03-21 09:37:55 +01:00
}
$modelClass = $componentClass ;
}
}
return $modelClass ;
2012-01-25 23:53:12 +01:00
}
/**
* Removes the result of query from this query .
*
* @ param DataQuery $subtractQuery
* @ param string $field
*/
public function subtract ( DataQuery $subtractQuery , $field = 'ID' ) {
$subSelect = $subtractQuery -> getFinalisedQuery ();
2012-05-01 06:42:14 +02:00
$fieldExpression = $this -> expressionForField ( $field , $subSelect );
2012-05-03 09:34:16 +02:00
$subSelect -> setSelect ( array ());
2012-05-01 07:44:31 +02:00
$subSelect -> selectField ( $fieldExpression , $field );
2012-01-25 23:53:12 +01:00
$this -> where ( $this -> expressionForField ( $field , $this ) . ' NOT IN (' . $subSelect -> sql () . ')' );
2012-05-01 06:42:14 +02:00
return $this ;
2012-01-25 23:53:12 +01:00
}
2011-03-21 09:37:55 +01:00
2009-11-22 06:16:38 +01:00
/**
2012-05-16 11:29:49 +02:00
* Select the given fields from the given table .
*
* @ param String $table Unquoted table name ( will be escaped automatically )
* @ param Array $fields Database column names ( will be escaped automatically )
2009-11-22 06:16:38 +01:00
*/
public function selectFromTable ( $table , $fields ) {
2012-05-16 11:29:49 +02:00
$table = Convert :: raw2sql ( $table );
2009-11-22 06:16:38 +01:00
$fieldExpressions = array_map ( create_function ( '$item' ,
2012-05-16 11:29:49 +02:00
" return ' \" $table\ " . \ " ' . Convert::raw2sql( \$ item) . ' \" '; " ), $fields );
2009-11-22 06:16:38 +01:00
2012-05-03 09:34:16 +02:00
$this -> query -> setSelect ( $fieldExpressions );
2012-05-01 06:42:14 +02:00
return $this ;
2009-11-22 06:16:38 +01:00
}
/**
* Query the given field column from the database and return as an array .
2012-05-16 11:29:49 +02:00
*
* @ param String $field See { @ link expressionForField ()} .
2009-11-22 06:16:38 +01:00
*/
public function column ( $field = 'ID' ) {
2012-04-20 00:08:17 +02:00
$query = $this -> getFinalisedQuery ( array ( $field ));
2012-05-03 09:34:16 +02:00
$originalSelect = $query -> getSelect ();
2012-05-01 07:09:57 +02:00
$fieldExpression = $this -> expressionForField ( $field , $query );
2012-05-03 09:34:16 +02:00
$query -> setSelect ( array ());
2012-05-01 07:44:31 +02:00
$query -> selectField ( $fieldExpression , $field );
2012-05-01 07:09:57 +02:00
$this -> ensureSelectContainsOrderbyColumns ( $query , $originalSelect );
2011-10-29 06:27:21 +02:00
return $query -> execute () -> column ( $field );
2009-11-22 06:16:38 +01:00
}
2012-05-16 11:29:49 +02:00
/**
* @ param String $field Select statement identifier , either the unquoted column name ,
* the full composite SQL statement , or the alias set through { @ link SQLQquery -> selectField ()} .
* @ param SQLQuery $query
* @ return String
*/
2009-11-22 06:16:38 +01:00
protected function expressionForField ( $field , $query ) {
// Special case for ID
if ( $field == 'ID' ) {
$baseClass = ClassInfo :: baseDataClass ( $this -> dataClass );
return " \" $baseClass\ " . \ " ID \" " ;
} else {
2012-05-03 09:34:16 +02:00
return $query -> expressionForField ( $field );
}
2009-11-22 06:16:38 +01:00
}
/**
2012-05-16 11:29:49 +02:00
* Select the given field expressions .
*
* @ param $fieldExpression String The field to select ( escaped SQL statement )
* @ param $alias String The alias of that field ( escaped SQL statement )
2009-11-22 06:16:38 +01:00
*/
2012-05-01 06:42:14 +02:00
protected function selectField ( $fieldExpression , $alias = null ) {
$this -> query -> selectField ( $fieldExpression , $alias );
2009-11-22 06:16:38 +01:00
}
//// QUERY PARAMS
/**
* 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 ;
/**
* Set an arbitrary query parameter , that can be used by decorators to add additional meta - data to the query .
* It 's expected that the $key will be namespaced, e.g, ' Versioned . stage ' instead of just ' stage ' .
*/
function setQueryParam ( $key , $value ) {
$this -> queryParams [ $key ] = $value ;
}
/**
* Set an arbitrary query parameter , that can be used by decorators to add additional meta - data to the query .
*/
function getQueryParam ( $key ) {
if ( isset ( $this -> queryParams [ $key ])) return $this -> queryParams [ $key ];
else return null ;
}
}