PHPackages                             firemidge/value-objects - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. firemidge/value-objects

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

firemidge/value-objects
=======================

Convenience methods for working with value objects

v2.7(1y ago)0460↓100%GPL-3.0-or-laterPHPPHP ^8.1CI failing

Since Jul 15Pushed 1y ago1 watchersCompare

[ Source](https://github.com/FireMidge/value-objects)[ Packagist](https://packagist.org/packages/firemidge/value-objects)[ RSS](/packages/firemidge-value-objects/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (1)Versions (22)Used By (0)

value-objects
=============

[](#value-objects)

**Tested with PHP 8.4. Requires PHP ^8.1.**

This library provides convenience methods for creating value objects in the form of traits, as well as some generic classes implementing said traits to use as-is where no configuration is needed.

You may use the below table to decide which type is best for you. *"Single Value" means the object will hold a single value, whereas "Array of Values" means the object can hold more than one value.*

**You can click on the relevant type to jump straight to their documentation.**

Single ValueArray of ValuesList of Valid Values[`IsStringEnumType`](#isstringenumtype)
[`IsIntEnumType`](#isintenumtype)
[`IsIntStringMapType`](#isintstringmaptype)[`IsStringArrayEnumType`](#isstringarrayenumtype)
[`IsIntArrayEnumType`](#isintarrayenumtype)
[`IsClassArrayEnumType`](#isclassarrayenumtype)
[`IsArrayEnumType`](#isarrayenumtype)Any Value/Custom Validation[`IsEmailType`](#isemailtype)
[`IsStringType`](#isstringtype)
[`IsFloatType`](#isfloattype)
[`IsIntType`](#isinttype)[`IsClassCollectionType`](#isclasscollectiontype)
[`IsCollectionType`](#iscollectiontype)### Generic classes

[](#generic-classes)

They only exist for convenience, already implementing a type trait with no or minimal configuration. All classes are extendable if needed, or you can implement the relevant trait directly.

NameImplemented traitNotes`AnyCollection``IsCollectionTrait`Used when you just want to access an array using normalised OOP methods rather than PHP-native global functions, while not caring about the type of elements.`AnyFloat``IsFloatType`Used when you do not want to customise min/max value or other validation rules and the value is a float.`AnyInteger``IsIntType`Used when you do not want to customise min/max value or other validation rules and the value is an integer.`AnyString``IsStringType`Used when there are no rules about the format of the string and there is no list of valid values either.`Email``IsEmailType`Used when dealing with an email address without custom validation rules/custom formatting.`Percentage``IsFloatType`Used when expecting a value between 0 and 100, which can be represented as a string (with a % symbol), an integer, or a float (with a customisable number of decimal places. Passing a value less than 0 or greater than 100 results in an exception (rather than being clipped silently. The class can be extended to configure further.Quality Control
---------------

[](#quality-control)

The following table is updated with each code update and is generated with the help of PhpUnit (unit testing tool) and Infection (mutation testing tool):

 PercentageDescriptionCode Coverage[![100%](docs/img/cc.png)](docs/img/cc.png)How many methods have been fully covered by tests.Mutation Score Indicator[![99%](docs/img/msi.png)](docs/img/msi.png)Indicates how many generated mutants were detected. *Note that some mutants are false positives.*Mutation Code Coverage[![99%](docs/img/mcc.png)](docs/img/mcc.png)Should be in the same ballpark as the normal code coverage. Formula: `(TotalMutantsCount - NotCoveredByTestsCount) / TotalMutantsCount`Covered Code MSI[![100%](docs/img/ccm.png)](docs/img/ccm.png)This is the MSI (Mutation Score Indicator) for code that is actually covered by tests. It shows how effective the tests really are. Formula: `TotalDefeatedMutants / (TotalMutantsCount - NotCoveredByTestsCount)`.IsStringEnumType
----------------

[](#isstringenumtype)

Use this type when there is a set of fixed valid values, and your object represents a single value.

*If there is a set of fixed valid values but your object represents an array of values, use [`IsStringArrayEnumType`](#isstringarrayenumtype).*

**If you do not need to do any configuration, there is a generic class available, implementing this type: `FireMidge\ValueObject\Generic\AnyString`.**

Example:

```
class Season
{
    use IsStringEnumType;

    public const SPRING = 'spring';
    public const SUMMER = 'summer';
    public const AUTUMN = 'autumn';
    public const WINTER = 'winter';

    public static function all() : array
    {
        return [
            self::SPRING,
            self::SUMMER,
            self::AUTUMN,
            self::WINTER,
        ];
    }
}
```

Usage:

```
$spring = Season::fromString(Season::SPRING);
```

IsIntEnumType
-------------

[](#isintenumtype)

Use this type when there is a set of fixed valid values, and your object represents a single value.

*If there is a set of fixed valid values but your object represents an array of values, use [`IsIntArrayEnumType`](#isintarrayenumtype).*

Example:

```
class Status
{
    use IsIntEnumType;

    public const INFORMATION  = 1;
    public const SUCCESS      = 2;
    public const REDIRECTION  = 3;
    public const CLIENT_ERROR = 4;
    public const SERVER_ERROR = 5;

    public static function all() : array
    {
        return [
            self::INFORMATION,
            self::SUCCESS,
            self::REDIRECTION,
            self::CLIENT_ERROR,
            self::SERVER_ERROR,
        ];
    }
}
```

Usage:

```
$success = Status::fromInt(Status::SUCCESS);
```

IsEmailType
-----------

[](#isemailtype)

Use this type when the value represents a single e-mail address. This trait uses [`IsStringType`](#isstringtype) under the hood but performs standard e-mail validation.

**If you do not need to do any configuration, there is a generic class available, implementing this type: `FireMidge\ValueObject\Generic\Email`.**

Example:

```
class Email
{
    use IsEmailType;
}
```

Usage:

```
$email = Email::fromString('hello@there.co.uk');
```

IsStringType
------------

[](#isstringtype)

Use this type when the value represents a single string value, but there is no fixed set of valid values.

If you are expecting an e-mail address, you can use the [`IsEmailType`](#isemailtype) trait instead, which will perform format validation checks.

### Validation

[](#validation)

To provide custom validation, override `protected function validate(string $value) : void`.

If you want to only validate the length of the string, you can call `validateLength(string $value, ?int $minLength = null, ?int $maxLength = null) : void` inside the `validate` method.

### String transformation

[](#string-transformation)

If you want to transform the input value but not fail validation, override `protected function transform(string $value) : string`.

There are 3 convenience methods available that you can call inside `transform` if you want:

- `trimAndLowerCase(string $value)`
- `trimAndUpperCase(string $value)`
- `trimAndCapitalise(string $value)`

Example:

```
class ProductName
{
    use IsStringType;

    protected function transform(string $value) : string
    {
        return $this->trimAndCapitalise($value);
    }

    protected function validate(string $value) : void
    {
        $this->validateLength($value, 2, 50);
    }
}
```

Usage:

```
// $productName will be 'Orange juice'
$productName = ProductName::fromString('  orange juice');
```

IsIntType
---------

[](#isinttype)

Use this type when the value represents a single integer value, but there is no fixed list of valid values, or it is not feasible to write up each valid value.

**If you do not need to do any configuration, there is a generic class available, implementing this type: `FireMidge\ValueObject\Generic\AnyInteger`.**

### Validation

[](#validation-1)

You can provide custom validation rules by overriding `protected function validate(int $value) : void`. By default, it will validate that the value is a positive integer.

If you only want to validate that a value is between a certain minimum and maximum value, override `protected static function minValidValue() : ?int ` and `protected static function maxValidValue() : ?int`. Returning `NULL` from either means there is no limitation to the minimum or the maximum value respectively.

Example:

```
class Percentage
{
    use IsIntType;

    protected static function minValidValue() : ?int
    {
        return 0;
    }

    protected static function maxValidValue() : ?int
    {
        return 100;
    }
}
```

**Note that there is a convenient `Percentage` class already available: `FireMidge\ValueObject\Generic\Percentage`.**

Another example, for a value without any limitations:

```
class Balance
{
    use IsIntType;

    protected static function minValidValue() : ?int
    {
        return null;
    }
}
```

Another example, for a value which has no upper limit but may never be below 5.

```
class Investment
{
    use IsIntType;

    protected static function minValidValue() : ?int
    {
        return 5;
    }

    // It is not necessary to add this in as this is the default.
    protected static function maxValidValue() : ?int
    {
        return null;
    }
}
```

Another example which only allows odd values:

```
class OddIntType
{
    use IsIntType;

    protected function validate(int $value) : void
    {
        if ($value % 2 === 0) {
            throw new InvalidValue(sprintf('Only odd values allowed. Value provided: %d', $value));
        }
    }
}
```

Usage:

```
$percentage = Percentage::fromInt(78);
```

IsFloatType
-----------

[](#isfloattype)

Use this type when the value represents a single float value.

**If you do not need to do any configuration, there is a generic class available, implementing this type: `FireMidge\ValueObject\Generic\AnyFloat`.**

### Validation

[](#validation-2)

You can provide custom validation rules by overriding `protected function validate(float $value) : void`. By default, it will only validate the float is above 0, but you can change this to allow unlimited values by overriding `minValidValue`.

If you only want to validate that a value is between a certain minimum and maximum value, override `protected static function minValidValue() : ?float ` and `protected static function maxValidValue() : ?float`. Returning `NULL` from either means there is no limitation to the minimum or the maximum value respectively.

Example, which allows a value between 0 and 100, and which automatically crops any decimal points after the 3rd:

```
class Percentage
{
    use IsFloatType;

    protected static function minValidValue() : ?float
    {
        return 0;
    }

    protected static function maxValidValue() : ?float
    {
        return 100;
    }

    protected function transform(float $value) : float
    {
        return round($value, 2);
    }
}
```

Usage:

```
// $percentage will be 78.58
$percentage = Percentage::fromFloat(78.578);
```

IsIntStringMapType
------------------

[](#isintstringmaptype)

Use this type if the value represents a single value which can be mapped between an integer and a string.

This may be useful when you e.g. store a value in the database as an integer (for faster indexing), but convert it to a string for a public API (for better readability).

Example:

```
class Season
{
    use IsIntStringMapType;

    protected static function provideMap() : array
    {
        return [
            1 => 'spring',
            2 => 'summer',
            3 => 'autumn',
            4 => 'winter',
        ];
    }
}
```

Usage:

```
// Returns 'summer'
$label = (Season::fromInt(2))->toString();

// Returns 4
$intValue = (Season::fromString('winter'))->toInt();
```

IsIntArrayEnumType
------------------

[](#isintarrayenumtype)

Use this type when the value represents an array of integer values, where each value must be one of a fixed list of values.

Useful when e.g. building filters, allowing to select a number of statuses or IDs (or others) to be included in the result.

### Unique values

[](#unique-values)

If each value can only appear once in the object, you have two options:

- If you want an exception to be thrown when duplicate values are being added (either via `fromArray` or via `withValue`), then override `protected static function areValuesUnique() : bool` and return `true`. An exception of type `DuplicateValue` will be thrown.
- If you do not want an exception to be thrown but want duplicate values to simply be silently ignored (both in `fromArray` and in `withValue`), override `protected static function ignoreDuplicateValues() : bool` and return `true`. If duplicate values are found, they are only added once to the array.

When both `areValuesUnique` and `ignoreDuplicateValues` return `true`, `ignoreDuplicateValues` takes precedence.

Example:

```
class Statuses
{
    use IsIntArrayEnumType;

    public const INFORMATION  = 1;
    public const SUCCESS      = 2;
    public const REDIRECTION  = 3;
    public const CLIENT_ERROR = 4;
    public const SERVER_ERROR = 5;

    public static function all() : array
    {
        return [
            self::INFORMATION,
            self::SUCCESS,
            self::REDIRECTION,
            self::CLIENT_ERROR,
            self::SERVER_ERROR,
        ];
    }

    protected static function areValuesUnique() : bool
    {
        return true;
    }
}
```

Usage:

```
$statusesToInclude = Statuses::fromArray([Statuses::INFORMATION, Statuses::SUCCESS]);
$allStatuses       = Statuses::withAll();

$statuses = (Statuses::fromArray([]))
    ->withValue(Statuses::SUCCESS)
    ->withValue(Statuses::SERVER_ERROR)
    ->withoutValue(Statuses::SUCCESS);

// The difference between tryWithoutValue and withoutValue is that the try method
// will throw an exception if you are trying to remove a value that did not previously
// exist, whereas withoutValue will simply ignore it.
$statusesWithoutSuccess = $statuses->tryWithoutValue(Statuses::SUCCESS);

$containsSuccess = $statusesToInclude->contains(Statuses::SUCCESS);
```

IsStringArrayEnumType
---------------------

[](#isstringarrayenumtype)

Use this type when the value represents an array of string values, where each value must be one of a fixed list of values.

Useful when e.g. building filters, allowing to select a number of fields in the result.

### Unique values

[](#unique-values-1)

If each value can only appear once in the object, you have two options:

- If you want an exception to be thrown when duplicate values are being added (either via `fromArray` or via `withValue`), then override `protected static function areValuesUnique() : bool` and return `true`. An exception of type `DuplicateValue` will be thrown.
- If you do not want an exception to be thrown but want duplicate values to simply be silently ignored (both in `fromArray` and in `withValue`), override `protected static function ignoreDuplicateValues() : bool` and return `true`. If duplicate values are found, they are only added once to the array.

When both `areValuesUnique` and `ignoreDuplicateValues` return `true`, `ignoreDuplicateValues` takes precedence.

Example:

```
class UserFieldList
{
    use IsStringArrayEnumType;

    public const NAME        = 'name';
    public const EMAIL       = 'email';
    public const STATUS      = 'status';
    public const FRIEND_LIST = 'friendList';

    protected static function all() : array
    {
        return [
            self::NAME,
            self::EMAIL,
            self::STATUS,
            self::FRIEND_LIST,
        ];
    }
}
```

Usage:

```
$fields = $fieldsFromRequest === null
    ? UserFieldList::withAll()
    : UserFieldList::fromArray($fieldsFromRequest);

$fields    = UserFieldList::fromArray([UserFieldList::NAME, UserFieldList::EMAIL]);
$allFields = UserFieldList::withAll();

$fields = (UserFieldList::fromArray([]))
    ->withValue(UserFieldList::FRIEND_LIST)
    ->withValue(UserFieldList::STATUS)
    ->withoutValue(Statuses::FRIEND_LIST);

$containsFriendList = $statusesToInclude->contains(UserFieldList::FRIEND_LIST);
```

IsClassArrayEnumType
--------------------

[](#isclassarrayenumtype)

Use this type when the value represents an array of class instances, and there is a list of valid values. This means the class instances represent enum types.

Example:

```
class Sources
{
    use IsClassArrayEnumType;

    protected static function className() : string
    {
        return Source::class;
    }
}
```

It is very similar to using [`IsStringArrayEnumType`](#isstringarrayenumtype) or [`IsIntArrayEnumType`](#isintarrayenumtype) with the exception that each item in this array type is a class instance. It means individual items can be added without having to be converted into a scalar type, as in the usage example below:

Usage:

```
$source = Source::fromString('invitation');

$sources = Sources::empty();
$sources = $sources->withValue($source);
```

Because classes implementing [`IsClassArrayEnumType`](#isclassarrayenumtype) hold objects, you can perform method calls on returned elements, as in the example below:

Usage:

```
$sources = Sources::withAll(); // $sources now holds an array with ALL possible Source values.

// Compare the first element that was added to $sources:
$sources->first()->isEqualTo(Source::invitation());

// Find a specific value. Returns `null` if the element does not exist in $sources.
$sourceOrNull = $sources->find(fn(Source $src) => $src->isEqualTo(Source::invitation()));

// You can also perform a pre-check whether a specific value exists in the instance of `IsClassArrayEnumType`:
$containsInvitation = $sources->contains(Source::invitation());
```

### Unique values

[](#unique-values-2)

By default, the same value can be added multiple times to the same instance. To control this behaviour, see the example below:

```
class Sources
{
    // Other code here...

    /**
     * This method is linked to ignoreDuplicateValues() - therefore, it is important what both of them do
     * in order to determine the eventual behaviour.
     *
     * Returning `true` here causes a `DuplicateValue` exception to be thrown when duplicate values are added,
     * either via `fromArray` or `withValue` - UNLESS you also return `true` from `ignoreDuplicateValues()`.
     *
     * Returning `false` here and from `ignoreDuplicateValues()` means the same values can be
     * added multiple times.
     *
     * Default: Returns `false` unless overridden.
     */
    protected static function areValuesUnique() : bool
    {
        return true;
    }

    /**
     * Returning `true` here means that when something attempts to add the same value to an instance
     * more than once, any duplicate values will be silently ignored (no exceptions thrown) - this
     * is the behaviour regardless of what `areValuesUnique` returns.
     *
     * Default: Returns `false` unless overridden.
     */
    protected static function ignoreDuplicateValues() : bool
    {
        return true;
    }
}
```

If each value can only appear once in the object, you have two options:

- If you want an exception to be thrown when duplicate values are being added (either via `fromArray` or via `withValue`), then override `protected static function areValuesUnique() : bool` and return `true`. An exception of type `DuplicateValue` will be thrown.
- If you do not want an exception to be thrown but want duplicate values to simply be silently ignored (both in `fromArray` and in `withValue`), override `protected static function ignoreDuplicateValues() : bool` and return `true`. If duplicate values are found, they are only added once to the array.

When both `areValuesUnique` and `ignoreDuplicateValues` return `true`, `ignoreDuplicateValues` takes precedence. **Note**: In order to perform these duplicate checks, the value object is converted into a string first. Make sure you have the `__toString` method implemented if you use custom classes and want these checks. (If you're using any of the types within this library, `__toString` is already implemented on them.)

### Validation

[](#validation-3)

By default, each element is already being validated for being an object, and an instance of the particular class returned by `className()`. However, if you want additional validation to be performed, you can override `protected function validateEach(mixed $value) : void`, which is executed for each value separately, both when instantiating it and when calling `withValue`. Note that this validation will also run before `withoutValue`, `tryWithoutValue` and `contains`, so you are notified when passing something entirely invalid rather than it being silently swallowed. Make sure to also call `parent::validateEach($value);` unless you want to repeat the default validation behaviour in your overridden version.

### From raw values

[](#from-raw-values)

If you want to instantiate your collection from "raw" values (as opposed to instances of a class) for convenience reasons (whilst internally converting them to the relevant instances), you can use `fromRawValues`.

Example:

```
$sources = Sources::fromRawArray([
    'invitation',
    'promotion',
    'reference',
]);
```

This works for a conversion to instances that implement `fromString`, `fromInt`, `fromBool`, `fromFloat`, `fromDouble`, `fromNumber` or accept the relevant parameter through their constructor. *Note that input types are NOT converted. That means if you pass a `string`, only the `fromString` factory method will be attempted.*If none of the above are present or succeed, the trait will attempt to pass the value into the constructor of the target class. (Should this fail as well, a `ConversionError` is thrown.)

#### Custom conversion

[](#custom-conversion)

If you would like to use the `fromRawValues` method but your target class has neither of the before-mentioned methods or ways of instantiating, you have three options:

##### 1) Provide a custom callback

[](#1-provide-a-custom-callback)

If you only need to do a custom conversion once, you can provide a callback to the `fromRawValues` method directly.

Example:

```
$months = CustomEnumArray::fromRawArray([
    'January',
    'May',
    'July',
], fn($v) => CustomClass::fromMonth($v)));
```

###### 2) Override `convertFromRaw`

[](#2-override-convertfromraw)

If you use a custom conversion more than once on the class, you have the option of overriding `protected static function convertFromRaw(mixed $value) : object` to automatically use your custom converter every time `fromRawValues` is called.

Example:

```
class CustomEnumArray
{
    use IsClassCollectionType {
        IsClassCollectionType::convertFromRaw as private _convertFromRaw;
    }

    protected static function className() : string
    {
        return CustomClass::class;
    }

    protected static function convertFromRaw(mixed $value) : object
    {
        try {
            return static::_convertFromRaw($value);
        } catch (ConversionError) {
            return CustomClass::fromMonth($value);
        }
    }
}
```

##### 3) Implement your own factory method

[](#3-implement-your-own-factory-method)

Since everything is just a trait, you of course have the option of simply creating your own and replace `fromRawValues`. If you want to keep the same name for your own method and change the signature, just alias the trait's method and make it private.

Usage:

```
$months = Months::fromArray([
    Month::fromString('December'),
    Month::fromString('August'),
    Month::fromString('October'),
]);

// Alternative way of instantiating the enum collection, if the values
// passed can be converted to the target class.
$months = Months::fromRawArray([
    'December',
    'August',
    'October',
];

// Returns 3
$numberOfMonths = $months->count();

// Returns `true`, although strings are passed, as long as `Month`
// implements the `__toString` method (e.g. via the trait `IsStringType`).
$emailsMatch = $emails->isEqualTo([
   'December',
   'August',
   'October',
]);
```

IsArrayEnumType
---------------

[](#isarrayenumtype)

Use this type when the value represents an array of values of a type other than `string`, `integer` or an instance of a specific class (for those we have [`IsStringArrayEnumType`](#isstringarrayenumtype), [`IsIntArrayEnumType`](#isintarrayenumtype) and [`IsClassArrayEnumType`](#isclassarrayenumtype) respectively) and where there is a list of valid values.

### Combination with other types

[](#combination-with-other-types)

You can combine this type with any other type, e.g. to get an array of float types, or an array of int enum types, etc. The difference to using a combination of [`IsStringEnumType`](#isstringenumtype) and [`IsArrayEnumType`](#isarrayenumtype) over [`IsStringArrayEnumType`](#isstringarrayenumtype) is that in the former case, each value is a value object, whereas in the latter, each value is just a scalar string. Of course you can also simply use the newer [`IsClassArrayEnumType`](#isclassarrayenumtype), which combines [`IsArrayEnumType`](#isarrayenumtype) and [`IsClassCollectionType`](#isclasscollectiontype), allowing you to hold an instance of value objects. See [`IsClassArrayEnumType`](#isclassarrayenumtype) for more information.

### Unique values

[](#unique-values-3)

By default, the same value can be added multiple times to the same instance. To control this behaviour, see the example below:

```
class Sources
{
    // Other code here...

    /**
     * This method is linked to ignoreDuplicateValues() - therefore, it is important what both of them do
     * in order to determine the eventual behaviour.
     *
     * Returning `true` here causes a `DuplicateValue` exception to be thrown when duplicate values are added,
     * either via `fromArray` or `withValue` - UNLESS you also return `true` from `ignoreDuplicateValues()`.
     *
     * Returning `false` here and from `ignoreDuplicateValues()` means the same values can be
     * added multiple times.
     *
     * Default: Returns `false` unless overridden.
     */
    protected static function areValuesUnique() : bool
    {
        return true;
    }

    /**
     * Returning `true` here means that when something attempts to add the same value to an instance
     * more than once, any duplicate values will be silently ignored (no exceptions thrown) - this
     * is the behaviour regardless of what `areValuesUnique` returns.
     *
     * Default: Returns `false` unless overridden.
     */
    protected static function ignoreDuplicateValues() : bool
    {
        return true;
    }
}
```

If each value can only appear once in the object, you have two options:

- If you want an exception to be thrown when duplicate values are being added (either via `fromArray` or via `withValue`), then override `protected static function areValuesUnique() : bool` and return `true`. An exception of type `DuplicateValue` will be thrown.
- If you do not want an exception to be thrown but want duplicate values to simply be silently ignored (both in `fromArray` and in `withValue`), override `protected static function ignoreDuplicateValues() : bool` and return `true`. If duplicate values are found, they are only added once to the array.

When both `areValuesUnique` and `ignoreDuplicateValues` return `true`, `ignoreDuplicateValues` takes precedence. **Note**: In order to perform these duplicate checks, the value object is converted into a string first. Make sure you have the `__toString` method implemented if you use custom classes and want these checks. (If you're using any of the types within this library, `__toString` is already implemented on them.)

### Validation

[](#validation-4)

You can provide custom validation by overriding `protected function validateEach(mixed $value) : void`, which is executed for each value separately, both when instantiating it and when calling `withValue`. Note that this validation will also run before `withoutValue`, `tryWithoutValue` and `contains`, so you are notified when passing something entirely invalid rather than it being silently swallowed.

Example:

```
use FireMidge\ValueObject\IsCollectionType;

/**
 * @extends IsCollectionType
 */
class StatusList
{
    use IsArrayEnumType;

    protected static function all() : array
    {
        return array_map(function($value) {
            return Status::fromInt($value);
        }, Status::all());
    }

    protected function validateEach(mixed $value) : void
    {
        if (! is_object($value) || (! $value instanceof Status)) {
            throw InvalidValue::notInstanceOf($value, Status::class);
        }
    }

    protected static function areValuesUnique() : bool
    {
        return true;
    }

    protected static function ignoreDuplicateValues() : bool
    {
        return true;
    }
}
```

*Note that the example above is for demonstration purpose only - all of the above functionality comes out of the box by using `IsClassArrayEnumType`.*

Usage:

```
$statuses    = StatusList::fromArray([Status::SUCCESS, Status::REDIRECTION]);
$allStatuses = StatusList::withAll();

// $duplicateStatusesIgnored will only contain Status::SUCCESS once.
// [ Status::SUCCESS, Status::REDIRECTION ]
// This is because of `ignoreDuplicateValues` returning true.
$duplicateStatusesIgnored = StatusList::fromArray([
    Status::SUCCESS,
    Status::REDIRECTION,
    Status::SUCCESS,
]);

// $newStatuses will only contain one instance of Status::REDIRECTION.
// This is because of `ignoreDuplicateValues` returning true.
$newStatuses = $statuses->withValue(Status::REDIRECTION);
```

You also have a variety of other array methods available, e.g.:

```
use FireMidge\ValueObject\Generic\AnyCollection;$statuses = StatusList::fromArray([
    Status::SUCCESS,
    Status::REDIRECTION,
]);
$errorStatuses = StatusList::fromArray([
    Status::SERVER_ERROR,
    Status::CLIENT_ERROR,
]);

// $newStatuses contains statuses from both $statuses and $errorStatuses,
// without modifying the merged classes.
$newStatuses = $statuses->withMerged($errorStatuses);

// This does modify $statuses, and cause it to append the values from
// $errorStatuses to its own.
$statuses->merge($errorStatuses);

// split() causes the values of one collection to be split into
// 2 collections. This modifies the original instance.
// In this case, $alsoErrorStatuses contains Status::SERVER_ERROR
// and Status::CLIENT_ERROR, while $statuses only keeps
// the first 2 elements, i.e. STATUS::SUCCESS and STATUS::REDIRECTION.
$alsoErrorStatuses = $statuses->split(2);

// pop() removes the last element and returns it.
// This modifies the original instance ($statuses).
$redirection = $statuses->pop();

// You can also pop multiple values at once, but note
// that the returned order will be reversed, as each element
// is popped individually and together, they are returned as a new
// collection instance.
$values = AnyCollection::fromArray('a', 'b', 'c', 'd', 'e');
$last2 = $values->popMultiple(2);
echo json_encode($last2); // ["e","d"]
```

IsClassCollectionType
---------------------

[](#isclasscollectiontype)

Use this type when the value represents an array of values, where each value must be an instance of a class and there is **no** finite list of valid values. If there is a list of valid values, use [`IsArrayEnumType`](#isarrayenumtype). If the values are not instances of a class, use [`IsCollectionType`](#iscollectiontype).

### Unique values

[](#unique-values-4)

If each value can only appear once in the object, you have two options:

- If you want an exception to be thrown when duplicate values are being added (either via `fromArray` or via `withValue`), then override `protected static function areValuesUnique() : bool` and return `true`. An exception of type `DuplicateValue` will be thrown.
- If you do not want an exception to be thrown but want duplicate values to simply be silently ignored (both in `fromArray` and in `withValue`), override `protected static function ignoreDuplicateValues() : bool` and return `true`. If duplicate values are found, they are only added once to the array.

When both `areValuesUnique` and `ignoreDuplicateValues` return `true`, `ignoreDuplicateValues` takes precedence.

### Validation

[](#validation-5)

You can provide custom validation by overriding `protected function validateEach(mixed $value) : void`, which is executed for each value separately, both when instantiating it and when calling `withValue`. Note that this validation will also run before `withoutValue`, `tryWithoutValue` and `contains`, so you are notified when passing something entirely invalid rather than it being silently swallowed.

Example:

```
use FireMidge\ValueObject\IsCollectionType;

/**
 * @extends IsCollectionType
 */
class EmailCollection
{
    use IsClassCollectionType, CanBeConvertedToStringArray;

    protected static function className() : string
    {
        return Email::class;
    }
}
```

### From raw values

[](#from-raw-values-1)

If you want to instantiate your collection from "raw" values (as opposed to instances of a class) for convenience reasons (whilst internally converting them to the relevant instances), you can use `fromRawValues`.

Example:

```
$emails = EmailCollection::fromRawArray([
    'hello@there.co.uk',
    'lorem@ipsum.it',
    'bass@player.at',
]);
```

This works for a conversion to instances that implement `fromString`, `fromInt`, `fromBool`, `fromFloat`, `fromDouble`, `fromNumber` or accept the relevant parameter through their constructor. *Note that input types are NOT converted. That means if you pass a `string`, only the `fromString` factory method will be attempted.*If none of the above are present or succeed, the trait will attempt to pass the value into the constructor of the target class. (Should this fail as well, a `ConversionError` is thrown.)

#### Custom conversion

[](#custom-conversion-1)

If you would like to use the `fromRawValues` method but your target class has neither of the before-mentioned methods or ways of instantiating, you have three options:

##### 1) Provide a custom callback

[](#1-provide-a-custom-callback-1)

If you only need to do a custom conversion once, you can provide a callback to the `fromRawValues` method directly.

Example:

```
$emails = CustomCollection::fromRawArray([
    'hello@there.co.uk',
    'lorem@ipsum.it',
    'bass@player.at',
], fn($v) => CustomClass::fromDomain(substr($v, strrpos($v, '.') + 1)));
```

###### 2) Override `convertFromRaw`

[](#2-override-convertfromraw-1)

If you use a custom conversion more than once on the class, you have the option of overriding `protected static function convertFromRaw(mixed $value) : object` to automatically use your custom converter every time `fromRawValues` is called.

Example:

```
class CustomCollection
{
    use IsClassCollectionType {
        IsClassCollectionType::convertFromRaw as private _convertFromRaw;
    }

    protected static function className() : string
    {
        return CustomClass::class;
    }

    protected static function convertFromRaw(mixed $value) : object
    {
        try {
            return static::_convertFromRaw($value);
        } catch (ConversionError) {
            return CustomClass::fromDomain(substr($value, strrpos($value, '.')+1));
        }
    }
}
```

##### 3) Implement your own factory method

[](#3-implement-your-own-factory-method-1)

Since everything is just a trait, you of course have the option of simply creating your own and replace `fromRawValues`. If you want to keep the same name for your own method and change the signature, just alias the trait's method and make it private.

Usage:

```
$emails = EmailCollection::fromArray([
    Email::fromString('hello@there.co.uk'),
    Email::fromString('lorem@ipsum.it'),
    Email::fromString('bass@player.at'),
]);

// Alternative way of instantiating the collection, if the values
// passed can be converted to the target class.
$emails = EmailCollection::fromRawArray([
    'hello@there.co.uk',
    'lorem@ipsum.it',
    'bass@player.at',
];

// Returns ['hello@there.co.uk', 'lorem@ipsum.it', 'bass@player.at']
// This method is provided by the trait `CanBeConvertedToStringArray`
$emailsAsStrings = $emails->toStringArray();

// Returns 3
$numberOfEmails = $emails->count();

// Returns `true`, even though strings are passed. This is because `Email`
// implements the `__toString` method (via the trait `IsStringType`).
$emailsMatch = $emails->isEqualTo([
   'hello@there.co.uk',
    'lorem@ipsum.it',
    'bass@player.at',
]);
```

IsCollectionType
----------------

[](#iscollectiontype)

Use this type when the value represents an array of values and there is **no** finite list of valid values. If there is a list of valid values, use [`IsArrayEnumType`](#isarrayenumtype) (or any of the more specific variations, e.g. [`IsStringArrayEnumType`](#isstringarrayenumtype) if applicable).

**If you do not need to do any configuration, there is a generic class available, implementing this type: `FireMidge\ValueObject\Generic\AnyCollection`.**

### Combination with other types

[](#combination-with-other-types-1)

You can combine this type with any other type, e.g. to get an array of float types, an array of e-mail addresses, etc. If you need each value to be an instance of a class, consider using [`IsClassCollectionType`](#isclasscollectiontype) instead.

### Unique values

[](#unique-values-5)

If each value can only appear once in the object, you have two options:

- If you want an exception to be thrown when duplicate values are being added (either via `fromArray` or via `withValue`), then override `protected static function areValuesUnique() : bool` and return `true`. An exception of type `DuplicateValue` will be thrown.
- If you do not want an exception to be thrown but want duplicate values to simply be silently ignored (both in `fromArray` and in `withValue`), override `protected static function ignoreDuplicateValues() : bool` and return `true`. If duplicate values are found, they are only added once to the array.

When both `areValuesUnique` and `ignoreDuplicateValues` return `true`, `ignoreDuplicateValues` takes precedence.

### Validation

[](#validation-6)

You can provide custom validation by overriding `protected function validateEach(mixed $value) : void`, which is executed for each value separately, both when instantiating it and when calling `withValue`. Note that this validation will also run before `withoutValue`, `tryWithoutValue` and `contains`, so you are notified when passing something entirely invalid rather than it being silently swallowed.

**It is recommended to set up validation, at least for the value type.**

### Value transformation

[](#value-transformation)

If you want to transform the input value but not fail validation, override `protected function transformEach(mixed $value)`.

By also using the trait `CanTransformStrings`, you'll get 3 convenience methods that you can call inside `transform` if you want:

- `trimAndLowerCase(string $value)`
- `trimAndUpperCase(string $value)`
- `trimAndCapitalise(string $value)`

Example:

```
use FireMidge\ValueObject\IsCollectionType;

/**
 * @extends IsCollectionType
 */
class ProductNameCollection
{
    use IsCollectionType;
    use CanTransformStrings;

    protected function validateEach(mixed $value) : void
    {
        if (! is_string($value)) {
            throw InvalidValue::invalidType($value, 'string');
        }
    }

    protected function transformEach(mixed $value) : mixed
    {
        if (! is_string($value)) {
            return $value;
        }

        return $this->trimAndCapitalise($value);
    }
}
```

Usage:

```
// $productNames will be an instance of ProductNameCollection
// with these values: [ 'Orange juice', 'Soap', 'Shampoo' ]
$productNames = ProductNameCollection::fromArray([
    '  orange juice',
    'soap ',
    'SHAMPOO',
]);
```

###  Health Score

38

—

LowBetter than 84% of packages

Maintenance42

Moderate activity, may be stable

Popularity15

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity73

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~162 days

Recently: every ~222 days

Total

11

Last Release

500d ago

Major Versions

v1.1 → v2.02022-07-08

PHP version history (2 changes)v1.0PHP &gt;=7.3

v2.0PHP ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/7621d18f972f83906945d947061a471e97a5065c2e6b9b5acd2d8091c36b7267?d=identicon)[FireMidge](/maintainers/FireMidge)

---

Top Contributors

[![FireMidge](https://avatars.githubusercontent.com/u/4896496?v=4)](https://github.com/FireMidge "FireMidge (10 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/firemidge-value-objects/health.svg)

```
[![Health](https://phpackages.com/badges/firemidge-value-objects/health.svg)](https://phpackages.com/packages/firemidge-value-objects)
```

###  Alternatives

[symfony/polyfill-php72

Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions

4.8k674.7M31](/packages/symfony-polyfill-php72)[symfony/polyfill-intl-icu

Symfony polyfill for intl's ICU-related data and classes

2.6k251.4M96](/packages/symfony-polyfill-intl-icu)[nette/php-generator

🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.5 features.

2.2k64.2M574](/packages/nette-php-generator)[consolidation/site-process

A thin wrapper around the Symfony Process Component that allows applications to use the Site Alias library to specify the target for a remote call.

5345.3M8](/packages/consolidation-site-process)[sycho/flarum-profile-cover

Adds the ability to add a cover image to a profile.

1836.6k](/packages/sycho-flarum-profile-cover)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
