Passed
Push — master ( 387e5c...a50c27 )
by Brent
03:15
created

Stitcher   C

Complexity

Total Complexity 36

Size/Duplication

Total Lines 386
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 18

Importance

Changes 0
Metric Value
dl 0
loc 386
rs 6.4532
c 0
b 0
f 0
wmc 36
lcom 4
cbo 18

14 Methods

Rating   Name   Duplication   Size   Complexity  
A loadTemplates() 0 17 2
A parseAdapters() 0 20 4
A parseVariables() 0 13 3
A save() 0 11 3
A getData() 0 11 2
A addPromise() 0 7 2
A __construct() 0 5 1
B create() 0 43 4
A get() 0 3 1
A getParameter() 0 3 1
B stitch() 0 35 5
B loadSite() 0 24 6
A getPromise() 0 3 1
A done() 0 5 1
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
    ];
51
52
    /**
53
     * A collection of promises representing Stitcher's state.
54
     *
55
     * @var Promise[]
56
     */
57
    private $promises = [];
58
59
    /**
60
     * @var string
61
     */
62
    private $srcDir;
63
64
    /**
65
     * @var string
66
     */
67
    private $publicDir;
68
69
    /**
70
     * @var string
71
     */
72
    private $templateDir;
73
74
    /**
75
     * @see \Brendt\Stitcher\Stitcher::create()
76
     *
77
     * @param string $srcDir
78
     * @param string $publicDir
79
     * @param string $templateDir
80
     */
81
    private function __construct(?string $srcDir = './src', ?string $publicDir = './public', ?string $templateDir = './src/template') {
82
        $this->srcDir = $srcDir;
83
        $this->publicDir = $publicDir;
84
        $this->templateDir = $templateDir;
85
    }
86
87
    /**
88
     * Static constructor
89
     *
90
     * @param string $configPath
91
     * @param array  $defaultConfig
92
     *
93
     * @return Stitcher
94
     *
95
     */
96
    public static function create(string $configPath = './config.yml', array $defaultConfig = []) : Stitcher {
97
        self::$container = new ContainerBuilder();
98
99
        $configPathParts = explode('/', $configPath);
100
        $configFileName = array_pop($configPathParts);
101
        $configPath = implode('/', $configPathParts) . '/';
102
        $configFiles = Finder::create()->files()->in($configPath)->name($configFileName);
103
        $srcDir = null;
104
        $publicDir = null;
105
        $templateDir = null;
106
107
        /** @var SplFileInfo $configFile */
108
        foreach ($configFiles as $configFile) {
109
            $config = array_merge(
110
                self::$configDefaults,
111
                Yaml::parse($configFile->getContents()),
112
                $defaultConfig
113
            );
114
115
            $flatConfig = Config::flatten($config);
116
            $flatConfig['directories.template'] = $flatConfig['directories.template'] ?? $flatConfig['directories.src'];
117
118
            foreach ($flatConfig as $key => $value) {
119
                self::$container->setParameter($key, $value);
120
            }
121
122
            $srcDir = $flatConfig['directories.src'] ?? $srcDir;
123
            $publicDir = $flatConfig['directories.public'] ?? $publicDir;
124
            $templateDir = $flatConfig['directories.template'] ?? $templateDir;
125
126
            if (isset($config['meta'])) {
127
                self::$container->setParameter('meta', $config['meta']);
128
            }
129
        }
130
131
        $stitcher = new self($srcDir, $publicDir, $templateDir);
132
        self::$container->set('stitcher', $stitcher);
133
134
        $serviceLoader = new YamlFileLoader(self::$container, new FileLocator(__DIR__));
135
        $serviceLoader->load('services.yml');
136
137
        return $stitcher;
138
    }
139
140
    /**
141
     * @param string $id
142
     *
143
     * @return mixed
144
     */
145
    public static function get(string $id) {
146
        return self::$container->get($id);
147
    }
148
149
    /**
150
     * @param string $key
151
     *
152
     * @return mixed
153
     */
154
    public static function getParameter(string $key) {
155
        return self::$container->getParameter($key);
156
    }
