Part-DB.Part-DB-server/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php
Treeed 29f92d9bd3
Split attachment paths (#848)
* fixed attachment statistics for sqlite

* Split attachment path into internal and external path, so the external source URL can be retained after a file is downloaded

* Make internal and external path for attachments nullable, to make clear that they have no internal or external path

* Added migrations for nullable columns for postgres and mysql

* Added migration for nullable internal and external pathes for sqlite

* Added translations

* Fixed upload error

* Restrict length of filename badge in attachment edit view

* Improved margins with badges in attachment edit

* Added a link to view external version from attachment edit

* Let media_url  stay in API attachments responses for backward compatibility

---------

Co-authored-by: jona <a@b.c>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-02-22 17:29:14 +01:00

358 lines
No EOL
13 KiB
PHP

<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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(
fn($target_value, $other_value) => $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(
fn($target_value, $other_value) => 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(
fn($target_value, $other_value) => 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(
fn($target_value, $other_value) => 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(
fn(bool $target_value, bool $other_value): bool => $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;
}
}
} elseif ($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', fn(Attachment $t, Attachment $o): bool => $t->getName() === $o->getName()
&& $t->getAttachmentType() === $o->getAttachmentType()
&& $t->getExternalPath() === $o->getExternalPath()
&& $t->getInternalPath() === $o->getInternalPath());
}
/**
* 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', fn(AbstractParameter $t, AbstractParameter $o): bool => $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<b>%s:</b>\n%s",
trim($t),
$other->getName(),
trim($o)
);
},
$target,
$other,
$field
);
}
}