Passed
Push — master ( 59db0f...9fb939 )
by Brent
04:37
created

Stitcher::stitch()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 21
nc 5
nop 2
dl 0
loc 35
rs 8.439
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
        'engines.async'      => true,
49
        'caches.image'       => true,
50
        'optimizer.options'  => [],
51
    ];
52
53
    /**
54
     * A collection of promises representing Stitcher's state.
55
     *
56
     * @var Promise[]
57
     */
58
    private $promises = [];
59
60
    /**
61
     * @var string
62
     */
63
    private $srcDir;
64
65
    /**
66
     * @var string
67
     */
68
    private $publicDir;
69
70
    /**
71
     * @var string
72
     */
73
    private $templateDir;
74
75
    /**
76
     * @see \Brendt\Stitcher\Stitcher::create()
77
     *
78
     * @param string $srcDir
79
     * @param string $publicDir
80
     * @param string $templateDir
81
     */
82
    private function __construct(?string $srcDir = './src', ?string $publicDir = './public', ?string $templateDir = './src/template') {
83
        $this->srcDir = $srcDir;
84
        $this->publicDir = $publicDir;
85
        $this->templateDir = $templateDir;
86
    }
87
88
    /**
89
     * Static constructor
90
     *
91
     * @param string $configPath
92
     * @param array  $defaultConfig
93
     *
94
     * @return Stitcher
95
     *
96
     */
97
    public static function create(string $configPath = './config.yml', array $defaultConfig = []) : Stitcher {
98
        self::$container = new ContainerBuilder();
99
100
        $configPathParts = explode('/', $configPath);
101
        $configFileName = array_pop($configPathParts);
102
        $configPath = implode('/', $configPathParts) . '/';
103
        $configFiles = Finder::create()->files()->in($configPath)->name($configFileName)->depth(0);
104
        $srcDir = null;
105
        $publicDir = null;
106
        $templateDir = null;
107
108
        /** @var SplFileInfo $configFile */
109
        foreach ($configFiles as $configFile) {
110
            $fileConfig = Yaml::parse($configFile->getContents());
111
112
            $config = array_merge(
113
                self::$configDefaults,
114
                Config::flatten($fileConfig),
115
                $defaultConfig
116
            );
117
118
119
            $flatConfig = Config::flatten($config);
120
            $flatConfig['directories.template'] = $flatConfig['directories.template'] ?? $flatConfig['directories.src'];
121
122
            foreach ($flatConfig as $key => $value) {
123
                self::$container->setParameter($key, $value);
124
            }
125
126
            $srcDir = $flatConfig['directories.src'] ?? $srcDir;
127
            $publicDir = $flatConfig['directories.public'] ?? $publicDir;
128
            $templateDir = $flatConfig['directories.template'] ?? $templateDir;
129
130
            if (isset($fileConfig['meta'])) {
131
                self::$container->setParameter('meta', $fileConfig['meta']);
132
            }
133
        }
134
135
        $stitcher = new self($srcDir, $publicDir, $templateDir);
136
        self::$container->set('stitcher', $stitcher);
137
138
        $serviceLoader = new YamlFileLoader(self::$container, new FileLocator(__DIR__));
139
        $serviceLoader->load('services.yml');
140
141
        return $stitcher;
142
    }
143
144
    /**
145
     * @param string $id
146
     *
147
     * @return mixed
148
     */
149
    public static function get(string $id) {
150
        return self::$container->get($id);
151
    }
152
153
    /**
154
     * @param string $key
155
     *
156
     * @return mixed
157
     */
158
    public static function getParameter(string $key) {
159
        return self::$container->getParameter($key);
160
    }
