Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.2.0 #18

Merged
merged 6 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Documentation

on:
pull_request:
branches: [ main ]
types: [ closed ]

jobs:
publish:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest

steps:
- name: Checkout source code
uses: actions/checkout@v2

- name: Pusblish Wiki
uses: SwiftDocOrg/github-wiki-publish-action@v1
with:
path: "wiki/"
env:
GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v2

- name: Install PHP
- name: Install PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
Expand All @@ -36,7 +36,6 @@ jobs:
run: composer validate

- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest

- name: Execute tests (Unit and Feature tests) via PHPUnit
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Check this small example of the usage:
$response = $queryBus->ask(new GetUserQuery('some-uuid-value'));
```

Check the [wiki](https://github.com/othercodes/ComplexHeart/wiki) for more detailed examples.

## References

- [Pro Codely TV](https://pro.codely.tv/library/)
Expand Down
9 changes: 2 additions & 7 deletions src/Domain/Bus/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,15 @@ abstract class Event extends Message
*/
public function __construct(string $aggregateId, array $data = [])
{
parent::__construct(
[
'aggregateId' => $aggregateId,
'data' => $data,
]
);
parent::__construct(array_merge(['aggregateId' => $aggregateId], $data));
}

/**
* Return the Aggregate Id.
*
* @return string
*/
public function aggregateId()
public function aggregateId(): string
{
return $this->fromPayload('aggregateId');
}
Expand Down
54 changes: 45 additions & 9 deletions src/Domain/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ class Collection extends BaseCollection
private int $perPage;

/**
* The type of item in the collection.
* The type of each items in the collection.
*
* @var string
*/
protected string $typeOf = 'mixed';
protected string $valueType = 'mixed';

/**
* The type of each keys in the collection.
*
* @var string
*/
protected string $keyType = 'mixed';

/**
* Collection constructor.
Expand Down Expand Up @@ -85,24 +92,53 @@ protected function invariantAmountOfItemsMustBeLessOrEqualsThanTotalItems(): boo
* @return bool
* @throws InvariantViolation
*/
protected function invariantItemsMustBeOfSameType(): bool
protected function invariantItemsMustMatchTheRequiredType(): bool
{
$primitives = ['integer', 'boolean', 'float', 'string', 'array', 'object', 'callable'];
if ($this->typeOf !== 'mixed') {
$check = in_array($this->typeOf, $primitives)
? fn($value): bool => gettype($value) !== $this->typeOf
: fn($value): bool => !($value instanceof $this->typeOf);
if ($this->valueType !== 'mixed') {
$primitives = ['integer', 'boolean', 'float', 'string', 'array', 'object', 'callable'];
$check = in_array($this->valueType, $primitives)
? fn($value): bool => gettype($value) !== $this->valueType
: fn($value): bool => !($value instanceof $this->valueType);

foreach ($this->items as $index => $item) {
if ($check($item)) {
throw new InvariantViolation("All items must be type of {$this->typeOf}");
throw new InvariantViolation("All items must be type of {$this->valueType}");
}
}
}

return true;
}

/**
* Invariant: Check the collection keys to match the required type.
*
* Supported types:
* - string
* - integer
*
* @return bool
* @throws InvariantViolation
*/
protected function invariantKeysMustMatchTheRequiredType(): bool
{
if ($this->keyType !== 'mixed') {
$supported = ['string', 'integer'];
if (!in_array($this->keyType, $supported)) {
throw new InvariantViolation(
"Unsupported key type, must be one of ".implode(', ', $supported)
);
}

foreach ($this->items as $index => $item) {
if (gettype($index) !== $this->keyType) {
throw new InvariantViolation("All keys must be type of {$this->keyType}");
}
}
}
return true;
}

/**
* Return the total amount of items on the data source.
*
Expand Down
13 changes: 9 additions & 4 deletions src/Domain/Contracts/Aggregate.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace OtherCode\ComplexHeart\Domain\Contracts;

use OtherCode\ComplexHeart\Domain\Bus\Event;
use OtherCode\ComplexHeart\Domain\Contracts\Bus\EventBus;

/**
* Interface Aggregate
Expand All @@ -15,9 +15,14 @@
interface Aggregate extends Entity
{
/**
* Pull the registered Domain Events.
* Publish the registered Domain Events.
*
* @return Event[]
* $aggregate = new Aggregate();
* // do things and generate events
* $aggregate->publishDomainEvents($eventBus);
*
* @param EventBus $eventBus
* @return void
*/
public function pullDomainEvents(): array;
public function publishDomainEvents(EventBus $eventBus): void;
}
3 changes: 2 additions & 1 deletion src/Domain/Contracts/Bus/QueryBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace OtherCode\ComplexHeart\Domain\Contracts\Bus;

use OtherCode\ComplexHeart\Domain\Bus\Query;
use OtherCode\ComplexHeart\Domain\Bus\Response;

/**
* Interface QueryBus
Expand All @@ -21,5 +22,5 @@ interface QueryBus
*
* @return mixed
*/
public function ask(Query $query);
public function ask(Query $query): Response;
}
14 changes: 7 additions & 7 deletions src/Domain/Traits/HasDomainEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace OtherCode\ComplexHeart\Domain\Traits;

use OtherCode\ComplexHeart\Domain\Bus\Event;
use OtherCode\ComplexHeart\Domain\Contracts\Bus\EventBus;

/**
* Trait HasDomainEvents
Expand All @@ -18,21 +19,20 @@ trait HasDomainEvents
/**
* List of registered domain events.
*
* @var array<Event>
* @var Event[]
*/
private array $_domainEvents = [];

/**
* Pull out all the registered domain events.
* Publish the registered Domain Events.
*
* @return array<Event>
* @param EventBus $eventBus
* @return void
*/
final public function pullDomainEvents(): array
final public function publishDomainEvents(EventBus $eventBus): void
{
$domainEvents = $this->_domainEvents;
$eventBus->publish(...$this->_domainEvents);
$this->_domainEvents = [];

return $domainEvents;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/Domain/Traits/HasServiceBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ final public function bind(ServiceBus $bus): void
$this->_serviceBus = $bus;
}

/**
* Return true if the Service Bus is bound to the object, false otherwise.
*
* @return bool
*/
final public function isBound(): bool
{
return isset($this->_serviceBus);
}

/**
* Return the Command Bus implementation.
*
Expand Down
24 changes: 21 additions & 3 deletions tests/Domain/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CollectionTest extends MockeryTestCase
public function testShouldSuccessfullyInstantiateACollection(): void
{
$c = new class (['foo', 'bar']) extends Collection {
protected string $typeOf = 'string';
protected string $valueType = 'string';
};

$this->assertInstanceOf(Collection::class, $c);
Expand All @@ -31,7 +31,7 @@ public function testShouldFailWithWrongPrimitiveValueItemTypes(): void
$this->expectException(InvariantViolation::class);

new class ([1, '2']) extends Collection {
protected string $typeOf = 'integer';
protected string $valueType = 'integer';
};
}

Expand All @@ -40,7 +40,7 @@ public function testShouldFailWithWrongClassValueItemTypes(): void
$this->expectException(InvariantViolation::class);

new class ([new stdClass(), '2']) extends Collection {
protected string $typeOf = stdClass::class;
protected string $valueType = stdClass::class;
};
}

Expand All @@ -57,4 +57,22 @@ public function testShouldFailDueToAmountOfItemsIsGreaterThanTotalItems(): void

new Collection([0, 1, 2, 3, 4], 2, 5);
}

public function testShouldFailDueToUnsupportedKeyType(): void
{
$this->expectException(InvariantViolation::class);

new class (['foo', 'bar']) extends Collection {
protected string $keyType = 'boolean';
};
}

public function testShouldFailDueToWrongKeyType(): void
{
$this->expectException(InvariantViolation::class);

new class (['foo', 'bar']) extends Collection {
protected string $keyType = 'string';
};
}
}
10 changes: 9 additions & 1 deletion tests/Domain/Traits/HasDomainEventTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

namespace OtherCode\ComplexHeart\Tests\Domain\Traits;

use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use OtherCode\ComplexHeart\Domain\Bus\Event;
use OtherCode\ComplexHeart\Domain\Contracts\Bus\EventBus;
use OtherCode\ComplexHeart\Domain\ValueObjects\UUIDValue;
use OtherCode\ComplexHeart\Tests\Sample\Order;
use OtherCode\ComplexHeart\Tests\Sample\OrderLine;
Expand All @@ -20,14 +23,19 @@ class HasDomainEventTest extends MockeryTestCase
{
public function testShouldAddAndPullDomainEvent(): void
{
$eventBus = Mockery::mock(EventBus::class);
$eventBus->shouldReceive('publish')
->once()
->with(Mockery::type(Event::class));

$o = Order::create(
UUIDValue::random(),
new OrderLine(UUIDValue::random(), new ProductName('PR 1')),
new OrderLine(UUIDValue::random(), new ProductName('PR 2')),
);

$this->assertCount(1, $o->getDomainEvents());
$this->assertCount(1, $o->pullDomainEvents());
$o->publishDomainEvents($eventBus);
$this->assertCount(0, $o->getDomainEvents());
}
}
Empty file.
Empty file.
36 changes: 36 additions & 0 deletions wiki/Domain-Modeling-Value-Objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

```php
use OtherCode\ComplexHeart\Domain\Contracts\ValueObject;
use OtherCode\ComplexHeart\Domain\Traits\IsValueObject;

