Completed
Push — master ( 895e90...df3299 )
by Tolan
02:29
created

AbstractViewComponent   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 520
Duplicated Lines 4.23 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 50
Bugs 7 Features 7
Metric Value
wmc 65
c 50
b 7
f 7
lcom 1
cbo 6
dl 22
loc 520
rs 5.7894

25 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 24 2
A log() 0 7 2
A dehydrate() 0 4 1
A rehydrate() 0 9 1
A __sleep() 0 9 1
B render() 0 26 4
B execMethod() 0 23 6
A getExecPath() 0 5 2
A getParent() 0 4 1
A setFlashMessage() 0 4 1
A setFlashError() 0 4 1
A getRootComponent() 0 8 2
initTemplate() 0 1 ?
initState() 0 1 ?
A getPath() 0 11 3
A addOrUpdateChild() 0 16 3
A renderChild() 0 9 2
A updateState() 0 4 1
D testInputs() 22 77 22
A updateView() 0 5 1
A updateProps() 0 5 1
A forceResponse() 0 4 1
A setExec() 0 8 2
A handleDependencyInjection() 0 13 2
A setLogger() 0 10 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AbstractViewComponent often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractViewComponent, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of the Patternseek ComponentView library.
4
 *
5
 * (c) 2014 Tolan Blundell <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
  * file that was distributed with this source code.
9
 */
10
11
namespace PatternSeek\ComponentView;
12
13
use PatternSeek\ComponentView\Template\AbstractTemplate;
14
use PatternSeek\ComponentView\ViewState\ViewState;
15
use PatternSeek\DependencyInjector\DependencyInjector;
16
use Psr\Log\LoggerInterface;
17
use Psr\Log\LogLevel;
18
19
/**
20
 * Class AbstractViewComponent
21
 * @package PatternSeek\ComponentView
22
 */
