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

[RFC] Bag-less Value Objects #30

Open
dshafik opened this issue May 12, 2024 · 1 comment
Open

[RFC] Bag-less Value Objects #30

dshafik opened this issue May 12, 2024 · 1 comment
Labels
rfc Request for Comments

Comments

@dshafik
Copy link
Owner

dshafik commented May 12, 2024

Currently in order to create Bag value objects you must extend the \Bag\Bag class. This requirement limits where you can use Bag value objects — you cannot simply add Value Object behavior to an existing value-object-like class. This is intended as an alternative way to use Bag.

We can solve this in two ways:

Composition using BagInterface and AsBag Trait

The first approach is more type-safe, by creating a BagInterface we enforce Bag-like UX on any class that wishes to be a Bag Value Object. Primarily this means enforcing the following methods:

interface BagInterface
{
    public static function from(mixed $values): BagInterface;

    public static function rules(): array;

    public static function collect(iterable $values = []): Collection;

    public static function validate(LaravelCollection|array $values): bool;

    public function with(mixed ...$values): BagInterface;

    public function get(?string $key = null): mixed;

    public function getRaw(?string $key = null): mixed;

    public function unwrapped(): array;

    public function toArray(): array;

    public function jsonSerialize(): array;

    public function toJson($options = 0): string;
}

We can then provide the implementation using the AsBag trait, meaning that to implement Bag functionality in any object, you can do the following:

use Bag\Contracts\BagInterface;
use Bag\Traits\AsBag;

readonly class MyValue implements BagInterface {
    use AsBag;

    public function __construct() { }
}

and the default Bag class can simply be the following to continue allowing the existing extend Bag UX:

readonly class Bag implements Arrayable, Jsonable, JsonSerializable, Castable, BagInterface
{
    use AsBag;
}

Bag::make()

Alternatively, we can implement a Bag::make() method with the following signature:

/**
 * @template T of object
 * @param class-string<T> $className
 * @return T
 */
public static function make(string $className, mixed $values): object;

This would then resolve the constructor arguments for $className and create a new instance. In addition static methods such as Bag::validate() and Bag::collect() can be updated to support objects that don't extend Bag.

However, with() is an instance method and the alternative to this to re-use Bag::make() and pass in the original value object values along with the new changes.

To enable support for features such as Hidden, we would also need to implement output functions such as toArray(), and toJson()/jsonSerialize(), which would either need to make heavy of (slow) reflection, or wrap around those functions in the underlying object.

Discussion

With the interface-and-trait implementation the resulting class has all the functionality and UX of Bag value objects, however it still requires you to update your existing class definition. The Bag::make() option requires less changes to the class, although you can opt-in to features using the attributes for features like casting or validation.

The primary downfall of the Bag::make() implementation is that the pipeline must be changed from working on Bag objects and instead just use object instead, losing any type safety in the process.

It should be noted that both of these solutions and the original extend Bag solution can co-exist, however there are concerns about complicating the UX by being too flexible.

@dshafik dshafik added the rfc Request for Comments label May 12, 2024
@adrian-enspired
Copy link

Composition using BagInterface and AsBag Trait

This is the approach I chose, in a similar situation. It's proven a good balance between being flexible and providing a default "hit the ground running" implementation.

@dshafik dshafik changed the title Bag-less Value Objects [RFC] Bag-less Value Objects May 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rfc Request for Comments
Projects
None yet
Development

No branches or pull requests

2 participants