Last active
June 21, 2021 16:59
-
-
Save alefcastelo/2dd460ee704c6b19fc968c6dbd6c13c3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace AlefCastelo; | |
use Attribute; | |
use ArrayObject; | |
use Exception; | |
use ReflectionObject; | |
use ReflectionProperty; | |
use RuntimeException; | |
interface AssertInterface | |
{ | |
public function isValid(mixed $value): bool; | |
} | |
interface ValidatorInterface | |
{ | |
public function isValid(object $value): bool; | |
} | |
class ViolationsException extends RuntimeException | |
{ | |
public function __construct( | |
public Violations $violations, | |
$message = "Input is not valid." | |
) { | |
parent::__construct($message); | |
} | |
public function toArray(): array | |
{ | |
$violations = []; | |
foreach ($this->violations->getArrayCopy() as $propertyViolations) { | |
$violations[$propertyViolations->propertyName] = $propertyViolations->toArray(); | |
} | |
return $violations; | |
} | |
} | |
class Violations extends ArrayObject | |
{ | |
public function add(PropertyViolations $violations): void | |
{ | |
$this->append($violations); | |
} | |
} | |
class PropertyViolations extends ArrayObject | |
{ | |
public function __construct( | |
public string $propertyName, | |
array $violations = [] | |
) { | |
parent::__construct($violations); | |
} | |
public function add(Violation $violation): void | |
{ | |
$this->append($violation); | |
} | |
public function toArray(): array | |
{ | |
$violations = []; | |
foreach ($this->getArrayCopy() as $violation) { | |
$violations[] = [ | |
'property' => $violation->property, | |
'type' => $violation->type, | |
'value' => $violation->value, | |
]; | |
} | |
return $violations; | |
} | |
} | |
class Violation | |
{ | |
public function __construct( | |
public string $property, | |
public string $type, | |
public mixed $value | |
) { | |
} | |
} | |
#[Attribute] | |
class Required implements AssertInterface | |
{ | |
public function __construct( | |
public string $type = 'is_required' | |
) { | |
} | |
public function isValid(mixed $value): bool | |
{ | |
if (is_null($value) || empty($value)) { | |
return false; | |
} | |
return true; | |
} | |
} | |
#[Attribute] | |
class Email implements AssertInterface | |
{ | |
public function __construct( | |
public string $type = 'email_not_valid' | |
) { | |
} | |
public function isValid(mixed $value): bool | |
{ | |
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { | |
return false; | |
} | |
return true; | |
} | |
} | |
class MemberRepository | |
{ | |
public function findByEmail(string $email): bool | |
{ | |
return '[email protected]' === $email; | |
} | |
} | |
#[Attribute] | |
class EmailExists implements AssertInterface | |
{ | |
public function __construct( | |
public MemberRepository $memberRepository, | |
public string $type = 'email_in_use' | |
) { | |
} | |
public function isValid(mixed $value): bool | |
{ | |
return !$this->memberRepository->findByEmail($value); | |
} | |
} | |
class Member | |
{ | |
public function __construct( | |
#[Required] | |
public string $name, | |
#[Email] | |
#[Required] | |
#[EmailExists] | |
public string $email | |
) { | |
} | |
} | |
$validators = [ | |
Email::class => new Email(), | |
EmailExists::class => new EmailExists(new MemberRepository()), | |
Required::class => new Required() | |
]; | |
class Validator implements ValidatorInterface{ | |
public function __construct( | |
protected array $constraints | |
) { | |
} | |
public function isValid(object $value): bool | |
{ | |
$reflectionObject = new ReflectionObject($value); | |
$reflectionProperties = $reflectionObject->getProperties(); | |
$violations = new Violations(); | |
foreach ($reflectionProperties as $reflectionProperty) { | |
$reflectionProperty->setAccessible(true); | |
$propertyViolations = $this->propertyIsValid($value, $reflectionProperty); | |
if (!$propertyViolations instanceof PropertyViolations) { | |
continue; | |
} | |
$violations->add($propertyViolations); | |
} | |
if ($violations->count() >= 1) { | |
throw new ViolationsException($violations); | |
} | |
return true; | |
} | |
public function propertyIsValid(object $object, ReflectionProperty $property): ?PropertyViolations | |
{ | |
$property->setAccessible(true); | |
$value = $property->getValue($object); | |
$violations = new PropertyViolations($property->getName()); | |
foreach ($property->getAttributes() as $attribute) { | |
if (!is_a($attribute->getName(), AssertInterface::class, true)) { | |
continue; | |
} | |
$validator = $this->constraints[$attribute->getName()]; | |
if ($validator->isValid($value)) { | |
continue; | |
} | |
$violations->add(new Violation($property->getName(), $validator->type, $value)); | |
} | |
if ($violations->count() >= 1) { | |
return $violations; | |
} | |
return null; | |
} | |
} | |
$member = new Member('', ''); | |
$validator = new Validator($validators); | |
try { | |
$validator->isValid($member); | |
} catch (ViolationsException $violations) { | |
print(json_encode($violations->toArray())); // {"name":[{"property":"name","type":"is_required","value":""}],"email":[{"property":"email","type":"email_not_valid","value":""},{"property":"email","type":"is_required","value":""}]} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment