Completed
Push — master ( 5d4578...5f67d7 )
by Filipe
02:35
created

EntityMapper::save()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 3
Metric Value
c 4
b 0
f 3
dl 0
loc 19
ccs 14
cts 14
cp 1
rs 9.4285
cc 2
eloc 13
nc 2
nop 2
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\RecordList;
13
use Slick\Database\Sql;
14
use Slick\Orm\Descriptor\Field\FieldDescriptor;
15
use Slick\Orm\Entity\EntityCollection;
16
use Slick\Orm\EntityInterface;
17
use Slick\Orm\EntityMapperInterface;
18
use Slick\Orm\Event\Save;
19
use Slick\Orm\Orm;
20
21
/**
22
 * Generic Entity Mapper
23
 *
24
 * @package Slick\Orm\Mapper
25
 * @author  Filipe Silva <[email protected]>
26
 */
27
class EntityMapper extends AbstractEntityMapper implements
28
    EntityMapperInterface
29
{
30
31
    /**
32
     * Add event trigger helpers
33
     */
34
    use EventTriggers;
35
36
    /**
37
     * Saves current entity object to database
38
     *
39
     * Optionally saves only the partial data if $data argument is passed. If
40
     * no data is given al the field properties will be updated.
41
     *
42
     * @param array $data Partial data to save
43
     * @param EntityInterface $entity
44
     *
45
     * @return self|$this|EntityMapperInterface
46
     */
47 4
    public function save(EntityInterface $entity, array $data = [])
48
    {
49 4
        $this->entity = $entity;
50 4
        $query = $this->getUpdateQuery();
51 4
        $data = $this->getData();
52 4
        $save = $this->triggerBeforeSave($query, $entity, $data);
53
54 4
        $query->set($data)
55 4
            ->execute();
56 4
        $lastId = $query->getAdapter()->getLastInsertId();
57 4
        if ($lastId) {
58 2
            $entity->setId($lastId);
59 2
        }
60
61 4
        $this->triggerAfterSave($save, $entity);
62
63 4
        $this->registerEntity($entity);
64 4
        return $this;
65
    }
66
67
    /**
68
     * Deletes current entity from database
69
     *
70
     * @param EntityInterface $entity
71
     *
72
     * @return self|$this|EntityInterface
73
     */
74 2
    public function delete(EntityInterface $entity)
75
    {
76 2
        $this->entity = $entity;
77 2
        $primaryKey = $this->getDescriptor()->getPrimaryKey()->getName();
78 2
        $table = $this->getDescriptor()->getTableName();
79 2
        $sql = Sql::createSql($this->getAdapter());
80
81 2
        $this->triggerBeforeDelete($entity);
82
83 2
        $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...
84 2
            $sql->delete($table),
85 2
            $primaryKey,
86
            $table
87 2
        )->execute();
88
89 2
        $this->triggerAfterDelete($entity);
90 2
        $this->removeEntity($entity);
91 2
        return $this;
92
    }
93
94
    /**
95
     * Creates the insert/update query for current entity state
96
     *
97
     * @return Sql\Insert|Sql\Update
98
     */
99 4
    protected function getUpdateQuery()
100
    {
101 4
        $primaryKey = $this->getDescriptor()->getPrimaryKey()->getName();
102 4
        $table = $this->getDescriptor()->getTableName();
103 4
        $sql = Sql::createSql($this->getAdapter());
104 4
        $query = (null === $this->entity->{$primaryKey})
105 4
            ? $sql->insert($table)
106 4
            : $this->setUpdateCriteria(
107 2
                $sql->update($table),
108 4
                $primaryKey,
109
                $table
110 4
            );
111 4
        return $query;
112
    }
113
114
    /**
115
     * Adds the update criteria for an update query
116
     *
117
     * @param Sql\SqlInterface|Sql\Update|Sql\delete $query
118
     * @param string $primaryKey
119
     * @param string $table
120
     *
121
     * @return Sql\SqlInterface|Sql\Update|Sql\delete
122
     */
123 6
    protected function setUpdateCriteria(
124
        Sql\SqlInterface $query, $primaryKey, $table
125
    ) {
126 4
        $key = "{$table}.{$primaryKey} = :id";
127 4
        $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...
128 6
        return $query;
129
    }
130
131
    /**
132
     * Gets data to be used in queries
133
     *
134
     * @return array
135
     */
136 4
    protected function getData()
137
    {
138 4
        $data = [];
139 4
        $fields = $this->getDescriptor()->getFields();
140
        /** @var FieldDescriptor $field */
141 4
        foreach ($fields as $field) {
142 4
            $data[$field->getField()] = $this->entity->{$field->getName()};
143 4
        }
144 4
        return $data;
145
    }
146
147
    /**
148
     * Creates an entity object from provided data
149
     *
150
     * Data can be an array with single row fields or a RecordList from
151
     * a query.
152
     *
153
     * @param array|RecordList $data
154
     *
155
     * @return EntityInterface|EntityMapperInterface[]|EntityCollection
156
     */
157 6
    public function createFrom($data)
158
    {
159 6
        if ($data instanceof RecordList) {
160 2
            return $this->createMultiple($data);
161
        }
162 4
        return $this->createSingle($data);
163
    }
164
165
    /**
166
     * Creates an entity for provided row array
167
     *
168
     * @param array $source
169
     * @return EntityInterface
170
     */
171 6
    protected function createSingle(array $source)
172
    {
173 6
        $data = [];
174
        /** @var FieldDescriptor $field */
175 6
        foreach ($this->getDescriptor()->getFields() as $field) {
176 6
            if (array_key_exists($field->getField(), $source)) {
177 6
                $data[$field->getName()] = $source[$field->getField()];
178 6
            }
179 6
        }
180 6
        $class = $this->getDescriptor()->className();
181 6
        return new $class($data);
182
    }
183
184
    /**
185
     * Creates an entity collection for provided record list
186
     *
187
     * @param RecordList $source
188
     * @return EntityCollection
189
     */
190 2
    protected function createMultiple(RecordList $source)
191
    {
192 2
        $data = [];
193 2
        foreach ($source as $item) {
194 2
            $data[] = $this->createSingle($item);
195 2
        }
196 2
        return new EntityCollection($data);
197
    }
198
    
199
    /**
200
     * Sets the entity in the identity map of its repository.
201
     *
202
     * This avoids a select when one client creates an entity and
203
     * other client gets it from the repository.
204
     *
205
     * @param EntityInterface $entity
206
     * @return $this|self|EntityMapper
207
     */
208 4
    protected function registerEntity(EntityInterface $entity)
209
    {
210 4
        Orm::getRepository($this->getEntityClassName())
211 4
            ->getIdentityMap()
212 4
            ->set($entity);
213 4
        return $this;
214
    }
215
216
    /**
217
     * Removes the entity from the identity map of its repository.
218
     *
219
     * @param EntityInterface $entity
220
     * @return $this|self|EntityMapper
221
     */
222 2
    protected function removeEntity(EntityInterface $entity)
223
    {
224 2
        Orm::getRepository($this->getEntityClassName())
225 2
            ->getIdentityMap()
226 2
            ->remove($entity);
227
    }
228
}