diff --git a/src/Common/EntityField.php b/src/Common/EntityField.php index a9ce096..4847005 100644 --- a/src/Common/EntityField.php +++ b/src/Common/EntityField.php @@ -6,9 +6,10 @@ use Ulmus\Annotation\Annotation; use Ulmus\Migration\FieldDefinition; use Ulmus\Ulmus, Ulmus\Adapter\AdapterInterface, - Ulmus\Annotation\Property\Field; + Ulmus\Annotation\Property\Field, + Ulmus\Query\WhereRawParameter; -class EntityField +class EntityField implements WhereRawParameter { public string $name; diff --git a/src/Common/EntityResolver.php b/src/Common/EntityResolver.php index 8aef889..b973e5d 100644 --- a/src/Common/EntityResolver.php +++ b/src/Common/EntityResolver.php @@ -83,16 +83,6 @@ class EntityResolver { throw new \InvalidArgumentException("Given `fieldKey` is unknown to the EntityResolver"); } - if ($escape) { - if ( isset($tag['object']->name) ) { - $tag['object']->name = 2; - } - - if ( isset($item['name']) ) { - $item['name'] = 2; - } - } - $fieldList[$key] = $item; break; @@ -135,7 +125,7 @@ class EntityResolver { { $list = []; - $search = $caseSensitive ? $this->properties : array_change_key_case($this->properties, \CASE_LOWER); + $search = $caseSensitive ? $this->properties : array_change_key_case($this->properties, \CASE_LOWER); if ( null !== ( $search[$field] ?? null ) ) { foreach($search[$field]['tags'] ?? [] as $tag) { diff --git a/src/Common/Sql.php b/src/Common/Sql.php index 1d5c01c..cd02dc3 100644 --- a/src/Common/Sql.php +++ b/src/Common/Sql.php @@ -2,11 +2,13 @@ namespace Ulmus\Common; +use Ulmus\Query\WhereRawParameter; + abstract class Sql { public static function function($name, ...$arguments) { - return new class($name, ...$arguments) { + return new class($name, ...$arguments) implements WhereRawParameter { protected string $as = ""; @@ -20,19 +22,23 @@ abstract class Sql { $this->parseArguments(); } - public function __toString() { + public function __toString() : string + { return implode(' ', array_filter([ "{$this->name}(" . implode(", ", $this->arguments) . ")", $this->as ? "AS {$this->as}" : false, ])); } - public function as($fieldName) { + public function as($fieldName) : self + { $this->as = $fieldName; + return $this; } - protected function parseArguments() { + protected function parseArguments() : void + { foreach($this->arguments as &$item) { $item = Sql::escape($item); } @@ -42,7 +48,7 @@ abstract class Sql { public static function identifier(string $identifier) : object { - return new class($identifier) { + return new class($identifier) implements WhereRawParameter { protected string $identifier; diff --git a/src/EntityCollection.php b/src/EntityCollection.php index 8e84639..0ef38e8 100644 --- a/src/EntityCollection.php +++ b/src/EntityCollection.php @@ -87,6 +87,13 @@ class EntityCollection extends \ArrayObject { return $removed; } + public function clear() : self + { + $this->exchangeArray([]); + + return $this; + } + public function search($value, string $field, bool $strict = true) : Generator { foreach($this->filters(fn($v) => isset($v->$field) ? ( $strict ? $v->$field === $value : $v->$field == $value ) : false) as $key => $item) { @@ -134,6 +141,11 @@ class EntityCollection extends \ArrayObject { return $obj; } + public function searchInstances(string $className) : self + { + return $this->filtersCollection(fn($obj) => is_a($obj, $className)); + } + public function column($field, bool $unique = false) : array { $list = []; @@ -146,8 +158,8 @@ class EntityCollection extends \ArrayObject { $value = $item->$field; } - if ($unique && in_array($value, $list)) { - break; + if ($unique && in_array($value, $list, true)) { + continue; } $list[] = $value; @@ -279,6 +291,13 @@ class EntityCollection extends \ArrayObject { } } + public function push($value) : self + { + $this->append($value); + + return $this; + } + public function mergeWith( /*array|EntityCollection*/ $datasets ) : self { if ( is_object($datasets) ) { @@ -331,6 +350,11 @@ class EntityCollection extends \ArrayObject { return $this; } + public function rsort(callable $callback, $function = "uasort") : self + { + return $this->sort(...func_get_args())->reverse(); + } + public function pop() /* : mixed */ { $arr = $this->getArrayCopy(); diff --git a/src/EntityTrait.php b/src/EntityTrait.php index 23f93fe..4487ed1 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -14,52 +14,23 @@ use Ulmus\Annotation\Property\Relation\{ Ignore as RelationIgnore }; trait EntityTrait { use EventTrait; - + /** * @Ignore */ protected bool $entityStrictFieldsDeclaration = false; - + /** * @Ignore */ protected array $entityDatasetUnmatchedFields = []; - + /** * @Ignore */ public array $entityLoadedDataset = []; - /** - * @Ignore - */ - public function __get(string $name) - { - $relation = new Repository\RelationBuilder($this); - - if ( false !== $data = $relation->searchRelation($name) ) { - return $this->$name = $data; - } - - throw new \Exception(sprintf("[%s] - Undefined variable: %s", static::class, $name)); - } - - /** - * @Ignore - */ - public function __isset(string $name) : bool - { - #if ( null !== $relation = static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) { - # return isset($this->{$relation->key}); - #} - if ( $this->isLoaded() && static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) { - return true; - } - - return isset($this->$name); - } - - /** + /**entityLoadedDataset * @Ignore */ public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self @@ -71,9 +42,7 @@ trait EntityTrait { foreach($dataset as $key => $value) { $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null; - if ( $field === null ) { - $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false); - } + $field ??= $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false); if ( $field === null ) { if ($this->entityStrictFieldsDeclaration ) { @@ -104,17 +73,22 @@ trait EntityTrait { } } elseif ( $field['type'] === 'bool' ) { - $this->{$field['name']} = (bool) $value; + $value = (bool) $value; } $this->{$field['name']} = $value; } elseif ( ! $field['builtin'] ) { - $this->{$field['name']} = Ulmus::instanciateObject($field['type'], [ $value ]); + try { + $this->{$field['name']} = Ulmus::instanciateObject($field['type'], [ $value ]); + } + catch(\Error $e) { + throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field['name'])); + } } # Keeping original data to diff on UPDATE query - if ( ! $loaded ) { + if ( ! $loaded /* || $isLoadedDataset */ ) { #if ( $field !== null ) { # $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() ); # $this->entityLoadedDataset[$annotation ? $annotation->name : $field['name']] = $dataset; # <--------- THIS TO FIX !!!!!! @@ -143,7 +117,7 @@ trait EntityTrait { return $this; } - + /** * @Ignore */ @@ -151,7 +125,7 @@ trait EntityTrait { { return $this->entityFillFromDataset($dataset); } - + /** * @Ignore */ @@ -160,53 +134,53 @@ trait EntityTrait { if ( $returnSource ) { return $this->entityLoadedDataset; } - + $dataset = []; - + $entityResolver = $this->resolveEntity(); - + foreach($entityResolver->fieldList(Common\EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) { $annotation = $entityResolver->searchFieldAnnotation($key, new Field() ); - if ( isset($this->$key) ) { + if ( isset($this->$key) ) { $realKey = $annotation->name ?? $key; - + switch (true) { case is_object($this->$key): $dataset[$realKey] = Ulmus::convertObject($this->$key); - break; - - case is_array($this->$key): + break; + + case is_array($this->$key): $dataset[$realKey] = Ulmus::encodeArray($this->$key); - break; + break; case is_bool($this->$key): $dataset[$realKey] = (int) $this->$key; - break; - + break; + default: - $dataset[$realKey] = $this->$key; + $dataset[$realKey] = $this->$key; } } elseif ( $field['nullable'] ) { $dataset[ $annotation->name ?? $key ] = null; } } - + # @TODO Must fix recursive bug ! if ($includeRelations) { foreach($entityResolver->properties as $name => $field){ $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ); - + if ( $relation && isset($this->$name) && ($relation->entity ?? $relation->bridge) !== static::class ) { if ( null !== $value = $this->$name ?? null ) { if ( is_iterable($value) ) { $list = []; - + foreach($value as $entity) { $list[] = $entity->entityGetDataset(false); } - + $dataset[$name] = $list; } elseif ( is_object($value) ) { @@ -215,11 +189,11 @@ trait EntityTrait { } } } - } - + } + return $dataset; } - + /** * @Ignore */ @@ -227,7 +201,7 @@ trait EntityTrait { { return $this->entityGetDataset($includeRelations); } - + /** * @Ignore */ @@ -235,7 +209,7 @@ trait EntityTrait { { return static::entityCollection([ $this ]); } - + /** * @Ignore */ @@ -244,16 +218,45 @@ trait EntityTrait { if ( null === $pkField = $this->resolveEntity()->getPrimaryKeyField($this) ) { throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class)); } - + $key = key($pkField); - + return isset($this->$key); } /** * @Ignore */ - public function __sleep() + public function __get(string $name) + { + $relation = new Repository\RelationBuilder($this); + + if ( false !== $data = $relation->searchRelation($name) ) { + return $this->$name = $data; + } + + throw new \Exception(sprintf("[%s] - Undefined variable: %s", static::class, $name)); + } + + /** + * @Ignore + */ + public function __isset(string $name) : bool + { + #if ( null !== $relation = static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) { + # return isset($this->{$relation->key}); + #} + if ( $this->isLoaded() && static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) { + return true; + } + + return isset($this->$name); + } + + /** + * @Ignore + */ + public function __sleep() { return array_keys($this->resolveEntity()->fieldList()); } @@ -279,9 +282,9 @@ trait EntityTrait { */ public function jsonSerialize() { - return $this->entityGetDataset(); + return $this->entityGetDataset(); } - + /** * @Ignore */ @@ -309,6 +312,15 @@ trait EntityTrait { return $collection; } + + /** + * @Ignore + */ + public static function queryBuilder() : QueryBuilder + { + return Ulmus::queryBuilder(static::class); + } + /** * @Ignore */ diff --git a/src/Query/Where.php b/src/Query/Where.php index f9e81db..a895409 100644 --- a/src/Query/Where.php +++ b/src/Query/Where.php @@ -2,8 +2,7 @@ namespace Ulmus\Query; -use Ulmus\Common\EntityField, - Ulmus\Common\Sql; +use Ulmus\Common\Sql; class Where extends Fragment { const OPERATOR_LIKE = "LIKE"; @@ -136,10 +135,11 @@ class Where extends Fragment { { if ( $value === null ) { $this->operator = in_array($this->operator, [ '!=', '<>' ]) ? Where::COMPARISON_IS . " " . Where::CONDITION_NOT : Where::COMPARISON_IS; + return Where::COMPARISON_NULL; } - elseif ( is_object($value) && ( $value instanceof EntityField ) ) { - return $value->name(); + elseif (is_object($value) && ( $value instanceof WhereRawParameter ) ) { + return (string) $value; } else { return $this->queryBuilder->addParameter($value); diff --git a/src/Query/WhereRawParameter.php b/src/Query/WhereRawParameter.php new file mode 100644 index 0000000..ed876ba --- /dev/null +++ b/src/Query/WhereRawParameter.php @@ -0,0 +1,7 @@ + ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH); $statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery(); - + if ( ( 0 !== $statement->lastInsertId ) && ( null !== $primaryKeyDefinition )) { @@ -183,23 +183,82 @@ class Repository return false; } + public function saveAll(EntityCollection $collection) : int + { + $changed = 0; + + foreach ($collection as $entity) { + $this->save($entity) && $changed++; + } + + return $changed; + + } + + public function loadCollectionRelation(EntityCollection $collection, /*array|string*/ $fields) : void + { + foreach ((array)$fields as $name) { + if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation()))) { + $relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type)); + + $order = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\OrderBy()); + $where = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\Where()); + + $baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type']; + $baseEntityResolver = $baseEntity::resolveEntity(); + + $property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name']; + $entityProperty = ($this->entityResolver->field($relation->key, 01, false) ?: $this->entityResolver->field($relation->key, 02))['name']; + + $repository = $baseEntity::repository(); + + foreach ($where as $condition) { + $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->value, $condition->operator, $condition->condition); + } + + foreach ($order as $item) { + $repository->orderBy($item->field, $item->order); + } + + $field = $relation->key; + + $values = []; + + $key = is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey); + + foreach ($collection as $item) { + $values[] = is_callable($field) ? $field($item) : $item->$entityProperty; + } + + $repository->where($key, $values); + + switch ($relationType) { + case 'onetoone': + $results = call_user_func([$repository, "loadOne"]); + $item->$name = $results ?: new $baseEntity(); + + break; + + case 'onetomany': + $results = call_user_func([$repository, $relation->function]); + + foreach ($collection as $item) { + $item->$name = $baseEntity::entityCollection(); + $item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty)); + } + + break; + } + } + } + } + public function replace(/*object|array*/ $entity, ? array $fieldsAndValue = null) : bool { return $this->save($entity, $fieldsAndValue, true); } - public function saveAll(EntityCollection $collection) : int - { - $changed = 0; - - foreach($collection as $entity) { - $this->save($entity) && $changed++; - } - - return $changed; - } - - public function replaceAll(EntityCollection $collection) : void + public function replaceAll(/*EntityCollection|array*/ $collection) : void { foreach($collection as $entity) { $this->replace($entity); @@ -262,6 +321,13 @@ class Repository } } + public function removeQueryFragment(/*? Query\Fragment|string*/ $fragment) : self + { + $fragment && $this->queryBuilder->removeFragment($fragment); + + return $this; + } + public function selectEntity(string $entity, string $alias, string $prependField = "") : self { $prependField and ($prependField .= "$"); @@ -479,6 +545,7 @@ class Repository $this->select("{$this->alias}.*"); } + # @TODO Apply FILTER annotation to this too ! foreach(array_filter((array) $fields) as $item) { $annotation = $this->entityResolver->searchFieldAnnotation($item, new Join) ?: $this->entityResolver->searchFieldAnnotation($item, new Relation); @@ -611,6 +678,30 @@ class Repository return $this; } + public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self + { + if ($count) { + $searchRequest->count = $searchRequest->filter($this->serverRequestCountRepository()) + ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND) + ->likes($searchRequest->likes(), Query\Where::CONDITION_OR) + ->groups($searchRequest->groups()) + ->count(); + } + + return $searchRequest->filter($this) + ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND) + ->likes($searchRequest->likes(), Query\Where::CONDITION_OR) + ->orders($searchRequest->orders()) + ->groups($searchRequest->groups()) + ->offset($searchRequest->offset()) + ->limit($searchRequest->limit()); + } + + protected function serverRequestCountRepository() : Repository + { + return new Repository\ServerRequestCountRepository($this->entityClass, $this->alias, $this->adapter); + } + public function collectionFromQuery(? string $entityClass = null) : EntityCollection { $class = $entityClass ?: $this->entityClass; @@ -831,29 +922,4 @@ class Repository } protected function finalizeQuery() : void {} - - public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self - { - if ($count) { - # @TODO Must be placed inside an event instead of directly there ! - $searchRequest->count = $searchRequest->filter($this->serverRequestCountRepository()) - ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND) - ->likes($searchRequest->likes(), Query\Where::CONDITION_OR) - ->groups($searchRequest->groups()) - ->count(); - } - - return $searchRequest->filter($this) - ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND) - ->likes($searchRequest->likes(), Query\Where::CONDITION_OR) - ->orders($searchRequest->orders()) - ->groups($searchRequest->groups()) - ->offset($searchRequest->offset()) - ->limit($searchRequest->limit()); - } - - protected function serverRequestCountRepository() : Repository - { - return new Repository\ServerRequestCountRepository($this->entityClass, $this->alias, $this->adapter); - } } \ No newline at end of file diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php index 2d5470c..8024250 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -118,7 +118,7 @@ class RelationBuilder $this->repository->open(); foreach($this->wheres as $condition) { - $this->repository->where($condition->field, $condition->value, $condition->operator); + $this->repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [ $this->entity ]) : $condition->value, $condition->operator); } $this->repository->close(); @@ -155,7 +155,10 @@ class RelationBuilder $entity = $annotation->entity ?? $this->resolver->properties[$name]['type']; } + $name = strtolower($name); + foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) { + if ( $key === sprintf(static::SUBQUERY_FIELD_SUFFIX, strtolower($name)) ) { if ($value) { if ( null === ( $dataset = \json_decode($value, true) ) ) { @@ -227,6 +230,11 @@ class RelationBuilder })->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} ); + + $this->applyWhere(); + + $this->applyOrderBy(); + if ($selectBridgeField && $relation->bridgeField) { $this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias); }