Completed
Push — master ( e1208c...9b683f )
by Fwolf
03:26
created

Adodb::pdoParam()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 46
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 32
nc 5
nop 2
dl 0
loc 46
rs 8.4751
c 0
b 0
f 0
1
<?php
2
namespace Fwlib\Bridge;
3
4
use Fwlib\Db\SqlGenerator;
5
use Fwlib\Util\UtilContainerAwareTrait;
6
7
/**
8
 * Extended ADOdb class
9
 *
10
 * Diff with ADOdb official method of extend sub-class by db type, neither
11
 * direct extend ADOdb class, here use ADOdb instance as property only, and
12
 * use magic function __call __get __set to route ADOdb method to it.  The
13
 * opposite this way is no modification to RecordSet class.
14
 *
15
 *
16
 * Notice:
17
 *
18
 * ADOdb for sybase under Windows 2003, call Affected_Rows() will cause
19
 * process error.
20
 *
21
 *
22
 * Encoding convert:
23
 *
24
 * If charset of database and system are different, this class will try to do
25
 * encoding convert before query, see __call() for affected method.
26
 *
27
 * Encoding convert for query result will NOT automatic done, although we
28
 * provide a method convertEncodingResult() to do this manually.
29
 *
30
 * @copyright   Copyright 2008-2015, 2017 Fwolf
31
 * @license     http://www.gnu.org/licenses/lgpl.html LGPL-3.0+
32
 */
