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