diff --git a/src/Annotation/Property/Field.php b/src/Annotation/Property/Field.php index 7712289..25d1d93 100644 --- a/src/Annotation/Property/Field.php +++ b/src/Annotation/Property/Field.php @@ -14,7 +14,7 @@ class Field implements \Ulmus\Annotation\Annotation { public array $attributes = []; - public bool $nullable = false; + public bool $nullable; public function __construct(? string $type = null, ? int $length = null) { diff --git a/src/Annotation/Property/Field/Date.php b/src/Annotation/Property/Field/Date.php new file mode 100644 index 0000000..2bbf061 --- /dev/null +++ b/src/Annotation/Property/Field/Date.php @@ -0,0 +1,11 @@ +method = $method; + } + } +} diff --git a/src/Annotation/Property/Having.php b/src/Annotation/Property/Having.php new file mode 100644 index 0000000..d2cb5a1 --- /dev/null +++ b/src/Annotation/Property/Having.php @@ -0,0 +1,5 @@ +type)); + } } diff --git a/src/Annotation/Property/Where.php b/src/Annotation/Property/Where.php index f7b0d35..e0e922d 100644 --- a/src/Annotation/Property/Where.php +++ b/src/Annotation/Property/Where.php @@ -6,13 +6,13 @@ use Ulmus\Query; class Where implements \Ulmus\Annotation\Annotation { - public string $field; + public /* stringable */ $field; public $value; public string $operator; - public function __construct(? string $field = null, $value = null, ? string $operator = null) + public function __construct(/* stringable */ $field = null, $value = null, ? string $operator = null) { if ( $field !== null ) { $this->field = $field; diff --git a/src/Common/EntityField.php b/src/Common/EntityField.php index 781f580..fd07f76 100644 --- a/src/Common/EntityField.php +++ b/src/Common/EntityField.php @@ -64,7 +64,7 @@ class EntityField public static function isObjectType($type) : bool { - # @ Should be fixed with isBuiltIn() instead, it won't be correct based only on name + # @Should be fixed with isBuiltIn() instead, it won't be correct based only on name # return strpos($type, "\\") !== false; } diff --git a/src/Entity/Field/Time.php b/src/Entity/Field/Time.php index df4db07..4b9f952 100644 --- a/src/Entity/Field/Time.php +++ b/src/Entity/Field/Time.php @@ -5,5 +5,4 @@ namespace Ulmus\Entity\Field; class Time extends Datetime { public string $format = "H:i:s"; - } diff --git a/src/EntityTrait.php b/src/EntityTrait.php index 153e59d..bb8ac09 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -8,8 +8,8 @@ use Ulmus\Repository, Ulmus\Common\EntityField; use Ulmus\Annotation\Classes\{ Method, Table, Collation, }; -use Ulmus\Annotation\Property\{ Field, Relation, OrderBy, Where, Join, Virtual }; -use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, }; +use Ulmus\Annotation\Property\{ Field, Relation, OrderBy, Where, Join, Filter, On }; +use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, }; trait EntityTrait { use EventTrait; @@ -40,69 +40,81 @@ trait EntityTrait { # @TODO REFACTOR THIS CODE ASAP ! if ( $this->isLoaded() ) { - if ( null !== ( $join= $entityResolver->searchFieldAnnotation($name, new Join() ) ) ) { + $annotation = $entityResolver->searchFieldAnnotation($name, new Annotation\Property\Join) ?: + $entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation); + + if ( $annotation ) { $vars = []; - $entity = $join->entity ?? $entityResolver->properties[$name]['type']; + $len = strlen( $name ) + 1; foreach($this->entityDatasetUnmatchedFields as $key => $value) { - $len = strlen( $name ) + 1; - if ( substr($key, 0, $len ) === "{$name}$" ) { $vars[substr($key, $len)] = $value; } } - if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) { - return ( new $entity() )->fromArray($data); + 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() ) ) ) { - $relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type)); - $order = $entityResolver->searchFieldAnnotationList($name, new OrderBy() ); $where = $entityResolver->searchFieldAnnotationList($name, new Where() ); + $filters = $entityResolver->searchFieldAnnotationList($name, new Filter() ); - if ( $relation->entity ?? false ) { - $baseEntity = $relation->entity(); + $baseEntity = $relation->entity ?? $relation->bridge ?? $entityResolver->properties[$name]['type']; - $repository = $baseEntity->repository(); + $repository = $baseEntity::repository()->open(); - foreach($where as $condition) { - $repository->where($condition->field, is_callable($condition->value) ? $f = call_user_func_array($condition->value, [ $this ]) : $condition->value, $condition->operator); - } - - foreach($order as $item) { - $repository->orderBy($item->field, $item->order); - } - - $field = $relation->key; + 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); } - switch( $relationType ) { - case 'onetoone': - if ($relation->foreignKey) { - $repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity->field($relation->foreignKey), is_callable($field) ? $field($this) : $this->$field ); + $repository->close(); + + foreach ($order as $item) { + $repository->orderBy($item->field, $item->order); + } + + $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 $baseEntity; + 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 ); + $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([$repository, $relation->function]); + return $this->$name = call_user_func([$applyFilter($repository), $relation->function]); case 'manytomany': if ( false === $relation->bridge ?? false ) { @@ -128,17 +140,21 @@ trait EntityTrait { ->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([ $repository, $relationRelation->function ]); + $this->$name = call_user_func([ $applyFilter($repository), $relationRelation->function ]); if ($relation->bridgeField ?? false) { $repository = $relationRelation->entity::repository(); @@ -148,10 +164,14 @@ trait EntityTrait { ->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); } @@ -176,8 +196,11 @@ trait EntityTrait { */ public function __isset(string $name) : bool { + #if ( null !== $relation = static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) { + # return isset($this->{$relation->key}); + #} if ( $this->isLoaded() && static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) { - return true; + return true; } return isset($this->$name); @@ -186,18 +209,18 @@ trait EntityTrait { /** * @Ignore */ - public function entityFillFromDataset(iterable $dataset, bool $isLoadedDataset = false) : self + public function entityFillFromDataset(iterable $dataset) : self { $loaded = $this->isLoaded(); $entityResolver = $this->resolveEntity(); - + foreach($dataset as $key => $value) { $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null; if ( $field === null ) { $field = $entityResolver->field($key, EntityResolver::KEY_ENTITY_NAME, false); - } + } if ( $field === null ) { if ($this->entityStrictFieldsDeclaration ) { @@ -227,6 +250,9 @@ trait EntityTrait { $value = substr($value, 0, $annotation->length); } } + elseif ( $field['type'] === 'bool' ) { + $this->{$field['name']} = (bool) $value; + } $this->{$field['name']} = $value; } @@ -297,6 +323,10 @@ trait EntityTrait { case is_array($this->$key): $dataset[$realKey] = Ulmus::encodeArray($this->$key); break; + + case is_bool($this->$key): + $dataset[$realKey] = (int) $this->$key; + break; default: $dataset[$realKey] = $this->$key; @@ -307,6 +337,7 @@ trait EntityTrait { } } + # @TODO Must fix recursive bug ! if ($includeRelations) { foreach($entityResolver->properties as $name => $field){ $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ); diff --git a/src/Migration/FieldDefinition.php b/src/Migration/FieldDefinition.php index c129c04..d888512 100644 --- a/src/Migration/FieldDefinition.php +++ b/src/Migration/FieldDefinition.php @@ -3,6 +3,7 @@ namespace Ulmus\Migration; use Ulmus\Annotation\Property\Field; +use Ulmus\Entity; class FieldDefinition { @@ -28,7 +29,6 @@ class FieldDefinition { $this->builtIn = $data['builtin']; $this->tags = $data['tags']; - $field = $this->getFieldTag(); $this->type = $field->type ?? $data['type']; $this->length = $field->length ?? null; @@ -45,8 +45,19 @@ class FieldDefinition { public function getSqlType(bool $typeOnly = false) : string { $type = $this->type; + $length = $this->length; + if ( is_a($type, Entity\Field\Date::class, true) ) { + $type = "DATE"; + } + elseif ( is_a($type, Entity\Field\Time::class, true) ) { + $type = "TIME"; + } + elseif ( is_a($type, \DateTime::class, true) ) { + $type = "DATETIME"; + } + switch($type) { case "bool": $type = "TINYINT"; diff --git a/src/Query/Create.php b/src/Query/Create.php index 4c9476c..bc345c5 100644 --- a/src/Query/Create.php +++ b/src/Query/Create.php @@ -31,7 +31,7 @@ class Create extends Fragment { public function renderFields() : string { return "(" . PHP_EOL . implode(",".PHP_EOL, array_map(function($field) { - return " ".EntityField::generateCreateColumn($field); + return " " . EntityField::generateCreateColumn($field); }, $this->fieldList)) . PHP_EOL . ")"; } } diff --git a/src/Query/Join.php b/src/Query/Join.php index 58a0726..d0f9c2c 100644 --- a/src/Query/Join.php +++ b/src/Query/Join.php @@ -37,6 +37,8 @@ class Join extends Fragment public /* QueryBuilder */ $queryBuilder; + public int $order = 40; + public function __construct(QueryBuilder $queryBuilder) { $this->queryBuilder = new QueryBuilder(); @@ -47,18 +49,14 @@ class Join extends Fragment { $this->side = $side; $this->table = $table; - $this->field = $field; - $this->value = $value; + + $this->where($this->field = $field, $this->value = $value); } public function render() : string { - if ($this->queryBuilder->where ?? false ) { - $where = $this->renderSegments([Where::CONDITION_AND, $this->queryBuilder->render(true)]); - } - return $this->renderSegments([ - strtoupper($this->side), $this->outer ? static::SQL_OUTER : "", static::SQL_TOKEN, $this->table, $this->alias ?? "", $this->attachment, $this->field, "=", $this->value, $where ?? "" + strtoupper($this->side), $this->outer ? static::SQL_OUTER : "", static::SQL_TOKEN, $this->table, $this->alias ?? "", $this->attachment, $this->queryBuilder->render(true) ?? "" ]); } } \ No newline at end of file diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index a19aa19..40fcf00 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -191,7 +191,7 @@ class QueryBuilder return $this; } - public function where($field, $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self + public function where(/* stringable*/ $field, $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self { # Empty IN case if ( [] === $value ) { diff --git a/src/Repository.php b/src/Repository.php index a2d831c..73931d4 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -2,6 +2,7 @@ namespace Ulmus; +use Ulmus\Annotation\Property\{ Where, Having, Relation, Join }; use Ulmus\Common\EntityResolver; class Repository @@ -145,7 +146,7 @@ class Repository $pkField = key($primaryKeyDefinition); $dataset[$pkField] = $statement->lastInsertId; } - + $entity->entityFillFromDataset($dataset, true); } else { @@ -161,7 +162,7 @@ class Repository $update = $this->updateSqlQuery($diff)->runQuery(); $entity->entityFillFromDataset($dataset); - + return $update ? (bool) $update->rowCount() : false; } } @@ -378,7 +379,6 @@ class Repository return $this->where($primaryKeyField[$pkField]->name ?? $pkField, $value); } - public function withJoin(/*string|array*/ $fields) : self { @@ -386,20 +386,53 @@ class Repository $this->select("{$this->alias}.*"); } + # Apply FILTER annotation to this too ! foreach((array) $fields as $item) { - if ( null !== $join = $this->entityResolver->searchFieldAnnotation($item, new Annotation\Property\Join) ) { - $alias = $join->alias ?? $item; + $annotation = $this->entityResolver->searchFieldAnnotation($item, new Join) ?: + $this->entityResolver->searchFieldAnnotation($item, new Relation); - $entity = $join->entity ?? $this->entityResolver->properties[$item]['type']; + if (( $annotation instanceof Relation ) && ( $annotation->normalizeType() === 'manytomany' )) { + throw new Exception("Many-to-many relation can not be preloaded within joins."); + } + + if ( $annotation ) { + $alias = $annotation->alias ?? $item; + + $entity = $annotation->entity ?? $this->entityResolver->properties[$item]['type']; foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { $this->select("$alias.$key as {$alias}\${$field['name']}"); } - $key = is_string($join->key) ? $this->entityClass::field($join->key) : $join->key; - $foreignKey = is_string($join->foreignKey) ? $entity::field($join->foreignKey, $alias) : $join->foreignKey; + $this->open(); - $this->join($join->type, $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias); + foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) { + if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) { + $this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); + } + } + + foreach($this->entityResolver->searchFieldAnnotationList($item, new Having() ) as $condition) { + $this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); + } + + $this->close(); + + $key = is_string($annotation->key) ? $this->entityClass::field($annotation->key) : $annotation->key; + + $foreignKey = is_string($annotation->foreignKey) ? $entity::field($annotation->foreignKey, $alias) : $annotation->foreignKey; + + $this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias) { + foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) { + $field = clone $condition->field; + + if ( is_object($condition->field) && ( $condition->field->entityClass === $entity ) ) { + $field->alias = $alias; + + $join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->value, $condition->operator); + } + } + }); } else { throw new \Exception("You referenced field `$item` which do not exist or do not contain a valid @Join annotation."); @@ -409,7 +442,6 @@ class Repository return $this; } - public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self { $searchRequest->count = $searchRequest->filter( clone $this )