Passed
Push — master ( 344c14...9aa841 )
by Christopher
01:46
created

src/Query.php (4 issues)

Checks if the types of the passed arguments in a function/method call are compatible.

Bug Minor
1
<?php
2
/**
3
 * @link      https://github.com/chrmorandi/yii2-ldap for the source repository
4
 * @package   yii2-ldap
5
 * @author    Christopher Mota <[email protected]>
6
 * @license   MIT License - view the LICENSE file that was distributed with this source code.
7
 * @since     1.0.0
8
 */
9
10
namespace chrmorandi\ldap;
11
12
use chrmorandi\ldap\Connection;
13
use Yii;
14
use yii\base\Component;
15
use yii\base\InvalidValueException;
16
use yii\db\QueryInterface;
17
use yii\db\QueryTrait;
18
19
/**
20
 * Query represents a SEARCH in LDAP database directory.
21
 *
22
 * Query provides a set of methods to facilitate the specification of different clauses
23
 * in a SEARCH statement. These methods can be chained together.
24
 *
25
 * For example,
26
 *
27
 * ```php
28
 * $query = new Query;
29
 * // compose the query
30
 * $query->select('id, name')
31
 *     ->from('user')
32
 *     ->limit(10);
33
 * // build and execute the query
34
 * $rows = $query->all();
35
 * ```
36
 *
37
 * Query internally uses the [[FilterBuilder]] class to generate the LDAP filters.
38
 *
39
 */
