Skip to content
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,7 @@ echo $score(); //0

### Variable methods

Currently, Strictus supports single type or nullable single type.

```
🗓️ Comming soon: Union types!
```

You can use the following methods to create variables:
You can use the following methods to create single type variables:

| Type | Nullable | Method |
|------------|----------|---------------------------------------------------|
Expand Down Expand Up @@ -230,6 +224,28 @@ You can use the following methods to create variables:
| Enum Type | Yes | Strictus::enum($enumType, $value, true) |
| Enum Type | Yes | Strictus::nullableEnum($enumType, $value) |

`Strictus` also supports union types:

```php
use Strictus\Enums\Type;

$unionTypesVariable = Strictus::union([Type::INT, Type::STRING], 'foo');

echo $unionTypesVariable->value; //foo

echo $unionTypesVariable(); //foo

// Update variable

$unionTypesVariable->value = 100;

echo $unionTypesVariable->value; //100

// Thrown an exception if the value is wrong union types

$unionTypesVariable->value = false; //StrictusTypeException
```

### Immutable Variables

If you want to create immutable variables, you can use the `->immutable()` method. If you try to assign a new value
Expand Down
17 changes: 17 additions & 0 deletions src/Enums/Type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Strictus\Enums;

enum Type
{
case INT;
case STRING;
case FLOAT;
case BOOLEAN;
case ARRAY;
case OBJECT;
case INSTANCE;
case ENUM;
}
22 changes: 22 additions & 0 deletions src/Exceptions/StrictusTypeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@

namespace Strictus\Exceptions;

use Strictus\Enums\Type;
use TypeError;

