entityClass = $entity; $this->alias = $alias; $this->entityResolver = Ulmus::resolveEntity($entity); $this->adapter = $adapter ?? $this->entityResolver->databaseAdapter(); $this->queryBuilder = new QueryBuilder(); } public function __clone() { #$this->queryBuilder = clone $this->queryBuilder; } public function loadOne() : ? object { return $this->limit(1)->collectionFromQuery()[0] ?? null; } public function loadOneFromField($field, $value) : ? object { return $this->where($field, $value)->loadOne(); } public function loadFromPk($value, /* ? stringable */ $primaryKey = null) : ? object { return $primaryKey ? $this->loadOneFromField($primaryKey, $value) : $this->wherePrimaryKey($value)->loadOne(); } public function loadAll() : EntityCollection { return $this->collectionFromQuery(); } public function loadFromField($field, $value) : EntityCollection { return $this->where($field, $value)->collectionFromQuery(); } public function count() : int { $this->removeQueryFragment($this->queryBuilder->getFragment(Query\Select::class)); if ( $this->queryBuilder->getFragment(Query\GroupBy::class) ) { $this->select( "DISTINCT COUNT(*) OVER ()" ); } else { $this->select(Common\Sql::function("COUNT", "*")); } $this->selectSqlQuery(); $this->finalizeQuery(); return Ulmus::runSelectQuery($this->queryBuilder, $this->adapter)->fetchColumn(0); } protected function deleteOne() { return $this->limit(1)->deleteSqlQuery()->runQuery(); } protected function deleteAll() { return $this->deleteSqlQuery()->runQuery(); } 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)->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 ) { throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); } else { $pkField = key($primaryKeyDefinition); return $this->deleteFromPk($entity->$pkField); } return false; } public function destroyAll(EntityCollection $collection) : void { foreach($collection as $entity) { $this->destroy($entity); } } public function save(object $entity) : bool { 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 ( ! $entity->isLoaded() ) { $statement = $this->insertSqlQuery($dataset)->runQuery(); if ( ( 0 !== $statement->lastInsertId ) && ( null !== $primaryKeyDefinition )) { $pkField = key($primaryKeyDefinition); $dataset[$pkField] = $statement->lastInsertId; } $entity->entityFillFromDataset($dataset, true); } else { if ( $primaryKeyDefinition === null ) { throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); } if ( [] !== $diff = $this->generateDatasetDiff($entity) ) { $pkField = key($primaryKeyDefinition); $pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField; $this->where($pkFieldName, $dataset[$pkFieldName]); $update = $this->updateSqlQuery($diff)->runQuery(); $entity->entityFillFromDataset($dataset); return $update ? (bool) $update->rowCount() : false; } } return false; } public function saveAll(EntityCollection $collection) : void { foreach($collection as $entity) { $this->save($entity); } } public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self { $schema = $schema ?: $this->entityResolver->schemaName(); $this->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $alias ?: $this->alias, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null); $this->finalizeQuery(); $result = Ulmus::runSelectQuery($this->queryBuilder, $this->adapter); return $this; } public function createTable() { return $this->createSqlQuery()->runQuery(); } public function generateDatasetDiff(object $entity) : array { return array_diff_assoc( array_change_key_case($entity->toArray()), array_change_key_case($entity->entityGetDataset(false, true)) ); } public function yield() : \Generator { $class = $this->entityClass; $this->selectSqlQuery(); $this->finalizeQuery(); foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) { yield ( new $class() )->entityFillFromDataset($entityData); } } 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 .= "$"); foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { if (null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore)) { $this->select("$alias.$key as {$prependField}{$field['name']}"); } } 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'], new RelationIgnore)) { $fieldlist[] = $key; $fieldlist[] = $entity::field($field['name'], $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() ); } public function select(/*array|Stringable*/ $fields, bool $distinct = false) : self { $this->queryBuilder->select($fields, $distinct); return $this; } public function distinct(/*array|Stringable*/ $fields) : self { $this->queryBuilder->select($fields); $this->queryBuilder->getFragment(Query\Select::class)->distinct = true; return $this; } public function insert(array $fieldlist, string $table, string $alias, ? string $schema) : self { $this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $alias, $this->escapedDatabase(), $schema); return $this; } public function values(array $dataset) : self { $this->queryBuilder->values($dataset); return $this; } public function update(string $table, string $alias, ? string $schema) : self { $this->queryBuilder->update($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema); return $this; } public function set(array $dataset) : self { $keys = array_keys($dataset); $escapedFields = array_combine($keys, array_map([ $this, 'escapeField' ], $keys)); $this->queryBuilder->set($dataset, $escapedFields); return $this; } public function delete() : self { $this->queryBuilder->delete($this->alias); return $this; } public function from(string $table, ? string $alias, ? string $schema) : self { $this->queryBuilder->from($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null); return $this; } public function join(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self { $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $alias); if ( $callback ) { $callback($join); } return $this; } public function outerJoin(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self { $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $alias); if ( $callback ) { $callback($join); } return $this; } public function match() : self { } public function notMatch() : self { } public function between() : self { } public function notBetween() : self { } public function groupBy($field) : self { $this->queryBuilder->groupBy($field); return $this; } public function groups(array $groups) : self { foreach($groups as $field ) { $this->groupBy($field); } return $this; } public function orderBy($field, ? string $direction = null) : self { $this->queryBuilder->orderBy($field, $direction); return $this; } # @UNTESTED public function randomizeOrder() : self { $this->queryBuilder->orderBy(Common\Sql::function('RAND', Sql::identifier('CURDATE()+0'))); return $this; } public function orders(array $orderList) : self { foreach($orderList as $field => $direction) { $this->orderBy($field, $direction); } return $this; } public function limit(int $value) : self { $this->queryBuilder->limit($value); return $this; } public function offset(int $value) : self { $this->queryBuilder->offset($value); return $this; } /* @TODO */ public function commit() : self { return $this; } /* @TODO */ public function rollback() : self { return $this; } public function wherePrimaryKey($value) : 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($primaryKeyField[$pkField]->name ?? $pkField, $value); } public function withJoin(/*string|array*/ $fields) : self { if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { $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); if (( $annotation instanceof Relation ) && ( $annotation->normalizeType() === 'manytomany' )) { throw new Exception("Many-to-many relation can not be preloaded within joins."); } if ( $annotation ) { $alias = $annotation->alias ?? $item; $entity = $annotation->entity ?? $this->entityResolver->properties[$item]['type']; foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore) ) { $this->select("$alias.$key as {$alias}\${$field['name']}"); } } $this->open(); foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) { if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) { $this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); } } foreach($this->entityResolver->searchFieldAnnotationList($item, new Having() ) as $condition) { $this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); } $this->close(); $key = is_string($annotation->key) ? $this->entityClass::field($annotation->key) : $annotation->key; $foreignKey = is_string($annotation->foreignKey) ? $entity::field($annotation->foreignKey, $alias) : $annotation->foreignKey; $this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias) { foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) { if ( ! is_object($condition->field) ) { $field = $this->entityClass::field($condition->field); } else { $field = clone $condition->field; } if ( $condition->field->entityClass === $entity ) { $field->alias = $alias; $join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->value, $condition->operator); } } }); } else { throw new \Exception("You referenced field `$item` which do not exist or do not contain a valid @Join annotation."); } } return $this; } public function withSubquery(/*string|array*/ $fields) : 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->alias}.*"); } # Apply FILTER annotation to this too ! foreach(array_filter((array) $fields) as $item) { if ( $relation = $this->entityResolver->searchFieldAnnotation($item, new Relation) ) { $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, new Where() ) as $condition) { $repository->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); } foreach($this->entityResolver->searchFieldAnnotationList($item, new Having() ) as $condition) { $repository->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $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 annotation."); } } 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; $entityCollection = $this->instanciateEntityCollection(); $this->selectSqlQuery(); $this->finalizeQuery(); foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) { $entityCollection->append( ( new $class() )->resetVirtualProperties()->entityFillFromDataset($entityData) ); } $this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $entityCollection); return $entityCollection; } public function arrayFromQuery() : array { $this->selectSqlQuery(); $this->finalizeQuery(); return Ulmus::datasetQueryBuilder($this->queryBuilder, $this->adapter); } public function runQuery() : ? \PDOStatement { $this->finalizeQuery(); return Ulmus::runQuery($this->queryBuilder, $this->adapter); } public function resetQuery() : self { $this->queryBuilder->reset(); return $this; } protected function insertSqlQuery(array $dataset) : 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()); } $this->values($dataset); return $this; } protected function updateSqlQuery(array $dataset) : self { if ( null === $this->queryBuilder->getFragment(Query\Update::class) ) { $this->update($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName()); } $this->set($dataset); return $this; } protected function selectSqlQuery() : self { if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { $this->select("{$this->alias}.*"); } if ( null === $this->queryBuilder->getFragment(Query\From::class) ) { $this->from($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName()); } return $this; } protected function deleteSqlQuery() : self { if ( null === $this->queryBuilder->getFragment(Query\Delete::class) ) { $this->delete(); } if ( null === $this->queryBuilder->getFragment(Query\From::class) ) { $this->from($this->entityResolver->tableName(), null, $this->entityResolver->schemaName()); } return $this; } public function createSqlQuery() : self { if ( null === $this->queryBuilder->getFragment(Query\Create::class) ) { $this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList()), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); } if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) { if ( $engine = $this->entityResolver->tableAnnotation()->engine ?? $this->entityResolver->databaseAdapter()->adapter()->defaultEngine() ) { $this->queryBuilder->engine($engine); } } return $this; } protected function fromRow($row) : self { } protected function fromCollection($rows) : self { } public function getSqlQuery(bool $flush = true) : string { $result = $this->queryBuilder->render(); $flush and $this->queryBuilder->reset(); return $result; } public function instanciateEntityCollection(...$arguments) : EntityCollection { return $this->entityClass::entityCollection(...$arguments); } public function instanciateEntity() : object { return new $this->entityClass(); } public function escapeField(string $identifier) : string { return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD); } public function escapeFieldList(array $fieldList) : array { foreach($fieldList as & $list) { $list['name'] = $this->escapeField($list['name']); $fieldTag = array_filter($list['tags'] ?? [], fn($item) => $item['object'] instanceof Field)[0]['object'] ?? null; if ( $fieldTag && isset($fieldTag->name) ) { $fieldTag->name = $this->escapeField($fieldTag->name); } } return $fieldList; } public function escapeTable(string $identifier) : string { return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_TABLE); } public function escapeDatabase(string $identifier) : string { return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_DATABASE); } public function escapedDatabase() : ? string { $name = $this->entityResolver->tableAnnotation()->database ?? $this->adapter->adapter()->database ?? null; return $name ? static::escapeDatabase($name) : null; } public function escapeSchema(string $identifier) : string { return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_SCHEMA); } public function hasFilters() : bool { return isset($this->queryBuilder->where); } protected function matchEntity(object $entity) { return get_class($entity) === $this->entityClass; } protected function finalizeQuery() : void {} }