Skip to content

Commit

Permalink
feat: advanced attriburtes on value objects
Browse files Browse the repository at this point in the history
  • Loading branch information
phpsa committed Feb 28, 2024
1 parent 1684d88 commit 6e5130d
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 28 deletions.
Binary file modified .DS_Store
Binary file not shown.
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ Collection of helpers for re-use accross a few of our projects
- [Observerable trait](#observerable-trait)
- [Date Manipulation](#date-manipulation)
- [Date(Carbon) Helpers attached to above:](#datecarbon-helpers-attached-to-above)
- [Value Objects](#value-objects)
- [Larastan Stubs](#larastan-stubs)
- [Value Objects](#value-objects)
- [Larastan Stubs](#larastan-stubs)
- [Filament Plugin](#filament-plugin)
- [Credits](#credits)

## Installation
Expand Down Expand Up @@ -181,7 +182,7 @@ methods available:

You can also use the CDCarbonDate to create a few differnt date objects.

### Value Objects
## Value Objects
Example:
```php
<?php
Expand Down Expand Up @@ -211,11 +212,41 @@ class SimpleValue extends ValueObject

$simpleValue = SimpleValue::make(value: 'hello World', count: 33);

```
Or using attributes to make advanced objects.
```php
<?php
declare(strict_types=1);

namespace CustomD\LaravelHelpers\Tests\ValueObjects;

use Illuminate\Support\Collection;
use CustomD\LaravelHelpers\ValueObjects\ValueObject;
use CustomD\LaravelHelpers\ValueObjects\Attributes\MakeableObject;
use CustomD\LaravelHelpers\ValueObjects\Attributes\ChildValueObject;
use CustomD\LaravelHelpers\ValueObjects\Attributes\CollectableValue;

class ComplexValue extends ValueObject
{
public function __construct(
#[ChildValueObject(StringValue::class)]
readonly public StringValue $value,
readonly public array $address,
#[ChildValueObject(SimpleValue::class)]
readonly public SimpleValue $simpleValue,
#[MakeableObject(Constructable::class)]
readonly public ?Constructable $constructable = null,
#[CollectableValue(SimpleValue::class)]
readonly ?Collection $simpleValues = null,
) {
}

}
```

Best practice is to use the make option, which will validate, if you use a public constructor it will not.

### Larastan Stubs
## Larastan Stubs
**these are temporary only till implemented by larastan**

add to your phpstan.neon.dist file
Expand All @@ -225,7 +256,7 @@ parameters:
- ./vendor/custom-d/laravel-helpers/larastan/blank_filled.stub
```
### Filament Plugin
## Filament Plugin
** this is only if you want to deal with user timezones for display, else will be in UTC in the Filament panel **
simply add to your panelProvider
Expand Down
13 changes: 7 additions & 6 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<coverage/>
<testsuites>
<testsuite name="package">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>
13 changes: 13 additions & 0 deletions phpunit.xml.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<testsuites>
<testsuite name="package">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
</phpunit>
12 changes: 12 additions & 0 deletions src/ValueObjects/Attributes/ChildValueObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
namespace CustomD\LaravelHelpers\ValueObjects\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
class ChildValueObject
{
public function __construct(public string $class)
{
}
}
13 changes: 13 additions & 0 deletions src/ValueObjects/Attributes/CollectableValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
namespace CustomD\LaravelHelpers\ValueObjects\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
class CollectableValue
{

public function __construct(public string $class)
{
}
}
13 changes: 13 additions & 0 deletions src/ValueObjects/Attributes/MakeableObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
namespace CustomD\LaravelHelpers\ValueObjects\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
class MakeableObject
{

public function __construct(public string $class, public bool $spread = false)
{
}
}
136 changes: 131 additions & 5 deletions src/ValueObjects/ValueObject.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
<?php
namespace CustomD\LaravelHelpers\ValueObjects;

use CustomD\LaravelHelpers\ValueObjects\Attributes\ChildValueObject;
use CustomD\LaravelHelpers\ValueObjects\Attributes\CollectableValue;
use CustomD\LaravelHelpers\ValueObjects\Attributes\MakeableObject;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Support\Arrayable;
use ReflectionClass;
use Illuminate\Support\Collection;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
use ReflectionParameter;

/**
* @implements Arrayable<string,mixed>
*/
abstract class ValueObject implements Arrayable
{

/**
*
* @param mixed $args
Expand All @@ -21,7 +26,11 @@ abstract class ValueObject implements Arrayable
*/
public static function make(...$args): static
{
$instance = new static(...$args); //@phpstan-ignore-line -- meant to be static
$mapped = static::resolveChildValueObjects(...$args);
$mapped = static::resolveMakeableObjects(...$mapped);
$mapped = static::resolveCollectableValueObjects(...$mapped);

$instance = new static(...$mapped); //@phpstan-ignore-line -- meant to be static
$instance->validate();
return $instance;
}
Expand All @@ -47,19 +56,20 @@ public static function fromRequest(FormRequest $request, bool $onlyValidated = t
/** @var array<string, mixed> */
$data = $onlyValidated ? $request->validated() : $request->all();

$args = collect($data)->only(static::getConstructorArgs())->toArray();
$args = collect($data)->only(
static::getConstructorArgs()->map(fn(ReflectionParameter $parameter) => $parameter->getName())
)->toArray();

return new static(...$args); //@phpstan-ignore-line -- meant to be static
}

/**
*
* @return Collection<int, string>
* @return Collection<int, ReflectionParameter>
*/
protected static function getConstructorArgs(): Collection
{
return collect((new ReflectionClass(static::class))->getConstructor()?->getParameters() ?? [])
->map(fn(ReflectionParameter $parameter) => $parameter->getName());
return collect((new ReflectionClass(static::class))->getConstructor()?->getParameters() ?? []);
}

/**
Expand All @@ -77,10 +87,126 @@ protected function validate(): void
$validator->validate();
}

/**
* @return array<string, mixed|ValueObject|Collection>
*/
public function toArray(): array
{
return static::getConstructorArgs()
->map(fn(ReflectionParameter $parameter) => $parameter->getName())
->mapWithKeys(fn($property) => [$property => $this->{$property}])
->toArray();
}


/**
* @param array<string, mixed|ValueObject|Collection> ...$args
* @return array<string, mixed|ValueObject|Collection>
*/
protected static function resolveCollectableValueObjects(...$args): array
{
static::getConstructorArgs()
->filter(fn (ReflectionParameter $parameter):bool => filled($parameter->getAttributes(CollectableValue::class)))
->each(function (ReflectionParameter $parameter) use (&$args): void {
$attributes = collect($parameter->getAttributes(CollectableValue::class))->first();
$name = $parameter->getName();

//should not be possible but hey
if ($attributes === null || ! isset($args[$name]) || ! class_exists($attributes::class)
) {
return;
}
$attribute = $attributes->getArguments()[0];
$arg = $args[$name];
if ($arg instanceof Collection) {
return;
}
/** @phpstan-ignore-next-line */
$args[$name] = collect($arg)->map(fn($item) => $attribute::make(...$item));
});

return $args; //@phpstan-ignore-line
}

/**
* @param array<string, mixed|ValueObject|Collection> ...$args
* @return array<string, mixed|ValueObject|Collection>
*/
protected static function resolveChildValueObjects(...$args): array
{
static::getConstructorArgs()
->filter(fn (ReflectionParameter $parameter):bool => filled($parameter->getAttributes(ChildValueObject::class)))
->each(function (ReflectionParameter $parameter) use (&$args): void {
$attributes = collect($parameter->getAttributes(ChildValueObject::class))->first();
$name = $parameter->getName();

//should not be possible but hey
if ($attributes === null || ! isset($args[$name])
) {
return;
}
$attribute = $attributes->getArguments()[0];
$arg = $args[$name];
if ($arg instanceof $attribute) {
return;
}

if (is_iterable($arg)) {
$args[$name] = $attribute::make(...$arg);
return;
}
$args[$name] = $attribute::make($arg);
});

return $args; //@phpstan-ignore-line
}

/**
* @param array<string, mixed|ValueObject|Collection> ...$args
* @return array<string, mixed|ValueObject|Collection>
*/
protected static function resolveMakeableObjects(...$args): array
{
static::getConstructorArgs()
->filter(fn (ReflectionParameter $parameter):bool => filled($parameter->getAttributes(MakeableObject::class)))
->each(function (ReflectionParameter $parameter) use (&$args): void {
$attributes = collect($parameter->getAttributes(MakeableObject::class))->first();
$name = $parameter->getName();

//should not be possible but hey
if ($attributes === null || ! isset($args[$name])
) {
return;
}
$attribute = $attributes->getArguments()[0];
$spread = $attributes->getArguments()[1] ?? false;
$arg = $args[$name];
if ($arg instanceof $attribute) {
return;
}

$methodExists = method_exists($attribute, 'make');
$constructorExists = method_exists($attribute, '__construct');

if (! $methodExists && ! $constructorExists) {
return;
}
if ($methodExists) {
if (is_iterable($arg) && $spread) {
$args[$name] = $attribute::make(...$arg);
return;
}
$args[$name] = $attribute::make($arg);
return;
}

if (is_iterable($arg) && $spread) {
$args[$name] = new $attribute(...$arg);
return;
}
$args[$name] = new $attribute($arg);
});

return $args; //@phpstan-ignore-line
}
}
29 changes: 20 additions & 9 deletions tests/ValueObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@ public function test_it_can_be_passed_an_array()
public function test_a_complex_value()
{
$data = [
'value' => 'test',
'address' => [
'value' => 'test',
'address' => [
'street' => '123 Fake St'
],
'simpleValue' => SimpleValue::make(
'simpleValue' => SimpleValue::make(
'test',
11
)
),
'simpleValues' => collect()
];
$value = ComplexValue::make(...$data);
$this->assertEquals('test', $value->value);
Expand All @@ -71,18 +72,28 @@ public function test_a_complex_value()
public function test_a_complex_value_construct()
{
$data = [
'value' => 'test',
'simpleValue' => SimpleValue::make(...[
'count' => 11,
'value' => 'test',
'simpleValue' => [
'count' => '11',
'value' => 'test',
]),
'address' => [
],
'address' => [
'street' => '123 Fake St'
],
'constructable' => ['a' => 'b', 'c' => 'd', 'this' => 'is_array'],
'simpleValues' => [
['value' => 'test', 'count' => 11],
['value' => 'test2', 'count' => 13],
['value' => 'test3', 'count' => 15],
]
];
$value = ComplexValue::make(...$data);
$this->assertEquals('test', $value->value);
$this->assertEquals(11, $value->simpleValue->count);
$this->assertEquals('123 Fake St', $value->address['street']);
$this->assertTrue($value->constructable instanceof \CustomD\LaravelHelpers\Tests\ValueObjects\Constructable);
$this->assertTrue($value->simpleValues instanceof \Illuminate\Support\Collection);
$this->assertSame(3, $value->simpleValues->count());
$this->assertSame('test', $value->simpleValues->first()->value);
}
}
Loading

0 comments on commit 6e5130d

Please sign in to comment.