Passed
Push — master ( 9676b0...44a032 )
by Anton
02:07
created

src/Mapper/DatabaseMapper.php (2 issues)

Labels
1
<?php
2
/**
3
 * Cycle DataMapper ORM
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
declare(strict_types=1);
9
10
namespace Cycle\ORM\Mapper;
11
12
use Cycle\ORM\Command\CommandInterface;
13
use Cycle\ORM\Command\ContextCarrierInterface;
14
use Cycle\ORM\Command\Database\Delete;
15
use Cycle\ORM\Command\Database\Insert;
16
use Cycle\ORM\Command\Database\Update;
17
use Cycle\ORM\Context\ConsumerInterface;
18
use Cycle\ORM\Exception\MapperException;
19
use Cycle\ORM\Heap\Node;
20
use Cycle\ORM\Heap\State;
21
use Cycle\ORM\MapperInterface;
22
use Cycle\ORM\ORMInterface;
23
use Cycle\ORM\Schema;
24
use Cycle\ORM\Select;
25
26
/**
27
 * Provides basic capabilities to work with entities persisted in SQL databases.
28
 */
29
abstract class DatabaseMapper implements MapperInterface
30
{
31
    /** @var Select\SourceInterface */
32
    protected $source;
33
34
    /** @var ORMInterface */
35
    protected $orm;
36
37
    /** @var string */
38
    protected $role;
39
40
    /** @var array */
41
    protected $columns;
42
43
    /** @var array */
44
    protected $fields;
45
46
    /** @var string */
47
    protected $primaryKey;
48
49
    /** @var string */
50
    protected $primaryColumn;
51
52
    /**
53
     * @param ORMInterface $orm
54
     * @param string       $role
55
     */
56
    public function __construct(ORMInterface $orm, string $role)
57
    {
58
        if (!$orm instanceof Select\SourceProviderInterface) {
0 ignored issues
show
$orm is always a sub-type of Cycle\ORM\Select\SourceProviderInterface.
Loading history...
59
            throw new MapperException("Source factory is missing");
60
        }
61
62
        $this->orm = $orm;
63
        $this->role = $role;
64
65
        $this->source = $orm->getSource($role);
66
        $this->columns = $orm->getSchema()->define($role, Schema::COLUMNS);
67
        $this->primaryKey = $orm->getSchema()->define($role, Schema::PRIMARY_KEY);
68
        $this->primaryColumn = $this->columns[$this->primaryKey] ?? $this->primaryKey;
69
70
        // Resolve field names
71
        foreach ($this->columns as $name => $column) {
72
            if (!is_numeric($name)) {
73
                $this->fields[] = $name;
74
            } else {
75
                $this->fields[] = $column;
76
            }
77
        }
78
    }
79
80
    /**
81
     * @inheritdoc
82
     */
83
    public function getRole(): string
84
    {
85
        return $this->role;
86
    }
87
88
    /**
89
     * @inheritdoc
90
     */
91
    public function queueCreate($entity, Node $node, State $state): ContextCarrierInterface
92
    {
93
        $columns = $this->fetchFields($entity);
94
95
        // sync the state
96
        $state->setStatus(Node::SCHEDULED_INSERT);
97
        $state->setData($columns);
98
99
        $columns[$this->primaryKey] = $this->nextPrimaryKey();
0 ignored issues
show
Are you sure the assignment to $columns[$this->primaryKey] is correct as $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 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...
100
        if (is_null($columns[$this->primaryKey])) {
101
            unset($columns[$this->primaryKey]);
102
        }
103
104
        $insert = new Insert(
105
            $this->source->getDatabase(),
106
            $this->source->getTable(),
107
            $this->mapColumns($columns)
108
        );
109
110
        if (!array_key_exists($this->primaryKey, $columns)) {
111
            $insert->forward(Insert::INSERT_ID, $state, $this->primaryKey);
112
        } else {
113
            $insert->forward($this->primaryKey, $state, $this->primaryKey);
114
        }
115
116
        return $insert;
117
    }
118
119
    /**
120
     * @inheritdoc
121
     */
122
    public function queueUpdate($entity, Node $node, State $state): ContextCarrierInterface
123
    {
124
        $data = $this->fetchFields($entity);
125
126
        // in a future mapper must support solid states
127
        $changes = array_udiff_assoc($data, $state->getData(), [static::class, 'compare']);
128
        unset($changes[$this->primaryKey]);
129
130
        $changedColumns = $this->mapColumns($changes);
131
132
        $update = new Update($this->source->getDatabase(), $this->source->getTable(), $changedColumns);
133
        $state->setStatus(Node::SCHEDULED_UPDATE);
134
        $state->setData($changes);
135
136
        // we are trying to update entity without PK right now
137
        $state->forward(
138
            $this->primaryKey,
139
            $update,
140
            $this->primaryColumn,
141
            true,
142
            ConsumerInterface::SCOPE
143
        );
144
145
        return $update;
146
    }
147
148
    /**
149
     * @inheritdoc
150
     */
151
    public function queueDelete($entity, Node $node, State $state): CommandInterface
152
    {
153
        $delete = new Delete($this->source->getDatabase(), $this->source->getTable());
154
        $state->setStatus(Node::SCHEDULED_DELETE);
155
        $state->decClaim();
156
157
        $delete->waitScope($this->primaryColumn);
158
        $state->forward(
159
            $this->primaryKey,
160
            $delete,
161
            $this->primaryColumn,
162
            true,
163
            ConsumerInterface::SCOPE
164
        );
165
166
        return $delete;
167
    }
168
169
    /**
170
     * Generate next sequential entity ID. Return null to use autoincrement value.
171
     *
172
     * @return mixed|null
173
     */
174
    protected function nextPrimaryKey()
175
    {
176
        return null;
177
    }
178
179
    /**
180
     * Get entity columns.
181
     *
182
     * @param object $entity
183
     * @return array
184
     */
185
    abstract protected function fetchFields($entity): array;
186
187
    /**
188
     * Map internal field names to database specific column names.
189
     *
190
     * @param array $columns
191
     * @return array
192
     */
193
    protected function mapColumns(array $columns): array
194
    {
195
        $result = [];
196
        foreach ($columns as $column => $value) {
197
            if (array_key_exists($column, $this->columns)) {
198
                $result[$this->columns[$column]] = $value;
199
            } else {
200
                $result[$column] = $value;
201
            }
202
        }
203
204
        return $result;
205
    }
206
207
    /**
208
     * @param mixed $a
209
     * @param mixed $b
210
     * @return int
211
     */
212
    protected static function compare($a, $b): int
213
    {
214
        if ($a == $b) {
215
            return 0;
216
        }
217
218
        return ($a > $b) ? 1 : -1;
219
    }
220
}