Skip to content

Commit

Permalink
Release 0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
othercodes authored Feb 10, 2021
2 parents f065a66 + 00b21ac commit edb931a
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 34 deletions.
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

0 comments on commit edb931a

Please sign in to comment.