1<?php
2
3/**
4 * `JOIN` keyword parser.
5 */
6
7namespace PhpMyAdmin\SqlParser\Components;
8
9use PhpMyAdmin\SqlParser\Component;
10use PhpMyAdmin\SqlParser\Parser;
11use PhpMyAdmin\SqlParser\Token;
12use PhpMyAdmin\SqlParser\TokensList;
13
14/**
15 * `JOIN` keyword parser.
16 *
17 * @category   Keywords
18 *
19 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
20 */
21class JoinKeyword extends Component
22{
23    /**
24     * Types of join.
25     *
26     * @var array
27     */
28    public static $JOINS = array(
29        'CROSS JOIN' => 'CROSS',
30        'FULL JOIN' => 'FULL',
31        'FULL OUTER JOIN' => 'FULL',
32        'INNER JOIN' => 'INNER',
33        'JOIN' => 'JOIN',
34        'LEFT JOIN' => 'LEFT',
35        'LEFT OUTER JOIN' => 'LEFT',
36        'RIGHT JOIN' => 'RIGHT',
37        'RIGHT OUTER JOIN' => 'RIGHT',
38        'NATURAL JOIN' => 'NATURAL',
39        'NATURAL LEFT JOIN' => 'NATURAL LEFT',
40        'NATURAL RIGHT JOIN' => 'NATURAL RIGHT',
41        'NATURAL LEFT OUTER JOIN' => 'NATURAL LEFT OUTER',
42        'NATURAL RIGHT OUTER JOIN' => 'NATURAL RIGHT OUTER',
43        'STRAIGHT_JOIN' => 'STRAIGHT'
44    );
45
46    /**
47     * Type of this join.
48     *
49     * @see static::$JOINS
50     *
51     * @var string
52     */
53    public $type;
54
55    /**
56     * Join expression.
57     *
58     * @var Expression
59     */
60    public $expr;
61
62    /**
63     * Join conditions.
64     *
65     * @var Condition[]
66     */
67    public $on;
68
69    /**
70     * Columns in Using clause.
71     *
72     * @var ArrayObj
73     */
74    public $using;
75
76    /**
77     * Constructor.
78     *
79     * @param string      $type  Join type
80     * @param Expression  $expr  join expression
81     * @param Condition[] $on    join conditions
82     * @param ArrayObj    $using columns joined
83     *
84     * @see JoinKeyword::$JOINS
85     */
86    public function __construct($type = null, $expr = null, $on = null, $using = null)
87    {
88        $this->type = $type;
89        $this->expr = $expr;
90        $this->on = $on;
91        $this->using = $using;
92    }
93
94    /**
95     * @param Parser     $parser  the parser that serves as context
96     * @param TokensList $list    the list of tokens that are being parsed
97     * @param array      $options parameters for parsing
98     *
99     * @return JoinKeyword[]
100     */
101    public static function parse(Parser $parser, TokensList $list, array $options = array())
102    {
103        $ret = array();
104
105        $expr = new self();
106
107        /**
108         * The state of the parser.
109         *
110         * Below are the states of the parser.
111         *
112         *      0 -----------------------[ JOIN ]----------------------> 1
113         *
114         *      1 -----------------------[ expr ]----------------------> 2
115         *
116         *      2 ------------------------[ ON ]-----------------------> 3
117         *      2 -----------------------[ USING ]---------------------> 4
118         *
119         *      3 --------------------[ conditions ]-------------------> 0
120         *
121         *      4 ----------------------[ columns ]--------------------> 0
122         *
123         * @var int
124         */
125        $state = 0;
126
127        // By design, the parser will parse first token after the keyword.
128        // In this case, the keyword must be analyzed too, in order to determine
129        // the type of this join.
130        if ($list->idx > 0) {
131            --$list->idx;
132        }
133
134        for (; $list->idx < $list->count; ++$list->idx) {
135            /**
136             * Token parsed at this moment.
137             *
138             * @var Token
139             */
140            $token = $list->tokens[$list->idx];
141
142            // End of statement.
143            if ($token->type === Token::TYPE_DELIMITER) {
144                break;
145            }
146
147            // Skipping whitespaces and comments.
148            if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
149                continue;
150            }
151
152            if ($state === 0) {
153                if (($token->type === Token::TYPE_KEYWORD)
154                    && ! empty(static::$JOINS[$token->keyword])
155                ) {
156                    $expr->type = static::$JOINS[$token->keyword];
157                    $state = 1;
158                } else {
159                    break;
160                }
161            } elseif ($state === 1) {
162                $expr->expr = Expression::parse($parser, $list, array('field' => 'table'));
163                $state = 2;
164            } elseif ($state === 2) {
165                if ($token->type === Token::TYPE_KEYWORD) {
166                    switch ($token->keyword) {
167                        case 'ON':
168                            $state = 3;
169                            break;
170                        case 'USING':
171                            $state = 4;
172                            break;
173                        default:
174                            if (! empty(static::$JOINS[$token->keyword])
175                            ) {
176                                $ret[] = $expr;
177                                $expr = new self();
178                                $expr->type = static::$JOINS[$token->keyword];
179                                $state = 1;
180                            } else {
181                                /* Next clause is starting */
182                                break 2;
183                            }
184                            break;
185                    }
186                }
187            } elseif ($state === 3) {
188                $expr->on = Condition::parse($parser, $list);
189                $ret[] = $expr;
190                $expr = new self();
191                $state = 0;
192            } elseif ($state === 4) {
193                $expr->using = ArrayObj::parse($parser, $list);
194                $ret[] = $expr;
195                $expr = new self();
196                $state = 0;
197            }
198        }
199
200        if (! empty($expr->type)) {
201            $ret[] = $expr;
202        }
203
204        --$list->idx;
205
206        return $ret;
207    }
208
209    /**
210     * @param JoinKeyword[] $component the component to be built
211     * @param array         $options   parameters for building
212     *
213     * @return string
214     */
215    public static function build($component, array $options = array())
216    {
217        $ret = array();
218        foreach ($component as $c) {
219            $ret[] = array_search($c->type, static::$JOINS) . ' ' . $c->expr
220                . (! empty($c->on)
221                    ? ' ON ' . Condition::build($c->on) : '')
222                . (! empty($c->using)
223                    ? ' USING ' . ArrayObj::build($c->using) : '');
224        }
225
226        return implode(' ', $ret);
227    }
228}
229