Skip to content
Draft

5.12 #19557

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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- 5.x
- '5.12'
- '*-internal'
pull_request:
permissions:
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG-5.12.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Release Notes for Craft CMS 5.12 (WIP)

### Development

- The `capitalize`, `lower`, `title`, and `upper` Twig filters now have `language` arguments, which default to the current application language. ([#19558](https://github.com/craftcms/cms/pull/19558))

### Extensibility

- Added `craft\i18n\Locale::languageId()`.
- Added `craft\elements\db\NestedElementQueryTrait::mustHaveField()`.
- Added `craft\elements\db\NestedElementQueryTrait::mustHaveOwner()`.
- `craft\helpers\ElementHelper::normalizeSlug()` now has a `$language` argument, which defaults to the current application language. ([#19558](https://github.com/craftcms/cms/pull/19558))
- `craft\helpers\StringHelper::toLowerCase()`, `::toTitleCase()`, and `::toUpperCase()` now have `$language` arguments, which default to the current application language. ([#19558](https://github.com/craftcms/cms/pull/19558))

### System

- Fixed a bug where entry and address indexes weren’t showing any results if they had a “Field” condition rule set to “is empty”.
7 changes: 6 additions & 1 deletion src/elements/db/AddressQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@ protected function beforePrepare(): bool
$this->normalizeNestedElementParams();

// Only join the elements_owners table if fieldId is specified
if (!empty($this->fieldId)) {
if (isset($this->fieldId)) {
$this->applyNestedElementParams('addresses.fieldId', 'addresses.primaryOwnerId');
} elseif (isset($this->primaryOwnerId) || isset($this->ownerId)) {
// User addresses don't get rows in the elements_owners table
Expand Down Expand Up @@ -968,6 +968,11 @@ protected function beforePrepare(): bool
return true;
}

protected function mustHaveField(): bool
{
return false;
}

/**
* @inheritdoc
*/
Expand Down
10 changes: 10 additions & 0 deletions src/elements/db/EntryQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,16 @@ protected function beforePrepare(): bool
return true;
}

protected function mustHaveField(): bool
{
return false;
}

protected function mustHaveOwner(): bool
{
return false;
}

/**
* @inheritdoc
*/
Expand Down
70 changes: 58 additions & 12 deletions src/elements/db/NestedElementQueryTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,49 @@ public function allowOwnerRevisions(?bool $value = true): static
return $this;
}

/**
* Returns whether the resulting elements will always have a field assigned to them.
*
* @since 5.12.0
*/
protected function mustHaveField(): bool
{
return true;
}

/**
* Returns whether the resulting elements will always have an owner assigned to them.
*
* @since 5.12.0
*/
protected function mustHaveOwner(): bool
{
return true;
}

private function applyNestedElementParams(string $fieldIdColumn, string $primaryOwnerIdColumn): void
{
$this->normalizeNestedElementParams();

if ($this->fieldId === false || $this->primaryOwnerId === false || $this->ownerId === false) {
$mustHaveField = $this->mustHaveField();
$mustHaveOwner = $this->mustHaveOwner();

if (
($mustHaveField && $this->fieldId === false) ||
($mustHaveOwner && ($this->primaryOwnerId === false || $this->ownerId === false)) ||
$this->fieldId === [] ||
$this->primaryOwnerId === [] ||
$this->ownerId === []
) {
throw new QueryAbortedException();
}

if (!empty($this->fieldId) || !empty($this->ownerId) || !empty($this->primaryOwnerId)) {
if (isset($this->fieldId) || isset($this->ownerId) || isset($this->primaryOwnerId)) {
// Join in the elements_owners table
$joinType = $mustHaveField || $this->fieldId || $this->ownerId || $this->primaryOwnerId
? 'INNER JOIN'
: 'LEFT JOIN';

$ownersCondition = [
'and',
'[[elements_owners.elementId]] = [[elements.id]]',
Expand All @@ -218,23 +251,25 @@ private function applyNestedElementParams(string $fieldIdColumn, string $primary
'elements_owners.ownerId',
'elements_owners.sortOrder',
])
->innerJoin(['elements_owners' => Table::ELEMENTS_OWNERS], $ownersCondition);
$this->subQuery->innerJoin(['elements_owners' => Table::ELEMENTS_OWNERS], $ownersCondition);
->join($joinType, ['elements_owners' => Table::ELEMENTS_OWNERS], $ownersCondition);

$this->subQuery->join($joinType, ['elements_owners' => Table::ELEMENTS_OWNERS], $ownersCondition);

if ($this->fieldId) {
$this->subQuery->andWhere([$fieldIdColumn => $this->fieldId]);
if (isset($this->fieldId)) {
$this->subQuery->andWhere([$fieldIdColumn => $this->fieldId ?: null]);
}

if ($this->primaryOwnerId) {
$this->subQuery->andWhere([$primaryOwnerIdColumn => $this->primaryOwnerId]);
if (isset($this->primaryOwnerId)) {
$this->subQuery->andWhere([$primaryOwnerIdColumn => $this->primaryOwnerId ?: null]);
}

// Ignore revision/draft blocks by default
$allowOwnerDrafts = $this->allowOwnerDrafts ?? ($this->id || $this->primaryOwnerId || $this->ownerId);
$allowOwnerRevisions = $this->allowOwnerRevisions ?? ($this->id || $this->primaryOwnerId || $this->ownerId);

if (!$allowOwnerDrafts || !$allowOwnerRevisions) {
$this->subQuery->innerJoin(
$this->subQuery->join(
$joinType,
['owners' => Table::ELEMENTS],
$this->ownerId ? '[[owners.id]] = [[elements_owners.ownerId]]' : "[[owners.id]] = [[$primaryOwnerIdColumn]]"
);
Expand All @@ -246,6 +281,10 @@ private function applyNestedElementParams(string $fieldIdColumn, string $primary
if (!$allowOwnerRevisions) {
$this->subQuery->andWhere(['owners.revisionId' => null]);
}

if ($this->ownerId === false) {
$this->subQuery->andWhere(['owners.id' => null]);
}
}

$this->defaultOrderBy = ['elements_owners.sortOrder' => SORT_ASC];
Expand All @@ -263,7 +302,7 @@ private function normalizeNestedElementParams(): void
}

/**
* Normalizes the fieldId param to an array of IDs or null
* Normalizes the fieldId param to an array of IDs, false, or null
*/
private function normalizeFieldId(): void
{
Expand All @@ -285,22 +324,29 @@ private function normalizeFieldId(): void
}

/**
* Normalizes the primaryOwnerId param to an array of IDs or null
* Normalizes the primaryOwnerId param to an array of IDs, false, or null
*
* @param mixed $value
* @return int[]|null|false
*/
private function normalizeOwnerId(mixed $value): array|null|false
{
if ($value === false) {
return false;
}

if (empty($value)) {
return null;
return is_array($value) ? [] : null;
}

if (is_numeric($value)) {
return [$value];
}

if (!is_array($value) || !ArrayHelper::isNumeric($value)) {
return false;
}

return $value;
}

Expand Down
7 changes: 4 additions & 3 deletions src/helpers/ElementHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,18 @@ public static function generateSlug(string $str, ?bool $ascii = null, ?string $l
$slug = StringHelper::toAscii($slug, $language);
}

return static::normalizeSlug($slug);
return static::normalizeSlug($slug, $language);
}

/**
* Normalizes a slug.
*
* @param string $slug
* @param string|null $language The slug’s langauge
* @return string
* @since 3.5.0
*/
public static function normalizeSlug(string $slug): string
public static function normalizeSlug(string $slug, ?string $language = null): string
{
// Special case for the homepage
if ($slug === Element::HOMEPAGE_URI) {
Expand All @@ -134,7 +135,7 @@ public static function normalizeSlug(string $slug): string
// Make it lowercase
$generalConfig = Craft::$app->getConfig()->getGeneral();
if (!$generalConfig->allowUppercaseInSlug) {
$slug = mb_strtolower($slug);
$slug = StringHelper::toLowerCase($slug, $language);
}

// Get the "words". Split on anything that is not alphanumeric or allowed punctuation
Expand Down
32 changes: 26 additions & 6 deletions src/helpers/StringHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@

use BackedEnum;
use Craft;
use craft\i18n\Locale;
use HTMLPurifier_Config;
use Illuminate\Support\Str;
use IteratorAggregate;
use LitEmoji\LitEmoji;
use Normalizer;
use Throwable;
use Transliterator;
use voku\helper\ASCII;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
Expand Down Expand Up @@ -2313,11 +2315,12 @@ public static function toKebabCase(string $str, string $glue = '-', bool $lower
* Converts all characters in the string to lowercase. An alias for PHP's mb_strtolower().
*
* @param string $str The string to convert to lowercase.
* @param string|null $language The string’s langauge
* @return string The lowercase string.
*/
public static function toLowerCase(string $str): string
public static function toLowerCase(string $str, ?string $language = null): string
{
return Str::lower($str);
return self::modifyCase($str, $language, 'Lower') ?? Str::lower($str);
}

/**
Expand Down Expand Up @@ -2414,11 +2417,12 @@ public static function toTabs(string $str, int $tabLength = 4): string
* Converts the first character of each word in the string to uppercase.
*
* @param string $str The string to convert case.
* @param string|null $language The string’s langauge
* @return string The title-cased string.
*/
public static function toTitleCase(string $str): string
public static function toTitleCase(string $str, ?string $language = null): string
{
return Str::title($str);
return self::modifyCase($str, $language, 'Title') ?? Str::title($str);
}

/**
Expand All @@ -2440,11 +2444,12 @@ public static function toTransliterate(string $str, bool $strict = false): strin
* Converts all characters in the string to uppercase. An alias for PHP's mb_strtoupper().
*
* @param string $str The string to convert to uppercase.
* @param string|null $language The string’s langauge
* @return string The uppercase string.
*/
public static function toUpperCase(string $str): string
public static function toUpperCase(string $str, ?string $language = null): string
{
return Str::upper($str);
return self::modifyCase($str, $language, 'Upper') ?? Str::upper($str);
}

/**
Expand Down Expand Up @@ -2803,4 +2808,19 @@ public static function invisibleCharsRegex(): string

return sprintf('/%s/iu', implode('|', $invisibleCharCodes));
}

private static function modifyCase(string $str, ?string $language, string $case): ?string
{
$language ??= Craft::$app->language;
$transliterator = Transliterator::create(sprintf('%s-%s', Locale::languageId($language), $case));

if (!$transliterator) {
return null;
}

// Normalize NFD chars to NFC
$str = Normalizer::normalize($str, Normalizer::FORM_C);

return $transliterator->transliterate($str);
}
}
19 changes: 14 additions & 5 deletions src/i18n/Locale.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@
*/
class Locale extends BaseObject
{
/**
* Returns a locale’s language ID.
*
* @return string
* @since 5.12.0
*/
public static function languageId(string $locale): string
{
$pos = strpos($locale, '-');
$lang = $pos !== false ? substr($locale, 0, $pos) : $locale;
return strtolower($lang);
}

/**
* @var int Positive prefix.
*/
Expand Down Expand Up @@ -296,11 +309,7 @@ public function __toString(): string
#[AllowedInSandbox]
public function getLanguageID(): string
{
if (($pos = strpos($this->id, '-')) !== false) {
return substr($this->id, 0, $pos);
}

return $this->id;
return static::languageId($this->id);
}

/**
Expand Down
Loading
Loading