Test Failed
Push — master ( c45665...275d9f )
by Alain
02:45
created

ViewBuilder   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 74.13%

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 9
dl 0
loc 305
ccs 43
cts 58
cp 0.7413
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
B create() 0 31 8
A getEngine() 0 4 1
A getView() 0 9 2
A getViewFinder() 0 4 1
A getEngineFinder() 0 4 1
A addLocation() 0 9 1
A getLocations() 0 4 1
A scanLocations() 0 20 5
A getFinder() 0 5 1
B resolveType() 0 28 6
A getConfig() 0 9 2
1
<?php declare(strict_types=1);
2
/**
3
 * Bright Nucleus View Component.
4
 *
5
 * @package   BrightNucleus\View
6
 * @author    Alain Schlesser <[email protected]>
7
 * @license   MIT
8
 * @link      http://www.brightnucleus.com/
9
 * @copyright 2016-2017 Alain Schlesser, Bright Nucleus
10
 */
11
12
namespace BrightNucleus\View;
13
14
use BrightNucleus\Config\ConfigFactory;
15
use BrightNucleus\Config\ConfigInterface;
16
use BrightNucleus\Config\ConfigTrait;
17
use BrightNucleus\Config\Exception\FailedToProcessConfigException;
18
use BrightNucleus\View\Engine\Engine;
19
use BrightNucleus\View\Engine\EngineFinder;
20
use BrightNucleus\View\View\ViewFinder;
21
use BrightNucleus\View\Exception\FailedToInstantiateView;
22
use BrightNucleus\View\Location\Locations;
23
use BrightNucleus\View\Location\Location;
24
25
/**
26
 * Class ViewBuilder.
27
 *
28
 * @since   0.1.0
29
 *
30
 * @package BrightNucleus\View
31
 * @author  Alain Schlesser <[email protected]>
32
 */
33
class ViewBuilder
34
{
35
36
    use ConfigTrait;
37
38
    const ENGINE_FINDER_KEY = 'EngineFinder';
39
    const VIEW_FINDER_KEY = 'ViewFinder';
40
41
    /**
42
     * BaseViewFinder instance.
43
     *
44
     * @since 0.1.0
45
     *
46
     * @var ViewFinder
47
     */
48
    protected $viewFinder;
49
50
    /**
51
     * BaseEngineFinder instance.
52
     *
53
     * @since 0.1.0
54
     *
55
     * @var EngineFinder
56
     */
57
    protected $engineFinder;
58
59
    /**
60
     * Locations to scan for views.
61
     *
62
     * @since 0.1.0
63
     *
64
     * @var Locations
65
     */
66
    protected $locations;
67
68
    /**
69
     * Cache of already resolved view paths.
70
     *
71
     * @since 0.4.6
72
     *
73
     * @var string[]
74
     */
75
    protected $viewPathCache = [];
76
77
    /**
78
     * Instantiate a ViewBuilder object.
79
     *
80 16
     * @since 0.1.0
81
     *
82
     * @param ConfigInterface   $config       Optional. Configuration settings.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $config not be null|ConfigInterface?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
83
     * @param ViewFinder|null   $viewFinder   Optional. ViewFinder instance.
84
     * @param EngineFinder|null $engineFinder Optional. EngineFinder instance.
85 16
     *
86 16
     * @throws FailedToProcessConfigException If the config could not be processed.
87 16
     */
88 16
    public function __construct(
89 16
        ConfigInterface $config = null,
90
        ViewFinder $viewFinder = null,
91
        EngineFinder $engineFinder = null
92
    ) {
93
        $this->processConfig($this->getConfig($config));
0 ignored issues
show
Bug introduced by
It seems like $config defined by parameter $config on line 89 can be null; however, BrightNucleus\View\ViewBuilder::getConfig() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
94
        $this->viewFinder   = $viewFinder ?? $this->getViewFinder();
95
        $this->engineFinder = $engineFinder ?? $this->getEngineFinder();
96
        $this->locations    = new Locations();
97
    }
98
99
    /**
100
     * Create a new view for a given URI.
101 30
     *
102
     * @since 0.1.0
103 30
     *
104 30
     * @param string $view View identifier to create a view for.
105
     * @param mixed  $type Type of view to create.
106 30
     *
107 29
     * @return View Instance of the requested view.
108 30
     * @throws FailedToInstantiateView If the view could not be instantiated.
109
     */
110
    public function create(string $view, $type = null): View
111
    {
112
        if (!array_key_exists($view, $this->viewPathCache)) {
113
            $uri    = $this->scanLocations([$view]);
114
            $engine = $uri ? $this->getEngine($uri) : false;
115
116
            $this->viewPathCache[$view]           = [];
117
            $this->viewPathCache[$view]['uri']    = $uri;
118
            $this->viewPathCache[$view]['engine'] = $engine;
119
120 30
            if ($type===null) {
121
                $this->viewPathCache[$view]['view'] = $uri
122 30
                    ? $this->getView($uri, $engine)
0 ignored issues
show
Security Bug introduced by
It seems like $engine defined by $uri ? $this->getEngine($uri) : false on line 114 can also be of type false; however, BrightNucleus\View\ViewBuilder::getView() does only seem to accept object<BrightNucleus\View\Engine\Engine>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
123
                    : false;
124
            }
125
        } else {
126
            $uri    = $this->viewPathCache[$view]['uri'];
127
            $engine = $this->viewPathCache[$view]['engine'];
128
        }
129
130
131
        if (!$uri || !$engine) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uri of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false 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...
132
            return $this->getViewFinder()->getNullObject();
133
        }
134
135
        if ($type === null) {
136 29
            return $this->viewPathCache[$view]['view'];
137
        }
138 29
139 29
        return $this->getView($uri, $engine, $type);
0 ignored issues
show
Bug introduced by
It seems like $engine can also be of type string; however, BrightNucleus\View\ViewBuilder::getView() does only seem to accept object<BrightNucleus\View\Engine\Engine>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
140 29
    }
