Completed
Push — master ( 372519...d75aef )
by Mathieu
02:59
created

ObjectRevision::setModelFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 1
Metric Value
c 1
b 1
f 1
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Charcoal\Object;
4
5
// Dependencies from `PHP`
6
use \Exception;
7
use \InvalidArgumentException;
8
use \DateTime;
9
use \DateTimeInterface;
10
11
// From `pimple/pimple`
12
use \Pimple\Container;
13
14
// From `charcoal-factory`
15
use \Charcoal\Factory\FactoryInterface;
16
17
// From `charcoal-core`
18
use \Charcoal\Model\AbstractModel;
19
20
// Local namespace dependencies
21
use \Charcoal\Object\ObjectRevisionInterface;
22
use \Charcoal\Object\RevisionableInterface;
23
24
/**
25
 *
26
 */
27
class ObjectRevision extends AbstractModel implements ObjectRevisionInterface
28
{
29
30
    /**
31
     * Object type of this revision (required)
32
     * @var string $objType
33
     */
34
    private $objType;
35
36
    /**
37
     * Object ID of this revision (required)
38
     * @var mixed $objectId
39
     */
40
    private $objId;
41
42
    /**
43
     * Revision number. Sequential integer for each object's ID. (required)
44
     * @var integer $revNum
45
     */
46
    private $revNum;
47
48
    /**
49
     * Timestamp; when this revision was created
50
     * @var string $revTs (DateTime)
51
     */
52
    private $revTs;
53
54
    /**
55
     * The (admin) user that was
56
     * @var string $revUser
57
     */
58
    private $revUser;
59
60
    /**
61
     * @var array $dataPrev
62
     */
63
    private $dataPrev;
64
65
    /**
66
     * @var array $dataObj
67
     */
68
    private $dataObj;
69
70
    /**
71
     * @var array $dataDiff
72
     */
73
    private $dataDiff;
74
75
    /**
76
     * @var FactoryInterface $modelFactory
77
     */
78
    private $modelFactory;
79
80
    /**
81
     * Dependencies
82
     * @param Container $container DI Container.
83
     * @return void
84
     */
85
    public function setDependencies(Container $container)
86
    {
87
        parent::setDependencies($container);
88
89
        $this->setModelFactory($container['model/factory']);
90
    }
91
92
    /**
93
     * @param FactoryInterface $factory The factory used to create models.
94
     * @return AdminScript Chainable
95
     */
96
    protected function setModelFactory(FactoryInterface $factory)
97
    {
98
        $this->modelFactory = $factory;
99
        return $this;
100
    }
101
102
    /**
103
     * @return FactoryInterface The model factory.
104
     */
105
    protected function modelFactory()
106
    {
107
        return $this->modelFactory;
108
    }
109
110
    /**
111
     * @param string $objType The object type (type-ident).
112
     * @throws InvalidArgumentException If the obj type parameter is not a string.
113
     * @return ObjectRevision Chainable
114
     */
115
    public function setObjType($objType)
116
    {
117
        if (!is_string($objType)) {
118
            throw new InvalidArgumentException(
119
                'Revisions obj type must be a string.'
120
            );
121
        }
122
        $this->objType = $objType;
123
        return $this;
124
    }
125
126
    /**
127
     * @return string
128
     */
129
    public function objType()
130
    {
131
        return $this->objType;
132
    }
133
134
    /**
135
     * @param mixed $objId The object ID.
136
     * @return ObjectRevision Chainable
137
     */
138
    public function setObjId($objId)
139
    {
140
        $this->objId = $objId;
141
        return $this;
142
    }
143
144
    /**
145
     * @return mixed
146
     */
147
    public function objId()
148
    {
149
        return $this->objId;
150
    }
151
152
    /**
153
     * @param integer $revNum The revision number.
154
     * @throws InvalidArgumentException If the revision number argument is not numerical.
155
     * @return ObjectRevision Chainable
156
     */
157
    public function setRevNum($revNum)
158
    {
159
        if (!is_numeric($revNum)) {
160
            throw new InvalidArgumentException(
161
                'Revision number must be an integer (numeric).'
162
            );
163
        }
164
        $this->revNum = (int)$revNum;
165
        return $this;
166
    }
167
168
    /**
169
     * @return integer
170
     */
171
    public function revNum()
172
    {
173
        return $this->revNum;
174
    }
175
176
    /**
177
     * @param mixed $revTs The revision's timestamp.
178
     * @throws InvalidArgumentException If the timestamp is invalid.
179
     * @return ObjectRevision Chainable
180
     */
181 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...
182
    {
183
        if ($revTs === null) {
184
            $this->revTs = null;
185
            return $this;
186
        }
187
        if (is_string($revTs)) {
188
            $revTs = new DateTime($revTs);
189
        }
190
        if (!($revTs instanceof DateTimeInterface)) {
191
            throw new InvalidArgumentException(
192
                'Invalid "Revision Date" value. Must be a date/time string or a DateTimeInterface object.'
193
            );
194
        }
195
        $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...
196
        return $this;
197
    }
198
199
    /**
200
     * @return DateTime|null
201
     */
202
    public function revTs()
203
    {
204
        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...
205
    }
206
207
    /**
208
     * @param string $revUser The revision user ident.
209
     * @throws InvalidArgumentException If the revision user parameter is not a string.
210
     * @return ObjectRevision Chainable
211
     */
212
    public function setRevUser($revUser)
213
    {
214
        if ($revUser === null) {
215
            $this->revUser = null;
216
            return $this;
217
        }
218
        if (!is_string($revUser)) {
219
            throw new InvalidArgumentException(
220
                'Revision user must be a string.'
221
            );
222
        }
223
        $this->revUser = $revUser;
224
        return $this;
225
    }
226
227
    /**
228
     * @return string
229
     */
230
    public function revUser()
231
    {
232
        return $this->revUser;
233
    }
234
235
    /**
236
     * @param string|array $data The previous revision data.
237
     * @return ObjectRevision
238
     */
239 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...
240
    {
241
        if (!is_array($data)) {
242
            $data = json_decode($data, true);
243
        }
244
        if ($data === null) {
245
            $data = [];
246
        }
247
        $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...
248
        return $this;
249
    }
250
251
    /**
252
     * @return array
253
     */
254
    public function dataPrev()
255
    {
256
        return $this->dataPrev;
257
    }
258
259
    /**
260
     * @param array|string $data The current revision (object) data.
261
     * @return ObjectRevision
262
     */
263 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...
264
    {
265
        if (!is_array($data)) {
266
            $data = json_decode($data, true);
267
        }
268
        if ($data === null) {
269
            $data = [];
270
        }
271
        $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...
272
        return $this;
273
    }
274
275
    /**
276
     * @return array
277
     */
278
    public function dataObj()
279
    {
280
        return $this->dataObj;
281
    }
282
283
     /**
284
      * @param array|string $data The data diff.
285
      * @return ObjectRevision
286
      */
287 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...
288
    {
289
        if (!is_array($data)) {
290
            $data = json_decode($data, true);
291
        }
292
        if ($data === null) {
293
            $data = [];
294
        }
295
        $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...
296
        return $this;
297
    }
298
299
    /**
300
     * @return array
301
     */
302
    public function dataDiff()
303
    {
304
        return $this->dataDiff;
305
    }
306
307
    /**
308
     * Create a new revision from an object
309
     *
310
     * 1. Load the last revision
311
     * 2. Load the current item from DB
312
     * 3. Create diff from (1) and (2).
313
     *
314
     * @param RevisionableInterface $obj The object to create the revision from.
315
     * @return ObjectRevision Chainable
316
     */
317
    public function createFromObject(RevisionableInterface $obj)
318
    {
319
        $prevRev = $this->lastObjectRevision($obj);
320
321
        $this->setObjType($obj->objType());
322
        $this->setObjId($obj->id());
323
        $this->setRevNum($prevRev->revNum() + 1);
324
        $this->setRevTs('now');
325
326
        $this->setDataObj($obj->data([
327
            'sortable'=>false
328
        ]));
329
        $this->setDataPrev($prevRev->dataObj());
330
331
        $diff = $this->createDiff();
332
        $this->setDataDiff($diff);
333
334
        return $this;
335
    }
336
337
    /**
338
     * @param array $dataPrev Optional. Previous revision data.
339
     * @param array $dataObj  Optional. Current revision (object) data.
340
     * @return array The diff data
341
     */
342
    public function createDiff(array $dataPrev = null, array $dataObj = null)
343
    {
344
        if ($dataPrev === null) {
345
            $dataPrev = $this->dataPrev();
346
        }
347
        if ($dataObj === null) {
348
            $dataObj = $this->dataObj();
349
        }
350
        $dataDiff = $this->recursiveDiff($dataPrev, $dataObj);
351
        return $dataDiff;
352
    }
353
354
    /**
355
     * Recursive arrayDiff.
356
     *
357
     * @param array $array1 First array.
358
     * @param array $array2 Second Array.
359
     * @return array The array diff.
360
     */
361
    public function recursiveDiff(array $array1, array $array2)
362
    {
363
        $diff = [];
364
365
        // Compare array1
366
        foreach ($array1 as $key => $value) {
367
            if (!array_key_exists($key, $array2)) {
368
                $diff[0][$key] = $value;
369
            } elseif (is_array($value)) {
370
                if (!is_array($array2[$key])) {
371
                    $diff[0][$key] = $value;
372
                    $diff[1][$key] = $array2[$key];
373
                } else {
374
                    $new = $this->recursiveDiff($value, $array2[$key]);
375
                    if ($new !== false) {
376
                        if (isset($new[0])) {
377
                            $diff[0][$key] = $new[0];
378
                        }
379
                        if (isset($new[1])) {
380
                            $diff[1][$key] = $new[1];
381
                        }
382
                    }
383
                }
384
            } elseif ($array2[$key] !== $value) {
385
                $diff[0][$key] = $value;
386
                $diff[1][$key] = $array2[$key];
387
            }
388
        }
389
390
        // Compare array2
391
        foreach ($array2 as $key => $value) {
392
            if (!array_key_exists($key, $array1)) {
393
                $diff[1][$key] = $value;
394
            }
395
        }
396
397
        return $diff;
398
    }
399
400
    /**
401
     * @param RevisionableInterface $obj The object  to load the last revision of.
402
     * @return ObjectRevision The last revision for the give object.
403
     */
404
    public function lastObjectRevision(RevisionableInterface $obj)
405
    {
406
        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...
407
            /** @todo Optionnally turn off for some models */
408
            $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...
409
        }
410
411
        $rev = $this->modelFactory()->create('charcoal/object/object-revision');
412
413
        $rev->loadFromQuery(
414
            '
415
            SELECT
416
                *
417
            FROM
418
                `'.$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...
419
            WHERE
420
                `obj_type` = :obj_type
421
            AND
422
                `obj_id` = :obj_id
423
            ORDER BY
424
                `rev_ts` desc
425
            LIMIT 1',
426
            [
427
                'obj_type' => $obj->objType(),
428
                'obj_id'   => $obj->id()
429
            ]
430
        );
431
432
        return $rev;
433
    }
434
}
435