diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index 320bf6e..9bceb75 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -39,7 +39,7 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { public string $database; public string $username; - + public string $password; public string $traceFile; @@ -51,7 +51,9 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { public function __construct( ?string $server = null, ?string $database = null, + #[\SensitiveParameter] ?string $username = null, + #[\SensitiveParameter] ?string $password = null, ?int $port = null ) { diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index d698c9e..5671c5d 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -38,7 +38,9 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { public function __construct( ?string $hostname = null, ?string $database = null, + #[\SensitiveParameter] ?string $username = null, + #[\SensitiveParameter] ?string $password = null, ?int $port = null, ?string $charset = null diff --git a/src/Adapter/SqlFieldMapper.php b/src/Adapter/SqlFieldMapper.php index 281e267..39d0a3d 100644 --- a/src/Adapter/SqlFieldMapper.php +++ b/src/Adapter/SqlFieldMapper.php @@ -3,6 +3,7 @@ namespace Ulmus\Adapter; use Ulmus\Migration\FieldDefinition; +use Ulmus\Entity; class SqlFieldMapper { @@ -42,7 +43,7 @@ class SqlFieldMapper switch($type) { case "bool": $this->type = "TINYINT"; - $length = 1; + $this->length = 1; break; case "array": @@ -52,6 +53,7 @@ class SqlFieldMapper case "string": if ($length && $length <= 255) { $this->type = "VARCHAR"; + $this->length = $length; break; } elseif (! $length || ( $length <= 65535 ) ) { @@ -74,11 +76,13 @@ class SqlFieldMapper case "float": $this->type = "DOUBLE"; - break; default: - $this->type = strtoupper($type); - break; + if ($length) { + $this->length = $length; + } + + $this->type ??= strtoupper($type); } $this->postProcess(); diff --git a/src/Cache/CacheEventTrait.php b/src/Cache/CacheEventTrait.php new file mode 100644 index 0000000..e9a3367 --- /dev/null +++ b/src/Cache/CacheEventTrait.php @@ -0,0 +1,22 @@ +cache->get($this->cacheKey); + + if ( $keys && is_iterable($keys) ) { + $this->cache->deleteMultiple(array_map(fn($e) => sprintf("%s:%s:", $this->cacheKey, $e), $keys)); + } + } +} \ No newline at end of file diff --git a/src/CacheTrait.php b/src/CacheTrait.php new file mode 100644 index 0000000..e217440 --- /dev/null +++ b/src/CacheTrait.php @@ -0,0 +1,117 @@ +cache = $cache; + + # Reading from cache + $this->eventRegister(new class($cacheKey) implements Event\Repository\CollectionFromQueryDatasetInterface { + + public function __construct( + protected string & $cacheKey + ) {} + + public function execute(RepositoryInterface $repository, array &$data): void + { + $this->cacheKey = $repository->queryBuilder->hashSerializedQuery(); + $data = $repository->getFromCache( $this->cacheKey) ?: []; + } + }); + + # Setting to cache + $this->eventRegister(new class($cacheKey) implements Event\Repository\CollectionFromQueryInterface { + public function __construct( + protected string & $cacheKey + ) {} + + + public function execute(RepositoryInterface $repository, EntityCollection $collection): EntityCollection + { + $repository->setToCache( $this->cacheKey, $collection->map(fn(EntityInterface $e) => $e->entityGetDataset(false, true))); + $this->cacheKey = ""; + + return $collection; + } + }); + + $this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Insert { + use Cache\CacheEventTrait; + + public function execute(RepositoryInterface $repository, object|array $entity, ?array $dataset = null, bool $replace = false): void + { + $this->purgeCache(); + } + }); + + # Cache invalidation + $this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Update { + use Cache\CacheEventTrait; + + public function execute(RepositoryInterface $repository, object|array $entity, ?array $dataset = null, bool $replace = false): void + { + $this->purgeCache(); + } + }); + + $this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Delete { + use Cache\CacheEventTrait; + + public function execute(RepositoryInterface $repository, EntityInterface $entity): void + { + $this->purgeCache(); + } + }); + + $this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Truncate { + use Cache\CacheEventTrait; + + public function execute(RepositoryInterface $repository, ?string $table = null, ?string $alias = null, ?string $schema = null): void + { + $this->purgeCache(); + } + }); + + return $this; + } + + protected function entityCacheKey() : string + { + return sprintf("%s.%s", $this->entityResolver->databaseName(), $this->entityResolver->tableName()); + } + + public function getFromCache(string $key) : mixed + { + $keys = $this->cache->get($this->entityCacheKey(), []); + + if (in_array($key, $keys)) { + return $this->cache->get(sprintf("%s:%s:", $this->entityCacheKey(), $key)); + } + + return null; + } + + public function setToCache(string $key, mixed $value) : void + { + $keys = $this->cache->get($this->entityCacheKey(), []); + + if (! in_array($key, $keys)) { + $keys[] = $key; + + $this->cache->set($this->entityCacheKey(), $keys); + } + + $this->cache->set(sprintf("%s:%s:", $this->entityCacheKey(), $key), $value); + } +} \ No newline at end of file diff --git a/src/Common/PdoObject.php b/src/Common/PdoObject.php index e1d1f59..e1989fb 100644 --- a/src/Common/PdoObject.php +++ b/src/Common/PdoObject.php @@ -49,7 +49,12 @@ class PdoObject extends PDO { if (false !== ( $statement = $this->prepare($sql) )) { return $this->execute($statement, $parameters, true); } - } + } + catch(\PDOException $pdo) { + if ( substr($pdo->getMessage(), 0, 30) !== 'There is no active transaction' ) { + throw $pdo; + } + } catch (\Throwable $e) { throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters)); } diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php index bfd2ad7..055f8dd 100644 --- a/src/ConnectionAdapter.php +++ b/src/ConnectionAdapter.php @@ -2,26 +2,23 @@ namespace Ulmus; +use Psr\SimpleCache\CacheInterface; use Ulmus\Adapter\AdapterInterface; use Ulmus\Common\PdoObject; class ConnectionAdapter { - public string $name; - - public array $configuration; - protected AdapterInterface $adapter; protected PdoObject $pdo; - public function __construct(string $name = "default", array $configuration = [], bool $default = false) - { - $this->name = $name; - - $this->configuration = $configuration; - + public function __construct( + public string $name = "default", + protected array $configuration = [], + public bool $default = false, + public ? CacheInterface $cacheObject = null + ) { Ulmus::registerAdapter($this, $default); } diff --git a/src/EntityTrait.php b/src/EntityTrait.php index d9c5df2..70b6b84 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -235,7 +235,7 @@ trait EntityTrait { #[Ignore] public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS) : EntityField { - $default = ( $alias === false ? '' : Repository::DEFAULT_ALIAS ); # bw compatibility, to be deprecated + $default = ( $alias === false ? '' : static::repository()::DEFAULT_ALIAS ); # bw compatibility, to be deprecated $alias = $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : $default; diff --git a/src/Event/Query/Alter.php b/src/Event/Query/Alter.php new file mode 100644 index 0000000..765d4c8 --- /dev/null +++ b/src/Event/Query/Alter.php @@ -0,0 +1,9 @@ +value(); + $operator = $this->operator(); + return $this->content ?: $this->content = implode(" ", array_filter([ $this->condition, $this->not ? Where::CONDITION_NOT : "", $this->field, - $this->operator(), + $operator, $value, ])); } @@ -112,7 +115,11 @@ class Where extends Fragment { protected function operator() : string { if ( is_array($this->value) ) { - return Where::COMPARISON_IN; + if (true === $not = $this->not) { + $this->not = false; + } + + return $not ? Where::COMPARISON_NOT_IN : Where::COMPARISON_IN; } # whitelisting operators diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index a20af9e..53a2b73 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -4,5 +4,5 @@ namespace Ulmus; class QueryBuilder extends QueryBuilder\Sql\MysqlQueryBuilder { - # Backward compatibility defaulting on MySQL/MariaDB query builder + } \ No newline at end of file diff --git a/src/QueryBuilder/QueryBuilderInterface.php b/src/QueryBuilder/QueryBuilderInterface.php index cbaa69f..dbbe895 100644 --- a/src/QueryBuilder/QueryBuilderInterface.php +++ b/src/QueryBuilder/QueryBuilderInterface.php @@ -12,4 +12,5 @@ interface QueryBuilderInterface public function reset() : void; public function getFragment(string $class, int $index = 0) : ? QueryFragmentInterface; public function removeFragment(QueryFragmentInterface|array|\Stringable|string $fragment) : void; + public function hashSerializedQuery() : string; } \ No newline at end of file diff --git a/src/QueryBuilder/Sql/MysqlQueryBuilder.php b/src/QueryBuilder/Sql/MysqlQueryBuilder.php index afa8f0e..3b5d2c8 100644 --- a/src/QueryBuilder/Sql/MysqlQueryBuilder.php +++ b/src/QueryBuilder/Sql/MysqlQueryBuilder.php @@ -432,8 +432,12 @@ class MysqlQueryBuilder extends SqlQueryBuilder return array_shift($this->queryStack); } - public function render(bool $skipToken = false) /* : mixed */ + public function render(bool $skipToken = false) : mixed { + if (isset($this->rendered)) { + return $this->rendered; + } + $sql = []; usort($this->queryStack, function($q1, $q2) { @@ -444,7 +448,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder $sql[] = $fragment->render($skipToken); } - return implode(" ", $sql); + return $this->rendered = implode(" ", $sql); } public function reset() : void @@ -453,8 +457,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder $this->whereConditionOperator = Query\Where::CONDITION_AND; $this->havingConditionOperator = Query\Where::CONDITION_AND; $this->parameterIndex = 0; - - unset($this->where, $this->having); + unset($this->where, $this->having, $this->rendered, $this->hash); } public function getFragment(string $class, int $index = 0) : ? QueryFragmentInterface diff --git a/src/QueryBuilder/SqlQueryBuilder.php b/src/QueryBuilder/SqlQueryBuilder.php index 13254bf..4c28eb4 100644 --- a/src/QueryBuilder/SqlQueryBuilder.php +++ b/src/QueryBuilder/SqlQueryBuilder.php @@ -7,6 +7,9 @@ use Ulmus\Query\QueryFragmentInterface; # TODO -> Extract from MysqlQueryBuilder to build an ISO/IEC 9075:2023 compatible layer for a basic SQL QueryBuilder class SqlQueryBuilder implements QueryBuilderInterface { + protected string $rendered; + + protected string $hash; public function push(QueryFragmentInterface $queryFragment): QueryBuilderInterface { @@ -18,7 +21,7 @@ class SqlQueryBuilder implements QueryBuilderInterface // TODO: Implement pull() method. } - public function render(bool $skipToken = false) + public function render(bool $skipToken = false) : mixed { // TODO: Implement render() method. } @@ -37,4 +40,9 @@ class SqlQueryBuilder implements QueryBuilderInterface { // TODO: Implement removeFragment() method. } + + public function hashSerializedQuery(): string + { + return $this->hash ??= md5(sprintf("%s:%s", $this->render(), serialize($this->parameters))); + } } \ No newline at end of file diff --git a/src/Repository.php b/src/Repository.php index aa0bb9f..05fc893 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -5,25 +5,27 @@ namespace Ulmus; use Ulmus\Attribute\Property\{ Field, OrderBy, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin }; +use Psr\SimpleCache\CacheInterface; use Ulmus\Common\EntityResolver; +use Ulmus\Entity\EntityInterface; use Ulmus\Repository\RepositoryInterface; use Ulmus\Repository\WithOptionEnum; class Repository implements RepositoryInterface { - use EventTrait, Repository\ConditionTrait, Repository\EscapeTrait; + use EventTrait, CacheTrait, Repository\ConditionTrait, Repository\EscapeTrait, Repository\QueryBuildingTrait; const DEFAULT_ALIAS = "this"; - public ? ConnectionAdapter $adapter; - public string $alias; public string $entityClass; public array $events = []; - protected QueryBuilder\QueryBuilderInterface $queryBuilder; + public ? ConnectionAdapter $adapters; + + public readonly QueryBuilder\QueryBuilderInterface $queryBuilder; protected EntityResolver $entityResolver; @@ -33,10 +35,21 @@ class Repository implements RepositoryInterface $this->entityClass = $entity; $this->alias = $alias; $this->entityResolver = Ulmus::resolveEntity($entity); - $this->adapter = $adapter ?? $this->entityResolver->databaseAdapter(); - $queryBuilder = $this->adapter->adapter()->queryBuilderClass(); - $this->queryBuilder = new $queryBuilder(); + 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() @@ -113,6 +126,8 @@ class Repository implements RepositoryInterface public function deleteAll() { + $this->eventExecute(Event\Query\Delete::class, $this); + return $this->deleteSqlQuery()->runDeleteQuery(); } @@ -139,6 +154,8 @@ class Repository implements RepositoryInterface else { $pkField = key($primaryKeyDefinition); + $this->eventExecute(Event\Query\Delete::class, $this, $entity); + return $this->deleteFromPk($entity->$pkField); } } @@ -193,6 +210,8 @@ class Repository implements RepositoryInterface $entity->entityFillFromDataset($dataset, true); + $this->eventExecute(Event\Query\Insert::class, $this, $entity, $dataset, $replace); + return (bool) ( $pkValue ?? $pdoObject->lastInsertId ); } else { @@ -211,6 +230,9 @@ class Repository implements RepositoryInterface $entity->entityFillFromDataset($dataset, true); + # $fieldsAndValue ??= &$dataset; + $this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $replace); + return $update ? (bool) $update->rowCount : false; } } @@ -229,64 +251,6 @@ class Repository implements RepositoryInterface return $changed; } - public function insertAll(EntityCollection|array $collection, int $size = 1000) : int - { - if ( empty($collection) ) { - return 0; - } - elseif ( is_array($collection) ) { - $collection = $this->entityClass::entityCollection($collection); - } - - foreach($collection as $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 ( ! $entity->isLoaded() ) { - # $dataset = array_filter($dataset, fn($item, $field) => ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH); - - $statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery(); - - if ( ( 0 !== $statement->lastInsertId ) && - ( null !== $primaryKeyDefinition )) { - - $pkField = key($primaryKeyDefinition); - $dataset[$pkField] = $statement->lastInsertId; - } - - $entity->entityFillFromDataset($dataset, true); - - return (bool) $statement->lastInsertId; - } - else { - if ( $primaryKeyDefinition === null ) { - throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); - } - - $diff = $fieldsAndValue ?? $this->generateWritableDataset($entity); - - if ( [] !== $diff ) { - $pkField = key($primaryKeyDefinition); - $pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField; - $this->where($pkFieldName, $dataset[$pkFieldName]); - - $update = $this->updateSqlQuery($diff)->runUpdateQuery(); - - $entity->entityFillFromDataset($dataset, true); - - return $update ? (bool) $update->rowCount : false; - } - } - - return 0; - } - public function replace(object|array $entity, ? array $fieldsAndValue = null) : bool { return $this->save($entity, $fieldsAndValue, true); @@ -303,35 +267,34 @@ class Repository implements RepositoryInterface return $changed; } - public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self + + public function createTable() : mixed { - $schema = $schema ?: $this->entityResolver->schemaName(); + $this->eventExecute(Event\Query\Create::class, $this); - $this->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $this->escapeIdentifier($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 alterTable(array $fields) + public function alterTable(array $fields) : mixed { + $this->eventExecute(Event\Query\Alter::class, $this, $fields); + return $this->alterSqlQuery($fields)->runQuery(); } - public function listTables(? string $database = null) + 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) + public function listColumns(? string $table = null) : EntityCollection { $table ??= $this->entityResolver->tableName(); @@ -358,11 +321,8 @@ class Repository implements RepositoryInterface return (string) $e1 !== (string) $e2; }); - - # return array_diff_assoc($oldValues ? $dataset : $array , $oldValues ? $array : $dataset ); } - public function generateWritableDataset(object $entity, bool $oldValues = false) : array { $intersect = []; @@ -370,7 +330,7 @@ class Repository implements RepositoryInterface $dataset = $this->generateDatasetDiff($entity, $oldValues); foreach($dataset as $field => $value) { - if ( false === ( $this->entityResolver->searchFieldAnnotation($field, [ Field::class, Field::class ], false)->readonly ?? false ) ) { + if ( false === ( $this->entityResolver->searchFieldAnnotation($field, Field::class, false)->readonly ?? false ) ) { $intersect[$field] = $field; } } @@ -389,15 +349,6 @@ class Repository implements RepositoryInterface } } - public function removeQueryFragment(null|Query\QueryFragmentInterface|string|array $fragment) : self - { - foreach((array) $fragment as $item) { - $this->queryBuilder->removeFragment($item); - } - - return $this; - } - public function selectEntity(string $entity, string $alias, string $prependField = "") : self { $prependField and ($prependField .= "$"); @@ -431,167 +382,6 @@ class Repository implements RepositoryInterface ); } - public function select(array|string|\Stringable $fields, bool $distinct = false) : self - { - $this->queryBuilder->select($fields, $distinct); - - return $this; - } - - public function distinct(array|string|\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, bool $replace = false) : self - { - $this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $this->escapeIdentifier($alias), $this->escapedDatabase(), $schema, $replace); - - 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->escapeIdentifier($alias) : null, $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(...$args) : self - { - $this->queryBuilder->delete(); - - return $this; - } - - public function from(string $table, ? string $alias, ? string $schema) : self - { - $this->queryBuilder->from($this->escapeTable($table), $alias ? $this->escapeIdentifier($alias) : null, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null); - - return $this; - } - - public function join(string $type, $table, string|\Stringable $field, mixed $value, ? string $alias = null, ? callable $callback = null) : self - { - $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $alias ? $this->escapeIdentifier($alias) : null); - - if ( $callback ) { - $callback($join); - } - - return $this; - } - - public function outerJoin(string $type, $table, string|\Stringable $field, mixed $value, ? string $alias = null, ? callable $callback = null) : self - { - $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $alias ? $this->escapeIdentifier($alias) : null); - - 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(string|\Stringable $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(string|object $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) { - if (is_numeric($field)) { - $this->orderBy($direction); - } - else { - # Associative array with 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 { @@ -873,17 +663,21 @@ class Repository implements RepositoryInterface $entityCollection = $entityClass::entityCollection(); $this->finalizeQuery(); - - foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) { + + $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->resetVirtualProperties()->entityFillFromDataset($entityData) ); + $entityCollection->append( $entity->entityFillFromDataset($entityData) ); } - $this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $entityCollection); + $this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $this, $entityCollection); return $entityCollection; } @@ -925,110 +719,6 @@ class Repository implements RepositoryInterface return Ulmus::runDeleteQuery($this->queryBuilder, $this->adapter); } - public function resetQuery() : self - { - $this->queryBuilder->reset(); - - return $this; - } - - protected function insertSqlQuery(array $dataset, bool $replace = false) : self - { - 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); - - 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) ) { - $fields = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); - $this->select($this->entityClass::fields(array_map(fn($f) => $f->object->name ?? $f->name, $fields))); - } - - 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->adapter->adapter(), $this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); - } - - return $this; - } - - public function alterSqlQuery(array $fields) : self - { - if ( null === $this->queryBuilder->getFragment(Query\Alter::class) ) { - $this->queryBuilder->alter($this->adapter->adapter(), $fields, $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); - } - - return $this; - } - - public function showDatabasesSqlQuery() : self - { - if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { - $this->queryBuilder->showDatabases(); - } - - return $this; - } - - public function showTablesSqlQuery() : self - { - if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { - $this->queryBuilder->showTables(); - } - - return $this; - } - - public function showColumnsSqlQuery(string $table) : self - { - if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { - $this->queryBuilder->showColumns($table); - } - - return $this; - } - protected function fromRow($row) : self { @@ -1039,15 +729,6 @@ class Repository implements RepositoryInterface } - 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); diff --git a/src/Repository/ConditionTrait.php b/src/Repository/ConditionTrait.php index a364c52..32a8ea4 100644 --- a/src/Repository/ConditionTrait.php +++ b/src/Repository/ConditionTrait.php @@ -151,7 +151,7 @@ trait ConditionTrait return $this; } - public function removeQueryFragment(Query\Fragment|\Stringable|string|array $fragment) : self + public function removeQueryFragment(null|Query\QueryFragmentInterface|string|\Stringable|array $fragment) : self { foreach((array) $fragment as $item) { $this->queryBuilder->removeFragment($item); diff --git a/src/Repository/QueryBuildingTrait.php b/src/Repository/QueryBuildingTrait.php new file mode 100644 index 0000000..543b266 --- /dev/null +++ b/src/Repository/QueryBuildingTrait.php @@ -0,0 +1,292 @@ +entityResolver->schemaName(); + + $this->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $this->escapeIdentifier($alias ?: $this->alias), $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null); + + return $this; + } + + public function select(array|string|\Stringable $fields, bool $distinct = false) : self + { + $this->queryBuilder->select($fields, $distinct); + + return $this; + } + + public function distinct(array|string|\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, bool $replace = false) : self + { + $this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $this->escapeIdentifier($alias), $this->escapedDatabase(), $schema, $replace); + + 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->escapeIdentifier($alias) : null, $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(...$args) : self + { + $this->queryBuilder->delete(); + + return $this; + } + + public function from(string $table, ? string $alias, ? string $schema) : self + { + $this->queryBuilder->from($this->escapeTable($table), $alias ? $this->escapeIdentifier($alias) : null, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null); + + return $this; + } + + public function join(string $type, $table, string|\Stringable $field, mixed $value, ? string $alias = null, ? callable $callback = null) : self + { + $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $alias ? $this->escapeIdentifier($alias) : null); + + if ( $callback ) { + $callback($join); + } + + return $this; + } + + public function outerJoin(string $type, $table, string|\Stringable $field, mixed $value, ? string $alias = null, ? callable $callback = null) : self + { + $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $alias ? $this->escapeIdentifier($alias) : null); + + 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(string|\Stringable $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(string|object $field, ? string $direction = null) : self + { + $this->queryBuilder->orderBy($field, $direction); + + return $this; + } + + # @UNTESTED + public function randomizeOrder() : self + { + $this->queryBuilder->orderBy(Common\Sql::function('RAND', Common\Sql::identifier('CURDATE()+0'))); + + return $this; + } + + public function orders(array $orderList) : self + { + foreach($orderList as $field => $direction) { + if (is_numeric($field)) { + $this->orderBy($direction); + } + else { + # Associative array with 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; + } + + public function getSqlQuery(bool $flush = true) : string + { + $result = $this->queryBuilder->render(); + + $flush and $this->queryBuilder->reset(); + + return $result; + } + + + public function resetQuery() : self + { + $this->queryBuilder->reset(); + + return $this; + } + + protected function insertSqlQuery(array $dataset, bool $replace = false) : self + { + 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); + + 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) ) { + $fields = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); + $this->select($this->entityClass::fields(array_map(fn($f) => $f->object->name ?? $f->name, $fields))); + } + + 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->adapter->adapter(), $this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); + } + + return $this; + } + + public function alterSqlQuery(array $fields) : self + { + if ( null === $this->queryBuilder->getFragment(Query\Alter::class) ) { + $this->queryBuilder->alter($this->adapter->adapter(), $fields, $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName()); + } + + return $this; + } + + public function showDatabasesSqlQuery() : self + { + if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { + $this->queryBuilder->showDatabases(); + } + + return $this; + } + + public function showTablesSqlQuery() : self + { + if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { + $this->queryBuilder->showTables(); + } + + return $this; + } + + public function showColumnsSqlQuery(string $table) : self + { + if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { + $this->queryBuilder->showColumns($table); + } + + return $this; + } +} \ No newline at end of file diff --git a/src/Ulmus.php b/src/Ulmus.php index 2722e2b..675c101 100644 --- a/src/Ulmus.php +++ b/src/Ulmus.php @@ -64,7 +64,7 @@ abstract class Ulmus } public static function runSelectQuery(QueryBuilder\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) - { + { $dataset = static::connector($adapter)->select($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? [])); $queryBuilder->reset(); @@ -74,7 +74,6 @@ abstract class Ulmus public static function runQuery(QueryBuilder\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) { $return = static::connector($adapter)->runQuery($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? [])); - $queryBuilder->reset(); return $return;