161
162
    /**
163
     * The core stitcher function. This function will compile the configured site and return an array of parsed
164
     * data.
165
     *
166
     * Compiling a site is done in the following steps.
167
     *
168
     *      - Load the site configuration @see \Brendt\Stitcher\Stitcher::loadSite()
169
     *      - Load all available templates @see \Brendt\Stitcher\Stitcher::loadTemplates()
170
     *      - Loop over all pages and transform every page with the configured adapters (in any are set) @see
171
     *      \Brendt\Stitcher\Stitcher::parseAdapters()
172
     *      - Loop over all transformed pages and parse the variables which weren't parsed by the page's adapters. @see
173
     *      \Brendt\Stitcher\Stitcher::parseVariables()
174
     *      - Add all variables to the template engine and render the HTML for each page.
175
     *
176
     * This function takes two optional parameters which are used to render pages on the fly when using the
177
     * developer controller. The first one, `routes` will take a string or array of routes which should be rendered,
178
     * instead of all available routes. The second one, `filterValue` is used to provide a filter when the
179
     * CollectionAdapter is used, and only one entry page should be rendered.
180
     *
181
     * @param string|array $routes
182
     * @param string       $filterValue
183
     *
184
     * @return array
185
     * @throws TemplateNotFoundException
186
     *
187
     * @see \Brendt\Stitcher\Stitcher::save()
188
     * @see \Brendt\Stitcher\Controller\DevController::run()
189
     * @see \Brendt\Stitcher\Adapter\CollectionAdapter::transform()
190
     */
191
    public function stitch($routes = [], string $filterValue = null) {
192
        /** @var TemplateEngineFactory $templateEngineFactory */
193
        $templateEngineFactory = self::get('factory.template');
194
        $templateEngine = $templateEngineFactory->getDefault();
195
        $blanket = [];
196
197
        $site = $this->loadSite((array) $routes);
198
        $templates = $this->loadTemplates();
199
200
        foreach ($site as $page) {
201
            $templateIsset = isset($templates[$page->getTemplatePath()]);
202
203
            if (!$templateIsset) {
204
                if ($template = $page->getTemplatePath()) {
205
                    throw new TemplateNotFoundException("Template {$template} not found.");
206
                } else {
207
                    throw new TemplateNotFoundException('No template was set.');
208
                }
209
            }
210
211
            $pages = $this->parseAdapters($page, $filterValue);
212
213
            $pageTemplate = $templates[$page->getTemplatePath()];
214
            foreach ($pages as $entryPage) {
215
                $entryPage = $this->parseVariables($entryPage);
216
217
                // Render each page
218
                $templateEngine->addTemplateVariables($entryPage->getVariables());
219
                $blanket[$entryPage->getId()] = $templateEngine->renderTemplate($pageTemplate);
220
                $templateEngine->clearTemplateVariables();
221
            }
222
        }
223
224
        return $blanket;
225
    }
226
227
    /**
228
     * Load a site from YAML configuration files in the `directories.src`/site directory.
229
     * All YAML files are loaded and parsed into Page objects and added to a Site collection.
230
     *
231
     * @param array $routes
232
     *
233
     * @return Site
234
     * @throws InvalidSiteException
235
     * @see \Brendt\Stitcher\Site\Page
236
     * @see \Brendt\Stitcher\Site\Site
237
     */
238
    public function loadSite(array $routes = []) : Site {
239
        /** @var SplFileInfo[] $files */
240
        $files = Finder::create()->files()->in("{$this->srcDir}/site")->name('*.yml');
241
        $site = new Site();
242
243
        foreach ($files as $file) {
244
            try {
245
                $fileContents = (array) Yaml::parse($file->getContents());
246
            } catch (ParseException $e) {
247
                throw new InvalidSiteException("{$file->getRelativePathname()}: {$e->getMessage()}");
248
            }
249
250
            foreach ($fileContents as $route => $data) {
251
                if (count($routes) && !in_array($route, $routes)) {
252
                    continue;
253
                }
254
255
                $page = new Page($route, $data);
256
                $site->addPage($page);
257
            }
258
        }
259
260
        return $site;
261
    }
262
263
    /**
264
     * Load all templates from either the `directories.template` directory. Depending on the configured template
265
     * engine, set with `engines.template`; .html or .tpl files will be loaded.
266
     *
267
     * @return SplFileInfo[]
268
     */
269
    public function loadTemplates() {
270
        /** @var TemplateEngineFactory $templateEngineFactory */
271
        $templateEngineFactory = self::get('factory.template');
272
        $templateEngine = $templateEngineFactory->getDefault();
273
        $templateExtension = $templateEngine->getTemplateExtension();
274
275
        /** @var SplFileInfo[] $files */
276
        $files = Finder::create()->files()->in($this->templateDir)->name("*.{$templateExtension}");
277
        $templates = [];
278
279
        foreach ($files as $file) {
280
            $id = str_replace(".{$templateExtension}", '', $file->getRelativePathname());
281
            $templates[$id] = $file;
282
        }
283
284
        return $templates;
285
    }
