From 16c6eb57fbddd777c6949bbf00df854faf634df6 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Mon, 1 Mar 2021 16:26:06 +0000 Subject: [PATCH] - Added a MssqlRepository - Adapter some code to allow more flexibility for the new LDAP adapter from ulmus-ldap. - Insert's fields are now escaped by default. - A new RelationBuilder was added, a lot of code from the EntityTrait was moved into this. Some code from the Repository class will need to be moved there too. --- src/Adapter/AdapterInterface.php | 3 +- src/Adapter/MsSQL.php | 3 + src/Adapter/MySQL.php | 3 + src/Annotation/Property/Field/Bigint.php | 11 + src/Annotation/Property/Field/Longtext.php | 11 + src/Annotation/Property/Field/Mediumtext.php | 11 + src/Annotation/Property/Field/Text.php | 11 + src/Annotation/Property/Field/Tinyint.php | 11 + src/Annotation/Property/Relation.php | 32 ++- src/Annotation/Property/Relation/Ignore.php | 5 + src/Annotation/Property/Where.php | 2 +- src/Annotation/Property/WithJoin.php | 15 + src/Common/EntityField.php | 2 +- src/Common/EntityResolver.php | 19 +- src/ConnectionAdapter.php | 5 + src/EntityCollection.php | 8 +- src/EntityTrait.php | 187 +------------ src/Query/Create.php | 1 - src/Query/Fragment.php | 2 +- src/Query/GroupBy.php | 2 +- src/Query/Having.php | 8 +- src/Query/Join.php | 2 +- src/Query/QueryBuilderInterface.php | 13 + src/Query/Set.php | 4 +- src/Query/Where.php | 10 +- src/QueryBuilder.php | 14 +- src/Repository.php | 143 ++++++++-- src/Repository/MssqlRepository.php | 42 +++ src/Repository/RelationBuilder.php | 259 ++++++++++++++++++ .../ServerRequestCountRepository.php | 7 + .../SearchRequestPaginationTrait.php | 2 + src/Ulmus.php | 36 +-- 32 files changed, 641 insertions(+), 243 deletions(-) create mode 100644 src/Annotation/Property/Field/Bigint.php create mode 100644 src/Annotation/Property/Field/Longtext.php create mode 100644 src/Annotation/Property/Field/Mediumtext.php create mode 100644 src/Annotation/Property/Field/Text.php create mode 100644 src/Annotation/Property/Field/Tinyint.php create mode 100644 src/Annotation/Property/Relation/Ignore.php create mode 100644 src/Annotation/Property/WithJoin.php create mode 100644 src/Query/QueryBuilderInterface.php create mode 100644 src/Repository/MssqlRepository.php create mode 100644 src/Repository/RelationBuilder.php create mode 100644 src/Repository/ServerRequestCountRepository.php diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php index ec8e6aa..d0ca171 100644 --- a/src/Adapter/AdapterInterface.php +++ b/src/Adapter/AdapterInterface.php @@ -9,8 +9,9 @@ interface AdapterInterface { public const IDENTIFIER_TABLE = 2; public const IDENTIFIER_DATABASE = 3; public const IDENTIFIER_SCHEMA = 4; + public const IDENTIFIER_VALUE = 5; - public function connect() : PdoObject; + public function connect() : object /* | PdoObject|mixed */; public function buildDataSourceName() : string; public function setup(array $configuration) : void; public function escapeIdentifier(string $segment, int $type) : string; diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index ade60fc..5d84ee7 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -194,6 +194,9 @@ class MsSQL implements AdapterInterface { case static::IDENTIFIER_TABLE: case static::IDENTIFIER_FIELD: return "[" . str_replace(["[", "]"], [ "[[", "]]" ], $segment) . "]"; + + case static::IDENTIFIER_VALUE: + return "'$segment'"; } } diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index e033103..910c162 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -131,6 +131,9 @@ class MySQL implements AdapterInterface { case static::IDENTIFIER_TABLE: case static::IDENTIFIER_FIELD: return "`" . str_replace("`", "``", $segment) . "`"; + + case static::IDENTIFIER_VALUE: + return "\"$segment\""; } } diff --git a/src/Annotation/Property/Field/Bigint.php b/src/Annotation/Property/Field/Bigint.php new file mode 100644 index 0000000..01b49ac --- /dev/null +++ b/src/Annotation/Property/Field/Bigint.php @@ -0,0 +1,11 @@ +type = $type; } } - + public function entity() { try { - $e = $this->entity; + $e = $this->entity; } catch (\Throwable $ex) { throw new \Exception("Your @Relation annotation seems to be missing an `entity` entry."); } - + return new $e(); } - + public function bridge() { $e = $this->bridge; - + return new $e(); } @@ -51,4 +51,24 @@ class Relation implements \Ulmus\Annotation\Annotation { { return strtolower(str_replace(['-', '_', ' '], '', $this->type)); } + + public function isOneToOne() : bool + { + return $this->normalizeType() === 'onetoone'; + } + + public function isOneToMany() : bool + { + return $this->normalizeType() === 'onetomany'; + } + + public function isManyToMany() : bool + { + return $this->normalizeType() === 'manytomany'; + } + + public function hasBridge() : bool + { + return isset($this->bridge); + } } diff --git a/src/Annotation/Property/Relation/Ignore.php b/src/Annotation/Property/Relation/Ignore.php new file mode 100644 index 0000000..0b6ef93 --- /dev/null +++ b/src/Annotation/Property/Relation/Ignore.php @@ -0,0 +1,5 @@ +field = $field; diff --git a/src/Annotation/Property/WithJoin.php b/src/Annotation/Property/WithJoin.php new file mode 100644 index 0000000..ff582b0 --- /dev/null +++ b/src/Annotation/Property/WithJoin.php @@ -0,0 +1,15 @@ +joins = (array)$joins; + } + } +} diff --git a/src/Common/EntityField.php b/src/Common/EntityField.php index fd07f76..c0f72c9 100644 --- a/src/Common/EntityField.php +++ b/src/Common/EntityField.php @@ -32,7 +32,7 @@ class EntityField $name = $this->entityResolver->databaseAdapter()->adapter()->escapeIdentifier($name, AdapterInterface::IDENTIFIER_FIELD); - return $useAlias ? "{$this->alias}.$name" : $name; + return $useAlias && $this->alias ? "{$this->alias}.$name" : $name; } public static function isScalarType($type) : bool diff --git a/src/Common/EntityResolver.php b/src/Common/EntityResolver.php index 5e6a952..3dc23fc 100644 --- a/src/Common/EntityResolver.php +++ b/src/Common/EntityResolver.php @@ -15,6 +15,8 @@ class EntityResolver { const KEY_COLUMN_NAME = 02; + const KEY_LC_ENTITY_NAME = 03; + public string $entityClass; public array $uses; @@ -64,6 +66,11 @@ class EntityResolver { } switch($fieldKey) { + case static::KEY_LC_ENTITY_NAME: + $key = strtolower($item['name']); + break; + + case static::KEY_ENTITY_NAME: $key = $item['name']; break; @@ -158,12 +165,12 @@ class EntityResolver { return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->database; } - public function databaseAdapter() : \Ulmus\ConnectionAdapter + public function sqlAdapter() : \Ulmus\ConnectionAdapter { if ( null !== $table = $this->getAnnotationFromClassname( Table::class ) ) { if ( $table->adapter ?? null ) { if ( null === ( $adapter = \Ulmus\Ulmus::$registeredAdapters[$table->adapter] ?? null ) ) { - throw new \Exception("Requested database adapter ( {$table->adapter} ) is not registered."); + throw new \Exception("Requested database adapter `{$table->adapter}` is not registered."); } else { return $adapter; @@ -174,6 +181,14 @@ class EntityResolver { return Ulmus::$defaultAdapter; } + /** + * Alias of sqlAdapter + */ + public function databaseAdapter() : \Ulmus\ConnectionAdapter + { + return $this->sqlAdapter(); + } + public function schemaName(bool $required = false) : ? string { if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) { diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php index 617b2c2..2964efe 100644 --- a/src/ConnectionAdapter.php +++ b/src/ConnectionAdapter.php @@ -65,6 +65,11 @@ class ConnectionAdapter return $this->pdo ?? $this->pdo = $this->connect()->pdo; } + public function connector() : object + { + return $this->pdo(); + } + /** * Instanciate an adapter which interact with the data source * @param string $name An Ulmus adapter or full class name implementing AdapterInterface diff --git a/src/EntityCollection.php b/src/EntityCollection.php index e490113..478058b 100644 --- a/src/EntityCollection.php +++ b/src/EntityCollection.php @@ -233,7 +233,6 @@ class EntityCollection extends \ArrayObject { public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self { - foreach($datasets as $dataset) { $this->append( $this->arrayToEntity($dataset, $entityClass) ); } @@ -247,7 +246,7 @@ class EntityCollection extends \ArrayObject { throw new \Exception("An entity class name must be provided to be instanciated and populated before insertion into this collection."); } - $className = $this->entityClass; + $className = $entityClass ?: $this->entityClass; return ( new $className() )->fromArray($dataset); } @@ -318,4 +317,9 @@ class EntityCollection extends \ArrayObject { { return $this->replaceWith(array_reverse($this->getArrayCopy()));; } + + public function slice(int $offset, ? int $length = null) : self + { + return new self(array_slice($this->getArrayCopy(), $offset, $length)); + } } diff --git a/src/EntityTrait.php b/src/EntityTrait.php index e7e2e74..842c21e 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -8,8 +8,9 @@ use Ulmus\Repository, Ulmus\Common\EntityField; use Ulmus\Annotation\Classes\{ Method, Table, Collation, }; -use Ulmus\Annotation\Property\{ Field, Filter, Relation, OrderBy, Where, Join, Virtual, On, }; -use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, }; +use Ulmus\Annotation\Property\{ Field, Filter, Relation, OrderBy, Where, Join, Virtual, On, WithJoin, }; +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 { use EventTrait; @@ -28,174 +29,18 @@ trait EntityTrait { * @Ignore */ public array $entityLoadedDataset = []; - + /** * @Ignore */ public function __get(string $name) { - $entityResolver = $this->resolveEntity(); - - # Resolve relations here if one is called - - # @TODO REFACTOR THIS CODE ASAP ! - if ( $this->isLoaded() ) { - $annotation = $entityResolver->searchFieldAnnotation($name, new Annotation\Property\Join) ?: - $entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation); - - if ( $annotation ) { - $vars = []; - - $len = strlen( $name ) + 1; - - foreach($this->entityDatasetUnmatchedFields as $key => $value) { - if ( substr($key, 0, $len ) === "{$name}$" ) { - $vars[substr($key, $len)] = $value; - } - } - - if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) { - $entity = $annotation->entity ?? $entityResolver->properties[$name]['type']; - - return $this->$name = ( new $entity() )->fromArray($data)->resetVirtualProperties(); - } - } - - if ( null !== ( $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ) ) ) { - $order = $entityResolver->searchFieldAnnotationList($name, new OrderBy() ); - $where = $entityResolver->searchFieldAnnotationList($name, new Where() ); - $filters = $entityResolver->searchFieldAnnotationList($name, new Filter() ); - - $baseEntity = $relation->entity ?? $relation->bridge ?? $entityResolver->properties[$name]['type']; - $repository = $baseEntity::repository()->open(); - - 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); - } - - $repository->close(); - - foreach ($order as $item) { - $repository->orderBy($item->field, $item->order); - } - - $applyFilter = function($repository) use ($filters, $name) { - foreach($filters as $filter) { - $repository = call_user_func_array([ $this, $filter->method ], [ $repository, $name ]); - } - - return $repository; - }; - - $field = $relation->key; - - $applyFilter = function($repository) use ($filters, $name) { - foreach($filters as $filter) { - $repository = call_user_func_array([ $this, $filter->method ], [ $repository, $name ]); - } - - return $repository; - }; - - switch( $relation->normalizeType() ) { - case 'onetoone': - $repository->limit(1); - - if ($relation->foreignKey) { - $repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), is_callable($field) ? $field($this) : $this->$field ); - } - - $this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository); - - $result = call_user_func([$repository, $relation->function]); - - if ( count($result) === 0 ) { - return new $baseEntity(); - } - - return $this->$name = $result[0]; - - case 'onetomany': - if ($relation->foreignKey) { - $repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), is_callable($field) ? $field($this) : $this->$field ); - } - - $this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository); - - return $this->$name = call_user_func([$applyFilter($repository), $relation->function]); - - case 'manytomany': - if ( false === $relation->bridge ?? false ) { - throw new \Exception("Your many-to-many @Relation() from variable `$name` is missing a 'bridge' value."); - } - - $bridgeEntity = Ulmus::resolveEntity($relation->bridge); - $bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, new Relation() ); - $relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, new Relation() ); - - if ($relationRelation === null) { - throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}"); - } - - $repository = $relationRelation->entity()->repository(); - - $bridgeAlias = uniqid("bridge_"); - $relationAlias = uniqid("relation_"); - - # @TODO Rewrite to be done here, this code must move somewhere else... - $repository->select("{$repository->alias}.*") - ->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias) - ->join(Query\Join::TYPE_INNER, $this->resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), static::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias) - ->where( static::field($bridgeRelation->foreignKey, $relationAlias), $this->{$bridgeRelation->foreignKey} ); - - $repository->open(); - - foreach($where as $condition) { - $repository->where($condition->field, $condition->value, $condition->operator); - } - - $repository->close(); - - foreach($order as $item) { - $repository->orderBy($item->field, $item->order); - } - - $this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository); - - $this->$name = call_user_func([ $applyFilter($repository), $relationRelation->function ]); - - if ($relation->bridgeField ?? false) { - $repository = $relationRelation->entity::repository(); - - $repository->select("$bridgeAlias.*") - ->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias) - ->join(Query\Join::TYPE_INNER, $this->resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), static::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias) - ->where( static::field($bridgeRelation->foreignKey, $relationAlias), $this->{$bridgeRelation->foreignKey} ); - - $repository->open(); - - foreach($where as $condition) { - $repository->where($condition->field, $condition->value, $condition->operator); - } - - $repository->close(); - - foreach($order as $item) { - $repository->orderBy($item->field, $item->order); - } - - $bridgeName = $relation->bridgeField; - - $this->$bridgeName = $repository->collectionFromQuery($relation->bridge); - } - - return $this->$name; - } - - return; - } + $relation = new Repository\RelationBuilder($this); + + if ( false !== $data = $relation->searchRelation($name) ) { + return $this->$name = $data; } - + throw new \Exception(sprintf("[%s] - Undefined variable: %s", static::class, $name)); } @@ -227,7 +72,7 @@ trait EntityTrait { $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null; if ( $field === null ) { - $field = $entityResolver->field($key, EntityResolver::KEY_ENTITY_NAME, false); + $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false); } if ( $field === null ) { @@ -261,7 +106,7 @@ trait EntityTrait { elseif ( $field['type'] === 'bool' ) { $this->{$field['name']} = (bool) $value; } - + $this->{$field['name']} = $value; } elseif ( ! $field['builtin'] ) { @@ -385,7 +230,7 @@ trait EntityTrait { */ public function toCollection() : EntityCollection { - return static::entityCollection([ $this->toArray() ]); + return static::entityCollection([ $this ]); } /** @@ -445,14 +290,6 @@ trait EntityTrait { return $collection; } - /** - * @Ignore - */ - public static function queryBuilder() : QueryBuilder - { - return Ulmus::queryBuilder(static::class); - } - /** * @Ignore */ diff --git a/src/Query/Create.php b/src/Query/Create.php index bc345c5..ed7ca4b 100644 --- a/src/Query/Create.php +++ b/src/Query/Create.php @@ -5,7 +5,6 @@ namespace Ulmus\Query; use Ulmus\Annotation, Ulmus\Common\EntityField; - class Create extends Fragment { const SQL_TOKEN = "CREATE TABLE"; diff --git a/src/Query/Fragment.php b/src/Query/Fragment.php index 5dd7d23..5b83a6b 100644 --- a/src/Query/Fragment.php +++ b/src/Query/Fragment.php @@ -6,7 +6,7 @@ abstract class Fragment { public int $order = 0; - public abstract function render() : string; + public abstract function render() /*: mixed*/; protected function renderSegments(array $segments, string $glue = " ") : string { diff --git a/src/Query/GroupBy.php b/src/Query/GroupBy.php index fd969cf..feb81b0 100644 --- a/src/Query/GroupBy.php +++ b/src/Query/GroupBy.php @@ -11,7 +11,7 @@ class GroupBy extends Fragment { public function set(array $order) : self { - $this->groupBy = [ $orderĀ ]; + $this->groupBy = [ $order ]; return $this; } diff --git a/src/Query/Having.php b/src/Query/Having.php index 166a56a..a106e99 100644 --- a/src/Query/Having.php +++ b/src/Query/Having.php @@ -14,13 +14,13 @@ class Having extends Fragment { public array $conditionList; - public QueryBuilder $queryBuilder; + public QueryBuilderInterface $queryBuilder; public ? Having $parent = null; public string $condition = Where::CONDITION_AND; - public function __construct(? QueryBuilder $queryBuilder, $condition = Where::CONDITION_AND) + public function __construct(? QueryBuilderInterface $queryBuilder, $condition = Where::CONDITION_AND) { $this->queryBuilder = $queryBuilder; $this->condition = $condition; @@ -70,11 +70,11 @@ class Having extends Fragment { public string $field; public string $operator; public string $condition; - public QueryBuilder $queryBuilder; + public QueryBuilderInterface $queryBuilder; protected string $content = ""; - public function __construct(QueryBuilder $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) { + public function __construct(QueryBuilderInterface $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) { $this->queryBuilder = $queryBuilder; $this->field = $field; $this->value = $value; diff --git a/src/Query/Join.php b/src/Query/Join.php index d0f9c2c..77a5e95 100644 --- a/src/Query/Join.php +++ b/src/Query/Join.php @@ -39,7 +39,7 @@ class Join extends Fragment public int $order = 40; - public function __construct(QueryBuilder $queryBuilder) + public function __construct(QueryBuilderInterface $queryBuilder) { $this->queryBuilder = new QueryBuilder(); $this->queryBuilder->parent = $queryBuilder; diff --git a/src/Query/QueryBuilderInterface.php b/src/Query/QueryBuilderInterface.php new file mode 100644 index 0000000..9f40562 --- /dev/null +++ b/src/Query/QueryBuilderInterface.php @@ -0,0 +1,13 @@ +queryBuilder = $queryBuilder; } diff --git a/src/Query/Where.php b/src/Query/Where.php index 7b24b73..d1db936 100644 --- a/src/Query/Where.php +++ b/src/Query/Where.php @@ -2,8 +2,6 @@ namespace Ulmus\Query; -use Ulmus\QueryBuilder; - use Ulmus\Common\EntityField, Ulmus\Common\Sql; @@ -24,13 +22,13 @@ class Where extends Fragment { public array $conditionList; - public QueryBuilder $queryBuilder; + public QueryBuilderInterface $queryBuilder; public ? Where $parent = null; public string $condition = self::CONDITION_AND; - public function __construct(? QueryBuilder $queryBuilder, $condition = self::CONDITION_AND) + public function __construct(? QueryBuilderInterface $queryBuilder, $condition = self::CONDITION_AND) { $this->queryBuilder = $queryBuilder; $this->condition = $condition; @@ -80,11 +78,11 @@ class Where extends Fragment { public string $field; public string $operator; public string $condition; - public QueryBuilder $queryBuilder; + public QueryBuilderInterface $queryBuilder; protected string $content = ""; - public function __construct(QueryBuilder $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) { + public function __construct(QueryBuilderInterface $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) { $this->queryBuilder = $queryBuilder; $this->field = $field; $this->value = $value; diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 23d50c6..ec78463 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -2,13 +2,15 @@ namespace Ulmus; -class QueryBuilder +use Ulmus\Query\QueryBuilderInterface; + +class QueryBuilder implements Query\QueryBuilderInterface { public Query\Where $where; public Query\Having $having; - public QueryBuilder $parent; + public QueryBuilderInterface $parent; /** * Those are the parameters we are going to bind to PDO. @@ -371,7 +373,7 @@ class QueryBuilder return array_shift($this->queryStack); } - public function render(bool $skipToken = false) : string + public function render(bool $skipToken = false) /* : mixed */ { $sql = []; @@ -396,11 +398,13 @@ class QueryBuilder unset($this->where, $this->having); } - public function getFragment(string $class) : ? Query\Fragment + public function getFragment(string $class, int $index = 0) : ? Query\Fragment { foreach($this->queryStack as $item) { if ( get_class($item) === $class ) { - return $item; + if ( $index-- === 0 ) { + return $item; + } } } diff --git a/src/Repository.php b/src/Repository.php index db8e757..0e54639 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -2,7 +2,7 @@ namespace Ulmus; -use Ulmus\Annotation\Property\{ Where, Having, Relation, Join }; +use Ulmus\Annotation\Property\{ Where, Having, Relation, Join, WithJoin, Relation\Ignore as RelationIgnore }; use Ulmus\Common\EntityResolver; class Repository @@ -13,7 +13,7 @@ class Repository public ? ConnectionAdapter $adapter; - protected QueryBuilder $queryBuilder; + protected Query\QueryBuilderInterface $queryBuilder; protected EntityResolver $entityResolver; @@ -271,6 +271,39 @@ class Repository } } + 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) : self { $this->queryBuilder->select($fields); @@ -444,8 +477,8 @@ class Repository $this->select("{$this->alias}.*"); } - # Apply FILTER annotation to this too ! - foreach((array) $fields as $item) { + # @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); @@ -459,7 +492,9 @@ class Repository $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']}"); + if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore) ) { + $this->select("$alias.$key as {$alias}\${$field['name']}"); + } } $this->open(); @@ -482,9 +517,14 @@ class Repository $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) ) { + $field = $this->entityClass::field($condition->field); + } + else { + $field = clone $condition->field; + } - if ( is_object($condition->field) && ( $condition->field->entityClass === $entity ) ) { + if ( $condition->field->entityClass === $entity ) { $field->alias = $alias; $join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->value, $condition->operator); @@ -500,13 +540,77 @@ class Repository return $this; } - public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self + public function withSubquery(/*string|array*/ $fields) : self { - $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(); + # 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(new Repository\ServerRequestCountRepository($this->entityClass, $this->alias, $this->adapter)) + ->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) @@ -551,18 +655,18 @@ class Repository 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_keys($dataset), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName()); + $this->insert(array_map([ $this, 'escapeField' ] , array_keys($dataset)), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName()); } $this->values($dataset); @@ -651,6 +755,11 @@ class Repository return new $this->entityClass(); } + public function escapeField(string $identifier) : string + { + return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD); + } + public function escapeTable(string $identifier) : string { return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_TABLE); diff --git a/src/Repository/MssqlRepository.php b/src/Repository/MssqlRepository.php new file mode 100644 index 0000000..e1d1741 --- /dev/null +++ b/src/Repository/MssqlRepository.php @@ -0,0 +1,42 @@ +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); + } + } + +} diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php new file mode 100644 index 0000000..4ff6d34 --- /dev/null +++ b/src/Repository/RelationBuilder.php @@ -0,0 +1,259 @@ +entity = $entity; + $this->resolver = $entity::resolveEntity(); + + if ($repository) { + $this->repository = $repository; + } + } + + public function searchRelation(string $name) /* : object|EntityCollection|bool */ + { + # Resolve relations here if one is called + if ( $this->entity->isLoaded() ) { + if ( false !== ( $dataset = $this->fetchFromDataset($name) ) ) { + return $dataset; + } + + return $this->resolveRelation($name); + } + } + + protected function resolveRelation(string $name) /* : object|EntityCollection */ + { + if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, new Relation() ) ) ) { + $this->orders = $this->resolver->searchFieldAnnotationList($name, new OrderBy() ); + $this->wheres = $this->resolver->searchFieldAnnotationList($name, new Where() ); + $this->filters = $this->resolver->searchFieldAnnotationList($name, new Filter() ); + $this->joins = $this->resolver->searchFieldAnnotationList($name, new WithJoin() ); + + switch( true ) { + case $relation->isOneToOne(): + if ( $relation->hasBridge() ) { + # @TODO ! dump($this->relationAnnotations($name, $relation)); + } + else { + $this->oneToOne($name, $relation); + } + + $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); + + $result = call_user_func([ $this->repository, $relation->function ]); + + return count($result) === 0 ? $this->instanciateEmptyEntity($name, $relation): $result[0]; + + case $relation->isOneToMany(): + $this->oneToMany($name, $relation); + + $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); + + return call_user_func([ $this->repository, $relation->function ]); + + case $relation->isManyToMany(): + $this->manyToMany($name, $relation, $relationRelation); + + $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); + + $results = call_user_func([ $this->repository, $relationRelation->function ]); + + if ($relation->bridgeField ?? false) { + $collection = $relation->bridge::entityCollection(); + + foreach($results as $entity) { + $collection->append($this->fetchFromDataset($relation->bridgeField, $entity->entityLoadedDataset)); + } + + $this->entity->{$relation->bridgeField} = $collection; + } + + return $results; + } + } + + return false; + } + + protected function applyFilter(Repository $repository, string $name) : Repository + { + foreach($this->filters ?? [] as $filter) { + $repository = call_user_func_array([ $this->entity, $filter->method ], [ $repository, $name ]); + } + + return $repository; + } + + protected function applyWhere() : void + { + if ($this->wheres) { + $this->repository->open(); + + foreach($this->wheres as $condition) { + $this->repository->where($condition->field, $condition->value, $condition->operator); + } + + $this->repository->close(); + } + } + + protected function applyOrderBy() : void + { + foreach($this->orders as $item) { + $this->repository->orderBy($item->field, $item->order); + } + } + + protected function instanciateEmptyEntity(string $name, Relation $relation) : object + { + $class = $relation->entity ?? $this->resolver->properties[$name]['type']; + + return new $class(); + } + + protected function fetchFromDataset($name, ? array $data = null) /* object|bool */ + { + $annotation = $this->resolver->searchFieldAnnotation($name, new Annotation\Property\Join) ?: + $this->resolver->searchFieldAnnotation($name, new Annotation\Property\Relation); + + if ( $annotation ) { + $vars = []; + $len = strlen( $name ) + 1; + + if ( ( $annotation instanceof Relation ) && $annotation->isManyToMany() ) { + $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity; + } + else { + $entity = $annotation->entity ?? $this->resolver->properties[$name]['type']; + } + + foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) { + if ( $key === "{$name}\$collection" ) { + if ($value) { + if ( null === ( $dataset = \json_decode($value, true) ) ) { + throw new \Exception(sprintf("JSON error '%s' from '%s'", \json_last_error_msg(), $value)); + } + + return $entity::entityCollection()->fromArray($dataset)->iterate(fn($e) => $e->resetVirtualProperties()); + } + else { + return $entity::entityCollection(); + } + } + elseif ( substr($key, 0, $len ) === "{$name}\$" ) { + $vars[substr($key, $len)] = $value; + } + } + + if ($vars) { + if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) { + return ( new $entity() )->fromArray($data)->resetVirtualProperties(); + } + else { + return new $entity(); + } + } + } + + return false; + } + + public function oneToOne(string $name, Relation $relation) : Repository + { + return $this->oneToMany($name, $relation)->limit(1); + } + + public function oneToMany(string $name, Relation $relation) : Repository + { + $baseEntity = $relation->entity ?? $this->resolver->properties[$name]['type']; + + $this->repository = $baseEntity::repository(); + + $this->applyWhere(); + + $this->applyOrderBy(); + + $field = $relation->key; + + if ($relation->foreignKey) { + $this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), ! is_string($field) && is_callable($field) ? $field($this->entity) : $this->entity->$field ); + } + + return $this->applyFilter($this->repository, $name); + } + + public function manyToMany(string $name, Relation $relation, ? Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository + { + extract($this->relationAnnotations($name, $relation)); + + $this->repository = $relationRelation->entity::repository(); + + $bridgeAlias = $relation->bridgeField ?? uniqid("bridge_"); + + $relationAlias = $relation->field; + + $this->repository->select("{$this->repository->alias}.*") + ->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias, function($join) { + + })->join(Query\Join::TYPE_INNER, $this->entity::resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), $this->entity::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias, function($join) { + + })->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} ); + + if ($selectBridgeField && $relation->bridgeField) { + $this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias); + } + + return $this->applyFilter($this->repository, $name); + } + + public static function relationAnnotations(string $name, Relation $relation) : array + { + if ( $relation->isOneToOne() || $relation->isManyToMany() ) { + if ( ! $relation->hasBridge() ) { + throw new \Exception("Your many-to-many @Relation() from variable `$name` is missing a 'bridge' field."); + } + + $bridgeEntity = Ulmus::resolveEntity($relation->bridge); + $bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, new Relation() ); + $relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, new Relation() ); + + if ($relationRelation === null) { + throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}"); + } + + $relationRelation->entity ??= $relation->bridge::resolveEntity()->properties[$relation->foreignField]['type']; + + return [ + 'bridgeEntity' => $bridgeEntity, + 'bridgeRelation' => $bridgeRelation, + 'relationRelation' => $relationRelation, + ]; + } + + return []; + } +} \ No newline at end of file diff --git a/src/Repository/ServerRequestCountRepository.php b/src/Repository/ServerRequestCountRepository.php new file mode 100644 index 0000000..964b4f7 --- /dev/null +++ b/src/Repository/ServerRequestCountRepository.php @@ -0,0 +1,7 @@ +render(); + $rendered = $queryBuilder->render(); - $statement = ( $adapter ?: static::$defaultAdapter )->pdo()->select($sql, $queryBuilder->parameters ?? []); + $statement = ( $adapter ?: static::$defaultAdapter )->connector()->select($rendered, $queryBuilder->parameters ?? []); - while ( $row = $statement->fetch() ) { + $i = 0; + + while ( $row = $statement->fetch() ) { yield $row; } @@ -37,13 +39,13 @@ abstract class Ulmus ]; } - public static function datasetQueryBuilder(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null) : array + public static function datasetQueryBuilder(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) : array { $rows = []; + + $rendered = $queryBuilder->render(); - $sql = $queryBuilder->render(); - - $statement = ( $adapter ?: static::$defaultAdapter )->pdo()->select($sql, $queryBuilder->parameters ?? []); + $statement = ( $adapter ?: static::$defaultAdapter )->connector()->select($rendered, $queryBuilder->parameters ?? []); while ( $row = $statement->fetch() ) { $rows[] = $row; @@ -56,22 +58,22 @@ abstract class Ulmus return $rows; } - public static function pdo(? ConnectionAdapter $adapter = null) : Common\PdoObject + public static function connector(? ConnectionAdapter $adapter = null) : object { - return ( $adapter ?: static::$defaultAdapter )->pdo(); + return ( $adapter ?: static::$defaultAdapter )->connector(); } - public static function runSelectQuery(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null) + public static function runSelectQuery(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) { - $dataset = static::pdo($adapter)->select($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? [])); + $dataset = static::connector($adapter)->select($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? [])); $queryBuilder->reset(); return $dataset; } - public static function runQuery(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null) + public static function runQuery(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) { - $return = static::pdo($adapter)->runQuery($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? [])); + $return = static::connector($adapter)->runQuery($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? [])); $queryBuilder->reset(); return $return; @@ -87,7 +89,7 @@ abstract class Ulmus return new static::$repositoryClass(...$arguments); } - public static function queryBuilder(...$arguments) : QueryBuilder + public static function queryBuilder(...$arguments) : Query\QueryBuilderInterface { return new static::$queryBuilderClass(...$arguments); } @@ -116,9 +118,9 @@ abstract class Ulmus static::$registeredAdapters[$adapter->name] = $adapter; } - protected static function fetchQueryBuilder(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null) : array + protected static function fetchQueryBuilder(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) : array { - $result = ( $adapter ?: static::$defaultAdapter )->pdo->select($queryBuilder->render(), $queryBuilder->parameters ?? [])->fetchAll(); + $result = ( $adapter ?: static::$defaultAdapter )->connector->select($queryBuilder->render(), $queryBuilder->parameters ?? [])->fetchAll(); $queryBuilder->reset(); return $result;