From e6d0279c1a0b475a3f9ef420cad37dbe84992f77 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Thu, 26 Jan 2023 13:25:59 +0000 Subject: [PATCH] - WIP on attributes ; on testing phase --- src/Annotation/Property/Join.php | 4 +- src/Attribute/Attribute.php | 19 +++++ src/Attribute/Obj/Collation.php | 8 ++ src/Attribute/Obj/Method.php | 8 ++ src/Attribute/Obj/Table.php | 14 ++++ src/Attribute/Property/Field.php | 17 ++++ src/Attribute/Property/Field/Bigint.php | 18 +++++ src/Attribute/Property/Field/Blob.php | 18 +++++ src/Attribute/Property/Field/CreatedAt.php | 17 ++++ src/Attribute/Property/Field/Date.php | 15 ++++ src/Attribute/Property/Field/Datetime.php | 15 ++++ src/Attribute/Property/Field/ForeignKey.php | 24 ++++++ src/Attribute/Property/Field/Id.php | 21 +++++ src/Attribute/Property/Field/Longblob.php | 18 +++++ src/Attribute/Property/Field/Longtext.php | 18 +++++ src/Attribute/Property/Field/Mediumblob.php | 18 +++++ src/Attribute/Property/Field/Mediumtext.php | 18 +++++ src/Attribute/Property/Field/PrimaryKey.php | 20 +++++ src/Attribute/Property/Field/Text.php | 18 +++++ src/Attribute/Property/Field/Time.php | 12 +++ src/Attribute/Property/Field/Tinyblob.php | 18 +++++ src/Attribute/Property/Field/Tinyint.php | 18 +++++ src/Attribute/Property/Field/UpdatedAt.php | 19 +++++ src/Attribute/Property/Filter.php | 11 +++ src/Attribute/Property/FilterJoin.php | 10 +++ src/Attribute/Property/GroupBy.php | 16 ++++ src/Attribute/Property/Having.php | 6 ++ src/Attribute/Property/Join.php | 19 +++++ src/Attribute/Property/On.php | 6 ++ src/Attribute/Property/OrWhere.php | 19 +++++ src/Attribute/Property/OrderBy.php | 16 ++++ src/Attribute/Property/Relation.php | 87 ++++++++++++++++++++ src/Attribute/Property/Relation/Ignore.php | 6 ++ src/Attribute/Property/Virtual.php | 13 +++ src/Attribute/Property/Where.php | 18 +++++ src/Attribute/Property/WithJoin.php | 10 +++ src/Common/EntityField.php | 3 +- src/Common/EntityResolver.php | 89 ++++++++++++++------- src/ConnectionAdapter.php | 2 +- src/EntityTrait.php | 60 ++------------ src/Migration/FieldDefinition.php | 7 +- src/Repository.php | 56 +++++++------ src/Repository/RelationBuilder.php | 68 ++++++++++------ src/Ulmus.php | 8 +- 44 files changed, 763 insertions(+), 142 deletions(-) create mode 100644 src/Attribute/Attribute.php create mode 100644 src/Attribute/Obj/Collation.php create mode 100644 src/Attribute/Obj/Method.php create mode 100644 src/Attribute/Obj/Table.php create mode 100644 src/Attribute/Property/Field.php create mode 100644 src/Attribute/Property/Field/Bigint.php create mode 100644 src/Attribute/Property/Field/Blob.php create mode 100644 src/Attribute/Property/Field/CreatedAt.php create mode 100644 src/Attribute/Property/Field/Date.php create mode 100644 src/Attribute/Property/Field/Datetime.php create mode 100644 src/Attribute/Property/Field/ForeignKey.php create mode 100644 src/Attribute/Property/Field/Id.php create mode 100644 src/Attribute/Property/Field/Longblob.php create mode 100644 src/Attribute/Property/Field/Longtext.php create mode 100644 src/Attribute/Property/Field/Mediumblob.php create mode 100644 src/Attribute/Property/Field/Mediumtext.php create mode 100644 src/Attribute/Property/Field/PrimaryKey.php create mode 100644 src/Attribute/Property/Field/Text.php create mode 100644 src/Attribute/Property/Field/Time.php create mode 100644 src/Attribute/Property/Field/Tinyblob.php create mode 100644 src/Attribute/Property/Field/Tinyint.php create mode 100644 src/Attribute/Property/Field/UpdatedAt.php create mode 100644 src/Attribute/Property/Filter.php create mode 100644 src/Attribute/Property/FilterJoin.php create mode 100644 src/Attribute/Property/GroupBy.php create mode 100644 src/Attribute/Property/Having.php create mode 100644 src/Attribute/Property/Join.php create mode 100644 src/Attribute/Property/On.php create mode 100644 src/Attribute/Property/OrWhere.php create mode 100644 src/Attribute/Property/OrderBy.php create mode 100644 src/Attribute/Property/Relation.php create mode 100644 src/Attribute/Property/Relation/Ignore.php create mode 100644 src/Attribute/Property/Virtual.php create mode 100644 src/Attribute/Property/Where.php create mode 100644 src/Attribute/Property/WithJoin.php diff --git a/src/Annotation/Property/Join.php b/src/Annotation/Property/Join.php index 185fcc7..0d63403 100644 --- a/src/Annotation/Property/Join.php +++ b/src/Annotation/Property/Join.php @@ -6,9 +6,9 @@ class Join implements \Notes\Annotation { public string $type; - public /*string|Stringable*/ $key; + public string|Stringable $key; - public /*string|Stringable*/ $foreignKey; + public string|Stringable $foreignKey; public string $entity; diff --git a/src/Attribute/Attribute.php b/src/Attribute/Attribute.php new file mode 100644 index 0000000..348792b --- /dev/null +++ b/src/Attribute/Attribute.php @@ -0,0 +1,19 @@ + "CURRENT_TIMESTAMP", + ], + public bool $nullable = false, + public mixed $default = null, + public bool $readonly = false, + ) {} +} diff --git a/src/Attribute/Property/Field/Date.php b/src/Attribute/Property/Field/Date.php new file mode 100644 index 0000000..aea7a75 --- /dev/null +++ b/src/Attribute/Property/Field/Date.php @@ -0,0 +1,15 @@ + false, + 'auto_increment' => false + ], + public bool $nullable = false, + public mixed $default = null, + public bool $readonly = false, + ) {} +} diff --git a/src/Attribute/Property/Field/Id.php b/src/Attribute/Property/Field/Id.php new file mode 100644 index 0000000..3374fdf --- /dev/null +++ b/src/Attribute/Property/Field/Id.php @@ -0,0 +1,21 @@ + true, + 'auto_increment' => true, + 'primary_key' => true, + ], + public bool $nullable = false, + public mixed $default = null, + public bool $readonly = false, + ) {} +} diff --git a/src/Attribute/Property/Field/Longblob.php b/src/Attribute/Property/Field/Longblob.php new file mode 100644 index 0000000..84d4bbb --- /dev/null +++ b/src/Attribute/Property/Field/Longblob.php @@ -0,0 +1,18 @@ + true, + ], + public bool $nullable = false, + public mixed $default = null, + public bool $readonly = false, + ) {} +} diff --git a/src/Attribute/Property/Field/Text.php b/src/Attribute/Property/Field/Text.php new file mode 100644 index 0000000..4adfcba --- /dev/null +++ b/src/Attribute/Property/Field/Text.php @@ -0,0 +1,18 @@ + "CURRENT_TIMESTAMP", + 'default' => null, + ], + public bool $nullable = true, + public mixed $default = null, + public bool $readonly = false, + ) {} +} diff --git a/src/Attribute/Property/Filter.php b/src/Attribute/Property/Filter.php new file mode 100644 index 0000000..816b6f8 --- /dev/null +++ b/src/Attribute/Property/Filter.php @@ -0,0 +1,11 @@ +fields = $field; + } + } +} diff --git a/src/Attribute/Property/Having.php b/src/Attribute/Property/Having.php new file mode 100644 index 0000000..fed0b8b --- /dev/null +++ b/src/Attribute/Property/Having.php @@ -0,0 +1,6 @@ +key = Attribute::handleArrayField($this->key); + $this->foreignKey = Attribute::handleArrayField($this->foreignKey); + } +} diff --git a/src/Attribute/Property/On.php b/src/Attribute/Property/On.php new file mode 100644 index 0000000..78e9cbc --- /dev/null +++ b/src/Attribute/Property/On.php @@ -0,0 +1,6 @@ +key = Attribute::handleArrayField($this->key); + } +} \ No newline at end of file diff --git a/src/Attribute/Property/OrderBy.php b/src/Attribute/Property/OrderBy.php new file mode 100644 index 0000000..03e0008 --- /dev/null +++ b/src/Attribute/Property/OrderBy.php @@ -0,0 +1,16 @@ +field = Attribute::handleArrayField($this->field); + } +} diff --git a/src/Attribute/Property/Relation.php b/src/Attribute/Property/Relation.php new file mode 100644 index 0000000..7497493 --- /dev/null +++ b/src/Attribute/Property/Relation.php @@ -0,0 +1,87 @@ +key = Attribute::handleArrayField($this->key); + $this->foreignKey = Attribute::handleArrayField($this->foreignKey); + $this->foreignField = Attribute::handleArrayField($this->foreignField); + $this->bridgeKey = Attribute::handleArrayField($this->bridgeKey); + $this->bridgeField = Attribute::handleArrayField($this->bridgeField); + $this->bridgeForeignKey = Attribute::handleArrayField($this->bridgeForeignKey); + $this->field = Attribute::handleArrayField($this->field); + + } + + public function entity() { + try { + $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(); + } + + public function normalizeType() : string + { + 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 function() : string + { + if ($this->function) { + return $this->function; + } + elseif ($this->isOneToOne()) { + return 'load'; + } + + return 'loadAll'; + } + + public function hasBridge() : bool + { + return (bool) $this->bridge; + } +} diff --git a/src/Attribute/Property/Relation/Ignore.php b/src/Attribute/Property/Relation/Ignore.php new file mode 100644 index 0000000..a9a13ad --- /dev/null +++ b/src/Attribute/Property/Relation/Ignore.php @@ -0,0 +1,6 @@ +field = Attribute::handleArrayField($field); + } +} diff --git a/src/Attribute/Property/WithJoin.php b/src/Attribute/Property/WithJoin.php new file mode 100644 index 0000000..7f5be96 --- /dev/null +++ b/src/Attribute/Property/WithJoin.php @@ -0,0 +1,10 @@ +entityResolver->searchFieldAnnotation($this->name, new Field() )->name ?? $this->name; + $name = $this->entityResolver->searchFieldAnnotation($this->name, [ Attribute\Property\Field::class, Field::class ] )->name ?? $this->name; $name = $this->entityResolver->databaseAdapter()->adapter()->escapeIdentifier($name, AdapterInterface::IDENTIFIER_FIELD); diff --git a/src/Common/EntityResolver.php b/src/Common/EntityResolver.php index 6b4c6f2..55cd2b5 100644 --- a/src/Common/EntityResolver.php +++ b/src/Common/EntityResolver.php @@ -2,11 +2,13 @@ namespace Ulmus\Common; +use Psr\SimpleCache\CacheInterface; use Ulmus\Ulmus, Ulmus\Annotation\Classes\Table, Ulmus\Annotation\Property\Field, Ulmus\Annotation\Property\Virtual, - Ulmus\Annotation\Property\Relation; + Ulmus\Annotation\Property\Relation, + Ulmus\Attribute; use Notes\Annotation; @@ -32,12 +34,12 @@ class EntityResolver { protected array $fieldList = []; - public function __construct(string $entityClass) + public function __construct(string $entityClass, ? CacheInterface $cache = null) { $this->entityClass = $entityClass; list($this->uses, $this->class, $this->methods, $this->properties) = array_values( - ObjectReflection::fromClass($entityClass)->read() + ObjectReflection::fromClass($entityClass, $cache)->read() ); $this->resolveAnnotations(); @@ -57,17 +59,31 @@ class EntityResolver { return null; } + public function searchField($name) : null|array + { + try{ + return $this->field($name, self::KEY_ENTITY_NAME, false) ?: $this->field($name, self::KEY_COLUMN_NAME, false); + } + catch(\Throwable $e) { + if ( $throwException) { + throw new \InvalidArgumentException("Can't find entity field's column named `$name` from entity {$this->entityClass}"); + } + } + + return null; + } + public function fieldList($fieldKey = self::KEY_ENTITY_NAME, bool $skipVirtual = false) : array { $fieldList = []; foreach($this->properties as $item) { foreach($item['tags'] ?? [] as $tag) { - if ( $tag['object'] instanceof Field ) { - if ( $skipVirtual && ($tag['object'] instanceof Virtual )) { + if ( $tag['object'] instanceof Field or $tag['object'] instanceof Attribute\Property\Field ) { + if ( $skipVirtual && ($tag['object'] instanceof Virtual or $tag['object'] instanceof Attribute\Property\Virtual )) { break; } - + switch($fieldKey) { case static::KEY_LC_ENTITY_NAME: $key = strtolower($item['name']); @@ -101,7 +117,7 @@ class EntityResolver { try{ if ( null !== ( $this->properties[$name] ?? null ) ) { foreach($this->properties[$name]['tags'] ?? [] as $tag) { - if ( $tag['object'] instanceof Relation ) { + if ( $tag['object'] instanceof Relation or $tag['object'] instanceof Attribute\Property\Relation ) { return $this->properties[$name]; } } @@ -118,22 +134,25 @@ class EntityResolver { return null; } - public function searchFieldAnnotation(string $field, Annotation $annotationType, bool $caseSensitive = true) : ? Annotation + public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object { - $found = $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive); - return $found ? $found[0] : null; + return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null; } - public function searchFieldAnnotationList(string $field, Annotation $annotationType, bool $caseSensitive = true) : array + public function searchFieldAnnotationList(string $field, array|object|string $annotationType, bool $caseSensitive = true) : array { $list = []; $search = $caseSensitive ? $this->properties : array_change_key_case($this->properties, \CASE_LOWER); + $annotations = is_array($annotationType) ? $annotationType : [ $annotationType ]; + if ( null !== ( $search[$field] ?? null ) ) { foreach($search[$field]['tags'] ?? [] as $tag) { - if ( $tag['object'] instanceof $annotationType ) { - $list[] = $tag['object']; + foreach($annotations as $annotation) { + if ( $tag['object'] instanceof $annotation ) { + $list[] = $tag['object']; + } } } } @@ -154,9 +173,9 @@ class EntityResolver { return $table->name ?? ""; } - public function tableAnnotation($required = false) : Table + public function tableAnnotation($required = false) : Table|Attribute\Obj\Table { - if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) { + if ( null === $table = $this->getTableAttribute() ) { if ($required) { throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation"); } @@ -172,7 +191,7 @@ class EntityResolver { public function sqlAdapter() : \Ulmus\ConnectionAdapter { - if ( null !== $table = $this->getAnnotationFromClassname( Table::class ) ) { + if ( null !== $table = $this->getTableAttribute() ) { if ( $table->adapter ?? null ) { if ( null === ( $adapter = \Ulmus\Ulmus::$registeredAdapters[$table->adapter] ?? null ) ) { throw new \Exception("Requested database adapter `{$table->adapter}` is not registered."); @@ -196,7 +215,7 @@ class EntityResolver { public function schemaName(bool $required = false) : ? string { - if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) { + if ( null === $table = $this->getTableAttribute() ) { throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation"); } @@ -210,7 +229,8 @@ class EntityResolver { public function getPrimaryKeyField() : ? array { foreach($this->fieldList() as $key => $value) { - if ( null !== ( $field = $this->searchFieldAnnotation($key, new Field() ) ) ) { + $field = $this->searchFieldAnnotation($key, [ Attribute\Property\Field::class, Field::class ]); + if ( null !== $field ) { if ( false !== ( $field->attributes['primary_key'] ?? false ) ) { return [ $key => $field ]; } @@ -230,13 +250,17 @@ class EntityResolver { return null; } + protected function getTableAttribute() + { + return $this->getAnnotationFromClassname(Attribute\Obj\Table::class, false) ?: $this->getAnnotationFromClassname( Table::class ); + } + /** * Transform an annotation into it's object's counterpart */ public function getAnnotationFromClassname(string $className, bool $throwError = true) : ? object { if ( $name = $this->uses[$className] ?? false ) { - foreach(array_reverse($this->class['tags']) as $item) { if ( $item['tag'] === $name ) { return $this->instanciateAnnotationObject($item); @@ -270,18 +294,23 @@ class EntityResolver { return null; } - public function instanciateAnnotationObject(array $tagDefinition) : Annotation + public function instanciateAnnotationObject(array|\ReflectionAttribute $tagDefinition) : object { - $arguments = $this->extractArguments($tagDefinition['arguments']); - - if ( false === $class = array_search($tagDefinition['tag'], $this->uses) ) { - throw new \InvalidArgumentException("Annotation class `{$tagDefinition['tag']}` was not found within {$this->entityClass} uses statement (or it's children / traits)"); + if ($tagDefinition instanceof \ReflectionAttribute) { + $obj = $tagDefinition->newInstance(); } + else { + $arguments = $this->extractArguments($tagDefinition['arguments']); - $obj = new $class(... $arguments['constructor']); + if (false === $class = array_search($tagDefinition['tag'], $this->uses)) { + throw new \InvalidArgumentException("Annotation class `{$tagDefinition['tag']}` was not found within {$this->entityClass} uses statement (or it's children / traits)"); + } - foreach($arguments['setter'] as $key => $value) { - $obj->$key = $value; + $obj = new $class(... $arguments['constructor']); + + foreach ($arguments['setter'] as $key => $value) { + $obj->$key = $value; + } } return $obj; @@ -309,18 +338,18 @@ class EntityResolver { protected function resolveAnnotations() { foreach($this->class['tags'] as &$tag) { - $tag['object'] = $this->instanciateAnnotationObject($tag); + $tag['object'] ??= $this->instanciateAnnotationObject($tag); } foreach($this->properties as &$property) { foreach($property['tags'] as &$tag){ - $tag['object'] = $this->instanciateAnnotationObject($tag); + $tag['object'] ??= $this->instanciateAnnotationObject($tag); } } foreach($this->methods as &$method) { foreach($method['tags'] as &$tag){ - $tag['object'] = $this->instanciateAnnotationObject($tag); + $tag['object'] ??= $this->instanciateAnnotationObject($tag); } } } diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php index 899305b..162bad3 100644 --- a/src/ConnectionAdapter.php +++ b/src/ConnectionAdapter.php @@ -64,7 +64,7 @@ class ConnectionAdapter public function pdo() : PdoObject { - return $this->pdo ?? $this->pdo = $this->connect()->pdo; + return $this->pdo ?? $this->connect()->pdo; } public function connector() : object diff --git a/src/EntityTrait.php b/src/EntityTrait.php index c062325..b7b1a46 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -66,7 +66,7 @@ trait EntityTrait { elseif ( EntityField::isScalarType($field['type']) ) { if ( $field['type'] === 'string' ) { - $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() ); + $annotation = $entityResolver->searchFieldAnnotation($field['name'], [ Attribute\Property\Field::class, Field::class ] ); if ( $annotation->length ?? null ) { $value = mb_substr($value, 0, $annotation->length); @@ -90,7 +90,6 @@ trait EntityTrait { } catch(\Error $e) { $f = $field['type']; - dump($f, $f::from($value)); throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field['name'])); } } @@ -126,17 +125,11 @@ trait EntityTrait { return $this; } - /** - * @Ignore - */ public function fromArray(iterable $dataset) : self { return $this->entityFillFromDataset($dataset); } - /** - * @Ignore - */ public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false) : array { if ( $returnSource ) { @@ -148,7 +141,7 @@ trait EntityTrait { $entityResolver = $this->resolveEntity(); foreach($entityResolver->fieldList(Common\EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) { - $annotation = $entityResolver->searchFieldAnnotation($key, new Field() ); + $annotation = $entityResolver->searchFieldAnnotation($key, [ Attribute\Property\Field::class, Field::class ]); if ( isset($this->$key) ) { $dataset[$annotation->name ?? $key] = static::repository()->adapter->adapter()->writableValue($this->$key); @@ -161,7 +154,7 @@ trait EntityTrait { # @TODO Must fix recursive bug ! if ($includeRelations) { foreach($entityResolver->properties as $name => $field){ - $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ); + $relation = $entityResolver->searchFieldAnnotation($key, [ Attribute\Property\Relation::class. Relation::class ] ); if ( $relation && isset($this->$name) && ($relation->entity ?? $relation->bridge) !== static::class ) { if ( null !== $value = $this->$name ?? null ) { @@ -185,9 +178,6 @@ trait EntityTrait { return $dataset; } - /** - * @Ignore - */ public function toArray($includeRelations = false, array $filterFields = null) : array { $dataset = $this->entityGetDataset($includeRelations); @@ -195,17 +185,11 @@ trait EntityTrait { return $filterFields ? array_intersect_key($dataset, array_flip($filterFields)) : $dataset; } - /** - * @Ignore - */ public function toCollection() : EntityCollection { return static::entityCollection([ $this ]); } - /** - * @Ignore - */ public function isLoaded() : bool { if (empty($this->entityLoadedDataset)) { @@ -221,9 +205,6 @@ trait EntityTrait { return isset($this->$key); } - /** - * @Ignore - */ public function __get(string $name) { $relation = new Repository\RelationBuilder($this); @@ -235,32 +216,25 @@ trait EntityTrait { 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() ) ) { + $rel = static::resolveEntity()->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Relation::class ]); + + if ( $this->isLoaded() && $rel ) { return true; } return isset($this->$name); } - /** - * @Ignore - */ public function __sleep() { return array_keys($this->resolveEntity()->fieldList()); } - /** - * @Ignore - */ public function __clone() { foreach($this as $prop) { @@ -274,33 +248,21 @@ trait EntityTrait { } } - /** - * @Ignore - */ public function jsonSerialize() { return $this->entityGetDataset(); } - /** - * @Ignore - */ public static function resolveEntity() : EntityResolver { return Ulmus::resolveEntity(static::class); } - /** - * @Ignore - */ public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository { return Ulmus::repository(static::class, $alias, $adapter); } - /** - * @Ignore - */ public static function entityCollection(...$arguments) : EntityCollection { $collection = new EntityCollection(...$arguments); @@ -309,26 +271,16 @@ trait EntityTrait { return $collection; } - /** - * @Ignore - */ public static function queryBuilder() : QueryBuilder { return Ulmus::queryBuilder(static::class); } - /** - * @Ignore - */ public static function field($name, ? string $alias = Repository::DEFAULT_ALIAS) : EntityField { - return new EntityField(static::class, $name, $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : Repository::DEFAULT_ALIAS, Ulmus::resolveEntity(static::class)); } - /** - * @Ignore - */ public static function fields(array $fields, ? string $alias = Repository::DEFAULT_ALIAS) : string { return implode(', ', array_map(function($item) use ($alias){ diff --git a/src/Migration/FieldDefinition.php b/src/Migration/FieldDefinition.php index 91f782b..8e1209b 100644 --- a/src/Migration/FieldDefinition.php +++ b/src/Migration/FieldDefinition.php @@ -4,6 +4,7 @@ namespace Ulmus\Migration; use Ulmus\Adapter\AdapterInterface; use Ulmus\Annotation\Property\Field; +use Ulmus\Attribute; use Ulmus\Entity; class FieldDefinition { @@ -40,7 +41,7 @@ class FieldDefinition { $this->type = $field->type ?? $data['type']; $this->length = $field->length ?? null; $this->precision = $field->precision ?? null; - $this->nullable = isset($field->nullable) ? $field->nullable : $data['nullable']; + $this->nullable = $data['nullable'] ?: $field->nullable; $this->update = $field->attributes['update'] ?? null; } @@ -69,9 +70,9 @@ class FieldDefinition { ])); } - public function getFieldTag() : ? Field + public function getFieldTag() : Field|Attribute\Property\Field|null { - $field = array_filter($this->tags, fn($item) => $item['object'] instanceof Field); + $field = array_filter($this->tags, fn($item) => $item['object'] instanceof Field || $item['object'] instanceof Attribute\Property\Field); return array_pop($field)['object']; } diff --git a/src/Repository.php b/src/Repository.php index 4a03ddb..ee5b6e2 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -2,7 +2,16 @@ namespace Ulmus; -use Ulmus\Annotation\Property\{Field, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin, Relation\Ignore as RelationIgnore}; +use Ulmus\Annotation\Property\{Field, + OrderBy, + Where, + Having, + Relation, + Filter, + Join, + FilterJoin, + WithJoin, + Relation\Ignore as RelationIgnore}; use Ulmus\Common\EntityResolver; class Repository @@ -332,7 +341,7 @@ class Repository $dataset = $this->generateDatasetDiff($entity, $oldValues); foreach($dataset as $field => $value) { - if ( false === ( $this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false ) ) { + if ( false === ( $this->entityResolver->searchFieldAnnotation($field, [ Attribute\Property\Field::class, Field::class ], false)->readonly ?? false ) ) { $intersect[$field] = $field; } } @@ -367,7 +376,7 @@ class Repository $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)) { + if (null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ])) { $this->select(sprintf("%s.$key as {$prependField}{$field['name']}", $this->escapeIdentifier($alias))); } } @@ -380,7 +389,7 @@ class Repository $fieldlist = []; foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { - if (null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore)) { + if (null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ])) { $fieldlist[] = $key; $fieldlist[] = $entity::field($field['name'], $this->escapeIdentifier($alias)); } @@ -574,7 +583,7 @@ class Repository 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) ) { $this->select("{$this->alias}.*"); @@ -582,10 +591,12 @@ class Repository # @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); + $annotation = $this->entityResolver->searchFieldAnnotation($item, [ Attribute\Property\Join::class, Join::class ]) ?: + $this->entityResolver->searchFieldAnnotation($item, [ Attribute\Property\Relation::class, Relation::class ]); - if (( $annotation instanceof Relation ) && ( $annotation->normalizeType() === 'manytomany' )) { + $isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation); + + if ($isRelation && ( $annotation->normalizeType() === 'manytomany' )) { throw new Exception("Many-to-many relation can not be preloaded within joins."); } @@ -595,10 +606,10 @@ class Repository $entity = $annotation->entity ?? $this->entityResolver->properties[$item]['type']; foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { - if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore) ) { + if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ]) ) { $escAlias = $this->escapeIdentifier($alias); - $name = $entity::resolveEntity()->searchFieldAnnotation($field['name'], new Field())->name ?? $field['name']; + $name = $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Field::class, Field::class ])->name ?? $field['name']; $this->select("$escAlias.$key as $alias\${$name}"); } @@ -606,17 +617,17 @@ class Repository $this->open(); - foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Where::class, Where::class ] ) 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) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Having::class, Having::class ] ) as $condition) { $this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); } - foreach($this->entityResolver->searchFieldAnnotationList($item, new Filter() ) as $filter) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Filter::class, Filter::class ] ) as $filter) { call_user_func_array([ $this->entityClass, $filter->method ], [ $this, $item, true ]); } @@ -627,8 +638,7 @@ class Repository $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) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Where::class, Where::class ]) as $condition) { if ( ! is_object($condition->field) ) { $field = $this->entityClass::field($condition->field); } @@ -644,7 +654,7 @@ class Repository } } - foreach($this->entityResolver->searchFieldAnnotationList($item, new FilterJoin() ) as $filter) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\FilterJoin::class, FilterJoin::class ]) as $filter) { call_user_func_array([ $this->entityClass, $filter->method ], [ $join, $item, true ]); } }); @@ -670,7 +680,7 @@ class Repository # Apply FILTER annotation to this too ! foreach(array_filter((array) $fields) as $item) { - if ( $relation = $this->entityResolver->searchFieldAnnotation($item, new Relation) ) { + if ( $relation = $this->entityResolver->searchFieldAnnotation($item, [ Attribute\Property\Relation::class, Relation::class ]) ) { $alias = $relation->alias ?? $item; if ( $relation->isManyToMany() ) { @@ -691,11 +701,11 @@ class Repository # $relation->isManyToMany() and $repository->selectJsonEntity($relation->bridge, $relation->bridgeField, true); - foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Where::class, Where::class ]) 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) { + foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Having::class, Having::class ] ) as $condition) { $repository->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator); } @@ -722,9 +732,9 @@ class Repository 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()))) { - $order = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\OrderBy()); - $where = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\Where()); + if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Relation::class ] ))) { + $order = $this->entityResolver->searchFieldAnnotationList($name, [ Attribute\Property\OrderBy::class, OrderBy::class ]); + $where = $this->entityResolver->searchFieldAnnotationList($name, [ Attribute\Property\Where::class, Where::class ]); $baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type']; $baseEntityResolver = $baseEntity::resolveEntity(); @@ -735,7 +745,7 @@ class Repository $repository = $baseEntity::repository(); foreach ($baseEntityResolver->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { - if (null === $baseEntityResolver->searchFieldAnnotation($field['name'], new RelationIgnore)) { + if (null === $baseEntityResolver->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ])) { $repository->select($baseEntityResolver->entityClass::field($key)); } } diff --git a/src/Repository/RelationBuilder.php b/src/Repository/RelationBuilder.php index f009ddb..5436d5a 100644 --- a/src/Repository/RelationBuilder.php +++ b/src/Repository/RelationBuilder.php @@ -2,7 +2,17 @@ namespace Ulmus\Repository; -use Ulmus\{ Ulmus, Annotation, Query, Common, Common\EntityResolver, Repository, Event, EntityCollection, }; +use Ulmus\{Attribute\Property\Field, + Ulmus, + Annotation, + Attribute, + Query, + Common, + Common\EntityResolver, + Repository, + Event, + EntityCollection}; + use Ulmus\Annotation\Property\{Filter, OrderBy, Relation, Relation\Ignore as RelationIgnore, Where, WithJoin, }; use Closure; @@ -15,7 +25,7 @@ class RelationBuilder protected Repository $repository; - protected /*object|string*/ $entity; + protected object|string $entity; protected EntityResolver $resolver; @@ -27,7 +37,7 @@ class RelationBuilder protected array $joins; - public function __construct(/*string|object*/ $entity, ? Repository $repository = null) + public function __construct(string|object $entity, ? Repository $repository = null) { $this->entity = $entity; $this->resolver = $entity::resolveEntity(); @@ -48,7 +58,7 @@ class RelationBuilder return $this->resolveRelation($name) ?: $this->resolveVirtual($name) ?: false; } else { - if ( $relation = $this->resolver->searchFieldAnnotation($name, new Relation() ) ) { + if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class , Relation::class ] ) ) { return $this->instanciateEmptyObject($name, $relation); } elseif ($virtual = $this->resolveVirtual($name)) { @@ -59,9 +69,9 @@ class RelationBuilder return false; } - protected function resolveVirtual(string $name) /* : bool|object|EntityCollection */ + protected function resolveVirtual(string $name) : bool|object { - if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, new Annotation\Property\Virtual()))) { + if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Virtual::class, Annotation\Property\Virtual::class ]))) { if ($virtual->closure ?? false) { return call_user_func_array($virtual->closure, [ $this->entity ]); } @@ -72,17 +82,18 @@ class RelationBuilder return false; } - protected function resolveRelation(string $name) /* : bool|object|EntityCollection */ + protected function resolveRelation(string $name) : bool|object { - 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() ); + if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Relation::class ] ) ) ) { + $this->orders = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\OrderBy::class, OrderBy::class ] ); + $this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Where::class, Where::class ] ); + $this->filters = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Filter::class, Filter::class ] ); + $this->joins = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\WithJoin::class, WithJoin::class ] ); switch( true ) { case $relation->isOneToOne(): if ( $relation->hasBridge() ) { + #dump($name, $relation); # @TODO ! dump($this->relationAnnotations($name, $relation)); } else { @@ -155,7 +166,7 @@ class RelationBuilder } } - protected function instanciateEmptyEntity(string $name, Relation $relation) : object + protected function instanciateEmptyEntity(string $name, Relation|Attribute\Property\Relation $relation) : object { $class = $relation->entity ?? $this->resolver->properties[$name]['type']; @@ -163,7 +174,7 @@ class RelationBuilder } - protected function instanciateEmptyObject(string $name, Relation $relation) : object + protected function instanciateEmptyObject(string $name, Relation|Attribute\Property\Relation $relation) : object { switch( true ) { case $relation->isOneToOne(): @@ -181,16 +192,18 @@ class RelationBuilder return new $class(); } - protected function fetchFromDataset($name, ? array $data = null) /* object|bool */ + 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); + $annotation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Join::class, Annotation\Property\Join::class ]) ?: + $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Annotation\Property\Relation::class ]); if ( $annotation ) { $vars = []; $len = strlen( $name ) + 1; - if ( ( $annotation instanceof Relation ) && $annotation->isManyToMany() ) { + $isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation); + + if ( $isRelation && $annotation->isManyToMany() ) { $entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity; } else { @@ -231,12 +244,12 @@ class RelationBuilder return false; } - public function oneToOne(string $name, Relation $relation) : Repository + public function oneToOne(string $name, Relation|Attribute\Property\Relation $relation) : Repository { return $this->oneToMany($name, $relation)->limit(1); } - public function oneToMany(string $name, Relation $relation) : Repository + public function oneToMany(string $name, Relation|Attribute\Property\Relation $relation) : Repository { $baseEntity = $relation->entity ?? $this->resolver->properties[$name]['type']; @@ -249,13 +262,17 @@ class RelationBuilder $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 ); + # dump($baseEntity::resolveEntity()->searchField($field)); + +# dump($this->resolver->properties[$name], $field); + $value = ! is_string($field) && is_callable($field) ? $field($this->entity) : $this->entity->$field; + $this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), $value ); } return $this->applyFilter($this->repository, $name); } - public function manyToMany(string $name, Relation $relation, ? Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository + public function manyToMany(string $name, Relation|Attribute\Property\Relation $relation, Relation|Attribute\Property\Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository { extract($this->relationAnnotations($name, $relation)); @@ -272,7 +289,6 @@ class RelationBuilder })->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} ); - $this->applyWhere(); $this->applyOrderBy(); @@ -284,7 +300,7 @@ class RelationBuilder return $this->applyFilter($this->repository, $name); } - public static function relationAnnotations(string $name, Relation $relation) : array + public static function relationAnnotations(string $name, Relation|Attribute\Property\Relation $relation) : array { if ( $relation->isOneToOne() || $relation->isManyToMany() ) { if ( ! $relation->hasBridge() ) { @@ -292,8 +308,8 @@ class RelationBuilder } $bridgeEntity = Ulmus::resolveEntity($relation->bridge); - $bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, new Relation() ); - $relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, new Relation() ); + $bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, [ Attribute\Property\Relation::class, Relation::class ]); + $relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, [ Attribute\Property\Relation::class, Relation::class ]); if ($relationRelation === null) { throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}"); diff --git a/src/Ulmus.php b/src/Ulmus.php index bb2c914..074e5b5 100644 --- a/src/Ulmus.php +++ b/src/Ulmus.php @@ -4,6 +4,8 @@ namespace Ulmus; use Generator; +use Psr\SimpleCache\CacheInterface; + abstract class Ulmus { public static string $repositoryClass = "\\Ulmus\\Repository"; @@ -13,8 +15,10 @@ abstract class Ulmus public static array $registeredAdapters = []; public static ConnectionAdapter $defaultAdapter; - + public static Entity\ObjectInstanciator $objectInstanciator; + + public static CacheInterface $cache; public static array $resolved = []; @@ -104,7 +108,7 @@ abstract class Ulmus public static function resolveEntity(string $entityClass) : Common\EntityResolver { - return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass); + return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass, static::$cache); } public static function repository(string $entityClass, string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository