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 Value | Array of Values | |
---|---|---|
List of Valid Values | IsStringEnumType IsIntEnumType IsIntStringMapType |
IsStringArrayEnumType IsIntArrayEnumType IsClassArrayEnumType IsArrayEnumType |
Any Value/Custom Validation | IsEmailType IsStringType IsFloatType IsIntType |
IsClassCollectionType IsCollectionType |
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.
Name | Implemented trait | Notes |
---|---|---|
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. |
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):
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
.
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);
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
.
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);
Use this type when the value represents a single e-mail address.
This trait uses 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('[email protected]');
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
trait instead, which will perform format validation checks.
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.
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');
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
.
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);
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
.
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);
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();
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.
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 viawithValue
), then overrideprotected static function areValuesUnique() : bool
and returntrue
. An exception of typeDuplicateValue
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 inwithValue
), overrideprotected static function ignoreDuplicateValues() : bool
and returntrue
. 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);
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.
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 viawithValue
), then overrideprotected static function areValuesUnique() : bool
and returntrue
. An exception of typeDuplicateValue
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 inwithValue
), overrideprotected static function ignoreDuplicateValues() : bool
and returntrue
. 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);
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
or 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
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());
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 viawithValue
), then overrideprotected static function areValuesUnique() : bool
and returntrue
. An exception of typeDuplicateValue
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 inwithValue
), overrideprotected static function ignoreDuplicateValues() : bool
and returntrue
. 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.)
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.
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.)
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:
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)));
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);
}
}
}
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',
]);
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
, IsIntArrayEnumType
and IsClassArrayEnumType
respectively) and where there is a list of valid values.
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
and IsArrayEnumType
over 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
, which combines IsArrayEnumType
and IsClassCollectionType
, allowing you to hold an instance of value objects. See IsClassArrayEnumType
for more information.
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 viawithValue
), then overrideprotected static function areValuesUnique() : bool
and returntrue
. An exception of typeDuplicateValue
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 inwithValue
), overrideprotected static function ignoreDuplicateValues() : bool
and returntrue
. 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.)
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<Status>
*/
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"]
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
.
If the values are not instances of a class, use IsCollectionType
.
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 viawithValue
), then overrideprotected static function areValuesUnique() : bool
and returntrue
. An exception of typeDuplicateValue
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 inwithValue
), overrideprotected static function ignoreDuplicateValues() : bool
and returntrue
. If duplicate values are found, they are only added once to the array.
When both areValuesUnique
and ignoreDuplicateValues
return true
, ignoreDuplicateValues
takes precedence.
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<Email>
*/
class EmailCollection
{
use IsClassCollectionType, CanBeConvertedToStringArray;
protected static function className() : string
{
return Email::class;
}
}
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([
'[email protected]',
'[email protected]',
'[email protected]',
]);
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.)
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:
If you only need to do a custom conversion once, you can provide a callback to the fromRawValues
method directly.
Example:
$emails = CustomCollection::fromRawArray([
'[email protected]',
'[email protected]',
'[email protected]',
], fn($v) => CustomClass::fromDomain(substr($v, strrpos($v, '.') + 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));
}
}
}
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('[email protected]'),
Email::fromString('[email protected]'),
Email::fromString('[email protected]'),
]);
// Alternative way of instantiating the collection, if the values
// passed can be converted to the target class.
$emails = EmailCollection::fromRawArray([
'[email protected]',
'[email protected]',
'[email protected]',
];
// Returns ['[email protected]', '[email protected]', '[email protected]']
// 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([
'[email protected]',
'[email protected]',
'[email protected]',
]);
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
(or any of the more specific variations, e.g. 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
.
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
instead.
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 viawithValue
), then overrideprotected static function areValuesUnique() : bool
and returntrue
. An exception of typeDuplicateValue
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 inwithValue
), overrideprotected static function ignoreDuplicateValues() : bool
and returntrue
. If duplicate values are found, they are only added once to the array.
When both areValuesUnique
and ignoreDuplicateValues
return true
, ignoreDuplicateValues
takes precedence.
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.
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<string>
*/
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',
]);