diff --git a/.gitignore b/.gitignore
index 783d717..3258f4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
vendor/
.idea/
coverage.xml
+clover.xml
package.xml
coverage-html/
.phpunit/
/.php-cs-fixer.cache
+tests/.DS_Store
diff --git a/clover.xml b/clover.xml
deleted file mode 100644
index 26f192e..0000000
--- a/clover.xml
+++ /dev/null
@@ -1,1664 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/composer.json b/composer.json
index a09f12f..0a5b607 100644
--- a/composer.json
+++ b/composer.json
@@ -28,7 +28,8 @@
}
},
"scripts": {
- "ci-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=ci --configuration phpunit.xml",
+ "ci-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --testsuite=ci --configuration phpunit.xml",
+ "unit-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --configuration phpunit.xml",
"phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon --memory-limit=256M"
}
}
diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php
index 2e9b0d1..7d42eeb 100644
--- a/src/Concerns/BaseData.php
+++ b/src/Concerns/BaseData.php
@@ -9,7 +9,6 @@
use Nuxtifyts\PhpDto\Normalizers\Concerns\HasNormalizers;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline;
use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable;
-use ReflectionClass;
use Throwable;
trait BaseData
diff --git a/src/Contexts/ClassContext.php b/src/Contexts/ClassContext.php
index c7cde2e..ac52b55 100644
--- a/src/Contexts/ClassContext.php
+++ b/src/Contexts/ClassContext.php
@@ -139,7 +139,8 @@ private function syncClassAttributes(): void
/**
* @throws ReflectionException
*
- * @return T
+ * @return Data
+ * @phpstan-return T
*/
public function newInstanceWithoutConstructor(): mixed
{
@@ -149,7 +150,8 @@ public function newInstanceWithoutConstructor(): mixed
/**
* @throws ReflectionException
*
- * @return T
+ * @return Data
+ * @phpstan-return T
*/
public function newInstanceWithConstructorCall(mixed ...$args): mixed
{
diff --git a/src/Contracts/ValidateableData.php b/src/Contracts/ValidateableData.php
new file mode 100644
index 0000000..3954688
--- /dev/null
+++ b/src/Contracts/ValidateableData.php
@@ -0,0 +1,32 @@
+ $data
+ *
+ * @throws DataValidationException
+ */
+ public static function validate(array $data): void;
+
+ /**
+ * @param array $data
+ *
+ * @throws DataValidationException
+ */
+ public static function validateAndCreate(array $data): static;
+
+ /**
+ * @return true|array>
+ */
+ public function isValid(): true|array;
+
+ /**
+ * @return array
+ */
+ public static function validationRules(): array;
+}
diff --git a/src/Enums/Property/Type.php b/src/Enums/Property/Type.php
index 3953d28..ff39a8a 100644
--- a/src/Enums/Property/Type.php
+++ b/src/Enums/Property/Type.php
@@ -20,4 +20,10 @@ enum Type: string
self::BOOLEAN,
self::STRING,
];
+
+ /** @var list */
+ public const array NUMERIC_TYPES = [
+ self::FLOAT,
+ self::INT,
+ ];
}
diff --git a/src/Exceptions/DataValidationException.php b/src/Exceptions/DataValidationException.php
new file mode 100644
index 0000000..269a395
--- /dev/null
+++ b/src/Exceptions/DataValidationException.php
@@ -0,0 +1,9 @@
+ $array
- * @param string $key
* @param array $default
*
* @return array
@@ -18,6 +20,130 @@ public static function getArray(array $array, string $key, array $default = []):
return is_array($value) ? $value : $default;
}
+ /**
+ * @param array $array
+ */
+ public static function getStringOrNull(array $array, string $key): ?string
+ {
+ $value = $array[$key] ?? null;
+
+ return is_string($value) ? $value : null;
+ }
+
+ /**
+ * @param array $array
+ */
+ public static function getString(array $array, string $key, string $default = ''): string
+ {
+ return self::getStringOrNull($array, $key) ?? $default;
+ }
+
+ /**
+ * @param array $array
+ */
+ public static function getIntegerOrNull(array $array, string $key): ?int
+ {
+ $value = $array[$key] ?? null;
+
+ return is_int($value) ? $value : null;
+ }
+
+ /**
+ * @param array $array
+ */
+ public static function getInteger(array $array, string $key, int $default = 0): int
+ {
+ return self::getIntegerOrNull($array, $key) ?? $default;
+ }
+
+ /**
+ * @param array $array
+ */
+ public static function getFloatOrNull(array $array, string $key): ?float
+ {
+ $value = $array[$key] ?? null;
+
+ return is_float($value) ? $value : null;
+ }
+
+ /**
+ * @param array $array
+ */
+ public static function getFloat(array $array, string $key, float $default = 0.0): float
+ {
+ return self::getFloatOrNull($array, $key) ?? $default;
+ }
+
+ /**
+ * @param array $array
+ */
+ public static function getBooleanOrNull(array $array, string $key): ?bool
+ {
+ $value = $array[$key] ?? null;
+
+ return is_bool($value) ? $value : null;
+ }
+
+ /**
+ * @param array $array
+ */
+ public static function getBoolean(array $array, string $key, bool $default = false): bool
+ {
+ return self::getBooleanOrNull($array, $key) ?? $default;
+ }
+
+ /**
+ * @template T of BackedEnum
+ *
+ * @param array $array
+ * @param class-string $enumClass
+ * @param ?T $default
+ *
+ * @return ?T
+ */
+ public static function getBackedEnumOrNull(
+ array $array,
+ string $key,
+ string $enumClass,
+ ?BackedEnum $default = null
+ ): ?BackedEnum {
+ $value = $array[$key] ?? null;
+
+ if ($value instanceof $enumClass) {
+ return $value;
+ } else if (
+ (is_string($value) || is_integer($value))
+ && $resolvedValue = $enumClass::tryFrom($value)
+ ) {
+ return $resolvedValue;
+ }
+
+ return is_null($default)
+ ? null
+ : ($default instanceof $enumClass
+ ? $default
+ : throw new InvalidArgumentException('Default value must be an instance of ' . $enumClass)
+ );
+ }
+
+ /**
+ * @template T of BackedEnum
+ *
+ * @param array $array
+ * @param class-string $enumClass
+ * @param T $default
+ *
+ * @return T
+ */
+ public static function getBackedEnum(
+ array $array,
+ string $key,
+ string $enumClass,
+ BackedEnum $default
+ ): BackedEnum {
+ return self::getBackedEnumOrNull($array, $key, $enumClass, $default) ?? $default;
+ }
+
/**
* @param array $array
* @param class-string