PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.1.1
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.1.1
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / vendor / wpfluent / framework / src / WPFluent / Database / Query / WPDBConnection.php

WPDBConnection.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.1.1, at vendor/wpfluent/framework/src/WPFluent/Database/Query/WPDBConnection.php

932 lines 23.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * WPDB Connection
5 */
6
7 namespace FluentBooking\Framework\Database\Query;
8
9 use Closure;
10 use Exception;
11 use DateTimeInterface;
12 use FluentBooking\Framework\Foundation\App;
13 use FluentBooking\Framework\Database\Schema;
14 use FluentBooking\Framework\Database\QueryException;
15 use FluentBooking\Framework\Database\ConnectionInterface;
16 use FluentBooking\Framework\Database\MultipleColumnsSelectedException;
17 use FluentBooking\Framework\Database\Events\QueryExecuted;
18 use FluentBooking\Framework\Database\Query\Expression;
19 use FluentBooking\Framework\Database\Query\Processors\Processor;
20 use FluentBooking\Framework\Database\Query\Processors\MySqlProcessor;
21 use FluentBooking\Framework\Database\Query\Processors\SQLiteProcessor;
22 use FluentBooking\Framework\Database\Query\Builder as QueryBuilder;
23 use FluentBooking\Framework\Database\Query\Grammars\Grammar;
24 use FluentBooking\Framework\Database\Query\Grammars\MySqlGrammar;
25 use FluentBooking\Framework\Database\Query\Grammars\SQLiteGrammar;
26 use FluentBooking\Framework\Database\Concerns\ManagesTransactions;
27 use FluentBooking\Framework\Database\DetectsLostConnections;
28
29 use FluentBooking\Framework\Database\Events\TransactionBeginning;
30 use FluentBooking\Framework\Database\Events\TransactionCommitted;
31 use FluentBooking\Framework\Database\Events\TransactionCommitting;
32 use FluentBooking\Framework\Database\Events\TransactionRolledBack;
33
34 class WPDBConnection implements ConnectionInterface
35 {
36 use DetectsLostConnections, ManagesTransactions;
37
38 /**
39 * $wpdb Global $wpdb instance
40 * @var Object
41 */
42 protected $wpdb;
43
44 /**
45 * The name of the connected database.
46 *
47 * @var string
48 */
49 protected $database;
50
51 /**
52 * The table prefix for the connection.
53 *
54 * @var string
55 */
56 protected $tablePrefix = '';
57
58 /**
59 * The database connection configuration options.
60 *
61 * @var array
62 */
63 protected $config = [];
64
65 /**
66 * The query grammar implementation.
67 *
68 * @var \FluentBooking\Framework\Database\Query\Grammars\Grammar
69 */
70 protected $queryGrammar;
71
72 /**
73 * The query post processor implementation.
74 *
75 * @var \FluentBooking\Framework\Database\Query\Processors\Processor
76 */
77 protected $postProcessor;
78
79 /**
80 * The number of active transactions.
81 *
82 * @var int
83 */
84 protected $transactions = 0;
85
86 /**
87 * The transaction manager instance.
88 *
89 * @var \FluentBooking\Framework\Database\DatabaseTransactionsManager|null
90 */
91 protected $transactionsManager;
92
93 /**
94 * All of the callbacks that should be invoked before a transaction is started.
95 *
96 * @var \Closure[]
97 */
98 protected $beforeStartingTransaction = [];
99
100 /**
101 * The event dispatcher.
102 *
103 * @var \FluentBooking\Framework\Events\Dispatcher
104 */
105 protected $event = null;
106
107 /**
108 * Create a new database connection instance.
109 *
110 * @param \wpdb $wpdb The WordPress database instance.
111 * @return void
112 */
113 public function __construct($wpdb)
114 {
115 $this->setupWpdbInstance($wpdb);
116
117 $this->useDefaultQueryGrammar();
118
119 $this->useDefaultPostProcessor();
120
121 $this->event = App::make('events');
122 }
123
124 /**
125 * Populate $wpdb instance & turn off db errors
126 *
127 * @param $wpdb Global $wpdb instance
128 * @return Null
129 */
130 protected function setupWpdbInstance($wpdb)
131 {
132 $this->wpdb = $wpdb;
133
134 $this->wpdb->show_errors(
135 $this->shouldShowErrors()
136 );
137 }
138
139 /**
140 * Determine if database errors should be shown.
141 *
142 * @return bool
143 */
144 protected function shouldShowErrors()
145 {
146 return strpos(App::env(), 'prod') === false;
147 }
148
149 /**
150 * Set the query grammar to the default implementation.
151 *
152 * @return void
153 */
154 public function useDefaultQueryGrammar()
155 {
156 $this->queryGrammar = $this->getDefaultQueryGrammar();
157 }
158
159 /**
160 * Get the default query grammar instance.
161 *
162 * @return \FluentBooking\Framework\Database\Query\Grammars\Grammar
163 */
164 protected function getDefaultQueryGrammar()
165 {
166 return $this->isSqlite() ? new SQLiteGrammar : new MySqlGrammar;
167 }
168
169 /**
170 * Set the query post processor to the default implementation.
171 *
172 * @return void
173 */
174 public function useDefaultPostProcessor()
175 {
176 $this->postProcessor = $this->getDefaultPostProcessor();
177 }
178
179 /**
180 * Get the default post processor instance.
181 *
182 * @return \FluentBooking\Framework\Database\Query\Processors\Processor
183 */
184 protected function getDefaultPostProcessor()
185 {
186 return $this->isSqlite() ? new SQLiteProcessor : new MySqlProcessor;
187 }
188
189 /**
190 * Begin a fluent query against a database table.
191 *
192 * @param \Closure|\FluentBooking\Framework\Database\Query\Builder|string $table
193 * @param string|null $as
194 * @return \FluentBooking\Framework\Database\Query\Builder
195 */
196 public function table($table, $as = null)
197 {
198 return $this->query()->from($table, $as);
199 }
200
201 /**
202 * Get a new query builder instance.
203 *
204 * @return \FluentBooking\Framework\Database\Query\Builder
205 */
206 public function query()
207 {
208 return new QueryBuilder(
209 $this, $this->getQueryGrammar(), $this->getPostProcessor()
210 );
211 }
212
213 /**
214 * Run a select statement and return a single result.
215 *
216 * @param string $query
217 * @param array $bindings
218 * @return mixed
219 */
220 public function selectOne($query, $bindings = [])
221 {
222 return $this->run($query, $bindings, function ($query, $bindings) {
223 $query = $this->bindParams($query, $bindings);
224
225 $result = $this->wpdb->get_row($query);
226
227 if ($result === false || $this->wpdb->last_error) {
228 throw new QueryException(
229 $query, $bindings, new Exception($this->wpdb->last_error)
230 );
231 }
232
233 return $result;
234 });
235 }
236
237 /**
238 * Run a select statement and return the first column of the first row.
239 *
240 * @param string $query
241 * @param array $bindings
242 * @return mixed
243 *
244 * @throws \FluentBooking\Framework\Database\MultipleColumnsSelectedException
245 */
246 public function scalar($query, $bindings = [])
247 {
248 $record = $this->selectOne($query, $bindings);
249
250 if (is_null($record)) {
251 return null;
252 }
253
254 $record = (array)$record;
255
256 if (count($record) > 1) {
257 throw new MultipleColumnsSelectedException(
258 'The query returned more than one column.'
259 );
260 }
261
262 return reset($record);
263 }
264
265 /**
266 * Run a select statement against the database.
267 *
268 * @param string $query
269 * @param array $bindings
270 * @return array
271 */
272 public function select($query, $bindings = [])
273 {
274 return $this->run($query, $bindings, function ($query, $bindings) {
275 $query = $this->bindParams($query, $bindings);
276
277 $result = $this->wpdb->get_results($query);
278
279 if ($result === false || $this->wpdb->last_error) {
280 throw new QueryException(
281 $query, $bindings, new Exception($this->wpdb->last_error)
282 );
283 }
284
285 return $result;
286 });
287 }
288
289 /**
290 * Bind the parameters into SQL query
291 *
292 * @param $query
293 * @param $bindings
294 * @return string
295 */
296 protected function bindParams(string $query, array $bindings)
297 {
298 $query = str_replace('"', '`', $query);
299
300 $bindings = $this->prepareBindings($bindings);
301
302 if (empty($bindings)) {
303 return $query;
304 }
305
306 $query = str_replace(['%', '?'], ['%%', '%s'], $query);
307
308 if ($this->wpdb->dbh instanceof \mysqli) {
309 // wpdb->prepare() casts null to '' for %s, which on TIMESTAMP
310 // columns becomes 0000-00-00 00:00:00. Splice literal NULL into
311 // the SQL for null bindings before prepare() ever sees them.
312 [$query, $bindings] = $this->spliceNullBindings($query, $bindings, '%s');
313
314 if (empty($bindings)) {
315 return $query;
316 }
317
318 return $this->wpdb->prepare($query, ...$bindings);
319 }
320
321 $bindings = array_map(function ($value) {
322 if ($value === null) return 'NULL';
323 if (is_bool($value)) return $value ? '1' : '0';
324 if (is_string($value)) return "'" . esc_sql($value) . "'";
325 return (string) $value;
326 }, $bindings);
327
328 return vsprintf($query, $bindings);
329 }
330
331 /**
332 * Replace placeholder occurrences whose binding is null with the
333 * literal SQL keyword NULL, returning the rewritten query and the
334 * remaining (non-null) bindings re-indexed.
335 *
336 * @param string $query
337 * @param array $bindings
338 * @param string $placeholder '%s' for wpdb->prepare path, '?' for mysqli native prepare
339 * @return array{0:string,1:array}
340 *
341 * @phpstan-ignore-next-line
342 */
343 protected function spliceNullBindings(string $query, array $bindings, string $placeholder): array
344 {
345 $bindings = array_values($bindings);
346
347 if (empty($bindings) || strpos($query, $placeholder) === false) {
348 return [$query, $bindings];
349 }
350
351 $parts = explode($placeholder, $query);
352 $last = count($parts) - 1;
353 $rebuilt = '';
354 $kept = [];
355
356 foreach ($parts as $idx => $part) {
357 $rebuilt .= $part;
358
359 if ($idx === $last) {
360 continue;
361 }
362
363 if (array_key_exists($idx, $bindings) && $bindings[$idx] === null) {
364 $rebuilt .= 'NULL';
365 } else {
366 $rebuilt .= $placeholder;
367 if (array_key_exists($idx, $bindings)) {
368 $kept[] = $bindings[$idx];
369 }
370 }
371 }
372
373 return [$rebuilt, $kept];
374 }
375
376 /**
377 * Run a select statement against the database and returns a generator.
378 *
379 * @param string $query
380 * @param array $bindings
381 * @return \Generator
382 * @throws \FluentAccount\Framework\Database\QueryException
383 */
384 public function cursor($query, $bindings = [])
385 {
386 // If not mysqli (e.g., SQLite), fallback to standard select
387 if (!$this->wpdb->dbh instanceof \mysqli) {
388 foreach ($this->select($query, $bindings) as $row) {
389 yield $row;
390 }
391 return;
392 }
393
394 $preparedQuery = str_replace('"', '`', $query);
395
396 $preparedQuery = str_replace('%', '%%', $preparedQuery);
397
398 $bindings = $this->prepareBindings($bindings);
399
400 // mysqli's bind_param can't bind null with type 's' (becomes '').
401 // Replace null bindings with literal NULL in the SQL itself.
402 [$preparedQuery, $bindings] = $this->spliceNullBindings($preparedQuery, $bindings, '?');
403
404 $this->wpdb->flush();
405 $this->wpdb->insert_id = 0;
406 $this->wpdb->check_current_query = true;
407
408 if (!$this->wpdb->check_connection()) {
409 throw new QueryException(
410 $query, $bindings, new Exception(
411 $this->wpdb->last_error ?: 'Error reconnecting to database.'
412 )
413 );
414 }
415
416 if (defined('SAVEQUERIES') && SAVEQUERIES) {
417 $this->wpdb->timer_start();
418 }
419
420 $statement = $this->wpdb->dbh->prepare($preparedQuery);
421
422 if ($statement === false) {
423 throw new QueryException(
424 $query, $bindings, new Exception(
425 'Failed to prepare statement: ' . $this->wpdb->dbh->error
426 )
427 );
428 }
429
430 if (!empty($bindings)) {
431 $types = '';
432 foreach ($bindings as $binding) {
433 if (is_int($binding)) {
434 $types .= 'i';
435 } elseif (is_double($binding)) {
436 $types .= 'd';
437 } else {
438 $types .= 's';
439 }
440 }
441
442 $statement->bind_param($types, ...$bindings);
443 }
444
445 if ($statement->execute()) {
446 $result = $statement->get_result();
447
448 if ($result) {
449 while ($row = $result->fetch_assoc()) {
450 yield (object) $row;
451 }
452 $result->free();
453 } else {
454 if ($statement->errno) {
455 throw new QueryException(
456 $query, $bindings, new Exception($statement->error)
457 );
458 }
459 }
460 $statement->close();
461 return;
462 }
463
464 // Error handling if statement execution fails
465 if ($statement->error || $statement->errno) {
466 $err = $statement->error
467 ? $statement->error
468 : 'Mysqli Error No: ' . $statement->errno;
469
470 $this->wpdb->last_error = $err;
471
472 throw new QueryException(
473 $query, $bindings, new Exception($err)
474 );
475 }
476 }
477
478 /**
479 * Raw cursor query for MySQLi (non-prepared).
480 *
481 * @param string $query
482 * @param array $bindings
483 * @return \Generator
484 * @throws \FluentAccount\Framework\Database\QueryException
485 */
486 public function rawCursor($query, $bindings = [])
487 {
488 if (!empty($bindings)) {
489 $query = str_replace(['%', '?'], ['%%', '%s'], $query);
490 $query = $this->wpdb->prepare($query, ...$bindings);
491 }
492
493 if (!$this->wpdb->dbh instanceof \mysqli) {
494 foreach ($this->select($query) as $row) { yield $row; }
495 return;
496 }
497
498 $stmt = $this->wpdb->dbh->query($query, MYSQLI_USE_RESULT);
499
500 if ($stmt instanceof \mysqli_result) {
501 try {
502 while ($row = $stmt->fetch_assoc()) {
503 yield (object) $row;
504 }
505 } finally {
506 $stmt->free();
507 }
508 } elseif ($this->wpdb->dbh->error) {
509 throw new QueryException(
510 $query, $bindings, new Exception($this->wpdb->dbh->error)
511 );
512 }
513 }
514
515 /**
516 * Run an insert statement against the database.
517 *
518 * @param string $query
519 * @param array $bindings
520 * @return bool
521 */
522 public function insert($query, $bindings = [])
523 {
524 return $this->statement($query, $bindings);
525 }
526
527 /**
528 * Run an update statement against the database.
529 *
530 * @param string $query
531 * @param array $bindings
532 * @return int
533 */
534 public function update($query, $bindings = [])
535 {
536 return $this->affectingStatement($query, $bindings);
537 }
538
539 /**
540 * Run a delete statement against the database.
541 *
542 * @param string $query
543 * @param array $bindings
544 * @return int
545 */
546 public function delete($query, $bindings = [])
547 {
548 return $this->affectingStatement($query, $bindings);
549 }
550
551 /**
552 * Execute an SQL statement and return the boolean result.
553 *
554 * @param string $query
555 * @param array $bindings
556 * @return bool
557 */
558 public function statement($query, $bindings = [])
559 {
560 return $this->run($query, $bindings, function ($query, $bindings) {
561 $query = $this->bindParams($query, $bindings, true);
562
563 $result = $this->unprepared($query);
564
565 if ($result === false || $this->wpdb->last_error) {
566 throw new QueryException(
567 $query, $bindings, new Exception($this->wpdb->last_error)
568 );
569 }
570
571 return $result;
572 });
573 }
574
575 /**
576 * Run an SQL statement and get the number of rows affected.
577 *
578 * @param string $query
579 * @param array $bindings
580 * @return int
581 */
582 public function affectingStatement($query, $bindings = [])
583 {
584 return $this->run($query, $bindings, function ($query, $bindings) {
585 $query = $this->bindParams($query, $bindings, true);
586
587 $result = $this->wpdb->query($query);
588
589 if ($result === false || $this->wpdb->last_error) {
590 throw new QueryException(
591 $query, $bindings, new Exception($this->wpdb->last_error)
592 );
593 }
594
595 return intval($result);
596 });
597 }
598
599 /**
600 * Run a raw, unprepared query against the PDO connection.
601 *
602 * @param string $query
603 * @return bool
604 */
605 public function unprepared($query)
606 {
607 return $this->wpdb->query($query);
608 }
609
610 /**
611 * Execute the given callback in "dry run" mode.
612 *
613 * @param \Closure $callback
614 * @return array
615 */
616 public function pretend(Closure $callback)
617 {
618 // ...
619 }
620
621 /**
622 * Prepare the query bindings for execution.
623 *
624 * @param array $bindings
625 * @return array
626 */
627 public function prepareBindings(array $bindings)
628 {
629 $grammar = $this->getQueryGrammar();
630
631 foreach ($bindings as $key => $value) {
632 // We need to transform all instances of DateTimeInterface into
633 // the actual date string. Each query grammar maintains its
634 // own date string format so we'll just ask the grammar
635 // for the format to get from the date.
636 if ($value instanceof DateTimeInterface) {
637 $bindings[$key] = $value->format($grammar->getDateFormat());
638 } elseif (is_bool($value)) {
639 $bindings[$key] = (int)$value;
640 }
641 }
642
643 return $bindings;
644 }
645
646 /**
647 * Run a SQL statement and log its execution context.
648 *
649 * @param string $query
650 * @param array $bindings
651 * @param \Closure $callback
652 * @return mixed
653 */
654 public function run($query, $bindings, $callback)
655 {
656 $start = microtime(true);
657
658 try {
659 return $callback($query, $bindings);
660 } finally {
661 $time = $this->getElapsedTime($start);
662 $this->event->dispatch(
663 new QueryExecuted($query, $bindings, $time, $this)
664 );
665 }
666 }
667
668 /**
669 * Get a new raw query expression.
670 *
671 * @param mixed $value
672 * @return \FluentBooking\Framework\Database\Query\Expression
673 */
674 public function raw($value)
675 {
676 return new Expression($value);
677 }
678
679 /**
680 * Get the query grammar used by the connection.
681 *
682 * @return \FluentBooking\Framework\Database\Query\Grammars\Grammar
683 */
684 public function getQueryGrammar()
685 {
686 $this->queryGrammar->setTablePrefix($this->wpdb);
687
688 return $this->queryGrammar;
689 }
690
691 /**
692 * Set the query grammar used by the connection.
693 *
694 * @param \FluentBooking\Framework\Database\Query\Grammars\Grammar $grammar
695 * @return $this
696 */
697 public function setQueryGrammar(Grammar $grammar)
698 {
699 $this->queryGrammar = $grammar;
700
701 return $this;
702 }
703
704 /**
705 * Get the query post processor used by the connection.
706 *
707 * @return \FluentBooking\Framework\Database\Query\Processors\Processor
708 */
709 public function getPostProcessor()
710 {
711 return $this->postProcessor;
712 }
713
714 /**
715 * Set the query post processor used by the connection.
716 *
717 * @param \FluentBooking\Framework\Database\Query\Processors\Processor $processor
718 * @return $this
719 */
720 public function setPostProcessor(Processor $processor)
721 {
722 $this->postProcessor = $processor;
723
724 return $this;
725 }
726
727 /**
728 * Return the last insert id
729 *
730 * @param string $args
731 *
732 * @return int
733 */
734 public function lastInsertId($args)
735 {
736 return $this->wpdb->insert_id;
737 }
738
739 /**
740 * Return self as PDO, the Processor instance uses it.
741 *
742 * @return \FluentBooking\Framework\Database\Query\WPDBConnection
743 */
744 public function getPdo()
745 {
746 return $this;
747 }
748
749 /**
750 * Returns the $wpdb object.
751 *
752 * @return Object $wpdb
753 */
754 public function getWPDB()
755 {
756 return $this->wpdb;
757 }
758
759 /**
760 * Get the database connection name.
761 *
762 * @return string|null
763 */
764 public function getName()
765 {
766 return $this->isSqlite() ? 'sqlite' : 'mysql';
767 }
768
769 /**
770 * Get the name of the connected database.
771 *
772 * @return string
773 */
774 public function getDatabaseName()
775 {
776 if ($this->isSqlite()) {
777 return 'sqlite';
778 }
779
780 return $this->wpdb->dbname;
781 }
782
783 /**
784 * Get the server version for the connection.
785 *
786 * @return string
787 *
788 * @phpstan-ignore-next-line
789 */
790 public function getServerVersion(): string
791 {
792 return $this->getWPDB()->db_version();
793 }
794
795 /**
796 * Get the column listing for a given table.
797 *
798 * @param string $table
799 * @return array
800 */
801 public function getColumnListing($table)
802 {
803 return Schema::getColumns($table);
804 }
805
806 /**
807 * Alias for getColumnListing.
808 *
809 * @param string $t
810 * @return array
811 */
812 public function getColumns($t)
813 {
814 return $this->getColumnListing($t);
815 }
816
817 /**
818 * Determine if the connected database is a sqlite database.
819 *
820 * @return bool
821 */
822 public function isSqlite()
823 {
824 return Schema::isSqlite();
825 }
826
827 /**
828 * Determine if the connected database is a mariadb database.
829 *
830 * @return bool
831 */
832 public function isMaria()
833 {
834 return Schema::isMaria();
835 }
836
837 /**
838 * Register a hook to be run just before a database transaction is started.
839 *
840 * @param \Closure $callback
841 * @return $this
842 */
843 public function beforeStartingTransaction(Closure $callback)
844 {
845 $this->beforeStartingTransaction[] = $callback;
846
847 return $this;
848 }
849
850 /**
851 * Register a database query listener with the connection.
852 *
853 * @param \Closure $callback
854 * @return void
855 */
856 public function listen(Closure $callback)
857 {
858 $this->event->listen(QueryExecuted::class, $callback);
859 }
860
861 /**
862 * Fire an event for this connection.
863 *
864 * @param string $event
865 * @return array|null
866 */
867 protected function fireConnectionEvent($event)
868 {
869 if (!$this->event) {
870 return;
871 }
872
873 switch ($event) {
874 case 'beganTransaction':
875 $payload = new TransactionBeginning($this);
876 break;
877 case 'committed':
878 $payload = new TransactionCommitted($this);
879 break;
880 case 'committing':
881 $payload = new TransactionCommitting($this);
882 break;
883 case 'rollingBack':
884 $payload = new TransactionRolledBack($this);
885 break;
886 default:
887 $payload = null;
888 break;
889 }
890
891 if ($payload !== null) {
892 return $this->event->dispatch($payload);
893 }
894 }
895
896 /**
897 * Get the elapsed time since a given starting point.
898 *
899 * @param int $start
900 * @return float
901 */
902 protected function getElapsedTime($start)
903 {
904 return round((microtime(true) - $start) * 1000, 2);
905 }
906
907 /**
908 * Get the table prefix for the connection.
909 *
910 * @return [type] [description]
911 */
912 public function getTablePrefix()
913 {
914 if (!$this->tablePrefix) {
915 $this->tablePrefix = $this->queryGrammar->getTablePrefix();
916 }
917
918 return $this->tablePrefix;
919 }
920
921 /**
922 * Get the table name with the table prefix.
923 *
924 * @param string $table
925 * @return string
926 */
927 public function getTableName($table)
928 {
929 return $this->getTablePrefix() . $table;
930 }
931 }
932