/**
* Class Color
* @method string value()
*/
final class Color implements ValueObject
{
use IsValueObject;

private string $value;

public function __construct(string $value)
{
$this->initialize(['value' => $value]);
}

protected function invariantValueMustBeHexadecimal(): bool
{
return preg_match('/^#(?:[0-9a-fA-F]{3}){1,2}$/', $this->value) === 1;
}

public function __toString(): string
{
return $this->value();
}
}

$red = new Color('#ff0000');
$red->equals(new Color('#00ff00')); // false
$red->value(); // #ff0000
$magenta = new Color('ff00ff'); // Exception InvariantViolation: Value must be hexadecimal.
```
21 changes: 21 additions & 0 deletions wiki/Domain-Modeling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## How to model the domain

Complex Heart allows you to model your domain Aggregates, Entities, and Value Objects using a set of traits. Great, but
why traits and not classes? Well, sometimes you have some kind of inheritance in your classes. Being forced to use a
certain base class is too invasive and personally, I don't like it. By using a set of traits and interfaces you have all
the functionality you need without compromising the essence of your own domain.

The available traits are:

- `HasAttributes` Provide some functionality to manage attributes.
- `HasEquality` Provide functionality to handle equality between objects.
- `HasInvariants` Allow invariant checking on instantiation (Guard Clause).
- `HasIdentity` Define the Entity/Aggregate identity.
- `HasDomainEvents` Provide domain event management.

On top of those base traits **Complex Heart** provide ready to use compositions:

- `IsModel` composed by `HasAttributes` and `HasInvariants`
- `IsValueObject` composed by `IsModel` and `HasEquality`
- `IsEntity` composed by `IsModel`, `HasIdentity`, `HasEquality`
- `IsAggregate` composed by `IsEntity`, `HasDomainEvents`
1 change: 1 addition & 0 deletions wiki/Home.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading