mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-07-25 13:24:48 +02:00
* 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>
358 lines
No EOL
13 KiB
PHP
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
|
|
);
|
|
}
|
|
} |