157
158
    /**
159
     * The core stitcher function. This function will compile the configured site and return an array of parsed
160
     * data.
161
     *
162
     * Compiling a site is done in the following steps.
163
     *
164
     *      - Load the site configuration @see \Brendt\Stitcher\Stitcher::loadSite()
165
     *      - Load all available templates @see \Brendt\Stitcher\Stitcher::loadTemplates()
166
     *      - Loop over all pages and transform every page with the configured adapters (in any are set) @see
167
     *      \Brendt\Stitcher\Stitcher::parseAdapters()
168
     *      - Loop over all transformed pages and parse the variables which weren't parsed by the page's adapters. @see
169
     *      \Brendt\Stitcher\Stitcher::parseVariables()
170
     *      - Add all variables to the template engine and render the HTML for each page.
171
     *
172
     * This function takes two optional parameters which are used to render pages on the fly when using the
173
     * developer controller. The first one, `routes` will take a string or array of routes which should be rendered,
174
     * instead of all available routes. The second one, `filterValue` is used to provide a filter when the
175
     * CollectionAdapter is used, and only one entry page should be rendered.
176
     *
177
     * @param string|array $routes
178
     * @param string       $filterValue
179
     *
180
     * @return array
181
     * @throws TemplateNotFoundException
182
     *
183
     * @see \Brendt\Stitcher\Stitcher::save()
184
     * @see \Brendt\Stitcher\Controller\DevController::run()
185
     * @see \Brendt\Stitcher\Adapter\CollectionAdapter::transform()
186
     */
187
    public function stitch($routes = [], string $filterValue = null) {
188
        /** @var TemplateEngineFactory $templateEngineFactory */
189
        $templateEngineFactory = self::get('factory.template');
190
        $templateEngine = $templateEngineFactory->getDefault();
191
        $blanket = [];
192
193
        $site = $this->loadSite((array) $routes);
194
        $templates = $this->loadTemplates();
195
196
        foreach ($site as $page) {
197
            $templateIsset = isset($templates[$page->getTemplatePath()]);
198
199
            if (!$templateIsset) {
200
                if ($template = $page->getTemplatePath()) {
201
                    throw new TemplateNotFoundException("Template {$template} not found.");
202
                } else {
203
                    throw new TemplateNotFoundException('No template was set.');
204
                }
205
            }
206
207
            $pages = $this->parseAdapters($page, $filterValue);
208
209
            $pageTemplate = $templates[$page->getTemplatePath()];
210
            foreach ($pages as $entryPage) {
211
                $entryPage = $this->parseVariables($entryPage);
212
213
                // Render each page
214
                $templateEngine->addTemplateVariables($entryPage->getVariables());
215
                $blanket[$entryPage->getId()] = $templateEngine->renderTemplate($pageTemplate);
216
                $templateEngine->clearTemplateVariables();
217
            }
218
        }
219
220
        return $blanket;
221
    }
222
223
    /**
224
     * Load a site from YAML configuration files in the `directories.src`/site directory.
225
     * All YAML files are loaded and parsed into Page objects and added to a Site collection.
226
     *
227
     * @param array $routes
228
     *
229
     * @return Site
230
     * @throws InvalidSiteException
231
     * @see \Brendt\Stitcher\Site\Page
232
     * @see \Brendt\Stitcher\Site\Site
233
     */
234
    public function loadSite(array $routes = []) : Site {
235
        /** @var SplFileInfo[] $files */
236
        $files = Finder::create()->files()->in("{$this->srcDir}/site")->name('*.yml');
237
        $site = new Site();
238
239
        foreach ($files as $file) {
240
            try {
241
                $fileContents = (array) Yaml::parse($file->getContents());
242
            } catch (ParseException $e) {
243
                throw new InvalidSiteException("{$file->getRelativePathname()}: {$e->getMessage()}");
244
            }
245
246
            foreach ($fileContents as $route => $data) {
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|null $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
     * @return Promise
400
     */
401
    public function getPromise() : Promise {
402
        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...
403
    }
404
405
    /**
406
     * @param callable $callback
407
     */
408
    public function done(callable $callback) {
409
        $donePromise = $this->getPromise();
410
411
        $donePromise->when($callback);
412
    }
413
414
}
415
416
417