diff --git a/src/Attribute/Attribute.php b/src/Attribute/Attribute.php index 5a77994..52aad3e 100644 --- a/src/Attribute/Attribute.php +++ b/src/Attribute/Attribute.php @@ -7,7 +7,7 @@ use Ulmus\Repository; class Attribute { - public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed + public static function handleArrayField(null|\Stringable|string|array $field, null|string|false $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed { if ( is_array($field) ) { if (count($field) < 2) { diff --git a/src/Attribute/Property/Where.php b/src/Attribute/Property/Where.php index 8312ed6..22465ae 100644 --- a/src/Attribute/Property/Where.php +++ b/src/Attribute/Property/Where.php @@ -17,7 +17,7 @@ class Where { public null|array|\Closure $generateValue = null, ) { $this->field = Attribute::handleArrayField($field); - $this->fieldValue = Attribute::handleArrayField($fieldValue); + $this->fieldValue = Attribute::handleArrayField($fieldValue, false); } public function getValue(/* null|EntityInterface */ $entity = null) : mixed diff --git a/src/Entity/Field/Datetime.php b/src/Entity/Field/Datetime.php index bc04752..43bcf6d 100644 --- a/src/Entity/Field/Datetime.php +++ b/src/Entity/Field/Datetime.php @@ -6,5 +6,4 @@ use Ulmus\Entity\EntityObjectInterface; class Datetime extends \DateTime implements EntityObjectInterface, \JsonSerializable { use DatetimeTrait; -} - +} \ No newline at end of file diff --git a/src/EntityTrait.php b/src/EntityTrait.php index bbd2c5b..56fe7ad 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -93,7 +93,7 @@ trait EntityTrait { $this->$field = $value; } - $this->eventExecute(Event\Entity\DatasetFill::class, $this); + $this->eventExecute(Event\Entity\DatasetFill::class, $generator, $this); $this->entityDatasetUnmatchedFields = $generator->getReturn(); @@ -263,6 +263,19 @@ trait EntityTrait { } } + #[Ignore] + public function __debugInfo() : array + { + try { + return $this->toArray(true) ; + } + catch (\Throwable $ex) { + return $this->entityGetDataset(false, true); + } + + return [ "An error occured trying to export this dataset..." ]; + } + #[Ignore] public function jsonSerialize() : mixed { diff --git a/src/Event/Query/Update.php b/src/Event/Query/Update.php index 4ebb772..5ff1656 100644 --- a/src/Event/Query/Update.php +++ b/src/Event/Query/Update.php @@ -5,5 +5,5 @@ namespace Ulmus\Event\Query; use Ulmus\Repository\RepositoryInterface; interface Update { - public function execute(RepositoryInterface $repository, object|array $entity, ? array $dataset = null, bool $replace = false) : void; + public function execute(RepositoryInterface $repository, object|array $entity, array $dataset, array $diff, bool $replace = false) : void; } diff --git a/src/Migration/FieldDefinition.php b/src/Migration/FieldDefinition.php index 6174007..b08f336 100644 --- a/src/Migration/FieldDefinition.php +++ b/src/Migration/FieldDefinition.php @@ -70,6 +70,10 @@ class FieldDefinition { $default = $this->getDefault(); $syntax = $this->adapter->tableSyntax(); + if (is_callable($default)) { + $default = "(" . call_user_func($default) . ")"; + } + return implode(' ', array_filter([ $this->isUnsigned() ? $syntax['unsigned'] : null, $this->nullable ? "NULL" : "NOT NULL", diff --git a/src/Repository.php b/src/Repository.php index 4d20fc7..1bbadc4 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -8,12 +8,13 @@ use Ulmus\Attribute\Property\{ use Psr\SimpleCache\CacheInterface; use Ulmus\Common\EntityResolver; use Ulmus\Entity\EntityInterface; +use Ulmus\Repository\JoinBuilder; use Ulmus\Repository\RepositoryInterface; use Ulmus\Repository\WithOptionEnum; class Repository implements RepositoryInterface { - use EventTrait, CacheTrait, Repository\ConditionTrait, Repository\EscapeTrait, Repository\QueryBuildingTrait; + use EventTrait, CacheTrait, Repository\ConditionTrait, Repository\EscapeTrait, Repository\QueryBuildingTrait, Repository\JoinBuilderTrait; const DEFAULT_ALIAS = "this"; @@ -263,7 +264,7 @@ class Repository implements RepositoryInterface $entity->entityFillFromDataset($dataset, true); - $this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $replace); + $this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $diff, $replace); return $update ? (bool) $update->rowCount : false; } @@ -450,112 +451,6 @@ class Repository implements RepositoryInterface return $this->where($this->entityClass::field($primaryKeyField[$pkField]->name ?? $pkField, $alias), $value); } - public function withJoin(string|array $fields, array $options = []) : self - { - $canSelect = null === $this->queryBuilder->getFragment(Query\Select::class); - - if ( $canSelect ) { - $select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); - $this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select), select: true)); - } - - $fields = (array) $fields; - usort($fields, fn($e1, $e2) => str_contains($e1, '.') <=> str_contains($e2, '.')); - - # @TODO Apply fff annotation to this too ! - foreach(array_filter($fields, fn($e) => $e && ! isset($this->joined[$e]) ) as $item) { - $this->joined[$item] = true; - - $attribute = clone ($this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?: - $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ])); - - $isRelation = $attribute instanceof Relation; - - if ($isRelation && ( $attribute->isManyToMany() )) { - throw new \Exception("Many-to-many relation can not be preloaded within joins."); - } - - if ( $attribute ) { - $alias = $attribute->alias ?? $item; - $entity = $attribute->entity ?? $this->entityResolver->getPropertyEntityType($item); - - foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { - if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) { - $escAlias = $this->escapeIdentifier($alias); - $fieldName = $this->escapeIdentifier($key); - - $name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name; - - if ($canSelect) { - # @TODO add handling of the new #[Select] attribute here ! - - $this->select("$escAlias.$fieldName as $alias\${$name}"); - } - } - } - - $this->open(); - - if ( ! in_array(WithOptionEnum::SkipWhere, $options)) { - foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) { - if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) { - $this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); - } - } - } - - if ( ! in_array(WithOptionEnum::SkipHaving, $options)) { - foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) { - $this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); - } - } - - if ( ! in_array(WithOptionEnum::SkipFilter, $options)) { - foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Filter::class ]) as $filter) { - call_user_func_array([$this->entityClass, $filter->method], [$this, $item, true]); - } - } - - $this->close(); - - $key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key; - - $foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey; - - $this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $options) { - if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) { - foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) { - if ( ! is_object($condition->field) ) { - $field = $this->entityClass::field($condition->field); - } - else { - $field = clone $condition->field; - } - - # Adding directly - if ( $field->entityClass === $entity ) { - $field->alias = $alias; - - $join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->getValue(), $condition->operator); - } - } - } - - if ( ! in_array(WithOptionEnum::SkipJoinFilter, $options) ) { - foreach ($this->entityResolver->searchFieldAnnotationList($item, [ FilterJoin::class ]) as $filter) { - call_user_func_array([$this->entityClass, $filter->method], [$join, $item, true]); - } - } - }); - } - else { - throw new \Exception("Referenced field `$item` which do not exist or do not contain a valid @Join or @Relation attribute."); - } - } - - return $this; - } - public function withSubquery(string|array $fields, array $options = []) : self { # We skip subqueries when counting results since it should not affect the row count. diff --git a/src/Repository/JoinBuilderTrait.php b/src/Repository/JoinBuilderTrait.php new file mode 100644 index 0000000..b7ff4d6 --- /dev/null +++ b/src/Repository/JoinBuilderTrait.php @@ -0,0 +1,153 @@ +escapeIdentifier(static::DEFAULT_ALIAS); + + $canSelect = null === $this->queryBuilder->getFragment(Query\Select::class); + + if ( $canSelect ) { + $select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); + + $this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select), select: true)); + } + + $fields = (array) $fields; + + usort($fields, fn($e1, $e2) => str_contains($e1, '.') <=> str_contains($e2, '.')); + + # @TODO Apply fff annotation to this too ! + foreach(array_filter($fields, fn($e) => $e && ! isset($this->joined[$e]) ) as $item) { + $this->joined[$item] = true; + + $attribute = clone ($this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?: + $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ])); + + $isRelation = $attribute instanceof Relation; + + if ($isRelation && ( $attribute->isManyToMany() )) { + throw new \Exception("Many-to-many relation can not be preloaded within joins."); + } + + if ( $attribute ) { + $alias = $attribute->alias ?? $item; + $entity = $attribute->entity ?? $this->entityResolver->getPropertyEntityType($item); + + foreach($entity::resolveEntity()->fieldList(EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { + if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) { + $escAlias = $this->escapeIdentifier($alias); + $fieldName = $this->escapeIdentifier($key); + + $name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name; + + if ($canSelect) { + # @TODO add handling of the new #[Select] attribute here ! + + $this->select("$escAlias.$fieldName as $alias\${$name}"); + } + } + } + + $this->open(); + + if ( ! in_array(WithOptionEnum::SkipWhere, $options)) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) { + $field = $this->joinFieldObject($condition->field, $this->entityClass); + + if ( $field->entityClass === $this->entityClass ) { + $this->where($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator); + } + } + } + + if ( ! in_array(WithOptionEnum::SkipHaving, $options)) { + foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) { + $field = $this->joinFieldObject($condition->field, $this->entityClass); + + $this->having($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator); + } + } + + if ( ! in_array(WithOptionEnum::SkipFilter, $options)) { + foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Filter::class ]) as $filter) { + call_user_func_array([$this->entityClass, $filter->method], [ $this, $item, true ]); + } + } + + $this->close(); + + $key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key; + + $foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey; + + $this->join(Query\Join::TYPE_LEFT, $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $defaultAlias, $options) { + if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) { + $field = $this->joinFieldObject($condition->field, $this->entityClass); + + # Adding directly + if ( $field->entityClass === $entity ) { + if ($field->alias === $defaultAlias) { + $field->alias = $this->escapeIdentifier($alias); + } + + $join->where($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator); + } + } + } + + if ( ! in_array(WithOptionEnum::SkipJoinFilter, $options) ) { + foreach ($this->entityResolver->searchFieldAnnotationList($item, [ FilterJoin::class ]) as $filter) { + call_user_func_array([$this->entityClass, $filter->method], [$join, $item, true]); + } + } + }); + } + else { + throw new \Exception("Referenced field `$item` which do not exist or do not contain a valid @Join or @Relation attribute."); + } + } + + return $this; + } + + protected function joinFieldObject(mixed $field, string $entityClass, string $alias = null) : \Stringable + { + if ( ! is_object($field) ) { + return $entityClass::field($field, $alias); + } + else { + return clone $field; + } + } + + + protected function joinFieldValue(mixed $value, string $alias = null) : mixed + { + if ( $value instanceof EntityField ) { + $value = clone $value; + + $value->alias = $value->alias ?: $this->escapeIdentifier($alias); + + return $value; + } + + return $value; + } +} \ No newline at end of file diff --git a/src/SearchRequest/ParsedSearchParameter.php b/src/SearchRequest/ParsedSearchParameter.php index 7054375..d36d79f 100644 --- a/src/SearchRequest/ParsedSearchParameter.php +++ b/src/SearchRequest/ParsedSearchParameter.php @@ -15,11 +15,13 @@ class ParsedSearchParameter ) {} public function value() : mixed{ + $value = $this->value instanceof \BackedEnum ? $this->value->value : $this->value; + return match ($this->searchMethod) { - SearchMethodEnum::Like => "%$this->value%", - SearchMethodEnum::LikeLeft => "%$this->value", - SearchMethodEnum::LikeRight => "$this->value%", - default => $this->value, + SearchMethodEnum::Like => "%$value%", + SearchMethodEnum::LikeLeft => "%$value", + SearchMethodEnum::LikeRight => "$value%", + default => $value, }; } diff --git a/src/SearchRequest/SearchRequestFromRequestTrait.php b/src/SearchRequest/SearchRequestFromRequestTrait.php index 3eb848d..f082a53 100644 --- a/src/SearchRequest/SearchRequestFromRequestTrait.php +++ b/src/SearchRequest/SearchRequestFromRequestTrait.php @@ -190,7 +190,28 @@ trait SearchRequestFromRequestTrait $this->$propertyName = !empty($value); } elseif ($value !== null) { + $this->$propertyName = $this->parseValue($property, $value); +/*======= + foreach ($property->getTypes() as $type) { + $enum = $type->type; + + if (enum_exists($enum)) { + try { + $this->$propertyName = $value instanceof \UnitEnum ? $value : $type->type::from($value); + } catch (\ValueError $ex) { + $cases = implode(', ', array_map(fn($e) => $e->value, $enum::cases())); + + throw new \ValueError( + sprintf("Given value '%s' do not exists within enum '%s'. Try one of those values instead : %s", $value, $enum, $cases) + ); + } + } + elseif ($type->builtIn || $value instanceof \Stringable ) { + $this->$propertyName = $value; + } + } +>>>>>>> d917678 (- Moved withJoin() method of repository into a trait)*/ } $this->parseAttributeMethod($attribute, $field, $propertyName);