<?php
namespace App\Service\ValidatorService;
use App\Controller\Api\Helpers\CustomInputBag;
use App\Controller\Api\Service\HttpStatusInterface;
use App\Entity\Unit;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\MappingException;
use phpDocumentor\Reflection\Types\Mixed_;
use phpDocumentor\Reflection\Types\This;
use SebastianBergmann\CodeUnit\ClassUnit;
use Symfony\Component\HttpFoundation\InputBag;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @template T of object
*/
class EntityHydratorValidator
{
private ValidatorInterface $validator;
/** @var T|null */
private $entity;
private ?InputBag $params;
private array $requires = [];
private ?\ReflectionClass $reflectedClass;
private ConstraintViolationListInterface $errors;
private EntityManagerInterface $entityManager;
private ?\Exception $exception = null;
public function getException(): ?\Exception
{
return $this->exception;
}
public function setException(\Exception $exception): void
{
$this->exception = $exception;
}
public function getErrors(): ConstraintViolationListInterface { return $this->errors; }
/**
* @template T of object
* @param class-string<T> $expected
* @param T $entity
* @return T
*/
public function assertHydratedEntity(string $expected, object $entity): object
{
if (!$entity instanceof $expected) {
throw new \Exception(sprintf(
'Entity should be instance of %s, %s given',
$expected,
get_class($entity)
));
}
return $entity;
}
/**
* @throws MappingException
*/
private function isNullable(string $entityClass, string $field): bool
{
$metadata = $this->entityManager->getClassMetadata($entityClass);
// Scalar field
if ($metadata->hasField($field)) {
return $metadata->isNullable($field);
}
// Association
if ($metadata->hasAssociation($field)) {
$assoc = $metadata->getAssociationMapping($field);
return $assoc['joinColumns'][0]['nullable'] ?? true;
}
throw new \InvalidArgumentException("No mapping found for field '$field'");
}
private function hasValue($value): bool
{
if ($value === null) {
return false;
}
if (is_string($value)) {
return trim($value) !== '';
}
if (is_array($value)) {
return count($value) > 0;
}
if ($value instanceof \Doctrine\Common\Collections\Collection) {
return !$value->isEmpty();
}
return true; // int, float, bool, object
}
public function __construct(ValidatorInterface $validator, EntityManagerInterface $entityManager)
{
$this->validator = $validator;
$this->entityManager = $entityManager;
}
public function reset(): self {
$this->entity = null;
$this->params = null;
$this->reflectedClass = null;
$this->exception = null;
return $this;
}
/**
* @param T $entity
* @throws \ReflectionException
*/
public function setEntity(object $entity): self {
// if($this->entity && get_class($this->entity) !== get_class($entity)) {
// throw new \RuntimeException(sprintf("Previous Entity doesn't match with curr Entity, please reset this hydrator before to use. expected %s, %s given", get_class($entity), get_class($this->entity)));
// }
$this->entity = $entity;
/**
* Proxy Entity yerine Gercek Entity
* $this->reflectedClass = new \ReflectionClass($this->entity);
* Proxy entity gonderdim: örnek -> Proxies\__CG__\App\Entity\Invoices
* Reflection proxy class’ı yansıtıyor →
* Proxy class’ta private property’ler yok →
*
* ClassUtils::getClass() şunu gördüm: App\Entity\Invoices no __CG__
* Proxy değil → gerçek entity class → property’ler görünür.
* Bu Güclü olmasina ragmen benim icin kötü oldu
* $this->reflectedClass = new \ReflectionClass($this->entity);
* Asagidaki yontemim sorunumu cözdü!!!
*/
//
$this->reflectedClass = new \ReflectionClass(
ClassUtils::getClass($this->entity)
);
return $this;
}
/** @return T|null */
public function getEntity() { return $this->entity; }
public function setParams(InputBag $params): self { $this->params = $params; return $this; }
public function setRequires(array $requires): self { $this->requires = $requires; return $this; }
private function getProperties (): array {
return array_map( fn($item ) => $item->getName(), $this->reflectedClass->getProperties() );
}
private function isPropertyExits( string $propName ): bool {
return in_array($propName, $this->getProperties());
}
/**
* @param array $fields
* @param int $uniqueEntityMessageTemplate
* @param array<string, string> $entityValueGetters
* @return string|null
* @throws \Exception
*/
public function validate(array $fields, int $uniqueEntityMessageTemplate = 0, array $entityValueGetters = [] ): ?string {
if(!isset($this->entity)){
throw new \Exception('Validation Entity required');
}
$requiredErrors = [];
// Loop through each field
foreach ($fields as $field) {
$fieldKey = $field;
// Gelen Field Degeri related olmus bir filed
if(gettype($field) === "array" ){
$fieldKey = key($field);
}
#dump($fieldKey);
if(!$this->isPropertyExits($fieldKey)){
$message = sprintf(
'Entity %s property %s not exits in class',
$this->reflectedClass->getName(),
$field
);
throw new \Exception($message . json_encode($this->reflectedClass->getProperties()));
}
// Null Control
$value = $this->params->get($fieldKey);
if ( (!$this->isNullable(ClassUtils::getClass($this->entity), $fieldKey) || in_array($fieldKey, $this->requires)) && !$this->hasValue($value)) {
$requiredErrors[] = sprintf(
// '%s::%s null olamaz',
// ClassUtils::getClass($this->entity),
'%s can not be null',
$fieldKey
);
continue;
}
// Build the setter method name in camelCase
$setter = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $fieldKey)));
// Get the value from params
$value = $this->params->get($fieldKey);
// Gelen Field Degeri related olmus bir filed
if(gettype($field) === "array" ){
$value = $field[$fieldKey];
}
// If the field is a date field, create a DateTimeImmutable instance
if (str_ends_with($fieldKey, '_at') && $value) {
$value = (new \DateTimeImmutable($value));// ->format('Y-m-d');
}
// Call the setter method
$this->entity->{$setter}($value);
}
if(count($requiredErrors)){
return implode(", ", $requiredErrors);
}
// Validate the entity
$this->errors = $this->validator->validate($this->entity);
#dump($this->errors);
if (count($this->errors)) {
// Get the UniqueEntity validation message
$parsedMessage = $this->errors[0]->getMessage();
// Split the message if it contains optional parts separated by '|'
$parsedMessage = explode('|', $parsedMessage);
$explodedMessage = $parsedMessage[$uniqueEntityMessageTemplate]; // The sprintf template
// Get the entity that caused the validation error
$entity = $this->errors[0]->getCause()[0];
// Retrieve the field values and format dates
$values = array_map(function($field) use ($entity, $entityValueGetters) {
$fieldKey = $field;
// Gelen Field Degeri related olmus bir filed
if(gettype($field) === "array" ){
$fieldKey = key($field);
}
// Tam olarak pramr icin de bir entity varsa ve o entity den getter yaparak value olumak istersen
// Tam olarak bunu yapiyor!
if(array_key_exists($fieldKey, $entityValueGetters)){
$getterProp = $entityValueGetters[$fieldKey];
$entityGetterMethod = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $getterProp)));
#dd($this->params->get($fieldKey));
return $this->params->get($fieldKey)->{$entityGetterMethod}();
}
$getter = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $fieldKey)));
$value = $entity->{$getter}();
if ($value instanceof \DateTimeInterface) {
return $value->format('Y-m-d');
}
if(gettype($value) === "object"){
try{
if (method_exists($value, 'getName')) {
$value = $value->getName();
} else {
$value = $value->getId();
}
} catch (\Exception $exception ){
return $value->getId();
}
}
return $value;
}, $fields);
#dd($explodedMessage, $values, sprintf($explodedMessage, ...$values));
// Format and return the exception message
return sprintf($explodedMessage, ...$values);
}
return null;
}
public function populateEntityFields(array $fields) {
if(!isset($this->entity)){
throw new \Exception('Entity required for fields!');
}
if(is_null($this->params)){
throw new \Exception('Params required for fields!');
}
$excludedFields = ['id', 'isNew'];
foreach ($fields as $field) {
if(in_array($field, $excludedFields)){
continue;
}
if(!$this->isPropertyExits($field)){
$message = sprintf(
'Entity %s property %s not exists in class',
$this->reflectedClass->getName(),
$field
);
throw new \Exception($message);
}
$setter = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $field)));
// Safe
if(!$this->reflectedClass->hasMethod($setter)){
throw new \Exception($setter . " Method of Entity not found");
}
#$property = $this->reflectedClass->getProperty($field);
#$type = $property->getType()->getName(); // Bool & int & String ...
$value = $this->params->get($field);
if(gettype($value) === "object" || is_array($value)){
$this->entity->{$setter}($value);
}
else if ($value !== null && trim($value) !== '') {
if (str_ends_with($field, '_at')) {
$value = new \DateTimeImmutable($value);
}
$this->entity->{$setter}($value);
}
else {
$this->entity->{$setter}(null);
}
// boş değerler için setter çağrılmaz, entity mevcut değeri korur
}
return $this->entity;
}
private function isJson(string $string): bool
{
try {
json_decode($string, false, 512, JSON_THROW_ON_ERROR);
return true;
} catch (\JsonException $e) {
return false;
}
}
/**
* @var array|string|null $data
*/
public function createInputBag($dataRaw): ?InputBag {
if(is_null($dataRaw)) return null;
if(gettype($dataRaw) === "string"){
if($this->isJson($dataRaw)){
// $data = json_decode($dataRaw, true);
$data = json_decode($dataRaw);
} else {
$data[] = $dataRaw;
}
} else {
$data = $dataRaw;
}
$ib = new InputBag();
try{
foreach ($data as $key => $value) {
$ib->set($key, $value);
}
} catch (\Exception $exception){
dd($dataRaw, $data, $exception);
}
return $ib;
}
final public function flush(): self {
try{
$this->entityManager->persist($this->entity);
$this->entityManager->flush();
$this->entityManager->refresh($this->entity);
} catch (\Exception $exception){
$this->setException($exception);
}
return $this;
}
}