Skip to content

Instantly share code, notes, and snippets.

@alefcastelo
Last active June 21, 2021 16:59
Show Gist options
  • Save alefcastelo/2dd460ee704c6b19fc968c6dbd6c13c3 to your computer and use it in GitHub Desktop.
Save alefcastelo/2dd460ee704c6b19fc968c6dbd6c13c3 to your computer and use it in GitHub Desktop.
<?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 'alefaraujocastelo@gmail.com' === $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