Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Join
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
6 / 6
27
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getForeignTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumns
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getJoin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
17
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Db\Sql;
16
17use Pop\Db\Sql\Parser\Expression;
18use Pop\Db\Sql\Parser\Operator;
19
20/**
21 * Join class
22 *
23 * @category   Pop
24 * @package    Pop\Db
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    7.0.0
29 */
30class Join
31{
32
33    /**
34     * Allowed JOIN keywords
35     * @var array
36     */
37    protected static $allowedJoins = [
38        'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'FULL JOIN',
39        'OUTER JOIN', 'LEFT OUTER JOIN', 'RIGHT OUTER JOIN', 'FULL OUTER JOIN',
40        'INNER JOIN', 'LEFT INNER JOIN', 'RIGHT INNER JOIN', 'FULL INNER JOIN'
41    ];
42
43    /**
44     * SQL object
45     * @var ?AbstractSql
46     */
47    protected ?AbstractSql $sql = null;
48
49    /**
50     * Foreign table
51     * @var ?string
52     */
53    protected ?string $foreignTable = null;
54
55    /**
56     * Columns
57     * @var array
58     */
59    protected array $columns = [];
60
61    /**
62     * Join type
63     * @var string
64     */
65    protected string $join = 'JOIN';
66
67    /**
68     * Constructor
69     *
70     * Instantiate the JOIN object
71     *
72     * @param  AbstractSql $sql
73     * @param  mixed       $foreignTable
74     * @param  array       $columns
75     * @param  string      $join
76     * @throws Exception
77     */
78    public function __construct(AbstractSql $sql, mixed $foreignTable, array $columns, string $join = 'JOIN')
79    {
80        $this->sql = $sql;
81
82        // If it's a sub-select
83        if (($foreignTable instanceof Select) || ($foreignTable instanceof \Pop\Db\Sql)) {
84            $this->foreignTable = (string)$foreignTable;
85        } else if (is_array($foreignTable)) {
86            if (count($foreignTable) !== 1) {
87                throw new Exception('Error: Only one table can be used in JOIN clause.');
88            }
89            $alias = array_key_first($foreignTable);
90            $table = $foreignTable[$alias];
91            $this->foreignTable = $this->sql->quoteId($table) . ' AS ' . $this->sql->quoteId($alias);
92        } else {
93            $this->foreignTable = $this->sql->quoteId($foreignTable);
94        }
95
96        $this->columns = $columns;
97        $this->join    = (in_array(strtoupper($join), self::$allowedJoins)) ? strtoupper($join) : 'JOIN';
98    }
99
100    /**
101     * Get foreign table
102     *
103     * @return string
104     */
105    public function getForeignTable(): string
106    {
107        return $this->foreignTable;
108    }
109
110    /**
111     * Get columns
112     *
113     * @return array
114     */
115    public function getColumns(): array
116    {
117        return $this->columns;
118    }
119
120    /**
121     * Get JOIN type
122     *
123     * @return string
124     */
125    public function getJoin(): string
126    {
127        return $this->join;
128    }
129
130    /**
131     * Render JOIN
132     *
133     * @return string
134     */
135    public function render(): string
136    {
137        $columns = [];
138
139        foreach ($this->columns as $column1 => $column2) {
140            if (Expression::isShorthand($column1)) {
141                ['column' => $column1, 'operator' => $operator] = Operator::parse($column1);
142                if (($column2 === null) && ($operator == 'NOT')) {
143                    $operator = 'IS ' . $operator;
144                }
145            } else {
146                if (is_string($column2) && str_starts_with($column2, '(')) {
147                    $operator = 'IN';
148                } else {
149                    $operator = ($column2 === null) ? 'IS' : '=';
150                }
151            }
152            $operator = ' ' . $operator . ' ';
153
154            if (is_array($column2)) {
155                foreach ($column2 as $c) {
156                    if ($c === null) {
157                        $c = 'NULL';
158                    } else if (is_string($c) && str_contains($c, '.')) {
159                        $c = $this->sql->quoteId($c);
160                    }
161                    $columns[] = ((str_contains($column1, '.')) ? $this->sql->quoteId($column1) : $column1) . $operator . $c;
162                }
163            } else {
164                if ($column2 === null) {
165                    $column2 = 'NULL';
166                } else if (($operator != ' IN ') && is_string($column2)) {
167                    $column2 = $this->sql->quoteId($column2);
168                }
169                $columns[] = $this->sql->quoteId($column1) . $operator . $column2;
170            }
171        }
172
173        return $this->join . ' ' . $this->foreignTable . ' ON ((' . implode(') AND (', $columns) . '))';
174    }
175
176    /**
177     * Return JOIN as string
178     *
179     * @return string
180     */
181    public function __toString(): string
182    {
183        return $this->render();
184    }
185
186}