Table::find()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Soluble\Normalist\Synthetic;
4
5
use Soluble\Normalist\Synthetic\ResultSet\ResultSet;
6
use Soluble\Db\Sql\Select;
7
use Zend\Db\Sql\Sql;
8
use Zend\Db\Sql\Expression;
9
use Zend\Db\Sql\Where;
10
use Zend\Db\Sql\Predicate;
11
use Zend\Db\Sql\PreparableSqlInterface;
12
use Zend\Db\Sql\SqlInterface;
13
use ArrayObject;
14
15
class Table
16
{
17
    /**
18
     * Table name
19
     * @var string
20
     */
21
    protected $table;
22
23
    /**
24
     * Prefixed table name or table name if no prefix
25
     * @var string
26
     */
27
    protected $prefixed_table;
28
29
    /**
30
     *
31
     * @var Table\Relation
32
     */
33
    protected $relation;
34
35
    /**
36
     * Primary key of the table
37
     * @var string|null
38
     */
39
    protected $primary_key;
40
41
    /**
42
     * Primary keys of the table in case there's a multiple column pk
43
     * @var array|null
44
     */
45
    protected $primary_keys;
46
47
    /**
48
     * Table alias useful when using join
49
     * @var string
50
     */
51
    protected $table_alias;
52
53
    /**
54
     * @param TableManager
55
     */
56
    protected $tableManager;
57
58
    /**
59
     *
60
     * @var string
61
     */
62
    protected $tablePrefix;
63
64
    /**
65
     *
66
     * @var Sql
67
     */
68
    protected $sql;
69
70
    /**
71
     *
72
     * @var array
73
     */
74
    protected $column_information;
75
76
    /**
77
     *
78
     * @param string $table table name
79
     * @param TableManager $tableManager
80
     *
81
     * @throws Exception\InvalidArgumentException
82
     */
83 131
    public function __construct($table, TableManager $tableManager)
84
    {
85 131
        $this->tableManager = $tableManager;
86 131
        $this->sql = new Sql($tableManager->getDbAdapter());
87 131
        if (!is_string($table) || trim($table) == '') {
88 3
            throw new Exception\InvalidArgumentException(__METHOD__ . ": Table name must be a non-empty string");
89
        }
90
91 128
        $this->table = $table;
92 128
        $this->prefixed_table = $this->tableManager->getTablePrefix() . $table;
93 128
    }
94
95
    /**
96
     * Get a TableSearch object
97
     *
98
     * @param string $table_alias whenever you want to alias the table (useful in joins)
99
     * @return TableSearch
100
     */
101 26
    public function search($table_alias = null)
102
    {
103 26
        return new TableSearch($this->select($table_alias), $this);
104 1
    }
105
106
    /**
107
     * Return all records in the table
108
     *
109
     * @return ResultSet
110
     */
111 1
    public function all()
112
    {
113 1
        return $this->search()->execute();
114
    }
115
116
117
118
    /**
119
     * Find a record
120
     *
121
     * @param integer|string|array $id
122
     *
123
     * @throws Exception\InvalidArgumentException when id is invalid
124
     * @throws Exception\PrimaryKeyNotFoundException
125
     *
126
     * @return Record|false
127
     */
128 32
    public function find($id)
129
    {
130 32
        $record = $this->findOneBy($this->getPrimaryKeyPredicate($id));
131 27
        return $record;
132
    }
133
134
135
    /**
136
     * Find a record by primary key, throw a NotFoundException if record does not exists
137
     *
138
     * @param integer|string|array $id
139
     *
140
     * @throws Exception\NotFoundException
141
     * @throws Exception\InvalidArgumentException when the id is not valid
142
     * @throws Exception\PrimaryKeyNotFoundException
143
     *
144
     * @return Record
145
     */
146 14
    public function findOrFail($id)
147
    {
148 14
        $record = $this->find($id);
149 14
        if ($record === false) {
150 1
            throw new Exception\NotFoundException(__METHOD__ . ": cannot find record '$id' in table '$this->table'");
151
        }
152 13
        return $record;
153
    }
154
155
    /**
156
     * Find a record by unique key
157
     *
158
     * @param  Where|\Closure|string|array|Predicate\PredicateInterface $predicate
159
     * @param  string $combination One of the OP_* constants from Predicate\PredicateSet
160
     *
161
     * @throws Exception\ColumnNotFoundException when a column in the predicate does not exists
162
     * @throws Exception\MultipleMatchesException when more than one record match the predicate
163
     * @throws Exception\InvalidArgumentException when the predicate is not correct / invalid column
164
     * @throws \Zend\Db\Sql\Exception\InvalidArgumentException
165
     *
166
     * @return Record|false
167
     */
168 51
    public function findOneBy($predicate, $combination = Predicate\PredicateSet::OP_AND)
169
    {
170 51
        $select = $this->select($this->table);
171
172
        try {
173 51
            $select->where($predicate, $combination);
174 51
        } catch (\Zend\Db\Sql\Exception\InvalidArgumentException $e) {
175
            $message = "Invalid predicates or combination detected (" . $e->getMessage() . ")";
176
            throw new Exception\InvalidArgumentException(__METHOD__ . ": $message");
177
        }
178
179
        try {
180 51
            $results = $select->execute()
181 47
                              ->toArray();
182 51
        } catch (\Exception $e) {
183 4
            $messages = [];
184 4
            $ex = $e;
185
            do {
186 4
                $messages[] = $ex->getMessage();
187 4
            } while ($ex = $ex->getPrevious());
188 4
            $message = implode(', ', array_unique($messages));
189
190 4
            $lmsg = '[' . get_class($e) . '] ' . strtolower($message) . '(code:' . $e->getCode() . ')';
191
192 4
            if (strpos($lmsg, 'column not found') !== false ||
193 4
                    strpos($lmsg, 'unknown column') !== false) {
194
                //"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'media_id' in 'where clause
195 2
                $rex = new Exception\ColumnNotFoundException(__METHOD__ . ": $message");
196 2
                throw $rex;
197
            } else {
198 2
                $sql_string = $select->getSqlString($this->sql->getAdapter()->getPlatform());
199 2
                $iqex = new Exception\InvalidArgumentException(__METHOD__ . ": $message - $sql_string");
200 2
                throw $iqex;
201
            }
202
        }
203
204 47
        if (count($results) == 0) {
205 12
            return false;
206
        }
207 45
        if (count($results) > 1) {
208 2
            throw new Exception\MultipleMatchesException(__METHOD__ . ": return more than one record");
209
        }
210
211 43
        $record = $this->record($results[0]);
212 43
        $record->setState(Record::STATE_CLEAN);
213 43
        return $record;
214
    }
215
216
    /**
217
     * Find a record by unique key and trhow an exception id record cannot be found
218
     *
219
     * @param  Where|\Closure|string|array|Predicate\PredicateInterface $predicate
220
     * @param  string $combination One of the OP_* constants from Predicate\PredicateSet
221
     *
222
     * @throws Exception\NotFoundException when the record is not found
223
     * @throws Exception\ColumnNotFoundException when a column in the predicate does not exists
224
     * @throws Exception\MultipleMatchesException when more than one record match the predicate
225
     * @throws Exception\InvalidArgumentException when the predicate is not correct / invalid column
226
     *
227
     * @return Record
228
     */
229 5
    public function findOneByOrFail($predicate, $combination = Predicate\PredicateSet::OP_AND)
230
    {
231 5
        $record = $this->findOneBy($predicate, $combination);
232 2
        if ($record === false) {
233 1
            throw new Exception\NotFoundException(__METHOD__ . ": cannot findOneBy record in table '$this->table'");
234
        }
235 1
        return $record;
236
    }
237
238
    /**
239
     * Count the number of record in table
240
     *
241
     * @return int number of record in table
242
     */
243 4
    public function count()
244
    {
245 4
        return $this->countBy("1=1");
246
    }
247
248
    /**
249
     * Find a record by unique key
250
     *
251
     * @param  Where|\Closure|string|array|Predicate\PredicateInterface $predicate
252
     * @param  string $combination One of the OP_* constants from Predicate\PredicateSet
253
     * @throws Exception\InvalidArgumentException
254
     *
255
     * @return int number of record matching predicates
256
     */
257 52
    public function countBy($predicate, $combination = Predicate\PredicateSet::OP_AND)
258
    {
259 4
        $select = $this->select()
260 4
                        ->columns(['count' => new Expression('count(*)')]);
261
262 1
        try {
263 4
            $select->where($predicate, $combination);
264 4
        } catch (\Zend\Db\Sql\Exception\InvalidArgumentException $e) {
265 52
            $message = "Invalid predicates or combination detected (" . $e->getMessage() . ")";
266
            throw new Exception\InvalidArgumentException(__METHOD__ . ": $message");
267
        }
268
269 4
        $result = $select->execute()->toArray();
270
271 4
        return (int) $result[0]['count'];
272
    }
273
274
    /**
275
     * Test if a record exists
276
     *
277
     * @param integer|string|array $id
278
     *
279
     * @throws Exception\InvalidArgumentException when the id is invalid
280
     * @throws Exception\PrimaryKeyNotFoundException
281
     *
282
     * @return boolean
283
     */
284 7
    public function exists($id)
285
    {
286 7
        $result = $this->select()->where($this->getPrimaryKeyPredicate($id))
287 6
                        ->columns(['count' => new Expression('count(*)')])
288 6
                        ->execute()
289 6
                        ->toArray();
290
291 6
        return ($result[0]['count'] > 0);
292
    }
293
294
    /**
295
     * Test if a record exists by a predicate
296
     *
297
     * @param  Where|\Closure|string|array|Predicate\PredicateInterface $predicate
298
     * @param  string $combination One of the OP_* constants from Predicate\PredicateSet
299
     *
300
     * @throws Exception\InvalidArgumentException when the predicate is not correct
301
     *
302
     * @return boolean
303
     */
304 2
    public function existsBy($predicate, $combination = Predicate\PredicateSet::OP_AND)
305
    {
306
        try {
307 2
            $select = $this->select()->where($predicate, $combination)
308 2
                    ->columns(['count' => new Expression('count(*)')]);
309 2
            $result = $select->execute()
310 1
                             ->toArray();
311 2
        } catch (\Exception $e) {
312 1
            throw new Exception\InvalidArgumentException(__METHOD__ . ": invaid usage ({$e->getMessage()})");
313
        }
314
315 1
        return ($result[0]['count'] > 0);
316
    }
317
318
    /**
319
     * Get a select object (Soluble\Db\Select)
320
     *
321
     * @param string $table_alias useful when you want to join columns
322
     * @return Select
323
     */
324 88
    public function select($table_alias = null)
325
    {
326 88
        $prefixed_table = $this->prefixed_table;
327 88
        $select = new \Soluble\Db\Sql\Select();
328 88
        $select->setDbAdapter($this->tableManager->getDbAdapter());
329 88
        if ($table_alias === null) {
330 39
            $table_spec = $prefixed_table;
331 39
        } else {
332 55
            $table_spec = [$table_alias => $prefixed_table];
333
        }
334 88
        $select->from($table_spec);
335 88
        return $select;
336
    }
337
338
    /**
339
     * Delete by primary/unique key value
340
     *
341
     * @param integer|string|array $id primary key(s) or a Record object
342
     *
343
     * @throws Exception\InvalidArgumentException if $id is not valid
344
     * @return integer the number of affected rows (maybe be greater than 1 with triggers or cascade)
345
     */
346 9
    public function delete($id)
347
    {
348 9
        return $this->deleteBy($this->getPrimaryKeyPredicate($id));
349
    }
350
351
    /**
352
     * Delete a record by predicate
353
     *
354
     * @param  Where|\Closure|string|array|Predicate\PredicateInterface $predicate
355
     * @param  string $combination One of the OP_* constants from Predicate\PredicateSet
356
     *
357
     * @return integer the number of affected rows (can be be influenced by triggers or cascade)
358
     */
359 17
    public function deleteBy($predicate, $combination = Predicate\PredicateSet::OP_AND)
360
    {
361 17
        $delete = $this->sql->delete($this->prefixed_table)
362 17
                ->where($predicate, $combination);
363
364 17
        $statement = $this->sql->prepareStatementForSqlObject($delete);
365 17
        $result = $statement->execute();
366 17
        return $result->getAffectedRows();
367
    }
368
369
    /**
370
     * Delete a record or throw an Exception
371
     *
372
     * @param integer|string|array $id primary key value
373
     *
374
     * @throws Exception\InvalidArgumentException if $id is not valid
375
     * @throws Exception\NotFoundException if record does not exists
376
     *
377
     * @return Table
378
     */
379 2
    public function deleteOrFail($id)
380
    {
381 2
        $deleted = $this->delete($id);
382 2
        if ($deleted == 0) {
383 1
            throw new Exception\NotFoundException(__METHOD__ . ": cannot delete record '$id' in table '$this->table'");
384
        }
385 1
        return $this;
386
    }
387
388
    /**
389
     * Update data into table
390
     *
391
     * @param array|ArrayObject $data
392
     * @param  Where|\Closure|string|array|Predicate\PredicateInterface $predicate
393
     * @param  string $combination One of the OP_* constants from Predicate\PredicateSet
394
     * @param  boolean $validate_datatypes ensure all datatype are compatible with column definition
395
     *
396
     * @throws Exception\InvalidArgumentException
397
     * @throws Exception\ColumnNotFoundException when $data contains columns that does not exists in table
398
     * @throws Exception\ForeignKeyException when insertion failed because of an invalid foreign key
399
     * @throws Exception\DuplicateEntryException when insertion failed because of an invalid foreign key
400
     * @throws Exception\NotNullException when insertion failed because a column cannot be null
401
     * @throws Exception\RuntimeException when insertion failed for another reason
402
     *
403
     * @return int number of affected rows
404
     */
405
406 5
    public function update($data, $predicate, $combination = Predicate\PredicateSet::OP_AND, $validate_datatypes = false)
407
    {
408 5
        $prefixed_table = $this->prefixed_table;
409
410 5
        if ($data instanceof ArrayObject) {
411 1
            $d = (array) $data;
412 5
        } elseif (is_array($data)) {
413 4
            $d = $data;
414 4
        } else {
415 1
            throw new Exception\InvalidArgumentException(__METHOD__ . ": requires data to be array or an ArrayObject");
416
        }
417
418 4
        $this->checkDataColumns($d);
419
420 4
        if ($validate_datatypes) {
421 1
            $this->validateDatatypes($d);
422 1
        }
423
424 4
        $update = $this->sql->update($prefixed_table);
425 4
        $update->set($d);
426 4
        $update->where($predicate, $combination);
427
428 4
        $result = $this->executeStatement($update);
429
430 3
        return  $result->getAffectedRows();
431
    }
432
433
    /**
434
     * Insert data into table
435
     *
436
     * @param array|ArrayObject $data
437
     * @param boolean $validate_datatypes ensure data are compatible with database columns datatypes
438
     *
439
     * @throws Exception\InvalidArgumentException when data is not an array or an ArrayObject
440
     * @throws Exception\ColumnNotFoundException when $data contains columns that does not exists in table
441
     * @throws Exception\ForeignKeyException when insertion failed because of an invalid foreign key
442
     * @throws Exception\DuplicateEntryException when insertion failed because of an invalid foreign key
443
     * @throws Exception\NotNullException when insertion failed because a column cannot be null
444
     * @throws Exception\RuntimeException when insertion failed for another reason
445
     *
446
     * @return Record
447
     */
448 19
    public function insert($data, $validate_datatypes = false)
449
    {
450 19
        $prefixed_table = $this->prefixed_table;
451
452 19
        if ($data instanceof \ArrayObject) {
453 1
            $d = (array) $data;
454 19
        } elseif (is_array($data)) {
455 18
            $d = $data;
456 18
        } else {
457 1
            $type = gettype($data);
458 1
            throw new Exception\InvalidArgumentException(__METHOD__ . ": expects data to be array or ArrayObject. Type receive '$type'");
459
        }
460
461 18
        $this->checkDataColumns($d);
462
463 17
        if ($validate_datatypes) {
464 1
            $this->validateDatatypes($d);
465 1
        }
466
467 17
        $insert = $this->sql->insert($prefixed_table);
468 17
        $insert->values($d);
469
470 17
        $this->executeStatement($insert);
471
472 13
        $pks = $this->getPrimaryKeys();
473
474
        // Should never happen, as getPrimaryKeys throws Exception when no pk exists
475
        //@codeCoverageIgnoreStart
476
        if (!is_array($pks)) {
477
            $msg = __METHOD__ . " Error getting primary keys of table " . $this->table . ", require array, returned type is: " . gettype($pks) ;
478
            throw new Exception\UnexpectedValueException($msg);
479
        }
480
        //@codeCoverageIgnoreEnd
481
482 12
        $nb_pks = count($pks);
483 12
        if ($nb_pks > 1) {
484
            // In multiple keys there should not be autoincrement value
485 2
            $id = [];
486 2
            foreach ($pks as $pk) {
487 2
                $id[$pk] = $d[$pk];
488 2
            }
489 12
        } elseif (array_key_exists($pks[0], $d) && $d[$pks[0]] !== null) {
490
            // not using autogenerated value
491
            //$id = $d[$this->getPrimaryKey()];
492 1
            $id = $d[$pks[0]];
493 1
        } else {
494 11
            $id = $this->tableManager->getDbAdapter()->getDriver()->getLastGeneratedValue();
495
        }
496
497 12
        return $this->findOrFail($id);
498
    }
499
500
501
    /**
502
     * Insert on duplicate key
503
     *
504
     * @param array|ArrayObject $data
505
     * @param array|null $duplicate_exclude
506
     * @param boolean $validate_datatypes ensure data are compatible with database columns datatypes
507
     *
508
     * @throws Exception\ColumnNotFoundException
509
     * @throws Exception\RecordNotFoundException
510
     * @throws Exception\ForeignKeyException when insertion failed because of an invalid foreign key
511
     * @throws Exception\DuplicateEntryException when insertion failed because of an invalid foreign key
512
     * @throws Exception\NotNullException when insertion failed because a column cannot be null
513
     *
514
     * @return Record|false
515
     */
516 27
    public function insertOnDuplicateKey($data, array $duplicate_exclude = [], $validate_datatypes = false)
517
    {
518 27
        $platform = $this->tableManager->getDbAdapter()->platform;
519
/*
520
        $unique_keys   = $this->tableManager->metadata()->getUniqueKeys($this->prefixed_table);
521
        $unique_keys[]
522
        $primary_keys = $this->getPrimaryKeys();
523
524
        $uniques = array_merge(array($unique_keys, 'primary_keys' => $primary_keys));
525
        $cols = ['index2', 'index1'];
526
        $data = ['index1' => 'coool', 'index2' => 'hello'];
527
528
529
        $unique_found = false;
530
        $data_columns = array_keys($data);
531
        var_dump($uniques);
532
        while ($cols = array_pop($uniques) && !$unique_found) {
533
534
            var_dump($cols);
535
            die();
536
            $intersect = array_intersect($cols, $data_columns);
537
            $unique_found = ($intersect == $data_columns);
538
        }
539
540
        dump($intersect);
541
542
        die();
543
        $intersect = array_intersect($cols, array_keys($data));
544
        dump($intersect);
545
        dump($intersect == $cols);
546
        die();
547
548
        foreach($unique_keys as $index => $columns) {
549
        //    if (array_diff_key($duplicate_exclude, $d))
550
551
        }
552
dump($unique_keys);
553
dump($primary_keys);
554
die();
555
 *
556
 */
557 27
        $primary = $this->getPrimaryKey();
558
559 27
        if ($data instanceof ArrayObject) {
560 1
            $d = (array) $data;
561 1
        } else {
562 27
            $d = $data;
563
        }
564
565 27
        $this->checkDataColumns($d);
566 26
        $this->checkDataColumns(array_fill_keys($duplicate_exclude, null));
567
568 26
        if ($validate_datatypes) {
569 1
            $this->validateDatatypes($d);
570 1
        }
571
572 26
        $insert = $this->sql->insert($this->prefixed_table);
573 26
        $insert->values($d);
574 26
        $sql_string = $this->sql->getSqlStringForSqlObject($insert);
575
576
577 26
        $extras = [];
578
579
        /**
580
         * No reason to exclude primary key from
581
         */
582
        //$excluded_columns = array_merge($duplicate_exclude, array($primary));
583 26
        $excluded_columns = $duplicate_exclude;
584
585 26
        foreach ($d as $column => $value) {
586 26
            if (!in_array($column, $excluded_columns)) {
587 26
                $v = ($value === null) ? 'NULL' : $v = $platform->quoteValue($value);
588 26
                $extras[] = $platform->quoteIdentifier($column) . ' = ' . $v;
589 26
            }
590 26
        }
591 26
        $sql_string .= ' on duplicate key update ' . implode(',', $extras);
592
593
        try {
594 26
            $this->executeStatement($sql_string);
595 26
        } catch (\Exception $e) {
596 1
            $messages = [];
597 1
            $ex = $e;
598
            do {
599 1
                $messages[] = $ex->getMessage();
600 1
            } while ($ex = $ex->getPrevious());
601 1
            $msg = implode(', ', array_unique($messages));
602 1
            $message = __METHOD__ . ": failed, $msg [ $sql_string ]";
603 1
            throw new Exception\RuntimeException($message);
604
        }
605
606 25
        if (array_key_exists($primary, $d)) {
607
            // not using autogenerated value
608 1
            $pk_value = $d[$primary];
609 1
            $record = $this->findOrFail($pk_value);
610 1
        } else {
611 25
            $id = $this->tableManager->getDbAdapter()->getDriver()->getLastGeneratedValue();
612
613
            // This test is not made with id !== null, understand why before changing
614 25
            if ($id > 0) {
615 11
                $pk_value = $id;
616 11
                $record = $this->find($pk_value);
617 11
            } else {
618
                // if the id was not generated, we have to guess on which key
619
                // the duplicate has been fired
620 15
                $unique_keys = $this->tableManager->metadata()->getUniqueKeys($this->prefixed_table);
621 15
                $data_columns = array_keys($d);
622
623
                // Uniques keys could be
624
                // array(
625
                //      'unique_idx' => array('categ', 'legacy_mapping'),
626
                //      'unique_idx_2' => array('test', 'test2')
627
                //      )
628
                //
629
630 15
                $record = false;
631
632 15
                foreach ($unique_keys as $index_name => $unique_columns) {
633
                    //echo "On duplicate key\n\n $index_name \n";
634 15
                    $intersect = array_intersect($data_columns, $unique_columns);
635 15
                    if (count($intersect) == count($unique_columns)) {
636
                        // Try to see if we can find a record with the key
637 15
                        $conditions = [];
638 15
                        foreach ($intersect as $key) {
639 15
                            $conditions[$key] = $d[$key];
640 15
                        }
641 15
                        $record = $this->findOneBy($conditions);
642 15
                        if ($record) {
643
                            //$found = true;
644
                            //$pk_value = $record[$primary];
645 15
                            break;
646
                        }
647
                    }
648 15
                }
649
650
                // I cannot write a test case for that
651
                // It should never happen but in case :
652
                //@codeCoverageIgnoreStart
653
                if (!$record) {
654
                    throw new \Exception(__METHOD__ . ": after probing all unique keys in table '{$this->table}', cannot dertermine which one was fired when using on duplicate key.");
655
                }
656
                //@codeCoverageIgnoreEnd
657
            }
658
        }
659 25
        return $record;
660
    }
661
662
663
    /**
664
     * Return table relations (foreign keys infos)
665
     *
666
     * @return array
667
     */
668 1
    public function getRelations()
669
    {
670 1
        return $this->tableManager->metadata()->getForeignKeys($this->prefixed_table);
671
    }
672
673
674
    /**
675
     * Return a record object for this table
676
     * If $data is specified, the record will be filled with the
677
     * data present in the associative array
678
     *
679
     *
680
     * If $throwException is true, if any non existing column is found
681
     * an error will be thrown
682
     *
683
     * @throws Exception\ColumnNotFoundException if $ignore_invalid_columns is false and some columns does not exists in table
684
     *
685
     * @param array|ArrayObject $data associative array containing initial data
686
     * @param boolean $ignore_invalid_columns if true will throw an exception if a column does not exists
687
     * @return Record
688
     */
689 50
    public function record($data = [], $ignore_invalid_columns = true)
690
    {
691 50
        if (!$ignore_invalid_columns) {
692 1
            $this->checkDataColumns((array) $data);
693
        }
694 49
        $record = new Record((array) $data, $this);
695
696 49
        $record->setState(Record::STATE_NEW);
697 49
        return $record;
698
    }
699
700
701
    /**
702
     * Return table relation reader
703
     *
704
     * @return Table\Relation
705
     */
706 5
    public function relation()
707
    {
708 5
        if ($this->relation === null) {
709 5
            $this->relation = new Table\Relation($this);
710 5
        }
711 5
        return $this->relation;
712
    }
713
714
    /**
715
     * Return table primary keys
716
     *
717
     * @throws Exception\PrimaryKeyNotFoundException when no pk
718
     * @throws Exception\RuntimeException when it cannot determine primary key on table
719
     *
720
     *
721
     * @return array
722
     */
723 54
    public function getPrimaryKeys()
724
    {
725 54
        if ($this->primary_keys === null) {
726
            try {
727 54
                $this->primary_keys = $this->tableManager->metadata()->getPrimaryKeys($this->prefixed_table);
728 54
            } catch (\Soluble\Schema\Exception\NoPrimaryKeyException $e) {
729 2
                throw new Exception\PrimaryKeyNotFoundException(__METHOD__ . ': ' . $e->getMessage());
730
            //@codeCoverageIgnoreStart
731
            } catch (\Soluble\Schema\Exception\ExceptionInterface $e) {
732
                throw new Exception\RuntimeException(__METHOD__ . ": Cannot determine primary key on table " . $this->prefixed_table);
733
            }
734
            //@codeCoverageIgnoreEnd
735 52
        }
736 52
        return $this->primary_keys;
737
    }
738
739
    /**
740
     * Return primary key, if multiple primary keys found will
741
     * throw an exception
742
     *
743
     * @throws Exception\PrimaryKeyNotFoundException when no pk found
744
     * @throws Exception\MultiplePrimaryKeysFoundException when multiple primary keys found
745
     * @throws Exception\RuntimeException when it cannot determine primary key on table
746
     *
747
     * @return int|string
748
     */
749 29
    public function getPrimaryKey()
750
    {
751 29
        if ($this->primary_key === null) {
752 29
            $pks = $this->getPrimaryKeys();
753 29
            if (count($pks) > 1) {
754 1
                throw new Exception\MultiplePrimaryKeysFoundException(__METHOD__ . ": Error getting unique primary key on table, multiple found on table " . $this->prefixed_table);
755
            }
756 28
            $this->primary_key = $pks[0];
757 28
        }
758 28
        return $this->primary_key;
759
    }
760
761
    /**
762
     * Return list of table columns
763
     *
764
     * @throws Soluble\Db\Metadata\Exception\InvalidArgumentException
765
     * @throws Soluble\Db\Metadata\Exception\ErrorException
766
     * @throws Soluble\Db\Metadata\Exception\ExceptionInterface
767
     * @throws Soluble\Db\Metadata\Exception\TableNotFoundException
768
     *
769
     * @return array
770
     */
771 50
    public function getColumnsInformation()
772
    {
773 50
        if ($this->column_information === null) {
774 50
            $this->column_information = $this->tableManager
775 50
                    ->metadata()
776 50
                    ->getColumnsInformation($this->prefixed_table);
777 50
        }
778 50
        return $this->column_information;
779
    }
780
781
    /**
782
     * Return the original table name
783
     *
784
     * @return string
785
     */
786 3
    public function getTableName()
787
    {
788 3
        return $this->table;
789
    }
790
791
    /**
792
     * Return the prefixed table
793
     *
794
     * @return string
795
     */
796 1
    public function getPrefixedTableName()
797
    {
798 1
        return $this->prefixed_table;
799
    }
800
801
802
    /**
803
     * Return underlying table manager
804
     *
805
     * @return TableManager
806
     */
807 21
    public function getTableManager()
808
    {
809 21
        return $this->tableManager;
810
    }
811
812
    /**
813
     * Execute a statement
814
     *
815
     * @todo move to driver if not will only support MySQL
816
     *
817
     * @throws Exception\ForeignKeyException when insertion failed because of an invalid foreign key
818
     * @throws Exception\DuplicateEntryException when insertion failed because of an invalid foreign key
819
     * @throws Exception\NotNullException when insertion failed because a column cannot be null
820
     * @throws Exception\RuntimeException when insertion failed for another reason
821
     *
822
     * @param string|PreparableSqlInterface $sqlObject
823
     * @return \Zend\Db\Adapter\Driver\ResultInterface
824
     */
825 39
    protected function executeStatement($sqlObject)
826
    {
827 39
        if ($sqlObject instanceof PreparableSqlInterface) {
828 19
            $statement = $this->sql->prepareStatementForSqlObject($sqlObject);
829 39
        } elseif (is_string($sqlObject)) {
830 26
            $statement = $this->tableManager->getDbAdapter()->createStatement($sqlObject);
831 26
        } else {
832
            //@codeCoverageIgnoreStart
833
            throw new Exception\InvalidArgumentException(__METHOD__ . ': expects sqlObject to be string or PreparableInterface');
834
             //@codeCoverageIgnoreEnd
835
        }
836
        try {
837 39
            $result = $statement->execute();
838 39
        } catch (\Exception $e) {
839
            // In ZF2, PDO_Mysql and MySQLi return different exception,
840
            // attempt to normalize by catching one exception instead
841
            // of RuntimeException and InvalidQueryException
842
843 7
            $messages = [];
844 7
            $ex = $e;
845
            do {
846 7
                $messages[] = $ex->getMessage();
847 7
            } while ($ex = $ex->getPrevious());
848 7
            $message = implode(', ', array_unique($messages));
849
850 7
            $lmsg = __METHOD__ . ':' . strtolower($message) . '(code:' . $e->getCode() . ')';
851
852 7
            if (strpos($lmsg, 'cannot be null') !== false) {
853
                // Integrity constraint violation: 1048 Column 'non_null_column' cannot be null
854 1
                $rex = new Exception\NotNullException(__METHOD__ . ': ' . $message, $e->getCode(), $e);
855 1
                throw $rex;
856 6
            } elseif (strpos($lmsg, 'duplicate entry') !== false) {
857 3
                $rex = new Exception\DuplicateEntryException(__METHOD__ . ': ' . $message, $e->getCode(), $e);
858 3
                throw $rex;
859 3
            } elseif (strpos($lmsg, 'constraint violation') !== false ||
860 3
                    strpos($lmsg, 'foreign key') !== false) {
861 1
                $rex = new Exception\ForeignKeyException(__METHOD__ . ': ' . $message, $e->getCode(), $e);
862 1
                throw $rex;
863
            } else {
864 2
                if ($sqlObject instanceof SqlInterface) {
865 1
                    $sql_string = $sqlObject->getSqlString($this->sql->getAdapter()->getPlatform());
866 1
                } else {
867 1
                    $sql_string = $sqlObject;
868
                }
869 2
                $iqex = new Exception\RuntimeException(__METHOD__ . ': ' . $message . "[$sql_string]", $e->getCode(), $e);
870 2
                throw $iqex;
871
            }
872
        }
873 35
        return $result;
874
    }
875
876
    /**
877
     * Return primary key predicate
878
     *
879
     * @param integer|string|array $id
880
     *
881
     * @throws Exception\InvalidArgumentException
882
     * @throws Exception\PrimaryKeyNotFoundException
883
     * @return array predicate
884
     */
885 37
    protected function getPrimaryKeyPredicate($id)
886
    {
887 37
        if (!is_scalar($id) && !is_array($id)) {
888 2
            throw new Exception\InvalidArgumentException(__METHOD__ . ": Id must be scalar or array, type " . gettype($id) . " received");
889
        }
890 36
        $keys = $this->getPrimaryKeys();
891 35
        if (count($keys) == 1) {
892 33
            $pk = $keys[0];
893 33
            if (!is_scalar($id)) {
894 3
                $type = gettype($id);
895 3
                throw new Exception\InvalidArgumentException(__METHOD__ . ": invalid primary key value. Table '{$this->table}' has a single primary key '$pk'. Argument must be scalar, '$type' given");
896
            }
897 30
            $predicate = [$pk => $id];
898 30
        } else {
899 4
            if (!is_array($id)) {
900 1
                $pks = implode(',', $keys);
901 1
                $type = gettype($id);
902 1
                throw new Exception\InvalidArgumentException(__METHOD__ . ": invalid primary key value. Table '{$this->table}' has multiple primary keys '$pks'. Argument must be an array, '$type' given");
903
            }
904
905 3
            $matched_keys = array_diff($id, $keys);
906 3
            if (count($matched_keys) == count($keys)) {
907 2
                $predicate = $matched_keys;
908 2
            } else {
909 1
                $pks = implode(',', $keys);
910 1
                $vals = implode(',', $matched_keys);
911 1
                throw new Exception\InvalidArgumentException(__METHOD__ . ": incomplete primary key value. Table '{$this->table}' has multiple primary keys '$pks', values received '$vals'");
912
            }
913
        }
914 30
        return $predicate;
915
    }
916
917
    /**
918
     * Check if all columns exists in table
919
     *
920
     * @param array $data
921
     * @throws Exception\ColumnNotFoundException
922
     */
923 42
    protected function checkDataColumns(array $data)
924
    {
925 42
        $diff = array_diff_key($data, $this->getColumnsInformation());
926 42
        if (count($diff) > 0) {
927 3
            $msg = implode(',', array_keys($diff));
928 3
            throw new Exception\ColumnNotFoundException(__METHOD__ . ": some specified columns '$msg' does not exists in table {$this->table}.");
929
        }
930 39
    }
931
932
    /**
933
     * Validate data with database column datatype
934
     *
935
     * @param array $data
936
     * @return void
937
     */
938 3
    protected function validateDatatypes(array $data)
939
    {
940
        // @todo code for validating datatypes
941
        // integer -> numeric
942
        // etc, etc...
943 3
        $columnInfo = $this->getColumnsInformation();
944 3
        foreach ($data as $column => $value) {
945
            // checks on types
946 3
        }
947 3
    }
948
}
949