286
287
    /**
288
     * This function takes a page and optional entry id. The page's adapters will be loaded and looped.
289
     * An adapter will transform a page's original configuration and variables to one or more pages.
290
     * An entry id can be provided as a filter. This filter can be used in an adapter to skip rendering unnecessary
291
     * pages. The filter parameter is used to render pages on the fly when using the developer controller.
292
     *
293
     * @param Page   $page
294
     * @param string $entryId
295
     *
296
     * @return Page[]
297
     *
298
     * @see  \Brendt\Stitcher\Adapter\Adapter::transform()
299
     * @see  \Brendt\Stitcher\Controller\DevController::run()
300
     */
301
    public function parseAdapters(Page $page, $entryId = null) {
302
        if (!$page->getAdapters()) {
303
            return [$page->getId() => $page];
304
        }
305
306
        $pages = [$page];
307
308
        foreach ($page->getAdapters() as $type => $adapterConfig) {
309
            /** @var Adapter $adapter */
310
            $adapter = self::get("adapter.{$type}");
311
312
            if ($entryId !== null) {
313
                $pages = $adapter->transform($pages, $entryId);
314
            } else {
315
                $pages = $adapter->transform($pages);
316
            }
317
        }
318
319
        return $pages;
320
    }
321
322
    /**
323
     * This function takes a Page object and parse its variables using a Parser. It will only parse variables which
324
     * weren't parsed already by an adapter.
325
     *
326
     * @param Page $page
327
     *
328
     * @return Page
329
     *
330
     * @see \Brendt\Stitcher\Factory\ParserFactory
331
     * @see \Brendt\Stitcher\Parser\Parser
332
     * @see \Brendt\Stitcher\Site\Page::isParsedVariable()
333
     */
334
    public function parseVariables(Page $page) {
335
        foreach ($page->getVariables() as $name => $value) {
336
            if ($page->isParsedVariable($name)) {
337
                continue;
338
            }
339
340
            $page
341
                ->setVariableValue($name, $this->getData($value))
342
                ->setVariableIsParsed($name);
343
        }
344
345
        return $page;
346
    }
347
348
    /**
349
     * This function will save a stitched output to HTML files in the `directories.public` directory.
350
     *
351
     * @param array $blanket
352
     *
353
     * @see \Brendt\Stitcher\Stitcher::stitch()
354
     */
355
    public function save(array $blanket) {
356
        $fs = new Filesystem();
357
358
        foreach ($blanket as $path => $page) {
359
            if ($path === '/') {
360
                $path = 'index';
361
            }
362
363
            $fs->dumpFile($this->publicDir . "/{$path}.html", $page);
364
        }
365
    }
366
367
    /**
368
     * This function will get the parser based on the value. This value is parsed by the parser, or returned if no
369
     * suitable parser was found.
370
     *
371
     * @param $value
372
     *
373
     * @return mixed
374
     *
375
     * @see \Brendt\Stitcher\Factory\ParserFactory
376
     */
377
    private function getData($value) {
378
        /** @var ParserFactory $parserFactory */
379
        $parserFactory = self::get('factory.parser');
380
        $parser = $parserFactory->getByFileName($value);
381
382
        if (!$parser) {
383
            return $value;
384
        }
385
386
        return $parser->parse($value);
387
    }
388
389
    /**
390
     * @param Promise|null $promise
391
     *
392
     * @return Stitcher
393
     */
394
    public function addPromise(?Promise $promise) : Stitcher {
395
        if ($promise) {
396
            $this->promises[] = $promise;
397
        }
398
399
        return $this;
400
    }
401
402
    /**
403
     * @return Promise
404
     */
405
    public function getPromise() : Promise {
406
        return \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...
407
    }
408
409
    /**
410
     * @param callable $callback
411
     */
412
    public function done(callable $callback) {
413
        $donePromise = $this->getPromise();
414
415
        $donePromise->when($callback);
416
    }
417
418
}
419
420
421