diff --git a/src/Services/EntityMergers/EntityMerger.php b/src/Services/EntityMergers/EntityMerger.php new file mode 100644 index 00000000..7dcc91af --- /dev/null +++ b/src/Services/EntityMergers/EntityMerger.php @@ -0,0 +1,29 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers; + +class EntityMerger +{ + +} \ No newline at end of file diff --git a/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php b/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php new file mode 100644 index 00000000..0d9cf751 --- /dev/null +++ b/src/Services/EntityMergers/Mergers/EntityMergerHelperTrait.php @@ -0,0 +1,185 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers\Mergers; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Service\Attribute\Required; + +/** + * 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); + } + + /** + * 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 + { + $this->useCallback( + function ($target_value, $other_value) { + return $target_value ?? $other_value; + }, + $target, + $other, + $field + ); + + return $target; + } + + /** + * 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 + { + $this->useCallback( + function ($target_value, $other_value) { + return empty($target_value) ? $other_value : $target_value; + }, + $target, + $other, + $field + ); + + return $target; + } + + /** + * 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 + { + $this->useCallback( + function ($target_value, $other_value) { + return max($target_value, $other_value); + }, + $target, + $other, + $field + ); + + return $target; + } + + /** + * 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 + { + $this->useCallback( + function ($target_value, $other_value) { + return min($target_value, $other_value); + }, + $target, + $other, + $field + ); + + return $target; + } + + /** + * 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 + * @return object + */ + protected function mergeCollections(object $target, object $other, string $field): 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) { + $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; + } +} \ No newline at end of file diff --git a/src/Services/EntityMergers/Mergers/EntityMergerInterface.php b/src/Services/EntityMergers/Mergers/EntityMergerInterface.php new file mode 100644 index 00000000..9c8d9eb9 --- /dev/null +++ b/src/Services/EntityMergers/Mergers/EntityMergerInterface.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers\Mergers; + + +interface EntityMergerInterface +{ + /** + * Determines if this merger supports merging the other entity into the target entity. + * @param object $target + * @param object $other + * @param array $context + * @return bool True if this merger supports merging the other entity into the target entity, false otherwise + */ + public function supports(object $target, object $other, array $context = []): bool; + + /** + * Merge the other entity into the target entity. + * The target entity will be modified and returned. + * @param object $target + * @param object $other + * @param array $context + * @return object + */ + public function merge(object $target, object $other, array $context = []): object; +} \ No newline at end of file diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php new file mode 100644 index 00000000..9fa5da05 --- /dev/null +++ b/src/Services/EntityMergers/Mergers/PartMerger.php @@ -0,0 +1,51 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\EntityMergers\Mergers; + +use App\Entity\Parts\Part; +use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; + +#[Autoconfigure(public: true)] +class PartMerger implements EntityMergerInterface +{ + + use EntityMergerHelperTrait; + + public function supports(object $target, object $other, array $context = []): bool + { + return $target instanceof Part && $other instanceof Part; + } + + public function merge(object $target, object $other, array $context = []): object + { + if (!$target instanceof Part || !$other instanceof Part) { + throw new \InvalidArgumentException('The target and the other entity must be instances of Part'); + } + + //Merge the fields + $this->mergeCollections($target, $other, 'partLots'); + + return $target; + } +} \ No newline at end of file diff --git a/tests/Services/EntityMergers/Mergers/PartMergerTest.php b/tests/Services/EntityMergers/Mergers/PartMergerTest.php new file mode 100644 index 00000000..c9b7956c --- /dev/null +++ b/tests/Services/EntityMergers/Mergers/PartMergerTest.php @@ -0,0 +1,89 @@ +. + */ + +namespace App\Tests\Services\EntityMergers\Mergers; + +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use App\Services\EntityMergers\Mergers\PartMerger; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + +class PartMergerTest extends KernelTestCase +{ + + /** @var PartMerger|null */ + protected ?PartMerger $merger = null; + + protected function setUp(): void + { + self::bootKernel(); + $this->merger = self::getContainer()->get(PartMerger::class); + } + + /** + * This test also functions as test for EntityMergerHelperTrait::mergeCollections() so its pretty long. + * @return void + */ + public function testMergePartLots() + { + $lot1 = (new PartLot())->setAmount(2)->setNeedsRefill(true); + $lot2 = (new PartLot())->setInstockUnknown(true)->setVendorBarcode('test'); + $lot3 = (new PartLot())->setDescription('lot3')->setAmount(3); + $lot4 = (new PartLot())->setDescription('lot4')->setComment('comment'); + + $part1 = (new Part()) + ->setName('Part 1') + ->addPartLot($lot1) + ->addPartLot($lot2); + + $part2 = (new Part()) + ->setName('Part 2') + ->addPartLot($lot3) + ->addPartLot($lot4); + + $merged = $this->merger->merge($part1, $part2); + + $this->assertInstanceOf(Part::class, $merged); + //We should now have all 4 lots + $this->assertCount(4, $merged->getPartLots()); + + //The existing lots should be the same instance as before + $this->assertSame($lot1, $merged->getPartLots()->get(0)); + $this->assertSame($lot2, $merged->getPartLots()->get(1)); + //While the new lots should be new instances + $this->assertNotSame($lot3, $merged->getPartLots()->get(2)); + $this->assertNotSame($lot4, $merged->getPartLots()->get(3)); + + //But the new lots, should be assigned to the target part and contain the same info + $clone3 = $merged->getPartLots()->get(2); + $clone4 = $merged->getPartLots()->get(3); + $this->assertSame($merged, $clone3->getPart()); + $this->assertSame($merged, $clone4->getPart()); + + } + + public function testSupports() + { + $this->assertFalse($this->merger->supports(new \stdClass(), new \stdClass())); + $this->assertFalse($this->merger->supports(new \stdClass(), new Part())); + $this->assertTrue($this->merger->supports(new Part(), new Part())); + } +}