33
class Adodb
34
{
35
    use UtilContainerAwareTrait;
36
37
38
    /**
39
     * PHP script charset
40
     *
41
     * Used to compare with db charset.
42
     *
43
     * This is charset of PHP script. Operation system has their default
44
     * charset, but text editor can specify other charset too.
45
     *
46
     * @var string
47
     */
48
    public $charsetPhp = 'UTF-8';
49
50
    /**
51
     * Real ADOdb connection object
52
     *
53
     * @var object
54
     */
55
    protected $conn = null;
56
57
    /**
58
     * Table schema
59
     *
60
     * {
61
     *  table: {
62
     *      col: {
63
     *          name: ts
64
     *          max_length: -1
65
     *          type: timestamp
66
     *          scale:
67
     *          not_null:
68
     *          primary_key:
69
     *          auto_increment:
70
     *          binary:
71
     *          unsigned:
72
     *          zerofill:
73
     *          has_default: 1
74
     *          default_value: CURRENT_TIMESTAMP
75
     *      }
76
     *  }
77
     * }
78
     *
79
     * Notice: col is ADOFieldObject object, not array !
80
     *
81
     * @var array
82
     */
83
    public $metaColumn = [];
84
85
    /**
86
     * Table column name array, with upper case column name as index
87
     *
88
     * {
89
     *  COLUMN: column
90
     * }
91
     *
92
     * @var array
93
     */
94
    public $metaColumnName = [];
95
96
    /**
97
     * Primary key columns of table
98
     *
99
     * {
100
     *  tbl: colPk
101
     *  OR
102
     *  tbl: [pkCol1, pkCol2]
103
     * }
104
     *
105
     * @var array
106
     */
107
    public $metaPrimaryKey = [];
108
109
    /**
110
     * Db profile
111
     *
112
     * {host, user, pass, name, type, lang}
113
     *
114
     * @var array
115
     */
116
    public $profile = null;
117
118
    /**
119
     * Total query count
120
     *
121
     * @var int
122
     */
123
    protected $queryCount = 0;
124
125
    /**
126
     * Sql generator object
127
     *
128
     * @var SqlGenerator
129
     */
130
    protected $sqlGenerator;
131
132
133
    /**
134
     * constructor
135
     *
136
     * $dbprofile = {type:, host:, user:, pass:, name:, lang:,}
137
     * type: mysql/sybase_ase etc.
138
     * name: db name to select.
139
     * lang: db server charset.
140
     *
141
     * if $pathAdodb is empty, should load ADOdb through ClassLoader.
142
     *
143
     * @param   array  $profile
144
     * @param   string $pathAdodb Include path of original ADOdb
145
     */
146
    public function __construct($profile, $pathAdodb = '')
147
    {
148
        // @codeCoverageIgnoreStart
149
        // Include ADOdb lib
150
        if (!empty($pathAdodb)) {
151
            /** @noinspection PhpIncludeInspection */
152
            require_once $pathAdodb;
153
        }
154
        // @codeCoverageIgnoreEnd
155
156
        // Trigger AutoLoader for ADOdb
157
        new \ADOFetchObj;
158
159
        $this->profile = $profile;
160
        $this->conn = ADONewConnection($profile['type']);
161
162
        // From ADOdb 5.11 Execute 2d array is disabled by default, we need
163
        // enable it for using write etc.
164
        $this->conn->bulkBind = true;
165
    }
166
167
168
    /**
169
     * Redirect method call to ADOdb
170
     *
171
     * @var string $name Method name
172
     * @var array  $arg  Method argument
173
     * @return  mixed
174
     */
175
    public function __call($name, $arg)
176
    {
177
        // Before call, convert $sql encoding first
178
        // Method list by ADOdb doc order
179
180
        if (in_array(
181
            $name,
182
            [
183
                'Execute',
184
                'SelectLimit',
185
                'Prepare',
186
                'PrepareSP',
187
                'GetOne',
188
                'GetRow',
189
                'GetAll',
190
                'GetCol',
191
                'GetAssoc',
192
                'ExecuteCursor',
193
            ]
194
        )) {
195
            // $sql is the 1st param
196
            $this->convertEncodingSql($arg[0]);
197
        } elseif (in_array(
198
            $name,
199
            [
200
                'CacheExecute',
201
                'CacheSelectLimit',
202
                'CacheGetOne',
203
                'CacheGetRow',
204
                'CacheGetAll',
205
                'CacheGetCol',
206
                'CacheGetAssoc',
207
            ]
208
        )) {
209
            // $sql is the 2nd param
210
            $this->convertEncodingSql($arg[1]);
211
        }
212
213
        // Count db query times, for all Adodb instance.
214
        // Use static var so multi Adodb object can be included in count.
215
        // Use standalone func for easy extend by sub class.
216
        // CacheXxx() method is not counted.
217 View Code Duplication
        if (in_array(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218
            $name,
219
            [
220
                'Execute',
221
                'SelectLimit',
222
                'GetOne',
223
                'GetRow',
224
                'GetAll',
225
                'GetCol',
226
                'GetAssoc',
227
                'ExecuteCursor',
228
            ]
229
        )) {
230
            $this->countQuery();
231
        }
232
233
        return call_user_func_array([$this->conn, $name], $arg);
234
    }
235
236
237
    /**
238
     * Redirect property get to ADOdb
239
     *
240
     * @param   string $name
241
     * @return  mixed
242
     */
243
    public function __get($name)
244
    {
245
        return $this->conn->$name;
246
    }
247
248
249
    /**
250
     * Redirect property set to adodb
251
     *
252
     * @param string $name
253
     * @param mixed  $val
254
     */
255
    public function __set($name, $val)
256
    {
257
        // For object need auto new in this class instead of $this->conn,
258
        // with mechanism in class AbstractAutoNewInstance with newInstanceXxx()
259
        // method, need check in __get() and __set() both. If only treat in
260
        // __get(), the new instance and assign operate will happen, but its
261
        // assigned to $this->conn->property, instead of $this->property, next
262
        // time when it's used(get), will trigger __get() again, and do useless
263
        // newInstanceXxx() again.
264
        //
265
        // By use get method similar with getService(), this is not problem
266
        // anymore.
267
268
        $this->conn->$name = $val;
269
    }
270
271
272
    /**
273
     * Connect to db
274
     *
275
     * If db is mysql, will auto execute 'set names utf8'.
276
     *
277
     * @see $profile
278
     * @param   boolean $forcenew Force new connection
279
     * @return  boolean
280
     */
281
    public function connect($forcenew = false)
282
    {
283
        if (!$forcenew && $this->isConnected()) {
284
            return true;
285
        }
286
287
288
        // @codeCoverageIgnoreStart
289
        // Mysqli doesn't allow port in host, grab it out and set
290 View Code Duplication
        if ('mysqli' == strtolower($this->conn->databaseType)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
291
            $ar = [];
292
            $i = preg_match('/:(\d+)$/', $this->profile['host'], $ar);
293
            if (0 < $i) {
294
                $this->conn->port = $ar[1];
295
                $this->profile['host'] =
296
                    preg_replace('/:(\d+)$/', '', $this->profile['host']);
297
            }
298
        }
299
        // @codeCoverageIgnoreEnd
300
301
302
        // To eliminate sybase 'Changed database context to XXX' message,
303
        // should edit php.ini and change mssql.min_message_severity to 11.
304
        // @link https://bugs.php.net/bug.php?id=34784
305
306
307
        try {
308
            // Disable error display temporary
309
            $iniDisplayErrors = ini_get('display_errors');
310
            ini_set('display_errors', '0');
311
312
            $rs = $this->conn->Connect(
313
                $this->profile['host'],
314
                $this->profile['user'],
315
                $this->profile['pass'],
316
                $this->profile['name']
317
            );
318
319
            // Recover original error display setting
320
            ini_set('display_errors', $iniDisplayErrors);
321
322
            if (empty($rs)) {
323
                // @codeCoverageIgnoreStart
324
                throw new \Exception($this->conn->ErrorMsg(), -1);
325
                // @codeCoverageIgnoreEnd
326
            }
327
        } catch (\Exception $e) {
328
            // @codeCoverageIgnoreStart
329
            // Log and output error
330
            $trace = "======== Adodb Connect Error ========\n"
331
                //. $e->getTraceAsString() . "\n"
332
                . $e->getMessage() . "\n";
333
            error_log($trace);
334
335
            if (!$this->getUtilContainer()->getEnv()->isCli()) {
336
                $trace = $this->getUtilContainer()->getString()
337
                    ->encodeHtml($trace);
338
            }
339
            echo $trace;
340
341
            return false;
342
            // @codeCoverageIgnoreEnd
343
        }
344
345
        // @codeCoverageIgnoreStart
346
        // Mysql db need to 'set names' after connect
347
        if ($this->isDbMysql()) {
348
            $lang = $this->profile['lang'];
349
            $names = str_replace('UTF-8', 'UTF8', strtoupper($lang));
350
            $sql = "SET NAMES '{$names}'";
351
            $this->conn->Execute($sql);
352
        }
353
354
        // @codeCoverageIgnoreEnd
355
356
        return true;
357
    }
358
359
360
    /**
361
     * Convert encoding from db to sys
362
     *
363
     * Mostly used on query result.
364
     *
365
     * @param   array|string &$result Array or string, not RecordSet object
366
     * @return  array|string
367
     */
368 View Code Duplication
    public function convertEncodingResult(&$result)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
369
    {
370
        if (empty($result) || $this->charsetPhp == $this->profile['lang']) {
371
            return $result;
372
        }
373
374
        if (is_array($result)) {
375
            foreach ($result as &$value) {
376
                $this->convertEncodingResult($value);
377
            }
378
            unset($value);
379
380
        } elseif (is_string($result)) {
381
            $result = mb_convert_encoding(
382
                $result,
383
                $this->charsetPhp,
384
                $this->profile['lang']
385
            );
386
        }
387
388
        return $result;
389
    }
390
391
392
    /**
393
     * Convert encoding from sys to db
394
     *
395
     * Mostly used on SQL statement.
396
     *
397
     * @param   mixed &$sql
398
     * @return  mixed
399
     */
400 View Code Duplication
    public function convertEncodingSql(&$sql)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
401
    {
402
        if (empty($sql) || $this->charsetPhp == $this->profile['lang']) {
403
            return $sql;
404
        }
405
406
        if (is_array($sql)) {
407
            foreach ($sql as &$val) {
408
                $this->convertEncodingSql($val);
409
            }
410
            unset($val);
411
        } elseif (is_string($sql)) {
412
            $sql = mb_convert_encoding(
413
                $sql,
414
                $this->profile['lang'],
415
                $this->charsetPhp
416
            );
417
        }
418
419
        return $sql;
420
    }
421
422
423
    /**
424
     * Count how many db query have executed
425
     *
426
     * Can be extend to count on multi db objects.
427
     *
428
     * @param   int $step
429
     */
430
    protected function countQuery($step = 1)
431
    {
432
        $this->queryCount += $step;
433
    }
434
435
436
    /**
437
     * Delete rows by condition user given
438
     *
439
     * Return value:
440
     * -1 error,
441
     * 0 not found,
442
     * N > 0 number of deleted rows.
443
     *
444
     * @param   string $table
445
     * @param   string $condition Not empty, can be raw sql where, having etc
446
     * @return  int
447
     */
448 View Code Duplication
    public function deleteRow($table, $condition)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
449
    {
450
        $condition = trim($condition);
451
        if (empty($condition)) {
452
            return -1;
453
        }
454
455
        $this->executePrepare(
456
            $this->getSqlGenerator()->get(['DELETE' => $table])
457
            . ' ' . $condition
458
        );
459
460
        if (0 != $this->conn->ErrorNo()) {
461
            // @codeCoverageIgnoreStart
462
            // Error is rollbacked, no errorno return
463
464
            // Execute error
465
            return -1;
466
467
            // @codeCoverageIgnoreEnd
468
        } else {
469
            return $this->conn->Affected_Rows();
470
        }
471
    }
472
473
474
    /**
475
     * Alias of errorMessage() for backward compatible
476
     */
477
    public function errorMsg()
478
    {
479
        return $this->getErrorMessage();
480
    }
481
482
483
    /**
484
     * Alias of errorCode() for backward compatible
485
     */
486
    public function errorNo()
487
    {
488
        return $this->getErrorCode();
489
    }
490
491
492
    /**
493
     * Execute SQL, without transaction
494
     *
495
     * @param   mixed $sql SQL statement or sqlCfg for SqlGenerator
496
     * @param   mixed $inputArr
497
     * @return  object
498
     */
499
    public function execute($sql, $inputArr = false)
500
    {
501
        if (is_array($sql)) {
502
            $sql = $this->getSqlGenerator()->get($sql);
503
        }
504
505
        $this->convertEncodingSql($sql);
506
507
        $this->countQuery();
508
509
        return $this->conn->Execute($sql, $inputArr);
510
    }
511
512
513
    /**
514
     * Prepare and execute sql, with transaction
515
     *
516
     * @param   string        $sql
517
     * @param   array|boolean $inputArr Optional parameters in sql
518
     * @return  object
519
     */
520
    public function executePrepare($sql, $inputArr = false)
521
    {
522
        if (is_array($sql)) {
523
            $sql = $this->getSqlGenerator()->getPrepared($sql);
524
        }
525
526
        $this->convertEncodingSql($sql);
527
528
        $stmt = $this->conn->Prepare($sql);
529
530
        $this->conn->BeginTrans();
531
532
        $rs = $this->conn->Execute($stmt, $inputArr);
533
534
        $this->countQuery();
535
536 View Code Duplication
        if (0 != $this->conn->ErrorNo()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
537
            // @codeCoverageIgnoreStart
538
539
            // Log error
540
            trigger_error(
541
                'ErrorNo: ' . $this->conn->ErrorNo()
542
                . "\nErrorMsg: " . $this->conn->ErrorMsg(),
543
                E_USER_ERROR
544
            );
545
            $this->conn->RollbackTrans();
546
547
            return -1;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return -1; (integer) is incompatible with the return type documented by Fwlib\Bridge\Adodb::executePrepare of type object.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
548
549
            // @codeCoverageIgnoreEnd
550
        } else {
551
            $this->conn->CommitTrans();
552
553
            return $rs;
554
        }
555
    }
556
557
558
    /**
559
     * Generate SQL statement
560
     *
561
     * User should avoid use SELECT/UPDATE/INSERT/DELETE simultaneously.
562
     *
563
     * Generate order by SQL statement format order.
564
     *
565
     * UPDATE/INSERT/DELETE is followed by [TBL_NAME], so need not use FROM.
566
     *
567
     * @see Fwlib\Db\SqlGenerator
568
     * @param   array $sqlConfig
569
     * @return  string
570
     */
571
    public function generateSql($sqlConfig)
572
    {
573
        if (!empty($sqlConfig)) {
574
            return $this->getSqlGenerator()->get($sqlConfig);
575
        } else {
576
            return '';
577
        }
578
    }
579
580
581
    /**
582
     * Generate SQL statement for Prepare
583
     *
584
     * Format like value -> ? or :name, and quote chars removed.
585
     *
586
     * @see generateSql()
587
     * @see Fwlib\Db\SqlGenerator
588
     * @param   array $sqlConfig
589
     * @return  string
590
     */
591
    public function generateSqlPrepared($sqlConfig)
592
    {
593
        if (!empty($sqlConfig)) {
594
            return $this->getSqlGenerator()->getPrepared($sqlConfig);
595
        } else {
596
            return '';
597
        }
598
    }
599
600
601
    /**
602
     * Get single row data from single table using key
603
     *
604
     * Also, this method can be used to retrieve data from a table by primary
605
     * or unique key, default and recommend for primary key, which can be auto
606
     * retrieved from table meta.
607
     *
608
     * Whatever key is used, the result should only contain maximum one row,
609
     * or the result is wrong, commonly only include data of first match row.
610
     *
611
     *
612
     * $keyValue, $column, $keyColumn support multiple value split by ',' or
613
     * array, eg: 'value' or 'value1, value2' or array('value1', 'value2')
614
     *
615
     * $column can use style like 'colName AS colAlias'.
616
     *
617
     * '*' can be used for $column, means all columns in table.
618
     *
619
     * Notice: if $column is array, must indexed by number start from 0.
620
     *
621
     * @param   string       $table
622
     * @param   int|string   $keyValue
623
     * @param   string|array $column    Empty or '*' for all column
624
     * @param   string|array $keyColumn Empty to use primary key
625
     * @return  int|string|array    Single value or array of it, null if error
626
     *                                  occur
627
     */
628
    public function getByKey(
629
        $table,
630
        $keyValue,
631
        $column = null,
632
        $keyColumn = []
633
    ) {
634
        $stringUtil = $this->getUtilContainer()->getString();
635
636
        // Treat key column
637
        if (empty($keyColumn)) {
638
            $keyColumn = $this->getMetaPrimaryKey($table);
639
        }
640
641
        // Convert key value and column name to array
642
        if (is_string($keyValue)) {
643
            $keyValue = $stringUtil->toArray($keyValue, ',');
644
        } else {
645
            $keyValue = (array)$keyValue;
646
        }
647
648
        if (is_string($keyColumn)) {
649
            $keyColumn = $stringUtil->toArray($keyColumn, ',');
650
        } else {
651
            $keyColumn = (array)$keyColumn;
652
        }
653
654
        // $keyColumn need to be array same count with $keyValue
655
        if (count($keyValue) != count($keyColumn)) {
656
            // @codeCoverageIgnoreStart
657
            trigger_error('Key value and column not match.', E_USER_WARNING);
658
659
            return null;
660
            // @codeCoverageIgnoreEnd
661
        }
662
663
664
        if (empty($column) || '*' == $column) {
665
            // Drop uppercase index
666
            $column = array_values($this->getMetaColumnName($table));
667
668
        } elseif (!is_array($column)) {
669
            if (is_string($column)) {
670
                $column = $stringUtil->toArray($column, ',');
671
            } else {
672
                // Column is not array nor string? is int? should not happen
673
                // @codeCoverageIgnoreStart
674
                $column = [$column];
675
                // @codeCoverageIgnoreEnd
676
            }
677
        }
678
679
        // $keyValue, $column, $keyColumn all converted to array
680
681
682
        // Retrieve from db
683
        $sqlConfig = [
684
            'SELECT' => $column,
685
            'FROM'   => $table,
686
            'LIMIT'  => 1,
687
        ];
688
        while (!empty($keyValue)) {
689
            $singleKey = array_shift($keyColumn);
690
            $sqlConfig['WHERE'][] = $singleKey . ' = '
691
                . $this->quoteValue($table, $singleKey, array_shift($keyValue));
692
            unset($singleKey);
693
        }
694
        $rs = $this->execute($sqlConfig);
695
        $ar = [];
696
        if (!empty($rs) && !$rs->EOF) {
697
            $ar = $rs->FetchRow();
698
        }
699
700
        // Return value
701
        if (empty($ar)) {
702
            return null;
703
704
        } else {
705
            if (1 == count($ar)) {
706
                return array_pop($ar);
707
708
            } else {
709
                return $ar;
710
            }
711
        }
712
    }
713
714
715
    /**
716
     * Dummy for ADOdb's ErrorNo()
717
     *
718
     * @return  int
719
     */
720
    public function getErrorCode()
721
    {
722
        return $this->conn->ErrorNo();
723
    }
724
725
726
    /**
727
     * Dummy for ADOdb's ErrorMsg()
728
     *
729
     * @return  string
730
     */
731
    public function getErrorMessage()
732
    {
733
        return $this->conn->ErrorMsg();
734
    }
735
736
737
    /**
738
     * Get table schema
739
     *
740
     * @see $metaColumn
741
     * @param   string  $table
742
     * @param   boolean $forcenew Force to retrieve instead of read from cache
743
     * @return  array
744
     */
745
    public function getMetaColumn($table, $forcenew = false)
746
    {
747
        if (!isset($this->metaColumn[$table]) || (true == $forcenew)) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
748
            $this->metaColumn[$table] = $this->conn->MetaColumns($table);
749
            if (empty($this->metaColumn[$table])) {
750
                return null;
751
            }
752
753
            // Convert columns to native case
754
            $colName = $this->getMetaColumnName($table, $forcenew);
755
            // $colName = array(COLUMN => column), $c is UPPER CASED
756
            $art = [];
757
            foreach ($this->metaColumn[$table] as $c => $ar) {
758
                $art[$colName[strtoupper($c)]] = $ar;
759
            }
760
            $this->metaColumn[$table] = $art;
761
762
            // @codeCoverageIgnoreStart
763
            // Fix sybase display timestamp column as varbinary
764 View Code Duplication
            if ($this->isDbSybase()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
765
                $s = $this->getMetaTimestamp($table);
766
                if (!empty($s)) {
767
                    $this->metaColumn[$table][$s]->type = 'timestamp';
768
                }
769
            }
770
            // @codeCoverageIgnoreEnd
771
        }
772
773
        return $this->metaColumn[$table];
774
    }
