Skip to content

Commit

Permalink
Implemented Query::leftJoin()
Browse files Browse the repository at this point in the history
  • Loading branch information
lorenzo committed Jun 4, 2015
1 parent 57bd9c2 commit 931dc06
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 7 deletions.
5 changes: 2 additions & 3 deletions src/ORM/Association.php
Expand Up @@ -464,8 +464,7 @@ protected function _options(array $options)
* will be merged with any conditions originally configured for this association
* - fields: a list of fields in the target table to include in the result
* - type: The type of join to be used (e.g. INNER)
* - matching: Indicates whether the query records should be filtered based on
* the records found on this association. This will force a 'INNER JOIN'
* the records found on this association
* - aliasPath: A dot separated string representing the path of association names
* followed from the passed query main table to this association.
* - propertyPath: A dot separated string representing the path of association
Expand All @@ -488,7 +487,7 @@ public function attachTo(Query $query, array $options = [])
'foreignKey' => $this->foreignKey(),
'conditions' => [],
'fields' => [],
'type' => empty($options['matching']) ? $joinType : 'INNER',
'type' => $joinType,
'table' => $target->table(),
'finder' => $this->finder()
];
Expand Down
9 changes: 6 additions & 3 deletions src/ORM/EagerLoader.php
Expand Up @@ -171,9 +171,11 @@ public function autoFields($value = null)
* @param string|null $assoc A single association or a dot separated path of associations.
* @param callable|null $builder the callback function to be used for setting extra
* options to the filtering query
* @param array $options Extra options for the association matching, such as 'joinType'
* and 'fields'
* @return array The resulting containments array
*/
public function matching($assoc = null, callable $builder = null)
public function matching($assoc = null, callable $builder = null, $options = [])
{
if ($this->_matching === null) {
$this->_matching = new self();
Expand All @@ -187,13 +189,14 @@ public function matching($assoc = null, callable $builder = null)
$last = array_pop($assocs);
$containments = [];
$pointer =& $containments;
$options += ['joinType' => 'INNER'];

foreach ($assocs as $name) {
$pointer[$name] = ['matching' => true];
$pointer[$name] = ['matching' => true] + $options;
$pointer =& $pointer[$name];
}

$pointer[$last] = ['queryBuilder' => $builder, 'matching' => true];
$pointer[$last] = ['queryBuilder' => $builder, 'matching' => true] + $options;
return $this->_matching->contain($containments);
}

Expand Down
63 changes: 63 additions & 0 deletions src/ORM/Query.php
Expand Up @@ -337,6 +337,69 @@ public function matching($assoc, callable $builder = null)
return $this;
}

/**
* Creates a LEFT JOIN with the passed association table while preserving
* the foreign key matching and the custom conditions that were originally set
* for it.
*
* This function will add entries in the `contain` graph.
*
* ### Example:
*
* ```
* // Get the count of articles per user
* $usersQuery
* ->select(['total_articles' => $query->func()->count('Articles.id')])
* ->leftJoin('Articles')
* ->group(['Users.id'])
* ->autoFields(true);
* ```
*
* You can also customize the conditions passed to the LEFT JOIN:
*
* ```
* // Get the count of articles per user with at least 5 votes
* $usersQuery
* ->select(['total_articles' => $query->func()->count('Articles.id')])
* ->leftJoin('Articles', function ($q) {
* return $q->where(['Articles.votes >=' => 5]);
* })
* ->group(['Users.id'])
* ->autoFields(true);
* ```
*
* It is possible to left join deep associations by using dot notation
*
* ### Example:
*
* ```
* // Total comments in articles by 'markstory'
* $query
* ->select(['total_comments' => $query->func()->count('Comments.id')])
* ->leftJoin('Comments.Users', function ($q) {
* return $q->where(['username' => 'markstory']);
* )
* ->group(['Users.id']);
* ```
*
* Please note that the query passed to the closure will only accept calling
* `select`, `where`, `andWhere` and `orWhere` on it. If you wish to
* add more complex clauses you can do it directly in the main query.
*
* @param string $assoc The association to join with
* @param callable $builder a function that will receive a pre-made query object
* that can be used to add custom conditions or selecting some fields
* @return $this
*/
public function leftJoinWith($assoc, callable $builder = null) {
$this->eagerLoader()->matching($assoc, $builder, [
'joinType' => 'LEFT',
'fields' => false
]);
$this->_dirty();
return $this;
}

/**
* Returns a key => value array representing a single aliased field
* that can be passed directly to the select() method.
Expand Down
48 changes: 47 additions & 1 deletion tests/TestCase/ORM/QueryTest.php
Expand Up @@ -2214,7 +2214,8 @@ public function testDebugInfo()
'matching' => [
'articles' => [
'queryBuilder' => null,
'matching' => true
'matching' => true,
'joinType' => 'INNER'
]
],
'extraOptions' => ['foo' => 'bar'],
Expand Down Expand Up @@ -2661,4 +2662,49 @@ public function testIsEmpty()
$this->assertFalse($table->find()->isEmpty());
$this->assertTrue($table->find()->where(['id' => -1])->isEmpty());
}

/**
* Tests that leftJoinWith() creates a left join with a given association and
* that no fields from such association are loaded.
*
* @return void
*/
public function testLeftJoinWith()
{
$table = TableRegistry::get('authors');
$table->hasMany('articles');
$table->articles->deleteAll(['author_id' => 4]);
$results = $table
->find()
->select(['total_articles' => 'count(articles.id)'])
->autoFields(true)
->leftJoinWith('articles')
->group(['authors.id']);

$expected = [
1 => 2,
2 => 0,
3 => 1,
4 => 0
];
$this->assertEquals($expected, $results->combine('id', 'total_articles')->toArray());
$fields = ['total_articles', 'id', 'name'];
$this->assertEquals($fields, array_keys($results->first()->toArray()));

$results = $table
->find()
->leftJoinWith('articles')
->where(['articles.id IS' => null]);

$this->assertEquals([2, 4], $results->extract('id')->toList());
$this->assertEquals(['id', 'name'], array_keys($results->first()->toArray()));

$results = $table
->find()
->leftJoinWith('articles')
->where(['articles.id IS NOT' => null]);

$this->assertEquals([1, 1, 3], $results->extract('id')->toList());
$this->assertEquals(['id', 'name'], array_keys($results->first()->toArray()));
}
}

0 comments on commit 931dc06

Please sign in to comment.