Passed
Push — master ( 5d6c6e...7e3b1d )
by y
01:51
created

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