775
776
777
    /**
778
     * Get table column name
779
     *
780
     * @see $metaColumnName
781
     * @param   string  $table
782
     * @param   boolean $forcenew Force to retrieve instead of read from cache
783
     * @return  array
784
     */
785
    public function getMetaColumnName($table, $forcenew = false)
786
    {
787 View Code Duplication
        if (!isset($this->metaColumnName[$table]) || (true == $forcenew)) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
788
            $this->metaColumnName[$table] = $this->conn->MetaColumnNames($table);
789
        }
790
791
        return $this->metaColumnName[$table];
792
    }
793
794
795
    /**
796
     * Get primary key column of a table
797
     *
798
     * Return single string value or array for multi column primary key.
799
     *
800
     * @param   string  $table
801
     * @param   boolean $forcenew Force to retrieve instead of read from cache
802
     * @return  mixed
803
     * @see $metaPrimaryKey
804
     */
805
    public function getMetaPrimaryKey($table, $forcenew = false)
806
    {
807
        if (!isset($this->metaPrimaryKey[$table]) || (true == $forcenew)) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
808
            // @codeCoverageIgnoreStart
809 View Code Duplication
            if ($this->isDbSybase()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
810
                /**
811
                 * MetaPrimaryKey() in ADOdb has error(till v5.18), find PK
812
                 * manually.
813
                 *
814
                 * @link http://topic.csdn.net/t/20030117/17/1369396.html
815
                 *
816
                 * SELECT name, keycnt,
817
                 *      index_col(tableName, indid, 1),    -- 1st PK col
818
                 *      index_col(tableName, indid, 2)     -- 2nd PK col if has
819
                 * FROM sysindexes
820
                 * WHERE status & 2048 = 2048
821
                 *      AND id = object_id(tableName)
822
                 *
823
                 * keycnt is column count in PK. if PK index is not cursor
824
                 * index(by 0x10 bit in status), its keycnt - 1.
825
                 *
826
                 * Test pass for PK include 2 columns.
827
                 */
828
                $rs = $this->execute(
829
                    [
830
                        'SELECT' => [
831
                            'name'   => 'a.name',
832
                            'keycnt' => 'a.keycnt',
833
                            'k1'     => "index_col('$table', indid, 1)",
834
                            'k2'     => "index_col('$table', indid, 2)",
835
                            'k3'     => "index_col('$table', indid, 3)",
836
                        ],
837
                        'FROM'   => [
838
                            'a' => 'sysindexes',
839
                            'b' => 'sysobjects',
840
                        ],
841
                        'WHERE'  => [
842
                            'a.status & 2048 = 2048 ',
843
                            "b.name = '$table'",
844
                            "a.id = b.id",
845
                        ],
846
                    ]
847
                );
848
                if (true == $rs && 0 < $rs->RowCount()) {
849
                    // Got
850
                    $ar = [$rs->fields['k1']];
851
                    if (!empty($rs->fields['k2'])) {
852
                        $ar[] = $rs->fields['k2'];
853
                    }
854
                    if (!empty($rs->fields['k3'])) {
855
                        $ar[] = $rs->fields['k3'];
856
                    }
857
                } else {
858
                    // Table have no primary key
859
                    $ar = '';
860
                }
861
862
            } else {
863
                // Find using ADOdb first
864
                $ar = $this->conn->MetaPrimaryKeys($table);
865
            }
866
            // @codeCoverageIgnoreEnd
867
868
869
            // Convert columns to native case
870
            if (!empty($ar)) {
871
                $colName = $this->GetMetaColumnName($table);
872
                // $colName = array(COLUMN => column), $c is UPPER CASED
873
                $art = [];
874
                foreach ($ar as $idx => $col) {
875
                    $art[] = $colName[strtoupper($col)];
876
                }
877
                $ar = $art;
878
            }
879
880
            if (is_array($ar) && 1 == count($ar)) {
881
                // Only 1 primary key column
882
                $ar = $ar[0];
883
            }
884
885
            // Set to cache
886
            if (!empty($ar)) {
887
                $this->metaPrimaryKey[$table] = $ar;
888
            }
889
        }
