Skip to content

Commit

Permalink
Merge branch 'main' into outside-listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
inxilpro authored Dec 18, 2024
2 parents 125c584 + b178c35 commit 318d44f
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 13 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
"require": {
"php": ">=8.1",
"glhd/bits": ">=0.3.0",
"illuminate/contracts": "^10.0|^11.0",
"illuminate/contracts": "^10.34|^11.0",
"internachi/modular": "^2.0",
"laravel/prompts": "^0.1.15",
"laravel/prompts": "^0.1.15|^0.2|^0.3",
"spatie/laravel-package-tools": "^1.14.0",
"symfony/property-access": "^6.2|^7.0",
"symfony/serializer": "^6.3|^7.0"
Expand Down
2 changes: 2 additions & 0 deletions config/verbs.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
Expand Down Expand Up @@ -42,6 +43,7 @@
'normalizers' => [
SelfSerializingNormalizer::class,
CollectionNormalizer::class,
ArrayDenormalizer::class,
ModelNormalizer::class,
StateNormalizer::class,
BitsNormalizer::class,
Expand Down
8 changes: 8 additions & 0 deletions docs/ids.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ A helper method you can use to generate a snowflake right out of the box: `snowf
For models that you're going to manage via events, pull in the `HasSnowflakes` trait:

```php
use Glhd\Bits\Database\HasSnowflakes;
use Glhd\Bits\Snowflake;

class JobApplication extends Model
{
use HasSnowflakes; // Add this to your model

// Any attribute can be cast to a `Snowflake` (or `Sonyflake`)
protected $casts = [
'id' => Snowflake::class,
];
}
```

Expand Down
4 changes: 2 additions & 2 deletions docs/states.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ to track:

```php
// CountIncremented.php
class CountIncremented class extends Event
class CountIncremented extends Event
{
#[StateId(CountState::class)]
public int $example_id;
Expand Down Expand Up @@ -251,7 +251,7 @@ which frees up your models to better serve your frontfacing UI needs. Once you'v
your state instance's id to correspond directly to a model instance.

```php
class FooCreated class
class FooCreated
{
#[StateId(FooState::class)]
public int $foo_id;
Expand Down
4 changes: 2 additions & 2 deletions src/Lifecycle/SnapshotStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ protected function loadOne(Bits|UuidInterface|AbstractUid|int|string $id, string

$snapshots = VerbSnapshot::query()
->where('type', '=', $type)
->where('id', $id)
->where('state_id', $id)
->take(2)
->get();

Expand All @@ -96,7 +96,7 @@ protected function loadMany(Collection $ids, string $type): StateCollection

$states = VerbSnapshot::query()
->where('type', '=', $type)
->whereIn('id', $ids)
->whereIn('state_id', $ids)
->get()
->map(fn (VerbSnapshot $snapshot) => $snapshot->state());

Expand Down
15 changes: 10 additions & 5 deletions src/Support/EventStateRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ protected function isStateDiscoveryAttribute(ReflectionAttribute $attribute): bo
/** @return Collection<int, State> */
protected function getProperties(Event $target): Collection
{
return $this->discovered_properties[$target::class] ??= $this->findAllProperties($target);
return $this->discovered_properties[$target::class][$target->id] ??= $this->findAllProperties($target);
}

/** @return Collection<int, State> */
Expand All @@ -119,11 +119,16 @@ protected function findAllProperties(Event $target): Collection
$reflect = new ReflectionClass($target);

return collect($reflect->getProperties(ReflectionProperty::IS_PUBLIC))
->filter(function (ReflectionProperty $property) {
$propertyType = $property->getType()?->getName();
->filter(function (ReflectionProperty $property) use ($target) {
$propertyType = $property->getType();
$propertyTypeName = $propertyType?->getName();

return $propertyType
&& (is_subclass_of($propertyType, State::class) || $propertyType === State::class || $propertyType === StateCollection::class);
if ($propertyType->allowsNull() && $property->getValue($target) === null) {
return false;
}

return $propertyTypeName
&& (is_subclass_of($propertyTypeName, State::class) || $propertyTypeName === State::class || $propertyTypeName === StateCollection::class);
})
->map(fn (ReflectionProperty $property) => $property->getValue($target))
->flatten();
Expand Down
2 changes: 1 addition & 1 deletion src/Support/LilWayneLyrics.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class LilWayneLyrics
{
protected const LYRICS = [
'I need a Wynn-Dixie grocery bag full of money rig ',
'I need a Winn-Dixie grocery bag full of money rig ',
"You think you're calling shots, you got the wrong number. I love Benjamin Franklin more than his own mother.",
'I play the hand that was dealt, I got a deck full of aces. I gave birth to your style, I need a check for my labor',
"It ain't my birthday but I got my name on the cake",
Expand Down
8 changes: 7 additions & 1 deletion src/VerbsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use Illuminate\Support\DateFactory;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
Expand Down Expand Up @@ -107,7 +109,11 @@ public function packageRegistered()
: new AnnotationLoader;

return new PropertyNormalizer(
propertyTypeExtractor: new ReflectionExtractor,
propertyTypeExtractor: new PropertyInfoExtractor(
typeExtractors: [
new PhpDocExtractor,
new ReflectionExtractor,
]),
classDiscriminatorResolver: new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory($loader)),
);
});
Expand Down
22 changes: 22 additions & 0 deletions tests/Feature/SerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,19 @@ public function __construct()
expect($result)->toBe('{"__verbs_initialized":false,"name":"Demo"}');
});

it('allows us to store a serializable class(es) as a property', function () {
$original_event = new EventWithPhpDocArray;

$serialized_data = app(Serializer::class)->serialize($original_event);

expect($serialized_data)->toBe('{"dto":{"fqcn":"DTO","foo":1},"dtos":[{"fqcn":"DTO","foo":1}]}');

$deserialized_event = app(Serializer::class)->deserialize(EventWithPhpDocArray::class, $serialized_data);

expect($deserialized_event->dto)->toBeInstanceOf(DTO::class)
->and($deserialized_event->dtos[0])->toBeInstanceOf(DTO::class);
});

class EventWithConstructorPromotion extends Event
{
public function __construct(
Expand Down Expand Up @@ -195,3 +208,12 @@ public function __construct()
$this->constructed = true;
}
}

class EventWithPhpDocArray extends Event
{
public function __construct(
public DTO $dto = new DTO,
/** @var DTO[] $dtos */
public array $dtos = [new DTO]
) {}
}
48 changes: 48 additions & 0 deletions tests/Unit/SnapshotStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
use Glhd\Bits\Bits;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Uid\AbstractUid;
use Thunk\Verbs\Attributes\Autodiscovery\StateId;
use Thunk\Verbs\Contracts\StoresSnapshots;
use Thunk\Verbs\Event;
use Thunk\Verbs\Models\VerbSnapshot;
use Thunk\Verbs\State;

Expand All @@ -23,6 +25,32 @@
->and($snapshot1)->id->not()->toBe($snapshot2->id);
});

it('loads snapshots based on the state_id', function () {
$id = snowflake()->make();

SnapshotStoreTestEvent::commit(
state_id: $id,
name: 'event one',
);

$store = app(StoresSnapshots::class);

$state = $store->load($id, SnapshotStoreTestDifferentState::class);

expect($state)
->id->toBe($id->id())
->name->toBe('event one');

$states = $store->load([$id], SnapshotStoreTestDifferentState::class);

expect($states)
->count()->toBe(1);

expect($states->first())
->id->toBe($id->id())
->name->toBe('event one');
});

class SnapshotStoreTestStateOne extends State
{
public function __construct(
Expand All @@ -38,3 +66,23 @@ public function __construct(
public string $name,
) {}
}

class SnapshotStoreTestEvent extends Event
{
#[StateId(SnapshotStoreTestDifferentState::class)]
public Bits $state_id;

public string $name;

public function apply(SnapshotStoreTestDifferentState $state)
{
$state->name = $this->name;
}
}

class SnapshotStoreTestDifferentState extends State
{
public Bits|UuidInterface|AbstractUid|int|string|null $id;

public string $name;
}
48 changes: 48 additions & 0 deletions tests/Unit/UseStatesDirectlyInEventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@
$this->assertTrue($user_request->acknowledged);
});

it('supports nullable state properties', function () {
$user_request1 = UserRequestState::new();

UserRequestsWithNullable::commit(
user_request1: $user_request1,
user_request2: null,
);

$this->assertTrue($user_request1->nullable);
});

it('supports using a nested state directly in events', function () {
$parent = ParentState::new();
$child = ChildState::new();
Expand Down Expand Up @@ -68,11 +79,35 @@
$this->assertTrue($user_request2->processed);
});

it('loads the correct state when multiple are used', function () {
$user_request1 = UserRequestState::new();

$event1 = UserRequestAcknowledged::fire(
user_request: $user_request1
);

$this->assertTrue($user_request1->acknowledged);

$this->assertEquals($event1->id, $user_request1->last_event_id);

$user_request2 = UserRequestState::new();

$event2 = UserRequestAcknowledged::fire(
user_request: $user_request2
);

$this->assertTrue($user_request2->acknowledged);

$this->assertEquals($event2->id, $user_request2->last_event_id);
});

class UserRequestState extends State
{
public bool $acknowledged = false;

public bool $processed = false;

public bool $nullable = false;
}

class UserRequestAcknowledged extends Event
Expand Down Expand Up @@ -101,6 +136,19 @@ public function apply()
}
}

class UserRequestsWithNullable extends Event
{
public function __construct(
public UserRequestState $user_request1,
public ?UserRequestState $user_request2
) {}

public function apply()
{
$this->user_request1->nullable = true;
}
}

class ParentState extends State
{
public ChildState $child;
Expand Down

0 comments on commit 318d44f

Please sign in to comment.