141
142 29
    /**
143
     * Get an Engine that can deal with the given URI.
144
     *
145
     * @since 0.1.0
146
     *
147
     * @param string $uri URI to get an engine for.
148
     *
149
     * @return Engine Instance of an engine that can deal with the given URI.
150
     */
151
    public function getEngine(string $uri): Engine
152 30
    {
153
        return $this->getEngineFinder()->find([$uri]);
154 30
    }
155
156
    /**
157
     * Get a view for a given URI, engine and type.
158
     *
159
     * @since 0.1.0
160
     *
161
     * @param string $uri    URI to get a view for.
162
     * @param Engine $engine Engine to use for the view.
163
     * @param mixed  $type   Type of view to get.
164 30
     *
165
     * @return View View that matches the given requirements.
166 30
     * @throws FailedToInstantiateView If the view could not be instantiated.
167
     */
168
    public function getView(string $uri, Engine $engine, $type = null): View
169
    {
170
        if (null === $type) {
171
            $view = $this->getViewFinder()->find([$uri], $engine);
172
            return $view->setBuilder( $this );
173
        }
174
175
        return $this->resolveType($type, $uri, $engine);
176 30
    }
177
178 30
    /**
179 30
     * Get the ViewFinder instance.
180
     *
181
     * @since 0.1.0
182
     *
183
     * @return ViewFinder Instance of a BaseViewFinder.
184
     */
185
    public function getViewFinder(): ViewFinder
186
    {
187
        return $this->viewFinder ?? $this->getFinder(static::VIEW_FINDER_KEY);
188
    }
189
190
    /**
191
     * Get the EngineFinder instance.
192
     *
193
     * @since 0.1.0
194
     *
195
     * @return EngineFinder Instance of a BaseEngineFinder.
196
     */
197
    public function getEngineFinder(): EngineFinder
198
    {
199
        return $this->engineFinder ?? $this->getFinder(static::ENGINE_FINDER_KEY);
200
    }
201
202 30
    /**
203
     * Add a location to scan with the BaseViewFinder.
204
     *
205
     * @since 0.1.0
206 30
     *
207 30
     * @param Location $location Location to scan with the BaseViewFinder.
208 30
     *
209 30
     * @return static
210
     */
