Completed
Push — master ( e1ce3b...f144d1 )
by Anton
14s queued 12s
created

src/Mapper/DatabaseMapper.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * Cycle DataMapper ORM
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM\Mapper;
13
14
use Cycle\ORM\Command\CommandInterface;
15
use Cycle\ORM\Command\ContextCarrierInterface;
16
use Cycle\ORM\Command\Database\Delete;
17
use Cycle\ORM\Command\Database\Insert;
18
use Cycle\ORM\Command\Database\Update;
19
use Cycle\ORM\Context\ConsumerInterface;
20
use Cycle\ORM\Exception\MapperException;
21
use Cycle\ORM\Heap\Node;
22
use Cycle\ORM\Heap\State;
23
use Cycle\ORM\MapperInterface;
24
use Cycle\ORM\ORMInterface;
25
use Cycle\ORM\Schema;
26
use Cycle\ORM\Select;
27
28
/**
29
 * Provides basic capabilities to work with entities persisted in SQL databases.
30
 */
31
abstract class DatabaseMapper implements MapperInterface
32
{
33
    /** @var Select\SourceInterface */
34
    protected $source;
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\SourceProviderInterface) {
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 queueCreate($entity, Node $node, State $state): ContextCarrierInterface
94
    {
95
        $columns = $this->fetchFields($entity);
96
97
        // sync the state
98
        $state->setStatus(Node::SCHEDULED_INSERT);
99
        $state->setData($columns);
100
101
        $columns[$this->primaryKey] = $columns[$this->primaryKey] ?? $this->nextPrimaryKey();
0 ignored issues
show
Are you sure the usage of $this->nextPrimaryKey() targeting Cycle\ORM\Mapper\DatabaseMapper::nextPrimaryKey() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

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

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

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

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