Completed
Branch 09branch (0a5c88)
by Anton
05:50
created

ViewManager::isolateLoader()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * spiral
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Views;
8
9
use Spiral\Core\Component;
10
use Spiral\Core\Container;
11
use Spiral\Core\Container\SingletonInterface;
12
use Spiral\Core\ContainerInterface;
13
use Spiral\Debug\Traits\BenchmarkTrait;
14
use Spiral\Files\FileManager;
15
use Spiral\Files\FilesInterface;
16
use Spiral\Views\Configs\ViewsConfig;
17
use Spiral\Views\Exceptions\LoaderException;
18
use Spiral\Views\Exceptions\ViewsException;
19
20
class ViewManager extends Component implements ViewsInterface, SingletonInterface
21
{
22
    use BenchmarkTrait;
23
24
    /**
25
     * Active view environment might define behaviour of engines and etc.
26
     *
27
     * @var EnvironmentInterface
28
     */
29
    private $environment = null;
30
31
    /**
32
     * Loader used to locate view files using simple notation (where no extension is included).
33
     *
34
     * @var LoaderInterface
35
     */
36
    private $loader = null;
37
38
    /**
39
     * View engines cache.
40
     *
41
     * @var EngineInterface[]
42
     */
43
    private $engines = [];
44
45
    /**
46
     * @var ViewsConfig
47
     */
48
    protected $config = null;
49
50
    /**
51
     * @var FilesInterface
52
     */
53
    protected $files = null;
54
55
    /**
56
     * @invisible
57
     * @var ContainerInterface
58
     */
59
    protected $container = null;
60
61
    /**
62
     * @param ViewsConfig        $config
63
     * @param FilesInterface     $files
64
     * @param ContainerInterface $container
65
     */
66
    public function __construct(
67
        ViewsConfig $config,
68
        FilesInterface $files = null,
69
        ContainerInterface $container = null
70
    ) {
71
        $this->config = $config;
72
        $this->files = $files ?? new FileManager();
73
        $this->container = $container ?? new Container();
0 ignored issues
show
Documentation Bug introduced by
It seems like $container ?? new \Spiral\Core\Container() can also be of type object<Spiral\Core\Container>. However, the property $container is declared as type object<Spiral\Core\ContainerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
74
75
        //Define engine's behaviour
76
        $this->loader = new ViewLoader($config->getNamespaces(), $files, $container);
0 ignored issues
show
Bug introduced by
It seems like $files defined by parameter $files on line 68 can be null; however, Spiral\Views\ViewLoader::__construct() 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...
Unused Code introduced by
The call to ViewLoader::__construct() has too many arguments starting with $container.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
77
        $this->environment = $this->createEnvironment($config);
78
    }
79
80
    /**
81
     * Creates copy of view manager with new environment.
82
     *
83
     * @param EnvironmentInterface $environment
84
     *
85
     * @return ViewManager
86
     */
87 View Code Duplication
    public function withEnvironment(EnvironmentInterface $environment): ViewManager
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...
88
    {
89
        $views = clone $this;
90
        $views->loader = clone $this->loader;
91
        $views->environment = $environment;
92
93
        //Not carrying already built engines with us
94
        $views->engines = [];
95
96
        return $views;
97
    }
98
99
    /**
100
     * Current view environment.
101
     *
102
     * @return EnvironmentInterface
103
     */
104
    public function getEnvironment(): EnvironmentInterface
105
    {
106
        return $this->environment;
107
    }
108
109
    /**
110
     * @param LoaderInterface $loader
111
     *
112
     * @return ViewManager
113
     */
114 View Code Duplication
    public function withLoader(LoaderInterface $loader): ViewManager
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...
115
    {
116
        $views = clone $this;
117
        $views->loader = $loader;
118
        $views->environment = clone $this->environment;
119
120
        //Not carrying already built engines with us
121
        $views->engines = [];
122
123
        return $views;
124
    }
125
126
    /**
127
     * View loader.
128
     *
129
     * @return LoaderInterface
130
     */
131
    public function getLoader(): LoaderInterface
132
    {
133
        return $this->loader;
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    public function get(string $path): ViewInterface
140
    {
141
        $engine = $this->detectEngine($path);
142
143
        return $this->engine($engine)->get($path);
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function render(string $path, array $context = []): string
150
    {
151
        $engine = $this->detectEngine($path);
152
153
        return $this->engine($engine)->render($path, $context);
154
    }
155
156
    /**
157
     * Pre-compile desired view file.
158
     *
159
     * @param string $path
160
     */
161
    public function compile(string $path)
162
    {
163
        $engine = $this->detectEngine($path);
164
165
        $this->engine($engine)->compile($path);
166
    }
167
168
    /**
169
     * Get engine by it's type.
170
     *
171
     * @param string $engine
172
     *
173
     * @return EngineInterface
174
     */
175
    public function engine(string $engine): EngineInterface
176
    {
177
        //Checking for an instance in cache
178
        if (!isset($this->engines[$engine])) {
179
            $this->engines[$engine] = $this->createEngine($engine);
180
        }
181
182
        return $this->engines[$engine];
183
    }
184
185
    /**
186
     * Detect engine by view path (automatically resolved based on extension).
187
     *
188
     * @todo this method might require some optimizations, for example utilizing memory cache for
189
     * @todo associations will be a nice idea.
190
     *
191
     * @param string $path
192
     *
193
     * @return string
194
     */
195
    protected function detectEngine(string $path): string
196
    {
197
        //File extension can help us to detect engine faster (attention, does not work with complex
198
        //extensions at this moment).
199
        $extension = $this->files->extension($path);
200
201
        $result = null;
202
        foreach ($this->config->getEngines() as $engine) {
203
            if (!empty($extension) && $extension == $this->config->engineExtension($engine)) {
204
                //Found by extension
205
                $result = $engine;
206
207
                break;
208
            }
209
210
            //Trying automatic (no extension) detection
211
            $loader = $this->isolateLoader($engine);
212
213
            try {
214
                if (!empty($loader->fetchName($path))) {
215
                    $result = $engine;
216
                }
217
            } catch (LoaderException $exception) {
218
                //Does not related to such engine
219
            }
220
        }
221
222
        if (empty($result)) {
223
            throw new ViewsException("Unable to detect view engine for '{$path}'");
224
        }
225
226
        return $result;
227
    }
228
229
    /**
230
     * Create engine instance.
231
     *
232
     * @param string $engine
233
     *
234
     * @return EngineInterface
235
     *
236
     * @throws ViewsException
237
     */
238
    protected function createEngine(string $engine): EngineInterface
239
    {
240
        if (!$this->config->hasEngine($engine)) {
241
            throw new ViewsException("Undefined engine '{$engine}'");
242
        }
243
244
        //Populating constructor parameters
245
        $options = $this->config->engineOptions($engine);
246
        $options += [
247
            'loader'      => $this->isolateLoader($engine),
248
            'environment' => $this->getEnvironment()
249
        ];
250
251
        //We have to create an engine
252
        $benchmark = $this->benchmark('engine', $engine);
253
        try {
254
            //Creating engine instance
255
            return $this->container->make($this->config->engineClass($engine), $options);
256
        } finally {
257
            $this->benchmark($benchmark);
258
        }
259
    }
260
261
    /**
262
     * @param ViewsConfig $config
263
     *
264
     * @return EnvironmentInterface
265
     */
266
    protected function createEnvironment(ViewsConfig $config): EnvironmentInterface
267
    {
268
        return new DynamicEnvironment(
269
            $config->environmentDependencies(),
270
            $config->cacheEnabled(),
271
            $config->cacheDirectory(),
272
            $this->container
273
        );
274
    }
275
276
    /**
277
     * Getting isolated view loader (class responsible for locating files and isolating view
278
     * namespaces). Isolation is done by forcing specific file extension. MUST NOT return same
279
     * instance for different engines!
280
     *
281
     * @param string $engine Forced extension value.
282
     *
283
     * @return LoaderInterface
284
     *
285
     * @throws ViewsException
286
     */
287
    protected function isolateLoader(string $engine = null): LoaderInterface
288
    {
289
        $extension = null;
290
        if (!empty($engine)) {
291
            if (!$this->config->hasEngine($engine)) {
292
                throw new ViewsException("Undefined view engine '{$engine}'");
293
            }
294
295
            $extension = $this->config->engineExtension($engine);
296
        }
297
298
        return $this->loader->withExtension($extension);
299
    }
300
}