diff --git a/src/Attribute/Property/ArrayOf.php b/src/Attribute/Property/ArrayOf.php new file mode 100644 index 0000000..ef3305a --- /dev/null +++ b/src/Attribute/Property/ArrayOf.php @@ -0,0 +1,72 @@ + $item) { + if (! is_object($item)) { + if (enum_exists($this->type)) { + if ($this->type::tryFrom($item) === null) { + throw new \InvalidArgumentException( + sprintf("Given item '%s' is not a valid enum for type %s ; try one of theses instead : %s", $item, $this->type, implode("', '", array_map(fn($e) => $e->value, $this->type::cases()))) + ); + } else { + $value = $this->type::from($item); + } + } + elseif (class_exists($this->type)) { + if (is_subclass_of($this->type, JsonUnserializable::class)) { + $value = (new \ReflectionClass($this->type))->newInstanceWithoutConstructor(); + $value->jsonUnserialize($item); + } + else { + $value = new $this->type($item); + } + } + + $return[$index] = $value; + } + } + if (! $property->type->builtIn) { + $cls = $property->type->type; + + return new $cls($return); + } + + return $return; + } + + public function pull(mixed $value, ReflectedProperty $property) : mixed + { + $return = []; + + foreach($value as $index => $item) { + if (is_object($item)) { + if (enum_exists($this->type)) { + $value = $item->value; + } elseif (class_exists($this->type)) { + $value = (string) $value; + } + + $return[$index] = $value; + } + } + + return $return; + } + +} \ No newline at end of file diff --git a/src/Attribute/Property/Join.php b/src/Attribute/Property/Join.php index 817cc92..b0e409f 100644 --- a/src/Attribute/Property/Join.php +++ b/src/Attribute/Property/Join.php @@ -33,4 +33,19 @@ class Join implements ResettablePropertyInterface { return $this->foreignKey; } + + public function isOneToOne() : bool + { + return true; + } + + public function isOneToMany() : bool + { + return false; + } + + public function isManyToMany() : bool + { + return false; + } } diff --git a/src/Attribute/RegisterEntityEventInterface.php b/src/Attribute/RegisterEntityEventInterface.php new file mode 100644 index 0000000..06694f4 --- /dev/null +++ b/src/Attribute/RegisterEntityEventInterface.php @@ -0,0 +1,10 @@ + $e->getAttributes(is_object($type) ? $type::class : $type), $this->reflectedClass->getProperties(true) + )); + } + public function getPropertyEntityType(string $name) : false|string { return $this->reflectedClass->getProperties(true)[$name]->getTypes()[0]->type ?? false; diff --git a/src/Common/Sql.php b/src/Common/Sql.php index d94bbe5..0a55976 100644 --- a/src/Common/Sql.php +++ b/src/Common/Sql.php @@ -84,7 +84,7 @@ abstract class Sql { return $value; } - public static function collate(string $name) : string + public static function collate(string $name) : object { if ( ! preg_match('/^[a-z0-9$_]+$/i',$name) ) { throw new \InvalidArgumentException(sprintf("Given identifier '%s' should contains supported characters in function name (a-Z, 0-9, $ and _)", $name)); diff --git a/src/Entity/ArrayCollection.php b/src/Entity/ArrayCollection.php new file mode 100644 index 0000000..7d1c892 --- /dev/null +++ b/src/Entity/ArrayCollection.php @@ -0,0 +1,23 @@ +fromarray($json); + } + + public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self + { + foreach($datasets as $dataset) { + \ArrayObject::append($dataset); + } + + return $this; + } +} \ No newline at end of file diff --git a/src/Entity/EntityInterface.php b/src/Entity/EntityInterface.php index bde318a..3835a0e 100644 --- a/src/Entity/EntityInterface.php +++ b/src/Entity/EntityInterface.php @@ -13,7 +13,7 @@ interface EntityInterface extends SearchableInterface /* extends \JsonSerializab { public function fromArray(iterable $dataset) : static; public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false) : array; - public function toArray($includeRelations = false, array $filterFields = null) : array; + public function toArray($includeRelations = false, array $filterFields = []) : array; public function toCollection() : EntityCollection; public function isLoaded() : bool; public function jsonSerialize() : mixed; diff --git a/src/Entity/EntityObjectInterface.php b/src/Entity/EntityObjectInterface.php index 71bc84b..57ff8e6 100644 --- a/src/Entity/EntityObjectInterface.php +++ b/src/Entity/EntityObjectInterface.php @@ -3,6 +3,6 @@ namespace Ulmus\Entity; interface EntityObjectInterface { - public function load(...$arguments); + public function load(mixed ...$arguments); public function save(); } diff --git a/src/Entity/EntityValueModifier.php b/src/Entity/EntityValueModifier.php new file mode 100644 index 0000000..4a45c87 --- /dev/null +++ b/src/Entity/EntityValueModifier.php @@ -0,0 +1,8 @@ +load(...$arguments); } + elseif ( ($obj = new $type() ) instanceof JsonUnserializable ) { + $obj->jsonUnserialize(json_decode($arguments[0], true)); + + return $obj; + } else { return new $type(...$arguments); } @@ -24,8 +29,14 @@ class ObjectInstanciator { if ( $obj instanceof EntityObjectInterface ) { return $obj->save(); } - - return (string) $obj; + elseif ( $obj instanceof \JsonSerializable ) { + return json_encode($obj->jsonSerialize()); + } + elseif ( $obj instanceof \Stringable ) { + return (string) $obj; + } + + throw new \InvalidArgumentException("Object %s could not be converted as an acceptable format."); } public function enum(\UnitEnum $obj) diff --git a/src/Entity/Sqlite/Column.php b/src/Entity/Sqlite/Column.php index 236360a..de14a7c 100644 --- a/src/Entity/Sqlite/Column.php +++ b/src/Entity/Sqlite/Column.php @@ -5,11 +5,11 @@ namespace Ulmus\Entity\Sqlite; use Notes\Common\ReflectedProperty; use Ulmus\EntityCollection; -use Ulmus\{Attribute\Obj\Table}; +use Ulmus\{Attribute\Obj\Table, Entity\EntityInterface}; use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where}; #[Table] -class Column +class Column implements EntityInterface { use \Ulmus\EntityTrait; diff --git a/src/Entity/Sqlite/Schema.php b/src/Entity/Sqlite/Schema.php index 0d11f29..9663a84 100644 --- a/src/Entity/Sqlite/Schema.php +++ b/src/Entity/Sqlite/Schema.php @@ -4,11 +4,11 @@ namespace Ulmus\Entity\Sqlite; use Ulmus\EntityCollection; use Ulmus\Query\{From, Select}; -use Ulmus\{Attribute\Obj\Table, Repository, Ulmus}; +use Ulmus\{Attribute\Obj\Table, Entity\EntityInterface, Repository, Ulmus}; use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where}; #[Table(name: "sqlite_master")] -class Schema +class Schema implements EntityInterface { use \Ulmus\EntityTrait; diff --git a/src/EntityCollection.php b/src/EntityCollection.php index 6ed54ed..7e67902 100644 --- a/src/EntityCollection.php +++ b/src/EntityCollection.php @@ -11,11 +11,29 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable { public static function instance(array $data = [], null|string $entityClass = null) : self { $instance = new static($data); - $instance->entityClass = $entityClass; + + if ($entityClass) { + $instance->defineEntityClass($entityClass); + } return $instance; } + public function defineEntityClass(string $entityClass) : self + { + if ($this->entityClass !== null) { + throw new \LogicException("This methods should only be used to convert instantiated EntityCollection outside of entities."); + } + + $this->entityClass = $entityClass; + + foreach($this as &$arr) { + $arr = new $entityClass($arr); + } + + return $this; + } + public function filters(Callable $callback, bool $yieldValueOnly = false) : Generator { $idx = 0; @@ -383,7 +401,7 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable { $className = $entityClass ?: $this->entityClass; - return ( new $className() )->fromArray($dataset); + return new $className($dataset) ; } public function arrayToEntities(array $list, ? string /*stringable*/ $entityClass = null) : self diff --git a/src/EntityTrait.php b/src/EntityTrait.php index 3586891..c8c5f20 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -3,7 +3,9 @@ namespace Ulmus; use Notes\Attribute\Ignore; + use Psr\Http\Message\ServerRequestInterface; + use Ulmus\{Attribute\Obj\JsonSerialize, Attribute\Property\Join, Attribute\Property\Relation, @@ -14,11 +16,8 @@ use Ulmus\{Attribute\Obj\JsonSerialize, Entity\DatasetHandler, Entity\EntityInterface, QueryBuilder\QueryBuilderInterface}; -use Ulmus\SearchRequest\{Attribute\SearchParameter, - SearchMethodEnum, - SearchRequestInterface, - SearchRequestFromRequestTrait, - SearchRequestPaginationTrait}; + +use Ulmus\{Attribute\RegisterEntityEventInterface, Entity\EntityValueModifier, SearchRequest, Event}; #[JsonSerialize(includeRelations: true)] trait EntityTrait { @@ -48,6 +47,9 @@ trait EntityTrait { #[Ignore] public function initializeEntity(iterable|null $dataset = null) : void { + # Attributs can attach themselves on events thrown by this trait + $this->entityRegisterAttributes(); + $this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration); if ($dataset) { @@ -57,18 +59,39 @@ trait EntityTrait { $this->resetVirtualProperties(); } + #[Ignore] + public function entityRegisterAttributes() : void + { + foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) { + foreach($property->attributes as $tag) { + if ( $tag->object instanceof RegisterEntityEventInterface ) { + $tag->object->attach($this, $field); + } + } + } + } + #[Ignore] public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self { + $properties = $this->resolveEntity()->reflectedClass->getProperties(true); $loaded = $this->isLoaded(); - $handler = $this->datasetHandler->push($dataset); + $generator = $this->datasetHandler->push($dataset); + + foreach($generator as $field => $value) { + foreach($properties[$field]->attributes as $tag) { + if ( $tag->object instanceof EntityValueModifier ) { + $value = $tag->object->push($value, $properties[$field]); + } + } - foreach($handler as $field => $value) { $this->$field = $value; } - $this->entityDatasetUnmatchedFields = $handler->getReturn(); + $this->eventExecute(Event\Entity\DatasetFill::class, $this); + + $this->entityDatasetUnmatchedFields = $generator->getReturn(); # Keeping original data to diff on UPDATE query if ( ! $loaded ) { @@ -77,7 +100,7 @@ trait EntityTrait { elseif ($overwriteDataset) { $this->entityLoadedDataset = array_change_key_case(is_array($dataset) ? $dataset : iterator_to_array($dataset), \CASE_LOWER) + $this->entityLoadedDataset; } - + return $this; } @@ -91,7 +114,6 @@ trait EntityTrait { return iterator_to_array($this->entityYieldDataset($includeRelations, $rewriteValue)); } - #[Ignore] protected function entityYieldDataset(bool $includeRelations = false, bool $rewriteValue = true) : \Generator { @@ -106,13 +128,11 @@ trait EntityTrait { } } - #[Ignore] public function resetVirtualProperties() : self { foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) { foreach($property->attributes as $tag) { - if ( $tag->object instanceof ResettablePropertyInterface ) { unset($this->$field); } @@ -133,7 +153,7 @@ trait EntityTrait { } #[Ignore] - public function toArray($includeRelations = false, array $filterFields = null, bool $rewriteValue = true) : array + public function toArray($includeRelations = false, array $filterFields = [], bool $rewriteValue = true) : array { $dataset = $this->entityGetDataset($includeRelations, false, $rewriteValue); @@ -193,7 +213,7 @@ trait EntityTrait { { $rel = static::resolveEntity()->searchFieldAnnotation($name, [ Attribute\Property\Relation::class ]); - if ( $this->isLoaded() && $rel ) { + if ( $rel && $this->isLoaded() ) { return true; } @@ -250,7 +270,7 @@ trait EntityTrait { } } - if (is_object($value) && ($value instanceof JsonSerializable) ) { + if (is_object($value) && ($value instanceof \JsonSerializable) ) { $dataset[$key] = $value->jsonSerialize(); } else { diff --git a/src/Event/Entity/DatasetFill.php b/src/Event/Entity/DatasetFill.php new file mode 100644 index 0000000..1621594 --- /dev/null +++ b/src/Event/Entity/DatasetFill.php @@ -0,0 +1,7 @@ +entityClass() )->fromArray($entity); + $entity = new $this->entityClass($entity); } if ( ! $this->matchEntity($entity) ) { @@ -245,6 +245,8 @@ class Repository implements RepositoryInterface $diff = $fieldsAndValue ?? $this->generateWritableDataset($entity); +# dump($diff); + if ( [] !== $diff ) { if ($primaryKeyDefinition) { $pkField = key($primaryKeyDefinition); @@ -457,14 +459,12 @@ class Repository implements RepositoryInterface $this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select))); } - # @TODO Apply FILTER annotation to this too ! - foreach(array_filter((array) $fields) as $item) { - if ( isset($this->joined[$item]) ) { - continue; - } - else { - $this->joined[$item] = 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 = $this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?: $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]); @@ -682,9 +682,15 @@ class Repository implements RepositoryInterface { if ($count) { $searchRequest->applyCount($this->serverRequestCountRepository()); - } - $searchRequest->apply($this); + # @TODO Handle no request to do if count is 0 here + #if ($searchRequest->count) { + $searchRequest->apply($this); + # } + } + else { + $searchRequest->apply($this); + } return $this; } @@ -772,9 +778,17 @@ class Repository implements RepositoryInterface return $this->entityClass::entityCollection(...$arguments); } - public function instanciateEntity(? string $entityClass = null) : object + public function instanciateEntity(? string $entityClass = null, array $dataset = []) : object { - $entity = ( new \ReflectionClass($entityClass ?? $this->entityClass) )->newInstanceWithoutConstructor(); + $entityClass ??= $this->entityClass; + + try { + $entity = new $entityClass($dataset); + } + catch (\Throwable $ex) { + $entity = ( new \ReflectionClass($entityClass) )->newInstanceWithoutConstructor()->fromArray($dataset); + } + $entity->initializeEntity(); return $entity; diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php index 212aa3a..50d7001 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -200,7 +200,7 @@ class RelationBuilder } } - protected function instanciateEmptyEntity(string $name, Relation $relation) : object + protected function instanciateEmptyEntity(string $name, Relation|Join $relation) : object { $class = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type; @@ -208,7 +208,7 @@ class RelationBuilder } - protected function instanciateEmptyObject(string $name, Relation $relation) : object + protected function instanciateEmptyObject(string $name, Relation|Join $relation) : object { switch( true ) { case $relation->isOneToOne(): @@ -267,7 +267,7 @@ class RelationBuilder if ($vars) { if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) { - return ( new $entity() )->fromArray($data)->resetVirtualProperties(); + return ( new $entity($data) )->resetVirtualProperties(); } else { return $this->fieldIsNullable($name) ? null : new $entity(); diff --git a/src/SearchRequest/SearchRequestPaginationTrait.php b/src/SearchRequest/SearchRequestPaginationTrait.php index e8beeaa..9a0c85a 100644 --- a/src/SearchRequest/SearchRequestPaginationTrait.php +++ b/src/SearchRequest/SearchRequestPaginationTrait.php @@ -2,6 +2,8 @@ namespace Ulmus\SearchRequest; +use Ulmus\Repository; + trait SearchRequestPaginationTrait { public int $count = 0; @@ -72,4 +74,61 @@ trait SearchRequestPaginationTrait { return $this; } + + public function wheresConditions() : iterable + { + return array_filter(($this->wheresConditions ?? []) + [ + + ], fn($i) => ! is_null($i) ) + [ ]; + } + + public function wheresOperators() : iterable + { + return array_filter(($this->wheresOperators ?? []) + [ + + ], fn($i) => ! is_null($i) ) + [ ]; + } + + public function likesConditions() : iterable + { + return array_filter(($this->likesConditions ?? []) + [ + + ], fn($i) => ! is_null($i) ) + [ ]; + } + + public function likesOperators() : iterable + { + return array_filter(($this->likesOperators ?? []) + [ + + ], fn($i) => ! is_null($i) ) + [ ]; + } + + public function filter(Repository $repository) : Repository + { + return $repository; + } + + public function applyCount(Repository\RepositoryInterface $repository): SearchRequestInterface + { + $this->count = $this->filter($repository) + ->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions()) + ->likes($this->likes(), $this->likesConditions()) + ->groups($this->groups()) + ->count(); + + return $this; + } + + public function apply(Repository\RepositoryInterface $repository): SearchRequestInterface + { + $this->filter($repository) + ->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions()) + ->likes($this->likes(), $this->likesConditions()) + ->orders($this->orders()) + ->groups($this->groups()) + ->offset($this->offset()) + ->limit($this->limit()); + + return $this; + } }