23
abstract class AbstractViewComponent
24
{
25
26
    /**
27
     * @var ExecHelper
28
     */
29
    public $exec;
30
    /**
31
     * Message to display when rendering component. Won't be serialised to will only be displayed once.
32
     * @var string
33
     */
34
    public $flashMessage;
35
    /**
36
     * Error to display when rendering component. Won't be serialised to will only be displayed once.
37
     * @var string
38
     */
39
    public $flashError;
40
    /**
41
     * If we have a parent in $parent, $handle is the parent's handle/identifier for us
42
     * @var string
43
     */
44
    public $handle;
45
    /**
46
     * @var ViewState An object containing state elements
47
     */
48
    protected $state;
49
    /**
50
     * @var AbstractViewComponent
51
     */
52
    protected $parent;
53
    /**
54
     * @var AbstractViewComponent[]
55
     */
56
    public $childComponents = [ ];
57
58
    /**
59
     * @var AbstractTemplate
60
     */
61
    protected $template;
62
63
    /**
64
     * @var LoggerInterface
65
     */
66
    protected $logger;
67
68
    /**
69
     * @var array
70
     */
71
    protected $props;
72
73
    /**
74
     * If set the render() will skip any processing and immediately return this response
75
     *
76
     * @var Response
77
     */
78
    private $forceResponse;
79
80
    /**
81
     * @param null $handle
82
     * @param AbstractViewComponent $parent
83
     * @param ExecHelper $execHelper
84
     * @param LoggerInterface $logger
85
     * @internal param array $initConfig
86
     */
87
    public function __construct(
88
        $handle = null,
89
        AbstractViewComponent $parent = null,
90
        ExecHelper $execHelper = null,
91
        LoggerInterface $logger = null
92
    ){
93
        // Null means we are root
94
        $this->parent = $parent;
95
96
        // Null means we are root
97
        $this->handle = $handle;
98
99
        if (null === $execHelper) {
100
            $execHelper = new ExecHelper();
101
        }
102
        $this->setExec( $execHelper );
103
104
        $this->handleDependencyInjection();
105
        
106
        $this->setLogger( $logger );
107
108
        // Set up the state container
109
        $this->initState();
110
    }
111
112
    /**
113
     * @param string $message
114
     * @param string $level A constant from LogLevel
115
     */
116
    protected function log( $message, $level ){
117
        if( isset( $this->logger ) ){
118
            $class = get_class( $this );
119
            $message = "[{$class}] {$message}";
120
            $this->logger->log( $level, $message );
121
        }
122
    }
123
124
    /**
125
     * User this to serialise ViewComponents as extra steps may be added later.
126
     * @return string
127
     */
128
    public function dehydrate(){
129
        $this->log( "Dehydrating", LogLevel::DEBUG );
130
        return serialize( $this );
131
    }
132
133
    /**
134
     * Use this to unserialise ViewComponents
135
     * @param $serialised
136
     * @param ExecHelper $execHelper
137
     * @param LoggerInterface $logger
138
     * @return AbstractViewComponent
139
     */
140
    
141
    public static function rehydrate( $serialised, ExecHelper $execHelper, LoggerInterface $logger = null ){
142
        /** @var AbstractViewComponent $view */
143
        $view = unserialize( $serialised );
144
        $view->setExec( $execHelper );
145
        $view->handleDependencyInjection();
146
        $view->setLogger( $logger );
147
        $view->log( "Rehydrated", LogLevel::DEBUG );
148
        return $view;
149
    }
150
151
    /**
152
     * @return array
153
     */
154
    public function __sleep()
155
    {
156
        return [
157
            'childComponents',
158
            'handle',
159
            'parent',
160
            'state'
161
        ];
162
    }
163
164
    /**
165
     * Entry point for rendering a component tree. Call updateView() first.
166
     * @param string|null $execMethodName An optional method on this or a subcomponent to execute before rendering
167
     * @param array|null $execArgs
168
     * @throws \Exception
169
     * @return Response
170
     */
171
    public function render( $execMethodName = null, array $execArgs = null )
172
    {
173
        $this->state->validate();
174
175
        // updateState() on any component can call $this->getRootComponent()->forceResponse()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
176
        // to force a particular response, usually a redirect.
177
        if (null !== $this->forceResponse) {
178
            return $this->forceResponse;
179
        }
180
        
181
        $this->initTemplate();
182
183
        // If we're called with an 'exec' then run it instead of rendering the whole tree.
184
        // It may still render the whole tree or it may just render a portion or just return JSON
185
        if ($execMethodName) { // Used to test for null but it could easily be an empty string
0 ignored issues
show
Bug Best Practice introduced by
The expression $execMethodName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
186
            $this->log( "Rendering with exec: {$execMethodName}, args:".var_export($execArgs, true ), LogLevel::DEBUG );
187
            $out = $this->execMethod( $execMethodName, $execArgs );
188
        }else {
189
            $this->log( "Rendering without exec", LogLevel::DEBUG );
190
            $out = $this->template->render( $this->state, $this->props );
191
            if (!( $out instanceof Response )) {
192
                throw new \Exception( get_class( $this->template ) . " returned invalid response. Should have been an instance of ViewComponentResponse" );
193
            }
194
        }
195
        return $out;
196
    }
197
198
    /**
199
     * Execute a component method within the page or component.
200
     * Called first on a top level component which then passes the call down to the appropriate sub-component (or executes on itself if appropriate).
201
     * @param array|string $methodName A methodname in the format subComponent.anotherSubComponent.methodName. Either dotted string as described, or parts in an array. The top level page component shouldn't be included
202
     * @param array $args
203
     * @throws \Exception
204
     * @return Response
205
     */
206
    protected function execMethod( $methodName, array $args = null )
207
    {
208
        if (!is_array( $methodName )) {
209
            $methodName = explode( '.', $methodName );
210
        }
211
        if (count( $methodName ) == 1) {
212
            $methodName = $methodName[ 0 ] . 'Handler';
213
            $out = $this->$methodName( $args );
214
        }else {
215
            $childName = array_shift( $methodName );
216
            $child = $this->childComponents[ $childName ];
217
            if ($child instanceof AbstractViewComponent) {
218
                $out = $child->execMethod( $methodName, $args );
219
            }else {
220
                throw new \Exception( implode( ".", $methodName ) . " is not a valid method." );
221
            }
222
        }
223
        if (!( $out instanceof Response )) {
224
            $nameStr = is_array( $methodName )?implode( ".", $methodName ):$methodName;
225
            throw new \Exception( $nameStr . " returned invalid response. Should have been an instance of ViewComponentResponse" );
226
        }
227
        return $out;
228
    }
229
230
    /**
231
     * @param $execMethod
232
     * @return string
233
     */
234
    public function getExecPath( $execMethod )
235
    {
236
        $path = $this->getPath();
237
        return ( $path === null?$execMethod:$path . '.' . $execMethod );
238
    }
239
240
    /**
241
     * @return AbstractViewComponent
242
     */
243
    public function getParent()
244
    {
245
        return $this->parent;
246
    }
247
248
249
    /**
250
     * @param $string
251
     */
252
    protected function setFlashMessage( $string )
253
    {
254
        $this->flashMessage = $string;
255
    }
256
257
    /**
258
     * @param $string
259
     */
260
    protected function setFlashError( $string )
261
    {
262
        $this->flashError = $string;
263
    }
264
265
    /**
266
     * Get the root component of the hierarchy
267
     *
268
     * @return AbstractViewComponent
269
     */
270
    protected function getRootComponent()
271
    {
272
        $cur = $this;
273
        while ($cur->parent !== null) {
274
            $cur = $cur->parent;
275
        }
276
        return $cur;
277
    }
278
279
    /**
280
     * Load or configure the component's template as necessary.
281
     * Called just before the template is used so can depend on $this->state to select template.
282
     *
283
     * @return void
284
     */
285
    abstract protected function initTemplate();
286
287
    /**
288
     * Initialise $this->state with either a new ViewState or an appropriate subclass
289
     * @return void
290
     */
291
    abstract protected function initState();
292
293
    /**
294
     * Return the this object's path in the current component hierarchy
295
     * @return string
296
     */
297
    protected function getPath()
298
    {
299
        if (null === $this->parent) {
300
            return null;
301
        }
302
        if (null !== ( $pPath = $this->parent->getPath() )) {
303
            return $pPath . '.' . $this->handle;
304
        }else {
305
            return $this->handle;
306
        }
307
    }
308
309
    /**
310
     * Can create a child component on this component and return it.
311
     *
312
     * @param string $handle
313
     * @param string $type
314
     * @param array $props
315
     * @return AbstractViewComponent
316
     * @throws \Exception
317
     */
318
    protected function addOrUpdateChild( $handle, $type, array $props = [ ] )
319
    {
320
        $this->log( "Adding/updating child '{$handle}' of type {$type}", LogLevel::DEBUG );
321
        if (!isset( $this->childComponents[ $handle ] )) {
322
            if( ! class_exists( $type ) ){
323
                throw new \Exception( "Class '{$type}' for sub-component  does not exist." );
324
            }
325
            $child = new $type( $handle, $this, $this->exec, $this->logger );
326
            $this->childComponents[ $handle ] = $child;
327
        }else {
328
            // exec, di and logger are set recursively in rehydrate()
329
            $child = $this->childComponents[ $handle ];
330
        }
331
        $child->updateProps( $props );
332
        $child->updateState();
333
    }
334
335
    /**
336
     * Render a child component.
337
     *
338
     * @param $handle
339
     * @return Response
340
     * @throws \Exception
341
     */
342
    public function renderChild( $handle )
343
    {
344
        if (!$this->childComponents[ $handle ]) {
345
            $message = "Attempted to render nonexistent child component with handle '{$handle}'";
346
            $this->log( $message, LogLevel::CRITICAL );
347
            throw new \Exception( $message );
348
        }
349
        return $this->childComponents[ $handle ]->render()->content;
350
    }
351
352
    /**
353
     * Using $this->props and $this->state, optionally update state and create/update child components via addOrUpdateChild().
354
     * @return void
355
     */
356
    protected function updateState()
357
    {
358
        //
359
    }
360
361
    /**
362
     * testInputs() compares a set of named inputs (props or args) in the associative array $inputs with an input specification.
363
     * It MUST be used by implementations' doUpdateState() and *Handler() methods to verify their input.
364
     *
365
     * $inputSpec is an array describing allowed inputs with a similar design to php method sigs.
366
     * The keys are field names, the values are 0 to 2 entry arrays with the following entries: [type,default].
367
     * Type can be set to null to allow any type, or if there is no default it can be left empty.
368
     * If default is not set then the field is required. If default is null then that us used as the default value.
369
     * As defaults can be any value, it's possible to create an object or callable to use as a default.
370
     * Type can be any of the types described at http://www.php.net/manual/en/function.gettype.php except null or unknown type. In addition it can be any class name, callable, float, bool or int.
371
     * E.g.
372
     *      [
373
     *          'anyTypeRequired'=>[],
374
     *          'anyTypeRequired2'=>[null],
375
     *          'anyTypeOptional'=>[null,null],
376
     *          'boolRequired'=>['bool'],
377
     *          'boolRequired2'=>['boolean'],
378
     *          'intOptional'=>['int',3],
379
     *          'intRequired'=>['integer'],
380
     *          'doubleRequired'=>['double'],
381
     *          'floatRequired'=>['float'],
382
     *          'stringRequired'=>['string'],
383
     *          'arrayRequired'=>['array'],
384
     *          'objectRequired'=>['object'],
385
     *          'resourceRequired'=>['resource'],
386
     *          'callableRequired'=>['callable'],
387
     *          'SomeClassRequired'=>['SomeClass'],
388
     *          'SomeClassOptional'=>['SomeClass',null],
389
     *          'SomeClassWithPrebuiltDefault'=>['SomeClass', new SomeClass( 'something' )],
390
     *      ]
391
     * @param array $inputSpec See above
392
     * @param array $inputs
393
     * @throws \Exception
394
     */
395
    protected function testInputs( array $inputSpec, array &$inputs )
396
    {
397
        
398
        foreach ($inputSpec as $fieldName => $fieldSpec) {
399
            // Required field
400 View Code Duplication
            if (( count( $fieldSpec ) < 2 )) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
401
                if (!isset( $inputs[ $fieldName ] )) {
402
                    $calledFunc = debug_backtrace()[1]['function'];
403
                    $callerFunc = debug_backtrace()[2]['function'];
404
                    $callerClass = debug_backtrace()[2]['class'];
405
                    $parentText = '';
406
                    if( $this->parent !== null ){
407
                        $parentText = " (parent component is ".get_class($this->parent).")";
408
                    }
409
                    throw new \Exception( $fieldName . " is a required field for " . get_class( $this )."::{$calledFunc}() called from {$callerClass}::{$callerFunc}(){$parentText}" );
410
                }
411
            }
412
            // Set default is unset
413
            if (!isset( $inputs[ $fieldName ] )) {
414
                $inputs[ $fieldName ] = $fieldSpec[ 1 ];
415
            }
416
            // Check type
417
            // Any type allowed, continue
418
            if (!isset( $fieldSpec[ 0 ] ) || $fieldSpec[ 0 ] === null) {
419
                continue;
420
            }
421
            $requiredType = $fieldSpec[ 0 ];
422
            $input = $inputs[ $fieldName ];
423
            // Specific type required
424
            // Null is allowed
425
            if (!is_null( $input )) {
426
                switch ($requiredType) {
427
                    case "boolean":
428
                    case "bool":
429
                    $failed = !is_bool( $input );
430
                        break;
431
                    case "integer":
432
                    case "int":
433
                    $failed = !is_int( $input+0 );
434
                        break;
435
                    case "double":
436
                        $failed = !is_double( $input+0 );
437
                        break;
438
                    case "float":
439
                        $failed = !is_float( $input+0 );
440
                        break;
441
                    case "string":
442
                        $failed = !is_string( $input );
443
                        break;
444
                    case "array":
445
                        $failed = !is_array( $input );
446
                        break;
447
                    case "object":
448
                        $failed = !is_object( $input );
449
                        break;
450
                    case "resource":
451
                        $failed = !is_resource( $input );
452
                        break;
453
                    case "callable":
454
                        $failed = !is_callable( $input );
455
                        break;
456
                    default:
457
                        $failed = !( $input instanceof $requiredType );
458
                }
459 View Code Duplication
                if ($failed) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
460
                    $calledFunc = debug_backtrace()[1]['function'];
461
                    $callerFunc = debug_backtrace()[2]['function'];
462
                    $callerClass = debug_backtrace()[2]['class'];
463
                    $parentText = '';
464
                    if( $this->parent !== null ){
465
                        $parentText = " (parent component is ".get_class($this->parent).")";
466
                    }
467
                    throw new \Exception( $fieldName . " should be of type " . $requiredType . "in " . get_class( $this )."::{$calledFunc}() called from {$callerClass}::{$callerFunc}(){$parentText}" );
468
                }
469
            }
470
        }
