Passed
Push — develop ( 63d0b7...ee8da9 )
by Brent
03:37
created

Stitcher::save()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 11
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 = is_array($routes) ? $routes: [$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
            if (!is_array($fileContents)) {
247
                continue;
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 $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
     * @param callable $callback
404
     */
405
    public function done(callable $callback) {
406
        $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...
407
408
        $donePromise->when($callback);
409
    }
410
411
}
412
413
414