. */ declare(strict_types=1); namespace App\Services\EntityMergers\Mergers; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Part; use Doctrine\Common\Collections\Collection; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Contracts\Service\Attribute\Required; use function Symfony\Component\String\u; /** * This trait provides helper methods for entity mergers. * By default, it uses the value from the target entity, unless it not fullfills a condition. */ trait EntityMergerHelperTrait { protected PropertyAccessorInterface $property_accessor; #[Required] public function setPropertyAccessor(PropertyAccessorInterface $property_accessor): void { $this->property_accessor = $property_accessor; } /** * Choice the value to use from the target or the other entity by using a callback function. * * @param callable $callback The callback to use. The signature is: function($target_value, $other_value, $target, $other, $field). The callback should return the value to use. * @param object $target The target entity * @param object $other The other entity * @param string $field The field to use * @return object The target entity with the value set */ protected function useCallback(callable $callback, object $target, object $other, string $field): object { //Get the values from the entities $target_value = $this->property_accessor->getValue($target, $field); $other_value = $this->property_accessor->getValue($other, $field); //Call the callback, with the signature: function($target_value, $other_value, $target, $other, $field) //The callback should return the value to use $value = $callback($target_value, $other_value, $target, $other, $field); //Set the value $this->property_accessor->setValue($target, $field, $value); return $target; } /** * Use the value from the other entity, if the value from the target entity is null. * * @param object $target The target entity * @param object $other The other entity * @param string $field The field to use * @return object The target entity with the value set */ protected function useOtherValueIfNotNull(object $target, object $other, string $field): object { return $this->useCallback( function ($target_value, $other_value) { return $target_value ?? $other_value; }, $target, $other, $field ); } /** * Use the value from the other entity, if the value from the target entity is empty. * * @param object $target The target entity * @param object $other The other entity * @param string $field The field to use * @return object The target entity with the value set */ protected function useOtherValueIfNotEmtpy(object $target, object $other, string $field): object { return $this->useCallback( function ($target_value, $other_value) { return empty($target_value) ? $other_value : $target_value; }, $target, $other, $field ); } /** * Use the larger value from the target and the other entity for the given field. * * @param object $target * @param object $other * @param string $field * @return object */ protected function useLargerValue(object $target, object $other, string $field): object { return $this->useCallback( function ($target_value, $other_value) { return max($target_value, $other_value); }, $target, $other, $field ); } /** * Use the smaller value from the target and the other entity for the given field. * * @param object $target * @param object $other * @param string $field * @return object */ protected function useSmallerValue(object $target, object $other, string $field): object { return $this->useCallback( function ($target_value, $other_value) { return min($target_value, $other_value); }, $target, $other, $field ); } /** * Perform an OR operation on the boolean values from the target and the other entity for the given field. * This effectively means that the value is true, if it is true in at least one of the entities. * @param object $target * @param object $other * @param string $field * @return object */ protected function useTrueValue(object $target, object $other, string $field): object { return $this->useCallback( function (bool $target_value, bool $other_value): bool { return $target_value || $other_value; }, $target, $other, $field ); } /** * Perform a merge of comma separated lists from the target and the other entity for the given field. * The values are merged and duplicates are removed. * @param object $target * @param object $other * @param string $field * @return object */ protected function mergeTags(object $target, object $other, string $field, string $separator = ','): object { return $this->useCallback( function (string|null $t, string|null $o) use ($separator): string { //Explode the strings into arrays $t_array = explode($separator, $t ?? ''); $o_array = explode($separator, $o ?? ''); //Merge the arrays and remove duplicates $tmp = array_unique(array_merge($t_array, $o_array)); //Implode the array back to a string return implode($separator, $tmp); }, $target, $other, $field ); } /** * Merge the collections from the target and the other entity for the given field and put all items into the target collection. * @param object $target * @param object $other * @param string $field * @param callable|null $equal_fn A function, which checks if two items are equal. The signature is: function(object $target, object other): bool. * Return true if the items are equal, false otherwise. If two items are equal, the item from the other collection is not added to the target collection. * If null, the items are compared by (instance) identity. * @return object */ protected function mergeCollections(object $target, object $other, string $field, ?callable $equal_fn = null): object { $target_collection = $this->property_accessor->getValue($target, $field); $other_collection = $this->property_accessor->getValue($other, $field); if (!$target_collection instanceof Collection) { throw new \InvalidArgumentException("The target field $field is not a collection"); } //Clone the items from the other collection $clones = []; foreach ($other_collection as $item) { //Check if the item is already in the target collection if ($equal_fn !== null) { foreach ($target_collection as $target_item) { if ($equal_fn($target_item, $item)) { continue 2; } } } else { if ($target_collection->contains($item)) { continue; } } $clones[] = clone $item; } $tmp = array_merge($target_collection->toArray(), $clones); //Create a new collection with the clones and merge it into the target collection $this->property_accessor->setValue($target, $field, $tmp); return $target; } /** * Merge the attachments from the target and the other entity. * @param AttachmentContainingDBElement $target * @param AttachmentContainingDBElement $other * @return object */ protected function mergeAttachments(AttachmentContainingDBElement $target, AttachmentContainingDBElement $other): object { return $this->mergeCollections($target, $other, 'attachments', function (Attachment $t, Attachment $o): bool { return $t->getName() === $o->getName() && $t->getAttachmentType() === $o->getAttachmentType() && $t->getPath() === $o->getPath(); }); } /** * Merge the parameters from the target and the other entity. * @param AbstractStructuralDBElement|Part $target * @param AbstractStructuralDBElement|Part $other * @return object */ protected function mergeParameters(AbstractStructuralDBElement|Part $target, AbstractStructuralDBElement|Part $other): object { return $this->mergeCollections($target, $other, 'parameters', function (AbstractParameter $t, AbstractParameter $o): bool { return $t->getName() === $o->getName() && $t->getSymbol() === $o->getSymbol() && $t->getUnit() === $o->getUnit() && $t->getValueMax() === $o->getValueMax() && $t->getValueMin() === $o->getValueMin() && $t->getValueTypical() === $o->getValueTypical() && $t->getValueText() === $o->getValueText() && $t->getGroup() === $o->getGroup(); }); } /** * Check if the two strings have equal content. * This method is case-insensitive and ignores whitespace. * @param string|\Stringable $t * @param string|\Stringable $o * @return bool */ protected function areStringsEqual(string|\Stringable $t, string|\Stringable $o): bool { $t_str = u($t)->trim()->folded(); $o_str = u($o)->trim()->folded(); return $t_str->equalsTo($o_str); } /** * Merge the text from the target and the other entity for the given field by attaching the other text to the target text via the given separator. * For example, if the target text is "Hello" and the other text is "World", the result is "Hello / World". * If the text is the same in both entities, the target text is returned. * @param object $target * @param object $other * @param string $field * @param string $separator * @return object */ protected function mergeTextWithSeparator(object $target, object $other, string $field, string $separator = ' / '): object { return $this->useCallback( function (string $t, string $o) use ($separator): string { //Check if the strings are equal if ($this->areStringsEqual($t, $o)) { return $t; } //Skip empty strings if (trim($t) === '') { return trim($o); } if (trim($o) === '') { return trim($t); } return trim($t) . $separator . trim($o); }, $target, $other, $field ); } /** * Merge two comments from the target and the other entity for the given field. * The comments of the both entities get concated, while the second part get a headline with the name of the old part. * @param AbstractNamedDBElement $target * @param AbstractNamedDBElement $other * @param string $field * @return object */ protected function mergeComment(AbstractNamedDBElement $target, AbstractNamedDBElement $other, string $field = 'comment'): object { return $this->useCallback( function (string $t, string $o) use ($other): string { //Check if the strings are equal if ($this->areStringsEqual($t, $o)) { return $t; } //Skip empty strings if (trim($t) === '') { return trim($o); } if (trim($o) === '') { return trim($t); } return sprintf("%s\n\n%s:\n%s", trim($t), $other->getName(), trim($o) ); }, $target, $other, $field ); } }