890
891
        if (isset($this->metaPrimaryKey[$table])) {
892
            return $this->metaPrimaryKey[$table];
893
894
        } else {
895
            return null;
896
        }
897
    }
898
899
900
    /**
901
     * Get name of timestamp column of a table
902
     *
903
     * Timestamp column are various for different db, hard to test.
904
     *
905
     * @param   $table
906
     * @return  string
907
     */
908
    public function getMetaTimestamp($table)
909
    {
910
        $arCol = $this->getMetaColumn($table);
911
        if (empty($arCol)) {
912
            return '';
913
        }
914
915
        // @codeCoverageIgnoreStart
916
        if ($this->isDbSybase()) {
917
            // Sybase timestamp column must be lower cased.
918
            // If col name is 'timestamp', will auto assign (timestamp) type.
919
            $rs = $this->execute(
920
                [
921
                    'SELECT' => [
922
                        'name'      => 'a.name',
923
                        'length'    => 'a.length',
924
                        'usertype'  => 'a.usertype',
925
                        'type'      => 'b.name',
926
                        'tableName' => 'c.name',
927
                    ],
928
                    'FROM'   => [
929
                        'a' => 'syscolumns',
930
                        'b' => 'systypes',
931
                        'c' => 'sysobjects',
932
                    ],
933
                    'WHERE'  => [
934
                        "a.id = c.id",
935
                        'a.type = b.type',
936
                        'a.usertype = b.usertype',
937
                        'b.type = 37',
938
                        'b.usertype = 80',
939
                    ],
940
                ]
941
            );
942 View Code Duplication
            while (!empty($rs) && !$rs->EOF) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
943
                if ($table == $rs->fields['tableName']) {
944
                    return $rs->fields['name'];
945
                }
946
                $rs->MoveNext();
947
            }
948
949
            return '';
950
951 View Code Duplication
        } elseif ($this->isDbMysql()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
952
            // Check 'type'
953
            foreach ($arCol as $k => $v) {
954
                if (isset($v->type) && 'timestamp' == $v->type) {
955
                    return $k;
956
                }
957
            }
958
959
        } else {
960
            // Do not trigger error, null means no implemented.
961
            // Use '||' to fool code inspection.
962
            return null ||
0 ignored issues
show
Bug Best Practice introduced by
The return type of return null || trigger_e...ented!', E_USER_ERROR); (boolean) is incompatible with the return type documented by Fwlib\Bridge\Adodb::getMetaTimestamp of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
963
                trigger_error(
964
                    __CLASS__ . '::getMetaTimestamp() for '
965
                    . $this->profile['type']
966
                    . ' not implemented!',
967
                    E_USER_ERROR
968
                );
969
        }
