Completed
Push — master ( 62a6e8...e0fd75 )
by smiley
02:52
created

src/Query/WhereTrait.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Trait WhereTrait
4
 *
5
 * @filesource   WhereTrait.php
6
 * @created      12.06.2017
7
 * @package      chillerlan\Database\Query
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2017 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\Database\Query;
14
15
/**
16
 * https://xkcd.com/1409/
17
 *
18
 * @implements \chillerlan\Database\Query\Where
19
 *
20
 * @property \chillerlan\Database\Drivers\DriverInterface $db
21
 * @property \chillerlan\Database\Dialects\Dialect        $dialect
22
 * @property array                                        $bindValues
23
 */
24
trait WhereTrait{
25
26
	private $operators = [
27
		'=', '>=', '>', '<=', '<', '<>', '!=',
28
		'|', '&', '<<', '>>', '+', '-', '*', '/',
29
		'%', '^', '<=>', '~', '!', 'DIV', 'MOD',
30
		'IS', 'IS NOT', 'IN', 'NOT IN', 'LIKE',
31
		'NOT LIKE', 'REGEXP', 'NOT REGEXP',
32
		'EXISTS', 'ANY', 'SOME',
33
//		'BETWEEN', 'NOT BETWEEN',
34
	];
35
36
	private $joinArgs = ['AND', 'OR', 'XOR'];
37
	/**
38
	 * @var array
39
	 */
40
	protected $where = [];
41
42
	/**
43
	 * @param mixed       $val1
44
	 * @param mixed|null  $val2
45
	 * @param string|null $operator
46
	 * @param bool|null   $bind
47
	 * @param string|null $join
48
	 *
49
	 * @return $this
50
	 */
51
	public function where($val1, $val2 = null, string $operator = null, bool $bind = null, string $join = null){
52
		$operator = $operator !== null ? strtoupper(trim($operator)) : '=';
53
		$bind     = $bind ?? true;
54
55
		$join = strtoupper(trim($join));
56
		$join = in_array($join, $this->joinArgs, true) ? $join : 'AND';
57
58
		if(in_array($operator, $this->operators, true)){
59
			$where = [
60
				is_array($val1)
61
					? strtoupper($val1[1]).'('.$this->dialect->quote($val1[0]).')'
62
					: $this->dialect->quote($val1)
63
			];
64
65
			if(in_array($operator, ['IN', 'NOT IN', 'ANY', 'SOME',], true)){
66
67
				if(is_array($val2)){
68
69
					if($bind){
70
						$where[] = $operator.'('.implode(',', array_fill(0, count($val2), '?')).')';
71
						$this->bindValues = array_merge($this->bindValues, $val2);
72
					}
73
					else{
74
						$where[] = $operator.'('.implode(',', array_map([$this->db, 'escape'], $val2)).')'; // @todo: quote
75
					}
76
77
				}
78
				else if($val2 instanceof Statement){
79
					$where[] = $operator.'('.$val2->sql().')';
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface chillerlan\Database\Query\Statement as the method sql() does only exist in the following implementations of said interface: chillerlan\Database\Quer...ery/QueryBuilder.php$10, chillerlan\Database\Quer...ery/QueryBuilder.php$11, chillerlan\Database\Quer...ery/QueryBuilder.php$13, chillerlan\Database\Quer...ery/QueryBuilder.php$14, chillerlan\Database\Quer...ery/QueryBuilder.php$16, chillerlan\Database\Quer...ery/QueryBuilder.php$18, chillerlan\Database\Quer...ery/QueryBuilder.php$19, chillerlan\Database\Quer...uery/QueryBuilder.php$4, chillerlan\Database\Quer...uery/QueryBuilder.php$5, chillerlan\Database\Quer...uery/QueryBuilder.php$6, chillerlan\Database\Quer...uery/QueryBuilder.php$8, chillerlan\Database\Quer...uery/QueryBuilder.php$9.

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...
80
					$this->bindValues = array_merge($this->bindValues, $val2->bindValues());
81
				}
82
83
			}
84
//			else if(in_array($operator, ['BETWEEN', 'NOT BETWEEN'], true)){
85
				// @todo
86
//			}
87
			else{
88
				$where[] = $operator;
89
90
				if($val2 instanceof Statement){
91
					$where[] = '('.$val2->sql().')';
92
					$this->bindValues = array_merge($this->bindValues, $val2->bindValues());
93
				}
94
				elseif(is_null($val2)){
95
					$where[] = 'NULL';
96
				}
97
				elseif(is_bool($val2)){
98
					$where[] = $val2 ? 'TRUE' : 'FALSE';
99
				}
100
				elseif(in_array(strtolower($val2), ['null', 'false', 'true', 'unknown'], true)){
101
					$where[] = strtoupper($val2);
102
				}
103
				else {
104
105
					if($bind){
106
						$where[] = '?';
107
						$this->bindValues[] = $val2;
108
					}
109
					else{
110
						if(!empty($val2)){
111
							$where[] = $val2;
112
						}
113
					}
114
115
				}
116
117
			}
118
119
			$this->where[] = [
120
				'join' => $join,
121
				'stmt' => implode(' ', $where),
122
			];
123
124
		}
125
126
		return $this;
127
	}
128
129
	/**
130
	 * @param string|null $join
131
	 *
132
	 * @return $this
133
	 */
134
	public function openBracket(string $join = null){
135
		$join = strtoupper(trim($join));
136
137
		if(in_array($join, $this->joinArgs, true)){
138
			$this->where[] = $join;
139
		}
140
141
		$this->where[] = '(';
142
143
		return $this;
144
	}
145
146
	/**
147
	 * @return $this
148
	 */
149
	public function closeBracket(){
150
		$this->where[] = ')';
151
152
		return $this;
153
	}
154
155
	/**
156
	 * @return string
157
	 */
158
	protected function _getWhere():string {
159
		$where = [];
160
161
		foreach($this->where as $k => $v){
162
			$last = $this->where[$k-1] ?? false;
163
164
			if(in_array($v,  $this->joinArgs + ['(', ')'], true)){
165
				$where[] = $v;
166
167
				continue;
168
			}
169
170
			if(!is_array($v)){
171
				continue;
172
			}
173
174
			if(!$last || $last === '('){
175
				$where[] = $v['stmt'];
176
			}
177
			else{
178
				$where[] = $v['join'].' '.$v['stmt'];
179
			}
180
181
		}
182
183
		return !empty($where) ? 'WHERE '.implode(' ', $where) : '';
184
	}
185
186
}
187