Trying to get simplenews to work in the use case of a blog where one of the newsletter types notifies subscribers of new posts. I want mail to happen without additional action beyond publishing a post. In order to do that (barring a better way), I set $node->simplenews_issue->status = SIMPLENEWS_STATUS_SEND_PUBLISH in a custom hook_node_create().

This causes a fatal error on saving the node if it is published on its first revision, because sending is triggered from simplenews_node_presave before the node has been given an id (so it can't be spooled.)

Attached patch adds support for sending immediately on publish like this.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mbaynton created an issue. See original summary.

mbaynton’s picture

Status: Active » Needs review
FileSize
1.32 KB
mbaynton’s picture

FileSize
1.59 KB

Commented new function

Berdir’s picture

Status: Needs review » Needs work

Thanks.

  1. +++ b/simplenews.module
    @@ -111,14 +111,43 @@ function simplenews_node_view(array &$build, NodeInterface $node, $display, $vie
    +  if ($node->id() !== NULL) {
    +    return simplenews_node_send_on_publish($node);
    +  } // else simplenews_node_insert will catch it.
    

    We don't really do comments like that. Should be above the code and explain what the code below is doing.

    Also, I think if (!$node->isNew()) is a bit more self-explanatory.

  2. +++ b/simplenews.module
    @@ -111,14 +111,43 @@ function simplenews_node_view(array &$build, NodeInterface $node, $display, $vie
    +  if (simplenews_node_send_on_publish($node)) {
    +    // Need to save again to capture changed simplenews_issue->status.
    +    $has_revs = $node->isNewRevision();
    +    $node->setNewRevision(FALSE);
    +    $node->save();
    

    so this will only do it if it is set to that value.

    That's not very clear from the function name, so we should have some comment that explains when and under which condition this is called.

    Also not sure if it really makes sense to have a helper function here. Having different explicit checks in those two hooks might better explain what the code is doing?

  3. +++ b/simplenews.module
    @@ -111,14 +111,43 @@ function simplenews_node_view(array &$build, NodeInterface $node, $display, $vie
    + * @return bool
    + *   True if the node was spooled, or false if the node was not sent as a newsletter.
    

    coding style nitpick: there should be an empty line between the @param's and the @return. descriptions should be kept < 80 characters. initial line of a function should below, descriptions for return/param should be split to multiple lines.

  4. +++ b/simplenews.module
    @@ -111,14 +111,43 @@ function simplenews_node_view(array &$build, NodeInterface $node, $display, $vie
    -    return;
    +    return false;
    

    coding style nitpick: FALSE

mbaynton’s picture

Status: Needs work » Needs review
FileSize
2.31 KB
1.88 KB

Thanks for the quick response. How's this?

so this will only do it if it is set to that value.

Yes; I could just return TRUE there, but if for some reason \Drupal::service('simplenews.spool_storage')->addFromEntity($node); failed to update the status, simplenews_node_insert would endless loop.

Also not sure if it really makes sense to have a helper function here.

Thought about it but kept it for now (with a different name.) There isn't branching within the helper function depending on where it's called from at all, so you just duplicate that code. The function and its usages are within 47 lines now amply commented so I think its fairly clear to see what is happening.

Berdir’s picture

Status: Needs review » Needs work
  1. +++ b/simplenews.module
    @@ -111,14 +111,49 @@ function simplenews_node_view(array &$build, NodeInterface $node, $display, $vie
    +  // Send newsletters that were only waiting to be published, unless they do
    +  // not exist in storage or have an id yet - the spool storage needs those.
    +  if (!$node->isNew()) {
    +    return simplenews_node_send_if_published($node);
    

    new function name works for me. comment is also almost fine, the "or have an id yet" part seems confusing since we don' check for that and it also doesn't really seem valid english to me? I think we can just remove that.

  2. +++ b/simplenews.module
    @@ -111,14 +111,49 @@ function simplenews_node_view(array &$build, NodeInterface $node, $display, $vie
    +  // simplenews_node_presave will skip any nodes that are published in their
    +  // 1st revision, so we also check if sending should be triggered here, after
    +  // 1st revisions are persisted to storage.
    

    a function reference should have (). I think this comment can be simplified a bit, using revisions here seems confusing to me. What about:

    // simplenews_node_presave() cannot automatically send new nodes, send those here and resave.

    Hm, now that I think about it, I had an idea. To avoid the double save and also possible problems if saving to spool fails (an exception thrown there in presave could interrupt the node from being saved), what if we always send the node in insert() and just change the flag in presave?

    That of course means that we need to save in a different way that we want to send the node. We could use a flag on the node object although that's a bit discouraged (but still quite widely used). Or we could use a pattern with a function that has a static property (or method on a service) that remembers the nid and on insert()/update(), we just check if we had added that/flagged that node?

We should also expand our test coverage for this a bit.

mbaynton’s picture

To avoid the double save and also possible problems if saving to spool fails (an exception thrown there in presave could interrupt the node from being saved)

yeah that would be bad

what if we always send the node in insert() and just change the flag in presave?

Do you mean change the flag to the value we ultimately want saved with the node, SIMPLENEWS_STATUS_SEND_PENDING?

Currently that happens in SpoolStorage::addFromEntity after a db insert to simplenews_mail_spool, which I'm guessing is important. So the status on the entity's field will only indicate it's spooled if it really has been. If we switch it up we have

  1. In presave, set to SIMPLENEWS_STATUS_SEND_PENDING
  2. Entity is saved
  3. All hook_entity_translation_insert(), hook_entity_translation_delete(), and earlier hook_insert() implementations don't crap out, hopefully
  4. Entity is actually spooled

Still I'm definitely for minimizing the amount of things that happen before the node is saved and like your idea because of it. But we would I think want to also find a way to mitigate the impacts of the possible inconsistent state where the node incorrectly thinks it's SEND_PENDING (which the UI currently relies on to tell the user their thing is sending).

mbaynton’s picture

Or, take the hit and always double-save? Would let you update the spool and entity transactionally even if you wanted and would simplify things quite a bit.

Berdir’s picture

Yes, you're right, that's risky too.

Fair enough, then lets change it so that we always do a double save, it shouldn't happen too often. Add a comment that explains why and we can get this in :)

mbaynton’s picture

Status: Needs work » Needs review
FileSize
1.96 KB

Ok this is seriously harder than it should be. When moving to a model where we always save the entity first and then possibly send as a result of publication afterward, hook_node_update breaks. You just can't recursively save a revisionable entity again from within hook_insert / hook_update. The reason is a super-annoying unset($entity->original) in EntityStorageBase::doPostSave(). The second recursive entity save will run to completion before the original one's hook_update implementations run, and those implementations expect $entity->original to exist, but the php object has already experienced one complete save process and part of completely saving the entity is unsetting $entity->original.

I see that ContentEntityBase implements __clone and so the obvious solution seemed to be saving a clone of the entity. It seems to work.

Just a brand new patch with no interdiff because this is basically starting over.

miro_dietiker’s picture

Status: Needs review » Needs work

This makes sense, but we really need a test for such an extra use case.

VitaliyDemchuk’s picture

There are some changes to this problem?

mbaynton’s picture

FileSize
2.39 KB

@Berdir, you were right all along in #6. My concern that your approach could lead to an inconsistent state between the saved simpenews_issue field and the actual mail spool if some hook_insert() et. al failed was inaccurate, or at least nowadays under core 8.5.6 it's inaccurate, because failures in those hooks result in the database transaction that updated the simpenews_issue field to be rolled back.

Here's an updated patch that follows the approach you suggested in #6 and seems to be working beautifully in fact; the single-save approach is clearly superior. I'm still strongly considering writing tests too so this is more likely to get in ;) but I wanted to post the patch regardless in case I don't do that and someone else finds it useful.

Just a brand new patch with no interdiff because this is basically starting over. Again.

AdamPS’s picture

Issue tags: +Needs tests