Port custom publishing options to Drupal 8

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Daniel Schaefer created an issue. See original summary.

kevinquillen’s picture

I started looking in to it a few weeks ago, but I am still learning Drupal 8.

Daniel Schaefer’s picture

kevinquillen’s picture

I see what you mean here, but in the context of creating them dynamically custom options can't be in their own yml files.

So, each action corresponds to a plugin, like node_promote_action:

namespace Drupal\node\Plugin\Action;

use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;

/**
 * Promotes a node.
 *
 * @Action(
 *   id = "node_promote_action",
 *   label = @Translation("Promote selected content to front page"),
 *   type = "node"
 * )
 */
class PromoteNode extends ActionBase {

  /**
   * {@inheritdoc}
   */
  public function execute($entity = NULL) {
    $entity->setPromoted(TRUE);
    $entity->save();
  }

  /**
   * {@inheritdoc}
   */
  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
    /** @var \Drupal\node\NodeInterface $object */
    $access = $object->access('update', $account, TRUE)
      ->andif($object->promote->access('edit', $account, TRUE));
    return $return_as_object ? $access : $access->isAllowed();
  }

}

I can't see how to apply this either in the context of dynamically creating custom pub options (see [#2330631] https://www.drupal.org/node/2330631).

My initial feeling is that custom pub options should be config entities that consist of a title/label and machine name. We can then re-implement the dynamic permissions. As a config entity, we can get these properties along with weight and sortable interface without much of a heavy lift (it appears).

From there, we need to attach it to the node form (and I assume) our own submit handler added on to properly save the state of any custom pubs ticked off. But I think you are right - in order to play nice in the new D8 world, they need to be proper actions. That would ensure that its usable in the core Actions realm, and forthcoming Rules release. To expand on that, CPO should also (IMO) support any content entity, not just nodes - but that can come later.

We also need to adapt the node schema when options are added and removed (add field, drop field) - I think I see a way to do this just glancing at node_update_8002 and node_update_8003 as an example.

I myself am still wrapping my head around a lot of these changes.

Does anyone know if the actions.yml file can define a callback to define dynamic action definitions, the same way permissions.yml can?

kevinquillen’s picture

Those statuses are stored in node_field_data... how do they get there? I am not seeing it in the node.install or config files of the module.

edit:

Oh, I see. In the Node class under baseFieldDefinitions():

