Passed
Push — master ( af4849...ba71ff )
by y
01:17
created

Junction::offsetExists()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 CLASS COLUMN` or `@for CLASS COLUMN`
17
 */
18
class Junction extends Table {
19
20
    /**
21
     * @var string
22
     */
23
    protected $interface;
24
25
    /**
26
     * `[class => column name]`
27
     */
28
    protected $keys = [];
29
30
    /**
31
     * @param DB $db
32
     * @param string $interface
33
     */
34
    public function __construct (DB $db, string $interface) {
35
        $this->interface = $interface;
36
        try {
37
            $interface = new ReflectionClass($interface);
38
        }
39
        catch (ReflectionException $exception) {
40
            throw new LogicException('Unexpected ReflectionException', 0, $exception);
41
        }
42
        $doc = $interface->getDocComment();
43
        preg_match('/@junction\s+(?<table>\S+)/', $doc, $junction);
44
        $columns = [];
45
        foreach (explode("\n", $doc) as $line) {
46
            if (preg_match('/@for(eign)?\s+(?<class>\S+)\s+(?<column>\S+)/', $line, $foreign)) {
47
                $columns[] = $foreign['column'];
48
                $this->keys[$foreign['class']] = $foreign['column'];
49
            }
50
        }
51
        parent::__construct($db, $junction['table'], $columns);
52
    }
53
54
    /**
55
     * Returns the number of references to an entity in the junction.
56
     *
57
     * @param EntityInterface $entity
58
     * @return int
59
     */
60
    public function count (EntityInterface $entity): int {
61
        $key = $this->getKey($entity);
62
        $count = $this->cache("count.{$key}", function() use ($key) {
63
            return $this->select(['COUNT(*)'])->where("{$key}=:id")->prepare();
64
        });
65
        $count->execute(['id' => $entity->getId()]);
66
        return (int)$count->fetchColumn();
67
    }
68
69
    /**
70
     * Returns a {@link Select} for entities in a has-many relationship.
71
     *
72
     * The `Select` is literal and can be iterated directly.
73
     *
74
     * @param EntityInterface $owner
75
     * @param string $class
76
     * @return Select
77
     */
78
    public function getCollection (EntityInterface $owner, string $class): Select {
79
        $record = $this->db->getRecord($class);
80
        $select = $record->select();
81
        $select->join($this, $this[$class]->isEqual($record['id']));
82
        $select->where($this[get_class($owner)]->isEqual($owner->getId()));
83
        return $select;
84
    }
85
86
    /**
87
     * @return string
88
     */
89
    final public function getInterface (): string {
90
        return $this->interface;
91
    }
92
93
    /**
94
     * Returns the foreign key column name specified in `@foreign CLASS COLUMN`.
95
     *
96
     * @param EntityInterface|string $class
97
     * @return string
98
     */
99
    public function getKey ($class): string {
100
        if (is_object($class)) {
101
            $class = get_class($class);
102
        }
103
        return $this->keys[$class];
104
    }
105
106
    /**
107
     * Adds a row that links entities.
108
     *
109
     * One entity for each column should be given, unless the table allows `NULL`.
110
     *
111
     * The given array doesn't need to be keyed in any special way,
112
     * the columns are looked up based on class.
113
     *
114
     * @param EntityInterface[] $entities
115
     * @return int Rows affected.
116
     */
117
    public function link (array $entities): int {
118
        $apply = [];
119
        foreach ($entities as $entity) {
120
            $apply[$this->getKey($entity)] = $entity->getId();
121
        }
122
        return $this->apply($apply);
123
    }
124
125
    /**
126
     * Falls back on lookup by class name.
127
     *
128
     * @param string $name
129
     * @return bool
130
     */
131
    public function offsetExists ($name): bool {
132
        return isset($this->columns[$name]) or isset($this->columns[$this->keys[$name]]);
133
    }
134
135
    /**
136
     * Falls back on lookup by class name.
137
     *
138
     * @param string $name
139
     * @return Column
140
     */
141
    public function offsetGet ($name): Column {
142
        return $this->columns[$name] ?? $this->columns[$this->keys[$name]];
143
    }
144
145
    /**
146
     * Removes rows that contain all of the given entities.
147
     *
148
     * The given array doesn't need to be keyed in any special way,
149
     * the columns are looked up based on class.
150
     *
151
     * @param EntityInterface[] $entities
152
     * @return int Rows affected.
153
     */
154
    public function unlink (array $entities): int {
155
        $match = [];
156
        foreach ($entities as $entity) {
157
            $match[$this->getKey($entity)] = $entity->getId();
158
        }
159
        return $this->delete($match);
160
    }
161
162
}