http://phing.info/

Source Code Coverage

Designed for use with PHPUnit2, Xdebug and Phing.

Methods: 10 LOC: 375 Statements: 200

Source file Statements Methods Total coverage
Parser.php 97.0% 100.0% 97.1%
   
1
<?php
2
/**
3
 * Xyster Framework
4
 *
5
 * This source file is subject to the new BSD license that is bundled
6
 * with this package in the file LICENSE.txt.
7
 * It is also available through the world-wide-web at this URL:
8
 * http://www.opensource.org/licenses/bsd-license.php
9
 *
10
 * @category  Xyster
11
 * @package   Xyster_Orm
12
 * @copyright Copyright (c) 2007-2008 Irrational Logic (http://irrationallogic.net)
13
 * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
14
 * @version   $Id: Parser.php 220 2008-02-09 18:04:52Z doublecompile $
15
 */
16
/**
17
 * @see Xyster_Orm_Xsql
18
 */
19 1
require_once 'Xyster/Orm/Xsql.php';
20
/**
21
 * Parses an XSQL string into a Xyster_Orm_Query object
22
 *
23
 * @category  Xyster
24
 * @package   Xyster_Orm
25
 * @copyright Copyright (c) 2007-2008 Irrational Logic (http://irrationallogic.net)
26
 * @license   http://www.opensource.org/licenses/bsd-license.php New BSD License
27
 */