970
        // @codeCoverageIgnoreEnd
971
972
        // No timestamp found
973
        return '';
974
    }
975
976
977
    /**
978
     * Getter of $profile
979
     *
980
     * @return  array
981
     */
982
    public function getProfile()
983
    {
984
        return $this->profile;
985
    }
986
987
988
    /**
989
     * Get string describe of profile
990
     *
991
     * Usually used for identify db source.
992
     *
993
     * @param   string $separator
994
     * @return  string
995
     */
996
    public function getProfileString($separator = '-')
997
    {
998
        return $this->profile['type'] . $separator .
999
            $this->profile['host'] . $separator .
1000
            $this->profile['name'];
1001
    }
1002
1003
1004
    /**
1005
     * Getter of $queryCount
1006
     *
1007
     * @return  int
1008
     */
1009
    public function getQueryCount()
1010
    {
1011
        return $this->queryCount;
1012
    }
1013
1014
1015
    /**
1016
     * Get rows count by condition user given
1017
     *
1018
     * Return value:
1019
     * -1: error,
1020
     * N >= 0: number of rows.
1021
     *
1022
     * @param   string $table
1023
     * @param   string $condition Raw sql, can be WHERE, HAVING etc
1024
     * @return  int
1025
     */
1026 View Code Duplication
    public function getRowCount($table, $condition = '')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1027
    {
1028
        $sqlCfg = [
1029
            'SELECT' => ['c' => 'COUNT(1)'],
1030
            'FROM'   => $table,
1031
        ];
1032
        $rs = $this->executePrepare(
1033
            $this->getSqlGenerator()->get($sqlCfg)
1034
            . ' ' . $condition
1035
        );
1036
        if (false == $rs || 0 != $this->conn->ErrorNo()
1037
            || 0 == $rs->RowCount()
1038
        ) {
1039
            // Execute error, rare happen
1040
            // @codeCoverageIgnoreStart
1041
            return -1;
1042
            // @codeCoverageIgnoreEnd
1043
        } else {
1044
            return $rs->fields['c'];
1045
        }
1046
    }
