diff --git a/src/base/Element.php b/src/base/Element.php index e9c692a3961..fe9c85b54bd 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -6001,4 +6001,20 @@ private function _getRelativeElement(mixed $criteria, int $dir): ?ElementInterfa ->id($elementIds[$key + $dir]) ->one(); } + + /** + * Renders the element using its partial template. + * + * If no partial template exists for the element, its string representation will be output instead. + * + * @return Markup + * @throws InvalidConfigException + * @throws NotSupportedException + * @see ElementHelper::renderElements() + * @since 5.0.0 + */ + public function render(): Markup + { + return ElementHelper::renderElements([$this]); + } } diff --git a/src/config/GeneralConfig.php b/src/config/GeneralConfig.php index 4b41eaf653d..e3bab966e10 100644 --- a/src/config/GeneralConfig.php +++ b/src/config/GeneralConfig.php @@ -1911,6 +1911,36 @@ class GeneralConfig extends BaseConfig */ public string $pageTrigger = 'p'; + /** + * @var string The path within the `templates` folder where element partial templates will live. + * + * Partial templates are used to render elements when calling [[\craft\elements\db\ElementQuery::render()]], + * [[\craft\elements\ElementCollection::render()]], or [[\craft\base\Element::render()]]. + * + * For example, you could render all the entries within a Matrix field like so: + * + * ```twig + * {{ entry.myMatrixField.render() }} + * ``` + * + * The full path to a partial template will also include the element type handle (e.g. `asset` or `entry`) and the + * field layout provider’s handle (e.g. the volume handle or entry type handle). For an entry of type `article`, + * that would be: `_partials/entry/article.twig`. + * + * ::: code + * ```php Static Config + * ->partialTemplatesPath('_cp/partials') + * ``` + * ```shell Environment Override + * CRAFT_PARTIAL_TEMPLATES_PATH=_cp/partials + * ``` + * ::: + * + * @group System + * @since 5.0.0 + */ + public string $partialTemplatesPath = '_partials'; + /** * @var string|null The query string param that Craft will check when determining the request’s path. * @@ -5255,6 +5285,43 @@ public function pageTrigger(string $value): self return $this; } + /** + * The path within the `templates` folder where element partial templates will live. + * + * Partial templates are used to render elements when calling [[\craft\elements\db\ElementQuery::render()]], + * [[\craft\elements\ElementCollection::render()]], or [[\craft\base\Element::render()]]. + * + * For example, you could render all the entries within a Matrix field like so: + * + * ```twig + * {{ entry.myMatrixField.render() }} + * ``` + * + * The full path to a partial template will also include the element type handle (e.g. `asset` or `entry`) and the + * field layout provider’s handle (e.g. the volume handle or entry type handle). For an entry of type `article`, + * that would be: `_partials/entry/article.twig`. + * + * ::: code + * ```php Static Config + * ->partialTemplatesPath('_cp/partials') + * ``` + * ```shell Environment Override + * CRAFT_PARTIAL_TEMPLATES_PATH=_cp/partials + * ``` + * ::: + * + * @group System + * @param string $value + * @return self + * @see $partialTemplatesPath + * @since 5.0.0 + */ + public function partialTemplatesPath(string $value): self + { + $this->partialTemplatesPath = $value; + return $this; + } + /** * The query string param that Craft will check when determining the request’s path. * diff --git a/src/elements/ElementCollection.php b/src/elements/ElementCollection.php index e70d7e747b9..3d5589a8082 100644 --- a/src/elements/ElementCollection.php +++ b/src/elements/ElementCollection.php @@ -9,7 +9,12 @@ use Craft; use craft\base\ElementInterface; +use craft\helpers\ElementHelper; +use craft\helpers\Html; use Illuminate\Support\Collection; +use Twig\Markup; +use yii\base\InvalidConfigException; +use yii\base\NotSupportedException; /** * ElementCollection represents a collection of elements. @@ -65,4 +70,20 @@ public function with(array|string $with): static } return $this; } + + /** + * Renders the elements using their partial templates. + * + * If no partial template exists for an element, its string representation will be output instead. + * + * @return Markup + * @throws InvalidConfigException + * @throws NotSupportedException + * @see ElementHelper::renderElements() + * @since 5.0.0 + */ + public function render(): Markup + { + return ElementHelper::renderElements($this->items); + } } diff --git a/src/elements/db/ElementQuery.php b/src/elements/db/ElementQuery.php index 9b65c6e215a..1a1246e6a21 100644 --- a/src/elements/db/ElementQuery.php +++ b/src/elements/db/ElementQuery.php @@ -40,9 +40,11 @@ use ReflectionClass; use ReflectionException; use ReflectionProperty; +use Twig\Markup; use yii\base\ArrayableTrait; use yii\base\Exception; use yii\base\InvalidArgumentException; +use yii\base\InvalidConfigException; use yii\base\InvalidValueException; use yii\base\NotSupportedException; use yii\db\Connection as YiiConnection; @@ -1824,6 +1826,22 @@ public function ids(?YiiConnection $db = null): array return $result; } + /** + * Executes the query and renders the resulting elements using their partial templates. + * + * If no partial template exists for an element, its string representation will be output instead. + * + * @return Markup + * @throws InvalidConfigException + * @throws NotSupportedException + * @see ElementHelper::renderElements() + * @since 5.0.0 + */ + public function render(): Markup + { + return ElementHelper::renderElements($this->all()); + } + /** * Returns the resulting elements set by [[setCachedResult()]], if the criteria params haven’t changed since then. * diff --git a/src/helpers/ElementHelper.php b/src/helpers/ElementHelper.php index 485055ec71a..b992a338345 100644 --- a/src/helpers/ElementHelper.php +++ b/src/helpers/ElementHelper.php @@ -19,9 +19,14 @@ use craft\fieldlayoutelements\CustomField; use craft\i18n\Locale; use craft\services\ElementSources; +use craft\web\View; use DateTime; use Throwable; +use Twig\Error\LoaderError as TwigLoaderError; +use Twig\Markup; use yii\base\Exception; +use yii\base\InvalidConfigException; +use yii\base\NotSupportedException; /** * Class ElementHelper @@ -870,4 +875,42 @@ public static function actionConfig(ElementActionInterface $action): array 'settings' => $action->getSettings() ?: null, ]; } + + /** + * Renders the given elements using their partial templates. + * + * If no partial template exists for an element, its string representation will be output instead. + * + * @param ElementInterface[] $elements + * @return Markup + * @throws InvalidConfigException + * @throws NotSupportedException + * @since 5.0.0 + */ + public static function renderElements(array $elements): Markup + { + $view = Craft::$app->getView(); + $generalConfig = Craft::$app->getConfig()->getGeneral(); + $output = []; + + foreach ($elements as $element) { + $refHandle = $element::refHandle(); + if ($refHandle === null) { + throw new NotSupportedException(sprintf('Element type “%s” doesn’t define a reference handle, so it doesn’t support partial templates.', $element::displayName())); + } + $providerHandle = $element->getFieldLayout()?->provider->getHandle(); + if ($providerHandle === null) { + throw new InvalidConfigException(sprintf('Element “%s” doesn’t have a field layout provider that defines a handle, so it can’t be rendered with a partial template.', $element)); + } + $template = sprintf('%s/%s/%s', $generalConfig->partialTemplatesPath, $refHandle, $providerHandle); + try { + $output[] = $view->renderTemplate($template, [$refHandle => $element], View::TEMPLATE_MODE_SITE); + } catch (TwigLoaderError) { + // fallback to the string representation of the element + $output[] = Html::tag('p', Html::encode((string)$element)); + } + } + + return new Markup(implode("\n", $output), Craft::$app->charset); + } }