<?php
namespace Boldr\Cms\CmsBundle\Twig;
use Boldr\Cms\CmsBundle\SiteConfiguration;
use Boldr\Cms\CmsBundle\Upload\UploadStorageManager;
use Boldr\Cms\CmsBundle\Presenter\Menu\{ MenuPresenter, MenuView };
use Boldr\Cms\CmsBundle\Entity\{ Upload, Menu };
use Boldr\Cms\CmsBundle\Controller\PermalinkController;
use Boldr\Cms\CmsBundle\Presenter\Attachment\AttachmentPresenterInterface;
use Boldr\Cms\CmsBundle\Search\SearchConfigurationPermalinkable;
use Boldr\Cms\CmsBundle\Link\{ LinkInterface };
use Boldr\Cms\CmsBundle\Permalink\PermalinkableInterface;
use Boldr\Cms\CmsBundle\Content\{ ContentManager, Assets };
use Boldr\Cms\CmsBundle\Renderer\{ HtmlRenderer };
use Boldr\Cms\CmsBundle\Cookies\CookieManager;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Twig\{ TwigFunction, Environment };
use Twig\Extension\AbstractExtension;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Container\ContainerInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\{ RequestFactoryInterface, UriFactoryInterface };
use Embed\Embed;
use Embed\Http\Crawler;
use Exception;
use MatthiasMullie\Minify;
class TwigExtension extends AbstractExtension
{
public function __construct(
private readonly ParameterBagInterface $parameterBag,
private readonly EntityManagerInterface $em,
private readonly RequestStack $requestStack,
private readonly MenuPresenter $menuPresenter,
private readonly PermalinkController $permalinkController,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly TagAwareCacheInterface $cache,
private readonly UploadStorageManager $uploadStorageManager,
private readonly SiteConfiguration $siteConfiguration,
private readonly AttachmentPresenterInterface $attachmentPresenter,
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
private readonly UriFactoryInterface $uriFactory,
private readonly HtmlRenderer $htmlRenderer,
private readonly ContentManager $contentManager,
private readonly CookieManager $cookieManager,
/** @var iterable<AssetProviderInterface> */
private readonly iterable $assetProviders,
private readonly ContainerInterface $linkGenerators,
) {}
public function getFunctions()
{
$functions = [];
$functions[] = new TwigFunction('boldr_cms_content_render', [$this, 'renderContent'], ['is_safe' => ['html'], 'needs_context' => true]);
$functions[] = new TwigFunction('boldr_cms_get_assets', [$this, 'getAssets'], ['needs_context' => true]);
$functions[] = new TwigFunction('boldr_cms_render_css', [$this, 'renderCss'], ['is_safe' => ['html']]);
$functions[] = new TwigFunction('boldr_cms_render_js', [$this, 'renderJs'], ['is_safe' => ['html']]);
$functions[] = new TwigFunction('boldr_cms_get_available_locales', [$this, 'getAvailableLocales']);
$functions[] = new TwigFunction('boldr_cms_get_enabled_locales', [$this, 'getEnabledLocales']);
$functions[] = new TwigFunction('boldr_cms_get_cookie_state', [$this, 'getCookieState']);
$functions[] = new TwigFunction('boldr_cms_get_site_name', [$this, 'getSiteName']);
$functions[] = new TwigFunction('boldr_cms_get_menus_at_location', [$this, 'getMenusAtLocation']);
$functions[] = new TwigFunction('boldr_cms_get_search_url', [$this, 'getSearchUrl']);
$functions[] = new TwigFunction('boldr_cms_get_permalink', [$this, 'getPermalink']);
$functions[] = new TwigFunction('boldr_cms_get_translation_url', [$this, 'getTranslationUrl']);
$functions[] = new TwigFunction('boldr_cms_get_attachment_url', [$this, 'getUploadUrl']);
$functions[] = new TwigFunction('boldr_cms_render_attachment', [$this, 'renderAttachment']);
$functions[] = new TwigFunction('boldr_cms_get_heading_style_class', [$this, 'getHeadingStyleClass']);
$functions[] = new TwigFunction('boldr_cms_get_default_heading_style', [$this, 'getDefaultHeadingStyle']);
$functions[] = new TwigFunction('boldr_cms_get_button_style_class', [$this, 'getButtonStyleClass']);
$functions[] = new TwigFunction('boldr_cms_get_primary_button_style', [$this, 'getPrimaryButtonStyle']);
$functions[] = new TwigFunction('boldr_cms_get_secondary_button_style', [$this, 'getSecondaryButtonStyle']);
$functions[] = new TwigFunction('boldr_cms_get_section_style_class', [$this, 'getSectionStyleClass']);
$functions[] = new TwigFunction('boldr_cms_get_defaut_section_style', [$this, 'getDefaultSectionStyle']);
$functions[] = new TwigFunction('boldr_cms_get_default_section_style', [$this, 'getDefaultSectionStyle']);
$functions[] = new TwigFunction('boldr_cms_generate_link', [$this, 'generateLink']);
$functions[] = new TwigFunction('boldr_cms_get_oembed_code', [$this, 'getOembedCode'], ['is_safe' => ['html']]);
$functions[] = new TwigFunction('boldr_cms_serialize_view', [$this, 'serializeView'], ['is_safe' => ['js', 'html']]);
$functions[] = new TwigFunction('boldr_cms_render', [$this, 'render'], ['needs_context' => true, 'is_safe' => ['html']]);
if (!class_exists('Symfony\\WebpackEncoreBundle\\Twig\\EntryFilesTwigExtension'))
{
$functions[] = new TwigFunction('encore_entry_link_tags', [$this, 'encoreNotInstalled'], ['is_safe' => ['html'], 'needs_environment' => true]);
$functions[] = new TwigFunction('encore_entry_script_tags', [$this, 'encoreNotInstalled'], ['is_safe' => ['html'], 'needs_environment' => true]);
}
return $functions;
}
public function getCookieState(): array
{
return [
'marketing' => $this->cookieManager->getAcceptsMarketingCookies(),
'analysis' => $this->cookieManager->getAcceptsAnalysisCookies(),
'preference' => $this->cookieManager->getAcceptsPreferenceCookies(),
];
}
public function render(array &$context, $renderable): string
{
$render = $this->htmlRenderer->render($renderable);
if ($render->getAssets() !== null)
{
if (isset($context['assets']))
{
$context['assets']->addAll($render->getAssets());
}
else
{
$context['assets'] = $render->getAssets();
}
}
return $render->getHtml();
}
public function serializeView($view): string
{
$serializer = new Serializer([new DynamicPropertyNormalizer], [new JsonEncoder]);
return $serializer->serialize($view, 'json');
}
public function getOembedCode(string $url): string
{
return $this->cache->get('oembed.'. sha1($url), function() use ($url) {
try
{
$embed = new Embed(new Crawler($this->httpClient, $this->requestFactory, $this->uriFactory));
$info = $embed->get($url);
$code = $info->code;
return $code ? $code->html : '[could not embed]';
}
catch (Exception $ex)
{
return '[could not embed]';
}
});
}
public function getDefaultSectionStyle(): string
{
return $this->parameterBag->get('boldr_cms.default_section_style');
}
public function getSectionStyleClass(string $sectionStyleName): string
{
/** @var array */
$sectionStyles = $this->parameterBag->get('boldr_cms.section_styles');
return $sectionStyles[$sectionStyleName]['css_class'];
}
public function renderCss(iterable $files)
{
$files = is_array($files) ? $files : iterator_to_array($files);
if (count($files) === 0)
{
return '';
}
if ($this->parameterBag->get('boldr_cms.minify_assets') && !$this->parameterBag->get('kernel.debug')) {
$publicPath = $this->parameterBag->get('kernel.project_dir').'/public';
$output = '';
$filesToMinify = [];
foreach ($files as $file)
{
if (strpos($file, ':') !== false || !file_exists($publicPath .'/'. $file))
{
$output .= '<link rel="stylesheet" href="'.$file.'" />'.PHP_EOL;
}
else
{
$filesToMinify[] = $file;
}
}
if (count($filesToMinify))
{
$hash = sha1(implode(',', $filesToMinify));
$dirPath = $publicPath.'/assets.min';
$minifiedPath = $dirPath.'/'.$hash.'.css';
$minifiedUrl = '/assets.min/'.$hash.'.css';
if (!file_exists($minifiedPath)) {
if (!file_exists($dirPath)) {
mkdir($dirPath);
}
$minify = new Minify\CSS;
foreach ($filesToMinify as $file) {
$minify->add($publicPath.$file);
}
$minify->minify($minifiedPath);
}
$output .= '<link rel="stylesheet" href="'.$minifiedUrl.'?v='.filemtime($minifiedPath).'" />'.PHP_EOL;
}
return $output;
}
else
{
$output = '';
foreach ($files as $file) {
$output .= '<link rel="stylesheet" href="'.$file.'" />'.PHP_EOL;
}
return $output;
}
}
public function renderJs(iterable $files)
{
$files = is_array($files) ? $files : iterator_to_array($files);
if (count($files) === 0)
{
return '';
}
if ($this->parameterBag->get('boldr_cms.minify_assets') && !$this->parameterBag->get('kernel.debug')) {
$output = '';
$filesToMinify = [];
$publicPath = $this->parameterBag->get('kernel.project_dir').'/public';
foreach ($files as $file)
{
if (strpos($file, ':') !== false || !file_exists($publicPath .'/'. $file))
{
$output .= '<script type="application/javascript" src="'.$file.'"></script>'.PHP_EOL;
}
else
{
$filesToMinify[] = $file;
}
}
if (count($filesToMinify))
{
$hash = sha1(implode(',', $filesToMinify));
$dirPath = $publicPath.'/assets.min';
$minifiedPath = $dirPath.'/'.$hash.'.js';
$minifiedUrl = '/assets.min/'.$hash.'.js';
if (!file_exists($minifiedPath)) {
if (!file_exists($dirPath)) {
mkdir($dirPath);
}
$minify = new Minify\JS;
foreach ($filesToMinify as $file) {
$minify->add($publicPath.$file);
}
$minify->minify($minifiedPath);
}
$output .= '<script type="application/javascript" src="'.$minifiedUrl.'?v='.filemtime($minifiedPath).'"></script>'.PHP_EOL;
}
return $output;
}
else
{
$output = '';
foreach ($files as $file) {
$output .= '<script type="application/javascript" src="'.$file.'"></script>'.PHP_EOL;
}
return $output;
}
}
public function renderAttachment($upload, array $sizes = ['small', 'medium', 'large'])
{
$upload = is_int($upload) ? $this->em->getReference(Upload::class, $upload) : $upload;
return $this->attachmentPresenter->createAttachmentView($upload, $sizes, $this->getCurrentLocale());
}
public function getPermalink(PermalinkableInterface $permalinkable, ?string $locale = null)
{
return $this->permalinkController->generate($permalinkable, $locale ?? $this->requestStack->getCurrentRequest()->getLocale());
}
public function generateLink(?LinkInterface $link)
{
if ($link === null)
return '';
return $this->linkGenerators->get(get_class($link))->generateLinkUrl($link);
}
public function getHeadingStyleClass(string $headingStyle)
{
/** @var array */
$styles = $this->parameterBag->get('boldr_cms.heading_styles');
$style = $styles[$headingStyle] ?? $styles['default'] ?? [];
return $style['css_class'] ?? '';
}
public function getDefaultHeadingStyle(): string
{
return $this->parameterBag->get('boldr_cms.default_heading_style');
}
public function getPrimaryButtonStyle(): string
{
return $this->parameterBag->get('boldr_cms.primary_button_style');
}
public function getSecondaryButtonStyle(): string
{
return $this->parameterBag->get('boldr_cms.secondary_button_style');
}
public function getButtonStyleClass(string $buttonStyle)
{
/** @var array */
$styles = $this->parameterBag->get('boldr_cms.button_styles');
$style = $styles[$buttonStyle] ?? $styles[$this->getPrimaryButtonStyle()] ?? null;
if ($style === null)
{
return '';
}
return $style['css_class'] ?? '';
}
public function getSearchUrl(string $searchConfigurationName)
{
/** @var array */
$searchConfigurations = $this->parameterBag->get('boldr_cms.search_configurations');
$searchConfiguration = $searchConfigurations[$searchConfigurationName] ?? null;
if ($searchConfiguration === null)
{
throw new \Exception('No search configuration with id "'. $searchConfigurationName .'" exists. Verify your search configuration in config/packages/boldr_cms.yaml');
}
return $this->permalinkController->generate(new SearchConfigurationPermalinkable($searchConfigurationName), $this->getCurrentLocale());
}
public function getAssets(array $context): Assets
{
$assets = $context['assets'] ?? new Assets;
foreach ($this->assetProviders as $assetProvider)
{
$assetProvider->addAssets($assets);
}
return $assets;
}
public function getUploadUrl($upload, ?string $size = null): string
{
if (is_int($upload))
{
$upload = $this->em->getRepository(Upload::class)->find($upload);
}
$request = $this->requestStack->getCurrentRequest();
$baseUrl = $request->getSchemeAndHttpHost().$request->getBasePath();
return $this->uploadStorageManager->getImageUrl($upload, $baseUrl, $size);
}
/**
* @return string[]
*/
public function getAvailableLocales(): array
{
return $this->siteConfiguration->getAvailableLocales();
}
/**
* @return string[]
*/
public function getEnabledLocales(): array
{
return $this->siteConfiguration->getEnabledLocales();
}
public function getTranslationUrl(string $locale): ?string
{
$request = $this->requestStack->getCurrentRequest();
/** @var array<string,string> */
$localeBaseUrls = $this->parameterBag->get('boldr_cms.locale_base_urls');
$localeBaseUrl = $localeBaseUrls[$locale] ?? '';
if (is_array($localeBaseUrl))
{
$localeBaseUrl = $localeBaseUrl[0];
}
if ($request->attributes->has('_permalinkable'))
{
return $localeBaseUrl . $this->permalinkController->generate($request->attributes->get('_permalinkable'), $locale);
}
$url = !$request->attributes->has('_route') ? '' : $localeBaseUrl . $this->urlGenerator->generate(
$request->attributes->get('_route'),
array_merge(
$request->attributes->get('_route_params') ?: [],
$request->query->all(),
['_locale' => $locale]
)
);
if (strpos($url, '?_locale=') === false && strpos($url, '&_locale=') === false)
{
return $url;
}
return null;
}
private function getCurrentLocale(): string
{
$request = $this->requestStack->getCurrentRequest();
$locale = $request === null ? $this->siteConfiguration->getDefaultLocale() : $request->getLocale();
return $locale;
}
public function getSiteName(): string {
$locale = $this->getCurrentLocale();
return $this->siteConfiguration->getSiteName($locale);
}
/**
* @return MenuView[]
*/
public function getMenusAtLocation(string $locationId): array
{
$locale = $this->requestStack->getCurrentRequest()->getLocale();
return $this->cache->get('menus_at_location.'.$locationId.'.'.$locale, function($cacheItem) use ($locationId, $locale) {
$cacheItem->tag('boldr_cms.menus_at_location');
$repo = $this->em->getRepository(Menu::class);
/** @phpstan-ignore-next-line */
$menus = $repo->createQueryBuilder('m')
->leftJoin('m.locations', 'l')
->where('l.location = ?1')
->setParameter(1, $locationId)
->getQuery()
->getResult();
$menuViews = [];
foreach ($menus as $menu)
{
$menuViews[] = $this->menuPresenter->createMenuView($menu, $locale);
}
return $menuViews;
});
}
public function encoreNotInstalled(Environment $twig)
{
if ($twig->isDebug())
return '<!-- Webpack Encore is not installed -->';
return '';
}
public function renderContent($context, array $contentElements): string
{
$assets = $context['assets'];
return $this->contentManager->renderAsHtml($contentElements, $assets);
}
}