1047
1048
1049
    /**
1050
     * Get delimiter between SQL for various db
1051
     *
1052
     * @param   string $tail Tail of line for eye candy
1053
     * @return  string
1054
     */
1055
    public function getSqlDelimiter($tail = "\n")
1056
    {
1057
        // @codeCoverageIgnoreStart
1058
        if ($this->isDbMysql()) {
1059
            $delimiter = ';';
1060
1061
        } elseif ($this->isDbSybase()) {
1062
            $delimiter = '';
1063
1064
        } else {
1065
            trigger_error(
1066
                __CLASS__ . '::getSqlDelimiter() for db type '
1067
                . $this->profile['type'] . ' not implement.',
1068
                E_USER_WARNING
1069
            );
1070
            $delimiter = '';
1071
        }
1072
1073
        // @codeCoverageIgnoreEnd
1074
1075
        return $delimiter . $tail;
1076
    }
1077
1078
1079
    /**
1080
     * Get SqlGenerator instance
1081
     *
1082
     * @return  SqlGenerator
1083
     */
1084
    protected function getSqlGenerator()
1085
    {
1086
        if (is_null($this->sqlGenerator)) {
1087
            $this->sqlGenerator = new SqlGenerator($this);
1088
        }
1089
1090
        return $this->sqlGenerator;
1091
    }
1092
1093
1094
    /**
1095
     * Get SQL: begin transaction
1096
     *
1097
     * @return  string
1098
     */
1099
    public function getSqlTransBegin()
1100
    {
1101
        // @codeCoverageIgnoreStart
1102
        if ($this->isDbMysql()) {
1103
            $header = 'START';
1104
        } else {
1105
            $header = 'BEGIN';
1106
        }
1107
1108
        // @codeCoverageIgnoreEnd
1109
1110
        return $header . ' TRANSACTION' . $this->getSqlDelimiter();
1111
    }
1112
1113
1114
    /**
1115
     * Get SQL: commit transaction
1116
     *
1117
     * @return  string
1118
     */
1119
    public function getSqlTransCommit()
1120
    {
1121
        return 'COMMIT' . $this->getSqlDelimiter();
1122
    }
1123
1124
1125
    /**
1126
     * Get SQL: rollback transaction
1127
     *
1128
     * @return  string
1129
     */
1130
    public function getSqlTransRollback()
1131
    {
1132
        return 'ROLLBACK' . $this->GetSqlDelimiter();
1133
    }
1134
1135
1136
    /**
1137
     * If current db is connected successful
1138
     *
1139
     * @return  boolean
1140
     */
1141
    public function isConnected()
1142
    {
1143
        return $this->conn->IsConnected();
1144
    }
1145
1146
1147
    /**
1148
     * If current db type is mysql
1149
     *
1150
     * @return  boolean
1151
     */
1152
    public function isDbMysql()
1153
    {
1154
        return ('mysql' == substr($this->conn->databaseType, 0, 5));
1155
    }
1156
1157
1158
    /**
1159
     * If current db type is sybase and connect with PDO (DBLIB)
1160
     *
1161
     * @return  bool
1162
     */
1163
    public function isDbPdoSybase()
1164
    {
1165
        return ('pdo_sybase' == substr($this->profile['type'], 0, 10)) ||
1166
            ('pdo_sybase_ase' == substr($this->profile['type'], 0, 14));
1167
    }
1168
1169
1170
    /**
1171
     * If current db type is sybase
1172
     *
1173
     * @return  boolean
1174
     */
1175
    public function isDbSybase()
1176
    {
1177
        return ('sybase' == substr($this->profile['type'], 0, 6)) ||
1178
            ('sybase_ase' == substr($this->profile['type'], 0, 10)) ||
1179
            ('pdo_sybase' == substr($this->profile['type'], 0, 10)) ||
1180
            ('pdo_sybase_ase' == substr($this->profile['type'], 0, 14));
1181
    }
1182
1183
1184
    /**
1185
     * If a table exists in db ?
1186
     *
1187
     * @param   string $table
1188
     * @return  boolean
1189
     */
1190 View Code Duplication
    public function isTableExist($table)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1191
    {
1192
        $table = addslashes($table);
1193
1194
        // @codeCoverageIgnoreStart
1195
        if ($this->isDbSybase()) {
1196
            $sql = "SELECT count(1) AS c FROM sysobjects WHERE name =
1197
                '{$table}' AND type = 'U'";
1198
            $rs = $this->execute($sql);
1199
1200
            return (0 != $rs->fields['c']);
1201
1202
        } elseif ($this->isDbMysql()) {
1203
            $sql = "SHOW TABLES LIKE '$table'";
1204
            $rs = $this->execute($sql);
1205
1206
            return (0 != $rs->RowCount());
1207
1208
        } else {
1209
            // :THINK: Better method ?
1210
            $sql = "SELECT 1 FROM $table";
1211
            $this->execute($sql);
1212
1213
            return (0 == $this->conn->ErrorNo());
1214
        }
1215
        // @codeCoverageIgnoreEnd
1216
    }
