ulmus/src/Repository.php

681 lines
22 KiB
PHP
Raw Normal View History

2019-08-21 20:13:00 +00:00
<?php
namespace Ulmus;
use Ulmus\Annotation\Property\{ Where, Having, Relation, Join };
2019-08-21 20:13:00 +00:00
use Ulmus\Common\EntityResolver;
class Repository
{
use EventTrait, Repository\ConditionTrait;
2019-08-21 20:13:00 +00:00
const DEFAULT_ALIAS = "this";
public ? ConnectionAdapter $adapter;
2019-08-21 20:13:00 +00:00
protected QueryBuilder $queryBuilder;
protected EntityResolver $entityResolver;
public string $alias;
public string $entityClass;
public array $events = [];
2019-08-21 20:13:00 +00:00
public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) {
$this->entityClass = $entity;
$this->alias = $alias;
$this->entityResolver = Ulmus::resolveEntity($entity);
2020-10-16 15:27:54 +00:00
$this->adapter = $adapter ?? $this->entityResolver->databaseAdapter();
$this->queryBuilder = new QueryBuilder();
2019-08-21 20:13:00 +00:00
}
public function __clone()
{
#$this->queryBuilder = clone $this->queryBuilder;
}
2019-08-21 20:13:00 +00:00
2020-01-31 21:38:48 +00:00
public function loadOne() : ? object
2019-08-21 20:13:00 +00:00
{
2020-01-31 21:38:48 +00:00
return $this->limit(1)->collectionFromQuery()[0] ?? null;
2019-08-21 20:13:00 +00:00
}
public function loadOneFromField($field, $value) : ? object
{
return $this->where($field, $value)->loadOne();
}
public function loadFromPk($value, /* ? stringable */ $primaryKey = null) : ? object
2019-08-21 20:13:00 +00:00
{
return $primaryKey ? $this->loadOneFromField($primaryKey, $value) : $this->wherePrimaryKey($value)->loadOne();
2019-08-21 20:13:00 +00:00
}
2020-01-31 21:38:48 +00:00
public function loadAll() : EntityCollection
2019-08-21 20:13:00 +00:00
{
2020-01-31 21:38:48 +00:00
return $this->collectionFromQuery();
2019-08-21 20:13:00 +00:00
}
public function loadFromField($field, $value) : EntityCollection
{
return $this->where($field, $value)->collectionFromQuery();
}
public function count() : int
{
if ( null !== $select = $this->queryBuilder->getFragment(Query\Select::class) ) {
$this->queryBuilder->removeFragment($select);
}
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);
}
2019-08-21 20:13:00 +00:00
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);
}
}
2020-01-31 21:38:48 +00:00
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}`");
}
2020-02-05 21:19:57 +00:00
$dataset = $entity->toArray();
$primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
2020-02-05 21:19:57 +00:00
if ( ! $entity->isLoaded() ) {
$statement = $this->insertSqlQuery($dataset)->runQuery();
if ( ( 0 !== $statement->lastInsertId ) &&
( null !== $primaryKeyDefinition )) {
2020-10-16 15:27:54 +00:00
$pkField = key($primaryKeyDefinition);
$dataset[$pkField] = $statement->lastInsertId;
}
$entity->entityFillFromDataset($dataset, true);
2020-02-05 21:19:57 +00:00
}
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;
}
2020-02-05 21:19:57 +00:00
}
2020-02-05 21:19:57 +00:00
return false;
2020-01-31 21:38:48 +00:00
}
public function saveAll(EntityCollection $collection) : void
2020-01-31 21:38:48 +00:00
{
2020-02-05 21:19:57 +00:00
foreach($collection as $entity) {
$this->save($entity);
}
2020-01-31 21:38:48 +00:00
}
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();
2020-10-16 15:27:54 +00:00
$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;
}
2020-10-16 15:27:54 +00:00
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
2019-08-21 20:13:00 +00:00
{
$class = $this->entityClass;
$this->selectSqlQuery();
$this->finalizeQuery();
2019-08-21 20:13:00 +00:00
foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) {
yield ( new $class() )->entityFillFromDataset($entityData);
}
2019-08-21 20:13:00 +00:00
}
public function select(/*array|Stringable*/ $fields) : self
2019-08-21 20:13:00 +00:00
{
$this->queryBuilder->select($fields);
2019-08-21 20:13:00 +00:00
return $this;
}
2020-02-05 21:19:57 +00:00
public function insert(array $fieldlist, string $table, string $alias, ? string $schema) : self
{
2020-10-16 15:27:54 +00:00
$this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $alias, $this->escapedDatabase(), $schema);
2020-02-05 21:19:57 +00:00
return $this;
}
public function values(array $dataset) : self
{
$this->queryBuilder->values($dataset);
2020-02-05 21:19:57 +00:00
return $this;
}
public function update(string $table, string $alias, ? string $schema) : self
{
2020-10-16 15:27:54 +00:00
$this->queryBuilder->update($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema);
return $this;
}
public function set(array $dataset) : self
{
$this->queryBuilder->set($dataset);
return $this;
}
public function delete() : self
{
$this->queryBuilder->delete($this->alias);
return $this;
}
public function from(string $table, ? string $alias, ? string $schema) : self
2019-08-21 20:13:00 +00:00
{
2020-10-16 15:27:54 +00:00
$this->queryBuilder->from($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null);
2019-08-21 20:13:00 +00:00
return $this;
}
public function join(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self
2019-08-21 20:13:00 +00:00
{
$join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $alias);
if ( $callback ) {
$callback($join);
}
return $this;
}
2019-08-21 20:13:00 +00:00
public function outerJoin(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self
2019-08-21 20:13:00 +00:00
{
$join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $alias);
if ( $callback ) {
$callback($join);
}
return $this;
}
2019-08-21 20:13:00 +00:00
public function match() : self
{
}
public function notMatch() : self
{
}
public function between() : self
{
}
public function notBetween() : self
{
}
public function groupBy($field) : self
2019-08-21 20:13:00 +00:00
{
$this->queryBuilder->groupBy($field);
2019-08-21 20:13:00 +00:00
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
2019-08-21 20:13:00 +00:00
{
$this->queryBuilder->orderBy($field, $direction);
2019-08-21 20:13:00 +00:00
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;
}
2019-08-21 20:13:00 +00:00
public function limit(int $value) : self
{
$this->queryBuilder->limit($value);
2019-08-21 20:13:00 +00:00
return $this;
}
public function offset(int $value) : self
{
$this->queryBuilder->offset($value);
2019-08-21 20:13:00 +00:00
return $this;
}
/* @TODO */
2019-08-21 20:13:00 +00:00
public function commit() : self
{
return $this;
}
/* @TODO */
2019-08-21 20:13:00 +00:00
public function rollback() : self
{
return $this;
}
2020-02-05 21:19:57 +00:00
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);
2020-02-05 21:19:57 +00:00
}
public function withJoin(/*string|array*/ $fields) : self
{
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
$this->select("{$this->alias}.*");
}
# Apply FILTER annotation to this too !
foreach((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) {
$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) {
$field = clone $condition->field;
if ( is_object($condition->field) && ( $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 filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self
2019-08-21 20:13:00 +00:00
{
$searchRequest->count = $searchRequest->filter( clone $this )
->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());
}
public function collectionFromQuery(? string $entityClass = null) : EntityCollection
2019-08-21 20:13:00 +00:00
{
$class = $entityClass ?: $this->entityClass;
$entityCollection = $this->instanciateEntityCollection();
$this->selectSqlQuery();
$this->finalizeQuery();
2019-08-21 20:13:00 +00:00
foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) {
$entityCollection->append( ( new $class() )->resetVirtualProperties()->entityFillFromDataset($entityData) );
2019-08-21 20:13:00 +00:00
}
$this->eventExecute(Event\RepositoryCollectionFromQueryInterface::class, $entityCollection);
2019-08-21 20:13:00 +00:00
return $entityCollection;
}
public function arrayFromQuery() : array
{
$this->selectSqlQuery();
$this->finalizeQuery();
return Ulmus::datasetQueryBuilder($this->queryBuilder, $this->adapter);
}
2019-08-21 20:13:00 +00:00
public function runQuery() : ? \PDOStatement
{
$this->finalizeQuery();
return Ulmus::runQuery($this->queryBuilder, $this->adapter);
}
public function resetQuery() : self
{
$this->queryBuilder->reset();
return $this;
}
2020-02-05 21:19:57 +00:00
protected function insertSqlQuery(array $dataset) : self
{
if ( null === $this->queryBuilder->getFragment(Query\Insert::class) ) {
$this->insert(array_keys($dataset), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
2020-02-05 21:19:57 +00:00
}
$this->values($dataset);
2020-02-05 21:19:57 +00:00
return $this;
}
2020-02-05 21:19:57 +00:00
protected function updateSqlQuery(array $dataset) : self
{
if ( null === $this->queryBuilder->getFragment(Query\Update::class) ) {
$this->update($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
2020-02-05 21:19:57 +00:00
}
$this->set($dataset);
2020-02-05 21:19:57 +00:00
return $this;
}
2019-08-21 20:13:00 +00:00
protected function selectSqlQuery() : self
{
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
2019-08-21 20:13:00 +00:00
$this->select("{$this->alias}.*");
}
if ( null === $this->queryBuilder->getFragment(Query\From::class) ) {
$this->from($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
2019-08-21 20:13:00 +00:00
}
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;
}
2020-10-16 15:27:54 +00:00
public function createSqlQuery() : self
{
if ( null === $this->queryBuilder->getFragment(Query\Create::class) ) {
$this->queryBuilder->create($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;
}
2019-08-21 20:13:00 +00:00
protected function fromRow($row) : self
{
}
protected function fromCollection($rows) : self
{
}
2020-10-16 15:27:54 +00:00
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 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);
}
2020-10-16 15:27:54 +00:00
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);
}
protected function matchEntity(object $entity) {
return get_class($entity) === $this->entityClass;
}
protected function finalizeQuery() : void {}
}