Brandon's answer was a helpful starting point for me but I think the answer goes deeper and has more scenarios to consider. Here's my attempt at clarifying that:
Historic Context
Before Drafts and Revisions were Elements, this was simple. When a new Element was created before and after save events had an $isNew property and if it was true
you could assume an Element was being saved the first time. If it was false
an Element was being updated. Drafts and Revisions were saved in separate tables, using a separate process.
I believe in Craft 3.2, Drafts and Revisions get updated and become more like Elements in the way they store their data. Rather than their content being stored as a JSON blob in their own unique workflow, they start getting saved like Elements where they have a primary table that stores their ID data and their content gets stored in the content table like other Elements.
At this point things get more confusing and $element->resaving
becomes a more important variable to know about because if you are running a task that re-saves all Elements, you are not only resaving all primary content in your database you are also potentially re-saving a bunch of Drafts and Revisions too. (Make sure any loosly written email triggering scripts are tended to before updating past 3.2, or when Craft resaves all Elements during it's upgrade process to 3.2, you might be triggering lots of emails!) I believe $isNew
continued to mean the same thing.
In Craft 3.7, Craft made a change to keep the entry ID consistent between its unpublished draft and the real entry (#9104) which had the unintended consequence of changing the meaning of $isNew
in the events where it was used. Brandon writes: "Before 3.7, the unpublished draft was duplicated into a normal entry, with a new ID, therefore $isNew
was true." However, after 3.7, $isNew
is only true when the initial unpublished draft is auto-saved in the control panel creating the first instance of the element in the db.
This was addressed in Craft 3.7.5 where a $firstSave property is introduced for entries.
The above summary is based on my experience and best recollections, so do your testing. My main goal here was to get a lot of these items written down so anybody else that runs into troubles and needs to figure out this stuff might find more information than I did while trying to make sense of it all. If anybody wants to confirm or correct specific version numbers, timeframes, or behaviors, please edit away.
For those new to Craft post 3.7.5, there will likely be less confusion or need to care about this than those of us that were working with the CMS from 1.x and 2.x and had to break down our original assumptions of what was going on here.
New and Updated Elements
Below I present a way to target new Elements and Updated Elements. These examples come from a discussion with Brandon Kelly about the topic here.
New Elements
To identify new elements, use the $element->firstSave
property.
The craft\base\Element::EVENT_AFTER_PROPAGATE
method only triggers once if you have a multi-site setup so you'll need less logic than if you use EVENT_AFTER_SAVE
which gets triggered after Drafts, Revisions, and other multi-site versions of an Element getting saved.
Here's an example of an event that will trigger once when a new Element is saved:
use craft\base\Element;
use craft\elements\Entry;
use craft\events\ModelEvent;
use yii\base\Event;
Event::on(
Entry::class,
Element::EVENT_AFTER_PROPAGATE,
function(ModelEvent $event) {
/** @var Entry $entry */
$entry = $event->sender;
if ($entry->firstSave && $entry->getStatus() === Entry::STATUS_LIVE) {
// boom
}
}
);
Updated Elements
For updated Elements, we need to check a few more things as there can be several scenarios where Elements get updated (canonical elements, drafts, revisions, multi-site). In addition to making sure we don't have a new element, and the element is not a Draft or Revision, you probably also want to make sure to not trigger anything when there is a mass re-save event such as when you update the URL Format of a Section and all Elements in that Section are re-saved. To avoid the ResaveElements
queue job scenario we check for !$element->resaving
.
Below is a helper method that would return true
or false
if an Element is being updated.
public function isUpdatedElement($element): bool
{
return
!$element->firstSave &&
!ElementHelper::isDraftOrRevision($element) &&
!$element->resaving;
}
Be sure to test. And note that each Element will have different "statuses" so you may need to target a status that is more meaningful to your scenario.
There may be a few other limitations to be aware of. In the case of trying to match a new, live Entry Element the $entry->firstSave && $entry->getStatus() === Entry::STATUS_LIVE
check above would not match a scenario where an author created a new entry and marked it as disabled
before they saved it for the first time. In that case, the disabled
status of the new entry would not match your check, and only when the author updated the Entry Element to be enabled
and re-saved would the element return true for something like the isUpdatedElement()
method.
Similarly, saving a new Entry Element with a postDate
in the future would return false for the $entry->firstSave && $entry->getStatus() === Entry::STATUS_LIVE
check. No action takes place when a future date becomes a date in the past on the calendar, this type of "new entry" scenario would require some other method to be identified, such as using a cron-job.
List of Things that happen
While Brandon's list of actions in his answer is a helpful starting point if you ever need to dig into this more than the examples above can address, there's more to it so I would be careful to treat that list as comprehensive. Here's my own list of actions that I ran into while testing.
- Press “New entry” from Entries index page
- Make additional changes to the unpublished draft
- Press “Create entry” from the Edit Entry page
- Press “Command+S” from the Edit Entry page (save new entry)
- Start editing the Current revision of an entry
- Make additional changes to the provisional draft
- Press “Save” on the provisional draft
- Press “Command+S” from the Edit Entry page (save existing entry with no edits)
- Press “Create a draft” on the Current revision (not a provisional draft)
- Press “Create a draft” on a provisional draft
- Press “Apply draft” on a (normal) draft
- Enable a disabled Entry Element via Element Action
- Disable an Entry Element via Element Action
- Trigger a ResaveElements Task (a simple way to test is to update the URL Format in a Section)
- When an Entry is propagated (If you run multi-site, every time you save a canonical entry you may be triggering an addition save for every other site. If you have a section where 10 Sites can be edited, you may be triggering 10 Elements to be saved behind the scenes)
- Programmatic creation (new & updated scenarios may vary if you are creating Elements via custom code, depending on what methods you are using)
- Guest Entries plugin (As an example of how custom code may differ, the Guest Entry Plugin calls
Craft::$app->getElements()->saveElement($entry)
directly vs sending the request to the EntriesController::actionSaveEntry
method which preps a few things before calling the Elements service saveElement
method.
Some related discussions