Completed
Push — master ( c3852c...c303c5 )
by Robert
03:11
created

TransformableSubscriber::handleField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 14
ccs 11
cts 11
cp 1
rs 9.4285
cc 2
eloc 9
nc 2
nop 4
crap 2
1
<?php
2
3
namespace MediaMonks\Doctrine\Transformable;
4
5
use Doctrine\Common\EventArgs;
6
use Doctrine\Common\Persistence\ObjectManager;
7
use Doctrine\ORM\Events;
8
use Doctrine\ORM\UnitOfWork;
9
use Gedmo\Mapping\Event\AdapterInterface;
10
use Gedmo\Mapping\MappedEventSubscriber;
11
use MediaMonks\Doctrine\Transformable\Transformer\TransformerInterface;
12
use MediaMonks\Doctrine\Transformable\Transformer\TransformerPool;
13
14
/**
15
 * @author Robert Slootjes <[email protected]>
16
 * @author Bas Bloembergen <[email protected]>
17
 */
18
class TransformableSubscriber extends MappedEventSubscriber
19
{
20
    const TRANSFORMABLE = 'transformable';
21
22
    const FUNCTION_TRANSFORM = 'transform';
23
    const FUNCTION_REVERSE_TRANSFORM = 'reverseTransform';
24
25
    const TYPE_TRANSFORMED = 'transformed';
26
    const TYPE_PLAIN = 'plain';
27
28
    /**
29
     * @var TransformerPool
30
     */
31
    protected $transformerPool;
32
33
    /**
34
     * @var array
35
     */
36
    protected $entityFieldValues = [];
37
38
    /**
39
     * TransformableListener constructor.
40
     * @param TransformerPool $transformerPool
41
     */
42 3
    public function __construct(TransformerPool $transformerPool)
43
    {
44 3
        $this->transformerPool = $transformerPool;
45 3
        parent::__construct();
46 3
    }
47
48
    /**
49
     * @return array
50
     */
51 3
    public function getSubscribedEvents()
52
    {
53
        return [
54 3
            Events::loadClassMetadata,
55 3
            Events::onFlush,
56 3
            Events::postPersist,
57 3
            Events::postLoad,
58 3
            Events::postUpdate,
59 3
        ];
60
    }
61
62
    /**
63
     * @param EventArgs $eventArgs
64
     * @return void
65
     */
66 2
    public function loadClassMetadata(EventArgs $eventArgs)
67
    {
68 2
        $ea = $this->getEventAdapter($eventArgs);
69 2
        $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\Common\EventArgs as the method getClassMetadata() does only exist in the following sub-classes of Doctrine\Common\EventArgs: Doctrine\Common\Persiste...dClassMetadataEventArgs, Doctrine\ORM\Event\LoadClassMetadataEventArgs, Doctrine\ORM\Tools\Event...ateSchemaTableEventArgs. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
70 2
    }
71
72
    /**
73
     * @param EventArgs $args
74
     */
75 1
    public function onFlush(EventArgs $args)
76
    {
77 1
        $this->transform($args);
78 1
    }
79
80
    /**
81
     * @param EventArgs $args
82
     */
83 1
    public function postPersist(EventArgs $args)
84
    {
85 1
        $this->reverseTransform($args);
86 1
    }
87
88
    /**
89
     * @param EventArgs $args
90
     */
91
    public function postLoad(EventArgs $args)
92
    {
93
        $this->reverseTransform($args);
94
    }
95
96
    /**
97
     * @param EventArgs $args
98
     */
99
    public function postUpdate(EventArgs $args)
100
    {
101
        $this->reverseTransform($args);
102
    }
103
104
    /**
105
     * @param EventArgs $args
106
     */
107 1
    protected function transform(EventArgs $args)
108
    {
109 1
        $ea  = $this->getEventAdapter($args);
110 1
        $om  = $ea->getObjectManager();
111 1
        $uow = $om->getUnitOfWork();
112
113 1
        foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
114
            $this->handle($ea, $om, $uow, $object, self::FUNCTION_TRANSFORM);
115 1
        }
116
117 1
        foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
118 1
            $this->handle($ea, $om, $uow, $object, self::FUNCTION_TRANSFORM);
119 1
        }
120 1
    }
121
122
    /**
123
     * @param EventArgs $args
124
     */
125 1
    protected function reverseTransform(EventArgs $args)
126
    {
127 1
        $ea = $this->getEventAdapter($args);
128 1
        $om = $ea->getObjectManager();
129
130 1
        $this->handle($ea, $om, $om->getUnitOfWork(), $ea->getObject(), self::FUNCTION_REVERSE_TRANSFORM);
0 ignored issues
show
Bug introduced by
The method getObject() does not exist on Gedmo\Mapping\Event\AdapterInterface. Did you maybe mean getObjectChangeSet()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
131 1
    }
