scanner-8.x-1.0-rc3/src/Plugin/Scanner/Paragraph.php
src/Plugin/Scanner/Paragraph.php
<?php
namespace Drupal\scanner\Plugin\Scanner;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* A Scanner plugin for handling Paragraph entities.
*
* @Scanner(
* id = "scanner_paragraph",
* type = "paragraph",
* )
*/
class Paragraph extends Entity {
use LoggerChannelTrait;
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function search(string $field, array $values): array {
$title_collect = [];
try {
[, $bundle, $fieldname] = explode(':', $field);
$query = $this->entityTypeManager->getStorage('paragraph')->getQuery();
$query->condition('type', $bundle);
if ($values['published']) {
$query->condition('status', 1);
}
$conditionVals = parent::buildCondition($values['search'], $values['mode'], $values['wholeword'], $values['regex'], $values['preceded'], $values['followed']);
$this->addQueryCondition($query, $conditionVals, $fieldname, $values['mode'], $values['language']);
// Disable the normal access check.
$query->accessCheck(FALSE);
$entities = $query->execute();
if (!empty($entities)) {
// Load the paragraph(s) which match the criteria.
$paragraphs = $this->entityTypeManager->getStorage('paragraph')
->loadMultiple($entities);
// Iterate over matched paragraphs to extract information that will be
// rendered in the results.
foreach ($paragraphs as $paragraph) {
if (!empty($paragraph)) {
// Load the entity the paragraph is referenced in.
$parentEntity = $paragraph->getParentEntity();
if (!empty($parentEntity)) {
if (!method_exists($parentEntity, 'getParentEntity')) {
continue;
}
$parentEntityType = $parentEntity->getEntityTypeId();
// In the case of nested relationships we need to find the base
// entity.
if ($parentEntityType != 'node') {
// If child is only nested one level deep.
if ($parentEntity->getParentEntity()->getEntityTypeId() == 'node') {
$parentEntity = $parentEntity->getParentEntity();
}
// Two or more levels of nesting.
else {
while ($parentEntity->getParentEntity()->getEntityTypeId() != 'node') {
$parentEntity = $parentEntity->getParentEntity();
}
}
}
$id = $parentEntity->id();
// Get the value of the specified field.
$paraField = $paragraph->get($fieldname);
$fieldType = $paraField->getFieldDefinition()->getType();
if (in_array($fieldType, [
'text_with_summary',
'text',
'text_long',
])) {
// Get the value of the field.
$fieldValue = $paraField->getValue()[0];
// Get the parent entity's title.
$title_collect[$id]['title'] = $parentEntity->getTitle();
// Find all instances of the term we're looking for.
preg_match_all($conditionVals['phpRegex'], $fieldValue['value'], $matches, PREG_OFFSET_CAPTURE);
$newValues = [];
// Build an array of strings which are displayed in the results
// with the searched term bolded.
foreach ($matches[0] as $v) {
// The offset of the matched term(s) in the field's text.
$start = $v[1];
if ($values['preceded'] !== '') {
// Bolding won't work if starting position is in the middle
// of a word (non-word bounded searches), therefore we move
// the start position back as many character as there are in
// the 'preceded' text.
$start -= strlen($values['preceded']);
}
$replaced = preg_replace($conditionVals['phpRegex'], "<strong>$v[0]</strong>", preg_split("/\s+/", substr($fieldValue['value'], $start), 6));
if (count($replaced) > 1) {
// The final index contains the remainder of the text, which
// we don't care about, so we discard it.
array_pop($replaced);
}
$newValues[] = implode(' ', $replaced);
}
$title_collect[$id]['field'] = $newValues;
}
elseif (in_array($fieldType, ['string', 'link'])) {
$title_collect[$id]['title'] = $parentEntity->getTitle();
preg_match($conditionVals['phpRegex'], $paraField->getString(), $matches, PREG_OFFSET_CAPTURE);
$match = $matches[0][0];
$replaced = preg_replace($conditionVals['phpRegex'], "<strong>$match</strong>", $paraField->getString());
$title_collect[$id]['field'] = [$replaced];
}
}
}
}
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
$this->getLogger('scanner')->error($e->getMessage());
}
return $title_collect;
}
/**
* {@inheritdoc}
*/
public function replace(string $field, array $values, array $undo_data): array {
[$entityType, $bundle, $fieldname] = explode(':', $field);
$data = $undo_data;
try {
$query = $this->entityTypeManager->getStorage($entityType)->getQuery();
$query->condition('type', $bundle);
if ($values['published']) {
$query->condition('status', 1);
}
$conditionVals = parent::buildCondition($values['search'], $values['mode'], $values['wholeword'], $values['regex'], $values['preceded'], $values['followed']);
$this->addQueryCondition($query, $conditionVals, $fieldname, $values['mode'], $values['language']);
// Disable the normal access check.
$query->accessCheck(FALSE);
$entities = $query->execute();
$paragraphs = $this->entityTypeManager->getStorage('paragraph')
->loadMultiple($entities);
foreach ($paragraphs as $pid => $paragraph) {
$paraField = $paragraph->get($fieldname);
$fieldType = $paraField->getFieldDefinition()->getType();
if (in_array($fieldType, ['text_with_summary', 'text', 'text_long'])) {
$fieldValue = $paraField->getValue()[0];
$fieldValue['value'] = preg_replace($conditionVals['phpRegex'], $values['replace'], $fieldValue['value']);
$paragraph->{$fieldname} = $fieldValue;
if (!isset($data["paragraph:$pid"]['new_vid'])) {
$data["paragraph:$pid"]['old_vid'] = $paragraph->getRevisionId();
// Create a new revision for the paragraph.
$paragraph->setNewRevision(TRUE);
}
// Save the paragraph with the updated field(s).
$paragraph->save();
$data["paragraph:$pid"]['new_vid'] = $paragraph->getRevisionId();
$this->handleParentRelationship($paragraph, $values, $data);
}
elseif ($fieldType == 'string') {
$fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $paraField->getString());
$paragraph->$fieldname = $fieldValue;
if (!isset($data["paragraph:$pid"]['new_vid'])) {
$data["paragraph:$pid"]['old_vid'] = $paragraph->getRevisionId();
$paragraph->setNewRevision(TRUE);
}
$paragraph->save();
$data["paragraph:$pid"]['new_vid'] = $paragraph->getRevisionId();
$this->handleParentRelationship($paragraph, $values, $data);
}
elseif ($fieldType == 'link') {
$new_value = [];
foreach ($paraField->getValue() as $delta => $field_value) {
foreach ($field_value as $field_element => $field_element_value) {
if (is_string($field_element_value)) {
$fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $field_element_value);
}
else {
$fieldValue = $field_element_value;
}
$new_value[$delta][$field_element] = $fieldValue;
}
}
$paragraph->$fieldname = $new_value;
if (!isset($data["paragraph:$pid"]['new_vid'])) {
$data["paragraph:$pid"]['old_vid'] = $paragraph->getRevisionId();
$paragraph->setNewRevision(TRUE);
}
$paragraph->save();
$data["paragraph:$pid"]['new_vid'] = $paragraph->getRevisionId();
$this->handleParentRelationship($paragraph, $values, $data);
}
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException | EntityStorageException $e) {
$this->getLogger('scanner')->error($e->getMessage());
}
return $data;
}
/**
* {@inheritdoc}
*/
public function undo(array $data): void {
try {
// Load the specified paragraph revision.
$paraRevision = $this->entityTypeManager->getStorage('paragraph')->loadRevision($data['old_vid']);
$paraRevision->setNewRevision(TRUE);
// Set this revision as the current/default revision.
$paraRevision->isDefaultRevision(TRUE);
$paraRevision->save();
}
catch (InvalidPluginDefinitionException | PluginNotFoundException | EntityStorageException $e) {
$this->getLogger('scanner')->error($e->getMessage());
}
}
/**
* Helper function to handle entity reference relationships.
*
* @param mixed $paragraph
* The paragraph entity.
* @param array $values
* An array containing the input values from the form.
* @param array $data
* An array containing the revision id's for the entities being processed.
*
* @return bool
* A boolean which denotes whether we were able to process the parent
* entity(s).
*/
protected function handleParentRelationship(mixed $paragraph, array $values, array &$data): bool {
$pid = $paragraph->id();
$parentEntity = $paragraph->getParentEntity();
if (empty($parentEntity)) {
return FALSE;
}
$id = $parentEntity->id();
$parentEntityType = $parentEntity->getEntityTypeId();
if ($parentEntityType == 'node') {
$parentField = $paragraph->get('parent_field_name')->getString();
$index = $this->getMultiValueIndex($parentEntity->$parentField->getValue(), $pid);
// Orphaned paragraphs cause issues, so we skip them (and their
// relationships).
if ($index < 0) {
$this->getLogger('scanner')->notice('Unable to find the delta for this paragraph in the parent entity\'s field (id: @id).', ['@id' => $pid]);
return FALSE;
}
if (!isset($data["node:$id"]['new_vid'])) {
$data["node:$id"]['old_vid'] = $parentEntity->getRevisionId();
// Create a new revision for the parent entity.
$parentEntity->setNewRevision(TRUE);
$parentEntity->revision_log = $this->t('Replaced @search with @replace via Scanner Search and Replace module.', [
'@search' => $values['search'],
'@replace' => $values['replace'],
]);
if (method_exists($parentEntity, 'setRevisionUserId')) {
$parentEntity->setRevisionUserId($this->currentUser->id());
}
}
// We need to update the parent entity as well so that it will display
// the lastest revision.
$parentEntity->$parentField->set($index, [
'target_id' => $pid,
'target_revision_id' => $paragraph->getRevisionId(),
]);
$parentEntity->save();
$data["node:$id"]['new_vid'] = $parentEntity->getRevisionId();
return TRUE;
}
elseif ($parentEntityType == 'paragraph') {
$grandParentEntity = $parentEntity->getParentEntity();
// Bail out if grandparent isn't a node, we only handle two levels of
// nesting.
if ($grandParentEntity->getEntityTypeId() != 'node') {
return FALSE;
}
$parentField = $paragraph->get('parent_field_name')->getString();
$index = $this->getMultiValueIndex($parentEntity->$parentField->getValue(), $pid);
// Orphaned paragraphs cause issues, so we skip them (and their
// relationships).
if ($index < 0) {
$this->getLogger('scanner')->notice('Unable to find the delta for this paragraph in the parent entity\'s field (id: @id).', ['@id' => $pid]);
return FALSE;
}
// Handle parent entity.
if (!isset($data["paragraph:$id"]['new_vid'])) {
$data["paragraph:$id"]['old_vid'] = $parentEntity->getRevisionId();
// Create a new revision for the paragraph.
$parentEntity->setNewRevision(TRUE);
if (method_exists($parentEntity, 'setRevisionUserId')) {
$parentEntity->setRevisionUserId($this->currentUser->id());
}
}
$parentEntity->$parentField->set($index, [
'target_id' => $paragraph->id(),
'target_revision_id' => $paragraph->getRevisionId(),
]);
$parentEntity->save();
$data["paragraph:$id"]['new_vid'] = $parentEntity->getRevisionId();
// Handle grandparent entity.
$grandParentId = $grandParentEntity->id();
$grandParentField = $parentEntity->get('parent_field_name')->getString();
$index = $this->getMultiValueIndex($grandParentEntity->$grandParentField->getValue(), $id);
// Orphaned paragraphs can cause issues so we skip them.
if ($index < 0) {
$this->getLogger('scanner')->notice('Unable to find the delta for this paragraph in the parent entity\'s field (id: @id).', ['@id' => $id]);
return FALSE;
}
if (!isset($data["node:$grandParentId"]['new_vid'])) {
$data["node:$grandParentId"]['old_vid'] = $grandParentEntity->getRevisionId();
// Create a new revision for the paragraph.
$grandParentEntity->setNewRevision(TRUE);
$grandParentEntity->revision_log = $this->t('Replaced @search with @replace via Scanner Search and Replace module.', [
'@search' => $values['search'],
'@replace' => $values['replace'],
]);
if (method_exists($grandParentEntity, 'setRevisionUserId')) {
$grandParentEntity->setRevisionUserId($this->currentUser->id());
}
}
$grandParentEntity->$grandParentField->set($index, [
'target_id' => $parentEntity->id(),
'target_revision_id' => $parentEntity->getRevisionId(),
]);
$grandParentEntity->save();
$data["node:$grandParentId"]['new_vid'] = $grandParentEntity->getRevisionId();
return TRUE;
}
else {
// Something we didn't expect.
return FALSE;
}
}
/**
* Get multiple value index.
*/
protected function getMultiValueIndex($values, $pid): int|string {
foreach ($values as $key => $value) {
if ($value['target_id'] == $pid) {
return $key;
}
}
return -1;
}
}