1217
1218
1219
    /**
1220
     * If timestamp column's value is unique
1221
     *
1222
     * @return  boolean
1223
     */
1224
    public function isTimestampUnique()
1225
    {
1226
        // Default for: sybase
1227
        $b = true;
1228
1229
        // @codeCoverageIgnoreStart
1230
        if ($this->isDbMysql()) {
1231
            $b = false;
1232
        }
1233
1234
        // @codeCoverageIgnoreEnd
1235
1236
        return $b;
1237
    }
1238
1239
1240
    /**
1241
     * Generate a bind placeholder portable
1242
     *
1243
     * @param   string $name
1244
     * @return  string
1245
     */
1246
    public function param($name)
1247
    {
1248
        $param = $this->conn->Param($name);
1249
1250
        if ($this->isDbPdoSybase()) {
1251
            $param = ":{$name}";
1252
        }
1253
1254
        return $param;
1255
    }
1256
1257
1258
    /**
1259
     * Generate a bind placeholder portable, for PDO only
1260
     *
1261
     * Will add CONVERT() for int/numeric data type
1262
     *
1263
     * @param   string $table
1264
     * @param   string $name
1265
     * @return  string
1266
     */
1267
    public function pdoParam($table, $name)
1268
    {
1269
        $param = $this->conn->Param($name);
1270
1271
        if ($this->isDbPdoSybase()) {
1272
            $param = ":{$name}";
1273
1274
            // Add explicit convert by data type
1275
            $this->getMetaColumn($table);
1276
            if (!isset($this->metaColumn[$table][$name]->type)) {
1277
                throw new \Exception(
1278
                    "Column to quote not exists($table.$name)."
1279
                );
1280
            }
1281
1282
            $columnMeta = $this->metaColumn[$table][$name];
1283
            $type = $columnMeta->type;
1284
            $precision = $columnMeta->precision;
1285
            $scale = $columnMeta->scale;
1286
            if (in_array($type, [
1287
                'bigint',
1288
                'bit',
1289
                'int',
1290
                'intn',     // Sybase - tinyint
1291
                'mediumint',
1292
                'smallint',
1293
                'tinyint',
1294
            ])) {
1295
                $type = rtrim($type, 'n');
1296
                $param = "CONVERT({$type}, $param)";
1297
1298
            } elseif (in_array($type, [
1299
                'decimal',
1300
                'double',
1301
                'float',
1302
                'numeric',
1303
                'numericn', // Sybase - numeric
1304
                'real',
1305
            ])) {
1306
                $type = rtrim($type, 'n');
1307
                $param = "CONVERT({$type}({$precision}, {$scale}), $param)";
1308
            }
1309
        }
1310
1311
        return $param;
1312
    }
1313
1314
1315
    /**
1316
     * Smart quote string in sql, by check columns type
1317
     *
1318
     * @param   string $table
1319
     * @param   string $col
1320
     * @param   mixed  $val
1321
     * @return  string
1322
     */
1323 View Code Duplication
    public function quoteValue($table, $col, $val)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1324
    {
1325
        $this->getMetaColumn($table);
1326
        if (!isset($this->metaColumn[$table][$col]->type)) {
1327
            trigger_error(
1328
                "Column to quote not exists($table.$col).",
1329
                E_USER_WARNING
1330
            );
1331
1332
            // @codeCoverageIgnoreStart
1333
            // Return quoted value for safety
1334
            $val = stripslashes($val);
1335
1336
            return $this->conn->qstr($val, false);
1337
            // @codeCoverageIgnoreEnd
1338
        }
1339
1340
        $type = $this->metaColumn[$table][$col]->type;
1341
        if (in_array(
1342
            $type,
1343
            [
1344
                'bigint',
1345
                'bit',
1346
                'decimal',
1347
                'double',
1348
                'float',
1349
                'int',
1350
                'intn',     // Sybase - tinyint
1351
                'mediumint',
1352
                'numeric',
1353
                'numericn', // Sybase - numeric
1354
                'real',
1355
                'smallint',
1356
                'tinyint',
1357
            ]
1358
        )) {
1359
            // Need not quote, output directly
1360
            return $val;
1361
1362
        } elseif ($this->isDbSybase() && 'timestamp' == $type) {
1363
            // Sybase timestamp
1364
            // @codeCoverageIgnoreStart
1365
            return '0x' . $val;
1366
            //elseif ($this->IsDbSybase() && 'varbinary' == $type && 'timestamp' == $col)
1367
            // @codeCoverageIgnoreEnd
1368
1369
        } else {
1370
            // Need quote, use db's quote method
1371
            $val = stripslashes($val);
1372
1373
            return $this->conn->qstr($val, false);
1374
        }
1375
    }
1376
1377
1378
    /**
1379
     * Set PHP script file charset
1380
     *
1381
     * @param   string $charset
1382
     * @see $charsetPhp
1383
     */
1384
    public function setCharsetPhp($charset)
1385
    {
1386
        $this->charsetPhp = $charset;
1387
    }
1388
1389
1390
    /**
1391
     * Setter of fetchMode
1392
     *
1393
     * This is a transfer method to fool code inspection.
1394
     *
1395
     * @param   int $fetchMode
1396
     * @return  int
1397
     */
1398
    public function setFetchMode($fetchMode)
1399
    {
1400
        $oldFetchMode = $this->conn->SetFetchMode($fetchMode);
1401
1402
        return $oldFetchMode;
1403
    }
1404
1405
1406
    /**
1407
     * Smart write data row(s) to db
1408
     *
1409
     * Can auto check row existence, and decide to use INSERT or UPDATE, this
1410
     * require primary key column included in $data array.  Also, table MUST
1411
     * have primary key defined.
1412
     *
1413
     * Param $data can include single row(1-dim array, index is column name)
1414
     * or multiple rows(2-dim array, index layer 1 MUST be number and will not
1415
     * write to db, layer 2 is same as single row).
1416
     *
1417
     * Param $mode is case insensitive:
1418
     * A: auto detect, for multiple rows data, will only detect by FIRST row.
1419
     * U: update,
1420
     * I: insert,
1421
     *
1422
     * Return number of inserted or updated rows:
1423
     * -1: got error,
1424
     * N >=0: success, which N is affected rows.
1425
     *
1426
     * Even data to write exists in db and same, it will still do write
1427
     * operation, and been counted in return value.
1428
     *
1429
     * @param   string $table
1430
     * @param   array  $data Row(s) data
1431
     * @param   string $mode Write mode
1432
     * @return  int
1433
     */