211 30
    public function addLocation(Location $location)
212
    {
213
        $this->locations->add($location);
214
215
        unset( $this->viewPathCache );
216
        $this->viewPathCache = [];
217
218
        return $this;
219
    }
220
221
    /**
222
     * Get the collection of locations registered with this ViewBuilder.
223
     *
224 30
     * @since 0.1.3
225
     *
226 30
     * @return Locations Collection of locations.
227 30
     */
228 30
    public function getLocations()
229
    {
230
        return $this->locations;
231 30
    }
232
233
    /**
234
     * Scan Locations for an URI that matches the specified criteria.
235
     *
236
     * @since 0.1.0
237
     *
238
     * @param array $criteria Criteria to match.
239
     *
240
     * @return string|false URI of the requested view, or false if not found.
241
     */
242
    public function scanLocations(array $criteria)
243
    {
244
        $uris = $this->locations->map(function ($location) use ($criteria) {
245
            /** @var Location $location */
246
            return $location->getURI($criteria);
247
        })->filter(function ($uri) {
248
            return false !== $uri;
249
        });
250
        
251
        // Fall back for absolute paths on current filesystem.
252
        if ($uris->isEmpty()) {
253
            foreach ($criteria as $criterion) {
254
                if (file_exists($criterion)) {
255
                    return $criterion;
256
                }
257
            }
258
        }
259
260
        return $uris->isEmpty() ? false : $uris->first();
261
    }
262
263
    /**
264
     * Get a finder instance.
265
     *
266
     * @since 0.1.1
267
     *
268
     * @param string $key Configuration key to use.
269
     *
270
     * @return ViewFinder|EngineFinder The requested finder instance.
271
     */
272
    protected function getFinder($key)
273
    {
274
        $finderClass = $this->config->getKey($key, 'ClassName');
275
        return new $finderClass($this->config->getSubConfig($key));
276
    }
277
278
    /**
279
     * Resolve the view type.
280
     *
281
     * @since 0.1.0
282 16
     *
283
     * @param mixed       $type   Type of view that was requested.
284 16
     * @param string      $uri    URI to get a view for.
285 16
     * @param Engine|null $engine Engine to use for the view.
286 15
     *
287 16
     * @return View Resolved View object.
288
     * @throws FailedToInstantiateView If the view type could not be resolved.
289 16
     */
290
    protected function resolveType($type, string $uri, Engine $engine = null): View
291
    {
292
        $configKey = [static::VIEW_FINDER_KEY, 'Views', $type];
293
294
        if (is_string($type) && $this->config->hasKey($configKey)) {
295
            $className = $this->config->getKey($configKey);
296
            $type      = new $className($uri, $engine, $this);
297
        }
298
299
        if (is_string($type)) {
300
            $type = new $type($uri, $engine, $this);
301
        }
302
303
        if (is_callable($type)) {
304
            $type = $type($uri, $engine, $this);
305
        }
306
307
        if (! $type instanceof View) {
308
            throw new FailedToInstantiateView(
309
                sprintf(
310
                    _('Could not instantiate view "%s".'),
311
                    serialize($type)
312
                )
313
            );
314
        }
315
316
        return $type;
317
    }
318
319
    /**
320
     * Get the configuration to use in the ViewBuilder.
321
     *
322
     * @since 0.2.0
323
     *
324
     * @param ConfigInterface|array $config Config to merge with defaults.
325
     *
326
     * @return ConfigInterface Configuration passed in through the constructor.
327
     */
328
    protected function getConfig($config = []): ConfigInterface
329
    {
330
        $defaults = ConfigFactory::create(dirname(__DIR__, 2) . '/config/defaults.php', $config);
331
        $config   = $config
332
            ? ConfigFactory::createFromArray(array_merge_recursive($defaults->getArrayCopy(), $config->getArrayCopy()))
333
            : $defaults;
334
335
        return $config->getSubConfig('BrightNucleus\View');
336
    }
337
}
338