libasynql  3.2.0
Asynchronous MySQL access library for PocketMine plugins.
GenericStatementFileParser.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 
27 use function array_pop;
28 use function assert;
29 use function count;
30 use function fclose;
31 use function fgets;
32 use function implode;
33 use function ltrim;
34 use function preg_split;
35 use function strpos;
36 use function substr;
37 use function trim;
38 use const PREG_SPLIT_NO_EMPTY;
40 
43  private $fileName;
45  private $fh;
47  private $lineNo = 0;
48 
50  private $identifierStack = [];
52  private $parsingQuery = false;
54  private $docLines = [];
56  private $variables = [];
58  private $buffer = [];
59 
61  private $knownDialect = null;
63  private $results = [];
64 
69  public function __construct(?string $fileName, $fh){
70  $this->fileName = $fileName;
71  $this->fh = $fh;
72  }
73 
79  public function parse() : void{
80  try{
81  while(($line = fgets($this->fh)) !== false){
82  $this->readLine($this->lineNo + 1, $line);
83  }
84  if(!empty($this->identifierStack)){
85  $this->error("Unexpected end of file, " . count($this->identifierStack) . " groups not closed");
86  }
87  }finally{
88  fclose($this->fh);
89  }
90  }
91 
95  public function getResults() : array{
96  return $this->results;
97  }
98 
99  private function readLine(int $lineNo, string $line) : void{
100  $this->lineNo = $lineNo; // In fact I don't need this parameter. I just want to get the line number onto the stack trace.
101  $line = trim($line);
102 
103  if($line === ""){
104  return;
105  }
106 
107  if($this->tryCommand($line)){
108  return;
109  }
110 
111  if(empty($this->identifierStack)){
112  $this->error("Unexpected query text; start a query with { first");
113  }
114  $this->buffer[] = $line;
115  $this->parsingQuery = true;
116  }
117 
118  private function tryCommand(string $line) : bool{
119  if(strpos($line, "-- #") !== 0){
120  return false;
121  }
122 
123  $line = ltrim(substr($line, 4));
124  if($line === ''){
125  return true;
126  }
127  $cmd = $line{0};
128  $args = [];
129  $argOffsets = [];
130  $regex =
131  '/[ \t]/';
132  foreach(preg_split($regex, substr($line, 1), -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE) as [$arg, $offset]){
133  $args[] = $arg;
134  $argOffsets[] = $offset;
135  }
136 
137  switch($cmd){
138  case "!":
139  $this->dialectCommand($args);
140  return true;
141  case "{":
142  $this->startCommand($args);
143  return true;
144  case "}":
145  $this->endCommand();
146  return true;
147  case "*":
148  $this->docCommand($args, $line, $argOffsets);
149  return true;
150  case ":":
151  $this->varCommand($args, $line, $argOffsets);
152  return true;
153  }
154 
155  return true;
156  }
157 
158  private function dialectCommand(array $args) : void{
159  // dialect command
160  if($this->knownDialect !== null){
161  $this->error("Dialect declared more than once");
162  }
163 
164  if(!isset($args[0])){
165  $this->error("Missing operand: DIALECT");
166  }
167 
168  $this->knownDialect = $args[0];
169  }
170 
171  private function startCommand(array $args) : void{
172  if($this->knownDialect === null){
173  $this->error("Dialect declaration must be the very first line");
174  }
175 
176  if($this->parsingQuery){
177  $this->error("Unexpected {, close previous query first");
178  }
179 
180  if(!isset($args[0])){
181  $this->error("Missing operand: IDENTIFIER_NAME");
182  }
183 
184  $this->identifierStack[] = $args[0];
185  }
186 
187  private function endCommand() : void{
188  if(count($this->identifierStack) === 0){
189  $this->error("No matching { for }");
190  }
191 
192  if($this->parsingQuery){
193  if(count($this->buffer) === 0){
194  $this->error("Documentation/Variables are declared but no query is provided");
195  }
196 
197  $query = implode("\n", $this->buffer);
198  $doc = implode("\n", $this->docLines); // double line breaks => single line breaks
199  assert($this->knownDialect !== null);
200  $stmt = GenericStatementImpl::forDialect($this->knownDialect, implode(".", $this->identifierStack), $query, $doc, $this->variables, $this->fileName, $this->lineNo);
201  $this->docLines = [];
202  $this->variables = [];
203  $this->buffer = [];
204  $this->parsingQuery = false;
205 
206  if(isset($this->results[$stmt->getName()])){
207  $this->error("Duplicate query name ({$stmt->getName()})");
208  }
209  $this->results[$stmt->getName()] = $stmt;
210  } // end query
211 
212  array_pop($this->identifierStack);
213  }
214 
215  private function varCommand(array $args, string $line, array $argOffsets) : void{
216  if(empty($this->identifierStack)){
217  $this->error("Unexpected variable declaration; start a query with { first");
218  }
219 
220  if(!isset($args[1])){
221  $this->error("Missing operand: VAR_TYPE");
222  }
223 
224  try{
225  $var = new GenericVariable($args[0], $args[1], isset($args[2]) ? substr($line, $argOffsets[2] + 1) : null);
226  }catch(InvalidArgumentException $e){
227  throw $this->error($e->getMessage());
228  }
229  if(isset($this->variables[$var->getName()])){
230  $this->error("Duplicate variable definition of :{$var->getName()}");
231  }
232  $this->variables[$var->getName()] = $var;
233  $this->parsingQuery = true;
234  }
235 
236  private function docCommand(array $args, string $line, array $argOffsets) : void{
237  if(empty($this->identifierStack)){
238  $this->error("Unexpected documentation; start a query with { first");
239  }
240 
241  $this->docLines[] = trim(substr($line, 1));
242  $this->parsingQuery = true;
243  }
244 
250  private function error(string $problem) : GenericStatementFileParseException{
251  throw new GenericStatementFileParseException($problem, $this->lineNo, $this->fileName);
252  }
253 }
static forDialect(string $dialect, string $name, string $query, string $doc, array $variables, ?string $file, int $lineNo)