28
class Xyster_Orm_Query_Parser
29
{
30
    /**
31
     * Parse a statement into a Xyster_Data_Criterion
32
     *
33
     * @param string $statement
34
     * @return Xyster_Data_Criterion
35
     */
36
    public function parseCriterion( $statement )
37
    {
38 119
        require_once 'Xyster/Data/Junction.php';
39
40 119
        $crit = null;
41 119
        $statement = trim($statement);
42
43 119
        $groups = Xyster_Orm_Xsql::matchGroups($statement);
44 119
        if ( count($groups) == 1 && strlen($groups[0]) == strlen($statement) ) {
45 55
            $statement = trim(substr($statement, 1, -1));
46 55
        }
47
48 119
        $crits = Xyster_Orm_Xsql_Split::Custom(' AND ')->split($statement, true);
49
        // in case it split the and of a "BETWEEN x AND y"
50 119
        if ( count($crits) == 2 && Xyster_Orm_Xsql::isLiteral($crits[1]) ) {
51 1
            $crits = array($statement);
52 1
        }
53
54 119
        if ( count($crits) < 2 ) {
55 119
            $subcrits = Xyster_Orm_Xsql_Split::Custom(' OR ')->split($statement, true);
56 119
            if ( count($subcrits) < 2 ) {
57 119
                $groups = Xyster_Orm_Xsql::matchGroups(trim($subcrits[0]));
58 119
                $crit = ( count($groups) > 0 && strlen($groups[0]) ) ?
59 2
                    $this->parseCriterion($subcrits[0]) :
60 119
                    $this->parseExpression($subcrits[0]);
61 119
            } else {
62 2
                $criteria = array_map(array($this, 'parseCriterion'), $subcrits);
63 2
                $crit = Xyster_Data_Criterion::fromArray('OR', $criteria);
64
            }
65
66 119
        } else {
67
68 5
            $crit = Xyster_Data_Junction::all($this->parseCriterion($crits[0]),
69 5
                $this->parseCriterion($crits[1]));
70
71 5
            if ( count($crits) > 2 ) {
72 1
                for ( $i=2; $i<count($crits); $i++ ) {
73 1
                    $crit->add($this->parseCriterion($crits[$i]));
74 1
                }
75 1
            }
76
        }
77
78 119
        return $crit;
79
    }
80
81
    /**
82
     * Parse string statement as Expression
83
     *
84
     * @param string $statement
85
     * @return Xyster_Data_Expression
86
     * @throws Xyster_Orm_Query_Parser_Exception if the expression syntax is incorrect
87
     */
88
    public function parseExpression( $statement )
89
    {
90 124
        require_once 'Xyster/Data/Expression.php';
91
92
        // remove whitespace characters we don't like
93 124
        $statement = Xyster_Orm_Xsql::splitSpace(preg_replace("/[\t\n\r]+/", " ", trim($statement)));
94
95 124
        $exp = array();
96 124
        foreach( $statement as $v ) {
97 124
            if( $v != "" ) {
98 124
                $exp[] = $v;
99 124
            }
100 124
        }
101
102 124
        $leftlit = $this->parseField($exp[0]);
103
104 124
        array_shift($exp);
105 124
        $upper0 = strtoupper($exp[0]);
106 124
        $upper1 = strtoupper($exp[1]);
107
108 124
        if ( $upper0 == "NOT" || ( $upper0 == "IS" && $upper1 == "NOT" ) ) {
109 2
            $operator = $upper0 . " " . $upper1;
110 2
            array_shift($exp);
111 2
        } else {
112 123
            $operator = $upper0;
113
        }
114
115 124
        require_once 'Xyster/Enum.php';
116 124
        if ( !in_array($operator, array_keys(Xyster_Enum::values('Xyster_Data_Operator_Expression'))) ) {
117 1
            require_once 'Xyster/Orm/Query/Parser/Exception.php';
118 1
            throw new Xyster_Orm_Query_Parser_Exception('Invalid expression operator: ' . $operator);
119 0
        }
120
121 123
        array_shift($exp);
122
123 123
        if ( ( $operator == "BETWEEN" || $operator == "NOT BETWEEN" ) && count($exp) != 3 ) {
124 1
            require_once 'Xyster/Orm/Query/Parser/Exception.php';
125 1
            throw new Xyster_Orm_Query_Parser_Exception('Invalid literal: ' . implode(" ",$exp));
126 0
        }
127
128 122
        if ( $operator == "IN" || $operator == "NOT IN" ) {
129 2
            $rightlit = trim(implode(' ', $exp));
130 2
            $matches = array();
131 2
            if ( !preg_match('/^\([\s]*(?P<choices>.*)[\s]*\)$/', $rightlit, $matches) ) {
132 1
                require_once 'Xyster/Orm/Query/Parser/Exception.php';
133 1
                throw new Xyster_Orm_Query_Parser_Exception('Invalid literal: ' . $rightlit);
134 0
            } else {
135 1
            	$inChoices = Xyster_Orm_Xsql::splitComma($matches['choices']);
136 1
                foreach( $inChoices as $k=>$choice ) {
137 1
                    $choice = trim($choice);
138 1
                    $this->_checkLiteral($choice);
139 1
                    if ( preg_match('/^"[^"]*"$/i', $choice) )
140 1
                        $inChoices[$k] = substr($choice, 1, -1);
141 1
                }
142 1
                $rightlit = $inChoices;
143
            }
144 1
        } else {
145 120
            $rightlit = ( $operator == "BETWEEN" || $operator == "NOT BETWEEN" ) ?
146 120
                array( $exp[0], $exp[2] ) : $exp[0];
147 120
            $this->_checkLiteral($rightlit);
148
        }
149
150 120
        if ( !is_array($rightlit) && preg_match('/^"[^"]*"$/i', $rightlit) ) {
151 7
            $rightlit = substr($rightlit, 1, -1);
152 7
        }
153
154 120
        $args = array( $leftlit );
155 120
        if ( in_array($operator, array("=","<>",">",">=","<","<=","LIKE","NOT LIKE",'IN','NOT IN')) ) {
156 120
            $args[] = $rightlit;
157 120
        } else if ( $operator == "BETWEEN" || $operator == "NOT BETWEEN" ) {
158 1
            $args[] = $rightlit[0];
159 1
            $args[] = $rightlit[1];
160 1
        }
161
162 120
        return call_user_func_array(array("Xyster_Data_Expression",
163 120
            Xyster_Data_Expression::getMethodName($operator)), $args);
164
    }
165
166
    /**
167
     * Parse a string as Field
168
     *
169
     * @param string $name
170
     * @return Xyster_Data_Field
171
     */
172
    public function parseField( $name )
173
    {
174 127
        $name = trim($name);
175
176 127
        require_once 'Xyster/Data/Field/Aggregate.php';
177 127
        $match = Xyster_Data_Field_Aggregate::match($name);
178
179 127
        if ( $match ) {
180 3
            require_once 'Xyster/Enum.php';
181 3
            $function = Xyster_Enum::valueOf('Xyster_Data_Aggregate', strtoupper($match['function']));
182 3
            $field = Xyster_Data_Field::aggregate($function, trim($match['field']));
183 3
        } else {
184 126
            $field = Xyster_Data_Field::named($name);
185
        }
186
187 127
        return $field;
188
    }
189
190
    /**
191
     * Parse a string as Field with alias
192
     *
193
     * @param string $name
194
     * @return Xyster_Data_Field
195
     */
196
    public function parseFieldAlias( $statement )
197
    {
198 3
        $statement = trim($statement);
199 3
        $matches = array();
200 3
        $alias = $statement;
201 3
        $pattern = '/[\s]+(AS[\s]+(?P<aliasA>[a-z0-9_]+)|"(?P<aliasQ>[a-z0-9_]+)")/i';
202
203 3
        if (preg_match($pattern, $statement, $matches)) {
204 2
            if (!empty($matches['aliasA']) || !empty($matches['aliasQ'])) {
205 2
                $alias = (!empty($matches['aliasA'])) ?
206 2
                    $matches['aliasA'] : $matches['aliasQ'];
207 2
            }
208 2
            $statement = str_replace($matches[0], "", $statement);
209 2
        }
210
211 3
        $field = $this->parseField($statement);
212 3
        $field->setAlias($alias);
213 3
        return $field;
214
    }
215
216
    /**
217
     * Parses string statement as Xyster_Orm_Query
218
     *
219
     * @param Xyster_Orm_Query $query
220
     * @param string $statement
221
     */
222
    public function parseQuery( Xyster_Orm_Query $query, $statement )
223
    {
224 2
        $expecting = array('where', 'order');
225
226 2
        $parts = $this->_baseParseQuery($query, $statement, $expecting);
227 2
        if ( !empty($parts['where']) ) {
228 2
            $query->where($this->parseCriterion($parts['where']));
229 2
        }
230 2
        if ( !empty($parts['order']) ) {
231 1
            $this->_parseClause($query, 'order', $parts['order']);
232 1
        }
233
    }
234
235
    /**
236
     * Parses string statement as Xyster_Orm_Query_Report
237
     *
238
     * @param Xyster_Orm_Query_Report $query
239
     * @param string $statement
240
     */
241
    public function parseReportQuery( Xyster_Orm_Query_Report $query, $statement )
242
    {
243 3
        $expecting = array('select', 'where', 'group', 'having', 'order');
244
245 3
        $parts = $this->_baseParseQuery($query, $statement, $expecting);
246 3
        if ( empty($parts['select']) ) {
247 1
            require_once 'Xyster/Orm/Query/Parser/Exception.php';
248 1
            throw new Xyster_Orm_Query_Parser_Exception('Invalid statement: ' . $statement);
249 0
        }
250
251 2
        $matches = array();
252 2
        if (preg_match('/^distinct[[:space:]]+/i', $parts['select'], $matches)) {
253 1
            $query->distinct(true);
254 1
            $parts['select'] = str_replace($matches[0], '', $parts['select']);
255 1
        }
256
257 2
        $this->_parseClause($query, 'select', $parts['select']);
258 2
        if ( !empty($parts['where']) ) {
259 2
            $query->where($this->parseCriterion($parts['where']));
260 2
        }
261 2
        if ( !empty($parts['order']) ) {
262 1
            $this->_parseClause($query, 'order', $parts['order']);
263 1
        }
264 2
        if ( !empty($parts['group']) ) {
265 1
            $this->_parseClause($query, 'group', $parts['group']);
266 1
        }
267 2
        if ( !empty($parts['having']) ) {
268 1
            $query->having($this->parseCriterion($parts['having']));
269 1
        }
270
    }
271
272
    /**
273
     * Parse string statement as a {@link Xyster_Data_Sort}
274
     *
275
     * @param string $statement
276
     * @return Xyster_Data_Sort
277
     * @throws Xyster_Orm_Query_Parser_Exception  if the statement syntax is invalid
278
     */
279
    public function parseSort( $statement )
280
    {
281 3
        $statement = trim($statement);
282 3
        $matches = array();
283 3
        $dir = 'ASC';
284
285 3
        if ( preg_match('/\s+(?P<dir>ASC|DESC)$/i', $statement, $matches) ) {
286 3
            $dir = $matches["dir"];
287 3
            $statement = trim(str_replace($matches[0], "", $statement));
288 3
        }
289
290 3
        $field = $this->parseField($statement);
291 3
        return (!strcasecmp($dir, 'DESC')) ? $field->desc() : $field->asc();
292
    }
293
294
    /**
295
     * Asserts a literal for syntactical correctness
296
     *
297
     * @param string $lit
298
     * @throws Xyster_Orm_Query_Parser_Exception if the syntax is incorrect
299
     */
300
    protected function _checkLiteral( $lit )
301
    {
302 121
        if ( is_array($lit) ) {
303 1
            foreach( $lit as $v ) {
304 1
                $this->_checkLiteral($v);
305 1
            }
306 121
        } else if ( !Xyster_Orm_Xsql::isLiteral($lit) ) {
307 1
            require_once 'Xyster/Orm/Query/Parser/Exception.php';
308 1
            throw new Xyster_Orm_Query_Parser_Exception('Invalid literal: ' . $lit);
309 0
        }
310
    }
311
312
    /**
313
     * Removes the limit and offset clause from a statement
314
     *
315
     * @param Xyster_Orm_Query $query  The query into which the parts are set
316
     * @param string $statement  The statement to parse
317
     * @param array $expecting
318
     * @return array  The statement split by parts
319
     */
320
    protected function _baseParseQuery( Xyster_Orm_Query $query, $statement, array $expecting )
321
    {
322 4
        $matches = array();
323 4
        if ( preg_match('/[\s]*LIMIT (?P<limit>[\d]+)( OFFSET (?P<offset>[\d]+))?$/i',$statement,$matches) ) {
324 2
            $statement = str_replace($matches[0], '', $statement);
325 2
        }
326 4
        $limit = ( isset($matches['limit']) ) ? $matches['limit'] : 0;
327 4
        $offset = ( isset($matches['offset']) ) ? $matches['offset'] : 0;
328 4
        if ( $limit ) {
329 2
            $query->limit($limit, $offset);
330 2
        }
331
332 4
        $parts = array();
333 4
        $part = '';
334 4
        $split = Xyster_Orm_Xsql::splitSpace(trim($statement));
335
336 4
        foreach( $split as $v ) {
337 4
            if ( in_array($part, array('order','group')) && !strcasecmp($v, 'by') ) {
338 2
                continue;
339 0
            }
340 4
            foreach( $expecting as $epart ) {
341 4
                if ( !strcasecmp($v, $epart) ) {
342 3
                    $part = strtolower($v);
343 3
                }
344 4
            }
345 4
            if ( !in_array(strtolower($v), $expecting) ) {
346 4
                if ( !isset($parts[$part]) ) {
347 4
                    $parts[$part] = "";
348 4
                }
349 4
                $parts[$part] .= $v . " ";
350 4
            }
351 4
        }
352
353 4
        return $parts;
354
    }
355
356
    /**
357
     * Parses a string statement clause into its corresponding parts
358
     *
359
     * @param Xyster_Orm_Query $query The query into which the parts will be set
360
     * @param string $type  The type of clause (either select, group, or order)
361
     * @param string $statement  The actual clause to parse
362
     */
363
    protected function _parseClause( Xyster_Orm_Query $query, $type, $statement )
364
    {
365
        $call = array(
366 3
                'select'=>array($this, 'parseFieldAlias'),
367 3
                'order'=>array($this, 'parseSort'),
368 3
                'group'=>array('Xyster_Data_Field', 'group')
369 3
            );
370 3
        $method = array('select'=>'field', 'order'=>'order', 'group'=>'group');
371 3
        foreach( Xyster_Orm_Xsql::splitComma($statement) as $item ) {
372 3
            $query->{$method[$type]}( call_user_func($call[$type], $item) );
373 3
        }
374
    }
375
}


Report generated at 2008-03-05T18:27:43-05:00