From 6f88d21721526c8838096e52984deb45c281c3a8 Mon Sep 17 00:00:00 2001 From: Jan-Paul Kleemans Date: Sat, 21 Nov 2020 15:30:21 +0100 Subject: [PATCH 1/6] Support json attributes --- src/AttributeEvents.php | 30 +++++++- test/AttributeEventsTest.php | 97 ++++++++++++++++++++++++ test/Fake/Events/InvoiceDownloaded.php | 12 +++ test/Fake/Events/OrderMetaUpdated.php | 12 +++ test/Fake/Events/PaypalPaymentDenied.php | 12 +++ test/Fake/Order.php | 5 ++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 test/Fake/Events/InvoiceDownloaded.php create mode 100644 test/Fake/Events/OrderMetaUpdated.php create mode 100644 test/Fake/Events/PaypalPaymentDenied.php diff --git a/src/AttributeEvents.php b/src/AttributeEvents.php index 81b4fbe..1a7ef31 100644 --- a/src/AttributeEvents.php +++ b/src/AttributeEvents.php @@ -2,7 +2,7 @@ namespace Kleemans; -use Illuminate\Support\Str; +use Illuminate\Support\{Arr, Str}; trait AttributeEvents { @@ -37,6 +37,19 @@ private function fireAttributeEvents(): void } } + // JSON attribute + elseif (Str::contains($attribute, '->')) { + [$attribute, $path] = explode('->', $attribute, 2); + $path = str_replace('->', '.', $path); + + if (!$this->isDirtyNested($attribute, $path)) { + continue; // Not changed + } + + $value = $this->getAttribute($attribute); + $value = Arr::get($value, $path); + } + // Regular attribute elseif (!$this->isDirty($attribute)) { continue; // Not changed @@ -85,6 +98,21 @@ public function isDirtyAccessor(string $attribute): bool return $originalValue !== $currentValue; } + public function isDirtyNested(string $attribute, string $path): bool + { + $original = $this->getOriginal($attribute); + $originalValue = Arr::get($original, $path); + + $current = $this->getAttribute($attribute); + $currentValue = Arr::get($current, $path); + + if ($currentValue === null) { + return false; + } + + return $originalValue !== $currentValue; + } + /** * @return array */ diff --git a/test/AttributeEventsTest.php b/test/AttributeEventsTest.php index 15360f3..b3b5969 100644 --- a/test/AttributeEventsTest.php +++ b/test/AttributeEventsTest.php @@ -27,6 +27,9 @@ class AttributeEventsTest extends TestCase Fake\Events\OrderShippingCountryChanged::class, Fake\Events\OrderPaid::class, Fake\Events\OrderPaidWithCash::class, + Fake\Events\OrderMetaUpdated::class, + Fake\Events\PaypalPaymentDenied::class, + Fake\Events\InvoiceDownloaded::class, ]; public function setUp(): void @@ -236,6 +239,8 @@ public function it_respects_withoutEvents() }); } + // Accessors + /** @test */ public function it_dispatches_event_on_accessor_change() { @@ -283,6 +288,96 @@ public function it_dispatches_event_on_change_of_accessor_for_existing_attribute $this->dispatcher->assertDispatched(Fake\Events\OrderPaidWithCash::class); } + // JSON attributes + + /** @test */ + public function it_dispatches_event_when_updating_json_attribute() + { + $order = new Fake\Order(); + $order->meta = ['gift_wrapping' => true]; + $order->save(); + + $order->meta = ['gift_wrapping' => false]; + $order->save(); + + $this->dispatcher->assertDispatched(Fake\Events\OrderMetaUpdated::class); + } + + /** @test */ + public function it_dispatches_event_when_updating_json_field() + { + $order = new Fake\Order(); + $order->meta = ['paypal_status' => 'pending']; + $order->save(); + + $meta = $order->meta; + $meta['paypal_status'] = 'denied'; + $order->meta = $meta; + + $order->save(); + + $this->dispatcher->assertDispatched(Fake\Events\PaypalPaymentDenied::class); + } + + /** @test */ + public function it_works_with_json_changes_through_update_method() + { + $order = new Fake\Order(); + $order->meta = ['paypal_status' => 'pending']; + $order->save(); + + $order->update(['meta->paypal_status' => 'denied']); + + $this->dispatcher->assertDispatched(Fake\Events\PaypalPaymentDenied::class); + } + + /** @test */ + public function it_dispatches_event_when_adding_json_field() + { + $order = new Fake\Order(); + $order->meta = ['gift_wrapping' => true]; + $order->save(); + + $meta = $order->meta; + $meta['paypal_status'] = 'denied'; + $order->meta = $meta; + + $order->save(); + + $this->dispatcher->assertDispatched(Fake\Events\PaypalPaymentDenied::class); + } + + /** @test */ + public function it_does_not_dispatch_on_initial_value_of_json_attribute() + { + $order = new Fake\Order(); + $order->meta = [ + 'gift_wrapping' => true, + 'paypal_status' => 'pending', + ]; + $order->save(); + + $this->dispatcher->assertNotDispatched(Fake\Events\OrderMetaUpdated::class); + $this->dispatcher->assertNotDispatched(Fake\Events\PaypalPaymentDenied::class); + } + + /** @test */ + public function it_works_with_nested_json_fields() + { + $order = new Fake\Order(); + $order->meta = ['invoice' => ['downloaded' => false]]; + $order->save(); + + $meta = $order->meta; + $meta['invoice']['downloaded'] = true; + $order->meta = $meta; + + $order->save(); + + $this->dispatcher->assertDispatched(Fake\Events\OrderMetaUpdated::class); + $this->dispatcher->assertDispatched(Fake\Events\InvoiceDownloaded::class); + } + // Setup methods private function initEventDispatcher() @@ -319,6 +414,7 @@ private function migrate() $table->integer('discount_percentage'); $table->boolean('tax_free'); $table->string('payment_gateway'); + $table->json('meta'); $table->timestamps(); }); } @@ -336,6 +432,7 @@ private function seed() 'discount_percentage' => 0, 'tax_free' => false, 'payment_gateway' => 'credit_card', + 'meta' => '{}', ] ]); } diff --git a/test/Fake/Events/InvoiceDownloaded.php b/test/Fake/Events/InvoiceDownloaded.php new file mode 100644 index 0000000..2942d07 --- /dev/null +++ b/test/Fake/Events/InvoiceDownloaded.php @@ -0,0 +1,12 @@ + 0, 'tax_free' => false, 'payment_gateway' => 'credit_card', + 'meta' => '{}', ]; protected $guarded = []; @@ -27,6 +28,7 @@ class Order extends Model 'paid_amount' => 'float', 'discount_percentage' => 'integer', 'tax_free' => 'boolean', + 'meta' => 'array', ]; protected $dispatchesEvents = [ @@ -44,6 +46,9 @@ class Order extends Model 'shipping_country:*' => Events\OrderShippingCountryChanged::class, 'is_paid:true' => Events\OrderPaid::class, 'payment_gateway:cash' => Events\OrderPaidWithCash::class, + 'meta:*' => Events\OrderMetaUpdated::class, + 'meta->paypal_status:denied' => Events\PaypalPaymentDenied::class, + 'meta->invoice->downloaded:true' => Events\InvoiceDownloaded::class, ]; public function getShippingCountryAttribute(): string From eea1304b02247708b0fc64b102645e43da4b9e90 Mon Sep 17 00:00:00 2001 From: Jan-Paul Kleemans Date: Sat, 21 Nov 2020 15:36:28 +0100 Subject: [PATCH 2/6] Fix lint issue --- src/AttributeEvents.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AttributeEvents.php b/src/AttributeEvents.php index 1a7ef31..3038f82 100644 --- a/src/AttributeEvents.php +++ b/src/AttributeEvents.php @@ -2,7 +2,8 @@ namespace Kleemans; -use Illuminate\Support\{Arr, Str}; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; trait AttributeEvents { From aa171a072c7e9d085b23c99a68601b79141a1d36 Mon Sep 17 00:00:00 2001 From: Jan-Paul Kleemans Date: Sat, 21 Nov 2020 15:46:21 +0100 Subject: [PATCH 3/6] Add documentation for JSON attributes --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index d27039b..cd142de 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,15 @@ Echo.channel('orders') }) ``` +## JSON attributes +For attributes stored as JSON, you can use the `->` operator: + +```php +protected $dispatchesEvents = [ + 'payment->status:completed' => PaymentCompleted::class, +]; +``` + ## Accessors For more complex state changes, you can use attributes defined by an accessor: From d1ed8d752cb43dad233d53b253e4c54ca7026c01 Mon Sep 17 00:00:00 2001 From: Jan-Paul Kleemans Date: Sat, 21 Nov 2020 15:48:17 +0100 Subject: [PATCH 4/6] Small change --- src/AttributeEvents.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AttributeEvents.php b/src/AttributeEvents.php index 3038f82..748c5b4 100644 --- a/src/AttributeEvents.php +++ b/src/AttributeEvents.php @@ -47,8 +47,7 @@ private function fireAttributeEvents(): void continue; // Not changed } - $value = $this->getAttribute($attribute); - $value = Arr::get($value, $path); + $value = Arr::get($this->getAttribute($attribute), $path); } // Regular attribute From d801c424cd33548991843ade247f52194acf1223 Mon Sep 17 00:00:00 2001 From: Jan-Paul Kleemans Date: Sat, 21 Nov 2020 15:49:39 +0100 Subject: [PATCH 5/6] Small change --- src/AttributeEvents.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/AttributeEvents.php b/src/AttributeEvents.php index 748c5b4..0f63906 100644 --- a/src/AttributeEvents.php +++ b/src/AttributeEvents.php @@ -100,11 +100,8 @@ public function isDirtyAccessor(string $attribute): bool public function isDirtyNested(string $attribute, string $path): bool { - $original = $this->getOriginal($attribute); - $originalValue = Arr::get($original, $path); - - $current = $this->getAttribute($attribute); - $currentValue = Arr::get($current, $path); + $originalValue = Arr::get($this->getOriginal($attribute), $path); + $currentValue = Arr::get($this->getAttribute($attribute), $path); if ($currentValue === null) { return false; From 817c820166498f4776be9ec02e7730e8954a7da5 Mon Sep 17 00:00:00 2001 From: Jan-Paul Kleemans Date: Sat, 21 Nov 2020 15:51:18 +0100 Subject: [PATCH 6/6] Fix test --- test/AttributeEventsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AttributeEventsTest.php b/test/AttributeEventsTest.php index b3b5969..73c7b4f 100644 --- a/test/AttributeEventsTest.php +++ b/test/AttributeEventsTest.php @@ -353,7 +353,7 @@ public function it_does_not_dispatch_on_initial_value_of_json_attribute() $order = new Fake\Order(); $order->meta = [ 'gift_wrapping' => true, - 'paypal_status' => 'pending', + 'paypal_status' => 'denied', ]; $order->save();