Test Failed
Push — develop ( 387e5c...98afd0 )
by Brent
04:42
created

Stitcher::addPromise()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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