Completed
Branch 09branch (31301e)
by Anton
02:42
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
8
namespace Spiral\Views;
9
10
use Spiral\Core\Component;
11
use Spiral\Core\Container;
12
use Spiral\Core\Container\SingletonInterface;
13
use Spiral\Core\ContainerInterface;
14
use Spiral\Debug\Traits\BenchmarkTrait;
15
use Spiral\Files\FileManager;
16
use Spiral\Files\FilesInterface;
17
use Spiral\Views\Configs\ViewsConfig;
18
use Spiral\Views\Exceptions\ViewsException;
19
use Spiral\Views\Loaders\FileLoader;
20
21
/**
22
 * Provides ability to manage view engines, loaders and environment (cache dependencies).
23
 * Attention, this is immutable class.
24
 */
25
class ViewManager extends Component implements ViewsInterface, SingletonInterface
26
{
27
    use BenchmarkTrait;
28
29
    /**
30
     * Active view environment might define behaviour of engines and etc.
31
     *
32
     * @var EnvironmentInterface
33
     */
34
    private $environment = null;
35
36
    /**
37
     * Loader used to locate view files using simple notation (where no extension is included).
38
     *
39
     * @var LoaderInterface
40
     */
41
    private $loader = null;
42
43
    /**
44
     * View engines cache.
45
     *
46
     * @var EngineInterface[]
47
     */
48
    private $engines = [];
49
50
    /**
51
     * @var ViewsConfig
52
     */
53
    protected $config = null;
54
55
    /**
56
     * @var FilesInterface
57
     */
58
    protected $files = null;
59
60
    /**
61
     * @invisible
62
     * @var ContainerInterface
63
     */
64
    protected $container = null;
65
66
    /**
67
     * @param ViewsConfig        $config
68
     * @param FilesInterface     $files
69
     * @param ContainerInterface $container
70
     */
71
    public function __construct(
72
        ViewsConfig $config,
73
        FilesInterface $files = null,
74
        ContainerInterface $container = null
75
    ) {
76
        $this->config = $config;
77
        $this->files = $files ?? new FileManager();
78
        $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...
79
80
        //Define engine's behaviour
81
        $this->loader = new FileLoader($config->getNamespaces(), $files, $container);
0 ignored issues
show
Bug introduced by
It seems like $files defined by parameter $files on line 73 can be null; however, Spiral\Views\Loaders\FileLoader::__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 FileLoader::__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...
82
        $this->environment = $this->createEnvironment($config);
83
    }
84
85
    /**
86
     * Creates copy of view manager with new environment.
87
     *
88
     * @param EnvironmentInterface $environment
89
     *
90
     * @return ViewManager
91
     */
92 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...
93
    {
94
        $views = clone $this;
95
        $views->loader = clone $this->loader;
96
        $views->environment = $environment;
97
98
        foreach ($this->engines as $name => $engine) {
99
            $views->engines[$name] = $engine->withEnvironment($environment);
100
        }
101
102
        return $views;
103
    }
104
105
    /**
106
     * Current view environment. View environment defines isolated cache version which provides
107
     * ability to create multiple cached versions for some views and improve application
108
     * performance.
109
     *
110
     * Example:
111
     * $this->views->compile('home');
112
     *
113
     * $this->translator->setLocale('ru');
114
     *
115
     * //Different cache id
116
     * $this->views->compile('home');
117
     *
118
     * @return EnvironmentInterface
119
     */
120
    public function getEnvironment(): EnvironmentInterface
121
    {
122
        return $this->environment;
123
    }
124
125
    /**
126
     * @param LoaderInterface $loader
127
     *
128
     * @return ViewManager
129
     */
130 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...
131
    {
132
        $views = clone $this;
133
        $views->loader = $loader;
134
        $views->environment = clone $this->environment;
135
136
        //Not carrying already built engines with us
137
        foreach ($this->engines as $name => $engine) {
138
            $views->engines[$name] = $engine->withLoader($views->engineLoader($name));
139
        }
140
141
        return $views;
142
    }
143
144
    /**
145
     * View loader.
146
     *
147
     * @return LoaderInterface
148
     */
149
    public function getLoader(): LoaderInterface
150
    {
151
        return $this->loader;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function get(string $path): ViewInterface
158
    {
159
        $engine = $this->detectEngine($path);
160
161
        return $this->engine($engine)->get($path);
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    public function render(string $path, array $context = []): string
168
    {
169
        $engine = $this->detectEngine($path);
170
171
        return $this->engine($engine)->render($path, $context);
172
    }
173
174
    /**
175
     * Pre-compile desired view file.
176
     *
177
     * @param string $path
178
     */
179
    public function compile(string $path)
180
    {
181
        $this->engine($this->detectEngine($path))->compile($path);
182
    }
183
184
    /**
185
     * Get engine by it's type.
186
     *
187
     * @param string $engine
188
     *
189
     * @return EngineInterface
190
     */
191
    public function engine(string $engine): EngineInterface
192
    {
193
        //Checking for an instance in cache
194
        if (!isset($this->engines[$engine])) {
195
            $this->engines[$engine] = $this->createEngine($engine);
196
        }
197
198
        return $this->engines[$engine];
199
    }
200
201
    /**
202
     * Detect engine by view path (automatically resolved based on extension). Method require
203
     * improvements and 2nd level caching.
204
     *
205
     * @param string $path
206
     *
207
     * @return string
208
     */
209
    protected function detectEngine(string $path): string
210
    {
211
        //File extension can help us to detect engine faster (attention, does not work with complex
212
        //extensions at this moment).
213
        $extension = $this->files->extension($path);
214
215
        $result = null;
216
        $previousMatch = 0;
217
        foreach ($this->config->getEngines() as $engine) {
218
            if (!empty($extension)) {
219
                if ($extension == $this->config->engineExtension($engine)) {
220
                    return $engine;
221
                } else {
222
                    continue;
223
                }
224
            }
225
226
            if (
227
                strlen($this->config->engineExtension($engine)) > $previousMatch
228
                && $this->engineLoader($engine)->exists($path)
229
            ) {
230
                $previousMatch = strlen($this->config->engineExtension($engine));
231
                $result = $engine;
232
            }
233
        }
234
235
        if (empty($result)) {
236
            throw new ViewsException("Unable to detect view engine for '{$path}'");
237
        }
238
239
        return $result;
240
    }
241
242
    /**
243
     * Create engine instance.
244
     *
245
     * @param string $engine
246
     *
247
     * @return EngineInterface
248
     *
249
     * @throws ViewsException
250
     */
251
    protected function createEngine(string $engine): EngineInterface
252
    {
253
        if (!$this->config->hasEngine($engine)) {
254
            throw new ViewsException("Undefined engine '{$engine}'");
255
        }
256
257
        //Populating constructor parameters
258
        $options = $this->config->engineOptions($engine);
259
        $options += [
260
            'loader'      => $this->engineLoader($engine),
261
            'environment' => $this->getEnvironment()
262
        ];
263
264
        //We have to create an engine
265
        $benchmark = $this->benchmark('engine', $engine);
266
        try {
267
            //Creating engine instance
268
            return $this->container->make($this->config->engineClass($engine), $options);
269
        } finally {
270
            $this->benchmark($benchmark);
271
        }
272
    }
273
274
    /**
275
     * @param ViewsConfig $config
276
     *
277
     * @return EnvironmentInterface
278
     */
279
    protected function createEnvironment(ViewsConfig $config): EnvironmentInterface
280
    {
281
        return new DynamicEnvironment(
282
            $config->environmentDependencies(),
283
            $config->cacheEnabled(),
284
            $config->cacheDirectory(),
285
            $this->container
286
        );
287
    }
288
289
    /**
290
     * Getting isolated view loader (class responsible for locating files and isolating view
291
     * namespaces). Isolation is done by forcing specific file extension. MUST NOT return same
292
     * instance for different engines!
293
     *
294
     * @param string $engine Forced extension value.
295
     *
296
     * @return LoaderInterface
297
     */
298
    private function engineLoader(string $engine = null): LoaderInterface
299
    {
300
        return $this->loader->withExtension($this->config->engineExtension($engine));
301
    }
302
}