diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index 5a065f8..fa4df77 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -92,11 +92,11 @@ class MsSQL implements AdapterInterface { $parts[] = "Server={$this->server}" . ( isset($this->port) ? ",{$this->port}" : "" ); $parts[] = "Database={$this->database}"; $parts[] = "ConnectionPooling={$this->connectionPooling}"; - + if ( $this->app ?? false ) { $parts[] = "APP={$this->app}"; } - + if ( $this->encrypt ?? false ) { $parts[] = "Encrypt=1"; } @@ -104,7 +104,7 @@ class MsSQL implements AdapterInterface { $parts[] = "Encrypt=0"; } - if ( $this->failoverPartner ?? false ) { + if ( $this->failoverPartner ?? false ) { $parts[] = "Failover_Partner={$this->failoverPartner}"; } @@ -133,7 +133,7 @@ class MsSQL implements AdapterInterface { } if ( $this->trustServerCertificate ?? false ) { - $parts[] = "TrustServerCertificate=1"; + $parts[] = "TrustServerCertificate=yes"; } if ( $this->WSID ?? false ) { @@ -238,4 +238,8 @@ class MsSQL implements AdapterInterface { return Repository\MssqlRepository::class; } + public function queryBuilderClass() : string + { + return QueryBuilder\MssqlQueryBuilder::class; + } } diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php index dd89b0d..7026e14 100644 --- a/src/Adapter/SQLite.php +++ b/src/Adapter/SQLite.php @@ -137,4 +137,14 @@ class SQLite implements AdapterInterface { 'unsigned' => "", ]; } + + public function repositoryClass() : string + { + return Repository\SqliteRepository::class; + } + + public function queryBuilderClass() : string + { + return QueryBuilder\SqliteQueryBuilder::class; + } } diff --git a/src/Annotation/Property/Virtual.php b/src/Annotation/Property/Virtual.php index 295c938..cf947f2 100644 --- a/src/Annotation/Property/Virtual.php +++ b/src/Annotation/Property/Virtual.php @@ -3,7 +3,17 @@ namespace Ulmus\Annotation\Property; class Virtual extends Field { - public Closure $closure; public bool $readonly = true; + + public \Closure $closure; + + public string $method; + + public function __construct(? \Closure $closure = null) + { + if ( $closure !== null ) { + $this->closure = $closure; + } + } } diff --git a/src/EntityCollection.php b/src/EntityCollection.php index 75b9e3f..44fc72d 100644 --- a/src/EntityCollection.php +++ b/src/EntityCollection.php @@ -44,6 +44,11 @@ class EntityCollection extends \ArrayObject { } } + public function filtersArray(Callable $callback) : array + { + return $this->filtersCollection($callback, true, false)->toArray(); + } + public function filtersOne(Callable $callback) : ? object { foreach($this->filters($callback, true) as $item) { diff --git a/src/EntityTrait.php b/src/EntityTrait.php index f5640a0..7e3edb8 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -9,7 +9,7 @@ use Ulmus\Repository, use Ulmus\Annotation\Classes\{ Method, Table, Collation, }; use Ulmus\Annotation\Property\{ Field, Filter, FilterJoin, Relation, OrderBy, Where, OrWhere, Join, Virtual, On, WithJoin, }; -use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, Bigint, Tinyint, Blob, Text, Mediumtext, Longtext, Tinyblob, Mediumblob, Longblob }; +use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, Bigint, Tinyint, Text, Mediumtext, Longtext, }; use Ulmus\Annotation\Property\Relation\{ Ignore as RelationIgnore }; trait EntityTrait { @@ -30,6 +30,10 @@ trait EntityTrait { */ public array $entityLoadedDataset = []; + public function __construct() { + $this->resetVirtualProperties(); + } + /**entityLoadedDataset * @Ignore */ @@ -108,7 +112,7 @@ trait EntityTrait { foreach($this->resolveEntity()->properties as $prop => $property) { if ( ! $property['builtin'] ) { foreach($property['tags'] as $tag) { - if ( in_array(strtolower($tag['tag']), [ 'relation', 'join' ] ) ) { + if ( in_array(strtolower($tag['tag']), [ 'relation', 'join', 'virtual' ] ) ) { unset($this->$prop); } } @@ -200,6 +204,10 @@ trait EntityTrait { */ public function isLoaded() : bool { + if (empty($this->entityLoadedDataset)) { + return false; + } + if ( null === $pkField = $this->resolveEntity()->getPrimaryKeyField($this) ) { throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class)); } @@ -311,7 +319,7 @@ trait EntityTrait { */ public static function field($name, ? string $alias = null) : EntityField { - return new EntityField(static::class, $name, $alias ?: Repository::DEFAULT_ALIAS, Ulmus::resolveEntity(static::class)); + return new EntityField(static::class, $name, $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : Repository::DEFAULT_ALIAS, Ulmus::resolveEntity(static::class)); } /** diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 10263db..0ec401e 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -247,6 +247,10 @@ class QueryBuilder implements Query\QueryBuilderInterface $limit->set($value); + if ($value === 0) { + $this->removeFragment(Query\Limit::class); + } + return $this; } diff --git a/src/QueryBuilder/MssqlQueryBuilder.php b/src/QueryBuilder/MssqlQueryBuilder.php index e139517..bd8e4f7 100644 --- a/src/QueryBuilder/MssqlQueryBuilder.php +++ b/src/QueryBuilder/MssqlQueryBuilder.php @@ -2,11 +2,9 @@ namespace Ulmus\QueryBuilder; -use Ulmus\QueryBuilder; +use Ulmus\{Query, QueryBuilder }; -use Ulmus\Query; - -class MssqlQueryBuilder extends QueryBuilder implements Ulmus\Query\QueryBuilderInterface +class MssqlQueryBuilder extends QueryBuilder implements Query\QueryBuilderInterface { public function limit(int $value) : self { @@ -17,6 +15,10 @@ class MssqlQueryBuilder extends QueryBuilder implements Ulmus\Query\QueryBuilder $offset->limit = $value; + if ($value === 0) { + $this->removeFragment(Query\MsSQL\Offset::class); + } + return $this; } diff --git a/src/Repository.php b/src/Repository.php index bd73acb..11433fa 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -68,7 +68,7 @@ class Repository public function count() : int { - $this->removeQueryFragment(Query\Select::class); + $this->removeQueryFragment([ Query\Select::class, Query\OrderBy::class, ]); if ( $this->queryBuilder->getFragment(Query\GroupBy::class) ) { $this->select( "DISTINCT COUNT(*) OVER ()" ); @@ -149,16 +149,20 @@ class Repository $statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery(); - if ( ( 0 !== $statement->lastInsertId ) && - ( null !== $primaryKeyDefinition )) { - + if ( null !== $primaryKeyDefinition ) { $pkField = key($primaryKeyDefinition); - $dataset[$pkField] = $statement->lastInsertId; + + if ($statement->lastInsertId ) { + $dataset[$pkField] = $statement->lastInsertId; + } + elseif ($replace) { + $pkValue = $dataset[$pkField]; + } } $entity->entityFillFromDataset($dataset, true); - return (bool) $statement->lastInsertId; + return (bool) ($pkValue ?? $statement->lastInsertId); } else { if ( $primaryKeyDefinition === null ) { @@ -270,7 +274,7 @@ class Repository { $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->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $this->escapeIdentifier($alias ?: $this->alias), $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null); $this->finalizeQuery(); @@ -337,7 +341,7 @@ class Repository 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']}"); + $this->select(sprintf("%s.$key as {$prependField}{$field['name']}", $this->escapeIdentifier($alias))); } } @@ -351,7 +355,7 @@ class Repository 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); + $fieldlist[] = $entity::field($field['name'], $this->escapeIdentifier($alias)); } } @@ -381,7 +385,7 @@ class Repository public function insert(array $fieldlist, string $table, string $alias, ? string $schema, bool $replace = false) : self { - $this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $alias, $this->escapedDatabase(), $schema, $replace); + $this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $this->escapeIdentifier($alias), $this->escapedDatabase(), $schema, $replace); return $this; } @@ -395,7 +399,7 @@ class Repository public function update(string $table, string $alias, ? string $schema) : self { - $this->queryBuilder->update($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema); + $this->queryBuilder->update($this->escapeTable($table), $this->escapeIdentifier($alias), $this->escapedDatabase(), $schema); return $this; } @@ -419,13 +423,13 @@ class Repository public function from(string $table, ? string $alias, ? string $schema) : self { - $this->queryBuilder->from($this->escapeTable($table), $alias, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null); + $this->queryBuilder->from($this->escapeTable($table), $this->escapeIdentifier($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); + $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $this->escapeIdentifier($alias)); if ( $callback ) { $callback($join); @@ -436,7 +440,7 @@ class Repository 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); + $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $this->escapeIdentifier($alias)); if ( $callback ) { $callback($join); @@ -564,7 +568,9 @@ class Repository 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']}"); + $escAlias = $this->escapeIdentifier($alias); + + $this->select("$escAlias.$key as $alias\${$field['name']}"); } } @@ -627,7 +633,7 @@ class Repository } if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { - $this->select("{$this->alias}.*"); + $this->select($this->escapeIdentifier($this->alias) . ".*"); } # Apply FILTER annotation to this too ! diff --git a/src/Repository/EscapeTrait.php b/src/Repository/EscapeTrait.php index eb69ab7..6269f89 100644 --- a/src/Repository/EscapeTrait.php +++ b/src/Repository/EscapeTrait.php @@ -7,6 +7,11 @@ use Ulmus\Annotation\Property\Field; trait EscapeTrait { + public function escapeIdentifier(string $identifier) : string + { + return $this->escapeField($identifier); + } + public function escapeField(string $identifier) : string { return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD); diff --git a/src/Repository/MssqlRepository.php b/src/Repository/MssqlRepository.php index ed388e7..629b46e 100644 --- a/src/Repository/MssqlRepository.php +++ b/src/Repository/MssqlRepository.php @@ -5,66 +5,11 @@ namespace Ulmus\Repository; use Ulmus\{Repository, Query, Ulmus, Common}; class MssqlRepository extends Repository { -/* - protected function finalizeQuery() : void - { - if ( null !== $offset = $this->queryBuilder->getFragment(Query\Offset::class) ) { - if ( null === $limit = $this->queryBuilder->getFragment(Query\Limit::class) ) { - throw new \Exception("Your offset query fragment is missing a LIMIT value."); - } - - # an order by is mandatory for mssql offset/limit - if ( null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class) ) { - $this->orderBy("(SELECT 0)"); - } - - $mssqlOffset = new \Ulmus\Query\MsSQL\Offset(); - - $mssqlOffset->set($offset->offset, $limit->limit); - - $this->queryBuilder->removeFragment($offset); - $this->queryBuilder->removeFragment($limit); - - $this->queryBuilder->push($mssqlOffset); - } - elseif ( null !== $limit = $this->queryBuilder->getFragment(Query\Limit::class) ) { - - if ( null !== $select = $this->queryBuilder->getFragment(Query\Select::class) ) { - $select->top = $limit->limit; - } - elseif ( null !== $delete = $this->queryBuilder->getFragment(Query\Delete::class) ) { - $delete->top = $limit->limit; - } - - $this->queryBuilder->removeFragment($limit); - } - } -*/ protected function finalizeQuery() : void { - if ( null !== $offset = $this->queryBuilder->getFragment(Query\Offset::class) ) { - # an order by is mandatory for mssql offset/limit - if ( null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class) ) { - $this->orderBy("(SELECT 0)"); - } - - if ( empty ($offset->offset ) ) { - if ( null !== $select = $this->queryBuilder->getFragment(Query\Select::class) ) { - $select->top = $offset->limit; - } - elseif ( null !== $delete = $this->queryBuilder->getFragment(Query\Delete::class) ) { - $delete->top = $offset->limit; - } - $this->queryBuilder->removeFragment($offset); - } - elseif ( empty($offset->limit) ) { - throw new \Exception("Your offset query fragment is missing a LIMIT value."); - } - } - - if ( null !== $limit = $this->queryBuilder->getFragment(Query\Limit::class) ) { - $this->queryBuilder->removeFragment($limit); + if ( null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class) ) { + $this->orderBy("(SELECT 0)"); } } diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php index 8024250..5c28a79 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -45,11 +45,29 @@ class RelationBuilder return $dataset; } - return $this->resolveRelation($name); + return $this->resolveRelation($name) ?: $this->resolveVirtual($name); } + elseif ( $relation = $this->resolver->searchFieldAnnotation($name, new Relation() ) ) { + return $this->instanciateEmptyObject($name, $relation); + } + + return false; } - protected function resolveRelation(string $name) /* : object|EntityCollection */ + protected function resolveVirtual(string $name) /* : bool|object|EntityCollection */ + { + if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, new Annotation\Property\Virtual()))) { + if ($virtual->closure ?? false) { + return call_user_func_array($virtual->closure, [ $this->entity ]); + } + + return call_user_func_array([ $this->entity, $virtual->method ], [ $this->entity ]); + } + + return false; + } + + protected function resolveRelation(string $name) /* : bool|object|EntityCollection */ { if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, new Relation() ) ) ) { $this->orders = $this->resolver->searchFieldAnnotationList($name, new OrderBy() ); @@ -139,6 +157,25 @@ class RelationBuilder return new $class(); } + + protected function instanciateEmptyObject(string $name, Relation $relation) : object + { + switch( true ) { + case $relation->isOneToOne(): + return $this->instanciateEmptyEntity($name, $relation); + + case $relation->isOneToMany(): + return ($relation->entity ?? $this->resolver->properties[$name]['type'])::entityCollection(); + + case $relation->isManyToMany(): + extract($this->relationAnnotations($name, $relation)); + + return $relation->bridgeField ?? false ? $relation->bridge::entityCollection() : $relationRelation->entity::entityCollection(); + } + + return new $class(); + } + protected function fetchFromDataset($name, ? array $data = null) /* object|bool */ { $annotation = $this->resolver->searchFieldAnnotation($name, new Annotation\Property\Join) ?: diff --git a/src/SearchRequest/SearchRequestPaginationTrait.php b/src/SearchRequest/SearchRequestPaginationTrait.php index b07b6e7..356703d 100644 --- a/src/SearchRequest/SearchRequestPaginationTrait.php +++ b/src/SearchRequest/SearchRequestPaginationTrait.php @@ -15,7 +15,17 @@ trait SearchRequestPaginationTrait { public bool $skipCount = false; public ? array $columns = null; - + + public function count(): int + { + return $this->count; + } + + public function page(): int + { + return $this->page; + } + public function limit(): int { return $this->limit; @@ -23,7 +33,7 @@ trait SearchRequestPaginationTrait { public function offset(): int { - return (int) ( abs( ( $this->page - 1 ) * $this->limit() ) ); + return (int) ( abs( ( $this->page() - 1 ) * $this->limit() ) ); } public function pagination(int $page, int $itemCount) : void @@ -34,17 +44,25 @@ trait SearchRequestPaginationTrait { public function pageCount() : int { - return ceil($this->count / $this->limit()); + return ceil($this->count() / $this->limit()); } public function hasPagination() : int { return $this->pageCount() > 1; } + public function skipCount(bool $value) : self { $this->skipCount = $value; return $this; } + + public function resultShown() : int + { + $total = $this->page() * $this->limit(); + + return $total <= $this->count() ? $total : $this->count(); + } }