libasynql  3.2.0
Asynchronous MySQL access library for PocketMine plugins.
GenericStatementImpl.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * libasynql
5  *
6  * Copyright (C) 2018 SOFe
7  *
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  */
20 
21 declare(strict_types=1);
22 
23 namespace poggit\libasynql\generic;
24 
25 use function array_key_exists;
26 use AssertionError;
31 use function get_class;
32 use function gettype;
33 use function in_array;
34 use function is_object;
35 use function mb_strlen;
36 use function mb_strpos;
37 use function mb_substr;
38 use function str_replace;
39 use function uksort;
40 
43  protected $name;
45  protected $query;
47  protected $doc;
49  protected $variables;
51  protected $file;
53  protected $lineNo;
54 
56  protected $varPositions = [];
57 
58  public function getName() : string{
59  return $this->name;
60  }
61 
62  public function getQuery() : string{
63  return $this->query;
64  }
65 
66  public function getDoc() : string{
67  return $this->doc;
68  }
69 
70  public function getVariables() : array{
71  return $this->variables;
72  }
73 
74  public function getFile() : ?string{
75  return $this->file;
76  }
77 
78  public function getLineNumber() : int{
79  return $this->lineNo;
80  }
81 
92  public static function forDialect(string $dialect, string $name, string $query, string $doc, array $variables, ?string $file, int $lineNo) : GenericStatementImpl{
93  static $classMap = [
94  SqlDialect::MYSQL => MysqlStatementImpl::class,
95  SqlDialect::SQLITE => SqliteStatementImpl::class,
96  ];
97  $className = $classMap[$dialect];
98  return new $className($name, $query, $doc, $variables, $file, $lineNo);
99  }
100 
101  public function __construct(string $name, string $query, string $doc, array $variables, ?string $file, int $lineNo){
102  $this->name = $name;
103  $this->query = $query;
104  $this->doc = $doc;
105  $this->variables = $variables;
106  $this->file = str_replace("\\", "/", $file);
107  $this->lineNo = $lineNo;
108 
109  $this->compilePositions();
110  }
111 
112  protected function compilePositions() : void{
113  uksort($this->variables, function($s1, $s2){
114  return mb_strlen($s2) <=> mb_strlen($s1);
115  });
116 
117  $usedNames = [];
118 
119  $positions = [];
120  $quotesState = null;
121  for($i = 1, $iMax = mb_strlen($this->query); $i < $iMax; ++$i){
122  $thisChar = mb_substr($this->query, $i, 1);
123 
124  if($quotesState !== null){
125  if($thisChar === "\\"){
126  ++$i; // skip one character
127  continue;
128  }
129  if($thisChar === $quotesState){
130  $quotesState = null;
131  continue;
132  }
133  continue;
134  }
135  if(in_array($thisChar, ["'", "\"", "`"], true)){
136  $quotesState = $thisChar;
137  continue;
138  }
139 
140  if($thisChar === ":"){
141  $name = null;
142 
143  foreach($this->variables as $variable){
144  if(mb_strpos($this->query, $variable->getName(), $i + 1) === $i + 1){
145  $positions[$i] = $name = $variable->getName();
146  break;
147  // if multiple variables match, the first one i.e. the longest one wins
148  }
149  }
150 
151  if($name !== null){
152  $usedNames[$name] = true;
153  $i += mb_strlen($name); // skip the name
154  }
155  }
156  }
157 
158  $newQuery = "";
159  $lastPos = 0;
160  foreach($positions as $pos => $name){
161  $newQuery .= mb_substr($this->query, $lastPos, $pos - $lastPos);
162  $this->varPositions[mb_strlen($newQuery)] = $name; // we aren't using $pos here, because we want the position in the cleaned string, not the position in the original query string
163  $lastPos = $pos + mb_strlen($name) + 1;
164  }
165  $newQuery .= mb_substr($this->query, $lastPos);
166 
167  $this->query = $newQuery;
168 
169  foreach($this->variables as $variable){
170  if(!isset($usedNames[$variable->getName()])){
171  throw new InvalidArgumentException("The variable {$variable->getName()} is not used anywhere in the query! Check for typos.");
172  }
173  }
174  }
175 
176  public function format(array $vars, ?string $placeHolder, ?array &$outArgs) : string{
177  $outArgs = [];
178  foreach($this->variables as $variable){
179  if(!$variable->isOptional() && !array_key_exists($variable->getName(), $vars)){
180  throw new InvalidArgumentException("Missing required variable {$variable->getName()}");
181  }
182  }
183 
184  $query = "";
185 
186  $lastPos = 0;
187  foreach($this->varPositions as $pos => $name){
188  $query .= mb_substr($this->query, $lastPos, $pos - $lastPos);
189  $value = $vars[$name] ?? $this->variables[$name]->getDefault();
190  try{
191  $query .= $this->formatVariable($this->variables[$name], $value, $placeHolder, $outArgs);
192  }catch(AssertionError $e){
193  throw new InvalidArgumentException("Invalid value for :$name - " . $e->getMessage() . ", " . self::getType($value) . " given", 0, $e);
194  }
195  $lastPos = $pos;
196  }
197  $query .= mb_substr($this->query, $lastPos);
198 
199  return $query;
200  }
201 
202  private static function getType($value){
203  return is_object($value) ? get_class($value) : gettype($value);
204  }
205 
206  protected abstract function formatVariable(GenericVariable $variable, $value, ?string $placeHolder, array &$outArgs) : string;
207 
208  public function jsonSerialize(){
209  return [
210  "name" => $this->name,
211  "query" => $this->query,
212  "doc" => $this->doc,
213  "variables" => $this->variables,
214  "file" => $this->file,
215  "lineNo" => $this->lineNo,
216  ];
217  }
218 }
format(array $vars, ?string $placeHolder, ?array &$outArgs)
__construct(string $name, string $query, string $doc, array $variables, ?string $file, int $lineNo)
static forDialect(string $dialect, string $name, string $query, string $doc, array $variables, ?string $file, int $lineNo)
formatVariable(GenericVariable $variable, $value, ?string $placeHolder, array &$outArgs)