This commit is contained in:
Dave Mc Nicoll 2021-11-24 16:36:13 +00:00
commit f14347f01d
9 changed files with 245 additions and 131 deletions

View File

@ -6,9 +6,10 @@ use Ulmus\Annotation\Annotation;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
use Ulmus\Ulmus, use Ulmus\Ulmus,
Ulmus\Adapter\AdapterInterface, Ulmus\Adapter\AdapterInterface,
Ulmus\Annotation\Property\Field; Ulmus\Annotation\Property\Field,
Ulmus\Query\WhereRawParameter;
class EntityField class EntityField implements WhereRawParameter
{ {
public string $name; public string $name;

View File

@ -83,16 +83,6 @@ class EntityResolver {
throw new \InvalidArgumentException("Given `fieldKey` is unknown to the EntityResolver"); throw new \InvalidArgumentException("Given `fieldKey` is unknown to the EntityResolver");
} }
if ($escape) {
if ( isset($tag['object']->name) ) {
$tag['object']->name = 2;
}
if ( isset($item['name']) ) {
$item['name'] = 2;
}
}
$fieldList[$key] = $item; $fieldList[$key] = $item;
break; break;
@ -135,7 +125,7 @@ class EntityResolver {
{ {
$list = []; $list = [];
$search = $caseSensitive ? $this->properties : array_change_key_case($this->properties, \CASE_LOWER); $search = $caseSensitive ? $this->properties : array_change_key_case($this->properties, \CASE_LOWER);
if ( null !== ( $search[$field] ?? null ) ) { if ( null !== ( $search[$field] ?? null ) ) {
foreach($search[$field]['tags'] ?? [] as $tag) { foreach($search[$field]['tags'] ?? [] as $tag) {

View File

@ -2,11 +2,13 @@
namespace Ulmus\Common; namespace Ulmus\Common;
use Ulmus\Query\WhereRawParameter;
abstract class Sql { abstract class Sql {
public static function function($name, ...$arguments) public static function function($name, ...$arguments)
{ {
return new class($name, ...$arguments) { return new class($name, ...$arguments) implements WhereRawParameter {
protected string $as = ""; protected string $as = "";
@ -20,19 +22,23 @@ abstract class Sql {
$this->parseArguments(); $this->parseArguments();
} }
public function __toString() { public function __toString() : string
{
return implode(' ', array_filter([ return implode(' ', array_filter([
"{$this->name}(" . implode(", ", $this->arguments) . ")", "{$this->name}(" . implode(", ", $this->arguments) . ")",
$this->as ? "AS {$this->as}" : false, $this->as ? "AS {$this->as}" : false,
])); ]));
} }
public function as($fieldName) { public function as($fieldName) : self
{
$this->as = $fieldName; $this->as = $fieldName;
return $this; return $this;
} }
protected function parseArguments() { protected function parseArguments() : void
{
foreach($this->arguments as &$item) { foreach($this->arguments as &$item) {
$item = Sql::escape($item); $item = Sql::escape($item);
} }
@ -42,7 +48,7 @@ abstract class Sql {
public static function identifier(string $identifier) : object public static function identifier(string $identifier) : object
{ {
return new class($identifier) { return new class($identifier) implements WhereRawParameter {
protected string $identifier; protected string $identifier;

View File

@ -87,6 +87,13 @@ class EntityCollection extends \ArrayObject {
return $removed; return $removed;
} }
public function clear() : self
{
$this->exchangeArray([]);
return $this;
}
public function search($value, string $field, bool $strict = true) : Generator public function search($value, string $field, bool $strict = true) : Generator
{ {
foreach($this->filters(fn($v) => isset($v->$field) ? ( $strict ? $v->$field === $value : $v->$field == $value ) : false) as $key => $item) { foreach($this->filters(fn($v) => isset($v->$field) ? ( $strict ? $v->$field === $value : $v->$field == $value ) : false) as $key => $item) {
@ -134,6 +141,11 @@ class EntityCollection extends \ArrayObject {
return $obj; return $obj;
} }
public function searchInstances(string $className) : self
{
return $this->filtersCollection(fn($obj) => is_a($obj, $className));
}
public function column($field, bool $unique = false) : array public function column($field, bool $unique = false) : array
{ {
$list = []; $list = [];
@ -146,8 +158,8 @@ class EntityCollection extends \ArrayObject {
$value = $item->$field; $value = $item->$field;
} }
if ($unique && in_array($value, $list)) { if ($unique && in_array($value, $list, true)) {
break; continue;
} }
$list[] = $value; $list[] = $value;
@ -279,6 +291,13 @@ class EntityCollection extends \ArrayObject {
} }
} }
public function push($value) : self
{
$this->append($value);
return $this;
}
public function mergeWith( /*array|EntityCollection*/ $datasets ) : self public function mergeWith( /*array|EntityCollection*/ $datasets ) : self
{ {
if ( is_object($datasets) ) { if ( is_object($datasets) ) {
@ -331,6 +350,11 @@ class EntityCollection extends \ArrayObject {
return $this; return $this;
} }
public function rsort(callable $callback, $function = "uasort") : self
{
return $this->sort(...func_get_args())->reverse();
}
public function pop() /* : mixed */ public function pop() /* : mixed */
{ {
$arr = $this->getArrayCopy(); $arr = $this->getArrayCopy();

View File

@ -30,36 +30,7 @@ trait EntityTrait {
*/ */
public array $entityLoadedDataset = []; public array $entityLoadedDataset = [];
/** /**entityLoadedDataset
* @Ignore
*/
public function __get(string $name)
{
$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));
}
/**
* @Ignore
*/
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 isset($this->$name);
}
/**
* @Ignore * @Ignore
*/ */
public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self
@ -71,9 +42,7 @@ trait EntityTrait {
foreach($dataset as $key => $value) { foreach($dataset as $key => $value) {
$field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null; $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null;
if ( $field === null ) { $field ??= $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
$field = $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
}
if ( $field === null ) { if ( $field === null ) {
if ($this->entityStrictFieldsDeclaration ) { if ($this->entityStrictFieldsDeclaration ) {
@ -104,17 +73,22 @@ trait EntityTrait {
} }
} }
elseif ( $field['type'] === 'bool' ) { elseif ( $field['type'] === 'bool' ) {
$this->{$field['name']} = (bool) $value; $value = (bool) $value;
} }
$this->{$field['name']} = $value; $this->{$field['name']} = $value;
} }
elseif ( ! $field['builtin'] ) { elseif ( ! $field['builtin'] ) {
$this->{$field['name']} = Ulmus::instanciateObject($field['type'], [ $value ]); try {
$this->{$field['name']} = Ulmus::instanciateObject($field['type'], [ $value ]);
}
catch(\Error $e) {
throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field['name']));
}
} }
# Keeping original data to diff on UPDATE query # Keeping original data to diff on UPDATE query
if ( ! $loaded ) { if ( ! $loaded /* || $isLoadedDataset */ ) {
#if ( $field !== null ) { #if ( $field !== null ) {
# $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() ); # $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() );
# $this->entityLoadedDataset[$annotation ? $annotation->name : $field['name']] = $dataset; # <--------- THIS TO FIX !!!!!! # $this->entityLoadedDataset[$annotation ? $annotation->name : $field['name']] = $dataset; # <--------- THIS TO FIX !!!!!!
@ -174,18 +148,18 @@ trait EntityTrait {
switch (true) { switch (true) {
case is_object($this->$key): case is_object($this->$key):
$dataset[$realKey] = Ulmus::convertObject($this->$key); $dataset[$realKey] = Ulmus::convertObject($this->$key);
break; break;
case is_array($this->$key): case is_array($this->$key):
$dataset[$realKey] = Ulmus::encodeArray($this->$key); $dataset[$realKey] = Ulmus::encodeArray($this->$key);
break; break;
case is_bool($this->$key): case is_bool($this->$key):
$dataset[$realKey] = (int) $this->$key; $dataset[$realKey] = (int) $this->$key;
break; break;
default: default:
$dataset[$realKey] = $this->$key; $dataset[$realKey] = $this->$key;
} }
} }
elseif ( $field['nullable'] ) { elseif ( $field['nullable'] ) {
@ -250,6 +224,35 @@ trait EntityTrait {
return isset($this->$key); return isset($this->$key);
} }
/**
* @Ignore
*/
public function __get(string $name)
{
$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));
}
/**
* @Ignore
*/
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 isset($this->$name);
}
/** /**
* @Ignore * @Ignore
*/ */
@ -309,6 +312,15 @@ trait EntityTrait {
return $collection; return $collection;
} }
/**
* @Ignore
*/
public static function queryBuilder() : QueryBuilder
{
return Ulmus::queryBuilder(static::class);
}
/** /**
* @Ignore * @Ignore
*/ */

View File

@ -2,8 +2,7 @@
namespace Ulmus\Query; namespace Ulmus\Query;
use Ulmus\Common\EntityField, use Ulmus\Common\Sql;
Ulmus\Common\Sql;
class Where extends Fragment { class Where extends Fragment {
const OPERATOR_LIKE = "LIKE"; const OPERATOR_LIKE = "LIKE";
@ -136,10 +135,11 @@ class Where extends Fragment {
{ {
if ( $value === null ) { if ( $value === null ) {
$this->operator = in_array($this->operator, [ '!=', '<>' ]) ? Where::COMPARISON_IS . " " . Where::CONDITION_NOT : Where::COMPARISON_IS; $this->operator = in_array($this->operator, [ '!=', '<>' ]) ? Where::COMPARISON_IS . " " . Where::CONDITION_NOT : Where::COMPARISON_IS;
return Where::COMPARISON_NULL; return Where::COMPARISON_NULL;
} }
elseif ( is_object($value) && ( $value instanceof EntityField ) ) { elseif (is_object($value) && ( $value instanceof WhereRawParameter ) ) {
return $value->name(); return (string) $value;
} }
else { else {
return $this->queryBuilder->addParameter($value); return $this->queryBuilder->addParameter($value);

View File

@ -0,0 +1,7 @@
<?php
namespace Ulmus\Query;
interface WhereRawParameter {}

View File

@ -2,7 +2,7 @@
namespace Ulmus; namespace Ulmus;
use Ulmus\Annotation\Property\{ Field, Filter, FilterJoin, Where, Having, Relation, Join, WithJoin, Relation\Ignore as RelationIgnore }; use Ulmus\Annotation\Property\{Field, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin, Relation\Ignore as RelationIgnore};
use Ulmus\Common\EntityResolver; use Ulmus\Common\EntityResolver;
class Repository class Repository
@ -183,23 +183,82 @@ class Repository
return false; return false;
} }
public function saveAll(EntityCollection $collection) : int
{
$changed = 0;
foreach ($collection as $entity) {
$this->save($entity) && $changed++;
}
return $changed;
}
public function loadCollectionRelation(EntityCollection $collection, /*array|string*/ $fields) : void
{
foreach ((array)$fields as $name) {
if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation()))) {
$relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type));
$order = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\OrderBy());
$where = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\Where());
$baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type'];
$baseEntityResolver = $baseEntity::resolveEntity();
$property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name'];
$entityProperty = ($this->entityResolver->field($relation->key, 01, false) ?: $this->entityResolver->field($relation->key, 02))['name'];
$repository = $baseEntity::repository();
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);
}
foreach ($order as $item) {
$repository->orderBy($item->field, $item->order);
}
$field = $relation->key;
$values = [];
$key = is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey);
foreach ($collection as $item) {
$values[] = is_callable($field) ? $field($item) : $item->$entityProperty;
}
$repository->where($key, $values);
switch ($relationType) {
case 'onetoone':
$results = call_user_func([$repository, "loadOne"]);
$item->$name = $results ?: new $baseEntity();
break;
case 'onetomany':
$results = call_user_func([$repository, $relation->function]);
foreach ($collection as $item) {
$item->$name = $baseEntity::entityCollection();
$item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty));
}
break;
}
}
}
}
public function replace(/*object|array*/ $entity, ? array $fieldsAndValue = null) : bool public function replace(/*object|array*/ $entity, ? array $fieldsAndValue = null) : bool
{ {
return $this->save($entity, $fieldsAndValue, true); return $this->save($entity, $fieldsAndValue, true);
} }
public function saveAll(EntityCollection $collection) : int public function replaceAll(/*EntityCollection|array*/ $collection) : void
{
$changed = 0;
foreach($collection as $entity) {
$this->save($entity) && $changed++;
}
return $changed;
}
public function replaceAll(EntityCollection $collection) : void
{ {
foreach($collection as $entity) { foreach($collection as $entity) {
$this->replace($entity); $this->replace($entity);
@ -262,6 +321,13 @@ class Repository
} }
} }
public function removeQueryFragment(/*? Query\Fragment|string*/ $fragment) : self
{
$fragment && $this->queryBuilder->removeFragment($fragment);
return $this;
}
public function selectEntity(string $entity, string $alias, string $prependField = "") : self public function selectEntity(string $entity, string $alias, string $prependField = "") : self
{ {
$prependField and ($prependField .= "$"); $prependField and ($prependField .= "$");
@ -479,6 +545,7 @@ class Repository
$this->select("{$this->alias}.*"); $this->select("{$this->alias}.*");
} }
# @TODO Apply FILTER annotation to this too !
foreach(array_filter((array) $fields) as $item) { foreach(array_filter((array) $fields) as $item) {
$annotation = $this->entityResolver->searchFieldAnnotation($item, new Join) ?: $annotation = $this->entityResolver->searchFieldAnnotation($item, new Join) ?:
$this->entityResolver->searchFieldAnnotation($item, new Relation); $this->entityResolver->searchFieldAnnotation($item, new Relation);
@ -611,6 +678,30 @@ class Repository
return $this; return $this;
} }
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self
{
if ($count) {
$searchRequest->count = $searchRequest->filter($this->serverRequestCountRepository())
->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)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
->orders($searchRequest->orders())
->groups($searchRequest->groups())
->offset($searchRequest->offset())
->limit($searchRequest->limit());
}
protected function serverRequestCountRepository() : Repository
{
return new Repository\ServerRequestCountRepository($this->entityClass, $this->alias, $this->adapter);
}
public function collectionFromQuery(? string $entityClass = null) : EntityCollection public function collectionFromQuery(? string $entityClass = null) : EntityCollection
{ {
$class = $entityClass ?: $this->entityClass; $class = $entityClass ?: $this->entityClass;
@ -831,29 +922,4 @@ class Repository
} }
protected function finalizeQuery() : void {} protected function finalizeQuery() : void {}
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self
{
if ($count) {
# @TODO Must be placed inside an event instead of directly there !
$searchRequest->count = $searchRequest->filter($this->serverRequestCountRepository())
->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)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
->orders($searchRequest->orders())
->groups($searchRequest->groups())
->offset($searchRequest->offset())
->limit($searchRequest->limit());
}
protected function serverRequestCountRepository() : Repository
{
return new Repository\ServerRequestCountRepository($this->entityClass, $this->alias, $this->adapter);
}
} }

View File

@ -118,7 +118,7 @@ class RelationBuilder
$this->repository->open(); $this->repository->open();
foreach($this->wheres as $condition) { foreach($this->wheres as $condition) {
$this->repository->where($condition->field, $condition->value, $condition->operator); $this->repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [ $this->entity ]) : $condition->value, $condition->operator);
} }
$this->repository->close(); $this->repository->close();
@ -155,7 +155,10 @@ class RelationBuilder
$entity = $annotation->entity ?? $this->resolver->properties[$name]['type']; $entity = $annotation->entity ?? $this->resolver->properties[$name]['type'];
} }
$name = strtolower($name);
foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) { foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) {
if ( $key === sprintf(static::SUBQUERY_FIELD_SUFFIX, strtolower($name)) ) { if ( $key === sprintf(static::SUBQUERY_FIELD_SUFFIX, strtolower($name)) ) {
if ($value) { if ($value) {
if ( null === ( $dataset = \json_decode($value, true) ) ) { if ( null === ( $dataset = \json_decode($value, true) ) ) {
@ -227,6 +230,11 @@ class RelationBuilder
})->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} ); })->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} );
$this->applyWhere();
$this->applyOrderBy();
if ($selectBridgeField && $relation->bridgeField) { if ($selectBridgeField && $relation->bridgeField) {
$this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias); $this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias);
} }