$fields['sticky'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Sticky at top of lists'))
      ->setRevisionable(TRUE)
      ->setTranslatable(TRUE)
      ->setDefaultValue(FALSE)
      ->setDisplayOptions('form', array(
        'type' => 'boolean_checkbox',
        'settings' => array(
          'display_label' => TRUE,
        ),
        'weight' => 16,
      ))
      ->setDisplayConfigurable('form', TRUE);

So what is considered best practice in this case? Should we extend this class, call the parent method and then add on our custom publish options like this? Or is there another entry point to use so there is a clear separation of concern?

hook_entity_type_alter / hook_entity_type_build seem close, but it isn't clear which one we should employ. I'd also like to find an OO way to do this, if that is the way things should be in D8. So it seems like you could quickly create a form that 'creates' pub options which are just key|value pairs in a cheap schema in the database, but that feels dirty, and I would like the system to be aware of this without an alter jungle.

I am not 100% certain an 'option' is a config entity, but I think they should be. Creating new CPO options then lets you set:

  • Label
  • Machine Name
  • Enabled (boolean)
  • Role access
  • Node types its available to

I think this would help in regards to removing a lot of legacy code from D7. Can anyone weigh in on if this is the correct route to take?

Daniel Schaefer’s picture

Yeah I've struggled with that too. I've asked for help over in the core section but no reply yet: https://www.drupal.org/node/2649072.

Config entities is probably the right track already. They could be used to create/edit/order/delete the Publishing Options. There is promising tutorial at Creating Custom Config Entities. I'll take a deeper look into the tutorial and test the code on my machine.

The second part, defining actions and hooking into the node/entity creation process I have no clue yet so I can't comment much on your thoughts there. Maybe someone can interpret the relevant code of the Promotion Option fieldset?

kevinquillen’s picture

I've done some example custom entities but I have not done anything in regards to altering how an existing one works (in this case, Node). I am struggling to understand what parts of the Node module are a good blueprint for development and what areas may be where they implemented quick solutions to carry on functionality (there are a lot of comments around some functions and methods that make me think that way).

Anyway... this route feels right, then getting CPOs to show on node type forms requires a form alter. At that point, each one should be an entity key (by virtue of hook_entity_type_alter) of the CPO machine name, and then similar code on the node form itself.

Daniel Schaefer’s picture

Okay read your edit just now. baseFieldDefinitions() looks right, good spot!

So if we created a function

$fields['myoption'] = BaseFieldDefinition::create('boolean')
       ...
      ->setDisplayOptions('form', array(
        'type' => 'boolean_checkbox',
        'settings' => array(
          'display_label' => TRUE,
        ),
        ...
      ))
      ->setDisplayConfigurable('form', TRUE);

would that automatically create a checkbox under the Promotion Option?

kevinquillen’s picture

Are you able to define actions dynamically like you can with permissions?

See: http://cgit.drupalcode.org/custom_pub/tree/custom_pub.permissions.yml?h=...

kevinquillen’s picture

Version: 7.x-1.x-dev » 8.x-1.x-dev
kevinquillen’s picture

It doesn't appear that you can define actions dynamically like you can with permissions.

kevinquillen’s picture

Moving on from that, alpha2 has basic functionality of CPO on the backend.

Todo

  • Set node type(s) per option (right now it applies to all node types)
  • Auto assign options under 'Promotion Options' on content / or their own tab
  • Set access checks to ensure current user can use the option(s)
  • Ensure Views can correctly filter on these options

What I may need help on

  • Re-implementing dynamic actions as derivatives (hook_action_info is gone)
  • Ensure Rules triggers work like before (may have to wait for Rules to have a stable release)
  • Ensure no-conflict with modules like Override Node Options
  • Ensure config is exportable
  • Providing tests moving forward
Daniel Schaefer’s picture

Great job. I've just tested alpha2 on simplytest.me and encountered no problems. In the end the workflow differs not much from creating a boolean field on the node form right now since you still have to add the status in Manage form display (there should be a hint on that after creating the status btw) and it does not show within Promotion Options on the node form and the content list view by default. But considering the changes in core I assume that is the best we can get for now? I like that you've managed to hook into the status list on the node configuration form! Being no programmer I'm having such a hard time understanding what's going on underneath but I'll take a look into your code to see where I may be able to contribute.

kevinquillen’s picture

Well, I followed how core was creating sticky, status, promoted checkboxes for nodes. They create them as boolean field definitions, which makes sense. But now, if they are just fields and not properties (status is set as an entity key still), do we get the same cost savings that we do in D7? I am not sure yet. It was very fast in D7 because a custom pub option was treated as an extension of the node entity, which meant you didn't attach on a field, two field tables were not created in the db, and left join added for queries against that value. It looks like CPOs are still in just the node table, and two field tables are not created for them. So that should count for something.

I also am having trouble finding a way to set options for specific content types, which using the field definition methods does not seem feasible. You can only set a field to one bundle or target entity, you can't pass an array. So right now, all custom publishing options created show on every single node type, regardless. Maybe that isn't such a big deal, but for UI purposes it would be nice to see only what you're intended to see. Also, each publish option is created and default value is set to FALSE, but that isn't retroactively applied to existing content. I guess that is okay, because if you have thousands of nodes, it is an expensive query to execute. Perhaps we could advise to use VBO or provide a batch form to bulk set nodes to that new value, though that seems kludgy.

Also, with how actions are defined now as plugins, I haven't found a way to dynamically define them so it can be triggered. What I mean by that is, look in the 8.x source for how permissions are created. Granted, permissions are simpler than actions, but I was kind of hoping a similar mechanism existed so we could offer the same functionality for actions that is in previous versions.

The way I grouped them on the content forms was look for promoted options, match the custom pub options in the form and group them in the same form item.

Daniel Schaefer’s picture

FileSize
14.94 KB

I think there is a way to include the custom option(s) in the fieldset. Look at line 183 of core/modules/node/src/NodeForm.php:

    // Node options for administrators.
    $form['options'] = array(
      '#type' => 'details',
      '#title' => t('Promotion options'),
      '#group' => 'advanced',
      '#attributes' => array(
        'class' => array('node-form-options'),
      ),
      '#attached' => array(
        'library' => array('node/drupal.node'),
      ),
      '#weight' => 95,
      '#optional' => TRUE,
    );

    if (isset($form['promote'])) {
      $form['promote']['#group'] = 'options';
    }

    if (isset($form['sticky'])) {
      $form['sticky']['#group'] = 'options';
    }

For testing purposes I've hard-coded an option [sponsored] into core/modules/node/src/Entity/node.php (around line 430)

    $fields['sponsored'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Define as sponsored content'))
      ->setRevisionable(TRUE)
      ->setTranslatable(TRUE)
      ->setDefaultValue(TRUE)
      ->setDisplayOptions('form', array(
        'type' => 'boolean_checkbox',
        'settings' => array(
          'display_label' => TRUE,
        ),
        'weight' => 17,
      ))
      ->setDisplayConfigurable('form', TRUE);	

and core/modules/node/src/NodeForm.php (around 206):


if (isset($form['sponsored'])) {
      $form['sponsored']['#group'] = 'options';
    }

After cache clear the option did show up at the right place:

custom promotion options

(Of course, upon submit an exception occurs. I thought it would be worth sharing anyway:

Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sponsored' in 'field list': INSERT INTO {node_field_data} (nid, vid, type, langcode, title, uid, status, created, changed, promote, sponsored, sticky, revision_translation_affected, default_langcode) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6, :db_insert_placeholder_7, :db_insert_placeholder_8, :db_insert_placeholder_9, :db_insert_placeholder_10, :db_insert_placeholder_11, :db_insert_placeholder_12, :db_insert_placeholder_13); Array ( [:db_insert_placeholder_0] => 6 [:db_insert_placeholder_1] => 6 [:db_insert_placeholder_2] => article [:db_insert_placeholder_3] => de [:db_insert_placeholder_4] => Test [:db_insert_placeholder_5] => 1 [:db_insert_placeholder_6] => 1 [:db_insert_placeholder_7] => 1455817505 [:db_insert_placeholder_8] => 1455817519 [:db_insert_placeholder_9] => 1 [:db_insert_placeholder_10] => 1 [:db_insert_placeholder_11] => 0 [:db_insert_placeholder_12] => 1 [:db_insert_placeholder_13] => 1 ) in Drupal\Core\Entity\Sql\SqlContentEntityStorage->saveToSharedTables() (Zeile 904 in /var/www/.d8/node-options/drupal/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

and

Drupal\Core\Entity\EntityStorageException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sponsored' in 'field list': INSERT INTO {node_field_data} (nid, vid, type, langcode, title, uid, status, created, changed, promote, sponsored, sticky, revision_translation_affected, default_langcode) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6, :db_insert_placeholder_7, :db_insert_placeholder_8, :db_insert_placeholder_9, :db_insert_placeholder_10, :db_insert_placeholder_11, :db_insert_placeholder_12, :db_insert_placeholder_13); Array ( [:db_insert_placeholder_0] => 5 [:db_insert_placeholder_1] => 5 [:db_insert_placeholder_2] => page [:db_insert_placeholder_3] => de [:db_insert_placeholder_4] => teszt [:db_insert_placeholder_5] => 1 [:db_insert_placeholder_6] => 1 [:db_insert_placeholder_7] => 1455816984 [:db_insert_placeholder_8] => 1455816992 [:db_insert_placeholder_9] => 1 [:db_insert_placeholder_10] => 0 [:db_insert_placeholder_11] => 0 [:db_insert_placeholder_12] => 1 [:db_insert_placeholder_13] => 1 ) in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (Zeile 757 in /var/www/.d8/node-options/drupal/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

)

kevinquillen’s picture

I think your errors pertain to how fields are created and then available... I had the same issues when writing the code that created the fields.

Basically, you need:

      $manager = \Drupal::entityDefinitionUpdateManager();

      // create the field definition for the node
      $storage_definition = BaseFieldDefinition::create('boolean')
        ->setLabel(t('@label', ['@label' => $label]))
        ->setDescription(t('@description', ['@description' => $description]))
        ->setRevisionable(TRUE)
        ->setTranslatable(TRUE)
        ->setDefaultValue(0)
        ->setDisplayConfigurable('form', true);

      $manager->installFieldStorageDefinition($id, 'node', 'mymodule', $storage_definition);

That should create the field and update the node_field_data table.

As for the grouping(s) it is kind of a pain that "Manage Form Display" does nothing for the containers, this disconnect is somewhat confusing. I think it should be totally controlled from the UI, but I digress.

I've updated the code so that we create our own fieldset in the event that node promotions changes for whatever reason in the future.

$custom_publish_options = false;

  foreach ($entities as $machine_name => $entity) {
    if (in_array($entity->id(), $form_keys)) {
      $form[$entity->id()]['#group'] = 'custom_publish_options';
      $form[$entity->id()]['#access'] = $user->hasPermission('can set node publish state to ' . $entity->id());
      $custom_publish_options = true;
    }
  }

  // show the fieldset if we have options the user can use.
  if ($custom_publish_options) {
    $form['custom_publish_options'] = array(
      '#type' => 'details',
      '#title' => t('Custom Publish Options'),
      '#group' => 'advanced',
      '#attributes' => array(
        'class' => array('node-form-custom-publish-options'),
      ),
      '#weight' => 100,
      '#optional' => TRUE,
    );
  }
Daniel Schaefer’s picture

FileSize
10.97 KB

Okay. So I've implemented my findings into the module. My initial testing was successful. You just add the custom_pub to the form display like usual and it will show within the promotion options group.

Only local images are allowed.

I don't know how to make a patch so I'll attach the patched module instead.

Maybe with the help of my findings regarding the database we can manage to eliminate the step of attaching the field to a node form.

EDIT: Just now saw your earlier post. Thanks for the hint. I'll get back to it another time. I think with my last patch we are fine for now?

kevinquillen’s picture

So the area I was talking about pertains to adding options to certain node types only, it does not seem that the field storage definitions supports passing multiple bundles in, it only accepts one. So right now, the node type options when creating a publishing option virtually do nothing.

I don't recognize what version your patch is from, try using the latest dev release. That said, custom options now go into their own fieldset to avoid any issues with node core.

Daniel Schaefer’s picture

I'm sorry. It seems we kinda talked past each other here.

Daniel Schaefer’s picture

I obviously didn't know about the dedicated fieldset for CPO. I've used alpha2. Will check out your latest version soon.

kevinquillen’s picture

I just pushed another change to the dev branch that takes care of setting options into the node form upon creating them.

I'll publish an alpha3 shortly, I don;t see a way to list the dev branch anymore as a download.

Daniel Schaefer’s picture

Cool, can't wait to see it!

I still like the idea of having the custom options listed with the core ones but I understand the issue. I'll give the new version a try soon and get back to you.

kevinquillen’s picture

Issue summary: View changes
kevinquillen’s picture

alpha4 is up... you can add configurable action(s) to set an option value on a node. Haven't figured out how to autocreate them yet when creating options, but it at least gets people going with Actions.

kevinquillen’s picture

I attempted to implement rules action support in the latest dev release.. and while the rules action shows up in the list of actions to perform, unfortunately Rules is still in an unstable build and this area of the UI is not completed so I can't really go much further.

kevinquillen’s picture

Status: Active » Fixed
kevinquillen’s picture

Marking as fixed as the main objective has been completed.

Specific features should now be broken out into issues.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.