diff --git a/src/Annotation/Property/Field.php b/src/Annotation/Property/Field.php index 25d1d93..30e9519 100644 --- a/src/Annotation/Property/Field.php +++ b/src/Annotation/Property/Field.php @@ -15,7 +15,9 @@ class Field implements \Ulmus\Annotation\Annotation { public array $attributes = []; public bool $nullable; - + + public bool $readonly = false; + public function __construct(? string $type = null, ? int $length = null) { if ( $type !== null ) { diff --git a/src/Common/EntityResolver.php b/src/Common/EntityResolver.php index 1c32bd1..b973e5d 100644 --- a/src/Common/EntityResolver.php +++ b/src/Common/EntityResolver.php @@ -115,18 +115,20 @@ class EntityResolver { return null; } - public function searchFieldAnnotation(string $field, Annotation $annotationType) : ? Annotation + public function searchFieldAnnotation(string $field, Annotation $annotationType, bool $caseSensitive = true) : ? Annotation { - $found = $this->searchFieldAnnotationList($field, $annotationType); + $found = $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive); return $found ? $found[0] : null; } - public function searchFieldAnnotationList(string $field, Annotation $annotationType) : array + public function searchFieldAnnotationList(string $field, Annotation $annotationType, bool $caseSensitive = true) : array { $list = []; - - if ( null !== ( $this->properties[$field] ?? null ) ) { - foreach($this->properties[$field]['tags'] ?? [] as $tag) { + + $search = $caseSensitive ? $this->properties : array_change_key_case($this->properties, \CASE_LOWER); + + if ( null !== ( $search[$field] ?? null ) ) { + foreach($search[$field]['tags'] ?? [] as $tag) { if ( $tag['object'] instanceof $annotationType ) { $list[] = $tag['object']; } diff --git a/src/Common/PdoObject.php b/src/Common/PdoObject.php index 22197b6..462c1a8 100644 --- a/src/Common/PdoObject.php +++ b/src/Common/PdoObject.php @@ -29,7 +29,7 @@ class PdoObject extends PDO { public function runQuery(string $sql, array $parameters = []): ? PDOStatement { static::$dump && call_user_func_array(static::$dump, [ $sql, $parameters ]); - + # \debogueur([ $sql, $parameters ]); try { if (false !== ( $statement = $this->prepare($sql) )) { return $this->execute($statement, $parameters, true); @@ -42,6 +42,21 @@ class PdoObject extends PDO { return null; } + public function runInsertQuery(array $filter, array $dataset) + { + return $this->runQuery($filter, $dataset); + } + + public function runUpdateQuery(array $filter, array $dataset) + { + return $this->runQuery($filter, $dataset); + } + + public function runDeleteQuery(string $sql, array $parameters = []): ? PDOStatement + { + return $this->runQuery($sql, $parameters); + } + public function execute(PDOStatement $statement, array $parameters = [], bool $commit = true): ? PDOStatement { try { diff --git a/src/EntityTrait.php b/src/EntityTrait.php index 10f043c..bd3cd1f 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -33,12 +33,12 @@ trait EntityTrait { /**entityLoadedDataset * @Ignore */ - public function entityFillFromDataset(iterable $dataset) : self + public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self { $loaded = $this->isLoaded(); - + $entityResolver = $this->resolveEntity(); - + foreach($dataset as $key => $value) { $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null; @@ -69,7 +69,7 @@ trait EntityTrait { $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() ); if ( $annotation->length ?? null ) { - $value = substr($value, 0, $annotation->length); + $value = mb_substr($value, 0, $annotation->length); } } elseif ( $field['type'] === 'bool' ) { @@ -86,7 +86,7 @@ trait EntityTrait { 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 ( $field !== null ) { @@ -95,8 +95,11 @@ trait EntityTrait { #} $this->entityLoadedDataset = array_change_key_case($dataset, \CASE_LOWER); } + elseif ($overwriteDataset) { + $this->entityLoadedDataset = array_change_key_case($dataset, \CASE_LOWER) + $this->entityLoadedDataset; + } } - + return $this; } diff --git a/src/Query/Insert.php b/src/Query/Insert.php index 867d488..d5ce00e 100644 --- a/src/Query/Insert.php +++ b/src/Query/Insert.php @@ -4,12 +4,16 @@ namespace Ulmus\Query; class Insert extends Fragment { - const SQL_TOKEN = "INSERT"; - + const SQL_INSERT_TOKEN = "INSERT"; + + const SQL_REPLACE_TOKEN = "REPLACE"; + public int $order = -100; public bool $quick = false; - + + public bool $replace = false; + public bool $ignore = false; public array $fieldlist = []; @@ -19,11 +23,11 @@ class Insert extends Fragment { public string $table; public ? string $alias = null; - + public function render() : string { return $this->renderSegments([ - static::SQL_TOKEN, + $this->replace ? static::SQL_REPLACE_TOKEN : static::SQL_INSERT_TOKEN, ( $this->priority ?? false ), ( $this->ignore ? 'IGNORE' : false ), 'INTO', $this->table, diff --git a/src/Query/Where.php b/src/Query/Where.php index 1df0d1a..a895409 100644 --- a/src/Query/Where.php +++ b/src/Query/Where.php @@ -113,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() diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 35742d6..2b9ada3 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -64,7 +64,7 @@ class QueryBuilder implements Query\QueryBuilderInterface return $this; } - public function insert(array $fieldlist, string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self + public function insert(array $fieldlist, string $table, ? string $alias = null, ? string $database = null, ? string $schema = null, bool $replace = false) : self { if ( null === $this->getFragment(Query\Insert::class) ) { if ( $schema ) { @@ -77,7 +77,8 @@ class QueryBuilder implements Query\QueryBuilderInterface $insert = new Query\Insert(); $this->push($insert); - + + $insert->replace = $replace; $insert->fieldlist = $fieldlist; $insert->alias = $alias; $insert->table = $table; diff --git a/src/Repository.php b/src/Repository.php index 58b7b63..b54d5c8 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -56,6 +56,11 @@ class Repository return $this->collectionFromQuery(); } + public function loadAllFromField($field, $value) : EntityCollection + { + return $this->loadFromField($field, $value); + } + public function loadFromField($field, $value) : EntityCollection { return $this->where($field, $value)->collectionFromQuery(); @@ -81,12 +86,12 @@ class Repository protected function deleteOne() { - return $this->limit(1)->deleteSqlQuery()->runQuery(); + return $this->limit(1)->deleteSqlQuery()->runDeleteQuery(); } protected function deleteAll() { - return $this->deleteSqlQuery()->runQuery(); + return $this->deleteSqlQuery()->runDeleteQuery(); } public function deleteFromPk($value) : bool @@ -125,7 +130,7 @@ class Repository } } - public function save(/*object|array*/ $entity) : bool + public function save(/*object|array*/ $entity, ? array $fieldsAndValue = null, bool $replace = false) : bool { if ( is_array($entity) ) { $entity = ( new $this->entityClass() )->fromArray($entity); @@ -140,8 +145,8 @@ class Repository $primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); if ( ! $entity->isLoaded() ) { - $statement = $this->insertSqlQuery($dataset)->runQuery(); - + $statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery(); + if ( ( 0 !== $statement->lastInsertId ) && ( null !== $primaryKeyDefinition )) { @@ -156,14 +161,16 @@ class Repository throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); } - if ( [] !== $diff = $this->generateDatasetDiff($entity) ) { + $diff = $fieldsAndValue ?? $this->generateWritableDataset($entity); + + if ( [] !== $diff ) { $pkField = key($primaryKeyDefinition); $pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField; $this->where($pkFieldName, $dataset[$pkFieldName]); - $update = $this->updateSqlQuery($diff)->runQuery(); + $update = $this->updateSqlQuery($diff)->runUpdateQuery(); - $entity->entityFillFromDataset($dataset); + $entity->entityFillFromDataset($dataset, true); return $update ? (bool) $update->rowCount() : false; } @@ -182,25 +189,25 @@ class Repository 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() ) ) ) { + 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() ); + $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']; + $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 ($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) { + foreach ($order as $item) { $repository->orderBy($item->field, $item->order); } @@ -210,25 +217,25 @@ class Repository $key = is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey); - foreach($collection as $item) { + foreach ($collection as $item) { $values[] = is_callable($field) ? $field($item) : $item->$entityProperty; } $repository->where($key, $values); - switch( $relationType ) { + switch ($relationType) { case 'onetoone': - $results = call_user_func([ $repository, "loadOne" ]); + $results = call_user_func([$repository, "loadOne"]); $item->$name = $results ?: new $baseEntity(); break; case 'onetomany': - $results = call_user_func([ $repository, $relation->function ]); + $results = call_user_func([$repository, $relation->function]); - foreach($collection as $item) { + foreach ($collection as $item) { $item->$name = $baseEntity::entityCollection(); - $item->$name->mergeWith( $results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty ) ); + $item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty)); } break; @@ -237,6 +244,18 @@ class Repository } } + public function replace(/*object|array*/ $entity, ? array $fieldsAndValue = null) : bool + { + return $this->save($entity, $fieldsAndValue, true); + } + + public function replaceAll(/*EntityCollection|array*/ $collection) : void + { + foreach($collection as $entity) { + $this->replace($entity); + } + } + public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self { $schema = $schema ?: $this->entityResolver->schemaName(); @@ -255,9 +274,29 @@ class Repository return $this->createSqlQuery()->runQuery(); } - public function generateDatasetDiff(object $entity) : array + public function generateDatasetDiff(object $entity, bool $oldValues = false) : array { - return array_diff_assoc( array_change_key_case($entity->toArray()), array_change_key_case($entity->entityGetDataset(false, true)) ); + $array = array_change_key_case($entity->toArray()); + + $dataset = array_change_key_case($entity->entityGetDataset(false, true)); + + return array_diff_assoc($oldValues ? $dataset : $array , $oldValues ? $array : $dataset ); + } + + + public function generateWritableDataset(object $entity, bool $oldValues = false) : array + { + $intersect = []; + + $dataset = $this->generateDatasetDiff($entity, $oldValues); + + foreach($dataset as $field => $value) { + if ( false === ( $this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false ) ) { + $intersect[$field] = $field; + } + } + + return array_intersect_key($dataset, $intersect); } public function yield() : \Generator @@ -328,9 +367,9 @@ class Repository return $this; } - public function insert(array $fieldlist, string $table, string $alias, ? string $schema) : self + public function insert(array $fieldlist, string $table, string $alias, ? string $schema, bool $replace = false) : self { - $this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $alias, $this->escapedDatabase(), $schema); + $this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $alias, $this->escapedDatabase(), $schema, $replace); return $this; } @@ -529,6 +568,13 @@ class Repository $this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); } +<<<<<<< HEAD +======= + foreach($this->entityResolver->searchFieldAnnotationList($item, new Filter() ) as $filter) { + call_user_func_array([ $this->entityClass, $filter->method ], [ $this, $item, true ]); + } + +>>>>>>> dc5e0885512a39387b59e8a2af37c87ff0269931 $this->close(); $key = is_string($annotation->key) ? $this->entityClass::field($annotation->key) : $annotation->key; @@ -550,6 +596,13 @@ class Repository $join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->value, $condition->operator); } } +<<<<<<< HEAD +======= + + foreach($this->entityResolver->searchFieldAnnotationList($item, new FilterJoin() ) as $filter) { + call_user_func_array([ $this->entityClass, $filter->method ], [ $join, $item, true ]); + } +>>>>>>> dc5e0885512a39387b59e8a2af37c87ff0269931 }); } else { @@ -681,6 +734,27 @@ class Repository return Ulmus::runQuery($this->queryBuilder, $this->adapter); } + public function runInsertQuery() /* : mixed */ + { + $this->finalizeQuery(); + + return Ulmus::runInsertQuery($this->queryBuilder, $this->adapter); + } + + public function runUpdateQuery() /* : mixed */ + { + $this->finalizeQuery(); + + return Ulmus::runUpdateQuery($this->queryBuilder, $this->adapter); + } + + public function runDeleteQuery() /* : mixed */ + { + $this->finalizeQuery(); + + return Ulmus::runDeleteQuery($this->queryBuilder, $this->adapter); + } + public function resetQuery() : self { $this->queryBuilder->reset(); @@ -688,10 +762,13 @@ class Repository return $this; } - protected function insertSqlQuery(array $dataset) : self + protected function insertSqlQuery(array $dataset, bool $replace = false) : self { - if ( null === $this->queryBuilder->getFragment(Query\Insert::class) ) { - $this->insert(array_map([ $this, 'escapeField' ] , array_keys($dataset)), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName()); + if ( null === $insert = $this->queryBuilder->getFragment(Query\Insert::class) ) { + $this->insert(array_map([ $this, 'escapeField' ] , array_keys($dataset)), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName(), $replace); + } + else { + $insert->replace = $replace; } $this->values($dataset); diff --git a/src/Repository/ConditionTrait.php b/src/Repository/ConditionTrait.php index b904235..89447df 100644 --- a/src/Repository/ConditionTrait.php +++ b/src/Repository/ConditionTrait.php @@ -53,7 +53,7 @@ trait ConditionTrait return $this; } - public function notWhere($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self + public function notWhere($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self { $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND, true); diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php index 47b3fb2..785f10a 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -102,7 +102,7 @@ class RelationBuilder protected function applyFilter(Repository $repository, string $name) : Repository { foreach($this->filters ?? [] as $filter) { - $repository = call_user_func_array([ $this->entity, $filter->method ], [ $repository, $name ]); + $repository = call_user_func_array([ $this->entity, $filter->method ], [ $repository, $name, false ]); } return $repository; @@ -154,7 +154,8 @@ class RelationBuilder $name = strtolower($name); foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) { - if ( $key === "{$name}\$collection" ) { + + if ( $key === sprintf(static::SUBQUERY_FIELD_SUFFIX, strtolower($name)) ) { if ($value) { if ( null === ( $dataset = \json_decode($value, true) ) ) { throw new \Exception(sprintf("JSON error '%s' from '%s'", \json_last_error_msg(), $value)); @@ -166,7 +167,7 @@ class RelationBuilder return $entity::entityCollection(); } } - elseif ( substr($key, 0, $len ) === "{$name}\$" ) { + elseif ( substr($key, 0, $len ) === sprintf(static::JOIN_FIELD_SEPARATOR, strtolower($name)) ) { $vars[substr($key, $len)] = $value; } } diff --git a/src/Ulmus.php b/src/Ulmus.php index 9636c2b..07fa13c 100644 --- a/src/Ulmus.php +++ b/src/Ulmus.php @@ -79,6 +79,24 @@ abstract class Ulmus return $return; } + public static function runInsertQuery(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) + { + return static::runQuery($queryBuilder, $adapter); + } + + public static function runUpdateQuery(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) + { + return static::runQuery($queryBuilder, $adapter); + } + + public static function runDeleteQuery(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) + { + $return = static::connector($adapter)->runDeleteQuery($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? [])); + $queryBuilder->reset(); + + return $return; + } + public static function resolveEntity(string $entityClass) : Common\EntityResolver { return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass);