php – Using IFNULL … GROUP_CONCAT() in CakePHP 3-ThrowExceptions

Exception or error:

I have a vanilla MySQL query as follows which works fine:

SELECT d.*, IFNULL(
     (SELECT GROUP_CONCAT(value) FROM display_substances `ds` 
         WHERE `ds`.`display_id` = `d`.`id`
         AND ds.substance_id = 2 
         GROUP BY `ds`.`display_id`
     ), 'Not Listed'
) `substances` FROM displays `d`;

The background to this is that I have 2 tables, displays and display_substances. I want to render a table of every row in displays and the corresponding values in display_substances for a given substance ID (represented by ds.substance_id = 2 in the query above).

Not every row in displays has a corresponding display_substances value. If this is the case it should output the words “Not Listed”. This effectively means that if there is no corresponding row in display_substances – then there is no data for that display.id – therefore the data is “Not Listed” in the database for that particular row. The query above does exactly this.

I want to be able to write my query using CakePHP’s ORM syntax.

The structures for the tables are as follows.

mysql> DESCRIBE displays;
+----------+----------------------+------+-----+---------+----------------+
| Field    | Type                 | Null | Key | Default | Extra          |
+----------+----------------------+------+-----+---------+----------------+
| id       | smallint(5) unsigned | NO   | PRI | NULL    | auto_increment |
| label    | varchar(255)         | NO   |     | NULL    |                |
+----------+----------------------+------+-----+---------+----------------+

mysql> DESCRIBE display_substances;
+--------------+-----------------------+------+-----+---------+----------------+
| Field        | Type                  | Null | Key | Default | Extra          |
+--------------+-----------------------+------+-----+---------+----------------+
| id           | mediumint(8) unsigned | NO   | PRI | NULL    | auto_increment |
| display_id   | smallint(5) unsigned  | NO   | MUL | NULL    |                |
| substance_id | mediumint(8) unsigned | NO   | MUL | NULL    |                |
| value        | text                  | NO   |     | NULL    |                |
+--------------+-----------------------+------+-----+---------+----------------+

The Table classes exist with the appropriate relationships defined.

// src/Model/Table/DisplaysTable.php
$this->hasMany('DisplaySubstances', [
    'foreignKey' => 'display_id'
]);

// src/Model/Table/DisplaysSubstancesTable.php
$this->belongsTo('Displays', [
   'foreignKey' => 'display_id',
   'joinType' => 'INNER'
]);

So far I have this:

    $substance_id = 2;

    $data = 
        $DisplaySubstances->find()
        ->contain(['Displays'])
        ->where(['DisplaySubstances.substance_id' => $substance_id)
        ->select(['Displays.id', 'Displays.label', 'Displays.anchor', 'DisplaySubstances.value'])
        ->enableHydration(false)->toArray();

This will get me the rows in displays which have corresponding values in display_substances for substance ID 2. This effectively is everything where we have data, but doesn’t include any rows which are “Not Listed”.

I don’t know how to write the IFNULL...GROUP_CONCAT part of my vanilla SQL query using Cake’s ORM syntax.

I’ve read How to use group_contact in cakephp query? so can see that it’s possible to use GROUP_CONCAT in the ->select() condition. But the linked example is a lot more simple because it doesn’t use an IFNULL condition and the corresponding action (returning “Not Listed”).

Please can someone advise how to write this in Cake’s ORM syntax, if this is possible?

CakePHP version is 3.5.18

How to solve:

IFNULL and GROUP_CONCAT are both SQL functions, so you’d use the functions builder, which can be used to create any function call you want, its magic call handler will create a generic function call if there’s no concrete implementation for the invoked method, ie $functionsBuilder->IFNULL() and $functionsBuilder->GROUP_CONCAT() will just work.

The functions builder also accepts expressions, so you can pass another query object to your IFNULL() call, ie the subquery in your example SQL.

$subquery = $DisplaySubstances->find()
    ->select(function (\Cake\ORM\Query $query) {
        return [
            $query->func()->GROUP_CONCAT(['value' => 'identifier'])
        ];
    })
    ->where(function (\Cake\Database\Expression\QueryExpression $exp) use ($substance_id) {
        return [
            $exp->equalFields('DisplaySubstances.display_id', 'Displays.id'),
            'DisplaySubstances.substance_id' => $substance_id
        ];
    })
    ->group('DisplaySubstances.display_id');

$query = $Displays->find()
    ->select(function (\Cake\ORM\Query $query) use ($subquery) {
        return [
            'substances' => $query->func()->IFNULL([$subquery, 'Not Listed'])
        ];
    })
    ->select($Displays);

See also

Leave a Reply

Your email address will not be published. Required fields are marked *