471
    }
472
473
    /**
474
     * Update the full component view tree.
475
     *
476
     * @var array $props
477
     */
478
    public function updateView( $props )
479
    {
480
        $this->updateProps( $props );
481
        $this->updateState();
482
    }
483
484
    /**
485
     * Update the component's properties ('input') array
486
     *
487
     * @var array $props
488
     */
489
    protected function updateProps( $props )
490
    {
491
        $this->log( "Storing new props: " . var_export( $props, true ), LogLevel::DEBUG );
492
        $this->props = $props;   
493
    }
494
495
    protected function forceResponse( Response $response )
496
    {
497
        $this->forceResponse = $response;
498
    }
499
500
    /**
501
     * @param ExecHelper $execHelper
502
     */
503
    private function setExec( ExecHelper $execHelper )
504
    {
505
        $this->exec = clone $execHelper;
506
        $this->exec->setComponent( $this );
507
        foreach( $this->childComponents as $child ){
508
            $child->setExec( $execHelper );
509
        }
510
    }
511
512
    /**
513
     *
514
     */
515
    private function handleDependencyInjection()
516
    {
517
        // It's a little strange that the object injects its own
518
        // dependencies but it means that callers don't need to do
519
        // it manually and you still get the advantage that the deps
520
        // are specified in the optional injectDependencies() method's
521
        // signature
522
        $this->log( "Dependency injection...", LogLevel::DEBUG );
523
        DependencyInjector::instance()->injectIntoMethod( $this );
524
        foreach( $this->childComponents as $child ){
525
            $child->handleDependencyInjection();
526
        }
527
    }
528
529
    /**
530
     * @param LoggerInterface $logger
531
     */
532
    private function setLogger( LoggerInterface $logger = null )
533
    {
534
        if( null !== $logger ){
535
            $this->logger = $logger;
536
            /** @var AbstractViewComponent $child */
537
            foreach( $this->childComponents as $child ){
538
                $child->setLogger( $logger );
539
            }
540
        }
541
    }
542
}
543