Passed
Push — master ( 355930...71a503 )
by Anton
01:51
created

DatabaseMapper::nextPrimaryKey()   A

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
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
namespace Spiral\Cycle\Mapper;
11
12
use Spiral\Cycle\Command\CommandInterface;
13
use Spiral\Cycle\Command\ContextCarrierInterface;
14
use Spiral\Cycle\Command\Database\Delete;
15
use Spiral\Cycle\Command\Database\Insert;
16
use Spiral\Cycle\Command\Database\Update;
17
use Spiral\Cycle\Context\ConsumerInterface;
18
use Spiral\Cycle\Exception\MapperException;
19
use Spiral\Cycle\Heap\Node;
20
use Spiral\Cycle\Heap\State;
21
use Spiral\Cycle\ORMInterface;
22
use Spiral\Cycle\Schema;
23
use Spiral\Cycle\Select;
24
25
/**
26
 * Provides basic capabilities to work with entities persisted in SQL databases.
27
 */
28
abstract class DatabaseMapper implements MapperInterface
29
{
30
    /** @var Select\SourceInterface */
31
    protected $source;
32
33
    /** @var null|RepositoryInterface */
34
    protected $repository;
35
36
    /** @var ORMInterface */
37
    protected $orm;
38
39
    /** @var string */
40
    protected $role;
41
42
    /** @var array */
43
    protected $columns;
44
45
    /** @var array */
46
    protected $fields;
47
48
    /** @var string */
49
    protected $primaryKey;
50
51
    /** @var string */
52
    protected $primaryColumn;
53
54
    /**
55
     * @param ORMInterface $orm
56
     * @param string       $role
57
     */
58
    public function __construct(ORMInterface $orm, string $role)
59
    {
60
        if (!$orm instanceof Select\SourceFactoryInterface) {
61
            throw new MapperException("Source factory is missing");
62
        }
63
64
        $this->orm = $orm;
65
        $this->role = $role;
66
67
        $this->source = $orm->getSource($role);
68
        $this->columns = $orm->getSchema()->define($role, Schema::COLUMNS);
69
        $this->primaryKey = $orm->getSchema()->define($role, Schema::PRIMARY_KEY);
70
        $this->primaryColumn = $this->columns[$this->primaryKey] ?? $this->primaryKey;
71
72
        // Resolve field names
73
        foreach ($this->columns as $name => $column) {
74
            if (!is_numeric($name)) {
75
                $this->fields[] = $name;
76
            } else {
77
                $this->fields[] = $column;
78
            }
79
        }
80
    }
81
82
    /**
83
     * @inheritdoc
84
     */
85
    public function getRole(): string
86
    {
87
        return $this->role;
88
    }
89
90
    /**
91
     * @inheritdoc
92
     */
93
    public function getRepository(): RepositoryInterface
94
    {
95
        if (!empty($this->repository)) {
96
            return $this->repository;
97
        }
98
99
        $selector = new Select($this->orm, $this->role);
100
        $selector->constrain($this->source->getConstrain(Select\SourceInterface::DEFAULT_CONSTRAIN));
101
102
        $repositoryClass = $this->orm->getSchema()->define($this->role, Schema::REPOSITORY) ?? Repository::class;
103
104
        return $this->repository = new $repositoryClass($selector);
105
    }
106
107
    /**
108
     * @inheritdoc
109
     */
110
    public function queueCreate($entity, State $state): ContextCarrierInterface
111
    {
112
        $columns = $this->fetchFields($entity);
113
114
        // sync the state
115
        $state->setStatus(Node::SCHEDULED_INSERT);
116
        $state->setData($columns);
117
118
        $columns[$this->primaryKey] = $this->nextPrimaryKey();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $columns[$this->primaryKey] is correct as $this->nextPrimaryKey() targeting Spiral\Cycle\Mapper\Data...apper::nextPrimaryKey() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
119
        if (is_null($columns[$this->primaryKey])) {
120
            unset($columns[$this->primaryKey]);
121
        }
122
123
        $insert = new Insert(
124
            $this->source->getDatabase(),
125
            $this->source->getTable(),
126
            $this->mapColumns($columns)
127
        );
128
129
        if (!array_key_exists($this->primaryKey, $columns)) {
130
            $insert->forward(Insert::INSERT_ID, $state, $this->primaryKey);
131
        } else {
132
            $insert->forward($this->primaryKey, $state, $this->primaryKey);
133
        }
134
135
        return $insert;
136
    }
137
138
    /**
139
     * @inheritdoc
140
     */
141
    public function queueUpdate($entity, State $state): ContextCarrierInterface
142
    {
143
        $data = $this->fetchFields($entity);
144
145
        // in a future mapper must support solid states
146
        $changes = array_udiff_assoc($data, $state->getData(), [static::class, 'compare']);
147
        unset($changes[$this->primaryKey]);
148
149
        $changedColumns = $this->mapColumns($changes);
150
151
        $update = new Update($this->source->getDatabase(), $this->source->getTable(), $changedColumns);
152
        $state->setStatus(Node::SCHEDULED_UPDATE);
153
        $state->setData($changes);
154
155
        // we are trying to update entity without PK right now
156
        $state->forward(
157
            $this->primaryKey,
158
            $update,
159
            $this->primaryColumn,
160
            true,
161
            ConsumerInterface::SCOPE
162
        );
163
164
        return $update;
165
    }
166
167
    /**
168
     * @inheritdoc
169
     */
170
    public function queueDelete($entity, State $state): CommandInterface
171
    {
172
        $delete = new Delete($this->source->getDatabase(), $this->source->getTable());
173
        $state->setStatus(Node::SCHEDULED_DELETE);
174
        $state->decClaim();
175
176
        $delete->waitScope($this->primaryColumn);
177
        $state->forward(
178
            $this->primaryKey,
179
            $delete,
180
            $this->primaryColumn,
181
            true,
182
            ConsumerInterface::SCOPE
183
        );
184
185
        return $delete;
186
    }
187
188
    /**
189
     * Generate next sequential entity ID. Return null to use autoincrement value.
190
     *
191
     * @return mixed|null
192
     */
193
    protected function nextPrimaryKey()
194
    {
195
        return null;
196
    }
197
198
    /**
199
     * Get entity columns.
200
     *
201
     * @param object $entity
202
     * @return array
203
     */
204
    abstract protected function fetchFields($entity): array;
205
206
    /**
207
     * Map internal field names to database specific column names.
208
     *
209
     * @param array $columns
210
     * @return array
211
     */
212
    protected function mapColumns(array $columns): array
213
    {
214
        $result = [];
215
        foreach ($columns as $column => $value) {
216
            if (array_key_exists($column, $this->columns)) {
217
                $result[$this->columns[$column]] = $value;
218
            } else {
219
                $result[$column] = $value;
220
            }
221
        }
222
223
        return $result;
224
    }
225
226
    /**
227
     * @param mixed $a
228
     * @param mixed $b
229
     * @return int
230
     */
231
    protected static function compare($a, $b): int
232
    {
233
        if ($a == $b) {
234
            return 0;
235
        }
236
237
        return ($a > $b) ? 1 : -1;
238
    }
239
}