Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/preload singles #12787

Merged
merged 5 commits into from
Mar 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@

### Development
- Added the “Letterbox” (`letterbox`) image transform mode. ([#8848](https://github.com/craftcms/cms/discussions/8848), [#12214](https://github.com/craftcms/cms/pull/12214))
- Added the `preloadSingles` config setting, which causes front-end Twig templates to automatically preload Single section entries which are referenced in the template. ([#12698](https://github.com/craftcms/cms/pull/12787))
- Control panel-defined image transforms now have an “Allow Upscaling” setting, which will initially be set to the `upscaleImages` config setting for existing transforms. ([#12214](https://github.com/craftcms/cms/pull/12214))
- Template-defined image transforms can now have an `upscale` setting. The `upscaleImages` config setting will be used by default if not set. ([#12214](https://github.com/craftcms/cms/pull/12214))
- Added the `exec` command, which executes an individual PHP statement and outputs the result. ([#12528](https://github.com/craftcms/cms/pull/12528))
Expand Down Expand Up @@ -119,6 +120,9 @@
- Added `craft\helpers\ImageTransforms::generateTransform()`.
- Added `craft\helpers\ImageTransforms::parseTransformString()`.
- Added `craft\helpers\StringHelper::toHandle()`.
- Added `craft\helpers\Template::fallback()`.
- Added `craft\helpers\Template::fallbackExists()`.
- Added `craft\helpers\Template::preloadSingles()`.
- Added `craft\image\Raster::scaleToFitAndFill()`.
- Added `craft\image\Raster::setFill()`.
- Added `craft\imagetransforms\FallbackTransformer`.
Expand All @@ -136,6 +140,7 @@
- Added `craft\services\Elements::deleteElementsForSite()`.
- Added `craft\services\Elements::EVENT_AFTER_DELETE_FOR_SITE`. ([#12354](https://github.com/craftcms/cms/issues/12354))
- Added `craft\services\Elements::EVENT_BEFORE_DELETE_FOR_SITE`. ([#12354](https://github.com/craftcms/cms/issues/12354))
- Added `craft\services\Entries::getSingleEntriesByHandle()`. ([#12698](https://github.com/craftcms/cms/pull/12787))
- Added `craft\services\Fields::getFieldsByType()`. ([#12381](https://github.com/craftcms/cms/discussions/12381))
- Added `craft\services\Path::getImageTransformsPath()`.
- Added `craft\services\Search::normalizeSearchQuery()`.
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@

## Unreleased (4.4)

- Added the `preloadSingles` config setting, which causes front-end Twig templates to automatically preload Single section entries which are referenced in the template. ([#12698](https://github.com/craftcms/cms/pull/12787))
- Improved some element selector modals for screen readers. ([#12783](https://github.com/craftcms/cms/pull/12783))
- Added `craft\helpers\Template::fallback()`.
- Added `craft\helpers\Template::fallbackExists()`.
- Added `craft\helpers\Template::preloadSingles()`.
- Added `craft\services\Entries::getSingleEntriesByHandle()`. ([#12698](https://github.com/craftcms/cms/pull/12787))
- Fixed a bug where search icons on element indexes weren’t hidden from screen readers. ([#12785](https://github.com/craftcms/cms/pull/12785))
- Fixed a bug where Categories and Tags fields weren’t getting properly converted to Entries fields via the `entrify/categories` and `entrify/tags` commands.

Expand Down
55 changes: 55 additions & 0 deletions src/config/GeneralConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -2000,6 +2000,30 @@ class GeneralConfig extends BaseConfig
*/
public bool $prefixGqlRootTypes = true;

/**
* @var bool Whether Single section entries should be preloaded for Twig templates.
*
* When enabled, Craft will make an educated guess on which Singles should be preloaded for each template based on
* the variable names that are referenced.
*
* ::: warning
* You will need to clear your compiled templates from the Caches utility before this setting will take effect.
* :::
*
* ::: code
* ```php Static Config
* ->preloadSingles()
* ```
* ```shell Environment Override
* CRAFT_PRELOAD_SINGLES=true
* ```
* :::
*
* @group System
* @since 4.4.0
*/
public bool $preloadSingles = false;

/**
* @var bool Whether CMYK should be preserved as the colorspace when manipulating images.
*
Expand Down Expand Up @@ -5214,6 +5238,37 @@ public function prefixGqlRootTypes(bool $value = true): self
return $this;
}

/**
* Whether Single section entries should be preloaded for Twig templates.
*
* When enabled, Craft will make an educated guess on which Singles should be preloaded for each template based on
* the variable names that are referenced.
*
* ::: warning
* You will need to clear your compiled templates from the Caches utility before this setting will take effect.
* :::
*
* ::: code
* ```php Static Config
* ->preloadSingles()
* ```
* ```shell Environment Override
* CRAFT_PRELOAD_SINGLES=true
* ```
* :::
*
* @group System
* @param bool $value
* @return self
* @see $preloadSingles
* @since 4.4.0
*/
public function preloadSingles(bool$value = true): self
{
$this->preloadSingles = $value;
return $this;
}

/**
* Whether CMYK should be preserved as the colorspace when manipulating images.
*
Expand Down
46 changes: 46 additions & 0 deletions src/helpers/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use yii\base\BaseObject;
use yii\base\InvalidConfigException;
use yii\base\UnknownMethodException;
use yii\base\UnknownPropertyException;
use yii\db\Query;
use yii\db\QueryInterface;
use function twig_get_attribute;
Expand Down Expand Up @@ -54,6 +55,40 @@ class Template
*/
private static array $_profileCounters;

/**
* @var array Dynamically-defined fallback variables
* @see fallbackExists()
* @see fallback()
*/
private static array $_fallbacks = [];

/**
* Returns whether a fallback variable has been defined.
*
* @param string $name
* @return bool
* @since 4.4.0
*/
public static function fallbackExists(string $name): bool
{
return isset(self::$_fallbacks[$name]);
}

/**
* Provides dynamically-defined fallback variable’s value.
*
* @param string $name
* @throws UnknownPropertyException if `$name` isn’t defined as a fallback variable.
* @since 4.4.0
*/
public static function fallback(string $name): mixed
{
if (!static::fallbackExists($name)) {
throw new UnknownPropertyException("$name is not defined as a fallback template variable.");
}
return self::$_fallbacks[$name];
}

/**
* Returns the attribute value for a given array/object.
*
Expand Down Expand Up @@ -339,4 +374,15 @@ public static function contextWithoutTemplate(array $context): array
// Template check copied from twig_var_dump()
return array_filter($context, fn($value) => !$value instanceof TwigTemplate && !$value instanceof TemplateWrapper);
}

/**
* Preloads Single section entries as fallback values for [[fallbackValue()]]
*
* @param string[] $handles
* @since 4.4.0
*/
public static function preloadSingles(array $handles): void
{
self::$_fallbacks += Craft::$app->getEntries()->getSingleEntriesByHandle($handles);
}
}
71 changes: 71 additions & 0 deletions src/services/Entries.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use craft\db\Query;
use craft\db\Table;
use craft\elements\Entry;
use craft\helpers\ArrayHelper;
use craft\models\Section;
use yii\base\Component;

/**
Expand All @@ -23,6 +25,11 @@
*/
class Entries extends Component
{
/**
* @var array<int,array<string,Entry|false>>
*/
private array $_singleEntries = [];

/**
* Returns an entry by its ID.
*
Expand Down Expand Up @@ -54,4 +61,68 @@ public function getEntryById(int $entryId, array|int|string $siteId = null, arra

return Craft::$app->getElements()->getElementById($entryId, Entry::class, $siteId, $criteria);
}

/**
* Returns an array of Single section entries which match a given list of section handles.
*
* @param string[] $handles
* @return array<string,Entry>
* @since 4.4.0
*/
public function getSingleEntriesByHandle(array $handles): array
{
$entries = [];
$siteId = Craft::$app->getSites()->getCurrentSite()->id;
$missingEntries = [];

if (!isset($this->_singleEntries[$siteId])) {
$this->_singleEntries[$siteId] = [];
}

foreach ($handles as $handle) {
if (isset($this->_singleEntries[$siteId][$handle])) {
if ($this->_singleEntries[$siteId][$handle] !== false) {
$entries[$handle] = $this->_singleEntries[$siteId][$handle];
}
} else {
$missingEntries[] = $handle;
}
}

if (!empty($missingEntries)) {
/** @var array<string,Section> $singleSections */
$singleSections = ArrayHelper::index(
Craft::$app->getSections()->getSectionsByType(Section::TYPE_SINGLE),
fn(Section $section) => $section->handle,
);
$fetchSectionIds = [];
$fetchSectionHandles = [];
foreach ($missingEntries as $handle) {
if (isset($singleSections[$handle])) {
$fetchSectionIds[] = $singleSections[$handle]->id;
$fetchSectionHandles[] = $handle;
} else {
$this->_singleEntries[$siteId][$handle] = false;
}
}
if (!empty($fetchSectionIds)) {
$fetchedEntries = Entry::find()
->sectionId($fetchSectionIds)
->siteId($siteId)
->all();
/** @var array<string,Entry> $fetchedEntries */
$fetchedEntries = ArrayHelper::index($fetchedEntries, fn(Entry $entry) => $entry->getSection()->handle);
foreach ($fetchSectionHandles as $handle) {
if (isset($fetchedEntries[$handle])) {
$this->_singleEntries[$siteId][$handle] = $fetchedEntries[$handle];
$entries[$handle] = $fetchedEntries[$handle];
} else {
$this->_singleEntries[$siteId][$handle] = false;
}
}
}
}

return $entries;
}
}
5 changes: 5 additions & 0 deletions src/web/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use craft\web\twig\Environment;
use craft\web\twig\Extension;
use craft\web\twig\GlobalsExtension;
use craft\web\twig\SinglePreloaderExtension;
use craft\web\twig\TemplateLoader;
use Throwable;
use Twig\Error\LoaderError as TwigLoaderError;
Expand Down Expand Up @@ -354,6 +355,10 @@ public function createTwig(): Environment
$twig->addExtension(new CpExtension());
} elseif (Craft::$app->getIsInstalled()) {
$twig->addExtension(new GlobalsExtension());

if (Craft::$app->getConfig()->getGeneral()->preloadSingles) {
$twig->addExtension(new SinglePreloaderExtension());
}
}

// Add plugin-supplied extensions
Expand Down
30 changes: 30 additions & 0 deletions src/web/twig/SinglePreloaderExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\web\twig;

use craft\web\twig\nodevisitors\SinglePreloader;
use Twig\Extension\AbstractExtension;

/**
* Single preloader Twig extension
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 4.4.0
*/
class SinglePreloaderExtension extends AbstractExtension
{
/**
* @inheritdoc
*/
public function getNodeVisitors(): array
{
return [
new SinglePreloader(),
];
}
}
86 changes: 86 additions & 0 deletions src/web/twig/nodes/FallbackNameExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\web\twig\nodes;

use craft\helpers\Template;
use Twig\Compiler;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Node;

/**
* Class NamespaceNode
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 4.4.0
*/
class FallbackNameExpression extends NameExpression
{
public function __construct(string $name, array $attributes = [], int $lineno = 0)
{
$attributes += [
'name' => $name,
'is_defined_test' => false,
'ignore_strict_check' => false,
'always_defined' => false,
];
Node::__construct([], $attributes, $lineno);
}

public function compile(Compiler $compiler): void
{
// no special handling for _self/etc.,or always-defined variables
if ($this->isSpecial() || $this->getAttribute('always_defined')) {
parent::compile($compiler);
return;
}

$name = $this->getAttribute('name');

$compiler->addDebugInfo($this);

if ($this->getAttribute('is_defined_test')) {
$compiler
->raw('(array_key_exists(')
->string($name)
->raw(sprintf(', $context) || %s::fallbackExists(', Template::class))
->string($name)
->raw('))');
} elseif ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) {
$compiler
->raw('(isset($context[')
->string($name)
->raw(']) || array_key_exists(')
->string($name)
->raw(', $context) ? $context[')
->string($name)
->raw(sprintf('] : (%s::fallbackExists(', Template::class))
->string($name)
->raw(sprintf(') ? %s::fallback(', Template::class))
->string($name)
->raw(') : null)');
} else {
$compiler
->raw('(isset($context[')
->string($name)
->raw(']) || array_key_exists(')
->string($name)
->raw(', $context) ? $context[')
->string($name)
->raw(sprintf('] : (%s::fallbackExists(', Template::class))
->string($name)
->raw(sprintf(') ? %s::fallback(', Template::class))
->string($name)
->raw(') : (function () { throw new RuntimeError(\'Variable ')
->string($name)
->raw(' does not exist.\', ')
->repr($this->lineno)
->raw(', $this->source); })()')
->raw('))');
}
}
}
Loading