EAV::getType()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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