| <?php |
| |
| namespace LdapRecord\Query; |
| |
| use UnexpectedValueException; |
| |
| class Grammar |
| { |
| /** |
| * The query operators and their method names. |
| * |
| * @var array |
| */ |
| public $operators = [ |
| '*' => 'has', |
| '!*' => 'notHas', |
| '=' => 'equals', |
| '!' => 'doesNotEqual', |
| '!=' => 'doesNotEqual', |
| '>=' => 'greaterThanOrEquals', |
| '<=' => 'lessThanOrEquals', |
| '~=' => 'approximatelyEquals', |
| 'starts_with' => 'startsWith', |
| 'not_starts_with' => 'notStartsWith', |
| 'ends_with' => 'endsWith', |
| 'not_ends_with' => 'notEndsWith', |
| 'contains' => 'contains', |
| 'not_contains' => 'notContains', |
| ]; |
| |
| /** |
| * The query wrapper. |
| * |
| * @var string|null |
| */ |
| protected $wrapper; |
| |
| /** |
| * Get all the available operators. |
| * |
| * @return array |
| */ |
| public function getOperators() |
| { |
| return array_keys($this->operators); |
| } |
| |
| /** |
| * Wraps a query string in brackets. |
| * |
| * Produces: (query) |
| * |
| * @param string $query |
| * @param string $prefix |
| * @param string $suffix |
| * |
| * @return string |
| */ |
| public function wrap($query, $prefix = '(', $suffix = ')') |
| { |
| return $prefix.$query.$suffix; |
| } |
| |
| /** |
| * Compiles the Builder instance into an LDAP query string. |
| * |
| * @param Builder $query |
| * |
| * @return string |
| */ |
| public function compile(Builder $query) |
| { |
| if ($this->queryMustBeWrapped($query)) { |
| $this->wrapper = 'and'; |
| } |
| |
| $filter = $this->compileRaws($query) |
| .$this->compileWheres($query) |
| .$this->compileOrWheres($query); |
| |
| switch ($this->wrapper) { |
| case 'and': |
| return $this->compileAnd($filter); |
| case 'or': |
| return $this->compileOr($filter); |
| default: |
| return $filter; |
| } |
| } |
| |
| /** |
| * Determine if the query must be wrapped in an encapsulating statement. |
| * |
| * @param Builder $query |
| * |
| * @return bool |
| */ |
| protected function queryMustBeWrapped(Builder $query) |
| { |
| return ! $query->isNested() && $this->hasMultipleFilters($query); |
| } |
| |
| /** |
| * Assembles all of the "raw" filters on the query. |
| * |
| * @param Builder $builder |
| * |
| * @return string |
| */ |
| protected function compileRaws(Builder $builder) |
| { |
| return $this->concatenate($builder->filters['raw']); |
| } |
| |
| /** |
| * Assembles all where clauses in the current wheres property. |
| * |
| * @param Builder $builder |
| * @param string $type |
| * |
| * @return string |
| */ |
| protected function compileWheres(Builder $builder, $type = 'and') |
| { |
| $filter = ''; |
| |
| foreach ($builder->filters[$type] as $where) { |
| $filter .= $this->compileWhere($where); |
| } |
| |
| return $filter; |
| } |
| |
| /** |
| * Assembles all or where clauses in the current orWheres property. |
| * |
| * @param Builder $query |
| * |
| * @return string |
| */ |
| protected function compileOrWheres(Builder $query) |
| { |
| $filter = $this->compileWheres($query, 'or'); |
| |
| if (! $this->hasMultipleFilters($query)) { |
| return $filter; |
| } |
| |
| // Here we will detect whether the entire query can be |
| // wrapped inside of an "or" statement by checking |
| // how many filter statements exist for each type. |
| if ($this->queryCanBeWrappedInSingleOrStatement($query)) { |
| $this->wrapper = 'or'; |
| } else { |
| $filter = $this->compileOr($filter); |
| } |
| |
| return $filter; |
| } |
| |
| /** |
| * Determine if the query can be wrapped in a single or statement. |
| * |
| * @param Builder $query |
| * |
| * @return bool |
| */ |
| protected function queryCanBeWrappedInSingleOrStatement(Builder $query) |
| { |
| return $this->has($query, 'or', '>=', 1) && |
| $this->has($query, 'and', '<=', 1) && |
| $this->has($query, 'raw', '=', 0); |
| } |
| |
| /** |
| * Concatenates filters into a single string. |
| * |
| * @param array $bindings |
| * |
| * @return string |
| */ |
| public function concatenate(array $bindings = []) |
| { |
| // Filter out empty query segments. |
| return implode( |
| array_filter($bindings, [$this, 'bindingValueIsNotEmpty']) |
| ); |
| } |
| |
| /** |
| * Determine if the binding value is not empty. |
| * |
| * @param string $value |
| * |
| * @return bool |
| */ |
| protected function bindingValueIsNotEmpty($value) |
| { |
| return ! empty($value); |
| } |
| |
| /** |
| * Determine if the query is using multiple filters. |
| * |
| * @param Builder $query |
| * |
| * @return bool |
| */ |
| protected function hasMultipleFilters(Builder $query) |
| { |
| return $this->has($query, ['and', 'or', 'raw'], '>', 1); |
| } |
| |
| /** |
| * Determine if the query contains the given filter statement type. |
| * |
| * @param Builder $query |
| * @param string|array $type |
| * @param string $operator |
| * @param int $count |
| * |
| * @return bool |
| */ |
| protected function has(Builder $query, $type, $operator = '>=', $count = 1) |
| { |
| $types = (array) $type; |
| |
| $filters = 0; |
| |
| foreach ($types as $type) { |
| $filters += count($query->filters[$type]); |
| } |
| |
| switch ($operator) { |
| case '>': |
| return $filters > $count; |
| case '>=': |
| return $filters >= $count; |
| case '<': |
| return $filters < $count; |
| case '<=': |
| return $filters <= $count; |
| default: |
| return $filters == $count; |
| } |
| } |
| |
| /** |
| * Returns a query string for equals. |
| * |
| * Produces: (field=value) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileEquals($field, $value) |
| { |
| return $this->wrap($field.'='.$value); |
| } |
| |
| /** |
| * Returns a query string for does not equal. |
| * |
| * Produces: (!(field=value)) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileDoesNotEqual($field, $value) |
| { |
| return $this->compileNot( |
| $this->compileEquals($field, $value) |
| ); |
| } |
| |
| /** |
| * Alias for does not equal operator (!=) operator. |
| * |
| * Produces: (!(field=value)) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileDoesNotEqualAlias($field, $value) |
| { |
| return $this->compileDoesNotEqual($field, $value); |
| } |
| |
| /** |
| * Returns a query string for greater than or equals. |
| * |
| * Produces: (field>=value) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileGreaterThanOrEquals($field, $value) |
| { |
| return $this->wrap("$field>=$value"); |
| } |
| |
| /** |
| * Returns a query string for less than or equals. |
| * |
| * Produces: (field<=value) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileLessThanOrEquals($field, $value) |
| { |
| return $this->wrap("$field<=$value"); |
| } |
| |
| /** |
| * Returns a query string for approximately equals. |
| * |
| * Produces: (field~=value) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileApproximatelyEquals($field, $value) |
| { |
| return $this->wrap("$field~=$value"); |
| } |
| |
| /** |
| * Returns a query string for starts with. |
| * |
| * Produces: (field=value*) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileStartsWith($field, $value) |
| { |
| return $this->wrap("$field=$value*"); |
| } |
| |
| /** |
| * Returns a query string for does not start with. |
| * |
| * Produces: (!(field=*value)) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileNotStartsWith($field, $value) |
| { |
| return $this->compileNot( |
| $this->compileStartsWith($field, $value) |
| ); |
| } |
| |
| /** |
| * Returns a query string for ends with. |
| * |
| * Produces: (field=*value) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileEndsWith($field, $value) |
| { |
| return $this->wrap("$field=*$value"); |
| } |
| |
| /** |
| * Returns a query string for does not end with. |
| * |
| * Produces: (!(field=value*)) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileNotEndsWith($field, $value) |
| { |
| return $this->compileNot($this->compileEndsWith($field, $value)); |
| } |
| |
| /** |
| * Returns a query string for contains. |
| * |
| * Produces: (field=*value*) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileContains($field, $value) |
| { |
| return $this->wrap("$field=*$value*"); |
| } |
| |
| /** |
| * Returns a query string for does not contain. |
| * |
| * Produces: (!(field=*value*)) |
| * |
| * @param string $field |
| * @param string $value |
| * |
| * @return string |
| */ |
| public function compileNotContains($field, $value) |
| { |
| return $this->compileNot( |
| $this->compileContains($field, $value) |
| ); |
| } |
| |
| /** |
| * Returns a query string for a where has. |
| * |
| * Produces: (field=*) |
| * |
| * @param string $field |
| * |
| * @return string |
| */ |
| public function compileHas($field) |
| { |
| return $this->wrap("$field=*"); |
| } |
| |
| /** |
| * Returns a query string for a where does not have. |
| * |
| * Produces: (!(field=*)) |
| * |
| * @param string $field |
| * |
| * @return string |
| */ |
| public function compileNotHas($field) |
| { |
| return $this->compileNot( |
| $this->compileHas($field) |
| ); |
| } |
| |
| /** |
| * Wraps the inserted query inside an AND operator. |
| * |
| * Produces: (&query) |
| * |
| * @param string $query |
| * |
| * @return string |
| */ |
| public function compileAnd($query) |
| { |
| return $query ? $this->wrap($query, '(&') : ''; |
| } |
| |
| /** |
| * Wraps the inserted query inside an OR operator. |
| * |
| * Produces: (|query) |
| * |
| * @param string $query |
| * |
| * @return string |
| */ |
| public function compileOr($query) |
| { |
| return $query ? $this->wrap($query, '(|') : ''; |
| } |
| |
| /** |
| * Wraps the inserted query inside an NOT operator. |
| * |
| * @param string $query |
| * |
| * @return string |
| */ |
| public function compileNot($query) |
| { |
| return $query ? $this->wrap($query, '(!') : ''; |
| } |
| |
| /** |
| * Assembles a single where query. |
| * |
| * @param array $where |
| * |
| * @throws UnexpectedValueException |
| * |
| * @return string |
| */ |
| protected function compileWhere(array $where) |
| { |
| $method = $this->makeCompileMethod($where['operator']); |
| |
| return $this->{$method}($where['field'], $where['value']); |
| } |
| |
| /** |
| * Make the compile method name for the operator. |
| * |
| * @param string $operator |
| * |
| * @throws UnexpectedValueException |
| * |
| * @return string |
| */ |
| protected function makeCompileMethod($operator) |
| { |
| if (! $this->operatorExists($operator)) { |
| throw new UnexpectedValueException("Invalid LDAP filter operator ['$operator']"); |
| } |
| |
| return 'compile'.ucfirst($this->operators[$operator]); |
| } |
| |
| /** |
| * Determine if the operator exists. |
| * |
| * @param string $operator |
| * |
| * @return bool |
| */ |
| protected function operatorExists($operator) |
| { |
| return array_key_exists($operator, $this->operators); |
| } |
| } |