Completed
Push — master ( 19dbb1...b206b9 )
by Mathieu
03:35
created

ObjectRevision::targetType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Charcoal\Object;
4
5
// Dependencies from `PHP`
6
use \InvalidArgumentException;
7
use \DateTime;
8
use \DateTimeInterface;
9
10
// From `pimple/pimple`
11
use \Pimple\Container;
12
13
// From `charcoal-factory`
14
use \Charcoal\Factory\FactoryInterface;
15
16
// From `charcoal-core`
17
use \Charcoal\Model\AbstractModel;
18
19
// Local namespace dependencies
20
use \Charcoal\Object\ObjectRevisionInterface;
21
use \Charcoal\Object\RevisionableInterface;
22
23
/**
24
 *
25
 */
26
class ObjectRevision extends AbstractModel implements ObjectRevisionInterface
27
{
28
29
    /**
30
     * Object type of this revision (required)
31
     * @var string $targetType
32
     */
33
    private $targetType;
34
35
    /**
36
     * Object ID of this revision (required)
37
     * @var mixed $objectId
38
     */
39
    private $targetId;
40
41
    /**
42
     * Revision number. Sequential integer for each object's ID. (required)
43
     * @var integer $revNum
44
     */
45
    private $revNum;
46
47
    /**
48
     * Timestamp; when this revision was created
49
     * @var string $revTs (DateTime)
50
     */
51
    private $revTs;
52
53
    /**
54
     * The (admin) user that was
55
     * @var string $revUser
56
     */
57
    private $revUser;
58
59
    /**
60
     * @var array $dataPrev
61
     */
62
    private $dataPrev;
63
64
    /**
65
     * @var array $dataObj
66
     */
67
    private $dataObj;
68
69
    /**
70
     * @var array $dataDiff
71
     */
72
    private $dataDiff;
73
74
    /**
75
     * @var FactoryInterface $modelFactory
76
     */
77
    private $modelFactory;
78
79
    /**
80
     * Dependencies
81
     * @param Container $container DI Container.
82
     * @return void
83
     */
84
    public function setDependencies(Container $container)
85
    {
86
        parent::setDependencies($container);
87
88
        $this->setModelFactory($container['model/factory']);
89
    }
90
91
    /**
92
     * @param FactoryInterface $factory The factory used to create models.
93
     * @return AdminScript Chainable
94
     */
95
    protected function setModelFactory(FactoryInterface $factory)
96
    {
97
        $this->modelFactory = $factory;
98
        return $this;
99
    }
100
101
    /**
102
     * @return FactoryInterface The model factory.
103
     */
104
    protected function modelFactory()
105
    {
106
        return $this->modelFactory;
107
    }
108
109
    /**
110
     * @param string $targetType The object type (type-ident).
111
     * @throws InvalidArgumentException If the obj type parameter is not a string.
112
     * @return ObjectRevision Chainable
113
     */
114
    public function setTargetType($targetType)
115
    {
116
        if (!is_string($targetType)) {
117
            throw new InvalidArgumentException(
118
                'Revisions obj type must be a string.'
119
            );
120
        }
121
        $this->targetType = $targetType;
122
        return $this;
123
    }
124
125
    /**
126
     * @return string
127
     */
128
    public function targetType()
129
    {
130
        return $this->targetType;
131
    }
132
133
    /**
134
     * @param mixed $targetId The object ID.
135
     * @return ObjectRevision Chainable
136
     */
137
    public function setTargetId($targetId)
138
    {
139
        $this->targetId = $targetId;
140
        return $this;
141
    }
142
143
    /**
144
     * @return mixed
145
     */
146
    public function targetId()
147
    {
148
        return $this->targetId;
149
    }
150
151
    /**
152
     * @param integer $revNum The revision number.
153
     * @throws InvalidArgumentException If the revision number argument is not numerical.
154
     * @return ObjectRevision Chainable
155
     */
156
    public function setRevNum($revNum)
157
    {
158
        if (!is_numeric($revNum)) {
159
            throw new InvalidArgumentException(
160
                'Revision number must be an integer (numeric).'
161
            );
162
        }
163
        $this->revNum = (int)$revNum;
164
        return $this;
165
    }
166
167
    /**
168
     * @return integer
169
     */
170
    public function revNum()
171
    {
172
        return $this->revNum;
173
    }
174
175
    /**
176
     * @param mixed $revTs The revision's timestamp.
177
     * @throws InvalidArgumentException If the timestamp is invalid.
178
     * @return ObjectRevision Chainable
179
     */
180 View Code Duplication
    public function setRevTs($revTs)
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...
181
    {
182
        if ($revTs === null) {
183
            $this->revTs = null;
184
            return $this;
185
        }
186
        if (is_string($revTs)) {
187
            $revTs = new DateTime($revTs);
188
        }
189
        if (!($revTs instanceof DateTimeInterface)) {
190
            throw new InvalidArgumentException(
191
                'Invalid "Revision Date" value. Must be a date/time string or a DateTimeInterface object.'
192
            );
193
        }
194
        $this->revTs = $revTs;
0 ignored issues
show
Documentation Bug introduced by
It seems like $revTs of type object<DateTimeInterface> is incompatible with the declared type string of property $revTs.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
195
        return $this;
196
    }
197
198
    /**
199
     * @return DateTime|null
200
     */
201
    public function revTs()
202
    {
203
        return $this->revTs;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->revTs; (string) is incompatible with the return type declared by the interface Charcoal\Object\ObjectRevisionInterface::revTs of type Charcoal\Object\DateTime|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
204
    }
205
206
    /**
207
     * @param string $revUser The revision user ident.
208
     * @throws InvalidArgumentException If the revision user parameter is not a string.
209
     * @return ObjectRevision Chainable
210
     */
211
    public function setRevUser($revUser)
212
    {
213
        if ($revUser === null) {
214
            $this->revUser = null;
215
            return $this;
216
        }
217
        if (!is_string($revUser)) {
218
            throw new InvalidArgumentException(
219
                'Revision user must be a string.'
220
            );
221
        }
222
        $this->revUser = $revUser;
223
        return $this;
224
    }
225
226
    /**
227
     * @return string
228
     */
229
    public function revUser()
230
    {
231
        return $this->revUser;
232
    }
233
234
    /**
235
     * @param string|array $data The previous revision data.
236
     * @return ObjectRevision
237
     */
238 View Code Duplication
    public function setDataPrev($data)
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...
239
    {
240
        if (!is_array($data)) {
241
            $data = json_decode($data, true);
242
        }
243
        if ($data === null) {
244
            $data = [];
245
        }
246
        $this->dataPrev = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type * is incompatible with the declared type array of property $dataPrev.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
247
        return $this;
248
    }
249
250
    /**
251
     * @return array
252
     */
253
    public function dataPrev()
254
    {
255
        return $this->dataPrev;
256
    }
257
258
    /**
259
     * @param array|string $data The current revision (object) data.
260
     * @return ObjectRevision
261
     */
262 View Code Duplication
    public function setDataObj($data)
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...
263
    {
264
        if (!is_array($data)) {
265
            $data = json_decode($data, true);
266
        }
267
        if ($data === null) {
268
            $data = [];
269
        }
270
        $this->dataObj = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type * is incompatible with the declared type array of property $dataObj.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
271
        return $this;
272
    }
273
274
    /**
275
     * @return array
276
     */
277
    public function dataObj()
278
    {
279
        return $this->dataObj;
280
    }
281
282
     /**
283
      * @param array|string $data The data diff.
284
      * @return ObjectRevision
285
      */
286 View Code Duplication
    public function setDataDiff($data)
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...
287
    {
288
        if (!is_array($data)) {
289
            $data = json_decode($data, true);
290
        }
291
        if ($data === null) {
292
            $data = [];
293
        }
294
        $this->dataDiff = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type * is incompatible with the declared type array of property $dataDiff.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
295
        return $this;
296
    }
297
298
    /**
299
     * @return array
300
     */
301
    public function dataDiff()
302
    {
303
        return $this->dataDiff;
304
    }
305
306
    /**
307
     * Create a new revision from an object
308
     *
309
     * 1. Load the last revision
310
     * 2. Load the current item from DB
311
     * 3. Create diff from (1) and (2).
312
     *
313
     * @param RevisionableInterface $obj The object to create the revision from.
314
     * @return ObjectRevision Chainable
315
     */
316
    public function createFromObject(RevisionableInterface $obj)
317
    {
318
        $prevRev = $this->lastObjectRevision($obj);
319
320
        $this->setObjType($obj->targetType());
0 ignored issues
show
Bug introduced by
The method targetType() does not seem to exist on object<Charcoal\Object\RevisionableInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method setObjType() does not exist on Charcoal\Object\ObjectRevision. Did you maybe mean objType()?

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...
321
        $this->setObjId($obj->id());
0 ignored issues
show
Bug introduced by
The method setObjId() does not seem to exist on object<Charcoal\Object\ObjectRevision>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
322
        $this->setRevNum($prevRev->revNum() + 1);
323
        $this->setRevTs('now');
324
325
        $this->setDataObj($obj->data([
326
            'sortable'=>false
327
        ]));
328
        $this->setDataPrev($prevRev->dataObj());
329
330
        $diff = $this->createDiff();
331
        $this->setDataDiff($diff);
332
333
        return $this;
334
    }
335
336
    /**
337
     * @param array $dataPrev Optional. Previous revision data.
338
     * @param array $dataObj  Optional. Current revision (object) data.
339
     * @return array The diff data
340
     */
341
    public function createDiff(array $dataPrev = null, array $dataObj = null)
342
    {
343
        if ($dataPrev === null) {
344
            $dataPrev = $this->dataPrev();
345
        }
346
        if ($dataObj === null) {
347
            $dataObj = $this->dataObj();
348
        }
349
        $dataDiff = $this->recursiveDiff($dataPrev, $dataObj);
350
        return $dataDiff;
351
    }
352
353
    /**
354
     * Recursive arrayDiff.
355
     *
356
     * @param array $array1 First array.
357
     * @param array $array2 Second Array.
358
     * @return array The array diff.
359
     */
360
    public function recursiveDiff(array $array1, array $array2)
361
    {
362
        $diff = [];
363
364
        // Compare array1
365
        foreach ($array1 as $key => $value) {
366
            if (!array_key_exists($key, $array2)) {
367
                $diff[0][$key] = $value;
368
            } elseif (is_array($value)) {
369
                if (!is_array($array2[$key])) {
370
                    $diff[0][$key] = $value;
371
                    $diff[1][$key] = $array2[$key];
372
                } else {
373
                    $new = $this->recursiveDiff($value, $array2[$key]);
374
                    if ($new !== false) {
375
                        if (isset($new[0])) {
376
                            $diff[0][$key] = $new[0];
377
                        }
378
                        if (isset($new[1])) {
379
                            $diff[1][$key] = $new[1];
380
                        }
381
                    }
382
                }
383
            } elseif ($array2[$key] !== $value) {
384
                $diff[0][$key] = $value;
385
                $diff[1][$key] = $array2[$key];
386
            }
387
        }
388
389
        // Compare array2
390
        foreach ($array2 as $key => $value) {
391
            if (!array_key_exists($key, $array1)) {
392
                $diff[1][$key] = $value;
393
            }
394
        }
395
396
        return $diff;
397
    }
398
399
    /**
400
     * @param RevisionableInterface $obj The object  to load the last revision of.
401
     * @return ObjectRevision The last revision for the give object.
402
     */
403
    public function lastObjectRevision(RevisionableInterface $obj)
404
    {
405
        if ($this->source()->tableExists() === false) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method tableExists() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

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...
406
            /** @todo Optionnally turn off for some models */
407
            $this->source()->createTable();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method createTable() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

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...
408
        }
409
410
        $rev = $this->modelFactory()->create(self::class);
411
412
        $rev->loadFromQuery(
413
            '
414
            SELECT
415
                *
416
            FROM
417
                `'.$this->source()->table().'`
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method table() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

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...
418
            WHERE
419
                `target_type` = :target_type
420
            AND
421
                `target_id` = :target_id
422
            ORDER BY
423
                `rev_ts` desc
424
            LIMIT 1',
425
            [
426
                'target_type' => $obj->targetType(),
0 ignored issues
show
Bug introduced by
The method targetType() does not seem to exist on object<Charcoal\Object\RevisionableInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
427
                'target_id'   => $obj->id()
428
            ]
429
        );
430
431
        return $rev;
432
    }
433
}
434