- Work done on Join and using the @Relation annotation in case there is no join.

- Added some field type (date, Datetime, Time)
- Added a new @Filter annotation which allows to filter repository from relation into a method.
This commit is contained in:
Dave M. 2021-01-26 16:48:40 +00:00
parent 65de1dd849
commit e465ecdf32
17 changed files with 197 additions and 61 deletions

View File

@ -14,7 +14,7 @@ class Field implements \Ulmus\Annotation\Annotation {
public array $attributes = []; public array $attributes = [];
public bool $nullable = false; public bool $nullable;
public function __construct(? string $type = null, ? int $length = null) public function __construct(? string $type = null, ? int $length = null)
{ {

View File

@ -0,0 +1,11 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Date extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = "date", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Datetime extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = "datetime", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Time extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = "time", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Ulmus\Annotation\Property;
class Filter implements \Ulmus\Annotation\Annotation {
public string $method;
public function __construct(string $method = null)
{
if ( $method !== null ) {
$this->method = $method;
}
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace Ulmus\Annotation\Property;
class Having extends Where {}

View File

@ -0,0 +1,5 @@
<?php
namespace Ulmus\Annotation\Property;
class On extends Where {}

View File

@ -20,6 +20,8 @@ class Relation implements \Ulmus\Annotation\Annotation {
public string $entity; public string $entity;
public string $join;
public string $function = "loadAll"; public string $function = "loadAll";
public function __construct(string $type = null) public function __construct(string $type = null)
@ -44,4 +46,9 @@ class Relation implements \Ulmus\Annotation\Annotation {
return new $e(); return new $e();
} }
public function normalizeType() : string
{
return strtolower(str_replace(['-', '_', ' '], '', $this->type));
}
} }

View File

@ -6,13 +6,13 @@ use Ulmus\Query;
class Where implements \Ulmus\Annotation\Annotation { class Where implements \Ulmus\Annotation\Annotation {
public string $field; public /* stringable */ $field;
public $value; public $value;
public string $operator; 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 ) { if ( $field !== null ) {
$this->field = $field; $this->field = $field;

View File

@ -64,7 +64,7 @@ class EntityField
public static function isObjectType($type) : bool 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; # return strpos($type, "\\") !== false;
} }

View File

@ -5,5 +5,4 @@ namespace Ulmus\Entity\Field;
class Time extends Datetime { class Time extends Datetime {
public string $format = "H:i:s"; public string $format = "H:i:s";
} }

View File

@ -8,8 +8,8 @@ use Ulmus\Repository,
Ulmus\Common\EntityField; Ulmus\Common\EntityField;
use Ulmus\Annotation\Classes\{ Method, Table, Collation, }; use Ulmus\Annotation\Classes\{ Method, Table, Collation, };
use Ulmus\Annotation\Property\{ Field, Relation, OrderBy, Where, Join, Virtual }; use Ulmus\Annotation\Property\{ Field, Relation, OrderBy, Where, Join, Filter, On };
use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, }; use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, };
trait EntityTrait { trait EntityTrait {
use EventTrait; use EventTrait;
@ -40,50 +40,62 @@ trait EntityTrait {
# @TODO REFACTOR THIS CODE ASAP ! # @TODO REFACTOR THIS CODE ASAP !
if ( $this->isLoaded() ) { 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 = []; $vars = [];
$entity = $join->entity ?? $entityResolver->properties[$name]['type'];
foreach($this->entityDatasetUnmatchedFields as $key => $value) {
$len = strlen( $name ) + 1; $len = strlen( $name ) + 1;
foreach($this->entityDatasetUnmatchedFields as $key => $value) {
if ( substr($key, 0, $len ) === "{$name}$" ) { if ( substr($key, 0, $len ) === "{$name}$" ) {
$vars[substr($key, $len)] = $value; $vars[substr($key, $len)] = $value;
} }
} }
if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) { if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) {
return ( new $entity() )->fromArray($data); $entity = $annotation->entity ?? $entityResolver->properties[$name]['type'];
return $this->$name = ( new $entity() )->fromArray($data)->resetVirtualProperties();
} }
} }
if ( null !== ( $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ) ) ) { if ( null !== ( $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ) ) ) {
$relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type));
$order = $entityResolver->searchFieldAnnotationList($name, new OrderBy() ); $order = $entityResolver->searchFieldAnnotationList($name, new OrderBy() );
$where = $entityResolver->searchFieldAnnotationList($name, new Where() ); $where = $entityResolver->searchFieldAnnotationList($name, new Where() );
$filters = $entityResolver->searchFieldAnnotationList($name, new Filter() );
if ( $relation->entity ?? false ) { $baseEntity = $relation->entity ?? $relation->bridge ?? $entityResolver->properties[$name]['type'];
$baseEntity = $relation->entity();
$repository = $baseEntity->repository(); $repository = $baseEntity::repository()->open();
foreach ($where as $condition) { foreach ($where as $condition) {
$repository->where($condition->field, is_callable($condition->value) ? $f = call_user_func_array($condition->value, [ $this ]) : $condition->value, $condition->operator); $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) { foreach ($order as $item) {
$repository->orderBy($item->field, $item->order); $repository->orderBy($item->field, $item->order);
} }
$field = $relation->key; $field = $relation->key;
$applyFilter = function($repository) use ($filters, $name) {
foreach($filters as $filter) {
$repository = call_user_func_array([ $this, $filter->method ], [ $repository, $name ]);
} }
switch( $relationType ) { return $repository;
};
switch( $relation->normalizeType() ) {
case 'onetoone': case 'onetoone':
$repository->limit(1);
if ($relation->foreignKey) { 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); $this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository);
@ -91,18 +103,18 @@ trait EntityTrait {
$result = call_user_func([$repository, $relation->function]); $result = call_user_func([$repository, $relation->function]);
if ( count($result) === 0 ) { if ( count($result) === 0 ) {
return $baseEntity; return new $baseEntity();
} }
return $this->$name = $result[0]; return $this->$name = $result[0];
case 'onetomany': case 'onetomany':
if ($relation->foreignKey) { 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); $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': case 'manytomany':
if ( false === $relation->bridge ?? false ) { 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) ->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} ); ->where( static::field($bridgeRelation->foreignKey, $relationAlias), $this->{$bridgeRelation->foreignKey} );
$repository->open();
foreach($where as $condition) { foreach($where as $condition) {
$repository->where($condition->field, $condition->value, $condition->operator); $repository->where($condition->field, $condition->value, $condition->operator);
} }
$repository->close();
foreach($order as $item) { foreach($order as $item) {
$repository->orderBy($item->field, $item->order); $repository->orderBy($item->field, $item->order);
} }
$this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository); $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) { if ($relation->bridgeField ?? false) {
$repository = $relationRelation->entity::repository(); $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) ->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} ); ->where( static::field($bridgeRelation->foreignKey, $relationAlias), $this->{$bridgeRelation->foreignKey} );
$repository->open();
foreach($where as $condition) { foreach($where as $condition) {
$repository->where($condition->field, $condition->value, $condition->operator); $repository->where($condition->field, $condition->value, $condition->operator);
} }
$repository->close();
foreach($order as $item) { foreach($order as $item) {
$repository->orderBy($item->field, $item->order); $repository->orderBy($item->field, $item->order);
} }
@ -176,6 +196,9 @@ trait EntityTrait {
*/ */
public function __isset(string $name) : bool 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() ) ) { if ( $this->isLoaded() && static::resolveEntity()->searchFieldAnnotation($name, new Relation() ) ) {
return true; return true;
} }
@ -186,7 +209,7 @@ trait EntityTrait {
/** /**
* @Ignore * @Ignore
*/ */
public function entityFillFromDataset(iterable $dataset, bool $isLoadedDataset = false) : self public function entityFillFromDataset(iterable $dataset) : self
{ {
$loaded = $this->isLoaded(); $loaded = $this->isLoaded();
@ -227,6 +250,9 @@ trait EntityTrait {
$value = substr($value, 0, $annotation->length); $value = substr($value, 0, $annotation->length);
} }
} }
elseif ( $field['type'] === 'bool' ) {
$this->{$field['name']} = (bool) $value;
}
$this->{$field['name']} = $value; $this->{$field['name']} = $value;
} }
@ -298,6 +324,10 @@ trait EntityTrait {
$dataset[$realKey] = Ulmus::encodeArray($this->$key); $dataset[$realKey] = Ulmus::encodeArray($this->$key);
break; break;
case is_bool($this->$key):
$dataset[$realKey] = (int) $this->$key;
break;
default: default:
$dataset[$realKey] = $this->$key; $dataset[$realKey] = $this->$key;
} }
@ -307,6 +337,7 @@ trait EntityTrait {
} }
} }
# @TODO Must fix recursive bug !
if ($includeRelations) { if ($includeRelations) {
foreach($entityResolver->properties as $name => $field){ foreach($entityResolver->properties as $name => $field){
$relation = $entityResolver->searchFieldAnnotation($name, new Relation() ); $relation = $entityResolver->searchFieldAnnotation($name, new Relation() );

View File

@ -3,6 +3,7 @@
namespace Ulmus\Migration; namespace Ulmus\Migration;
use Ulmus\Annotation\Property\Field; use Ulmus\Annotation\Property\Field;
use Ulmus\Entity;
class FieldDefinition { class FieldDefinition {
@ -28,7 +29,6 @@ class FieldDefinition {
$this->builtIn = $data['builtin']; $this->builtIn = $data['builtin'];
$this->tags = $data['tags']; $this->tags = $data['tags'];
$field = $this->getFieldTag(); $field = $this->getFieldTag();
$this->type = $field->type ?? $data['type']; $this->type = $field->type ?? $data['type'];
$this->length = $field->length ?? null; $this->length = $field->length ?? null;
@ -45,8 +45,19 @@ class FieldDefinition {
public function getSqlType(bool $typeOnly = false) : string public function getSqlType(bool $typeOnly = false) : string
{ {
$type = $this->type; $type = $this->type;
$length = $this->length; $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) { switch($type) {
case "bool": case "bool":
$type = "TINYINT"; $type = "TINYINT";

View File

@ -37,6 +37,8 @@ class Join extends Fragment
public /* QueryBuilder */ $queryBuilder; public /* QueryBuilder */ $queryBuilder;
public int $order = 40;
public function __construct(QueryBuilder $queryBuilder) public function __construct(QueryBuilder $queryBuilder)
{ {
$this->queryBuilder = new QueryBuilder(); $this->queryBuilder = new QueryBuilder();
@ -47,18 +49,14 @@ class Join extends Fragment
{ {
$this->side = $side; $this->side = $side;
$this->table = $table; $this->table = $table;
$this->field = $field;
$this->value = $value; $this->where($this->field = $field, $this->value = $value);
} }
public function render() : string public function render() : string
{ {
if ($this->queryBuilder->where ?? false ) {
$where = $this->renderSegments([Where::CONDITION_AND, $this->queryBuilder->render(true)]);
}
return $this->renderSegments([ 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) ?? ""
]); ]);
} }
} }

View File

@ -191,7 +191,7 @@ class QueryBuilder
return $this; 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 # Empty IN case
if ( [] === $value ) { if ( [] === $value ) {

View File

@ -2,6 +2,7 @@
namespace Ulmus; namespace Ulmus;
use Ulmus\Annotation\Property\{ Where, Having, Relation, Join };
use Ulmus\Common\EntityResolver; use Ulmus\Common\EntityResolver;
class Repository class Repository
@ -379,27 +380,59 @@ class Repository
return $this->where($primaryKeyField[$pkField]->name ?? $pkField, $value); return $this->where($primaryKeyField[$pkField]->name ?? $pkField, $value);
} }
public function withJoin(/*string|array*/ $fields) : self public function withJoin(/*string|array*/ $fields) : self
{ {
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
$this->select("{$this->alias}.*"); $this->select("{$this->alias}.*");
} }
# Apply FILTER annotation to this too !
foreach((array) $fields as $item) { foreach((array) $fields as $item) {
if ( null !== $join = $this->entityResolver->searchFieldAnnotation($item, new Annotation\Property\Join) ) { $annotation = $this->entityResolver->searchFieldAnnotation($item, new Join) ?:
$alias = $join->alias ?? $item; $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) { foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
$this->select("$alias.$key as {$alias}\${$field['name']}"); $this->select("$alias.$key as {$alias}\${$field['name']}");
} }
$key = is_string($join->key) ? $this->entityClass::field($join->key) : $join->key; $this->open();
$foreignKey = is_string($join->foreignKey) ? $entity::field($join->foreignKey, $alias) : $join->foreignKey;
$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 { else {
throw new \Exception("You referenced field `$item` which do not exist or do not contain a valid @Join annotation."); 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; return $this;
} }
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self
{ {
$searchRequest->count = $searchRequest->filter( clone $this ) $searchRequest->count = $searchRequest->filter( clone $this )