Skip to content

Latest commit

 

History

History
191 lines (147 loc) · 4.63 KB

types.md

File metadata and controls

191 lines (147 loc) · 4.63 KB

Providing Type Information

One goal of this library is to make it possible to provide type information for static analysis and for IDEs. Almost every interface and implementation in the library provides type templates to make this possible, and the few extension points exist for the purpose of allowing users to provide type information.

Results

Recall that the ValidationResult class declares the following as part of its definition:

namespace Phly\RuleValidation;

/**
 * @template T
 */
interface ValidationResult
{
    /** @return T */
    public function value(): mixed;
}

This means that you can indicate that a result wraps a value of a specific type:

/** @var ValidationResult<DateTimeInterface> $date */
echo $date->value()->format('Y-m-d');

The Result type declares this same template, so you can also write the above as follows if you are using the shipped result type:

/** @var Result<DateTimeInterface> $date */
echo $date->value()->format('Y-m-d');

When you write your own implementations, implement the template:

/** @template-implements ValidationResult<int> */
class IntegerResult implements ValidationResult
{
    // ...
}

What if you want to allow multiple types?

/**
 * @template T of int|float
 * @template-implements ValidationResult<T>
 */
class NumericResult implements ValidationResult
{
    /** @return T */
    public function value(): int|float
    {
    }

    // ...
}

You can then hint on these types:

/** @var IntegerResult $count */
$count = $form->count;

/** @var NumericResult<float> $currency */
$currency = $form->currency;

Result Sets

As noted in the ResultSet reference, all the methods are marked final; why extend it then?

To provide typehints for the composed results!

/**
 * @property-read TextResult $title
 * @property-read NumericResult<float> $amount
 */
class TransactionFormResult extends ResultSet
{
}

This will allow you to annotate instances:

/** @var TransactionFormResult $result */
$result = $ruleSet->validate($data);

// Static analysis and IDEs will then understand that
// $amount is a float:
$amount = $result->amount->value();

It also gives you a value you can use with rule sets.

Rules

When defining a rule, ensure your validate(), default(), and missing() methods declare what they return.

You could define the more specific ValidationResult implementation they return:

public function validate(mixed $value, array $context): IntegerResult;

Or you could provide a return annotation that declares the ValidationResult type:

/**
 * @return Result<int>
 */
public function validate(mixed $value, array $context): Result

Rule Sets

The RuleSet class defines @template T of ResultSet. This gives you a couple possibilities.

First, when using the RuleSetOptions, you can provide the result set class to use, and you can then hint to static analysis and IDEs what you are using:

$options = new RuleSetOptions();
$options->setResultSetClass(TransactionFormResult::class);
$options->addRule(/* ... */);

/** @var RuleSet<TransactionFormResult> $ruleSet */
$ruleSet = new RuleSet($options);

Alternately, you could extend the class:

/**
 * @template-extends RuleSet<TransactionFormResult>
 */
class TransactionForm extends RuleSet
{
    public function __construct()
    {
        $options = new ResultSetOptions();
        $options->setResultSetClass(TransactionFormResult::class);
        $options->addRule(/* ... */);

        parent::__construct($options);
    }
}

Setting the options explicitly in the constructor of the extending class ensures you know the state. If you want to prevent child classes redefining the constructor and changing the semantics and behavior of your ruleset, create a named constructor:

/**
 * @template-extends RuleSet<TransactionFormResult>
 */
class TransactionForm extends RuleSet
{
    public static function create(): self
    {
        $options = new ResultSetOptions();
        $options->setResultSetClass(TransactionFormResult::class);
        $options->addRule(/* ... */);

        return new self($options);
    }
}

Alternately, you can also make your constructor final.

If you are creating an anonymous class extending RuleSet, use the @template-extends annotation:

/** @var RuleSet<TransactionForm> $ruleSet */
$ruleSet = new /** @template-extends RuleSet<TransactionFormResult> */ class($options) extends RuleSet {
    // ...
};