ReadQueries::paginateQuery()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 9.472
c 0
b 0
f 0
cc 3
nc 3
nop 6
1
<?php
2
/*
3
 * This file is part of the PommProject/ModelManager package.
4
 *
5
 * (c) 2014 - 2015 Grégoire HUBERT <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace PommProject\ModelManager\Model\ModelTrait;
11
12
use PommProject\Foundation\Pager;
13
use PommProject\Foundation\Where;
14
use PommProject\ModelManager\Exception\ModelException;
15
use PommProject\ModelManager\Model\CollectionIterator;
16
use PommProject\ModelManager\Model\FlexibleEntity\FlexibleEntityInterface;
17
use PommProject\ModelManager\Model\Projection;
18
19
/**
20
 * ReadQueries
21
 *
22
 * Basic read queries for model instances.
23
 *
24
 * @package     ModelManager
25
 * @copyright   2014 - 2015 Grégoire HUBERT
26
 * @author      Grégoire HUBERT
27
 * @license     X11 {@link http://opensource.org/licenses/mit-license.php}
28
 */
29
trait ReadQueries
30
{
31
    use BaseTrait;
32
33
    /**
34
     * escapeIdentifier
35
     *
36
     * @see Model
37
     */
38
    abstract protected function escapeIdentifier($string);
39
40
    /**
41
     * getStructure
42
     *
43
     * @see Model
44
     */
45
    abstract public function getStructure();
46
47
    /**
48
     * findAll
49
     *
50
     * Return all elements from a relation. If a suffix is given, it is append
51
     * to the query. This is mainly useful for "order by" statements.
52
     * NOTE: suffix is inserted as is with NO ESCAPING. DO NOT use it to place
53
     * "where" condition nor any untrusted params.
54
     *
55
     * @access public
56
     * @param  string             $suffix
57
     * @return CollectionIterator
58
     */
59
    public function findAll($suffix = null)
60
    {
61
        $sql = strtr(
62
            "select :fields from :table :suffix",
63
            [
64
                ':fields' => $this->createProjection()->formatFieldsWithFieldAlias(),
65
                ':table'  => $this->getStructure()->getRelation(),
66
                ':suffix' => $suffix,
67
            ]
68
        );
69
70
        return $this->query($sql);
71
    }
72
73
    /**
74
     * findWhere
75
     *
76
     * Perform a simple select on a given condition
77
     * NOTE: suffix is inserted as is with NO ESCAPING. DO NOT use it to place
78
     * "where" condition nor any untrusted params.
79
     *
80
     * @access public
81
     * @param  mixed              $where
82
     * @param  array              $values
83
     * @param  string             $suffix order by, limit, etc.
84
     * @return CollectionIterator
85
     */
86
    public function findWhere($where, array $values = [], $suffix = '')
87
    {
88
        if (!$where instanceof Where) {
89
            $where = new Where($where, $values);
90
        }
91
92
        return $this->query($this->getFindWhereSql($where, $this->createProjection(), $suffix), $where->getValues());
93
    }
94
95
    /**
96
     * findByPK
97
     *
98
     * Return an entity upon its primary key. If no entities are found, null is
99
     * returned.
100
     *
101
     * @access public
102
     * @param  array          $primary_key
103
     * @return FlexibleEntityInterface
104
     */
105
    public function findByPK(array $primary_key)
106
    {
107
        $where = $this
108
            ->checkPrimaryKey($primary_key)
109
            ->getWhereFrom($primary_key)
110
            ;
111
112
        $iterator = $this->findWhere($where);
113
114
        return $iterator->isEmpty() ? null : $iterator->current();
115
    }
116
117
    /**
118
     * countWhere
119
     *
120
     * Return the number of records matching a condition.
121
     *
122
     * @access public
123
     * @param  string|Where $where
124
     * @param  array        $values
125
     * @return int
126
     */
127
    public function countWhere($where, array $values = [])
128
    {
129
        $sql = sprintf(
130
            "select count(*) as result from %s where :condition",
131
            $this->getStructure()->getRelation()
132
        );
133
134
        return $this->fetchSingleValue($sql, $where, $values);
135
    }
136
137
    /**
138
     * existWhere
139
     *
140
     * Check if rows matching the given condition do exist or not.
141
     *
142
     * @access public
143
     * @param  mixed $where
144
     * @param  array $values
145
     * @return bool
146
     */
147
    public function existWhere($where, array $values = [])
148
    {
149
        $sql = sprintf(
150
            "select exists (select true from %s where :condition) as result",
151
            $this->getStructure()->getRelation()
152
        );
153
154
        return $this->fetchSingleValue($sql, $where, $values);
155
    }
156
157
    /**
158
     * fetchSingleValue
159
     *
160
     * Fetch a single value named « result » from a query.
161
     * The query must be formatted with ":condition" as WHERE condition
162
     * placeholder. If the $where argument is a string, it is turned into a
163
     * Where instance.
164
     *
165
     * @access protected
166
     * @param  string       $sql
167
     * @param  mixed        $where
168
     * @param  array        $values
169
     * @return mixed
170
     */
171
    protected function fetchSingleValue($sql, $where, array $values)
172
    {
173
        if (!$where instanceof Where) {
174
            $where = new Where($where, $values);
175
        }
176
177
        $sql = str_replace(":condition", (string) $where, $sql);
178
179
        return $this
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PommProject\Foundation\Client\ClientInterface as the method query() does only exist in the following implementations of said interface: PommProject\Foundation\P...ry\PreparedQueryManager, PommProject\Foundation\Q...ager\QueryManagerClient, PommProject\Foundation\Q...ager\SimpleQueryManager, PommProject\ModelManager\Model\Model.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
180
            ->getSession()
181
            ->getClientUsingPooler('query_manager', '\PommProject\Foundation\PreparedQuery\PreparedQueryManager')
182
            ->query($sql, $where->getValues())
183
            ->current()['result']
184
            ;
185
    }
186
187
    /**
188
     * paginateFindWhere
189
     *
190
     * Paginate a query.
191
     *
192
     * @access public
193
     * @param  Where    $where
194
     * @param  int      $item_per_page
195
     * @param  int      $page
196
     * @param  string   $suffix
197
     * @return Pager
198
     */
199
    public function paginateFindWhere(Where $where, $item_per_page, $page = 1, $suffix = '')
200
    {
201
        $projection = $this->createProjection();
202
203
        return $this->paginateQuery(
204
            $this->getFindWhereSql($where, $projection, $suffix),
205
            $where->getValues(),
206
            $this->countWhere($where),
207
            $item_per_page,
208
            $page,
209
            $projection
210
        );
211
    }
212
213
    /**
214
     * paginateQuery
215
     *
216
     * Paginate a SQL query.
217
     * It is important to note it adds limit and offset at the end of the given
218
     * query.
219
     *
220
     * @access  protected
221
     * @param   string       $sql
222
     * @param   array        $values parameters
223
     * @param   int          $count
224
     * @param   int          $item_per_page
225
     * @param   int          $page
226
     * @param   Projection   $projection
227
     * @throws  \InvalidArgumentException if pager args are invalid.
228
     * @return  Pager
229
     */
230
    protected function paginateQuery($sql, array $values, $count, $item_per_page, $page = 1, Projection $projection = null)
231
    {
232
        if ($page < 1) {
233
            throw new \InvalidArgumentException(
234
                sprintf("Page cannot be < 1. (%d given)", $page)
235
            );
236
        }
237
238
        if ($item_per_page <= 0) {
239
            throw new \InvalidArgumentException(
240
                sprintf("'item_per_page' must be strictly positive (%d given).", $item_per_page)
241
            );
242
        }
243
244
        $offset = $item_per_page * ($page - 1);
245
        $limit  = $item_per_page;
246
247
        return new Pager(
248
            $this->query(
249
                sprintf("%s offset %d limit %d", $sql, $offset, $limit),
250
                $values,
251
                $projection
252
            ),
253
            $count,
254
            $item_per_page,
255
            $page
256
        );
257
    }
258
259
    /**
260
     * getFindWhereSql
261
     *
262
     * This is the standard SQL query to fetch instances from the current
263
     * relation.
264
     *
265
     * @access protected
266
     * @param  Where        $where
267
     * @param  Projection   $projection
268
     * @param  string       $suffix
269
     * @return string
270
     */
271
    protected function getFindWhereSql(Where $where, Projection $projection, $suffix = '')
272
    {
273
        return strtr(
274
            'select :projection from :relation where :condition :suffix',
275
            [
276
                ':projection' => $projection->formatFieldsWithFieldAlias(),
277
                ':relation'   => $this->getStructure()->getRelation(),
278
                ':condition'  => (string) $where,
279
                ':suffix'     => $suffix,
280
            ]
281
        );
282
    }
283
284
    /**
285
     * hasPrimaryKey
286
     *
287
     * Check if model has a primary key
288
     *
289
     * @access protected
290
     * @return bool
291
     */
292
    protected function hasPrimaryKey()
293
    {
294
        $primaryKeys = $this->getStructure()->getPrimaryKey();
295
296
        return !empty($primaryKeys);
297
    }
298
299
    /**
300
     * checkPrimaryKey
301
     *
302
     * Check if the given values fully describe a primary key. Throw a
303
     * ModelException if not.
304
     *
305
     * @access private
306
     * @param  array $values
307
     * @throws ModelException
308
     * @return $this
309
     */
310
    protected function checkPrimaryKey(array $values)
311
    {
312
        if (!$this->hasPrimaryKey()) {
313
            throw new ModelException(
314
                sprintf(
315
                    "Attached structure '%s' has no primary key.",
316
                    get_class($this->getStructure())
317
                )
318
            );
319
        }
320
321
        foreach ($this->getStructure()->getPrimaryKey() as $key) {
322
            if (!isset($values[$key])) {
323
                throw new ModelException(
324
                    sprintf(
325
                        "Key '%s' is missing to fully describes the primary key {%s}.",
326
                        $key,
327
                        join(', ', $this->getStructure()->getPrimaryKey())
328
                    )
329
                );
330
            }
331
        }
332
333
        return $this;
334
    }
335
336
    /**
337
     * getWhereFrom
338
     *
339
     * Build a condition on given values.
340
     *
341
     * @access protected
342
     * @param  array $values
343
     * @return Where
344
     */
345
    protected function getWhereFrom(array $values)
346
    {
347
        $where = new Where();
348
349
        foreach ($values as $field => $value) {
350
            $where->andWhere(
351
                sprintf(
352
                    "%s = $*::%s",
353
                    $this->escapeIdentifier($field),
354
                    $this->getStructure()->getTypeFor($field)
355
                ),
356
                [$value]
357
            );
358
        }
359
360
        return $where;
361
    }
362
}
363