132
133
    /**
134
     * @param AdapterInterface $ea
135
     * @param ObjectManager $om
136
     * @param UnitOfWork $uow
137
     * @param object $entity
138
     * @param string $method
139
     */
140 1
    protected function handle(AdapterInterface $ea, ObjectManager $om, UnitOfWork $uow, $entity, $method)
141
    {
142
        /**
143
         * @var \Doctrine\ORM\EntityManager $om
144
         */
145 1
        $meta = $om->getClassMetadata(get_class($entity));
146 1
        $config = $this->getConfiguration($om, $meta->name);
147
148 1
        if (isset($config[self::TRANSFORMABLE]) && $config[self::TRANSFORMABLE]) {
149 1
            foreach ($config[self::TRANSFORMABLE] as $column) {
150 1
                $this->handleField($entity, $method, $column, $meta);
151 1
            }
152 1
            $ea->recomputeSingleObjectChangeSet($uow, $meta, $entity);
0 ignored issues
show
Documentation introduced by
$uow is of type object<Doctrine\ORM\UnitOfWork>, but the function expects a object<Gedmo\Mapping\Event\UnitOfWork>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
153 1
        }
154 1
    }
155
156
    /**
157
     * @param $entity
158
     * @param $method
159
     * @param $column
160
     * @param $meta
161
     */
162 1
    protected function handleField($entity, $method, $column, $meta)
163
    {
164 1
        $field = $column['field'];
165 1
        $oid   = spl_object_hash($entity);
166
167 1
        $reflProp = $meta->getReflectionProperty($field);
168 1
        $oldValue = $reflProp->getValue($entity);
169 1
        $newValue = $this->getNewValue($oid, $field, $column['name'], $method, $oldValue);
170 1
        $reflProp->setValue($entity, $newValue);
171
172 1
        if ($method === self::FUNCTION_REVERSE_TRANSFORM) {
173 1
            $this->storeOriginalFieldData($oid, $field, $oldValue, $newValue);
174 1
        }
175 1
    }
176
177
    /**
178
     * @param $oid
179
     * @param $field
180
     * @param $transformerName
181
     * @param $method
182
     * @param $value
183
     * @return mixed
184
     */
185 1
    protected function getNewValue($oid, $field, $transformerName, $method, $value)
186
    {
187
        if ($method === self::FUNCTION_TRANSFORM
188 1
            && $this->getOriginalPlainFieldValue($oid, $field) === $value
189 1
        ) {
190
            return $this->getOriginalTransformedFieldValue($oid, $field);
191
        }
192 1
        return $this->performTransformerOperation($transformerName, $method, $value);
193
    }
194
195
    /**
196
     * @param $transformerName
197
     * @param $method
198
     * @param $oldValue
199
     * @return mixed
200
     */
201 1
    protected function performTransformerOperation($transformerName, $method, $oldValue)
202
    {
203 1
        return $this->getTransformer($transformerName)->$method($oldValue);
204
    }
205
206
    /**
207
     * @param $oid
208
     * @param $field
209
     * @return null
210
     */
211 1 View Code Duplication
    protected function getOriginalPlainFieldValue($oid, $field)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
212
    {
213 1
        $data = $this->getFieldData($oid, $field);
214 1
        if (empty($data)) {
215 1
            return null;
216
        }
217
        return $data[self::TYPE_PLAIN];
218
    }
219
220
    /**
221
     * @param $oid
222
     * @param $field
223
     * @return mixed
224
     */
225 View Code Duplication
    protected function getOriginalTransformedFieldValue($oid, $field)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
    {
227
        $data = $this->getFieldData($oid, $field);
228
        if (empty($data)) {
229
            return null;
230
        }
231
        return $data[self::TYPE_TRANSFORMED];
232
    }
233
234
    /**
235
     * @param $oid
236
     * @param $field
237
     * @return array|null
238
     */
239 1
    protected function getFieldData($oid, $field)
240
    {
241 1
        if (!isset($this->entityFieldValues[$oid][$field])) {
242 1
            return null;
243
        }
244
        return $this->entityFieldValues[$oid][$field];
245
    }
246
247
    /**
248
     * @param $oid
249
     * @param $field
250
     * @param $transformed
251
     * @param $plain
252
     */
253 1
    protected function storeOriginalFieldData($oid, $field, $transformed, $plain)
254
    {
255 1
        $this->entityFieldValues[$oid][$field] = [
256 1
            self::TYPE_TRANSFORMED => $transformed,
257 1
            self::TYPE_PLAIN       => $plain
258 1
        ];
259 1
    }
260
261
    /**
262
     * @param $name
263
     * @return TransformerInterface
264
     */
265 1
    protected function getTransformer($name)
266
    {
267 1
        return $this->transformerPool->get($name);
268
    }
269
270
    /**
271
     * {@inheritDoc}
272
     */
273 3
    protected function getNamespace()
274
    {
275 3
        return __NAMESPACE__;
276
    }
277
}
278