Passed
Push — master ( 615897...95e8b1 )
by y
02:20
created

Junction::findAll()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 8
rs 10
cc 2
nc 2
nop 2
1
<?php
2
3
namespace Helix\DB;
4
5
use Helix\DB;
6
use ReflectionClass;
7
8
/**
9
 * Represents a junction table, derived from an annotated interface.
10
 *
11
 * Interface Annotations:
12
 *
13
 * - `@junction <TABLE>`
14
 * - `@foreign <COLUMN> <CLASS FQN>` or `@for <COLUMN> <CLASS FQN>`
15
 *
16
 * @method static static factory(DB $db, string $table, array $classes)
17
 *
18
 * @TODO Remove the `for` tag, it's vague.
19
 */
20
class Junction extends Table {
21
22
    protected const RX_JUNCTION = '/\*\h*@junction\h+(?<table>\w+)/i';
23
    protected const RX_FOREIGN = '/\*\h*@for(eign)?\h+(?<column>\w+)\h+(?<class>\S+)/i';
24
25
    /**
26
     * `[column => class]`
27
     *
28
     * @var string[]
29
     */
30
    protected $classes = [];
31
32
    /**
33
     * @param DB $db
34
     * @param string $interface
35
     * @return Junction
36
     */
37
    public static function fromInterface (DB $db, string $interface) {
38
        $ref = new ReflectionClass($interface);
39
        assert($ref->isInterface());
40
        $doc = $ref->getDocComment();
41
        preg_match(static::RX_JUNCTION, $doc, $junction);
42
        preg_match_all(static::RX_FOREIGN, $doc, $foreign, PREG_SET_ORDER);
43
        $classes = array_column($foreign, 'class', 'column');
44
        return static::factory($db, $junction['table'], $classes);
45
    }
46
47
    /**
48
     * @param DB $db
49
     * @param string $table
50
     * @param string[] $classes
51
     */
52
    public function __construct (DB $db, string $table, array $classes) {
53
        parent::__construct($db, $table, array_keys($classes));
54
        $this->classes = $classes;
55
    }
56
57
    /**
58
     * Returns a {@link Select} for entities referenced by a foreign key.
59
     *
60
     * The {@link Select} is literal and can be iterated directly.
61
     *
62
     * @param string $key The column referencing the class to collect.
63
     * @param array $match Keyed by junction column.
64
     * @return Select|EntityInterface[]
65
     */
66
    public function findAll (string $key, array $match = []) {
67
        $record = $this->getRecord($key);
68
        $select = $record->loadAll();
69
        $select->join($this, $this[$key]->isEqual($record['id']));
70
        foreach ($match as $a => $b) {
71
            $select->where($this->db->match($this[$a], $b));
72
        }
73
        return $select;
74
    }
75
76
    /**
77
     * @param string $column
78
     * @return string
79
     */
80
    final public function getClass (string $column): string {
81
        return $this->classes[$column];
82
    }
83
84
    /**
85
     * @return string[]
86
     */
87
    final public function getClasses () {
88
        return $this->classes;
89
    }
90
91
    /**
92
     * @param string $column
93
     * @return Record
94
     */
95
    public function getRecord (string $column) {
96
        return $this->db->getRecord($this->classes[$column]);
97
    }
98
99
    /**
100
     * @return Record[]
101
     */
102
    public function getRecords () {
103
        return array_map(fn($class) => $this->db->getRecord($class), $this->classes);
104
    }
105
106
    /**
107
     * `INSERT IGNORE` to link entities.
108
     *
109
     * @param int[] $ids Keyed by column.
110
     * @return int Rows affected.
111
     */
112
    public function link (array $ids): int {
113
        $statement = $this->cache(__FUNCTION__, function() {
114
            $columns = implode(',', array_keys($this->columns));
115
            $slots = implode(',', $this->db->slots(array_keys($this->columns)));
116
            if ($this->db->isSQLite()) {
117
                $sql = "INSERT OR IGNORE INTO {$this} ({$columns}) VALUES ({$slots})";
118
            }
119
            else {
120
                $sql = "INSERT IGNORE INTO {$this} ({$columns}) VALUES ({$slots})";
121
            }
122
            return $this->db->prepare($sql);
123
        });
124
        $affected = $statement($ids)->rowCount();
125
        $statement->closeCursor();
126
        return $affected;
127
    }
128
129
    /**
130
     * Alias for {@link delete()}
131
     *
132
     * @param array $ids Keyed by Column
133
     * @return int Rows affected
134
     */
135
    public function unlink (array $ids): int {
136
        return $this->delete($ids);
137
    }
138
}