1434
    public function write($table, $data, $mode = 'A')
1435
    {
1436
        // Find primary key column first
1437
        $arPk = $this->getMetaPrimaryKey($table);
1438
1439
        // Convert single row data to multi row mode
1440
        if (!isset($data[0])) {
1441
            $data = [0 => $data];
1442
        }
1443
1444
        // Convert primary key to array if it's single string now
1445
        if (!is_array($arPk)) {
1446
            $arPk = [0 => $arPk];
1447
        }
1448
1449
        // Columns in $data
1450
        $arCols = array_keys($data[0]);
1451
        // Check if primary key is assigned in $data
1452
        $arPkInData = true;
1453
        foreach ($arPk as $key) {
1454
            if (!in_array($key, $arCols)) {
1455
                $arPkInData = false;
1456
            }
1457
        }
1458
        // If no primary key column in $data, return -1
1459
        if (false == $arPkInData) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1460
            return -1;
1461
        }
1462
1463
        $mode = strtoupper($mode);
1464
        $sqlCfg = [];
1465
1466
        // Auto determine mode
1467 View Code Duplication
        if ('A' == $mode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1468
            $where = ' WHERE ';
1469
            foreach ($arPk as $key) {
1470
                $where .= " $key = "
1471
                    . $this->quoteValue($table, $key, $data[0][$key])
1472
                    . ' AND ';
1473
            }
1474
            $where = substr($where, 0, strlen($where) - 5);
1475
            if (0 < $this->getRowCount($table, $where)) {
1476
                $mode = 'U';
1477
            } else {
1478
                $mode = 'I';
1479
            }
1480
        }
1481
1482
        // Prepare sql
1483 View Code Duplication
        if ('U' == $mode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1484
            $sqlCfg = [
1485
                'UPDATE' => $table,
1486
                'LIMIT'  => 1,
1487
            ];
1488
            // Primary key cannot change, so exclude them from SET clause,
1489
            // Here use prepare, actual value will assign later, do quote
1490
            // then.
1491
            // :NOTICE: Remember to put PK data to end of row data array when
1492
            // assign actual value, because WHERE clause is after SET clause.
1493
            foreach ($arPk as $key) {
1494
                $sqlCfg['WHERE'][] = "$key = "
1495
                    . $this->pdoParam($table, $key);
1496
                unset($arCols[array_search($key, $arCols)]);
1497
            }
1498
            foreach ($arCols as $key) {
1499
                $sqlCfg['SET'][$key] = $this->pdoParam($table, $key);
1500
            }
1501
1502
        } elseif ('I' == $mode) {
1503
            $arVal = [];
1504
            foreach ($arCols as $key) {
1505
                $arVal[$key] = $this->pdoParam($table, $key);
1506
            }
1507
            $sqlCfg = [
1508
                'INSERT' => $table,
1509
                'VALUES' => $arVal,
1510
            ];
1511
        }
1512
        $sql = $this->getSqlGenerator()->getPrepared($sqlCfg);
1513
        // @codeCoverageIgnoreStart
1514
        if (empty($sql)) {
1515
            return -1;
1516
        }
1517
        // @codeCoverageIgnoreEnd
1518
1519
        // Change PK position in data array
1520 View Code Duplication
        if ('U' == $mode) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1521
            foreach ($data as &$row) {
1522
                foreach ($arPk as $key) {
1523
                    $v = $row[$key];
1524
                    unset($row[$key]);
1525
                    $row[$key] = $v;
1526
                }
1527
            }
1528
            unset($row);
1529
        }
1530
1531
        // Convert data encoding
1532
        $this->convertEncodingSql($data);
1533
1534
1535
        // When use PDO, param is ? and value array must NOT be named.
1536
        // So consider change to use named param binding, this works better bcs
1537
        // does not restrict order value array.
1538
        //
1539
        // Another problem, ADOdb treat int as varchar, cause another error.
1540
        // @see https://bugs.php.net/bug.php?id=57655 for this bug.
1541
        //   > All PDO_DBLIB binds are done as strings.
1542
        // For those column, use CONVERT(), auto done in pdoParam().
1543
        if ($this->isDbPdoSybase()) {
1544
            foreach ($data as &$row) {
1545
                $keyChangedRow = [];
1546
                foreach ($row as $key => $val) {
1547
                    $keyChangedRow[":$key"] = $val;
1548
                }
1549
                $row = $keyChangedRow;
1550
            }
1551
            unset($row);
1552
        }
1553
1554
1555
        // Do db prepare
1556
        $stmt = $this->conn->Prepare($sql);
1557
1558
        // @codeCoverageIgnoreStart
1559
        // Execute, actual write data
1560
        $this->conn->BeginTrans();
1561
        try {
1562
            $this->conn->Execute($stmt, $data);
1563
1564
        } catch (\Exception $e) {
1565
            // Show error message ?
1566
            $this->conn->RollbackTrans();
1567
1568
            return -1;
1569
        }
1570
1571
        // Any other error ?
1572 View Code Duplication
        if (0 != $this->conn->ErrorNo()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1573
            // Log error
1574
            trigger_error(
1575
                'ErrorNo: ' . $this->conn->ErrorNo() . "\n" .
1576
                'ErrorMsg: ' . $this->conn->ErrorMsg(),
1577
                E_USER_WARNING
1578
            );
1579
            $this->conn->RollbackTrans();
1580
1581
            return -1;
1582
1583
        } else {
1584
            $this->conn->CommitTrans();
1585
1586
            return count($data);
1587
        }
1588
        // @codeCoverageIgnoreEnd
1589
    }
1590
}
1591