Skip to content

Commit d5b601a

Browse files
authored
Merge pull request #17024 from craftcms/feature/cms-74-statically-stored-entry-statuses
Store entry statuses statically
2 parents 0739479 + b989e08 commit d5b601a

File tree

10 files changed

+275
-15
lines changed

10 files changed

+275
-15
lines changed

CHANGELOG-WIP.md

+3
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@
5454
- Number previews within card attribute designers now show an in-range value, if the Min Value and Max Value settings are set. ([#16986](https://github.com/craftcms/cms/pull/16986))
5555
- Range previews within card attribute designers new shew an in-range value. ([#16986](https://github.com/craftcms/cms/pull/16986))
5656
- Section and entry type names are now linked to their edit pages from entry indexes, for admins.
57+
- Added the `staticStatuses` config setting, for opting into entry statuses being stored statically and only updated on save. ([#17024](https://github.com/craftcms/cms/pull/17024))
5758
- Added the `db/repair` command. ([#16812](https://github.com/craftcms/cms/pull/16812))
5859
- Added the `fields/delete` command. ([#16828](https://github.com/craftcms/cms/pull/16828))
60+
- Added the `update-statuses` command. ([#17024](https://github.com/craftcms/cms/pull/17024))
5961
- Added the `--batch-size` option for `resave/*` commands. ([#16586](https://github.com/craftcms/cms/issues/16586))
6062
- The `users/create` command now prompts to send an activation email, or outputs an activation URL. ([#16794](https://github.com/craftcms/cms/pull/16794))
6163
- Dragging headings within the Customize Sources modal now also drags any subsequent sources. ([#16737](https://github.com/craftcms/cms/issues/16737))
@@ -97,6 +99,7 @@
9799
- Added `craft\db\Table::BULKOPEVENTS`.
98100
- Added `craft\db\Table::SEARCHINDEXQUEUE_FIELDS`.
99101
- Added `craft\db\Table::SEARCHINDEXQUEUE`.
102+
- Added `craft\elements\Entry::$oldStatus`. ([#17024](https://github.com/craftcms/cms/pull/17024))
100103
- Added `craft\elements\Entry::$placeInStructure`.
101104
- Added `craft\elements\actions\Copy`.
102105
- Added `craft\elements\actions\MoveDown`.

src/config/GeneralConfig.php

+43
Original file line numberDiff line numberDiff line change
@@ -2956,6 +2956,24 @@ class GeneralConfig extends BaseConfig
29562956
*/
29572957
public mixed $softDeleteDuration = 2592000;
29582958

2959+
/**
2960+
* @var bool Whether entries’ statuses should be stored statically, and only get updated on entry save, or when the
2961+
* `update-statuses` command is executed.
2962+
*
2963+
* ::: code
2964+
* ```php Static Config
2965+
* ->staticStatuses()
2966+
* ```
2967+
* ```shell Environment Override
2968+
* CRAFT_STATIC_STATUSES=true
2969+
* ```
2970+
* :::
2971+
*
2972+
* @group System
2973+
* @since 5.7.0
2974+
*/
2975+
public bool $staticStatuses = false;
2976+
29592977
/**
29602978
* @var bool Whether user IP addresses should be stored/logged by the system.
29612979
*
@@ -6643,6 +6661,31 @@ public function softDeleteDuration(mixed $value): self
66436661
return $this;
66446662
}
66456663

6664+
/**
6665+
* Whether entries’ statuses should be stored statically, and only get updated on entry save, or when the
6666+
* `update-statuses` command is executed.
6667+
*
6668+
* ::: code
6669+
* ```php Static Config
6670+
* ->staticStatuses()
6671+
* ```
6672+
* ```shell Environment Override
6673+
* CRAFT_STATIC_STATUSES=true
6674+
* ```
6675+
* :::
6676+
*
6677+
* @group System
6678+
* @param bool $value
6679+
* @return self
6680+
* @see $staticStatuses
6681+
* @since 5.7.0
6682+
*/
6683+
public function staticStatuses(bool $value = true): self
6684+
{
6685+
$this->staticStatuses = $value;
6686+
return $this;
6687+
}
6688+
66466689
/**
66476690
* Whether user IP addresses should be stored/logged by the system.
66486691
*

src/config/app.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
'id' => 'CraftCMS',
55
'name' => 'Craft CMS',
66
'version' => '5.6.14',
7-
'schemaVersion' => '5.7.0.2',
7+
'schemaVersion' => '5.7.0.3',
88
'minVersionRequired' => '4.5.0',
99
'basePath' => dirname(__DIR__), // Defines the @app alias
1010
'runtimePath' => '@storage/runtime', // Defines the @runtime alias
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
/**
3+
* @link https://craftcms.com/
4+
* @copyright Copyright (c) Pixel & Tonic, Inc.
5+
* @license https://craftcms.github.io/license/
6+
*/
7+
8+
namespace craft\console\controllers;
9+
10+
use Craft;
11+
use craft\console\Controller;
12+
use craft\elements\Entry;
13+
use craft\events\MultiElementActionEvent;
14+
use craft\helpers\Console;
15+
use craft\helpers\DateTimeHelper;
16+
use craft\helpers\Db;
17+
use craft\services\Elements;
18+
use yii\console\ExitCode;
19+
20+
/**
21+
* Updates statically-stored entry statuses.
22+
*
23+
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
24+
* @since 5.7.0
25+
*/
26+
class UpdateStatusesController extends Controller
27+
{
28+
/**
29+
* Updates statically-stored entry statuses.
30+
*
31+
* @return int
32+
*/
33+
public function actionIndex(): int
34+
{
35+
$now = Db::prepareDateForDb(DateTimeHelper::now());
36+
$elementsService = Craft::$app->getElements();
37+
38+
$conditions = [
39+
Entry::STATUS_LIVE => [
40+
'and',
41+
['<=', 'entries.postDate', $now],
42+
[
43+
'or',
44+
['entries.expiryDate' => null],
45+
['>', 'entries.expiryDate', $now],
46+
],
47+
],
48+
Entry::STATUS_PENDING => ['>', 'entries.postDate', $now],
49+
Entry::STATUS_EXPIRED => [
50+
'and',
51+
['not', ['entries.expiryDate' => null]],
52+
['<=', 'entries.expiryDate', $now],
53+
],
54+
];
55+
56+
foreach ($conditions as $status => $condition) {
57+
$query = Entry::find()
58+
->site('*')
59+
->unique()
60+
->status(null)
61+
->andWhere(['not', ['status' => $status]])
62+
->andWhere($condition);
63+
64+
$this->do("Updating $status entries", function() use ($elementsService, $query) {
65+
$count = (int)$query->count();
66+
67+
$beforeCallback = function(MultiElementActionEvent $e) use ($query, $count) {
68+
if ($e->query === $query) {
69+
$this->stdout(Console::indentStr() . " - [$e->position/$count] Updating entry ({$e->element->id}) ... ");
70+
}
71+
};
72+
73+
$afterCallback = function(MultiElementActionEvent $e) use ($query, &$fail) {
74+
if ($e->query === $query) {
75+
if ($e->exception) {
76+
$this->stdout('error: ' . $e->exception->getMessage() . PHP_EOL, Console::FG_RED);
77+
$fail = true;
78+
} elseif ($e->element->hasErrors()) {
79+
$this->stdout('failed: ' . implode(', ', $e->element->getErrorSummary(true)) . PHP_EOL, Console::FG_RED);
80+
$fail = true;
81+
} else {
82+
$this->stdout('done' . PHP_EOL, Console::FG_GREEN);
83+
}
84+
}
85+
};
86+
87+
$elementsService->on(Elements::EVENT_BEFORE_RESAVE_ELEMENT, $beforeCallback);
88+
$elementsService->on(Elements::EVENT_AFTER_RESAVE_ELEMENT, $afterCallback);
89+
$elementsService->resaveElements($query, true, updateSearchIndex: false);
90+
$elementsService->off(Elements::EVENT_BEFORE_RESAVE_ELEMENT, $beforeCallback);
91+
$elementsService->off(Elements::EVENT_AFTER_RESAVE_ELEMENT, $afterCallback);
92+
});
93+
}
94+
95+
return ExitCode::OK;
96+
}
97+
}

src/elements/Entry.php

+49-14
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,16 @@ protected static function prepElementQueryForTableAttribute(ElementQueryInterfac
848848
*/
849849
public ?DateTime $expiryDate = null;
850850

851+
/**
852+
* @var self::STATUS_*|null The entry’s previous status, if it had one
853+
*/
854+
public ?string $oldStatus = null;
855+
856+
/**
857+
* @var self::STATUS_LIVE|self::STATUS_PENDING|self::STATUS_EXPIRED
858+
*/
859+
private string $status;
860+
851861
/**
852862
* @var bool Whether the entry was deleted along with its entry type
853863
* @see beforeDelete()
@@ -912,6 +922,9 @@ protected static function prepElementQueryForTableAttribute(ElementQueryInterfac
912922
public function init(): void
913923
{
914924
parent::init();
925+
if (isset($this->id)) {
926+
$this->oldStatus = $this->getStatus();
927+
}
915928
$this->_oldTypeId = $this->_typeId;
916929
}
917930

@@ -1766,23 +1779,35 @@ public function getStatus(): ?string
17661779
{
17671780
$status = parent::getStatus();
17681781

1769-
if ($status == self::STATUS_ENABLED && $this->postDate) {
1770-
$currentTime = DateTimeHelper::currentTimeStamp();
1771-
$postDate = $this->postDate->getTimestamp();
1772-
$expiryDate = $this->expiryDate?->getTimestamp();
1773-
1774-
if ($postDate <= $currentTime && ($expiryDate === null || $expiryDate > $currentTime)) {
1775-
return self::STATUS_LIVE;
1776-
}
1782+
if ($status !== self::STATUS_ENABLED) {
1783+
return $status;
1784+
}
17771785

1778-
if ($postDate > $currentTime) {
1779-
return self::STATUS_PENDING;
1780-
}
1786+
return $this->status ?? $this->_status();
1787+
}
17811788

1782-
return self::STATUS_EXPIRED;
1783-
}
1789+
/**
1790+
* @return self::STATUS_LIVE|self::STATUS_PENDING|self::STATUS_EXPIRED
1791+
*/
1792+
private function _status(): string
1793+
{
1794+
$now = DateTimeHelper::now();
1795+
return match (true) {
1796+
!$this->postDate || $this->postDate > $now => self::STATUS_PENDING,
1797+
$this->expiryDate && $this->expiryDate <= $now => self::STATUS_EXPIRED,
1798+
default => self::STATUS_LIVE,
1799+
};
1800+
}
17841801

1785-
return $status;
1802+
/**
1803+
* Sets the status, if it’s stored statically.
1804+
*
1805+
* @param self::STATUS_LIVE|self::STATUS_PENDING|self::STATUS_EXPIRED $status
1806+
* @since 5.7.0
1807+
*/
1808+
public function setStatus(string $status): void
1809+
{
1810+
$this->status = $status;
17861811
}
17871812

17881813
/**
@@ -2712,6 +2737,16 @@ public function afterSave(bool $isNew): void
27122737
$record->postDate = Db::prepareDateForDb($this->postDate);
27132738
$record->expiryDate = Db::prepareDateForDb($this->expiryDate);
27142739

2740+
// todo: update after the next breakpoint
2741+
if (Craft::$app->getDb()->columnExists(Table::ENTRIES, 'status')) {
2742+
$status = $this->_status();
2743+
$record->status = $status;
2744+
// only update $this->status if it's already set, indicating that staticStatuses is enabled
2745+
if (isset($this->status)) {
2746+
$this->status = $status;
2747+
}
2748+
}
2749+
27152750
// Capture the dirty attributes from the record
27162751
$dirtyAttributes = array_keys($record->getDirtyAttributes());
27172752
$record->save(false);

src/elements/db/EntryQuery.php

+19
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,14 @@ protected function beforePrepare(): bool
860860
'entries.expiryDate',
861861
]);
862862

863+
// todo: update after the next breakpoint
864+
if (
865+
Craft::$app->getConfig()->getGeneral()->staticStatuses &&
866+
Craft::$app->getDb()->columnExists(Table::ENTRIES, 'status')
867+
) {
868+
$this->query->addSelect(['entries.status']);
869+
}
870+
863871
$this->_applySectionIdParam();
864872
$this->applyNestedElementParams('entries.fieldId', 'entries.primaryOwnerId');
865873

@@ -984,6 +992,17 @@ private function loadAuthorIds(array $entries): void
984992
*/
985993
protected function statusCondition(string $status): mixed
986994
{
995+
if (
996+
in_array($status, [Entry::STATUS_LIVE, Entry::STATUS_PENDING, Entry::STATUS_EXPIRED]) &&
997+
Craft::$app->getConfig()->getGeneral()->staticStatuses
998+
) {
999+
return [
1000+
'elements.enabled' => true,
1001+
'elements_sites.enabled' => true,
1002+
'entries.status' => $status,
1003+
];
1004+
}
1005+
9871006
// Always consider “now” to be the current time @ 59 seconds into the minute.
9881007
// This makes entry queries more cacheable, since they only change once every minute (https://github.com/craftcms/cms/issues/5389),
9891008
// while not excluding any entries that may have just been published in the past minute (https://github.com/craftcms/cms/issues/7853).

src/migrations/Install.php

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use craft\db\Migration;
1616
use craft\db\Table;
1717
use craft\elements\Asset;
18+
use craft\elements\Entry;
1819
use craft\elements\User;
1920
use craft\enums\CmsEdition;
2021
use craft\enums\PropagationMethod;
@@ -414,6 +415,11 @@ public function createTables(): void
414415
'typeId' => $this->integer()->notNull(),
415416
'postDate' => $this->dateTime(),
416417
'expiryDate' => $this->dateTime(),
418+
'status' => $this->enum('status', [
419+
Entry::STATUS_LIVE,
420+
Entry::STATUS_PENDING,
421+
Entry::STATUS_EXPIRED,
422+
])->notNull()->defaultValue(Entry::STATUS_LIVE),
417423
'deletedWithEntryType' => $this->boolean()->null(),
418424
'deletedWithSection' => $this->boolean()->null(),
419425
'dateCreated' => $this->dateTime()->notNull(),
@@ -893,6 +899,7 @@ public function createIndexes(): void
893899
$this->createIndex(null, Table::SYSTEMMESSAGES, ['language'], false);
894900
$this->createIndex(null, Table::ENTRIES, ['postDate'], false);
895901
$this->createIndex(null, Table::ENTRIES, ['expiryDate'], false);
902+
$this->createIndex(null, Table::ENTRIES, ['status'], false);
896903
$this->createIndex(null, Table::ENTRIES, ['sectionId'], false);
897904
$this->createIndex(null, Table::ENTRIES, ['typeId'], false);
898905
$this->createIndex(null, Table::ENTRIES_AUTHORS, ['authorId'], false);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace craft\migrations;
4+
5+
use craft\db\Migration;
6+
use craft\db\Table;
7+
use craft\elements\Entry;
8+
use craft\helpers\DateTimeHelper;
9+
use craft\helpers\Db;
10+
11+
/**
12+
* m250403_171253_static_statuses migration.
13+
*/
14+
class m250403_171253_static_statuses extends Migration
15+
{
16+
/**
17+
* @inheritdoc
18+
*/
19+
public function safeUp(): bool
20+
{
21+
$this->addColumn(Table::ENTRIES, 'status', $this->enum('status', [
22+
Entry::STATUS_LIVE,
23+
Entry::STATUS_PENDING,
24+
Entry::STATUS_EXPIRED,
25+
])->notNull()->defaultValue(Entry::STATUS_LIVE)->after('expiryDate'));
26+
$this->createIndex(null, Table::ENTRIES, ['status'], false);
27+
28+
$currentTimeDb = Db::prepareDateForDb(DateTimeHelper::now());
29+
$this->update(Table::ENTRIES, ['status' => Entry::STATUS_PENDING], [
30+
'or',
31+
['postDate' => null],
32+
['>', 'postDate', $currentTimeDb],
33+
]);
34+
$this->update(Table::ENTRIES, ['status' => Entry::STATUS_EXPIRED], [
35+
'and',
36+
['not', ['postDate' => null]],
37+
['<=', 'entries.expiryDate', $currentTimeDb],
38+
]);
39+
40+
return true;
41+
}
42+
43+
/**
44+
* @inheritdoc
45+
*/
46+
public function safeDown(): bool
47+
{
48+
$this->dropColumn(Table::ENTRIES, 'status');
49+
return true;
50+
}
51+
}

src/records/Entry.php

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* @property int $typeId Type ID
2222
* @property string|null $postDate Post date
2323
* @property string|null $expiryDate Expiry date
24+
* @property string $status Live
2425
* @property Element $element Element
2526
* @property Section $section Section
2627
* @property EntryType $type Type

0 commit comments

Comments
 (0)