0

I'm building a Drupal 8 module which will create several content types (with several fields) when the module is installed. The content types, field storages field instances, form displays & view displays are created with yml files in mymodule/config/install.

More details on the content types and fields:

  • Let there are two node types, A and B.
  • B has field called reference which is required, can have 1 value and the reference is to a node of type A. So basically B always has exactly one parent of type A.

What I want to do is to make the reference field read-only after the node is created.

In Drupal 7 I did this with Form API and hook_node_validate basically as follows:

  • I first checked if the form submission was for a new node or an existing node
  • If it was an existing node, I loaded the node (which is the node before the form submission at hand) and compared the value of the form submission to the value found on the actual node.
  • If the values were not the same, I returned an error.

Now, in Drupal 8 we have the Validation API which is really good thing because it kicks in with also other mechanisms than form submits. I'm only wondering how to implement the read-only locking described above.

Should I create a Constraint Plugin which would do similar loading of the previous version like I explained above and add that constraint with hook_entity_bundle_field_info_alter() or is there some other way?

Edit: Just to clarify, I'm interested to make the reference field non-editable on data level so that the value can't be changed even if updating the node programmatically. In other words, just making the field read-only on UI level on node edit form is not enough.

  • The best way is to use hook_node_presave and throw exception of values were changed. You could use a constraint but to my knowledge only Form API uses validate() method before calling save() on the entity. – user21641 May 30 '16 at 9:09
  • Hi, thanks for you input! I believe that calling validate() is the responsibility of the module that is actually making the changes to the entity. If some module doesn't do that before saving the entity, it is not behaving well. I'll post an example using Validation API as an example for others as another answer. – Markus Sipilä Jun 10 '16 at 10:33
0

After educating myself on the Validation API, here's an example how to add the constraint to validate that the value of an entity reference field has not been changed.

Note: As pointed out by Ivan Jaros, this constraint is not validated if some module is programmatically saving entities without validating the entities first via Validation API.

  1. Add a new constraint plugin.

    /**
     * @file
     * Contains \Drupal\example\Plugin\Validation\Constraint\ValueNotChangedConstraint.
     */
    
    namespace Drupal\example\Plugin\Validation\Constraint;
    
    use Symfony\Component\Validator\Constraint;
    
    /**
     * Constraint for checking that the value has not been changed.
     *
     * @Constraint(
     *   id = "ValueNotChanged",
     *   label = @Translation("Value has not changed", context = "Validation"),
     *   type = { "string", "entity:node" }
     * )
     */
    class ValueNotChangedConstraint extends Constraint {
    
      /**
       * Message shown when the validation fails.
       *
       * @var string
       */
      public $message = 'Reference value can not be changed.';
    
    }
    
  2. Add validator for this constraint.

    /**
     * @file
     * Contains \Drupal\example\Plugin\Validation\Constraint\ValueNotChangedConstraintValidator.
     */
    
    namespace Drupal\example\Plugin\Validation\Constraint;
    
    use Symfony\Component\Validator\Constraint;
    use Symfony\Component\Validator\ConstraintValidator;
    use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
    use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
    use Drupal\Core\Entity\EntityTypeManagerInterface;
    
    /**
     * Validates the ValueNotChangedConstraint.
     */
    class ValueNotChangedConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
    
      protected $entityTypeManager;
    
      /**
       * Constructor.
       *
       * @param EntityTypeManagerInterface $entity_type_manager
       *   EntityTypeManager service for loading entities.
       */
      public function __construct(EntityTypeManagerInterface $entity_type_manager) {
        $this->entityTypeManager  = $entity_type_manager;
      }
    
      /**
       * {@inheritdoc}
       */
      public static function create(ContainerInterface $container) {
        // Instantiates this class.
        return new static(
          // Load the service required to construct this class.
          $container->get('entity_type.manager')
        );
      }
    
      /**
       * Validator 2.5 and upwards compatible execution context.
       *
       * @var \Symfony\Component\Validator\Context\ExecutionContextInterface
       */
      protected $context;
    
      /**
       * {@inheritdoc}
       */
      public function validate($data, Constraint $constraint) {
        $violation = $this->validateValueNotChanged($data, $constraint);
        if ($violation instanceof ConstraintViolationBuilderInterface) {
          $violation->addViolation();
        }
        return;
      }
    
      /**
       * Validates that the value is the same than previously.
       *
       * @param EntityReferenceFieldItemListInterface $data
       *   List of referenced entities.
       * @param Constraint $constraint
       *   Our constraint.
       *
       * @return \Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface
       */
      protected function validateValueNotChanged(EntityReferenceFieldItemListInterface $data, Constraint $constraint) {
        // Get the field name of the field we're validating.
        $field_name = $data->getFieldDefinition()->get('field_name');
    
        // Get the entity the field belongs to.
        $parent_id = $data->getEntity()->id();
    
        // Entities that are about to be created don't have an ID at this point.
        // We're only interested in existing entities.
        if ($parent_id) {
    
          // Load the parent node from the database. The new updates are not saved
          // yet so this will be the "old" version of the entity.
          $parent = $this->entityTypeManager->getStorage('node')->load($parent_id);
    
          // Get the old value we want to compare against.
          $old_value = $parent->get($field_name);
    
          // Compare the field item lists.
          if (!$old_value->equals($data)) {
            return $this->context->buildViolation($constraint->message);
          }
        }
      }
    
    }
    
  3. Add the constraint to the desired field with hook_entity_bundle_field_info_alter().

    /**
     * Implemeantion of hook_entity_bundle_field_info_alter().
     *
     * Adds constraint to field_example_reference field.
     */
    function example_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle){
      switch ($bundle) {
        case 'our_content_type':
          $fields['field_example_reference']->addConstraint('ValueNotChanged');
          break;
      }
    }
    

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.