Completed
Push — 2.0 ( 8caec2...40b285 )
by Vermeulen
01:14
created

src/Queries/AbstractQuery.php (2 issues)

1
<?php
2
3
namespace BfwSql\Queries;
4
5
use \Exception;
6
7
/**
8
 * Abstract class used for all query writer class.
9
 * 
10
 * @package bfw-sql
11
 * @author Vermeulen Maxime <[email protected]>
12
 * @version 2.0
13
 * 
14
 * @method \BfwSql\Queries\AbstractQuery table(string|array $nameInfos, string|array|null $columns=null)
15
 * @method \BfwSql\Queries\AbstractQuery where(string $expr, array|null $preparedParams = null)
16
 */
17
abstract class AbstractQuery
18
{
19
    /**
20
     * @const ERR_ASSEMBLE_MISSING_TABLE_NAME Exception code if the user
21
     * try to generate a request without table name.
22
     */
23
    const ERR_ASSEMBLE_MISSING_TABLE_NAME = 2401001;
24
    
25
    /**
26
     * @const ERR_CALL_UNKNOWN_METHOD Exception code if the __call method
27
     * receive a method which is really unknown
28
     */
29
    const ERR_CALL_UNKNOWN_METHOD = 2401002;
30
    
31
    /**
32
     * @const ERR_ASSEMBLE_EMPTY_PART Exception code if during the query
33
     * assemble, a part is empty but should not be empty.
34
     */
35
    const ERR_ASSEMBLE_EMPTY_PART = 2401003;
36
    
37
    /**
38
     * @var \BfwSql\SqlConnect $sqlConnect SqlConnect object
39
     */
40
    protected $sqlConnect;
41
    
42
    /**
43
     * @var \BfwSql\Executers\Common $executer
44
     */
45
    protected $executer;
46
    
47
    /**
48
     * @var string $assembledRequest The request will be executed
49
     */
50
    protected $assembledRequest = '';
51
    
52
    /**
53
     * @var string[] $preparedParams Arguments used by prepared request
54
     */
55
    protected $preparedParams = [];
56
    
57
    /**
58
     * @var array $queriesParts All parts used to generate the final request
59
     */
60
    protected $queriesParts = [];
61
    
62
    /**
63
     * @var string $requestType The type of the request (ex: "select")
64
     */
65
    protected $requestType = '';
66
    
67
    /**
68
     * @var \BfwSql\Queries\SGBD\AbstractSGBD $querySgbd The QueriesSGBD
69
     * instance which define query part allowed per SGBD or change some
70
     * query part generation per SGBD.
71
     */
72
    protected $querySgbd;
73
    
74
    /**
75
     * Constructor
76
     * 
77
     * @param \BfwSql\SqlConnect $sqlConnect Instance of SGBD connexion
78
     */
79
    public function __construct(\BfwSql\SqlConnect $sqlConnect)
80
    {
81
        $this->sqlConnect = $sqlConnect;
82
        
83
        $usedClass      = \BfwSql\UsedClass::getInstance();
84
        $executerClass  = $usedClass->obtainClassNameToUse('ExecutersCommon');
85
        $this->executer = new $executerClass($this);
86
        
87
        $sgbdType        = $this->sqlConnect->getType();
88
        $querySgbdClass  = $usedClass->obtainClassNameToUse(
89
            'QueriesSgbd'.ucfirst($sgbdType)
90
        );
91
        $this->querySgbd = new $querySgbdClass($this);
92
        
93
        $this->defineQueriesParts();
94
    }
95
    
96
    /**
97
     * Define all part which will be necessary to generate the final request
98
     * 
99
     * @return void
100
     */
101
    protected function defineQueriesParts()
102
    {
103
        $this->queriesParts = [
104
            'table' => new Parts\Table($this),
105
            'where' => new Parts\WhereList($this)
106
        ];
107
        
108
        $this->querySgbd->disableQueriesParts($this->queriesParts);
109
    }
110
    
111
    /**
112
     * Magic method __call, used when the user call a non-existing method
113
     * @link http://php.net/manual/en/language.oop5.overloading.php#object.call
114
     * 
115
     * @param string $name the name of the method being called
116
     * @param array $args an enumerated array containing the parameters
117
     *  passed to the $name'ed method.
118
     * 
119
     * @return object The current instance if the part object have the method
120
     * __invoke(), or directly the part object asked
121
     * 
122
     * @throws Exception If the part not exist
123
     */
124
    public function __call(string $name, array $args)
125
    {
126
        if (!isset($this->queriesParts[$name])) {
127
            throw new Exception(
128
                'Unknown called method '.$name,
129
                self::ERR_CALL_UNKNOWN_METHOD
130
            );
131
        }
132
        
133
        if (!method_exists($this->queriesParts[$name], '__invoke')) {
134
            return $this->queriesParts[$name];
135
        }
136
        
137
        $this->queriesParts[$name](...$args);
138
        return $this;
139
    }
140
    
141
    /**
142
     * Getter to access at sqlConnect property
143
     * 
144
     * @return \BfwSql\SqlConnect
145
     */
146
    public function getSqlConnect(): \BfwSql\SqlConnect
147
    {
148
        return $this->sqlConnect;
149
    }
150
    
151
    /**
152
     * Getter accessor to property executer
153
     * 
154
     * @return \BfwSql\Executers\Common
155
     */
156
    public function getExecuter(): \BfwSql\Executers\Common
157
    {
158
        return $this->executer;
159
    }
160
    
161
    /**
162
     * Getter accessor to property queriesParts
163
     * 
164
     * @return array
165
     */
166
    public function getQueriesParts(): array
167
    {
168
        return $this->queriesParts;
169
    }
170
    
171
    /**
172
     * Getter to access to assembledRequest property
173
     * 
174
     * @return string
175
     */
176
    public function getAssembledRequest(): string
177
    {
178
        return $this->assembledRequest;
179
    }
180
    
181
    /**
182
     * Getter to access at preparedParams property
183
     * 
184
     * @return array
185
     */
186
    public function getPreparedParams(): array
187
    {
188
        return $this->preparedParams;
189
    }
190
    
191
    /**
192
     * Getter to access at requestType property
193
     * 
194
     * @return array
195
     */
196
    public function getRequestType()
197
    {
198
        return $this->requestType;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->requestType returns the type string which is incompatible with the documented return type array.
Loading history...
199
    }
200
    
201
    /**
202
     * Getter to access at querySgbd property
203
     * 
204
     * @return array
205
     */
206
    public function getQuerySgbd()
207
    {
208
        return $this->querySgbd;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->querySgbd returns the type BfwSql\Queries\SGBD\AbstractSGBD which is incompatible with the documented return type array.
Loading history...
209
    }
210
    
211
    /**
212
     * Check if a request is assemble or not.
213
     * If not, run the method assembleRequest.
214
     * 
215
     * @return boolean
216
     */
217
    public function isAssembled(): bool
218
    {
219
        if ($this->assembledRequest === '') {
220
            return false;
221
        }
222
        
223
        return true;
224
    }
225
    
226
    /**
227
     * Write the query
228
     * 
229
     * @return void
230
     */
231
    protected function assembleRequest()
232
    {
233
        if (empty($this->queriesParts['table']->getName())) {
234
            throw new Exception(
235
                'The main table of the request should be declared.',
236
                self::ERR_ASSEMBLE_MISSING_TABLE_NAME
237
            );
238
        }
239
        
240
        $generateOrder = $this->obtainGenerateOrder();
241
        
242
        $this->assembledRequest = '';
243
        
244
        foreach ($generateOrder as $partName => $partInfos) {
245
            $requestPart = $this->assembleRequestPart(
246
                $partName,
247
                $partInfos
248
            );
249
            
250
            if ($requestPart !== '') {
251
                //To avoid many blank lines into generated request.
252
                $this->assembledRequest .= $requestPart."\n";
253
            }
254
        }
255
    }
256
    
257
    /**
258
     * Define the order to call all queries part.
259
     * Each item contain an array with somes key to define some how generate
260
     * the query part.
261
     * Properties is:
262
     * * callback: The callback to call to generate the query part
263
     * * canBeEmpty : If the part can be empty, or not
264
     * * prefix : The prefix to add before the generated sql
265
     * * usePartPrefix : If the prefix should be added before the generated sql
266
     * 
267
     * @return array
268
     */
269
    protected abstract function obtainGenerateOrder(): array;
270
    
271
    /**
272
     * Generate the sql query for a part
273
     * 
274
     * @param string $partName The part name
275
     * @param array $partInfos Infos about the generation of the sql part
276
     * 
277
     * @return string
278
     * 
279
     * @throws \Exception If the sql generated is empty but should not be
280
     */
281
    protected function assembleRequestPart(
282
        string $partName,
283
        array $partInfos
284
    ): string {
285
        $this->addMissingKeysToPartInfos($partName, $partInfos);
286
        
287
        $generateSqlPart = '';
288
289
        if (isset($partInfos['callback'])) {
290
            $generateSqlPart = $partInfos['callback']();
291
        }
292
        
293
        if (empty($generateSqlPart)) {
294
            if ($partInfos['canBeEmpty'] === false) {
295
                throw new Exception(
296
                    'The part '.$partName.' should not be empty.',
297
                    self::ERR_ASSEMBLE_EMPTY_PART
298
                );
299
            }
300
            
301
            return '';
302
        }
303
        
304
        if ($partInfos['usePartPrefix'] === true) {
305
            return $partInfos['prefix'].' '.$generateSqlPart;
306
        }
307
        
308
        return $generateSqlPart;
309
    }
310
    
311
    /**
312
     * Check each item of the array containing generation infos for a sql part
313
     * and add missing keys
314
     * 
315
     * @param string $partName The part name
316
     * @param array &$partInfos Infos about the generation of the sql part
317
     * 
318
     * @return void
319
     */
320
    protected function addMissingKeysToPartInfos(
321
        string $partName,
322
        array &$partInfos
323
    ) {
324
        $defaultValues = $this->obtainPartInfosDefaultValues($partName);
325
        
326
        if (!isset($partInfos['callback'])) {
327
            $partInfos['callback'] = [$defaultValues, 'generate'];
328
        }
329
        
330
        if (!isset($partInfos['prefix'])) {
331
            $partInfos['prefix'] = $defaultValues->getPartPrefix();
332
        }
333
        
334
        if (!isset($partInfos['usePartPrefix'])) {
335
            $partInfos['usePartPrefix'] = $defaultValues->getUsePartPrefix();
336
        }
337
        
338
        if (!isset($partInfos['canBeEmpty'])) {
339
            $partInfos['canBeEmpty'] = $defaultValues->getCanBeEmpty();
340
        }
341
    }
342
    
343
    /**
344
     * Return the object containing all default value for a part infos.
345
     * Used by the method who add missing key into the part infos array.
346
     * If the part not exist into the array queriesParts, use an anonymous
347
     * class who extend AbstractPart to have all method and property with
348
     * their default values.
349
     * 
350
     * @param string $partName The part name
351
     * 
352
     * @return \BfwSql\Queries\Parts\AbstractPart
353
     */
354
    protected function obtainPartInfosDefaultValues(
355
        string $partName
356
    ): Parts\AbstractPart {
357
        if (
358
            isset($this->queriesParts[$partName]) &&
359
            $this->queriesParts[$partName] instanceof Parts\AbstractPart
360
        ) {
361
            return $this->queriesParts[$partName];
362
        }
363
        
364
        return new class($this) extends Parts\AbstractPart {
365
            public function generate(): string
366
            {
367
                return '';
368
            }
369
        };
370
    }
371
    
372
    /**
373
     * Return the assembled request
374
     * 
375
     * @param boolean $force : Force to re-assemble request
376
     * 
377
     * @return string
378
     */
379
    public function assemble(bool $force = false): string
380
    {
381
        if ($this->isAssembled() === false || $force === true) {
382
            $this->assembleRequest();
383
        }
384
        
385
        return $this->assembledRequest;
386
    }
387
    
388
    /**
389
     * Execute the assembled request
390
     * 
391
     * @throws \Exception If the request fail
392
     * 
393
     * @return \PDOStatement|integer
394
     */
395
    public function execute()
396
    {
397
        return $this->executer->execute();
398
    }
399
    
400
    /**
401
     * To call this own request without use query writer
402
     * 
403
     * @param string $request The user request
404
     * 
405
     * @return $this
406
     */
407
    public function query(string $request): self
408
    {
409
        $this->assembledRequest = $request;
410
        
411
        return $this;
412
    }
413
    
414
    /**
415
     * Add filters to prepared requests
416
     * 
417
     * @param array $preparedParams Filters to add in prepared request
418
     * 
419
     * @return $this
420
     */
421
    public function addPreparedParams(array $preparedParams): self
422
    {
423
        foreach ($preparedParams as $prepareKey => $prepareValue) {
424
            $this->preparedParams[$prepareKey] = $prepareValue;
425
        }
426
        
427
        return $this;
428
    }
429
}
430