Passed
Push — develop ( 9c4ea8...8f0383 )
by Brent
02:39
created

Stitcher::parseVariables()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Brendt\Stitcher;
4
5
use AsyncInterop\Promise;
6
use Brendt\Stitcher\Adapter\Adapter;
7
use Brendt\Stitcher\Exception\InvalidSiteException;
8
use Brendt\Stitcher\Exception\TemplateNotFoundException;
9
use Brendt\Stitcher\Factory\ParserFactory;
10
use Brendt\Stitcher\Factory\TemplateEngineFactory;
11
use Brendt\Stitcher\Site\Page;
12
use Brendt\Stitcher\Site\Site;
13
use Symfony\Component\Config\FileLocator;
14
use Symfony\Component\DependencyInjection\ContainerBuilder;
15
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
16
use Symfony\Component\Filesystem\Filesystem;
17
use Symfony\Component\Finder\Finder;
18
use Symfony\Component\Finder\SplFileInfo;
19
use Symfony\Component\Yaml\Exception\ParseException;
20
use Symfony\Component\Yaml\Yaml;
21
22
/**
23
 * The Stitcher class is the core compiler of every Stitcher application. This class takes care of all routes, pages,
24
 * templates and data, and "stitches" everything together.
25
 *
26
 * The stitching process is done in several steps, with the final result being a fully rendered website in the
27
 * `directories.public` folder.
28
 */
