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); |
|
|
|
|
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) { |
|
|
|
|
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); |
|
|
|
|
403
|
|
|
|
404
|
|
|
$donePromise->when($callback); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
|
410
|
|
|
|
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.