40
class Query extends Component implements QueryInterface
41
{
42
    use QueryTrait;
43
44
    const SEARCH_SCOPE_SUB  = 'ldap_search';
45
    const SEARCH_SCOPE_ONE  = 'ldap_list';
46
    const SEARCH_SCOPE_BASE = 'ldap_read';
47
48
    /**
49
     * @var string the scope of search
50
     * The search scope:
51
     * Query::SEARCH_SCOPE_SUB searches the complete subtree including the $baseDn node. This is the default value.
52
     * Query::SEARCH_SCOPE_ONE restricts search to one level below $baseDn.
53
     * Query::SEARCH_SCOPE_BASE restricts search to the $baseDn itself; this can be used to efficiently retrieve a single entry by its DN.
54
     */
55
    public $scope = self::SEARCH_SCOPE_SUB;
56
57
    /**
58
     * @var array the columns being selected. For example, `['id', 'name']`.
59
     * This is used to construct the SEARCH function in a LDAP statement. If not set, it means selecting all columns.
60
     * @see select()
61
     */
62
    public $select = [];
63
64
    /**
65
     * @var string The search filter. Format is described in the LDAP documentation.
66
     * @see http://www.faqs.org/rfcs/rfc4515.html
67
     */
68
    public $filter;
69
70
    /**
71
     * The distinguished name to perform searches upon.
72
     * @var string|null
73
     */
74
    protected $dn;
75
    protected $dataReader;
76
77
    /**
78
     * Creates a LDAP command that can be used to execute this query.
79
     * @param Connection $db the database connection.
80
     * If this parameter is not given, the `db` application component will be used.
81
     * @return DataReader
82
     */
83
    public function execute($db = null)
84
    {
85
        if ($db === null) {
86
            $db = Yii::$app->get('ldap');
87
        }
88
        $this->dn = isset($this->dn) ? $this->dn : $db->baseDn;
89
90
        $this->filter = (new FilterBuilder)->build($this->where);
0 ignored issues
show
It seems like $this->where can also be of type yii\db\ExpressionInterface; however, parameter $condition of chrmorandi\ldap\FilterBuilder::build() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
        $this->filter = (new FilterBuilder)->build(/** @scrutinizer ignore-type */ $this->where);
Loading history...
91
        if (empty($this->filter)) {
92
            throw new InvalidValueException('You must define a filter for the search.');
93
        }
94
95
        if (ctype_digit((string) $this->limit)) {
96
            $db->pageSize = $this->limit;
97
        }
98
        if (ctype_digit((string) $this->offset)) {
99
            $db->offset = $this->offset == 0 ? 1 : $this->offset;
100
        }
101
102
        $params = [$this->dn, $this->filter, $this->select, 0, $this->limit, 0];
103
104
        return $db->executeQuery($this->scope, $params);
105
    }
106
107
    /**
108
     * Executes the query and returns all results as an array.
109
     * @param Connection $db the database connection.
110
     * If this parameter is not given, the `db` application component will be used.
111
     * @return array the query results. If the query results in nothing, an empty array will be returned.
112
     */
113
    public function all($db = null)
114
    {
115
        if ($db === null) {
116
            $db = Yii::$app->get('ldap');
117
        }
118
119
        if (!($this->dataReader instanceof DataReader)) {
120
            /** @var $result DataReader */
121
            $this->dataReader = $this->execute($db);
0 ignored issues
show
It seems like $db can also be of type mixed; however, parameter $db of chrmorandi\ldap\Query::execute() does only seem to accept chrmorandi\ldap\Connection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

121
            $this->dataReader = $this->execute(/** @scrutinizer ignore-type */ $db);
Loading history...
122
        } else {
123
            if (ctype_digit((string) $this->limit)) {
124
                $db->pageSize = $this->limit;
125
            }
126
            if (ctype_digit((string) $this->offset)) {
127
                $db->offset = $this->offset == 0 ? 1 : $this->offset;
128
            }
129
        }
130
131
        return $this->populate($this->dataReader->toArray());
132
    }
133
134
    /**
135
     * Converts the raw query results into the format as specified by this query.
136
     * This method is internally used to convert the data fetched from database
137
     * into the format as required by this query.
138
     * @param array $rows the raw query result from database
139
     * @return array the converted query result
140
     */
141
    public function populate($rows)
142
    {
143
        if ($this->indexBy === null) {
144
            return $rows;
145
        }
146
        $result = [];
147
        foreach ($rows as $row) {
148
            if (is_string($this->indexBy)) {
149
                $key = $row[$this->indexBy];
150
            } else {
151
                $key = call_user_func($this->indexBy, $row);
152
            }
153
            $result[$key] = $row;
154
        }
155
        return $result;
156
    }
157
158
    /**
159
     * Executes the query and returns a single row of result.
160
     * @param Connection $db the database connection.
161
     * If this parameter is not given, the `db` application component will be used.
162
     * @return array|bool the first row (in terms of an array) of the query result. False is returned if the query
163
     * results in nothing.
164
     */
165
    public function one($db = null)
166
    {
167
        if ($db === null) {
168
            $db = Yii::$app->get('ldap');
169
        }
170
        $this->limit = 1;
171
        $result      = $this->execute($db);
0 ignored issues
show
It seems like $db can also be of type mixed; however, parameter $db of chrmorandi\ldap\Query::execute() does only seem to accept chrmorandi\ldap\Connection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

171
        $result      = $this->execute(/** @scrutinizer ignore-type */ $db);
Loading history...
172
        return $result->toArray();
173
    }
174
175
    /**
176
     * Returns the number of entries in a search.
177
     * @param string $q do not use
178
     * @param Connection $db the database connection
179
     * If this parameter is not given (or null), the `db` application component will be used.
180
     * @return integer number of entries.
181
     */
182
    public function count($q = null, $db = NULL)
183
    {
184
        $this->limit      = 20;
185
        $this->dataReader = $this->execute($db);
186
        return $this->dataReader->count();
187
    }
188
189
    /**
190
     * Returns a value indicating whether the query result contains any row of data.
191
     * @param Connection $db the database connection.
192
     * If this parameter is not given, the `db` application component will be used.
193
     * @return bool whether the query result contains any row of entries.
194
     */
195
    public function exists($db = null)
196
    {
197
        $result = $this->execute($db);
198
        return (bool) $result->count();
199
    }
200
201
    /**
202
     * Sets the SELECT part of the query.
203
     * @param string|array $columns the columns to be selected.
204
     * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
205
     *
206
     * ```php
207
     * $query->addSelect(['cn, mail'])->one();
208
     * ```
209
     *
210
     * @return $this the query object itself
211
     */
212
    public function select($columns)
213
    {
214
        if (!is_array($columns)) {
215
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
216
        }
217
        $this->select = $columns;
218
        return $this;
219
    }
220
221
    /**
222
     * Add more columns to the select part of the query.
223
     *
224
     * ```php
225
     * $query->addSelect(['cn, mail'])->one();
226
     * ```
227
     *
228
     * @param string|array $columns the columns to add to the select. See [[select()]] for more
229
     * details about the format of this parameter.
230
     * @return $this the query object itself
231
     * @see select()
232
     */
233
    public function addSelect($columns)
234
    {
235
        if (!is_array($columns)) {
236
            $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
237
        }
238
        if ($this->select === null) {
239
            $this->select = $columns;
240
        } else {
241
            $this->select = array_merge($this->select, $columns);
0 ignored issues
show
It seems like $columns can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

241
            $this->select = array_merge($this->select, /** @scrutinizer ignore-type */ $columns);
Loading history...
242
        }
243
        return $this;
244
    }
245
246
    /**
247
     * Adds a filtering condition for a specific column and allow the user to choose a filter operator.
248
     *
249
     * It adds an additional WHERE condition for the given field and determines the comparison operator
250
     * based on the first few characters of the given value.
251
     * The condition is added in the same way as in [[andFilterWhere]] so [[isEmpty()|empty values]] are ignored.
252
     * The new condition and the existing one will be joined using the 'AND' operator.
253
     *
254
     * The comparison operator is intelligently determined based on the first few characters in the given value.
255
     * In particular, it recognizes the following operators if they appear as the leading characters in the given value:
256
     *
257
     * - `<`: the column must be less than the given value.
258
     * - `>`: the column must be greater than the given value.
259
     * - `<=`: the column must be less than or equal to the given value.
260
     * - `>=`: the column must be greater than or equal to the given value.
261
     * - `~=`: the column must approximate the given value.
262
     * - `=`: the column must be equal to the given value.
263
     * - If none of the above operators is detected, the `$defaultOperator` will be used.
264
     *
265
     * @param string $name the column name.
266
     * @param string $value the column value optionally prepended with the comparison operator.
267
     * @param string $defaultOperator The operator to use, when no operator is given in `$value`.
268
     * Defaults to `=`, performing an exact match.
269
     * @return $this The query object itself
270
     */
271
    public function andFilterCompare($name, $value, $defaultOperator = '=')
272
    {
273
        if (preg_match("/^(~=|>=|>|<=|<|=)/", $value, $matches)) {
274
            $operator = $matches[1];
275
            $value    = substr($value, strlen($operator));
276
        } else {
277
            $operator = $defaultOperator;
278
        }
279
        return $this->andFilterWhere([$operator, $name, $value]);
280
    }
281
282
    /**
283
     * Creates a new Query object and copies its property values from an existing one.
284
     * The properties being copies are the ones to be used by query builders.
285
     * @param Query $from the source query object
286
     * @return Query the new Query object
287
     */
288
    public static function create(Query $from)
289
    {
290
        return new self([
291
            'where'   => $from->where,
292
            'limit'   => $from->limit,
293
            'offset'  => $from->offset,
294
            'orderBy' => $from->orderBy,
295
            'indexBy' => $from->indexBy,
296
            'select'  => $from->select,
297
        ]);
298
    }
299
300
}
301