DEV Community

Davey Shafik
Davey Shafik

Posted on • Originally published at daveyshafik.com on

Introducing Bag 1.0: Immutable Values Objects for PHP

For the last couple of years I’ve been using Value Objects in my projects to bring language-level strict types to what would typically be array data structures in my code. From method inputs to JSON API responses, value objects have almost entirely replaced arrays throughout. The ability to get runtime type checking and IDE auto-complete has eliminated many potential bugs, from key typos, to assigning an incorrectly typed value by accident: what type is an “amount” property in a credit card transaction API response? An integer of cents (or other minor units), a Money object such as brick/money or moneyphp/money? Or worst of all, a float?

About 18 months ago, I started using the excellent spatie/laravel-data v3 package for a new project, but I quickly realized there were a few features missing, most notably, factory support. Additionally, the collection class didn’t extend the Laravel Collection class and is anemic by comparison.

Note: spatie/laravel-data v4 adds support for factories, and has a slightly more capable DataCollection class. See below for details.

So I extended the base Data class and added factory support, following a similar pattern to Eloquent factories, including support for Sequences, and I extended DataCollection to add some missing functionality, and mostly, it was good.

Enter Spatie/Laravel-Data v4

Earlier this year, Spatie released v4 with support for Laravel 11 (and up until last month, no support for Laravel 11 in v3), with significant changes, including support for Factories, and a better DataCollection class. Unfortunately, the Factories built into v4 were incompatible with my own, and didn’t have the same feature set, and while better, the updated DataCollection class was still lackluster.

The upgrade process was difficult, and while ultimately successful, I was unhappy with the outcome. Then I decided that I would much prefer if my value objects were immutable, which was impossible using either v3 or v4, and ultimately, that, along with the difficult upgrade path to v4, led me to create Bag.

What is Bag?

Bag is a new library built from scratch — inspired by spatie/laravel-data — that provides immutable value objects for PHP. Built on top of Laravel’s excellent Validation and Collection classes, as well as Eloquent Factory Sequences, it is the value object library I wanted spatie/laravel-data to be.

Additionally, I leaned harder into the use of Attributes, for identifying Collection classes to be used for each value object class, wrapping, hiding data in both toArray() and toJson()/jsonSerialize(), and for identifying transformers (what Spatie calls “magical data object creation“).

I simplified input/output casting (as opposed to casting being for inputs, and transformers being for output),

I also added support for Variadics, something that the Spatie library does not allow. For a more detailed comparison of the two libraries, see the Bag documentation here.

Performance

Despite having a few more features, simple benchmarks of Bag do show it as being about 40-45% faster than spatie/laravel-data v4, and a whopping 70-78% faster than v3.

Benchmark Methodology

The benchmark script was intentionally very simple, using Laravel’s Benchmark class to get the average of 10 runs of a loop (default: 1000 iterations) that creates instances of a Bag or Spatie value object. I ran it both with 1,000 iterations and 10,000 iterations.

The value objects have the following features:

  • Class-level Input/Output Name Mapping to/from SnakeCase
  • A single property input name mapping from CamelCase
  • A single property input name mapping from an alias
  • A property with integer and required validations
  • A property input/output case from/to DateTime/formatted date string

You can see all the code for the benchmarks here.

Current results look like the following:

Iterations Bag Spatie v3 Spatie v4 Difference (ms) Difference (% Faster)
1,000 427.693ms 975.182ms -547.489 +78.63%
1,000 429.019ms 679.554ms -250.535 +45.2%
10,000 4,663.161ms 9,906.135ms -5,242.974 +71.96%
10,000 4,483.669ms 6,914.524ms -2,430.851 +42.68%

Example Benchmark Data

What’s Next?

If you want to try out Bag, check out the Getting Started documentation.

For Bag, currently I consider it to be feature complete and have released v1.0.0. I am currently working on support for Bag-less value objects: that is, support for using any class as a value object — if you want to contribute to this idea, you can comment on the RFC.

And with that, I’ll leave you with the adorable mascot for Bag for you to enjoy:

The Bag Mascot

Top comments (3)

Collapse
 
goodevilgenius profile image
Dan Jones

I've used spatie/laravel-data in the past, and mostly been happy with it, but there have always been a few points that were a bit awkward, that I usually ended up creating classes that wrapped their classes as my base.

I'll be interested to have a look at this library, and see if it would be easier to use.

I'm also curious how usable this library would be outside of a laravel project? Are the dependencies on illuminate/* libraries simple enough that this would be useful and still work well if I was using a completely different framework?

Collapse
 
dshafik profile image
Davey Shafik

I'd love to hear what friction you ran into.

One of the future features I'd like to add is the ability to easily modify the pipeline, and replace it altogether.

As far as usage outside of Laravel, the biggest issue is going to be the amount of transient dependencies causing conflicts with your own project or other framework. Support for both L10 and L11 means that there are multiple major versions of most underlying dependencies supported, so I think it will be less of an issue that otherwise.

Thanks for checking out Bag!

Collapse
 
lazarow profile image
Arkadiusz Nowakowski

Hi, great work! It feels like I can use it in my projects right away. I'm not very keen on attributes, however, maybe time to look at them from a different (nicer) perspective.