Yii Advanced Filters Extension
  • Package
  • Class

Packages

  • advancedfilters
    • components
    • dbhelpers
    • filters

Classes

  • AfBaseFilter
  • AfDefaultFilter
  • AfExactFilter
  • AfRangeFilter
  • AfRegexFilter
  • AfSubstringFilter
  1 <?php
  2 
  3 /**
  4  * AfParser class file.
  5  * 
  6  * @author Keith Burton <kburton@kappasoft.net>
  7  * @package advancedfilters.components
  8  */
  9 
 10 /**
 11  * This class is responsible for parsing the entered expression into segments
 12  * split by the "and" and "or" delimiters, and determining whether logic should
 13  * be inverted.
 14  * 
 15  * The resulting segments are passed to AfBaseFilter to construct an
 16  * appropriate filter object.
 17  */
 18 class AfParser extends CComponent
 19 {
 20     /**
 21      * @var string the string used to 'or' filter expressions together.
 22      * Set this to an empty string to remove this functionality.
 23      */
 24     public $orDelimiter = '|';
 25     
 26     /**
 27      * @var string the string used to 'and' filter expressions together.
 28      * Set this to an empty string to remove this functionality.
 29      */
 30     public $andDelimiter = '&';
 31     
 32     /**
 33      * @var string the string which can be prepended to a delimiter string to
 34      * allow its use within a filter expression.
 35      * Set this to an empty string to disallow escaping.
 36      */
 37     public $escapeSequence = '\\';
 38     
 39     /**
 40      * @var string the string which can be prepended to a filter expression in
 41      * order to invert its logic and return the opposite results.
 42      * You can specify values for both $invertLogicPrefix and
 43      * $invertLogicSuffix to require that the expression be enclosed
 44      * between two specific strings in order to invert the logic.
 45      * Set both to an empty string to prevent logic inversion.
 46      */
 47     public $invertLogicPrefix = '!';
 48     
 49     /**
 50      * @var string the string which can be appended to a filter expression in
 51      * order to invert its logic and return the opposite results.
 52      * You can specify values for both $invertLogicPrefix and
 53      * $invertLogicSuffix to require that the expression be enclosed
 54      * between two specific strings in order to invert the logic.
 55      * Set both to an empty string to prevent logic inversion.
 56      */
 57     public $invertLogicSuffix = '';
 58     
 59     /**
 60      * @var CDbConnection|string either a CDbConnection object or the string
 61      * name of an application component representing a CDbConnection.
 62      * Defaults to 'db'.
 63      */
 64     public $dbConnection = 'db';
 65     
 66     /**
 67      * @var array mapping between PDO driver and database helper class path.
 68      * Each database helper must extend AfBaseDbHelper.
 69      * If the $dbConnection has a driver name that is not specified in this
 70      * array, or it maps to null, an AfException will be thrown.
 71      */
 72     public $driverMap = array(
 73         'pgsql'=>null,               // PostgreSQL
 74         'mysqli'=>'AfMysqlDbHelper', // MySQL
 75         'mysql'=>'AfMysqlDbHelper',  // MySQL
 76         'sqlite'=>null,              // sqlite 3
 77         'sqlite2'=>null,             // sqlite 2
 78         'mssql'=>'AfMssqlDbHelper',  // Mssql driver on windows
 79         'dblib'=>'AfMssqlDbHelper',  // dblib drivers on linux
 80         'sqlsrv'=>'AfMssqlDbHelper', // Mssql
 81         'oci'=>null,                 // Oracle driver
 82     );
 83     
 84     /**
 85      * @var array the default filters to load, and the priorities of each.
 86      * Lower priority values mean that the pattern will be tested against the
 87      * filter earlier, so more specific filters should be given a lower number
 88      * than more general filters.
 89      * Override the 'active' property to specify whether each filter should be
 90      * used when processing filter expressions. The default filter cannot be
 91      * deactivated.
 92      * Any additional configuration will be applied to the specific filter class
 93      * when it is instantiated.
 94      */
 95     public $filterConfig = array(
 96         'range'=>array(
 97             'class'=>'AfRangeFilter',
 98             'priority'=>10,
 99         ),
100         'exact'=>array(
101             'class'=>'AfExactFilter',
102             'priority'=>20,
103         ),
104         'substring'=>array(
105             'class'=>'AfSubstringFilter',
106             'priority'=>30,
107         ),
108         'regex'=>array(
109             'class'=>'AfRegexFilter',
110             'priority'=>40,
111         ),
112         'default'=>array(
113             'class'=>'AfDefaultFilter',
114             'priority'=>50,
115         ),
116     );
117     
118     /**
119      * @var array Holds a two level array of "anded" filters grouped inside
120      * "ored" filters.
121      */
122     private $filters = array();
123     
124     /**
125      * Parses the provided filter expression into segments and creates an array
126      * of filters to process the full expression.
127      * 
128      * This class should not be instantiated directly, but by using methods
129      * of the AdvancedFilters application component.
130      * 
131      * @param string $columnExpression the disambiguated column name (or a
132      * valid SQL expression).
133      * @param string $filterExpression the entered filter expression.
134      * @param array $config the configuration array.
135      */
136     public function __construct($columnExpression, $filterExpression, $config)
137     {
138         $this->applyConfig($config);
139         
140         // Get concrete database connection and helper objects
141         $dbConnection = $this->getDbConnectionObject();
142         $dbHelper = $this->getDbHelper();
143         
144         $filterExpressionParts = $this->tokenize(trim($filterExpression));
145         
146         foreach ($filterExpressionParts as $filterExpressionPartOr)
147         {
148             $andFilters = array();
149             
150             foreach ($filterExpressionPartOr as $filterExpressionPartAnd)
151             {
152                 $invertLogic = $this->processLogicInversion(
153                         $filterExpressionPartAnd);
154                 
155                 $andFilters[] = AfBaseFilter::createFilter(
156                         $columnExpression, $filterExpressionPartAnd,
157                         $invertLogic, $dbConnection, $dbHelper,
158                         $this->filterConfig);
159             }
160             
161             $this->filters[] = $andFilters;
162         }
163     }
164     
165     /**
166      * Returns a CDbCriteria object created by merging the criteria returned
167      * from all filters.
168      * 
169      * @return CDbCriteria the criteria object.
170      */
171     public function getCriteria()
172     {
173         $orCriteria = new CDbCriteria;
174         
175         foreach ($this->filters as $andFilters)
176         {
177             $andCriteria = new CDbCriteria;
178             
179             foreach ($andFilters as $andFilter)
180                 $andCriteria->mergeWith($andFilter->getCriteria(), 'AND');
181             
182             $orCriteria->mergeWith($andCriteria, 'OR');
183         }
184         
185         return $orCriteria;
186     }
187     
188     /**
189      * Updates this object's properties from a configuration array.
190      * 
191      * @param array $config the configuration array.
192      */
193     private function applyConfig($config)
194     {
195         if (isset($config['orDelimiter']))
196             $this->orDelimiter = $config['orDelimiter'];
197         
198         if (isset($config['andDelimiter']))
199             $this->andDelimiter = $config['andDelimiter'];
200         
201         if (isset($config['invertLogicPrefix']))
202             $this->invertLogicPrefix = $config['invertLogicPrefix'];
203         
204         if (isset($config['invertLogicSuffix']))
205             $this->invertLogicSuffix = $config['invertLogicSuffix'];
206         
207         if (isset($config['escapeSequence']))
208             $this->escapeSequence = $config['escapeSequence'];
209         
210         if (isset($config['dbConnection']))
211             $this->dbConnection = $config['dbConnection'];
212         
213         if (isset($config['driverMap']))
214             $this->driverMap = CMap::mergeArray($this->driverMap,
215                     $config['driverMap']);
216         
217         if (isset($config['filterConfig']))
218             $this->filterConfig = CMap::mergeArray($this->filterConfig,
219                     $config['filterConfig']);
220     }
221     
222     /**
223      * Split the provided string on the $orDelimiter and $andDelimiter
224      * characters, removing whitespace on either side of the delimiters.
225      * $escapeSequence, $orDelimiter and $andDelimiter strings can be
226      * escaped with a preceding $escapeSequence string.
227      * 
228      * @param string $filterExpression the expression to split.
229      * @return array a two level array of "and" patterns grouped inside "or"
230      * patterns.
231      */
232     private function tokenize($filterExpression)
233     {
234         // A two level array of "anded" expressions grouped inside "ored"
235         // expressions
236         $tokens = array();
237         
238         // First split on unescaped $orDelimiter characters as "or" operations
239         // have lower precedence than "and" operations
240         $orSegments = $this->splitOnDelimiter($filterExpression,
241                 $this->orDelimiter, $this->escapeSequence);
242         
243         foreach ($orSegments as $orSegment)
244         {
245             // Now split each "or" segment on unescaped $andDelimiter characters
246             $andSegments = $this->splitOnDelimiter($orSegment,
247                     $this->andDelimiter, $this->escapeSequence, true);
248             
249             // Only update the tokens array if non-empty segments are found
250             if (count($andSegments))
251                 $tokens[] = $andSegments;
252         }
253         
254         return $tokens;
255     }
256     
257     /**
258      * Split a string by unescaped delimiters, trimming each segment.
259      * 
260      * @param string $string the string to split.
261      * @param string $delimiter the delimiter string. If this is an empty
262      * string, a single element array will be returned containing the original
263      * string trimmed.
264      * @param string $escapeSequence the string which can be prepended to the
265      * delimiter to prevent splitting. The escape sequence should be doubled up
266      * to prevent escaping of the delimiter. If this is an empty string, no
267      * escaping will be performed.
268      * @param boolean $removeEscapeSequences whether to remove escape sequences
269      * from the string. Escape sequences before a delimiter will always be
270      * removed. This should only be set to true when splitting a string segment
271      * for the final time and will transform doubled escape sequences into a
272      * single escape sequence, and remove the escape sequence in scenarios where
273      * it is not valid.
274      * @return array the delimiter separated string segments.
275      */
276     private function splitOnDelimiter($string, $delimiter, $escapeSequence,
277             $removeEscapeSequences = false)
278     {
279         // If the delimiter is empty, trim and return the original string
280         if ($delimiter === '')
281             return array(trim($string));
282         
283         $delimiter = strtolower($delimiter);
284         $escapeSequence = strtolower($escapeSequence);
285         
286         $delimLength = strlen($delimiter);
287         $escLength = strlen($escapeSequence);
288         $stringLength = strlen($string);
289         
290         $segments = array();
291         $currentPos = 0;
292         $extracted = '';
293         
294         while ($currentPos < $stringLength)
295         {
296             // Only deal with escapes if the escape character is not empty
297             if ($escapeSequence !== '')
298             {
299                 // Check for escape character first
300                 $substr = substr($string, $currentPos, $escLength);
301 
302                 if (strtolower($substr) === $escapeSequence)
303                 {
304                     // Check for an escaped escape character
305                     $substr2 = substr($string, $currentPos + $escLength,
306                             $escLength);
307 
308                     if (strtolower($substr2) === $escapeSequence)
309                     {
310                         // Only add the first escape if we're not removing escape
311                         // characters
312                         if (!$removeEscapeSequences)
313                             $extracted .= $substr;
314 
315                         // Add the escaped character and update position
316                         $extracted .= $substr2;
317                         $currentPos += $escLength * 2;
318                         continue;
319                     }
320 
321                     // If we have an escaped delimiter, just add the delimiter
322                     $substr = substr($string, $currentPos + $escLength,
323                             $delimLength);
324 
325                     if (strtolower($substr) === $delimiter)
326                     {
327                         $extracted .= $substr;
328                         $currentPos += $escLength + $delimLength;
329                         continue;
330                     }
331 
332                     // If it's an unknown escape and we're removing escape
333                     // characters, skip this escape sequence
334                     if ($removeEscapeSequences)
335                     {
336                         $currentPos += $escLength;
337                         continue;
338                     }
339                 }
340             }
341             
342             // Now check for the delimiter
343             $substr = substr($string, $currentPos, $delimLength);
344             
345             // If we're at a delimiter, add the extracted string to the array
346             if (strtolower($substr) === $delimiter)
347             {
348                 $extracted = trim($extracted);
349                 
350                 // Only add non-empty segments
351                 if ($extracted !== '')
352                     $segments[] = $extracted;
353                 
354                 // Update positions and clear extracted string
355                 $extracted = '';
356                 $currentPos += $delimLength;
357                 continue;
358             }
359             
360             // If there are no escapes or delimiters to deal with, append the
361             // next character to the extracted string and update positions.
362             $extracted .= substr($string, $currentPos, 1);
363             $currentPos++;
364         }
365         
366         $extracted = trim($extracted);
367         
368         // Add final extracted string to array if it is not empty
369         if ($extracted !== '')
370             $segments[] = $extracted;
371         
372         return $segments;
373     }
374     
375     /**
376      * Analyses a filter expression segment to see if it is surrounded by a
377      * logic inversion prefix and suffix. If it is, the prefix and suffix and
378      * any whitespace are trimmed from the string and true is returned.
379      * Otherwise, false is returned.
380      * 
381      * @param string $filterExpression a segment which may or may not be
382      * surrounded by a logic inversion prefix and suffix. It is received by
383      * reference and will be stripped and trimmed in place if the prefix and
384      * suffix are found.
385      * @return boolean whether the condition logic should be inverted.
386      */
387     private function processLogicInversion(&$filterExpression)
388     {
389         // If logic inversion is disabled, return early
390         if ($this->invertLogicPrefix === '' && $this->invertLogicSuffix === '')
391             return false;
392         
393         $prefix = strtolower($this->invertLogicPrefix);
394         $suffix = strtolower($this->invertLogicSuffix);
395         $string = $filterExpression;
396         
397         $prefixLength = strlen($prefix);
398         $suffixLength = strlen($suffix);
399         $stringLength = strlen($filterExpression);
400         
401         // No inversion if the string is shorter than the prefix and suffix
402         if ($stringLength < $prefixLength + $suffixLength)
403             return false;
404         
405         // No inversion if the prefix doesn't match
406         if (strtolower(substr($string, 0, $prefixLength)) !== $prefix)
407             return false;
408         
409         // No inversion if the suffix doesn't match
410         if (strtolower(substr($string, -$suffixLength, $suffixLength))
411                 !== $suffix)
412             return false;
413         
414         // Inversion has been requested, now strip the filter expression
415         $filterExpression = $stringLength === $prefixLength + $suffixLength ? ''
416                 : trim(substr($string, $prefixLength,
417                         $stringLength - $prefixLength - $suffixLength));
418         
419         return true;
420     }
421     
422     /**
423      * @var CDbConnection caches the connection object for the life of this
424      * AfParser object.
425      */
426     private $_dbConnectionObjectCache;
427     
428     /**
429      * Returns an instance of CDbConnection determined by the $dbConnection
430      * property.
431      * 
432      * @return CDbConnection the connection object.
433      */
434     private function getDbConnectionObject()
435     {
436         if ($this->_dbConnectionObjectCache === null)
437         {
438             $dbConnection = $this->dbConnection;
439 
440             if (is_string($dbConnection))
441                 $dbConnection = Yii::app()->$dbConnection;
442             
443             $this->_dbConnectionObjectCache = $dbConnection;
444         }
445 
446         return $this->_dbConnectionObjectCache;
447     }
448     
449     /**
450      * @var array maps a class path to a concrete instance of a subclass of
451      * AfBaseDbHelper. The classes contain no state and are shared amongst all
452      * instances of AfParser.
453      */
454     private static $_dbHelperCache = array();
455     
456     /**
457      * Gets a database helper object to allow database specific syntax to be
458      * used in criteria conditions.
459      * Database helpers are cached and shared amongst all instances of AfParser.
460      * 
461      * @return AfBaseDbHelper the database helper object.
462      * @throws AfException if no database helper is available for the database
463      * connection driver.
464      */
465     private function getDbHelper()
466     {
467         $dbConnection = $this->getDbConnectionObject();
468         $driverName = $dbConnection->getDriverName();
469         
470         // Throw an exception if no helper is available for this driver name
471         if (!isset($this->driverMap[$driverName]))
472         {
473             throw new AfException(
474                     "No database helper available for driver '$driverName'.");
475         }
476         
477         // Get the required helper class path
478         $dbHelperClassPath = $this->driverMap[$driverName];
479         
480         // If the cache doesn't yet contain this database helper, add it
481         if (!isset(self::$_dbHelperCache[$dbHelperClassPath]))
482         {
483             self::$_dbHelperCache[$dbHelperClassPath]
484                     = Yii::createComponent($dbHelperClassPath);
485         }
486         
487         // Return the cached helper object
488         return self::$_dbHelperCache[$dbHelperClassPath];
489     }
490 }
491 
Yii Advanced Filters Extension API documentation generated by ApiGen