cbr-1.0.0/cbr.module
cbr.module
<?php
use Drupal\cbr\Plugin\Field\FieldType\CBRCaseStatus;
use Drupal\cbr\Plugin\Field\FieldType\CBRFieldInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
//** Entity-API hooks **/
/** Implements hook_entity_insert()
* @param EntityInterface $entity The entity being inserted.
*/
function cbr_node_insert(EntityInterface $entity)
{
if ($entity->getEntityTypeId() == 'node') {
/** @var \Drupal\node\NodeInterface $entity */
cbr_set_case_status($entity);
if (cbr_is_enabled_on_entity($entity) && cbr_number_of_nodes($entity->getType()) > 1) {
// cbr_calculate_similarity($entity); //Uncomment to calculate similarity on insert directly
cbr_add_to_calculation_queue($entity);
}
}
}
/** Implements hook_entity_update()
* @param EntityInterface $entity The entity being updated.
*/
function cbr_node_update(EntityInterface $entity)
{
if ($entity->getEntityTypeId() == 'node') {
/** @var \Drupal\node\NodeInterface $entity */
if (cbr_is_enabled_on_entity($entity) && cbr_number_of_nodes($entity->getType()) > 1) {
// cbr_calculate_similarity($entity); //Uncomment to recalculate similarity on update directly
cbr_add_to_calculation_queue($entity);
}
}
}
/** Implements hook_entity_delete()
* @param EntityInterface $entity The entity being deleted.
*/
function cbr_node_delete(EntityInterface $entity)
{
if ($entity->getEntityTypeId() == 'node') {
//Delete similarity
$query = \Drupal::database()->delete('cbr_similarity');
$query->condition($query->orConditionGroup()->condition('nid1', $entity->id())->condition('nid2', $entity->id()));
$query->execute();
//Delete from calculation queue
$query = \Drupal::database()->delete('cbr_calculation_queue');
$query->condition($query->orConditionGroup()->condition('nid', $entity->id())->condition('nid', $entity->id()));
$query->execute();
}
}
/**
* Implements hook_form_node_form_alter()
* Add a second submit button to the node form to save the node and calculate the similarity.
*/
function cbr_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id)
{
if (cbr_is_enabled_on_entity($form_state->getFormObject()->getEntity())) {
$form['actions']['save_and_calculate_similarity'] = [
'#button_type' => 'primary',
'#type' => 'submit',
'#value' => t('Save and calculate similarity'),
'#weight' => 1,
'#submit' => ['::submitForm', '::save', 'cbr_submit_node_form'],
];
}
return $form;
}
function cbr_submit_node_form(array &$form, FormStateInterface $form_state)
{
if (cbr_number_of_nodes($form_state->getFormObject()->getEntity()->getType()) > 1) {
cbr_calculate_similarity($form_state->getFormObject()->getEntity(), true);
}
}
function cbr_add_to_calculation_queue(EntityInterface $entity)
{
$query = \Drupal::database()->merge('cbr_calculation_queue');
$query->key('nid', $entity->id());
$query->fields(['updated' => time()]);
$query->execute();
}
function cbr_cron()
{
$cbr_similarity_calculator = \Drupal::service('cbr.similarity_calculator');
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$logger = \Drupal::logger('cbr');
$query = \Drupal::database()->select('cbr_calculation_queue', 'c');
$query->fields('c', ['nid']);
$query->orderBy('updated', 'DESC');
$query->range(0, 100); //limit to 100 cases at a cron run to avoid timeouts
$nids = $query->execute()->fetchCol();
$done = [];
foreach ($nids as $nid) {
$entity = $node_storage->load($nid);
if ($entity) {
//Get all others Node-IDs
$other_nids = \Drupal::entityQuery('node')
->condition('nid', $entity->id(), "<>")
->condition('type', $entity->bundle())
->execute();
//Split other nids into chunks of 10
$other_nids_chunks = array_chunk($other_nids, 10);
foreach ($other_nids_chunks as $other_nids_chunk) {
$entities = $node_storage->loadMultiple($other_nids_chunk);
//Reset memory cache to avoid memory issues
$node_storage->resetCache($other_nids_chunk);
//loop through all entities of chunk and calculate similarity
foreach ($entities as $other_entity) {
//Skip if already calculated
if ($done[$entity->id()][$other_entity->id()] ?? false) {
continue;
}
$similarity = $cbr_similarity_calculator->calculate($entity, $other_entity);
$cbr_similarity_calculator->saveSimilarity($entity->id(), $other_entity->id(), $similarity);
$done[$entity->id()][$other_entity->id()] = true;
$done[$other_entity->id()][$entity->id()] = true;
}
}
}
$logger->info('Calculated similarity between node nid @nid and @count other nodes', ['@nid' => $entity->id(), '@count' => count($other_nids)]);
}
}
/** Returns true, if an entity has CBR enabled
*
* @param EntityInterface $entity The entity
* @return True, if CBR is active on this content type or false, if not
*/
function cbr_is_enabled_on_entity(EntityInterface $entity): bool
{
$has_cbr_field = false;
foreach ($entity->getFields(false) as $field_item) {
if ($field_item->first() instanceof CBRFieldInterface) {
$has_cbr_field = true;
break;
}
}
$has_cbr_status_field = false;
foreach ($entity->getFieldDefinitions() as $field_item) {
if ($field_item->getType() == 'cbr_case_status') {
$has_cbr_status_field = true;
break;
}
}
if ($has_cbr_field && !$has_cbr_status_field) {
\Drupal::messenger()->addWarning(t('This content type has CBR fields, but no CBR status field was found. Please add a CBR status field.', []));
\Drupal::logger('cbr')->warning('CBR fields on @type, but no CBR status field is defined.', ['@type' => $entity->getEntityTypeId()]);
}
return $has_cbr_status_field;
}
/**
* Return the number of nodes of a given type.
* @param string $type The type of nodes to count.
*/
function cbr_number_of_nodes(string $type): int
{
$connection = \Drupal::database();
$query = $connection->select('node_field_data', 'n');
$query->addExpression('COUNT(*)');
$query->condition('n.type', $type);
return $query->execute()->fetchField();
}
/**
* Set the case status of an entity.
* @param EntityInterface $entity The entity to set the status for.
*/
function cbr_set_case_status(EntityInterface $entity)
{
foreach ($entity->getFieldDefinitions() as $field_item) {
if ($field_item->getType() == 'cbr_case_status' && !isset($entity->get($field_item->getName())->value)) {
$entity->get($field_item->getName())->setValue(CBRCaseStatus::CASE_STATUS_NEW);
$entity->save();
break;
}
}
}
/**
* Start the calculation of the similarity of a given entity. This will start a new batch job.
* @param EntityInterface $entity The entity to calculate the similarity for.
*/
function cbr_calculate_similarity(EntityInterface $entity)
{
$batch = [
'operations' => [
['cbr_batch_calculation', [$entity, \Drupal::routeMatch()->getRouteName()]]
],
'finished' => 'cbr_batch_finished',
'title' => t('Processing CBR calculation'),
'init_message' => t('CBR calculation is starting.'),
'progress_message' => t('Processed @current out of @total.'),
'error_message' => t('CBR core calculate has encountered an error.'),
];
batch_set($batch);
}
/**
* Batch callback to calculate the similarity of a given entity.
* @param EntityInterface $entity The entity to calculate the similarity for.
* @param string $src_route The current route.
* @param array $context The batch context.
*/
function cbr_batch_calculation(EntityInterface $entity, $src_route, &$context)
{
//Set some information on first batch call
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_node'] = 0;
$context['sandbox']['sum_weight'] = 0;
$context['results']['nid'] = $entity->id();
$context['results']['src_route'] = $src_route;
$context['results']['start_time'] = microtime(TRUE);
//Get all Node-IDs
$context['sandbox']['nids'] = \Drupal::entityQuery('node')
->condition('nid', $entity->id(), "<>")
->condition('type', $entity->bundle())
->execute();
$context['sandbox']['max'] = count($context['sandbox']['nids']);
}
$cbr_similarity_calculator = \Drupal::service('cbr.similarity_calculator');
// With each pass through the callback, we calculate the similarity of one node to another node of the same type.
$nid2 = array_shift($context['sandbox']['nids']);
if ($nid2 != NULL && !isset($context['results']['similarity'][$entity->id()][$nid2])) {
$entity2 = \Drupal::EntityTypeManager()->getStorage('node')->load($nid2);
$similarity = $cbr_similarity_calculator->calculate($entity, $entity2);
$context['results']['similarity'][$entity->id()][$entity2->id()] = $similarity;
$context['results']['similarity'][$entity2->id()][$entity->id()] = $similarity;
$cbr_similarity_calculator->saveSimilarity($entity->id(), $entity2->id(), $similarity);
}
//If nids is empty, we are done
if (count($context['sandbox']['nids']) == 0) {
$context['finished'] = 1;
} else {
// Update our progress information.
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
$context['sandbox']['progress']++;
$context['message'] = t('Now processing Node @nid', array('@nid' => $context['sandbox']['nids'][0]));
}
}
/**
* Batch 'finished' callback
* @param bool $success Whether the batch was successful.
* @param array $results The results of the batch.
* @param array $operations The operations that were run.
*/
function cbr_batch_finished($success, $results, $operations)
{
if ($success) {
if ($results['src_route'] != 'cbr.merge') {
$count = count($results['similarity']);
//druation of batch process in seconds with two decimals
$duration = round((microtime(TRUE) - $results['start_time']) * 100) / 100;
Drupal::messenger()->addMessage(t('Calculated similarity for @count cases in @duration seconds.', ['@count' => $count, '@duration' => $duration]));
}
} else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
Drupal::messenger()
->addMessage(t('An error occurred while processing @operation with arguments: @args', [
'@operation' => $error_operation[0],
'@args' => print_r($error_operation[0], TRUE),
]));
}
// Redirect to the list of cases
$link_to_similar_cases = \Drupal\Core\Url::fromUri("internal:/node/{$results['nid']}/similar-cases");
return new RedirectResponse($link_to_similar_cases->toString());
}
/**
* Views hook to show the similarity via the views module.
*/
function cbr_views_data()
{
//Field CBR_SIMILARITY
$data = [];
$data['cbr_similarity'] = [];
$data['cbr_similarity']['table']['group'] = t('Case Based Reasoning');
$data['cbr_similarity']['table']['join'] = [
'node_field_data' => [
'left_field' => 'nid',
'field' => 'nid2',
],
];
$data['cbr_similarity']['nid1'] = array(
'title' => t('Base Case Node ID'),
'help' => t('Base case for which similar cases should be found.'),
'argument' => [
// ID of argument handler plugin to use.
'id' => 'numeric',
]
);
$data['cbr_similarity']['similarity'] = [
'title' => t('Similarity'),
'help' => t('Similarity between two cases. A higher similarity means a more similar case.'),
'field' => [
// ID of field handler plugin to use.
'id' => 'numeric',
],
'sort' => [
// ID of sort handler plugin to use.
'id' => 'standard',
],
'filter' => [
// ID of filter handler plugin to use.
'id' => 'numeric',
]
];
return $data;
}