Files
php-flasher/src/Prime/Response/Presenter/HtmlPresenter.php
T
Younes ENNAJI 3f920f2c01 Simplify PHPDoc comments in Plugin, HTTP, Stamp, and Test constraint classes
Removes verbose documentation while preserving type annotations.
2026-01-16 00:07:42 +01:00

147 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Flasher\Prime\Response\Presenter;
use Flasher\Prime\Response\Response;
use Livewire\LivewireManager;
final class HtmlPresenter implements PresenterInterface
{
public const FLASHER_REPLACE_ME = '/** {--FLASHER_REPLACE_ME--} **/';
public const HEAD_END_PLACE_HOLDER = '</head>';
public const BODY_END_PLACE_HOLDER = '</body>';
/**
* @throws \JsonException
*/
public function render(Response $response): string
{
/** @var array{csp_script_nonce?: ?string, envelopes_only?: bool} $context */
$context = $response->getContext();
/** @var array{envelopes: array<int, array{metadata: array{html?: string}}>} $options */
$options = $response->toArray();
$html = '';
foreach ($options['envelopes'] as $index => $envelope) {
if (isset($envelope['metadata']['html'])) {
$html .= $envelope['metadata']['html'];
unset($options['envelopes'][$index]);
}
}
$options['envelopes'] = array_values($options['envelopes']);
$jsonOptions = json_encode($options, \JSON_THROW_ON_ERROR);
if ($context['envelopes_only'] ?? false) {
return $jsonOptions;
}
$nonce = $context['csp_script_nonce'] ?? null;
$mainScript = $response->getMainScript();
$replaceMe = self::FLASHER_REPLACE_ME;
$nonceAttribute = $nonce ? " nonce='{$nonce}'" : '';
$scriptTagWithNonce = $nonce ? "tag.setAttribute('nonce', '{$nonce}');" : '';
$livewireListener = $this->getLivewireListenerScript();
return $html.<<<JAVASCRIPT
<script type="text/javascript" class="flasher-js"{$nonceAttribute}>
(function(window, document) {
const merge = (first, second) => {
if (Array.isArray(first) && Array.isArray(second)) {
return [...first, ...second.filter(item => !first.includes(item))];
}
if (typeof first === 'object' && typeof second === 'object') {
for (const [key, value] of Object.entries(second)) {
first[key] = key in first ? { ...first[key], ...value } : value;
}
return first;
}
return undefined;
};
const mergeOptions = (...options) => {
const result = {};
options.forEach(option => {
Object.entries(option).forEach(([key, value]) => {
result[key] = key in result ? merge(result[key], value) : value;
});
});
return result;
};
const renderCallback = (options) => {
if(!window.flasher) {
throw new Error('Flasher is not loaded');
}
window.flasher.render(options);
};
const render = (options) => {
if (options instanceof Event) {
options = options.detail;
}
if (['interactive', 'complete'].includes(document.readyState)) {
renderCallback(options);
} else {
document.addEventListener('DOMContentLoaded', () => renderCallback(options));
}
};
const addScriptAndRender = (options) => {
const mainScript = '{$mainScript}';
if (window.flasher || !mainScript || document.querySelector('script[src="' + mainScript + '"]')) {
render(options);
} else {
const tag = document.createElement('script');
tag.src = mainScript;
tag.type = 'text/javascript';
{$scriptTagWithNonce}
tag.onload = () => render(options);
document.head.appendChild(tag);
}
};
const addRenderListener = () => {
if (1 === document.querySelectorAll('script.flasher-js').length) {
document.addEventListener('flasher:render', render);
}
{$livewireListener}
};
const options = [];
options.push({$jsonOptions});
{$replaceMe}
addScriptAndRender(mergeOptions(...options));
addRenderListener();
})(window, document);
</script>
JAVASCRIPT;
}
private function getLivewireListenerScript(): string
{
if (!class_exists(LivewireManager::class)) {
return '';
}
return <<<JAVASCRIPT
document.addEventListener('livewire:navigating', () => {
document.querySelectorAll('.fl-no-cache').forEach(el => el.remove());
});
JAVASCRIPT;
}
}