29
class Stitcher
30
{
31
    /**
32
     * @var ContainerBuilder
33
     */
34
    protected static $container;
35
36
    /**
37
     * @var array
38
     */
39
    protected static $configDefaults = [
40
        'directories.src'      => './src',
41
        'directories.public'   => './public',
42
        'directories.cache'    => './.cache',
43
        'meta'                 => [],
44
        'minify'               => false,
45
        'engines.template'     => 'smarty',
46
        'engines.image'        => 'gd',
47
        'engines.optimizer'    => true,
48
        'caches.image'         => true,
49
    ];
50
51
    /**
52
     * A collection of promises representing Stitcher's state.
53
     *
54
     * @var Promise[]
55
     */
56
    private $promises = [];
57
58
    /**
59
     * @var string
60
     */
61
    private $srcDir;
62
63
    /**
64
     * @var string
65
     */
66
    private $publicDir;
67
68
    /**
69
     * @var string
70
     */
71
    private $templateDir;
72
73
    /**
74
     * @see \Brendt\Stitcher\Stitcher::create()
75
     *
76
     * @param string $srcDir
77
     * @param string $publicDir
78
     * @param string $templateDir
79
     */
80
    private function __construct(?string $srcDir = './src', ?string $publicDir = './public', ?string $templateDir = './src/template') {
81
        $this->srcDir = $srcDir;
82
        $this->publicDir = $publicDir;
83
        $this->templateDir = $templateDir;
84
    }
85
86
    /**
87
     * Static constructor
88
     *
89
     * @param string $configPath
90
     * @param array  $defaultConfig
91
     *
92
     * @return Stitcher
93
     *
94
     */
95
    public static function create(string $configPath = './config.yml', array $defaultConfig = []) : Stitcher {
96
        self::$container = new ContainerBuilder();
97
98
        $configPathParts = explode('/', $configPath);
99
        $configFileName = array_pop($configPathParts);
100
        $configPath = implode('/', $configPathParts) . '/';
101
        $configFiles = Finder::create()->files()->in($configPath)->name($configFileName);
102
        $srcDir = null;
103
        $publicDir = null;
104
        $templateDir = null;
105
106
        /** @var SplFileInfo $configFile */
107
        foreach ($configFiles as $configFile) {
108
            $config = array_merge(
109
                self::$configDefaults,
110
                Yaml::parse($configFile->getContents()),
111
                $defaultConfig
112
            );
113
114
            $flatConfig = Config::flatten($config);
115
            $flatConfig['directories.template'] = $flatConfig['directories.template'] ?? $flatConfig['directories.src'];
116
117
            foreach ($flatConfig as $key => $value) {
118
                self::$container->setParameter($key, $value);
119
            }
120
121
            $srcDir = $flatConfig['directories.src'] ?? $srcDir;
122
            $publicDir = $flatConfig['directories.public'] ?? $publicDir;
123
            $templateDir = $flatConfig['directories.template'] ?? $templateDir;
124
125
            if (isset($config['meta'])) {
126
                self::$container->setParameter('meta', $config['meta']);
127
            }
128
        }
129
130
        $stitcher = new self($srcDir, $publicDir, $templateDir);
131
        self::$container->set('stitcher', $stitcher);
132
133
        $serviceLoader = new YamlFileLoader(self::$container, new FileLocator(__DIR__));
134
        $serviceLoader->load('services.yml');
135
136
        return $stitcher;
137
    }
138
139
    /**
140
     * @param string $id
141
     *
142
     * @return mixed
143
     */
144
    public static function get(string $id) {
145
        return self::$container->get($id);
146
    }
147
148
    /**
149
     * @param string $key
150
     *
151
     * @return mixed
152
     */
153
    public static function getParameter(string $key) {
154
        return self::$container->getParameter($key);
155
    }
156
157
    /**
158
     * The core stitcher function. This function will compile the configured site and return an array of parsed
159
     * data.
160
     *
161
     * Compiling a site is done in the following steps.
162
     *
163
     *      - Load the site configuration @see \Brendt\Stitcher\Stitcher::loadSite()
164
     *      - Load all available templates @see \Brendt\Stitcher\Stitcher::loadTemplates()
165
     *      - Loop over all pages and transform every page with the configured adapters (in any are set) @see
166
     *      \Brendt\Stitcher\Stitcher::parseAdapters()
167
     *      - Loop over all transformed pages and parse the variables which weren't parsed by the page's adapters. @see
168
     *      \Brendt\Stitcher\Stitcher::parseVariables()
169
     *      - Add all variables to the template engine and render the HTML for each page.
170
     *
171
     * This function takes two optional parameters which are used to render pages on the fly when using the
172
     * developer controller. The first one, `routes` will take a string or array of routes which should be rendered,
173
     * instead of all available routes. The second one, `filterValue` is used to provide a filter when the
174
     * CollectionAdapter is used, and only one entry page should be rendered.
175
     *
176
     * @param string|array $routes
177
     * @param string       $filterValue
178
     *
179
     * @return array
180
     * @throws TemplateNotFoundException
181
     *
182
     * @see \Brendt\Stitcher\Stitcher::save()
183
     * @see \Brendt\Stitcher\Controller\DevController::run()
184
     * @see \Brendt\Stitcher\Adapter\CollectionAdapter::transform()
185
     */
186
    public function stitch($routes = [], string $filterValue = null) {
187
        /** @var TemplateEngineFactory $templateEngineFactory */
188
        $templateEngineFactory = self::get('factory.template');
189
        $templateEngine = $templateEngineFactory->getDefault();
190
        $blanket = [];
191
192
        $site = $this->loadSite($routes);
0 ignored issues
show
Bug introduced by
It seems like $routes defined by parameter $routes on line 186 can also be of type string; however, Brendt\Stitcher\Stitcher::loadSite() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
193
        $templates = $this->loadTemplates();
194
195
        foreach ($site as $page) {
196
            $templateIsset = isset($templates[$page->getTemplatePath()]);
197
198
            if (!$templateIsset) {
199
                if ($template = $page->getTemplatePath()) {
200
                    throw new TemplateNotFoundException("Template {$template} not found.");
201
                } else {
202
                    throw new TemplateNotFoundException('No template was set.');
203
                }
204
            }
205
206
            $pages = $this->parseAdapters($page, $filterValue);
207
208
            $pageTemplate = $templates[$page->getTemplatePath()];
209
            foreach ($pages as $entryPage) {
210
                $entryPage = $this->parseVariables($entryPage);
211
212
                // Render each page
213
                $templateEngine->addTemplateVariables($entryPage->getVariables());
214
                $blanket[$entryPage->getId()] = $templateEngine->renderTemplate($pageTemplate);
215
                $templateEngine->clearTemplateVariables();
216
            }
217
        }
218
219
        return $blanket;
220
    }
221
222
    /**
223
     * Load a site from YAML configuration files in the `directories.src`/site directory.
224
     * All YAML files are loaded and parsed into Page objects and added to a Site collection.
225
     *
226
     * @param array $routes
227
     *
228
     * @return Site
229
     * @throws InvalidSiteException
230
     * @see \Brendt\Stitcher\Site\Page
231
     * @see \Brendt\Stitcher\Site\Site
232
     */
233
    public function loadSite($routes = []) : Site {
234
        /** @var SplFileInfo[] $files */
235
        $files = Finder::create()->files()->in("{$this->srcDir}/site")->name('*.yml');
236
        $site = new Site();
237
        $routes = (array) $routes;
238
239
        foreach ($files as $file) {
240
            try {
241
                $fileContents = Yaml::parse($file->getContents());
242
            } catch (ParseException $e) {
243
                throw new InvalidSiteException("{$file->getRelativePathname()}: {$e->getMessage()}");
244
            }
245
246
            foreach ($fileContents as $route => $data) {
0 ignored issues
show
Bug introduced by
The expression $fileContents of type string|array|object<stdClass> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
247
                if (count($routes) && !in_array($route, $routes)) {
248
                    continue;
249
                }
250
251
                $page = new Page($route, $data);
252
                $site->addPage($page);
253
            }
254
        }
255
256
        return $site;
257
    }
258
259
    /**
260
     * Load all templates from either the `directories.template` directory. Depending on the configured template
261
     * engine, set with `engines.template`; .html or .tpl files will be loaded.
262
     *
263
     * @return SplFileInfo[]
264
     */
265
    public function loadTemplates() {
266
        /** @var TemplateEngineFactory $templateEngineFactory */
267
        $templateEngineFactory = self::get('factory.template');
268
        $templateEngine = $templateEngineFactory->getDefault();
269
        $templateExtension = $templateEngine->getTemplateExtension();
270
271
        /** @var SplFileInfo[] $files */
272
        $files = Finder::create()->files()->in($this->templateDir)->name("*.{$templateExtension}");
273
        $templates = [];
274
275
        foreach ($files as $file) {
276
            $id = str_replace(".{$templateExtension}", '', $file->getRelativePathname());
277
            $templates[$id] = $file;
278
        }
279
280
        return $templates;
281
    }
282
283
    /**
284
     * This function takes a page and optional entry id. The page's adapters will be loaded and looped.
285
     * An adapter will transform a page's original configuration and variables to one or more pages.
286
     * An entry id can be provided as a filter. This filter can be used in an adapter to skip rendering unnecessary
287
     * pages. The filter parameter is used to render pages on the fly when using the developer controller.
288
     *
289
     * @param Page   $page
290
     * @param string $entryId
291
     *
292
     * @return Page[]
293
     *
294
     * @see  \Brendt\Stitcher\Adapter\Adapter::transform()
295
     * @see  \Brendt\Stitcher\Controller\DevController::run()
296
     */
297
    public function parseAdapters(Page $page, $entryId = null) {
298
        if (!$page->getAdapters()) {
299
            return [$page->getId() => $page];
300
        }
301
302
        $pages = [$page];
303
304
        foreach ($page->getAdapters() as $type => $adapterConfig) {
305
            /** @var Adapter $adapter */
306
            $adapter = self::get("adapter.{$type}");
307
308
            if ($entryId !== null) {
309
                $pages = $adapter->transform($pages, $entryId);
310
            } else {
311
                $pages = $adapter->transform($pages);
312
            }
313
        }
314
315
        return $pages;
316
    }
317
318
    /**
319
     * This function takes a Page object and parse its variables using a Parser. It will only parse variables which
320
     * weren't parsed already by an adapter.
321
     *
322
     * @param Page $page
323
     *
324
     * @return Page
325
     *
326
     * @see \Brendt\Stitcher\Factory\ParserFactory
327
     * @see \Brendt\Stitcher\Parser\Parser
328
     * @see \Brendt\Stitcher\Site\Page::isParsedVariable()
329
     */
330
    public function parseVariables(Page $page) {
331
        foreach ($page->getVariables() as $name => $value) {
332
            if ($page->isParsedVariable($name)) {
333
                continue;
334
            }
335
336
            $page
337
                ->setVariableValue($name, $this->getData($value))
338
                ->setVariableIsParsed($name);
339
        }
340
341
        return $page;
342
    }
343
344
    /**
345
     * This function will save a stitched output to HTML files in the `directories.public` directory.
346
     *
347
     * @param array $blanket
348
     *
349
     * @see \Brendt\Stitcher\Stitcher::stitch()
350
     */
351
    public function save(array $blanket) {
352
        $fs = new Filesystem();
353
354
        foreach ($blanket as $path => $page) {
355
            if ($path === '/') {
356
                $path = 'index';
357
            }
358
            
359
            $fs->dumpFile($this->publicDir . "/{$path}.html", $page);
360
        }
361
    }
362
363
    /**
364
     * This function will get the parser based on the value. This value is parsed by the parser, or returned if no
365
     * suitable parser was found.
366
     *
367
     * @param $value
368
     *
369
     * @return mixed
370
     *
371
     * @see \Brendt\Stitcher\Factory\ParserFactory
372
     */
373
    private function getData($value) {
374
        /** @var ParserFactory $parserFactory */
375
        $parserFactory = self::get('factory.parser');
376
        $parser = $parserFactory->getByFileName($value);
377
378
        if (!$parser) {
379
            return $value;
380
        }
381
382
        return $parser->parse($value);
383
    }
384
385
    /**
386
     * @param Promise $promise
387
     *
388
     * @return Stitcher
389
     */
390
    public function addPromise(?Promise $promise) : Stitcher {
391
        if ($promise) {
392
            $this->promises[] = $promise;
393
        }
394
395
        return $this;
396
    }
397
398
    /**
399
     * @param callable $callback
400
     */
401
    public function done(callable $callback) {
402
        $donePromise = \Amp\all($this->promises);
0 ignored issues
show
Documentation introduced by
$this->promises is of type array<integer,object<AsyncInterop\Promise>>, but the function expects a array<integer,object<Promise>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
403
404
        $donePromise->when($callback);
405
    }
406
407
}
408
409
410