Passed
Push — develop ( d94550...8e88eb )
by Brent
02:30
created

Stitcher::getParameter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
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
191
        $blanket = [];
192
193
        $site = $this->loadSite();
194
        $templates = $this->loadTemplates();
195
196
        if (is_string($routes)) {
197
            $routes = [$routes];
198
        }
199
200
        foreach ($site as $page) {
201
            $route = $page->getId();
202
203
            $skipRoute = count($routes) && !in_array($route, $routes);
204
            if ($skipRoute) {
205
                continue;
206
            }
207
208
            $templateIsset = isset($templates[$page->getTemplatePath()]);
209
210
            if (!$templateIsset) {
211
                if ($template = $page->getTemplatePath()) {
212
                    throw new TemplateNotFoundException("Template {$template} not found.");
213
                } else {
214
                    throw new TemplateNotFoundException('No template was set.');
215
                }
216
            }
217
218
            $pages = $this->parseAdapters($page, $filterValue);
219
220
            $pageTemplate = $templates[$page->getTemplatePath()];
221
            foreach ($pages as $entryPage) {
222
                $entryPage = $this->parseVariables($entryPage);
223
224
                // Render each page
225
                $templateEngine->addTemplateVariables($entryPage->getVariables());
226
                $blanket[$entryPage->getId()] = $templateEngine->renderTemplate($pageTemplate);
227
                $templateEngine->clearTemplateVariables();
228
            }
229
        }
230
231
        return $blanket;
232
    }
233
234
    /**
235
     * Load a site from YAML configuration files in the `directories.src`/site directory.
236
     * All YAML files are loaded and parsed into Page objects and added to a Site collection.
237
     *
238
     * @return Site
239
     * @throws InvalidSiteException
240
     *
241
     * @see \Brendt\Stitcher\Site\Page
242
     * @see \Brendt\Stitcher\Site\Site
243
     */
244
    public function loadSite() {
245
        /** @var SplFileInfo[] $files */
246
        $files = Finder::create()->files()->in("{$this->srcDir}/site")->name('*.yml');
247
        $site = new Site();
248
249
        foreach ($files as $file) {
250
            try {
251
                $fileContents = Yaml::parse($file->getContents());
252
            } catch (ParseException $e) {
253
                throw new InvalidSiteException("{$file->getRelativePathname()}: {$e->getMessage()}");
254
            }
255
256
            if (!is_array($fileContents)) {
257
                continue;
258
            }
259
260
            foreach ($fileContents as $route => $data) {
261
                $page = new Page($route, $data);
262
                $site->addPage($page);
263
            }
264
        }
265
266
        return $site;
267
    }
268
269
    /**
270
     * Load all templates from either the `directories.template` directory. Depending on the configured template
271
     * engine, set with `engines.template`; .html or .tpl files will be loaded.
272
     *
273
     * @return SplFileInfo[]
274
     */
275
    public function loadTemplates() {
276
        /** @var TemplateEngineFactory $templateEngineFactory */
277
        $templateEngineFactory = self::get('factory.template');
278
        $templateEngine = $templateEngineFactory->getDefault();
279
        $templateExtension = $templateEngine->getTemplateExtension();
280
281
        /** @var SplFileInfo[] $files */
282
        $files = Finder::create()->files()->in($this->templateDir)->name("*.{$templateExtension}");
283
        $templates = [];
284
285
        foreach ($files as $file) {
286
            $id = str_replace(".{$templateExtension}", '', $file->getRelativePathname());
287
            $templates[$id] = $file;
288
        }
289
290
        return $templates;
291
    }
292
293
    /**
294
     * This function takes a page and optional entry id. The page's adapters will be loaded and looped.
295
     * An adapter will transform a page's original configuration and variables to one or more pages.
296
     * An entry id can be provided as a filter. This filter can be used in an adapter to skip rendering unnecessary
297
     * pages. The filter parameter is used to render pages on the fly when using the developer controller.
298
     *
299
     * @param Page   $page
300
     * @param string $entryId
301
     *
302
     * @return Page[]
303
     *
304
     * @see  \Brendt\Stitcher\Adapter\Adapter::transform()
305
     * @see  \Brendt\Stitcher\Controller\DevController::run()
306
     */
307
    public function parseAdapters(Page $page, $entryId = null) {
308
        if (!$page->getAdapters()) {
309
            return [$page->getId() => $page];
310
        }
311
312
        $pages = [$page];
313
314
        foreach ($page->getAdapters() as $type => $adapterConfig) {
315
            /** @var Adapter $adapter */
316
            $adapter = self::get("adapter.{$type}");
317
318
            if ($entryId !== null) {
319
                $pages = $adapter->transform($pages, $entryId);
320
            } else {
321
                $pages = $adapter->transform($pages);
322
            }
323
        }
324
325
        return $pages;
326
    }
327
328
    /**
329
     * This function takes a Page object and parse its variables using a Parser. It will only parse variables which
330
     * weren't parsed already by an adapter.
331
     *
332
     * @param Page $page
333
     *
334
     * @return Page
335
     *
336
     * @see \Brendt\Stitcher\Factory\ParserFactory
337
     * @see \Brendt\Stitcher\Parser\Parser
338
     * @see \Brendt\Stitcher\Site\Page::isParsedVariable()
339
     */
340
    public function parseVariables(Page $page) {
341
        foreach ($page->getVariables() as $name => $value) {
342
            if ($page->isParsedVariable($name)) {
343
                continue;
344
            }
345
346
            $page
347
                ->setVariableValue($name, $this->getData($value))
348
                ->setVariableIsParsed($name);
349
        }
350
351
        return $page;
352
    }
353
354
    /**
355
     * This function will save a stitched output to HTML files in the `directories.public` directory.
356
     *
357
     * @param array $blanket
358
     *
359
     * @see \Brendt\Stitcher\Stitcher::stitch()
360
     */
361
    public function save(array $blanket) {
362
        $fs = new Filesystem();
363
364
        foreach ($blanket as $path => $page) {
365
            if ($path === '/') {
366
                $path = 'index';
367
            }
368
            
369
            $fs->dumpFile($this->publicDir . "/{$path}.html", $page);
370
        }
371
    }
372
373
    /**
374
     * This function will get the parser based on the value. This value is parsed by the parser, or returned if no
375
     * suitable parser was found.
376
     *
377
     * @param $value
378
     *
379
     * @return mixed
380
     *
381
     * @see \Brendt\Stitcher\Factory\ParserFactory
382
     */
383
    private function getData($value) {
384
        /** @var ParserFactory $parserFactory */
385
        $parserFactory = self::get('factory.parser');
386
        $parser = $parserFactory->getByFileName($value);
387
388
        if (!$parser) {
389
            return $value;
390
        }
391
392
        return $parser->parse($value);
393
    }
394
395
    /**
396
     * @param Promise $promise
397
     *
398
     * @return Stitcher
399
     */
400
    public function addPromise(?Promise $promise) : Stitcher {
401
        if ($promise) {
402
            $this->promises[] = $promise;
403
        }
404
405
        return $this;
406
    }
407
408
    /**
409
     * @param callable $callback
410
     */
411
    public function done(callable $callback) {
412
        $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...
413
414
        $donePromise->when($callback);
415
    }
416
417
}
418
419
420