Completed
Push — master ( 57b531...5d4578 )
by Filipe
02:14
created

EntityMapper::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1.3847

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 14
ccs 3
cts 11
cp 0.2727
rs 9.4285
cc 1
eloc 12
nc 1
nop 1
crap 1.3847
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\Event\Save;
22
use Slick\Orm\Orm;
23
24
/**
25
 * Generic Entity Mapper
26
 *
27
 * @package Slick\Orm\Mapper
28
 * @author  Filipe Silva <[email protected]>
29
 */
30
class EntityMapper implements EntityMapperInterface
31
{
32
    /**
33
     * @var EntityDescriptorInterface
34
     */
35
    protected $descriptor;
36
37
    /**
38
     * @var EntityInterface
39
     */
40
    protected $entity;
41
42
    /**
43
     * @var AdapterInterface
44
     */
45
    protected $adapter;
46
47
    /**
48
     * @var string
49
     */
50
    protected $entityClassName;
51
52
    /**
53
     * Saves current entity object to database
54
     *
55
     * Optionally saves only the partial data if $data argument is passed. If
56
     * no data is given al the field properties will be updated.
57
     *
58
     * @param array $data Partial data to save
59
     * @param EntityInterface $entity
60
     *
61
     * @return self|$this|EntityMapperInterface
62
     */
63 4
    public function save(EntityInterface $entity, array $data = [])
64
    {
65 4
        $this->entity = $entity;
66 4
        $query = $this->getUpdateQuery();
67 4
        $data = $this->getData();
68
69 2
        $save = $query instanceof Sql\Insert
70 3
            ? (new Save($entity, $data))
71 2
                ->setAction(Save::ACTION_BEFORE_INSERT)
72 3
            : (new Save($entity, $data))
73 4
                ->setAction(Save::ACTION_BEFORE_UPDATE);
74
75
        /** @var Save $save */
76 4
        $save = Orm::getEmitter($this->getEntityClassName())->emit($save);
77 4
        $query->set($data)
78 4
            ->execute();
79 4
        $lastId = $query->getAdapter()->getLastInsertId();
80 4
        if ($lastId) {
81 2
            $entity->setId($lastId);
82 1
        }
83
84 4
        $action = $save->getName() == Save::ACTION_BEFORE_INSERT
85 3
            ? Save::ACTION_AFTER_INSERT
86 4
            : Save::ACTION_BEFORE_UPDATE;
87
88 4
        $save->setEntity($entity)->setAction($action);
89 4
        Orm::getEmitter($this->getEntityClassName())->emit($save);
90
91 4
        $this->registerEntity($entity);
92 4
        return $this;
93 1
    }
94
95
    /**
96
     * Deletes current entity from database
97
     *
98
     * @param EntityInterface $entity
99
     *
100
     * @return self|$this|EntityInterface
101
     */
102 1
    public function delete(EntityInterface $entity)
103
    {
104
        $this->entity = $entity;
105
        $primaryKey = $this->getDescriptor()->getPrimaryKey()->getName();
106 1
        $table = $this->getDescriptor()->getTableName();
107
        $sql = Sql::createSql($this->getAdapter());
108 1
        $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...
109
            $sql->delete($table),
110
            $primaryKey,
111
            $table
112
        )->execute();
113
        $this->removeEntity($entity);
114
        return $this;
115
    }
116
117
    /**
118
     * Get entity descriptor
119
     *
120
     * @return EntityDescriptorInterface
121
     */
122 10
    public function getDescriptor()
123
    {
124 10
        if (null == $this->descriptor) {
125 6
            $this->setDescriptor(
126 6
                EntityDescriptorRegistry::getInstance()
127 6
                    ->getDescriptorFor($this->getEntityClassName())
128 3
            );
129 3
        }
130 10
        return $this->descriptor;
131
    }
132
133
    /**
134
     * Set entity descriptor
135
     *
136
     * @param EntityDescriptorInterface $descriptor
137
     *
138
     * @return $this|self|EntityMapper
139
     */
140 10
    public function setDescriptor($descriptor)
141
    {
142 10
        $this->descriptor = $descriptor;
143 10
        return $this;
144
    }
145
146
    /**
147
     * Sets the adapter for this mapper
148
     *
149
     * @param AdapterInterface $adapter
150
     *
151
     * @return self|$this|EntityMapper
152
     */
153 8
    public function setAdapter(AdapterInterface $adapter)
154
    {
155 8
        $this->adapter = $adapter;
156 8
        return $this;
157
    }
158
159
    /**
160
     * Retrieves the current adapter
161
     *
162
     * @return AdapterInterface
163
     */
164 4
    public function getAdapter()
165
    {
166 4
        return $this->adapter;
167
    }
168
169
    /**
170
     * Gets entity class name for this mapper
171
     *
172
     * @return string
173
     */
