Files
cms-core/Classes/Mail/TemplatedEmailFactory.php

221 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Mail;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Fluid\View\TemplatePaths;
/**
* Factory for creating FluidEmail instances.
*
* Provides three creation methods:
* - create(): For backend/CLI usage with global configuration only
* - createFromRequest(): For frontend usage with site-aware template paths
* - createWithOverrides(): For extensions needing custom template path overrides
*/
#[Autoconfigure(public: true)]
readonly class TemplatedEmailFactory
{
/**
* Create a FluidEmail instance with template paths resolved from site settings.
*
* Use this method for frontend contexts (e.g., form submissions, felogin)
* where site-specific email templates should be applied.
*
* The factory extracts the site from the request attribute and merges
* site-specific email settings with global mail configuration.
*
* Site settings used:
* - email.templateRootPaths: array of template root paths
* - email.layoutRootPaths: array of layout root paths
* - email.partialRootPaths: array of partial root paths
* - email.format: email format (html, plain, both)
*/
public function createFromRequest(ServerRequestInterface $request): FluidEmail
{
return $this->createWithOverrides([], [], [], $request);
}
/**
* Create a FluidEmail instance using global configuration only.
*
* Use this method for backend/CLI contexts (e.g., login notifications,
* scheduler tasks, install tool) where no site context is available
* or site-specific templates are not desired.
*
* Template paths are read from $GLOBALS['TYPO3_CONF_VARS']['MAIL'].
*/
public function create(?ServerRequestInterface $request = null): FluidEmail
{
$templatePaths = $this->buildTemplatePathsFromGlobals();
$fluidEmail = new FluidEmail($templatePaths);
if ($request !== null) {
$fluidEmail->setRequest($request);
}
return $fluidEmail;
}
/**
* Create a FluidEmail instance with custom template path overrides.
*
* Use this method when extensions need to provide their own template paths
* that are merged on top of the base configuration. The base configuration
* is built from global $GLOBALS['TYPO3_CONF_VARS']['MAIL'] with site settings
* merged on top when a request with a site attribute is provided.
*
* The override paths are then merged using array_replace(), so higher numeric
* keys in overrides will take precedence, while existing numeric keys will
* be overwritten.
*
* @param string[] $templateRootPaths Additional template root paths to merge
* @param string[] $layoutRootPaths Additional layout root paths to merge
* @param string[] $partialRootPaths Additional partial root paths to merge
* @param ServerRequestInterface|null $request Optional request for site resolution and ViewHelper context
*/
public function createWithOverrides(
array $templateRootPaths = [],
array $layoutRootPaths = [],
array $partialRootPaths = [],
?ServerRequestInterface $request = null,
): FluidEmail {
$site = $request?->getAttribute('site');
$templatePaths = $this->buildTemplatePathsWithSiteOverrides($site);
if ($templateRootPaths !== []) {
$templatePaths->setTemplateRootPaths(
array_replace($templatePaths->getTemplateRootPaths(), $templateRootPaths)
);
}
if ($layoutRootPaths !== []) {
$templatePaths->setLayoutRootPaths(
array_replace($templatePaths->getLayoutRootPaths(), $layoutRootPaths)
);
}
if ($partialRootPaths !== []) {
$templatePaths->setPartialRootPaths(
array_replace($templatePaths->getPartialRootPaths(), $partialRootPaths)
);
}
$fluidEmail = new FluidEmail($templatePaths);
if ($request !== null) {
$fluidEmail->setRequest($request);
}
if ($site instanceof Site) {
$format = $site->getSettings()->get('email.format', '');
if ($format !== '' && is_string($format)) {
$fluidEmail->format($format);
}
}
return $fluidEmail;
}
/**
* Build template paths from global config with site settings merged on top.
*
* Site settings take precedence and are merged using array_replace()
* to allow overriding specific numeric keys.
*/
private function buildTemplatePathsWithSiteOverrides(?object $site): TemplatePaths
{
$templatePaths = $this->buildTemplatePathsFromGlobals();
if ($site instanceof Site && !$site->getSettings()->isEmpty()) {
$settings = $site->getSettings();
$siteTemplateRootPaths = $settings->get('email.templateRootPaths', []);
if (is_array($siteTemplateRootPaths) && $siteTemplateRootPaths !== []) {
$templatePaths->setTemplateRootPaths(
$this->mergeYamlSiteSettingsArrayWithCurrent($templatePaths->getTemplateRootPaths(), $siteTemplateRootPaths)
);
}
$siteLayoutRootPaths = $settings->get('email.layoutRootPaths', []);
if (is_array($siteLayoutRootPaths) && $siteLayoutRootPaths !== []) {
$templatePaths->setLayoutRootPaths(
$this->mergeYamlSiteSettingsArrayWithCurrent($templatePaths->getLayoutRootPaths(), $siteLayoutRootPaths)
);
}
$sitePartialRootPaths = $settings->get('email.partialRootPaths', []);
if (is_array($sitePartialRootPaths) && $sitePartialRootPaths !== []) {
$templatePaths->setPartialRootPaths(
$this->mergeYamlSiteSettingsArrayWithCurrent($templatePaths->getPartialRootPaths(), $sitePartialRootPaths)
);
}
}
return $templatePaths;
}
/**
* When using the Site Settings GUI, the entered "stringlist" arrays have running numerical
* indexes:
*
* email.partialRootPaths:
* - 'EXT:my_extension/Resources/Private/Partials/Email'
* - 'EXT:my_extension/Resources/Private/Partials/Email2'
*
* This would resolve to an array with the keys "0" and "1". This would override the
* global template paths that already use "0" as the EXT:core base template paths.
*
* For a manually maintained settings.yaml, integrators however might use named indexes.
* This method here allows to deal with both:
*
* - if the input is a sequential list (PHP `is_array_list`), all array keys are APPENDED to the array
* - if the input has specific array keys (100, 200, ...) the array keys are REPLACED
*/
private function mergeYamlSiteSettingsArrayWithCurrent(array $currentArray, array $yamlArray): array
{
if (array_is_list($yamlArray)) {
// array_merge() would replace numerical array keys, which we do not want.
// Data must be stacked on top of the existing structure, with higher priority than globals.
$nextKey = max(array_keys($currentArray)) + 1;
$outputArray = $currentArray;
foreach ($yamlArray as $value) {
$outputArray[$nextKey++] = $value;
}
return $outputArray;
}
return array_replace($currentArray, $yamlArray);
}
/**
* Build template paths from global mail configuration.
*/
private function buildTemplatePathsFromGlobals(): TemplatePaths
{
$globalConfig = $GLOBALS['TYPO3_CONF_VARS']['MAIL'] ?? [];
$templatePaths = new TemplatePaths();
$templatePaths->setTemplateRootPaths($globalConfig['templateRootPaths'] ?? []);
$templatePaths->setLayoutRootPaths($globalConfig['layoutRootPaths'] ?? []);
$templatePaths->setPartialRootPaths($globalConfig['partialRootPaths'] ?? []);
return $templatePaths;
}
}