From f657bd9794d3f1660ffe3178852907ca54225afd Mon Sep 17 00:00:00 2001 From: Attila Fulop <1162360+fulopattila122@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:01:23 +0300 Subject: [PATCH] Added new property value getter and setter (replace) methods --- Changelog.md | 5 ++ src/Properties/Changelog.md | 5 ++ src/Properties/Contracts/PropertyValue.php | 12 ++- src/Properties/Models/PropertyValue.php | 29 +++++++- src/Properties/Models/PropertyValueProxy.php | 5 ++ .../Tests/ModelPropertyValuesTest.php | 73 +++++++++++++++++++ src/Properties/Tests/PropertyValueTest.php | 44 +++++++++++ src/Properties/Traits/HasPropertyValues.php | 53 ++++++++++---- 8 files changed, 211 insertions(+), 15 deletions(-) diff --git a/Changelog.md b/Changelog.md index f76cf75b..599acd39 100644 --- a/Changelog.md +++ b/Changelog.md @@ -66,6 +66,11 @@ - Added the `AdjusterAliases` class that for decoupling FQCNs from the database - Added automatic mapping of adjuster FQCN <-> aliases when saving an adjustment into the DB and when calling the `getAdjuster()` method - Added the `itemsPreAdjustmentTotal()` method to the Foundation's adjustable Cart model +- Added the `replacePropertyValues()` and `replacePropertyValuesByScalar()` methods to the `HasPropertyValues` trait +- BC: Added the following methods to the `PropertyValue` interface: + - `findByPropertyAndValue()` + - `getByScalarPropertiesAndValues()` +- BC: Added the mixed return type to the `getCastedValue` method of the `PropertyValue` interface - BC: Added the `findBySku()` method to the `Product` and `MasterProductVariant` interfaces - BC: The `MasterProduct` interface no longer extends the `Product` interface - BC: The `Checkout` interface now extends the `ArrayAccess` and the `Shippable` interfaces (until here, only the concrete classes have implementation it) diff --git a/src/Properties/Changelog.md b/src/Properties/Changelog.md index dbec27e7..12992a56 100644 --- a/src/Properties/Changelog.md +++ b/src/Properties/Changelog.md @@ -12,6 +12,11 @@ - Added Laravel 11 Support - Changed minimum Laravel version to v10.43 - Changed the behavior of assignPropertyValues/assignPropertyValue methods so that it throws an `UnknownPropertyException` when passing an inexistent property slug +- Added the `replacePropertyValues()` and `replacePropertyValuesByScalar()` methods to the `HasPropertyValues` trait +- BC: Added the following methods to the `PropertyValue` interface: + - `findByPropertyAndValue()` + - `getByScalarPropertiesAndValues()` +- BC: Added the mixed return type to the `getCastedValue` method of the `PropertyValue` interface ## 3.x Series diff --git a/src/Properties/Contracts/PropertyValue.php b/src/Properties/Contracts/PropertyValue.php index 9ac90520..7efd77bc 100644 --- a/src/Properties/Contracts/PropertyValue.php +++ b/src/Properties/Contracts/PropertyValue.php @@ -14,10 +14,20 @@ namespace Vanilo\Properties\Contracts; +use Illuminate\Database\Eloquent\Collection; + interface PropertyValue { /** * Returns the transformed value according to the underlying type */ - public function getCastedValue(); + public function getCastedValue(): mixed; + + public static function findByPropertyAndValue(string $propertySlug, mixed $value): ?PropertyValue; + + /** + * @example ['color' => 'blue', 'shape' => 'heart'] + * @param array $conditions The keys of the entries = the property slug, the values = the scalar property value + */ + public static function getByScalarPropertiesAndValues(array $conditions): Collection; } diff --git a/src/Properties/Models/PropertyValue.php b/src/Properties/Models/PropertyValue.php index 0d9b488c..76b8b6ed 100644 --- a/src/Properties/Models/PropertyValue.php +++ b/src/Properties/Models/PropertyValue.php @@ -17,6 +17,7 @@ use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\SluggableScopeHelpers; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Vanilo\Properties\Contracts\PropertyValue as PropertyValueContract; @@ -55,6 +56,32 @@ public static function findByPropertyAndValue(string $propertySlug, mixed $value return static::byProperty($property)->whereSlug($value)->first(); } + /** + * @example ['color' => 'blue', 'shape' => 'heart'] + * @param array $conditions The keys of the entries = the property slug, the values = the scalar property value + */ + public static function getByScalarPropertiesAndValues(array $conditions): Collection + { + if (empty($conditions)) { + return new Collection([]); + } + + $query = self::query() + ->select('property_values.*') + ->join('properties', 'property_values.property_id', '=', 'properties.id'); + + $count = 0; + foreach ($conditions as $property => $value) { + match ($count) { + 0 => $query->where(fn($q) => $q->where('properties.slug', '=', $property)->where('property_values.value', '=', $value)), + default => $query->orWhere(fn($q) => $q->where('properties.slug', '=', $property)->where('property_values.value', '=', $value)), + }; + $count++; + } + + return $query->get(); + } + public function property(): BelongsTo { return $this->belongsTo(PropertyProxy::modelClass()); @@ -80,7 +107,7 @@ public function scopeByProperty($query, $property) /** * Returns the transformed value according to the underlying type */ - public function getCastedValue() + public function getCastedValue(): mixed { return $this->property->getType()->transformValue((string) $this->value, $this->settings); } diff --git a/src/Properties/Models/PropertyValueProxy.php b/src/Properties/Models/PropertyValueProxy.php index f433b069..5510040c 100644 --- a/src/Properties/Models/PropertyValueProxy.php +++ b/src/Properties/Models/PropertyValueProxy.php @@ -14,8 +14,13 @@ namespace Vanilo\Properties\Models; +use Illuminate\Database\Eloquent\Collection; use Konekt\Concord\Proxies\ModelProxy; +/** + * @method static PropertyValue|null findByPropertyAndValue(string $propertySlug, mixed $value) + * @method static Collection getByScalarPropertiesAndValues(array $conditions) + */ class PropertyValueProxy extends ModelProxy { } diff --git a/src/Properties/Tests/ModelPropertyValuesTest.php b/src/Properties/Tests/ModelPropertyValuesTest.php index efbd1fa0..0dc31814 100644 --- a/src/Properties/Tests/ModelPropertyValuesTest.php +++ b/src/Properties/Tests/ModelPropertyValuesTest.php @@ -147,6 +147,79 @@ public function multiple_values_can_be_assigned_to_a_model_by_scalar_value() $this->assertEquals('Hua lu guy', $sword->valueOfProperty('shape')->getCastedValue()); } + /** @test */ + public function multiple_entries_can_be_replaced_by_scalar_key_value_pairs_at_once() + { + $wheel = Product::create(['name' => 'Wheel']); + $finish = Property::create(['name' => 'Finish', 'slug' => 'finish', 'type' => 'text']); + $diameter = Property::create(['name' => 'Diameter', 'slug' => 'diameter', 'type' => 'integer']); + $brand = Property::create(['name' => 'Brand', 'slug' => 'brand', 'type' => 'text']); + $finish->propertyValues()->createMany([ + ['title' => 'Glossy', 'value' => 'glossy'], + ['title' => 'Matte', 'value' => 'matte'], + ['title' => 'Cube', 'value' => 'cube'], + ]); + $diameter->propertyValues()->createMany([ + ['title' => '16"', 'value' => 16], + ['title' => '17"', 'value' => 17], + ['title' => '18"', 'value' => 18], + ]); + $brand->propertyValues()->createMany([ + ['title' => 'Dezent', 'value' => 'dezent'], + ['title' => 'Carmani', 'value' => 'carmani'], + ['title' => 'Borbet', 'value' => 'borbet'], + ['title' => 'ATS', 'value' => 'ats'], + ]); + + $wheel->replacePropertyValuesByScalar(['finish' => 'glossy', 'diameter' => 16]); + + $this->assertEquals(16, $wheel->valueOfProperty('diameter')->getCastedValue()); + $this->assertEquals('glossy', $wheel->valueOfProperty('finish')->getCastedValue()); + $this->assertNull($wheel->valueOfProperty('brand')); + + $wheel->replacePropertyValuesByScalar(['finish' => 'matte', 'diameter' => 17]); + $wheel->refresh(); + + $this->assertEquals(17, $wheel->valueOfProperty('diameter')->getCastedValue()); + $this->assertEquals('matte', $wheel->valueOfProperty('finish')->getCastedValue()); + $this->assertNull($wheel->valueOfProperty('brand')); + + $wheel->replacePropertyValuesByScalar(['diameter' => 17, 'brand' => 'ats']); + $wheel->refresh(); + + $this->assertEquals(17, $wheel->valueOfProperty('diameter')->getCastedValue()); + $this->assertNull($wheel->valueOfProperty('finish')); + $this->assertEquals('ats', $wheel->valueOfProperty('brand')->getCastedValue()); + + $wheel->replacePropertyValuesByScalar(['diameter' => 18, 'brand' => 'ats', 'finish' => 'matte']); + $wheel->refresh(); + + $this->assertEquals(18, $wheel->valueOfProperty('diameter')->getCastedValue()); + $this->assertEquals('matte', $wheel->valueOfProperty('finish')->getCastedValue()); + $this->assertEquals('ats', $wheel->valueOfProperty('brand')->getCastedValue()); + + $wheel->replacePropertyValuesByScalar([]); + $wheel->refresh(); + + $this->assertNull($wheel->valueOfProperty('finish')); + $this->assertNull($wheel->valueOfProperty('brand')); + $this->assertNull($wheel->valueOfProperty('diameter')); + } + + /** @test */ + public function replace_by_scalar_will_create_new_values_if_necessary() + { + $tyre = Product::create(['name' => 'Tyre']); + $origin = Property::create(['name' => 'Origin', 'slug' => 'origin', 'type' => 'text']); + $origin->propertyValues()->createMany([ + ['title' => 'China', 'value' => 'china'], + ['title' => 'India', 'value' => 'india'], + ]); + + $tyre->replacePropertyValuesByScalar(['origin' => 'canada']); + $this->assertEquals('canada', $tyre->fresh()->valueOfProperty('origin')?->getCastedValue()); + } + /** @test */ public function attempting_to_assign_values_with_inexistent_properties_throws_an_exception() { diff --git a/src/Properties/Tests/PropertyValueTest.php b/src/Properties/Tests/PropertyValueTest.php index c35ed13c..28a29892 100644 --- a/src/Properties/Tests/PropertyValueTest.php +++ b/src/Properties/Tests/PropertyValueTest.php @@ -173,4 +173,48 @@ public function the_property_and_value_finder_returns_null_when_attempting_to_lo { $this->assertNull(PropertyValue::findByPropertyAndValue('hey-i-am-so-stupid', 'gold')); } + + /** @test */ + public function multiple_entries_can_be_returned_by_scalar_key_value_pairs() + { + $shape = Property::create(['name' => 'Shape', 'type' => 'text']); + $material = Property::create(['name' => 'Material', 'type' => 'text']); + $shape->propertyValues()->createMany([ + ['title' => 'Heart', 'value' => 'heart'], + ['title' => 'Sphere', 'value' => 'sphere'], + ['title' => 'Cube', 'value' => 'cube'], + ]); + $material->propertyValues()->createMany([ + ['title' => 'Wood', 'value' => 'wood'], + ['title' => 'Glass', 'value' => 'glass'], + ['title' => 'Metal', 'value' => 'metal'], + ['title' => 'Plastic', 'value' => 'plastic'], + ]); + + $values = PropertyValue::getByScalarPropertiesAndValues([ + 'shape' => 'heart', + 'material' => 'wood', + ]); + $this->assertCount(2, $values); + $this->assertInstanceOf(PropertyValue::class, $values[0]); + $this->assertInstanceOf(PropertyValue::class, $values[1]); + $this->assertContains('shape', $values->map->property->map->slug); + $this->assertContains('material', $values->map->property->map->slug); + $this->assertContains('heart', $values->map->value); + $this->assertContains('wood', $values->map->value); + } + + /** @test */ + public function it_returns_an_empty_resultset_if_not_values_get_passed_to_get_by_scalar_key_value_pairs() + { + $season = Property::create(['name' => 'Season', 'type' => 'text']); + $season->propertyValues()->createMany([ + ['title' => 'Winter'], + ['title' => 'Summer'], + ['title' => 'All Seasons'], + ]); + + $values = PropertyValue::getByScalarPropertiesAndValues([]); + $this->assertCount(0, $values); + } } diff --git a/src/Properties/Traits/HasPropertyValues.php b/src/Properties/Traits/HasPropertyValues.php index 75f39044..db264d72 100644 --- a/src/Properties/Traits/HasPropertyValues.php +++ b/src/Properties/Traits/HasPropertyValues.php @@ -31,19 +31,7 @@ public function assignPropertyValue(string|Property $property, mixed $value): vo return; } - $propertyValue = PropertyValueProxy::findByPropertyAndValue($property, $value); - if (null === $propertyValue) { - if (null === $propertyId = $property instanceof Property ? $property->id : PropertyProxy::findBySlug($property)?->id) { - throw UnknownPropertyException::createFromSlug($property); - } - $propertyValue = PropertyValueProxy::create([ - 'property_id' => $propertyId, - 'value' => $value, - 'title' => $value, - ]); - } - - $this->addPropertyValue($propertyValue); + $this->addPropertyValue($this->findOrCreateByPropertyValue($property, $value)); } public function assignPropertyValues(iterable $propertyValues): void @@ -53,6 +41,28 @@ public function assignPropertyValues(iterable $propertyValues): void } } + /** + * @param array The key of the array is the property slug, the value is the scalar property value + */ + public function replacePropertyValuesByScalar(array $propertyValues): void + { + $valuesToSet = PropertyValueProxy::getByScalarPropertiesAndValues($propertyValues); + if (count($propertyValues) !== count($valuesToSet)) { + foreach ($propertyValues as $property => $value) { + if (!$valuesToSet->contains(fn (PropertyValue $pv) => $pv->value == $value && $pv->property->slug === $property)) { + $valuesToSet[] = $this->findOrCreateByPropertyValue($property, $value); + } + } + } + + $this->replacePropertyValues(...$valuesToSet); + } + + public function replacePropertyValues(PropertyValue ...$propertyValues): void + { + $this->propertyValues()->sync(collect($propertyValues)->pluck('id')); + } + public function valueOfProperty(string|Property $property): ?PropertyValue { $propertySlug = is_string($property) ? $property : $property->slug; @@ -101,4 +111,21 @@ public function removePropertyValue(PropertyValue $propertyValue) { return $this->propertyValues()->detach($propertyValue); } + + protected function findOrCreateByPropertyValue(string|Property $property, mixed $value): PropertyValue + { + $result = PropertyValueProxy::findByPropertyAndValue($property, $value); + if (null === $result) { + if (null === $propertyId = $property instanceof Property ? $property->id : PropertyProxy::findBySlug($property)?->id) { + throw UnknownPropertyException::createFromSlug($property); + } + $result = PropertyValueProxy::create([ + 'property_id' => $propertyId, + 'value' => $value, + 'title' => $value, + ]); + } + + return $result; + } }