Passed
Push — master ( f39217...bc6df6 )
by y
02:20
created

EAV::getValueType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Helix\DB;
4
5
use Helix\DB;
6
7
/**
8
 * Array storage in an extension table.
9
 *
10
 * @method static static factory(DB $db, string $name, string $valueType = 'string')
11
 */
12
class EAV extends Table {
13
14
    /**
15
     * @var string
16
     */
17
    protected $valueType;
18
19
    /**
20
     * @param DB $db
21
     * @param string $name
22
     * @param string $valueType PHP-native scalar type (implied nullable).
23
     */
24
    public function __construct (DB $db, string $name, string $valueType = 'string') {
25
        parent::__construct($db, $name, ['entity', 'attribute', 'value']);
26
        $this->valueType = $valueType;
27
    }
28
29
    /**
30
     * Whether an entity has an attribute.
31
     *
32
     * @param int $id
33
     * @param string $attribute
34
     * @return bool
35
     */
36
    public function exists (int $id, string $attribute): bool {
37
        $statement = $this->cache(__FUNCTION__, function() {
38
            return $this->select(['COUNT(*) > 0'])->where('entity = ? AND attribute = ?')->prepare();
39
        });
40
        $exists = (bool)$statement([$id, $attribute])->fetchColumn();
41
        $statement->closeCursor();
42
        return $exists;
43
    }
44
45
    /**
46
     * Pivots and self-joins to return a {@link Select} for the `entity` column,
47
     * matching on attribute and value.
48
     *
49
     * @see DB::match()
50
     *
51
     * @param array $match `[attribute => value]`. If empty, selects all IDs for entities having at least one attribute.
52
     * @return Select
53
     */
54
    public function find (array $match) {
55
        $select = $this->select([$this['entity']]);
56
        $prior = $this;
57
        foreach ($match as $attribute => $value) {
58
            $alias = $this->setName("{$this}__{$attribute}");
59
            $select->join("{$this} AS {$alias}",
60
                $alias['entity']->isEqual($prior['entity']),
61
                $alias['attribute']->isEqual($attribute),
62
                $this->db->match($alias['value'], $value)
63
            );
64
            $prior = $alias;
65
        }
66
        $select->group($this['entity']);
67
        return $select;
68
    }
69
70
    /**
71
     * @return string
72
     */
73
    final public function getValueType (): string {
74
        return $this->valueType;
75
    }
76
77
    /**
78
     * Returns an entity's attributes.
79
     *
80
     * @param int $id
81
     * @return array `[attribute => value]`
82
     */
83
    public function load (int $id): array {
84
        $statement = $this->cache(__FUNCTION__, function() {
85
            $select = $this->select(['attribute', 'value']);
86
            $select->where('entity = ?');
87
            $select->order('attribute');
88
            return $select->prepare();
89
        });
90
        return array_map([$this, 'setType'], $statement([$id])->fetchAll(DB::FETCH_KEY_PAIR));
91
    }
92
93
    /**
94
     * Returns associative attribute-value arrays for the given IDs.
95
     *
96
     * @param int[] $ids
97
     * @return array[] `[id => attribute => value]
98
     */
99
    public function loadAll (array $ids): array {
100
        if (empty($ids)) {
101
            return [];
102
        }
103
        if (count($ids) === 1) {
104
            return [current($ids) => $this->load(current($ids))];
105
        }
106
        $loadAll = $this->select(['entity', 'attribute', 'value'])
107
            ->where($this->db->match('entity', $this->db->marks($ids)))
108
            ->order('entity, attribute');
109
        $values = array_fill_keys($ids, []);
110
        foreach ($loadAll->getEach(array_values($ids)) as $row) {
111
            $values[$row['entity']][$row['attribute']] = $this->setType($row['value']);
112
        }
113
        return $values;
114
    }
115
116
    /**
117
     * Upserts an entity's attributes with those given.
118
     * Stored attributes not given here are pruned.
119
     *
120
     * @param int $id
121
     * @param array $values `[attribute => value]`
122
     * @return $this
123
     */
124
    public function save (int $id, array $values) {
125
        $this->delete([
126
            $this['entity']->isEqual($id),
127
            $this['attribute']->isNotEqual(array_keys($values))
128
        ]);
129
        $statement = $this->cache(__FUNCTION__, function() {
130
            if ($this->db->isSQLite()) {
131
                return $this->db->prepare(
132
                    "INSERT INTO {$this} (entity,attribute,value) VALUES (?,?,?)"
133
                    . " ON CONFLICT (entity,attribute) DO UPDATE SET value=excluded.value"
134
                );
135
            }
136
            return $this->db->prepare(
137
                "INSERT INTO {$this} (entity,attribute,value) VALUES (?,?,?)"
138
                . " ON DUPLICATE KEY UPDATE value=VALUES(value)"
139
            );
140
        });
141
        foreach ($values as $attribute => $value) {
142
            $statement->execute([$id, $attribute, $value]);
143
        }
144
        $statement->closeCursor();
145
        return $this;
146
    }
147
148
    /**
149
     * @param mixed $value
150
     * @return null|scalar
151
     */
152
    protected function setType ($value) {
153
        if (isset($value)) {
154
            settype($value, $this->valueType);
155
        }
156
        return $value;
157
    }
158
}