0

I'm currently setting up a site and using 2 custom content entities (generated with Drupal Console) :

  • A tenant, with some custom fields
  • A Building, with some custom fields

Theses two entities need a many to many relationship.

Here is my current status, I'd like to know if it is the valid way to do or if I'm implementing this badly.

I'd also like to know how to handle the update of an entity properly (if I add a tenant in a building, I need to update the tenant to add him to this building)

My issue is that if I create a new tenant, add him to an existing building, the ID of my tenant will be null since it is a new entity not saved yet. But I can't save my tenant yet since I may have to add others data to it first, or pass some validation...

A workaround would be to do the building mapping at last, and saving twice my tenant (one when setting basing fields so it can have an ID and one after setting building relationship).

I know that Doctrine can handle many to many relationship by referencing the entity when it is not saved and I'd like to know if their is a way to do it with Drupal properly.


/**
 * Defines the Tenant entity.
 *
 * @ingroup bnpp_re_business
 *
 * @ContentEntityType(
 *   id = "bnpp_re_business_tenant",
 *   label = @Translation("Tenant"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\bnpp_re_business\TenantListBuilder",
 *     "views_data" = "Drupal\bnpp_re_business\Entity\TenantViewsData",
 *     "translation" = "Drupal\bnpp_re_business\TenantTranslationHandler",
 *
 *     "access" = "Drupal\bnpp_re_business\TenantAccessControlHandler",
 *   },
 *   base_table = "bnpp_re_business_tenant",
 *   data_table = "bnpp_re_business_tenant_field_data",
 *   translatable = TRUE,
 *   admin_permission = "administer tenant entities",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *     "langcode" = "langcode",
 *     "published" = "status",
 *   },
 * )
 */
class Tenant extends ContentEntityBase implements TenantInterface {


  /**
   * {@inheritdoc}
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *
   * @return array|\Drupal\Core\Field\FieldDefinitionInterface[]
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);

    // ... some fields 

    $fields[self::BUILDING_CODE] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Buildings'))
      ->setDescription(t('The associated buildings.'))
      ->setSetting('target_type', 'bnpp_re_business_building')
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED);

    return $fields;
  }

  // ... some methods

  public function addBuilding(int $building_id) {
    $items = $this->get(self::BUILDING_CODE)->getValue();
    if (is_null($items) || !is_array($items)) {
      $items = [];
    }
    $data = ['target_id' => $building_id];
    if (!in_array($data, $items)) {
      $items[] = $data;
      $this->set(self::BUILDING_CODE, $items);
      $building = Building::load($building_id);
      // FIXME Here is the bug. If my tenant is new, it does not have an ID yet so this will be null.
      $building->addTenant($this->id());
    }
  }


}

/**
 * Defines the Building entity.
 *
 * @ingroup bnpp_re_business
 *
 * @ContentEntityType(
 *   id = "bnpp_re_business_building",
 *   label = @Translation("Building"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\bnpp_re_business\BuildingListBuilder",
 *     "views_data" = "Drupal\bnpp_re_business\Entity\BuildingViewsData",
 *     "translation" = "Drupal\bnpp_re_business\BuildingTranslationHandler",
 *
 *     "access" = "Drupal\bnpp_re_business\BuildingAccessControlHandler",
 *   },
 *   base_table = "bnpp_re_business_building",
 *   data_table = "bnpp_re_business_building_field_data",
 *   translatable = TRUE,
 *   admin_permission = "administer building entities",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *     "langcode" = "langcode",
 *     "published" = "status",
 *   },
 * )
 */
class Building extends ContentEntityBase implements BuildingInterface {


  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);

    // some fields ... 

    $fields[self::TENANT] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Tenants'))
      ->setDescription(t('The associated building.'))
      ->setSetting('target_type', 'bnpp_re_business_tenant')
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED);
    return $fields;
  }

  // some methods...

  public function addTenant($tenant_id) {
    $items = $this->get(self::TENANT)->getValue();
    if (is_null($items) || !is_array($items)) {
      $items = [];
    }
    $data = ['target_id' => $tenant_id];
    if (!in_array($data, $items)) {
      $items[] = $data;
      $this->set(self::Tenant, $items);
      // Since we only call this method from addBuilding (Tenant Entity) method, we save the building.
      $this->save();
    }
  }

}

Thanks by advance for your help !

2
  • The entity reference field can handle new unsaved entities. just add the entity to the field and save the parent entity.
    – 4uk4
    Oct 10, 2019 at 11:03
  • so instead of $items = $this->get(self::TENANT)->getValue(); if (is_null($items) || !is_array($items)) { $items = []; } $data = ['target_id' => $tenant_id]; if (!in_array($data, $items)) { $items[] = $data; $this->set(self::Tenant, $items); } If could be doing this ? $items = $this->get(self::TENANT)->getValue(); if (is_null($items) || !is_array($items)) { $items = []; } if (!in_array($data, $items)) { $items[] = $data; $this->set(self::Tenant, $items); } and passing the entity as $data ?
    – Sylvain
    Oct 10, 2019 at 11:12

1 Answer 1

0

I think you need a "many-to-many" helper table like "TenantBuilding" and that must be a custom entity also. TenantBuilding then contains reference fields to Tenant and Building.

This is because each entity in Drupal is (more or less) a table in your DB. And there's no such SQL Syntax to describe many2many relations with just two tables.

Then, when you save your entities, encapsulate the creation of all of them in a Drupal::database()->startTransaction().

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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