Passed
Push — master ( 6df7c9...74bc2a )
by y
02:15
created

Junction   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 93
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 10
eloc 33
c 2
b 0
f 0
dl 0
loc 93
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A fromInterface() 0 16 4
A __construct() 0 3 1
A unlink() 0 2 1
A link() 0 16 2
A getCollection() 0 8 2
1
<?php
2
3
namespace Helix\DB;
4
5
use Helix\DB;
6
use LogicException;
7
use ReflectionClass;
8
use ReflectionException;
9
10
/**
11
 * Represents a junction table, derived from an annotated interface.
12
 *
13
 * Interface Annotations:
14
 *
15
 * - `@junction TABLE`
16
 * - `@foreign COLUMN CLASS` or `@for COLUMN CLASS`
17
 */
18
class Junction extends Table {
19
20
    /**
21
     * `[column => class]`
22
     *
23
     * @var string[]
24
     */
25
    protected $classes = [];
26
27
    /**
28
     * @param DB $db
29
     * @param string $interface
30
     * @return Junction
31
     */
32
    public static function fromInterface (DB $db, string $interface) {
33
        try {
34
            $ref = new ReflectionClass($interface);
35
        }
36
        catch (ReflectionException $exception) {
37
            throw new LogicException('Unexpected ReflectionException', 0, $exception);
38
        }
39
        $doc = $ref->getDocComment();
40
        $classes = [];
41
        foreach (explode("\n", $doc) as $line) {
42
            if (preg_match('/@for(eign)?\s+(?<column>\S+)\s+(?<class>\S+)/', $line, $foreign)) {
43
                $classes[$foreign['column']] = $foreign['class'];
44
            }
45
        }
46
        preg_match('/@junction\s+(?<table>\S+)/', $doc, $junction);
47
        return new static($db, $junction['table'], $classes);
48
    }
49
50
    /**
51
     * @param DB $db
52
     * @param string $table
53
     * @param string[] $classes
54
     */
55
    public function __construct (DB $db, string $table, array $classes) {
56
        parent::__construct($db, $table, array_keys($classes));
57
        $this->classes = $classes;
58
    }
59
60
    /**
61
     * Returns a {@link Select} for entities referenced by a foreign key.
62
     *
63
     * The {@link Select} is literal and can be iterated directly.
64
     *
65
     * @param string $key The column referencing the class to collect.
66
     * @param array $match Keyed by junction column.
67
     * @return Select
68
     */
69
    public function getCollection (string $key, array $match = []) {
70
        $record = $this->db->getRecord($this->classes[$key]);
71
        $select = $record->select();
72
        $select->join($this, $this[$key]->isEqual($record['id']));
73
        foreach ($match as $a => $b) {
74
            $select->where($this->db->match($this[$a], $b));
75
        }
76
        return $select;
77
    }
78
79
    /**
80
     * `INSERT IGNORE` to link entities.
81
     *
82
     * @param int[] $ids Keyed by column.
83
     * @return int Rows affected.
84
     */
85
    public function link (array $ids): int {
86
        $link = $this->cache(__FUNCTION__, function() {
87
            $columns = implode(',', array_keys($this->columns));
88
            $slots = implode(',', SQL::slots(array_keys($this->columns)));
89
            switch ($this->db) {
90
                case 'sqlite':
91
                    return $this->db->prepare(
92
                        "INSERT OR IGNORE INTO {$this} ({$columns}) VALUES ({$slots})"
93
                    );
94
                default:
95
                    return $this->db->prepare(
96
                        "INSERT IGNORE INTO {$this} ({$columns}) VALUES ({$slots})"
97
                    );
98
            }
99
        });
100
        return $link($ids)->rowCount();
101
    }
102
103
    /**
104
     * Alias for {@link delete()}
105
     *
106
     * @param array $ids Keyed by Column
107
     * @return int Rows affected
108
     */
109
    public function unlink (array $ids): int {
110
        return $this->delete($ids);
111
    }
112
}