174 6
    public function getEntityClassName()
175
    {
176 6
        if (null == $this->entityClassName) {
177 4
            $this->setEntityClassName(get_class($this->entity));
178 2
        }
179 6
        return $this->entityClassName;
180
    }
181
182
    /**
183
     * Sets entity class name for this mapper
184
     *
185
     * @param string $entityClassName
186
     *
187
     * @return self|$this|EntityMapper
188
     */
189 8
    public function setEntityClassName($entityClassName)
190
    {
191 8
        $this->entityClassName = $entityClassName;
192 8
        return $this;
193
    }
194
195
    /**
196
     * Creates the insert/update query for current entity state
197
     *
198
     * @return Sql\Insert|Sql\Update
199
     */
200 4
    protected function getUpdateQuery()
201
    {
202 4
        $primaryKey = $this->getDescriptor()->getPrimaryKey()->getName();
203 4
        $table = $this->getDescriptor()->getTableName();
204 4
        $sql = Sql::createSql($this->getAdapter());
205 4
        $query = (null === $this->entity->{$primaryKey})
206 3
            ? $sql->insert($table)
207 3
            : $this->setUpdateCriteria(
208 3
                $sql->update($table),
209 1
                $primaryKey,
210
                $table
211 2
            );
212 4
        return $query;
213
    }
214
215
    /**
216
     * Adds the update criteria for an update query
217
     *
218
     * @param Sql\SqlInterface|Sql\Update|Sql\delete $query
219
     * @param string $primaryKey
220
     * @param string $table
221
     *
222
     * @return Sql\SqlInterface|Sql\Update|Sql\delete
223
     */
224 2
    protected function setUpdateCriteria(
225
        Sql\SqlInterface $query, $primaryKey, $table
226
    ) {
227 2
        $key = "{$table}.{$primaryKey} = :id";
228 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...
229 2
        return $query;
230
    }
231
232
    /**
233
     * Gets data to be used in queries
234
     *
235
     * @return array
236
     */
237 4
    protected function getData()
238
    {
239 4
        $data = [];
240 4
        $fields = $this->getDescriptor()->getFields();
241
        /** @var FieldDescriptor $field */
242 4
        foreach ($fields as $field) {
243 4
            $data[$field->getField()] = $this->entity->{$field->getName()};
244 2
        }
245 4
        return $data;
246
    }
247
248
    /**
249
     * Creates an entity object from provided data
250
     *
251
     * Data can be an array with single row fields or a RecordList from
252
     * a query.
253
     *
254
     * @param array|RecordList $data
255
     *
256
     * @return EntityInterface|EntityMapperInterface[]|EntityCollection
257
     */
258 6
    public function createFrom($data)
259
    {
260 6
        if ($data instanceof RecordList) {
261 2
            return $this->createMultiple($data);
262
        }
263 4
        return $this->createSingle($data);
264
    }
265
266
    /**
267
     * Creates an entity for provided row array
268
     *
269
     * @param array $source
270
     * @return EntityInterface
271
     */
272 6
    protected function createSingle(array $source)
273
    {
274 6
        $data = [];
275
        /** @var FieldDescriptor $field */
276 6
        foreach ($this->getDescriptor()->getFields() as $field) {
277 6
            if (array_key_exists($field->getField(), $source)) {
278 6
                $data[$field->getName()] = $source[$field->getField()];
279 3
            }
280 3
        }
281 6
        $class = $this->getDescriptor()->className();
282 6
        return new $class($data);
283
    }
284
285
    /**
286
     * Creates an entity collection for provided record list
287
     *
288
     * @param RecordList $source
289
     * @return EntityCollection
290
     */
291 2
    protected function createMultiple(RecordList $source)
292
    {
293 2
        $data = [];
294 2
        foreach ($source as $item) {
295 2
            $data[] = $this->createSingle($item);
296 1
        }
297 2
        return new EntityCollection($data);
298
    }
299
    
300
    /**
301
     * Sets the entity in the identity map of its repository.
302
     *
303
     * This avoids a select when one client creates an entity and
304
     * other client gets it from the repository.
305
     *
306
     * @param EntityInterface $entity
307
     * @return $this|self|EntityMapper
308
     */
309 4
    protected function registerEntity(EntityInterface $entity)
310
    {
311 4
        Orm::getRepository($this->getEntityClassName())
312 4
            ->getIdentityMap()
313 4
            ->set($entity);
314 4
        return $this;
315
    }
316
317
    /**
318
     * Removes the entity from the identity map of its repository.
319
     *
320
     * @param EntityInterface $entity
321
     * @return $this|self|EntityMapper
322
     */
323
    protected function removeEntity(EntityInterface $entity)
324
    {
325
        Orm::getRepository($this->getEntityClassName())
326
            ->getIdentityMap()
327
            ->remove($entity);
328
    }
329
}