cbr-1.0.0/src/Plugin/Field/FieldType/CBRTaxonomyReferenceField.php
src/Plugin/Field/FieldType/CBRTaxonomyReferenceField.php
<?php
namespace Drupal\cbr\Plugin\Field\FieldType;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldConfig;
/**
* Defines the 'cbr_taxonomy_reference' entity field type.
*
* @FieldType(
* id = "cbr_taxonomy_reference",
* label = @Translation("CBR Taxonomy reference"),
* description = @Translation("An entity field containing an entity reference."),
* category = @Translation("Case Based Reasoning"),
* default_widget = "cbr_entity_reference_autocomplete",
* default_formatter = "cbr_entity_reference_label",
* list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList",
* )
*/
class CBRTaxonomyReferenceField extends EntityReferenceItem implements CBRFieldInterface
{
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state): array
{
$form = parent::fieldSettingsForm($form, $form_state) + CBRFieldHelper::cbrFieldSettingsForm($form, $form_state);
/** @var FieldConfig $field_config */
$field_config = $form_state->getFormObject()->getEntity();
$form['cbr_settings']['similarity_function'] = [
'#type' => 'select',
'#title' => t('Similarity function'),
'#description' => t('Select the similarity function to use for this field. <br>
• "Jaccard" - Use jaccard if you reference multiple taxonomy terms. <br>
• "Hierarchy distance" - Use hierarchy distance if your terms are organized in a hierarchy.'),
'#options' => [
'jaccard' => t('Jaccard'),
'hierarchy_distance' => t('Hierarchy distance'),
],
'#default_value' => $field_config->getThirdPartySetting('cbr', 'similarity_function', 'jaccard'),
'#required' => true
];
$form['#entity_builders'][] = [$this, 'saveCBRFieldTaxonomySettings'];
return $form;
}
/**
* Entity builder callback to save the similarity function.
* @param $entity_type The entity type.
* @param FieldConfig $field_config The field config.
* @param $form The form array.
* @param FormStateInterface $form_state The form state.
*/
public static function saveCBRFieldTaxonomySettings($entity_type, FieldConfig $field_config, &$form, FormStateInterface $form_state)
{
$field_config->setThirdPartySetting('cbr', 'similarity_function', $form_state->getValue(['settings', 'cbr_settings', 'similarity_function']));
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data): array
{
$element['target_type'] = [
'#type' => 'select',
'#title' => t('Type of item to reference'),
'#default_value' => 'taxonomy_term',
'#options' => [
'taxonomy_term' => t('Taxonomy term'),
],
'#required' => TRUE,
'#disabled' => TRUE,
'#size' => 1,
];
return $element;
}
/**
* {@inheritdoc}
*/
public static function getPreconfiguredOptions(): array
{
$options = [];
return $options;
}
/**
* {@inheritdoc}
*/
public function calculateSimilarity($tids1, $tids2, FieldConfig $field_config): float
{
$similarity_function = $field_config->getThirdPartySetting('cbr', 'similarity_function', 'jaccard');
switch ($similarity_function) {
case 'jaccard':
return $this->calculateJaccardSimilarity($tids1, $tids2);
case 'hierarchy_distance':
return $this->calculateHierarchyDistanceSimilarity($tids1, $tids2);
default:
die("Unknown similarity function: " . $similarity_function);
return 0; // should never happen
}
}
public function summerize(array $values): array
{
$merged = [];
foreach ($values as $value) {
$merged = array_merge($merged, $value);
}
return array_unique($merged);
}
public function getValueForSimilarityCalculation(FieldConfig $field_config): array
{
$tids = [];
foreach ($this->parent as $field) {
$tids[] = $field->getValue()['target_id'];
}
return $tids;
}
private function calculateJaccardSimilarity($tids1, $tids2): float
{
//return 0, if no referenced entities
if (empty($tids1)) {
return 0;
}
//return 0, if no referenced entities
if (empty($tids2)) {
return 0;
}
//calculate jaccard similarity
$tids1 = array_unique($tids1);
$tids2 = array_unique($tids2);
$intersection = array_intersect($tids1, $tids2);
$union = array_unique(array_merge($tids1, $tids2));
if (empty($union)) {
return 0;
}
return count($intersection) / count($union);
}
private function calculateHierarchyDistanceSimilarity($tids1, $tids2): float
{
//return 0, if no referenced entities
if (empty($tids1)) {
return 0;
}
//return 0, if no referenced entities
if (empty($tids2)) {
return 0;
}
//compare each elemet of tids1 with each element of tids2
$hierarchy_distance = 0;
foreach ($tids1 as $tid1) {
foreach ($tids2 as $tid2) {
$hierarchy_distance += $this->getHierarchyDistance($tid1, $tid2);
}
}
return $hierarchy_distance / (count($tids1) * count($tids2));
}
private function getHierarchyDistance($tid1, $tid2): float
{
if ($tid1 == $tid2) {
return 1;
}
//get all parents of term 1
$parents1 = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadAllParents($tid1);
$parents1 = array_keys($parents1);
//get all parents of term 2
$parents2 = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadAllParents($tid2);
$parents2 = array_keys($parents2);
if (empty($parents1)) {
return count($parents2);
}
if (empty($parents2)) {
return count($parents1);
}
//Select the last element of the parents array
//This is the root of the tree
$term1 = end($parents1);
$term2 = end($parents2);
//While both terms are the same, keep going down the tree
while ($term1 == $term2) {
array_pop($parents1);
array_pop($parents2);
$term1 = end($parents1);
$term2 = end($parents2);
}
//We found the Lowest Common Ancestor (LCA)!
//The distance is the number of edges between the two terms
$distance = count($parents1) + count($parents2);
return 1 / ($distance + 1);
}
}