entityClass = $entity; $this->alias = $alias; $this->entityResolver = Ulmus::resolveEntity($entity); if ($adapter) { $this->adapter = $adapter; $qbClass = $adapter->adapter()->queryBuilderClass(); $this->queryBuilder = new $qbClass(); } else { $this->adapter = $this->entityResolver->databaseAdapter(); $this->queryBuilder = $this->entityClass::queryBuilder(); } if ($this->adapter->cacheObject) { $this->attachCachingObject($this->adapter->cacheObject); } } public function __clone() { #$this->queryBuilder = clone $this->queryBuilder; } public function loadOne() : ? object { return $this->limit(1)->selectSqlQuery()->collectionFromQuery()[0] ?? null; } public function loadOneFromField($field, $value) : ? object { return $this->where($field, $value)->loadOne(); } public function loadFromPk($value, null|string|\Stringable $primaryKey = null) : ? object { return $primaryKey ? $this->loadOneFromField($primaryKey, $value) : $this->wherePrimaryKey($value)->loadOne(); } public function loadAll () : EntityCollection { return $this->selectSqlQuery()->collectionFromQuery(); } public function loadAllFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection { return $this->loadFromField($field, $value, $operator); } public function loadFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection { return $this->selectSqlQuery()->where($field, $value, $operator)->collectionFromQuery(); } public function count() : int { $this->removeQueryFragment([ Query\Select::class, Query\OrderBy::class, ]); if ( $this->queryBuilder->getFragment(Query\GroupBy::class) ) { $this->select( "DISTINCT COUNT(*) OVER ()" ); } else { $pk = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); if ($pk && count($pk) === 1) { $field = key($pk); $this->select(Common\Sql::function('COUNT', Common\Sql::raw("DISTINCT " . $this->entityClass::field($field)))); } else { $this->select(Common\Sql::function('COUNT', Common\Sql::raw('*'))); } } $this->selectSqlQuery(); $this->finalizeQuery(); return Ulmus::runSelectQuery($this->queryBuilder, $this->adapter)->fetchColumn(0); } public function exists(mixed $primaryKey) : bool { return $this->wherePrimaryKey($primaryKey)->count() !== 0; } public function deleteOne() { return $this->limit(1)->deleteSqlQuery()->runDeleteQuery(); } public function deleteAll() { $this->eventExecute(Event\Query\Delete::class, $this); return $this->deleteSqlQuery()->runDeleteQuery(); } public function deleteFromPk($value) : bool { if ( $value !== 0 && empty($value) ) { throw new Exception\EntityPrimaryKeyUnknown("A primary key value has to be defined to delete an item."); } return (bool) $this->wherePrimaryKey($value, false)->deleteOne()->rowCount; } public function deleteFromCompoundKeys(array $values) : bool { foreach($values as $field => $value) { $this->where($field, $value); } return (bool) $this->deleteOne()->rowCount; } public function destroy(object $entity) : bool { if ( ! $this->matchEntity($entity) ) { throw new \Exception("Your entity class `" . get_class($entity) . "` cannot match entity type of repository `{$this->entityClass}`"); } $primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); if ( $primaryKeyDefinition !== null ) { $pkField = key($primaryKeyDefinition); $this->eventExecute(Event\Query\Delete::class, $this, $entity); return $this->deleteFromPk($entity->$pkField); } else { $compoundKeyFields = Ulmus::resolveEntity($this->entityClass)->getCompoundKeyFields(); if ($compoundKeyFields) { $this->eventExecute(Event\Query\Delete::class, $this, $entity); return $this->deleteFromCompoundKeys(array_combine($compoundKeyFields->column, array_map(fn($column) => $entity->$column, $compoundKeyFields->column))); } else { throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); } } } public function destroyAll(EntityCollection $collection) : void { foreach($collection as $entity) { $this->destroy($entity); } } public function clone(object|array $entity) : object|false { $entity = is_object($entity) ? clone $entity : $entity; foreach(Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField() as $key => $field) { unset($entity->$key); } return $this->save($entity) ? $entity : false; } public function save(object|array $entity, ? array $fieldsAndValue = null, bool $replace = false) : bool { if ( is_array($entity) ) { $entity = new $this->entityClass($entity); } if ( ! $this->matchEntity($entity) ) { throw new \Exception("Your entity class `" . get_class($entity) . "` cannot match entity type of repository `{$this->entityClass}`"); } $dataset = $entity->toArray(); $primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); if ( $replace || ! $entity->isLoaded() ) { # $dataset = array_filter($dataset, fn($item, $field) => ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH); if (empty($dataset)) { throw new \InvalidArgumentException("Unable to add a new row from an empty dataset."); } $pdoObject = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery(); if ( null !== $primaryKeyDefinition ) { $pkField = key($primaryKeyDefinition); if ($pdoObject->lastInsertId ) { $dataset[$pkField] = $pdoObject->lastInsertId; } elseif ($replace) { $pkValue = $dataset[$pkField]; } } $entity->entityFillFromDataset($dataset, true); $this->eventExecute(Event\Query\Insert::class, $this, $entity, $dataset, $replace); return (bool) ( $pkValue ?? $pdoObject->lastInsertId ); } else { if ( $primaryKeyDefinition === null ) { if ( null === $compoundKeyFields = Ulmus::resolveEntity($this->entityClass)->getCompoundKeyFields() ) { throw new \Exception(sprintf("No primary key or compound key found for entity %s", $this->entityClass)); } } $diff = $fieldsAndValue ?? $this->generateWritableDataset($entity); # dump($diff); if ( [] !== $diff ) { if ($primaryKeyDefinition) { $pkField = key($primaryKeyDefinition); $pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField; $this->where($pkFieldName, $dataset[$pkFieldName]); } else { foreach($compoundKeyFields->column as $field) { $this->where($field, $dataset[$field]); } } $update = $this->updateSqlQuery($diff)->runUpdateQuery(); $entity->entityFillFromDataset($dataset, true); # $fieldsAndValue ??= &$dataset; $this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $replace); return $update ? (bool) $update->rowCount : false; } } return false; } public function saveAll(EntityCollection|array $collection) : int { $changed = 0; foreach ($collection as $entity) { $this->save($entity) && $changed++; } return $changed; } public function replace(object|array $entity, ? array $fieldsAndValue = null) : bool { return $this->save($entity, $fieldsAndValue, true); } public function replaceAll(EntityCollection|array $collection) : int { $changed = 0; foreach($collection as $entity) { $this->replace($entity) && $changed++; } return $changed; } public function createTable() : mixed { $this->eventExecute(Event\Query\Create::class, $this); return $this->createSqlQuery()->runQuery(); } public function alterTable(array $fields) : mixed { $this->eventExecute(Event\Query\Alter::class, $this, $fields); return $this->alterSqlQuery($fields)->runQuery(); } public function truncateTable() : mixed { $this->eventExecute(Event\Query\Truncate::class, $this, $table, $alias, $schema); return $this->truncate()->runQuery(); } public function listTables(? string $database = null) : mixed { return $this->showTablesSqlQuery($database)->runQuery(); } public function listColumns(? string $table = null) : EntityCollection { $table ??= $this->entityResolver->tableName(); $this->showColumnsSqlQuery($table); return $this->collectionFromQuery(Entity\InformationSchema\Column::class); } public function generateDatasetDiff(object $entity, bool $oldValues = false) : array { $array = array_change_key_case($entity->toArray()); $dataset = array_change_key_case($entity->entityGetDataset(false, true)); return array_udiff_assoc($oldValues ? $dataset : $array , $oldValues ? $array : $dataset, function($e1, $e2) { if ( is_array($e1) ) { if (is_array($e2)) { return Ulmus::encodeArray($e1) !== Ulmus::encodeArray($e2); } else { return false; } } if (is_null($e1) || is_null($e2)) { return $e1 !== $e2; } if ($e1 instanceof \BackedEnum) { $e1 = $e1->value; } if ($e2 instanceof \BackedEnum) { $e2 = $e2->value; } return (string) $e1 !== (string) $e2; }); } 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, Field::class, false)->readonly ?? false ) ) { $intersect[$field] = $field; } } return array_intersect_key($dataset, $intersect); } public function yield() : \Generator { $this->selectSqlQuery(); $this->finalizeQuery(); foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) { yield $this->instanciateEntity()->entityFillFromDataset($entityData); } } public function selectEntity(string $entity, string $alias, string $prependField = "") : self { $prependField and ($prependField .= "$"); foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { if (null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) { $this->select(sprintf("%s.$key as {$prependField}{$field->name}", $this->escapeIdentifier($alias))); } } return $this; } public function selectJsonEntity(string $entity, string $alias, bool $skipFrom = false) : self { $fieldlist = []; foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { if (null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) { $fieldlist[] = $key; $fieldlist[] = $entity::field($field->name, $this->escapeIdentifier($alias)); } } $this->select( Common\Sql::function('JSON_ARRAYAGG', Common\Sql::function('JSON_OBJECT', ... $fieldlist)) ); return $skipFrom ? $this : $this->from( $entity::resolveEntity()->tableName(), $alias, $entity::resolveEntity()->schemaName() ); } /* @TODO */ public function commit() : self { return $this; } /* @TODO */ public function rollback() : self { return $this; } public function wherePrimaryKey(mixed $value, null|string|bool $alias = self::DEFAULT_ALIAS) : self { if ( null === $primaryKeyField = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField() ) { throw new Exception\EntityPrimaryKeyUnknown("Entity has no field containing attributes 'primary_key'"); } $pkField = key($primaryKeyField); 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))); } $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 ]); $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->reflectedClass->getProperties(true)[$item]->getTypes()[0]->type; 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) { $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. if ( $this instanceof Repository\ServerRequestCountRepository ) { return $this; } if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { $this->select($this->escapeIdentifier($this->alias) . ".*"); } # Apply FILTER annotation to this too ! foreach(array_filter((array) $fields) as $item) { if ( $relation = $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]) ) { $alias = $relation->alias ?? $item; if ( $relation->isManyToMany() ) { $entity = $relation->bridge; $repository = $relation->bridge::repository(); $repository->queryBuilder->parent = $this->queryBuilder; extract(Repository\RelationBuilder::relationAnnotations($item, $relation)); $repository->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $relation->bridgeField), $relationRelation->entity::field($relationRelation->foreignKey, $alias), $relation->bridgeField) ->where( $entity::field($bridgeRelation->key, $relation->bridgeField), $entity::field($bridgeRelation->foreignKey, $this->alias)) ->selectJsonEntity($relationRelation->entity, $alias)->open(); } else { $entity = $relation->entity ?? $this->entityResolver->properties[$item]['type']; $repository = $entity::repository()->selectJsonEntity($entity, $alias)->open(); } # $relation->isManyToMany() and $repository->selectJsonEntity($relation->bridge, $relation->bridgeField, true); foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) { $repository->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); } foreach($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ] ) as $condition) { $repository->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator); } $repository->close(); $key = is_string($relation->key) ? $this->entityClass::field($relation->key) : $relation->key; if (! $relation->isManyToMany() ) { $foreignKey = is_string($relation->foreignKey) ? $entity::field($relation->foreignKey, $alias) : $relation->foreignKey; $repository->where( $foreignKey, $key); } $this->select("(" . $r = Common\Sql::raw($repository->queryBuilder->render() . ") as $item\$collection")); } else { throw new \Exception("You referenced field `$item` which do not exist or do not contain a valid @Join attribute."); } } return $this; } public function loadCollectionRelation(EntityCollection $collection, array|string $fields) : void { foreach ((array)$fields as $name) { if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] ))) { $order = $this->entityResolver->searchFieldAnnotationList($name, [ OrderBy::class ]); $where = $this->entityResolver->searchFieldAnnotationList($name, [ Where::class ]); $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 ($baseEntityResolver->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { if (null === $baseEntityResolver->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) { $repository->select($baseEntityResolver->entityClass::field($key)); } } foreach ($where as $condition) { # $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->getValue(), $condition->operator, $condition->condition); $repository->where($condition->field, $condition->getValue(/* why repository sent here ??? $this */), $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; } $values = array_unique($values); $repository->where($key, $values); $results = $repository->loadAll(); if ($relation->isOneToOne()) { foreach ($collection as $item) { $item->$name = $results->searchOne($item->$entityProperty, $property) ?: new $baseEntity(); } } elseif ($relation->isOneToMany()) { foreach ($collection as $item) { $item->$name = $baseEntity::entityCollection(); $item->$name->mergeWith($results->searchAll($item->$entityProperty, $property)); } } } } } public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self { if ($count) { $searchRequest->applyCount($this->serverRequestCountRepository()); # @TODO Handle no request to do if count is 0 here #if ($searchRequest->count) { $searchRequest->apply($this); # } } else { $searchRequest->apply($this); } return $this; } protected function serverRequestCountRepository() : Repository { return new Repository\ServerRequestCountRepository($this->entityClass, $this->alias, $this->adapter); } public function collectionFromQuery(? string $entityClass = null) : EntityCollection { $entityClass ??= $this->entityClass; $entityCollection = $entityClass::entityCollection(); $this->finalizeQuery(); $dataset = []; $this->eventExecute(\Ulmus\Event\Repository\CollectionFromQueryDatasetInterface::class, $this, $dataset); foreach($dataset ?: Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) { $this->eventExecute(\Ulmus\Event\Repository\CollectionFromQueryItemInterface::class, $entityData); $entity = $this->instanciateEntity($entityClass); $entity->loadedFromAdapter = $this->adapter->name; $entityCollection->append( $entity->entityFillFromDataset($entityData) ); } $this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $this, $entityCollection); return $entityCollection; } public function arrayFromQuery() : array { $this->selectSqlQuery(); $this->finalizeQuery(); return Ulmus::datasetQueryBuilder($this->queryBuilder, $this->adapter); } public function runQuery() : mixed { $this->finalizeQuery(); 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); } protected function fromRow($row) : self { } protected function fromCollection($rows) : self { } public function instanciateEntityCollection(...$arguments) : EntityCollection { return $this->entityClass::entityCollection(...$arguments); } public function instanciateEntity(? string $entityClass = null, array $dataset = []) : object { $entityClass ??= $this->entityClass; try { $entity = new $entityClass($dataset); } catch (\Throwable $ex) { $entity = ( new \ReflectionClass($entityClass) )->newInstanceWithoutConstructor()->fromArray($dataset); } $entity->initializeEntity(); return $entity; } public function hasFilters() : bool { return isset($this->queryBuilder->where); } protected function matchEntity(object $entity) { return $entity::class === $this->entityClass; } protected function finalizeQuery() : void {} }