Total Complexity | 56 |
Total Lines | 405 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like ThemeCompiler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ThemeCompiler, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
28 | #[\Shopware\Core\Framework\Log\Package('storefront')] |
||
29 | class ThemeCompiler implements ThemeCompilerInterface |
||
30 | { |
||
31 | /** |
||
32 | * @internal |
||
33 | * |
||
34 | * @param Package[] $packages |
||
35 | */ |
||
36 | public function __construct( |
||
37 | private readonly FilesystemOperator $filesystem, |
||
38 | private readonly FilesystemOperator $tempFilesystem, |
||
39 | private readonly ThemeFileResolver $themeFileResolver, |
||
40 | private readonly bool $debug, |
||
41 | private readonly EventDispatcherInterface $eventDispatcher, |
||
42 | private readonly ThemeFileImporterInterface $themeFileImporter, |
||
43 | private readonly iterable $packages, |
||
44 | private readonly CacheInvalidator $logger, |
||
45 | private readonly AbstractThemePathBuilder $themePathBuilder, |
||
46 | private readonly string $projectDir, |
||
47 | private readonly AbstractScssCompiler $scssCompiler, |
||
48 | private readonly MessageBusInterface $messageBus, |
||
49 | private readonly int $themeFileDeleteDelay, |
||
50 | private readonly bool $autoPrefix = false |
||
51 | ) { |
||
52 | } |
||
53 | |||
54 | public function compileTheme( |
||
55 | string $salesChannelId, |
||
56 | string $themeId, |
||
57 | StorefrontPluginConfiguration $themeConfig, |
||
58 | StorefrontPluginConfigurationCollection $configurationCollection, |
||
59 | bool $withAssets, |
||
60 | Context $context |
||
61 | ): void { |
||
62 | $resolvedFiles = $this->themeFileResolver->resolveFiles($themeConfig, $configurationCollection, false); |
||
63 | |||
64 | $styleFiles = $resolvedFiles[ThemeFileResolver::STYLE_FILES]; |
||
65 | |||
66 | $concatenatedStyles = $this->concatenateStyles( |
||
67 | $styleFiles, |
||
68 | $themeConfig, |
||
69 | $salesChannelId |
||
70 | ); |
||
71 | |||
72 | $compiled = $this->compileStyles( |
||
73 | $concatenatedStyles, |
||
74 | $themeConfig, |
||
75 | $styleFiles->getResolveMappings(), |
||
76 | $salesChannelId, |
||
77 | $themeId, |
||
78 | $context |
||
79 | ); |
||
80 | |||
81 | $concatenatedScripts = $this->getConcatenatedScripts($resolvedFiles[ThemeFileResolver::SCRIPT_FILES], $themeConfig, $salesChannelId); |
||
82 | |||
83 | $newThemeHash = Uuid::randomHex(); |
||
84 | $themePrefix = $this->themePathBuilder->generateNewPath($salesChannelId, $themeId, $newThemeHash); |
||
|
|||
85 | $oldThemePrefix = $this->themePathBuilder->assemblePath($salesChannelId, $themeId); |
||
86 | |||
87 | try { |
||
88 | $this->writeCompiledFiles($themePrefix, $themeId, $compiled, $concatenatedScripts, $withAssets, $themeConfig, $configurationCollection); |
||
89 | } catch (\Throwable $e) { |
||
90 | // delete folder in case of error and rethrow exception |
||
91 | if ($themePrefix !== $oldThemePrefix) { |
||
92 | $this->filesystem->deleteDirectory($themePrefix); |
||
93 | } |
||
94 | |||
95 | throw $e; |
||
96 | } |
||
97 | |||
98 | $this->themePathBuilder->saveSeed($salesChannelId, $themeId, $newThemeHash); |
||
99 | |||
100 | // only delete the old directory if the `themePathBuilder` actually returned a new path and supports seeding |
||
101 | if ($themePrefix !== $oldThemePrefix) { |
||
102 | $stamps = []; |
||
103 | |||
104 | if ($this->themeFileDeleteDelay > 0) { |
||
105 | // also delete with a delay, so that the old theme is still available for a while in case some CDN delivers stale content |
||
106 | // delay is configured in seconds, symfony expects milliseconds |
||
107 | $stamps[] = new DelayStamp($this->themeFileDeleteDelay * 1000); |
||
108 | } |
||
109 | $this->messageBus->dispatch( |
||
110 | new Envelope( |
||
111 | new DeleteThemeFilesMessage($oldThemePrefix, $salesChannelId, $themeId), |
||
112 | $stamps |
||
113 | ) |
||
114 | ); |
||
115 | } |
||
116 | |||
117 | // Reset cache buster state for improving performance in getMetadata |
||
118 | $this->logger->invalidate(['theme-metaData'], true); |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * @param array<string, string> $resolveMappings |
||
123 | */ |
||
124 | public function getResolveImportPathsCallback(array $resolveMappings): \Closure |
||
147 | }; |
||
148 | } |
||
149 | |||
150 | private function copyAssets( |
||
151 | StorefrontPluginConfiguration $configuration, |
||
152 | StorefrontPluginConfigurationCollection $configurationCollection, |
||
153 | string $outputPath |
||
154 | ): void { |
||
155 | if (!$configuration->getAssetPaths()) { |
||
156 | return; |
||
157 | } |
||
158 | |||
159 | foreach ($configuration->getAssetPaths() as $asset) { |
||
160 | if (mb_strpos((string) $asset, '@') === 0) { |
||
161 | $name = mb_substr((string) $asset, 1); |
||
162 | $config = $configurationCollection->getByTechnicalName($name); |
||
163 | if (!$config) { |
||
164 | throw new InvalidThemeException($name); |
||
165 | } |
||
166 | |||
167 | $this->copyAssets($config, $configurationCollection, $outputPath); |
||
168 | |||
169 | continue; |
||
170 | } |
||
171 | |||
172 | if ($asset[0] !== '/' && file_exists($this->projectDir . '/' . $asset)) { |
||
173 | $asset = $this->projectDir . '/' . $asset; |
||
174 | } |
||
175 | |||
176 | $assets = $this->themeFileImporter->getCopyBatchInputsForAssets($asset, $outputPath, $configuration); |
||
177 | |||
178 | CopyBatch::copy($this->filesystem, ...$assets); |
||
179 | } |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * @param array<string, string> $resolveMappings |
||
184 | */ |
||
185 | private function compileStyles( |
||
186 | string $concatenatedStyles, |
||
187 | StorefrontPluginConfiguration $configuration, |
||
188 | array $resolveMappings, |
||
189 | string $salesChannelId, |
||
190 | string $themeId, |
||
191 | Context $context |
||
192 | ): string { |
||
193 | $variables = $this->dumpVariables($configuration->getThemeConfig() ?? [], $themeId, $salesChannelId, $context); |
||
194 | $features = $this->getFeatureConfigScssMap(); |
||
195 | |||
196 | $resolveImportPath = $this->getResolveImportPathsCallback($resolveMappings); |
||
197 | |||
198 | $importPaths = []; |
||
199 | |||
200 | $cwd = \getcwd(); |
||
201 | if ($cwd !== false) { |
||
202 | $importPaths[] = $cwd; |
||
203 | } |
||
204 | |||
205 | $importPaths[] = $resolveImportPath; |
||
206 | |||
207 | $compilerConfig = new CompilerConfiguration( |
||
208 | [ |
||
209 | 'importPaths' => $importPaths, |
||
210 | 'outputStyle' => $this->debug ? OutputStyle::EXPANDED : OutputStyle::COMPRESSED, |
||
211 | ] |
||
212 | ); |
||
213 | |||
214 | try { |
||
215 | $cssOutput = $this->scssCompiler->compileString( |
||
216 | $compilerConfig, |
||
217 | $features . $variables . $concatenatedStyles |
||
218 | ); |
||
219 | } catch (\Throwable $exception) { |
||
220 | throw new ThemeCompileException( |
||
221 | $configuration->getTechnicalName(), |
||
222 | $exception->getMessage() |
||
223 | ); |
||
224 | } |
||
225 | |||
226 | if ($this->autoPrefix === true) { |
||
227 | $autoPreFixer = new Autoprefixer($cssOutput); |
||
228 | /** @var string|false $cssOutput */ |
||
229 | $cssOutput = $autoPreFixer->compile($this->debug); |
||
230 | if ($cssOutput === false) { |
||
231 | throw new ThemeCompileException( |
||
232 | $configuration->getTechnicalName(), |
||
233 | 'CSS parser not initialized' |
||
234 | ); |
||
235 | } |
||
236 | } |
||
237 | |||
238 | return $cssOutput; |
||
239 | } |
||
240 | |||
241 | private function getImportFileExtension(string $extension): string |
||
242 | { |
||
243 | // If the import has no extension, it must be a SCSS module. |
||
244 | if ($extension === '') { |
||
245 | return '.scss'; |
||
246 | } |
||
247 | |||
248 | // If the import has a .min extension, we assume it must be a compiled CSS file. |
||
249 | if ($extension === 'min') { |
||
250 | return '.css'; |
||
251 | } |
||
252 | |||
253 | // If it has any other extension, we don't assume a specific extension. |
||
254 | return ''; |
||
255 | } |
||
256 | |||
257 | /** |
||
258 | * Converts the feature config array to a SCSS map syntax. |
||
259 | * This allows reading of the feature flag config inside SCSS via `map.get` function. |
||
260 | * |
||
261 | * Output example: |
||
262 | * $sw-features: ("FEATURE_NEXT_1234": false, "FEATURE_NEXT_1235": true); |
||
263 | * |
||
264 | * @see https://sass-lang.com/documentation/values/maps |
||
265 | */ |
||
266 | private function getFeatureConfigScssMap(): string |
||
267 | { |
||
268 | $allFeatures = Feature::getAll(); |
||
269 | |||
270 | $featuresScss = implode(',', array_map(fn ($value, $key) => sprintf('"%s": %s', $key, json_encode($value, \JSON_THROW_ON_ERROR)), $allFeatures, array_keys($allFeatures))); |
||
271 | |||
272 | return sprintf('$sw-features: (%s);', $featuresScss); |
||
273 | } |
||
274 | |||
275 | /** |
||
276 | * @param array<string, string|int> $variables |
||
277 | * |
||
278 | * @return array<string> |
||
279 | */ |
||
280 | private function formatVariables(array $variables): array |
||
283 | } |
||
284 | |||
285 | /** |
||
286 | * @param array{fields?: array{value: null|string|array<mixed>, scss?: bool, type: string}[]} $config |
||
287 | */ |
||
288 | private function dumpVariables(array $config, string $themeId, string $salesChannelId, Context $context): string |
||
289 | { |
||
290 | $variables = [ |
||
291 | 'theme-id' => $themeId, |
||
292 | ]; |
||
293 | |||
294 | foreach ($config['fields'] ?? [] as $key => $data) { |
||
295 | if (!\is_array($data) || !$this->isDumpable($data)) { |
||
296 | continue; |
||
297 | } |
||
298 | |||
299 | if (\in_array($data['type'], ['media', 'textarea'], true) && \is_string($data['value'])) { |
||
300 | $variables[$key] = '\'' . $data['value'] . '\''; |
||
301 | } elseif ($data['type'] === 'switch' || $data['type'] === 'checkbox') { |
||
302 | $variables[$key] = (int) ($data['value']); |
||
303 | } elseif (!\is_array($data['value'])) { |
||
304 | $variables[$key] = (string) $data['value']; |
||
305 | } |
||
306 | } |
||
307 | |||
308 | foreach ($this->packages as $key => $package) { |
||
309 | $variables[sprintf('sw-asset-%s-url', $key)] = sprintf('\'%s\'', $package->getUrl('')); |
||
310 | } |
||
311 | |||
312 | $themeVariablesEvent = new ThemeCompilerEnrichScssVariablesEvent( |
||
313 | $variables, |
||
314 | $salesChannelId, |
||
315 | $context |
||
316 | ); |
||
317 | |||
318 | $this->eventDispatcher->dispatch($themeVariablesEvent); |
||
319 | |||
320 | $dump = str_replace( |
||
321 | ['#class#', '#variables#'], |
||
322 | [self::class, implode(\PHP_EOL, $this->formatVariables($themeVariablesEvent->getVariables()))], |
||
323 | $this->getVariableDumpTemplate() |
||
324 | ); |
||
325 | |||
326 | $this->tempFilesystem->write('theme-variables.scss', $dump); |
||
327 | |||
328 | return $dump; |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * @param array{value: string|array<mixed>|null, scss?: bool, type: string} $data |
||
333 | */ |
||
334 | private function isDumpable(array $data): bool |
||
361 | } |
||
362 | |||
363 | private function getVariableDumpTemplate(): string |
||
364 | { |
||
365 | return <<<PHP_EOL |
||
366 | // ATTENTION! This file is auto generated by the #class# and should not be edited. |
||
367 | |||
368 | #variables# |
||
369 | |||
370 | PHP_EOL; |
||
371 | } |
||
372 | |||
373 | private function concatenateStyles( |
||
374 | FileCollection $styleFiles, |
||
375 | StorefrontPluginConfiguration $themeConfig, |
||
376 | string $salesChannelId |
||
377 | ): string { |
||
378 | $concatenatedStyles = ''; |
||
379 | foreach ($styleFiles as $file) { |
||
380 | $concatenatedStyles .= $this->themeFileImporter->getConcatenableStylePath($file, $themeConfig); |
||
381 | } |
||
382 | $concatenatedStylesEvent = new ThemeCompilerConcatenatedStylesEvent($concatenatedStyles, $salesChannelId); |
||
383 | $this->eventDispatcher->dispatch($concatenatedStylesEvent); |
||
384 | |||
385 | return $concatenatedStylesEvent->getConcatenatedStyles(); |
||
386 | } |
||
387 | |||
388 | private function getConcatenatedScripts( |
||
389 | FileCollection $scriptFiles, |
||
390 | StorefrontPluginConfiguration $themeConfig, |
||
391 | string $salesChannelId |
||
392 | ): string { |
||
393 | $concatenatedScripts = ''; |
||
394 | foreach ($scriptFiles as $file) { |
||
395 | $concatenatedScripts .= $this->themeFileImporter->getConcatenableScriptPath($file, $themeConfig); |
||
396 | } |
||
397 | |||
398 | $concatenatedScriptsEvent = new ThemeCompilerConcatenatedScriptsEvent($concatenatedScripts, $salesChannelId); |
||
399 | $this->eventDispatcher->dispatch($concatenatedScriptsEvent); |
||
400 | |||
401 | return $concatenatedScriptsEvent->getConcatenatedScripts(); |
||
402 | } |
||
403 | |||
404 | private function writeCompiledFiles( |
||
433 | } |
||
434 | } |
||
435 | } |
||
436 |
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.