From 9ad2e94d123b2bd20a6aa0b0abf23eb821601dde Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Thu, 21 Oct 2021 19:02:21 +0000 Subject: [PATCH] =?UTF-8?q?-=20Work=20done=20on=20Ulmus=20which=20were=20a?= =?UTF-8?q?dded=20within=20the=20Bottin=20de=20suppl=C3=A9ance=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Common/EntityField.php | 5 +- src/Common/EntityResolver.php | 12 +-- src/Common/Sql.php | 16 ++- src/EntityCollection.php | 28 ++++- src/EntityTrait.php | 94 +++++++++-------- src/Query/Where.php | 10 +- src/Query/WhereRawParameter.php | 7 ++ src/Repository.php | 157 +++++++++++++++++++---------- src/Repository/RelationBuilder.php | 17 ++-- 9 files changed, 219 insertions(+), 127 deletions(-) create mode 100644 src/Query/WhereRawParameter.php 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 9936711..1c32bd1 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; @@ -131,7 +121,7 @@ class EntityResolver { return $found ? $found[0] : null; } - public function searchFieldAnnotationList(string $field, Annotation $annotationType) : array + public function searchFieldAnnotationList(string $field, Annotation $annotationType) : array { $list = []; 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 475fd6d..10f043c 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -8,7 +8,7 @@ use Ulmus\Repository, Ulmus\Common\EntityField; use Ulmus\Annotation\Classes\{ Method, Table, Collation, }; -use Ulmus\Annotation\Property\{ Field, Filter, FilterJoin, Relation, OrderBy, Where, OrWhere, Join, Virtual, On, WithJoin, }; +use Ulmus\Annotation\Property\{ Field, Filter, Relation, OrderBy, Where, Join, Virtual, On, WithJoin, }; use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, Bigint, Tinyint, Text, Mediumtext, Longtext, }; use Ulmus\Annotation\Property\Relation\{ Ignore as RelationIgnore }; @@ -29,37 +29,8 @@ trait EntityTrait { * @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) : self @@ -70,11 +41,9 @@ 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 ) { throw new \Exception("Field `$key` can not be found within your entity ".static::class); @@ -98,23 +67,28 @@ trait EntityTrait { if ( $field['type'] === 'string' ) { $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() ); - + if ( $annotation->length ?? null ) { $value = substr($value, 0, $annotation->length); } } 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 || $isLoadedDataset ) { + if ( ! $loaded /* || $isLoadedDataset */ ) { #if ( $field !== null ) { # $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() ); # $this->entityLoadedDataset[$annotation ? $annotation->name : $field['name']] = $dataset; # <--------- THIS TO FIX !!!!!! @@ -250,7 +224,36 @@ trait EntityTrait { /** * @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()); } @@ -306,6 +309,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 851ec1d..1df0d1a 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"; @@ -114,7 +113,7 @@ class Where extends Fragment { } # whitelisting operators - return in_array(strtoupper($this->operator), [ '=', '!=', '<>', 'LIKE', 'IS', 'IS NOT' ]) ? $this->operator : "="; + return in_array(strtoupper($this->operator), [ '=', '!=', '<>', '>', '<', '>=', '<=', 'LIKE', 'IS', 'IS NOT' ]) ? $this->operator : "="; } protected function value() @@ -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 @@ +removeQueryFragment(Query\Select::class); + $this->removeQueryFragment($this->queryBuilder->getFragment(Query\Select::class)); if ( $this->queryBuilder->getFragment(Query\GroupBy::class) ) { $this->select( "DISTINCT COUNT(*) OVER ()" ); @@ -79,12 +79,12 @@ class Repository return Ulmus::runSelectQuery($this->queryBuilder, $this->adapter)->fetchColumn(0); } - public function deleteOne() + protected function deleteOne() { return $this->limit(1)->deleteSqlQuery()->runQuery(); } - public function deleteAll() + protected function deleteAll() { return $this->deleteSqlQuery()->runQuery(); } @@ -125,8 +125,12 @@ class Repository } } - public function save(object $entity, ? array $fieldsAndValue = null) : bool + public function save(/*object|array*/ $entity) : bool { + if ( is_array($entity) ) { + $entity = ( new $this->entityClass() )->fromArray($entity); + } + if ( ! $this->matchEntity($entity) ) { throw new \Exception("Your entity class `" . get_class($entity) . "` cannot match entity type of repository `{$this->entityClass}`"); } @@ -136,7 +140,7 @@ class Repository $primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); if ( ! $entity->isLoaded() ) { - $statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset)->runQuery(); + $statement = $this->insertSqlQuery($dataset)->runQuery(); if ( ( 0 !== $statement->lastInsertId ) && ( null !== $primaryKeyDefinition )) { @@ -152,9 +156,7 @@ class Repository throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); } - $diff = $fieldsAndValue ?? $this->generateDatasetDiff($entity); - - if ( [] !== $diff ) { + if ( [] !== $diff = $this->generateDatasetDiff($entity) ) { $pkField = key($primaryKeyDefinition); $pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField; $this->where($pkFieldName, $dataset[$pkFieldName]); @@ -170,13 +172,71 @@ class Repository return false; } - public function saveAll(EntityCollection $collection) : void + public function saveAll(/*EntityCollection|array*/ $collection) : void { foreach($collection as $entity) { $this->save($entity); } } + 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 truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self { $schema = $schema ?: $this->entityResolver->schemaName(); @@ -213,6 +273,13 @@ class Repository } } + public function removeQueryFragment(? Query\Fragment $fragment) : self + { + $fragment && $this->queryBuilder->removeFragment($fragment); + + return $this; + } + public function selectEntity(string $entity, string $alias, string $prependField = "") : self { $prependField and ($prependField .= "$"); @@ -430,6 +497,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); @@ -461,10 +529,6 @@ class Repository $this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); } - foreach($this->entityResolver->searchFieldAnnotationList($item, new Filter() ) as $filter) { - call_user_func_array([ $this->entityClass, $filter->method ], [ $this, $item ]); - } - $this->close(); $key = is_string($annotation->key) ? $this->entityClass::field($annotation->key) : $annotation->key; @@ -486,10 +550,6 @@ class Repository $join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->value, $condition->operator); } } - - foreach($this->entityResolver->searchFieldAnnotationList($item, new FilterJoin() ) as $filter) { - call_user_func_array([ $this->entityClass, $filter->method ], [ $join, $item ]); - } }); } else { @@ -562,6 +622,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; @@ -655,7 +739,7 @@ class Repository public function createSqlQuery() : self { if ( null === $this->queryBuilder->getFragment(Query\Create::class) ) { - $this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); + $this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList()), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); } if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) { @@ -667,16 +751,6 @@ class Repository return $this; } - public function alterSqlQuery(array $fields) : self - { - if ( null === $this->queryBuilder->getFragment(Query\Alter::class) ) { - $this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); - } - - - return $this; - } - protected function fromRow($row) : self { @@ -758,29 +832,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 6df3d15..47b3fb2 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -9,10 +9,6 @@ use Closure; class RelationBuilder { - const SUBQUERY_FIELD_SUFFIX = "%s\$collection"; - - const JOIN_FIELD_SEPARATOR = "%s\$"; - protected Repository $repository; protected /*object|string*/ $entity; @@ -118,7 +114,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,8 +151,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, $name) ) { + if ( $key === "{$name}\$collection" ) { if ($value) { if ( null === ( $dataset = \json_decode($value, true) ) ) { throw new \Exception(sprintf("JSON error '%s' from '%s'", \json_last_error_msg(), $value)); @@ -168,7 +166,7 @@ class RelationBuilder return $entity::entityCollection(); } } - elseif ( substr($key, 0, $len ) === sprintf(static::JOIN_FIELD_SEPARATOR, $name) ) { + elseif ( substr($key, 0, $len ) === "{$name}\$" ) { $vars[substr($key, $len)] = $value; } } @@ -227,6 +225,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); }