Completed
Push — master ( 00ac10...57b531 )
by Filipe
03:06
created

EntityMapper::getData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.4285
cc 2
eloc 6
nc 2
nop 0
crap 2
1
<?php
2
3
/**
4
 * This file is part of slick/orm package
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Orm\Mapper;
11
12
use Slick\Database\Adapter\AdapterInterface;
13
use Slick\Database\RecordList;
14
use Slick\Database\Sql;
15
use Slick\Orm\Descriptor\EntityDescriptorInterface;
16
use Slick\Orm\Descriptor\EntityDescriptorRegistry;
17
use Slick\Orm\Descriptor\Field\FieldDescriptor;
18
use Slick\Orm\Entity\EntityCollection;
19
use Slick\Orm\EntityInterface;
20
use Slick\Orm\EntityMapperInterface;
21
use Slick\Orm\Orm;
22
23
/**
24
 * Generic Entity Mapper
25
 *
26
 * @package Slick\Orm\Mapper
27
 * @author  Filipe Silva <[email protected]>
28
 */
29
class EntityMapper implements EntityMapperInterface
30
{
31
    /**
32
     * @var EntityDescriptorInterface
33
     */
34
    protected $descriptor;
35
36
    /**
37
     * @var EntityInterface
38
     */
39
    protected $entity;
40
41
    /**
42
     * @var AdapterInterface
43
     */
44
    protected $adapter;
45
46
    /**
47
     * @var string
48
     */
49
    protected $entityClassName;
50
51
    /**
52
     * Saves current entity object to database
53
     *
54
     * Optionally saves only the partial data if $data argument is passed. If
55
     * no data is given al the field properties will be updated.
56
     *
57
     * @param array $data Partial data to save
58
     * @param EntityInterface $entity
59
     *
60
     * @return self|$this|EntityMapperInterface
61
     */
62 4
    public function save(EntityInterface $entity, array $data = [])
63
    {
64 4
        $this->entity = $entity;
65 4
        $query = $this->getUpdateQuery();
66 4
        $query->set($this->getData())
67 4
            ->execute();
68 4
        $lastId = $query->getAdapter()->getLastInsertId();
69 4
        if ($lastId) {
70 2
            $entity->setId($lastId);
71 1
        }
72 4
        $this->registerEntity($entity);
73 4
        return $this;
74
    }
75
76
    /**
77
     * Deletes current entity from database
78
     *
79
     * @param EntityInterface $entity
80
     *
81
     * @return self|$this|EntityInterface
82
     */
83 1
    public function delete(EntityInterface $entity)
84
    {
85
        $this->entity = $entity;
86
        $primaryKey = $this->getDescriptor()->getPrimaryKey()->getName();
87
        $table = $this->getDescriptor()->getTableName();
88
        $sql = Sql::createSql($this->getAdapter());
89
        $this->setUpdateCriteria(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Slick\Database\Sql\SqlInterface as the method execute() does only exist in the following implementations of said interface: Slick\Database\Sql\AbstractExecutionOnlySql, Slick\Database\Sql\Ddl\AlterTable, Slick\Database\Sql\Ddl\CreateIndex, Slick\Database\Sql\Ddl\CreateTable, Slick\Database\Sql\Ddl\DropIndex, Slick\Database\Sql\Ddl\DropTable, Slick\Database\Sql\Delete, Slick\Database\Sql\Insert, Slick\Database\Sql\Update.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
90
            $sql->delete($table),
91
            $primaryKey,
92
            $table
93 1
        )->execute();
94
        $this->removeEntity($entity);
95
        return $this;
96
    }
97
98
    /**
99
     * Get entity descriptor
100
     *
101
     * @return EntityDescriptorInterface
102
     */
103 10
    public function getDescriptor()
104
    {
105 10
        if (null == $this->descriptor) {
106 6
            $this->setDescriptor(
107 6
                EntityDescriptorRegistry::getInstance()
108 6
                    ->getDescriptorFor($this->getEntityClassName())
109 3
            );
110 3
        }
111 10
        return $this->descriptor;
112
    }
113
114
    /**
115
     * Set entity descriptor
116
     *
117
     * @param EntityDescriptorInterface $descriptor
118
     *
119
     * @return $this|self|EntityMapper
120
     */
121 10
    public function setDescriptor($descriptor)
122
    {
123 10
        $this->descriptor = $descriptor;
124 10
        return $this;
125
    }
126
127
    /**
128
     * Sets the adapter for this mapper
129
     *
130
     * @param AdapterInterface $adapter
131
     *
132
     * @return self|$this|EntityMapper
133
     */
134 8
    public function setAdapter(AdapterInterface $adapter)
135
    {
136 8
        $this->adapter = $adapter;
137 8
        return $this;
138
    }
139
140
    /**
141
     * Retrieves the current adapter
142
     *
143
     * @return AdapterInterface
144
     */
145 4
    public function getAdapter()
146
    {
147 4
        return $this->adapter;
148
    }
149
150
    /**
151
     * Gets entity class name for this mapper
152
     *
153
     * @return string
154
     */
155 6
    public function getEntityClassName()
156
    {
157 6
        if (null == $this->entityClassName) {
158 4
            $this->setEntityClassName(get_class($this->entity));
159 2
        }
160 6
        return $this->entityClassName;
161
    }
162
163
    /**
164
     * Sets entity class name for this mapper
165
     *
166
     * @param string $entityClassName
167
     *
168
     * @return self|$this|EntityMapper
169
     */
170 8
    public function setEntityClassName($entityClassName)
171
    {
172 8
        $this->entityClassName = $entityClassName;
173 8
        return $this;
174
    }
175
176
    /**
177
     * Creates the insert/update query for current entity state
178
     *
179
     * @return Sql\Insert|Sql\Update
180
     */
181 4
    protected function getUpdateQuery()
182
    {
183 4
        $primaryKey = $this->getDescriptor()->getPrimaryKey()->getName();
184 4
        $table = $this->getDescriptor()->getTableName();
185 4
        $sql = Sql::createSql($this->getAdapter());
186 4
        $query = (null === $this->entity->{$primaryKey})
187 3
            ? $sql->insert($table)
188 3
            : $this->setUpdateCriteria(
189 3
                $sql->update($table),
190 1
                $primaryKey,
191
                $table
192 2
            );
193 4
        return $query;
194
    }
195
196
    /**
197
     * Adds the update criteria for an update query
198
     *
199
     * @param Sql\SqlInterface|Sql\Update|Sql\delete $query
200
     * @param string $primaryKey
201
     * @param string $table
202
     *
203
     * @return Sql\SqlInterface|Sql\Update|Sql\delete
204
     */
205 2
    protected function setUpdateCriteria(
206
        Sql\SqlInterface $query, $primaryKey, $table
207
    ) {
208 2
        $key = "{$table}.{$primaryKey} = :id";
209 2
        $query->where([$key => [':id' => $this->entity->{$primaryKey}]]);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Slick\Database\Sql\SqlInterface as the method where() does only exist in the following implementations of said interface: Slick\Database\Sql\Delete, Slick\Database\Sql\Select, Slick\Database\Sql\Update.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
210 2
        return $query;
211
    }
212
213
    /**
214
     * Gets data to be used in queries
215
     *
216
     * @return array
217
     */
218 4
    protected function getData()
219
    {
220 4
        $data = [];
221 4
        $fields = $this->getDescriptor()->getFields();
222
        /** @var FieldDescriptor $field */
223 4
        foreach ($fields as $field) {
224 4
            $data[$field->getField()] = $this->entity->{$field->getName()};
225 2
        }
226 4
        return $data;
227
    }
228
229
    /**
230
     * Creates an entity object from provided data
231
     *
232
     * Data can be an array with single row fields or a RecordList from
233
     * a query.
234
     *
235
     * @param array|RecordList $data
236
     *
237
     * @return EntityInterface|EntityMapperInterface[]|EntityCollection
238
     */
239 6
    public function createFrom($data)
240
    {
241 6
        if ($data instanceof RecordList) {
242 2
            return $this->createMultiple($data);
243
        }
244 4
        return $this->createSingle($data);
245
    }
246
247
    /**
248
     * Creates an entity for provided row array
249
     *
250
     * @param array $source
251
     * @return EntityInterface
252
     */
253 6
    protected function createSingle(array $source)
254
    {
255 6
        $data = [];
256
        /** @var FieldDescriptor $field */
257 6
        foreach ($this->getDescriptor()->getFields() as $field) {
258 6
            if (array_key_exists($field->getField(), $source)) {
259 6
                $data[$field->getName()] = $source[$field->getField()];
260 3
            }
261 3
        }
262 6
        $class = $this->getDescriptor()->className();
263 6
        return new $class($data);
264
    }
265
266
    /**
267
     * Creates an entity collection for provided record list
268
     *
269
     * @param RecordList $source
270
     * @return EntityCollection
271
     */
272 2
    protected function createMultiple(RecordList $source)
273
    {
274 2
        $data = [];
275 2
        foreach ($source as $item) {
276 2
            $data[] = $this->createSingle($item);
277 1
        }
278 2
        return new EntityCollection($data);
279
    }
280
    
281
    /**
282
     * Sets the entity in the identity map of its repository.
283
     *
284
     * This avoids a select when one client creates an entity and
285
     * other client gets it from the repository.
286
     *
287
     * @param EntityInterface $entity
288
     * @return $this|self|EntityMapper
289
     */
290 4
    protected function registerEntity(EntityInterface $entity)
291
    {
292 4
        Orm::getRepository($this->getEntityClassName())
293 4
            ->getIdentityMap()
294 4
            ->set($entity);
295 4
        return $this;
296
    }
297
298
    /**
299
     * Removes the entity from the identity map of its repository.
300
     *
301
     * @param EntityInterface $entity
302
     * @return $this|self|EntityMapper
303
     */
304
    protected function removeEntity(EntityInterface $entity)
305
    {
306
        Orm::getRepository($this->getEntityClassName())
307
            ->getIdentityMap()
308
            ->remove($entity);
309
    }
310
}