diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php index fd4b655..f383caa 100644 --- a/src/Adapter/AdapterInterface.php +++ b/src/Adapter/AdapterInterface.php @@ -28,4 +28,5 @@ interface AdapterInterface { public function repositoryClass() : string; public function queryBuilderClass() : string; public function tableSyntax() : array; + public function whitelistAttributes(array &$parameters) : void; } diff --git a/src/Adapter/DefaultAdapterTrait.php b/src/Adapter/DefaultAdapterTrait.php index a8fe13a..a46b25a 100644 --- a/src/Adapter/DefaultAdapterTrait.php +++ b/src/Adapter/DefaultAdapterTrait.php @@ -30,9 +30,12 @@ trait DefaultAdapterTrait return $this->database; } - public function schemaTable(ConnectionAdapter $parent, $databaseName, string $tableName) /* : ? object */ + public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object { - return Table::repository(Repository::DEFAULT_ALIAS, $parent)->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName); + return Table::repository(Repository::DEFAULT_ALIAS, $adapter) + ->select(\Ulmus\Common\Sql::raw('this.*')) + ->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName) + ->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName); } public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string @@ -92,4 +95,9 @@ trait DefaultAdapterTrait return $typeOnly ? $type : $type . ( isset($length) ? "($length" . ( ! empty($precision) ? ",$precision" : "" ) . ")" : "" ); } -} \ No newline at end of file + + public function whitelistAttributes(array &$parameters) : void + { + $parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES)); + } +} diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index 1d88805..bd6e130 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -14,6 +14,10 @@ use Ulmus\{Entity\InformationSchema\Table, Repository, QueryBuilder}; class MsSQL implements AdapterInterface { use DefaultAdapterTrait; + const ALLOWED_ATTRIBUTES = [ + 'default', 'primary_key', 'auto_increment', + ]; + const DSN_PREFIX = "sqlsrv"; public int $port; diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index ab31bf9..b5cc47a 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -14,6 +14,10 @@ use Ulmus\Migration\FieldDefinition; class MySQL implements AdapterInterface { use DefaultAdapterTrait; + const ALLOWED_ATTRIBUTES = [ + 'default', 'primary_key', 'auto_increment', 'update', + ]; + const DSN_PREFIX = "mysql"; public string $hostname; diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php index 743e2e0..d07ca0d 100644 --- a/src/Adapter/SQLite.php +++ b/src/Adapter/SQLite.php @@ -3,6 +3,7 @@ namespace Ulmus\Adapter; use Ulmus\Common\PdoObject; +use Ulmus\ConnectionAdapter; use Ulmus\Entity\Sqlite\Table; use Ulmus\Exception\AdapterConfigurationException; @@ -11,6 +12,9 @@ use Ulmus\{Repository, QueryBuilder, Ulmus}; class SQLite implements AdapterInterface { use DefaultAdapterTrait; + const ALLOWED_ATTRIBUTES = [ + 'default', 'primary_key', 'auto_increment' + ]; const DSN_PREFIX = "sqlite"; @@ -87,9 +91,11 @@ class SQLite implements AdapterInterface { return substr($base, 0, strrpos($base, '.') ?: strlen($base)); } - public function schemaTable(string $databaseName, string $tableName) : null|object + public function schemaTable(ConnectionAdapter $adapter, string $databaseName, string $tableName) : null|object { - return Table::repository()->loadOneFromField(Table::field('tableName'), $tableName); + return Table::repository(Repository::DEFAULT_ALIAS, $adapter) + ->select(\Ulmus\Common\Sql::raw('this.*')) + ->loadOneFromField(Table::field('tableName'), $tableName); } public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string @@ -197,4 +203,4 @@ class SQLite implements AdapterInterface { $pdo->sqliteCreateFunction('month', fn($date) => ( new \DateTime($date) )->format('m'), 1); $pdo->sqliteCreateFunction('year', fn($date) => ( new \DateTime($date) )->format('Y'), 1); } -} \ No newline at end of file +} diff --git a/src/Attribute/Property/Relation.php b/src/Attribute/Property/Relation.php index 9a8ddb6..58e6f0d 100644 --- a/src/Attribute/Property/Relation.php +++ b/src/Attribute/Property/Relation.php @@ -9,7 +9,7 @@ class Relation { public function __construct( public Relation\RelationTypeEnum|string $type, public \Stringable|string|array $key = "", - public null|\Closure $generateKey = null, + public null|\Closure|array $generateKey = null, public null|\Stringable|string|array $foreignKey = null, public null|\Stringable|string|array $foreignField = null, public array $foreignKeys = [], @@ -20,7 +20,7 @@ class Relation { public null|\Stringable|string|array $field = null, public null|string $entity = null, public null|string $join = null, - public string $function = "loadAll", + public null|string $function = null, ) { $this->key = Attribute::handleArrayField($this->key); $this->foreignKey = Attribute::handleArrayField($this->foreignKey); @@ -54,17 +54,17 @@ class Relation { public function isOneToOne() : bool { - return $this->type === Relation\RelationTypeEnum::oneToOne || $this->normalizeType() === 'onetoone'; + return $this->type instanceof Relation\RelationTypeEnum ? $this->type === Relation\RelationTypeEnum::oneToOne : $this->normalizeType() === 'onetoone'; } public function isOneToMany() : bool { - return $this->type === Relation\RelationTypeEnum::oneToMany || $this->normalizeType() === 'onetomany'; + return $this->type instanceof Relation\RelationTypeEnum ? $this->type === Relation\RelationTypeEnum::oneToMany : $this->normalizeType() === 'onetomany'; } public function isManyToMany() : bool { - return $this->type === Relation\RelationTypeEnum::manyToMany || $this->normalizeType() === 'manytomany'; + return $this->type instanceof Relation\RelationTypeEnum ? $this->type === Relation\RelationTypeEnum::manyToMany : $this->normalizeType() === 'manytomany'; } public function function() : string @@ -73,7 +73,7 @@ class Relation { return $this->function; } elseif ($this->isOneToOne()) { - return 'load'; + return 'loadOne'; } return 'loadAll'; diff --git a/src/Attribute/Property/Where.php b/src/Attribute/Property/Where.php index b073022..ce90e21 100644 --- a/src/Attribute/Property/Where.php +++ b/src/Attribute/Property/Where.php @@ -13,13 +13,23 @@ class Where { public string $operator = Query\Where::OPERATOR_EQUAL, public string $condition = Query\Where::CONDITION_AND, public string|\Stringable|array|null $fieldValue = null, + public null|array $generateValue = null, ) { $this->field = Attribute::handleArrayField($field); $this->fieldValue = Attribute::handleArrayField($fieldValue); } - public function getValue() : mixed + public function getValue(/* null|EntityInterface */ $entity = null) : mixed { + if ($this->generateValue) { + if ($entity) { + return call_user_func_array($this->generateValue, [ $entity ]); + } + else { + throw new \Exception(sprintf("Could not generate value from non-instanciated entity for field %s.", (string) $this->field)); + } + } + return $this->fieldValue ?? $this->value; } } diff --git a/src/Entity/Sqlite/Column.php b/src/Entity/Sqlite/Column.php index bb420b2..4537b3c 100644 --- a/src/Entity/Sqlite/Column.php +++ b/src/Entity/Sqlite/Column.php @@ -12,7 +12,7 @@ class Column { use \Ulmus\EntityTrait; - #[Id] + #[Field\Id] public int $cid; #[Field] diff --git a/src/Entity/Sqlite/Schema.php b/src/Entity/Sqlite/Schema.php index f15aae8..491a1c2 100644 --- a/src/Entity/Sqlite/Schema.php +++ b/src/Entity/Sqlite/Schema.php @@ -12,7 +12,7 @@ class Schema { use \Ulmus\EntityTrait; - #[Id] + #[Field\Id] public ? string $name; #[Field] @@ -27,6 +27,6 @@ class Schema #[Field] public ? string $sql; - #[Relation("oneToMany", key: "tableName", foreignKey: "tableName", entity: "Schema")] + #[Relation("oneToMany", key: "tableName", foreignKey: "tableName", entity: Column::class)] public EntityCollection $columns; } \ No newline at end of file diff --git a/src/EntityCollection.php b/src/EntityCollection.php index 100c275..0058f41 100644 --- a/src/EntityCollection.php +++ b/src/EntityCollection.php @@ -123,14 +123,14 @@ class EntityCollection extends \ArrayObject { return $this; } - public function search($value, string $field, bool $strict = true) : Generator + public function search(mixed $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) { yield $key => $item; } } - public function searchOne($value, string $field, bool $strict = true) : ? object + public function searchOne(mixed $value, string $field, bool $strict = true) : ? object { # Returning first value only foreach($this->search($value, $field, $strict) as $item) { @@ -140,7 +140,7 @@ class EntityCollection extends \ArrayObject { return null; } - public function searchAll(/* mixed*/ $values, string $field, bool $strict = true, bool $compareArray = false) : self + public function searchAll(mixed $values, string $field, bool $strict = true, bool $compareArray = false) : self { $collection = new static(); @@ -155,7 +155,7 @@ class EntityCollection extends \ArrayObject { return $collection; } - public function diffAll(/* mixed */ $values, string $field, bool $strict = true, bool $compareArray = false) : self + public function diffAll(mixed $values, string $field, bool $strict = true, bool $compareArray = false) : self { $obj = new static($this->getArrayCopy()); @@ -214,7 +214,7 @@ class EntityCollection extends \ArrayObject { return $list; } - public function unique(\Stringable|callable $field, bool $strict = false) : self + public function unique(\Stringable|callable|string $field, bool $strict = false) : self { $list = []; $obj = new static(); diff --git a/src/EntityTrait.php b/src/EntityTrait.php index 6fd97fd..ef1bf56 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -34,7 +34,6 @@ trait EntityTrait { $entityResolver = $this->resolveEntity(); foreach($dataset as $key => $value) { - $field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null; $field ??= $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false); @@ -51,7 +50,18 @@ trait EntityTrait { } elseif ( $field['type'] === 'array' ) { if ( is_string($value)) { - $this->{$field['name']} = substr($value, 0, 1) === "a" ? unserialize($value) : json_decode($value, true); + if (substr($value, 0, 1) === "a") { + $this->{$field['name']} = unserialize($value); + } + else { + $data = json_decode($value, true); + + if (json_last_error() !== \JSON_ERROR_NONE) { + throw new \Exception(sprintf("JSON error while decoding in EntityTrait : '%s' given %s", json_last_error_msg(), $value)); + } + + $this->{$field['name']} = $data; + } } elseif ( is_array($value) ) { $this->{$field['name']} = $value; diff --git a/src/Migration/FieldDefinition.php b/src/Migration/FieldDefinition.php index 8e1209b..449ae83 100644 --- a/src/Migration/FieldDefinition.php +++ b/src/Migration/FieldDefinition.php @@ -23,7 +23,7 @@ class FieldDefinition { public ? int $precision; - public ? int $length; + public null|int|string $length; public ? string $update; @@ -38,6 +38,8 @@ class FieldDefinition { $this->tags = $data['tags']; $field = $this->getFieldTag(); + $adapter->whitelistAttributes($field->attributes); + $this->type = $field->type ?? $data['type']; $this->length = $field->length ?? null; $this->precision = $field->precision ?? null; diff --git a/src/Repository.php b/src/Repository.php index 0ad31a5..1f19b63 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -39,7 +39,9 @@ class Repository $this->alias = $alias; $this->entityResolver = Ulmus::resolveEntity($entity); $this->adapter = $adapter ?? $this->entityResolver->databaseAdapter(); - $this->queryBuilder = Ulmus::queryBuilder($entity); + + $queryBuilder = $this->adapter->adapter()->queryBuilderClass(); + $this->queryBuilder = new $queryBuilder(); } public function __clone() @@ -49,7 +51,7 @@ class Repository public function loadOne() : ? object { - return $this->limit(1)->selectSqlQuery()->collectionFromQuery()[0] ?? null; + return $this->limit(1)->selectSqlQuery()->collectionFromQuery()[0]; } public function loadOneFromField($field, $value) : ? object @@ -606,7 +608,7 @@ class Repository $isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation); - if ($isRelation && ( $annotation->normalizeType() === 'manytomany' )) { + if ($isRelation && ( $annotation->isManyToMany() )) { throw new Exception("Many-to-many relation can not be preloaded within joins."); } @@ -618,10 +620,12 @@ class Repository foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ]) ) { $escAlias = $this->escapeIdentifier($alias); + $fieldName = $this->escapeIdentifier($key); $name = $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Field::class, Field::class ])->name ?? $field['name']; - $this->select("$escAlias.$key as $alias\${$name}"); + + $this->select("$escAlias.$fieldName as $alias\${$name}"); } } @@ -761,7 +765,8 @@ class Repository } foreach ($where as $condition) { - $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->getValue(), $condition->operator, $condition->condition); + # $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->getValue(), $condition->operator, $condition->condition); + $repository->where($condition->field, $condition->getValue($this), $condition->operator, $condition->condition); } foreach ($order as $item) { @@ -778,17 +783,21 @@ class Repository $values[] = is_callable($field) ? $field($item) : $item->$entityProperty; } + $values = array_unique($values); + $repository->where($key, $values); - $results = call_user_func([ $repository, $relation->function() ]); + $results = $repository->loadAll(); if ($relation->isOneToOne()) { - $item->$name = $results ?: new $baseEntity(); + foreach ($collection as $item) { + $item->$name = $results->searchOne($item->$entityProperty, $property) ?: new $baseEntity(); + } } elseif ($relation->isOneToMany()) { foreach ($collection as $item) { $item->$name = $baseEntity::entityCollection(); - $item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty)); + $item->$name->mergeWith($results->searchAll($item->$entityProperty, $property)); } } } diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php index cd1ef38..5202053 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -102,23 +102,21 @@ class RelationBuilder $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]; + return call_user_func([ $this->repository, $relation->function() ]) ?? $this->instanciateEmptyEntity($name, $relation); case $relation->isOneToMany(): $this->oneToMany($name, $relation); $this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository); - return call_user_func([ $this->repository, $relation->function ]); + 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 ]); + $results = call_user_func([ $this->repository, $relationRelation->function() ]); if ($relation->bridgeField ?? false) { $collection = $relation->bridge::entityCollection(); @@ -152,7 +150,7 @@ class RelationBuilder $this->repository->open(); foreach($this->wheres as $condition) { - $this->repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [ $this->entity ]) : $condition->getValue(), $condition->operator); + $this->repository->where($condition->field, $condition->getValue($this->entity), $condition->operator); } $this->repository->close(); @@ -201,7 +199,7 @@ class RelationBuilder $vars = []; $len = strlen( $name ) + 1; - $isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation); + $isRelation = ( $annotation instanceof Relation ) || ( $annotation instanceof Attribute\Property\Relation ); if ( $isRelation && $annotation->isManyToMany() ) { $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity; @@ -262,7 +260,13 @@ class RelationBuilder $field = $relation->key; if ($relation->foreignKey) { - $value = ! is_string($field) && is_callable($field) ? $field($this->entity) : $this->entity->$field; + if ( $relation->generateKey ) { + $value = call_user_func_array($relation->generateKey, [ $this->entity ]); + } + else { + $value = $this->entity->$field; + } + $this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), $value ); }