/**
* @internal
*/
final class StrictusTypeException extends TypeError
{
public static function becauseInvalidSupportedType(): self
{
$typeClass = Type::class;

return new self("Type must be an enum instance of `{$typeClass}`");
}

public static function becauseNotSupportedType(string $type): self
{
return new self("Not support {$type} type");
}

public static function becauseNullInstanceType(): self
{
return new self('The instance type isn\'t nullable');
}

public static function becauseUnInstanceableType(): self
{
return new self('Can\'t detect instanceable type');
}
}
2 changes: 2 additions & 0 deletions src/Interfaces/StrictusTypeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/**
* @internal
*
* @property mixed $value
*/
interface StrictusTypeInterface
{
Expand Down
18 changes: 18 additions & 0 deletions src/Strictus.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Strictus;

use Strictus\Enums\Type;
use Strictus\Types\StrictusArray;
use Strictus\Types\StrictusBoolean;
use Strictus\Types\StrictusEnum;
Expand All @@ -12,6 +13,7 @@
use Strictus\Types\StrictusInteger;
use Strictus\Types\StrictusObject;
use Strictus\Types\StrictusString;
use Strictus\Types\StrictusUnion;

final class Strictus
{
Expand Down Expand Up @@ -94,4 +96,20 @@ public static function nullableEnum(string $enumType, mixed $enum): StrictusEnum
{
return new StrictusEnum($enumType, $enum, true);
}

/**
* @param array<int, Type> $types
*/
public static function union(array $types, mixed $value, bool $nullable = false): StrictusUnion
{
return new StrictusUnion($types, $value, $nullable);
}

/**
* @param array<int, Type> $types
*/
public static function nullableUnion(array $types, mixed $value): StrictusUnion
{
return new StrictusUnion($types, $value, true);
}
}
220 changes: 220 additions & 0 deletions src/Types/StrictusUnion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<?php

declare(strict_types=1);

namespace Strictus\Types;

use Strictus\Concerns\StrictusTyping;
use Strictus\Enums\Type;
use Strictus\Exceptions\StrictusTypeException;
use Strictus\Interfaces\StrictusTypeInterface;
use UnitEnum;

/**
* @internal
*
* @property ?StrictusTypeInterface $value
*/
final class StrictusUnion implements StrictusTypeInterface
{
use StrictusTyping;

private string $errorMessage = 'Expected Union';

private ?Type $type = null;

/** @var array<string, class-string>|null */
private ?array $instances = null;

/**
* @param array<int, Type> $types
*/
public function __construct(private array $types, mixed $value, private bool $nullable)
{
if ($this->nullable) {
$this->errorMessage .= ' Or Null';
}

$this->validateTypes($types);

$this->validate($value);

$this->value = $this->getStrictusType($value);
}

public function __invoke(mixed $value = new StrictusUndefined()): mixed
{
if ($value instanceof StrictusUndefined) {
return $this->value?->value;
}

$this->immutableValidate();

$this->validate($value);

$this->value = $this->getStrictusType($value);

return $this;
}

public function __get(string $value): mixed
{
return $this->value?->value;
}

public function __set(string $name, mixed $value): void
{
if ($name !== 'value') {
return;
}

$this->immutableValidate();

$this->validate($value);

$this->value = $this->getStrictusType($value);
}

/**
* @param array<int, Type> $types $types
*/
private function validateTypes(array $types): void
{
foreach ($types as $type) {
if (! $type instanceof Type) {
throw StrictusTypeException::becauseInvalidSupportedType();
}
}
}

private function validate(mixed $value): void
{
if ($value === null && ! $this->nullable) {
throw new StrictusTypeException($this->errorMessage);
}

$this->detectType($value);
if ($value !== null && ! in_array($this->type, $this->types, true)) {
throw new StrictusTypeException($this->errorMessage);
}
}

private function detectType(mixed $value): void
{
if (is_null($value)) {
$this->type = null;

return;
}

if (is_int($value)) {
$this->type = Type::INT;

return;
}

if (is_string($value)) {
$this->type = Type::STRING;

return;
}

if (is_float($value)) {
$this->type = Type::FLOAT;

return;
}

if (is_bool($value)) {
$this->type = Type::BOOLEAN;

return;
}

if (is_array($value)) {
$this->type = Type::ARRAY;

return;
}

if (false === is_object($value)) {
throw StrictusTypeException::becauseNotSupportedType(gettype($value));
}

$class = get_class($value);
if ('stdClass' === $class) {
$this->type = Type::OBJECT;

return;
}

if ($value instanceof UnitEnum) {
$this->type = Type::ENUM;
$this->setInstance($class);

return;
}

if (class_exists($class)) {
$this->type = Type::INSTANCE;
$this->setInstance($class);

return;
}

throw StrictusTypeException::becauseNotSupportedType(gettype($value));
}

/**
* @param class-string $instance
*/
private function setInstance(string $instance): void
{
if (null === $this->type) {
throw StrictusTypeException::becauseNullInstanceType();
}

if (Type::ENUM !== $this->type && Type::INSTANCE !== $this->type) {
throw StrictusTypeException::becauseUnInstanceableType();
}

if (isset($this->instances[$this->type->name]) && $this->instances[$this->type->name]) {
return;
}

$this->instances = [
$this->type->name => $instance,
];
}

private function getStrictusType(mixed $value): ?StrictusTypeInterface
{
return match ($this->type) {
null => null,
Type::INT => new StrictusInteger($value, $this->nullable),
Type::STRING => new StrictusString($value, $this->nullable),
Type::FLOAT => new StrictusFloat($value, $this->nullable),
Type::BOOLEAN => new StrictusBoolean($value, $this->nullable),
Type::ARRAY => new StrictusArray($value, $this->nullable),
Type::OBJECT => new StrictusObject($value, $this->nullable),
default => $this->getInstanceableStrictusType($value),
};
}

private function getInstanceableStrictusType(mixed $value): StrictusTypeInterface
{
if (null === $this->type) {
throw StrictusTypeException::becauseInvalidSupportedType();
}

if (null === $this->instances || (! isset($this->instances[$this->type->name]))) {
throw StrictusTypeException::becauseNullInstanceType();
}

return match ($this->type) {
Type::INSTANCE => new StrictusInstance($this->instances[$this->type->name], $value, $this->nullable),
Type::ENUM => new StrictusEnum($this->instances[$this->type->name], $value, $this->nullable),
default => throw StrictusTypeException::becauseUnInstanceableType(),
};
}
}
Loading